Skip to content

fix(search): restore the API reference to search and stop docs-version renames from breaking it - #428

Open
eugenia-scandit wants to merge 7 commits into
mainfrom
fix/search-tag-ssot
Open

fix(search): restore the API reference to search and stop docs-version renames from breaking it#428
eugenia-scandit wants to merge 7 commits into
mainfrom
fix/search-tag-ssot

Conversation

@eugenia-scandit

Copy link
Copy Markdown
Collaborator

What was broken

Releasing 8.5.3 renamed the docs version, so docusaurus_tag on every page at
the site root changed from docs-default-current to docs-default-8.5.3
while not a single URL changed.

Two consequences, both silent:

  1. The API reference lives in the same index but is not a Docusaurus version, so
    it carried the tag docs-default-current. While that tag matched the guides'
    tag, it was found alongside them for free. After the rename ~3,200 pages left
    every result. A query with 106 matching pages in the index returned 86.
  2. buildVersionTagByMajor let current win its major unconditionally. With
    8.6.0-beta as current and 8.5.3 as lastVersion, typing "v8" sent readers
    at the unreleased beta's tag — and it still returned results, so nothing
    looked wrong.

The monitoring could only report "too few records".

What this changes

  • docusaurus.config.ts is the only place versions are declared and tags are
    built. DOCS_LAST_VERSION is declared once beside docsVersions (it used to
    be restated in the preset), and docVersionTag is the only constructor of
    docs-default-* strings.
  • buildVersionTagByMajor follows lastVersion and skips unreleased versions.
  • The API reference is mapped per version instead of bolted on: each docs
    version gets the API tree it belongs with — 7.6.14 → api-reference-7.6,
    6.28.11 → api-reference-6.28, and the version served at the root → the
    unversioned tree. A reader on 6.28.11 no longer gets 8.x API pages.
  • Fixes a regression introduced in this same PR: hoisting DOCS_LAST_VERSION
    left scripts/update-version.py reading the lastVersion: line, so the next
    release would have aborted with "Already in production state". Verified by
    running a real release end to end.

Why the mapping mirrors the site rather than assuming symmetry

The sitemap contains zero /data-capture-sdk/ URLs, so the crawler can only
reach that tree by following links. The 7.6.14 guides link to
/7.6/data-capture-sdk/…, but the guides served at the root link to the
unversioned /data-capture-sdk/… — nothing anywhere links to /8.5/.
Pointing the served version at api-reference-8.5 would have pointed at a tree
the crawler never reaches, dropping the current API reference out of search a
second time.

Preventing a repeat

  • yarn verify:search-tags (in CI) compares the build against the live index:
    the tag a typed major routes to must be the tag the site serves; the build must
    emit every tag the config names; and content under a tag search cannot reach is
    an error. Crawler lag warns instead of failing.
  • yarn test:search-facets (in CI) — 7 assertions on what the widget actually
    sends to Algolia, including that a legacy version gets its API reference and
    not the current one.

Both reproduce both bugs against the pre-fix config.

eugenia-scandit and others added 4 commits August 19, 2026 10:46
Releasing 8.5.3 renamed the docs version from `current` to `8.5.3`.
Docusaurus builds `docusaurus_tag` from the version NAME, so every page
at the site root flipped from `docs-default-current` to
`docs-default-8.5.3` without a single URL changing.

Two things broke, both silently:

1. The generated API reference under /data-capture-sdk/ is crawled into
   the same index but is not a Docusaurus version - it is tagged
   `docs-default-current`. While lastVersion was "current" it happened to
   share the guides' tag and rode along for free. After the rename it no
   longer matched the contextual filter, and ~3,200 pages left every
   search result. A representative query returned 86 hits where the index
   held 106 reachable ones.

2. buildVersionTagByMajor let `current` win its major unconditionally.
   With 8.6.0-beta as current and 8.5.3 as lastVersion, typing "v8"
   routed readers at the unreleased beta's tag - which still returned
   results, because that tag held the API reference, so nothing looked
   wrong.

Neither failed loudly. Search just got worse, and the monitoring could
only report "too few records".

Changes:
- docusaurus.config.ts is now the single source of truth for versions and
  every tag derived from them. DOCS_LAST_VERSION is declared once beside
  docsVersions instead of being restated in the preset, docVersionTag is
  the only place a `docs-default-*` string is built, and
  ALWAYS_ON_SEARCH_TAGS names the indexed content Docusaurus cannot tag
  for us.
- buildVersionTagByMajor now follows lastVersion and skips unreleased
  versions, so a typed major routes where the reader actually is.
- SearchBar widens the contextual OR group with the always-on tags, and
  the typed-version rewrite no longer clobbers them.
- A build plugin writes build/search-tags.json, and
  scripts/verify-search-tags.cjs (yarn verify:search-tags, wired into
  CI) checks it three ways: the routed tag for the served major must be
  the served tag, the build must actually emit every tag the config
  names, and no tag holding real content may sit outside what search can
  reach. Crawl lag warns; config drift fails.

The gate reproduces both bugs on the pre-fix config and passes on this
one. It also surfaces two orphan cohorts left by earlier renames -
docs-default-8.0.0 (10 pages) and docs-default-6.28.1 (18) - which want
purging from the index.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
My previous commit restored the API reference to search by OR-ing its tag
in on every page - including the frozen 7.6.14 and 6.28.11 trees, where it
had never appeared before. Measured on a 7.6.14 page, one query went from
53 hits to 73, and all 20 extras were current-SDK API pages that do not
apply to a reader on that version.

Before the 8.5.3 rename this scoping happened by accident: the API
reference shared the served version's tag, so it appeared there and
nowhere else. This makes it explicit - alwaysOnScopeTags names the
versions the extra tags apply to (the served version and the
in-development one), and the widget injects them only when the page's own
tag is in that set.

Also starts the crawler migration. The API reference should not be tagged
with a version name at all: `docs-default-current` is the 8.6.0 beta's own
tag, which is exactly why a release could rename the API reference out of
search. ALWAYS_ON_SEARCH_TAGS now lists `api-reference` alongside it, so
retagging the Algolia crawler's /data-capture-sdk/** action can land
before or after this commit without breaking search either way. The gate
reports an empty always-on tag as a pending migration, and only fails if
no always-on tag carries the content.

Adds scripts/test-search-facets.cjs (yarn test:search-facets, wired into
CI). It reads the two functions out of the shipped module rather than
copying them, and pins both failure modes: too narrow and content vanishes
from search, too wide and legacy readers get results that do not apply.
Neither throws in production, so only an assertion catches them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
I had this wrong twice, and the correction matters: the API reference IS
versioned. It is published per major.minor line at
/<major.minor>/data-capture-sdk/<framework>/ - /6.28/, /7.6/, /8.4/, /8.5/,
/8.6/ all return 200. My earlier probes used /6.28.11/, which 404s, and I
concluded from that there was only one API reference. There is one per line,
and each docs version has a matching one.

That makes the previous design wrong in both directions: OR-ing one API
reference into every version polluted legacy search with current-SDK pages,
and scoping it to the served version alone hid the 6.28 and 7.6 API
references from the readers they belong to.

So the API reference is now mapped per version rather than bolted on.
buildApiReferenceTags derives, from docsVersions alone, the tag each docs
version's API reference carries: docs-default-6.28.11 -> api-reference-6.28,
docs-default-7.6.14 -> api-reference-7.6, docs-default-8.5.3 ->
api-reference-8.5, docs-default-current -> api-reference-8.6. A reader on
6.28.11 gets the 6.28 API reference and never the 8.x one, and typing "v7"
moves the guides and the API reference together.

Nothing here needs maintaining per release. The repo reads the line out of
docsVersions; the crawler is expected to read it out of the URL
(/8.5/data-capture-sdk/... -> api-reference-8.5). Neither side hard-codes a
version, which is the whole point: tagging the API reference with a docs
version NAME is what broke search when 8.5.3 renamed it.

Also fixes a regression I introduced. Hoisting DOCS_LAST_VERSION left
scripts/update-version.py reading the `lastVersion:` line, whose last quoted
string is now "current" - so the next release would have aborted with
"Already in production state". All four read/write sites now target the
constant, which is the value both the docs plugin and the tag derivation
actually use.

Tests rewritten around the version mapping: a legacy version gets ITS API
reference and not the current one, an unknown version adds nothing rather
than guessing, and a typed version moves both together. 7 passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…links

Correcting my own previous commit. It mapped the served version to
`api-reference-8.5`, on the assumption that every docs version has a
matching /<major.minor>/data-capture-sdk/ tree the crawler can reach. It
does not, and the assumption would have dropped the current API reference
out of search a second time.

What the site really does, verified against production:

  - the sitemap contains ZERO /data-capture-sdk/ URLs, so the crawler can
    only find that tree by following links
  - the 7.6.14 guides link to /7.6/data-capture-sdk/...
  - the guides served at the root link to the UNVERSIONED
    /data-capture-sdk/... - nothing anywhere links to /8.5/

So /8.5/data-capture-sdk/ exists but is undiscoverable, and pointing the
served version at it would have pointed at an empty tag.

The mapping now mirrors the site's own linking instead of assuming a
symmetry that isn't there: the version served at the root and the
in-development one take the unversioned tree (`api-reference-latest`),
every frozen version takes its own line (`api-reference-7.6`,
`api-reference-6.28`). That is what the crawler will actually produce from
the URL, and it stays automatic - this file reads the version out of
docsVersions, the crawler out of the URL, neither hard-codes one.

Records are only added by this change, never removed: the unversioned tree
keeps its URLs and just changes tag. maxLostRecordsPercentage does not need
raising.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

🚀 View preview at
https://Scandit.github.io/data-capture-documentation/pr-preview/pr-428/

Built to branch gh-pages at 2026-08-25 13:43 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@moritzhartmeier moritzhartmeier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two blocking bugs. Everything else is smaller.

Verified by execution: ran both new scripts, queried the live Algolia index, simulated a release bump through the gate, dry-ran both update-version.py transitions on this branch, and typechecked (tsc clean). All 5 CI checks pass and the 7 facet tests pass — the two bugs below are ones the tests are shaped not to catch.

What holds up: the buildVersionTagByMajor priority fix is correct and is a genuine independent bug — confirmed major 8 now resolves to docs-default-8.5.3 rather than the unreleased beta. The update-version.py fix works: I ran 8.5.3 -> 8.5.4 and then the 8.6.0 promotion, and the constant, the docsVersions key and versions.json all land correctly, ending at DOCS_LAST_VERSION = "current". The per-version API mapping is also cycle-stable — it still derives correctly after promotion, when current becomes the served version again. And checking a build artifact against the live index rather than re-deriving config against itself is the right instinct.

Blocking

1. rewriteVersionTag ANDs the API-reference tag against the whole query, so typing "v7" returns zero guides. Reproduced; detail inline. Lines 141-142 of the same file warn against exactly this.

2. The gate hard-fails on every release PR — the change it exists to protect. Simulated 8.5.3 -> 8.5.4 against the live index: two blocking FAILs, neither gated by --strict. Detail inline.

One factual correction

Releasing 8.5.3 didn't rename the docs version. That happened at e92c1b16 Release 8.6.0-beta.1 (#423), which set lastVersion: "8.5.2" and made current the unreleased beta; 8.5.3 was a later patch bump. The wrong cause is stated in the docusaurus.config.ts SSOT block, in the gate's docstring, and in three commit messages, so it's worth correcting where it will be read later.

It also isn't a one-off: the root-served tag moves on the production->beta transition, on patches during the beta window, and again on beta->production. That's a cycle, which strengthens the case for something here — but it means the migration shim in particular needs to go rather than be carried forward (see inline).

Comment thread src/theme/SearchBar/index.js Outdated
Comment thread scripts/test-search-facets.cjs
Comment thread scripts/verify-search-tags.cjs Outdated
Comment thread docusaurus.config.ts Outdated
Comment thread scripts/verify-search-tags.cjs Outdated
Comment thread scripts/test-search-facets.cjs Outdated
Comment thread docusaurus.config.ts Outdated
Comment thread scripts/verify-search-tags.cjs Outdated
reachable.has(tag)
? "reachable "
: routable.has(tag)
? "typed-only "

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

api-reference-7.6 / api-reference-6.28 print as typed-only, but they're contextually reachable from those versions' pages — the comment just above says as much. Mislabel in the operator-facing report.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Partially fixed — the API-reference tags now read contextual, which is right. But the frozen docs version tags still print typed-only:

    2501  contextual   api-reference-7.6
     343  typed-only   docs-default-7.6.14
     264  typed-only   docs-default-6.28.11

A reader browsing the 7.6.14 guides reaches docs-default-7.6.14 from that page's own contextual filter with nothing typed — exactly the case the new contextual set was introduced for. It only covers values of apiReferenceTagsByVersionTag, not the version tags that key it.

Cheap fix: add the non-served keys of that map to contextual alongside their values. Report-only, so entirely up to you whether it is worth the line.

Comment thread scripts/verify-search-tags.cjs Outdated
Comment thread .github/workflows/build-docs.yml Outdated
…e shim

BLOCKING 1 - rewriteVersionTag ANDed the API tag against the whole query.

`swap` appended targetApi at every array level it was called on, including
depth 0, where facetFilters entries are ANDed. Typing "v7" produced three
top-level entries, the last being docusaurus_tag:api-reference-7.6, so only
7.6 API pages could match and every guide was filtered out - the exact
opposite of the intent, and what lines 141-142 already warn about.

Now appended only when recursing, and only into the group that already
carries the page's own docusaurus_tag. The second condition matters
independently: appending to the ["language:en"] group would OR the API tag
against the language filter and quietly widen it. Results deduped, which
also removes the doubled 7.6 tag the reviewer saw.

The test that should have caught this could not: tagsOf() only inspects the
nested OR group, so a stray top-level entry was invisible to every
assertion. Added `assert out.length === 2` plus an explicit "no API tag at
the top level" check - the same guard the sibling test at line 89 already
had. Verified in both directions: against the old implementation the suite
fails with `actual: 3, expected: 2`.

BLOCKING 2 - the gate hard-failed on every release PR.

Simulated 8.5.3 -> 8.5.4 exactly as update-version.py writes it. Both
failures were "the live index has not caught up with this build", which the
PR body says should warn: the new served tag holds nothing yet, and the
outgoing tag is unreachable because this build renamed it.

Added `releasePending` - detected, not declared: the served tag having no
records at all means the index predates this build. While it holds, both
classes warn instead of failing, with the reason printed. --strict still
fails both, so main and the schedule see them. Post-crawl they are real
problems again and fail as before. Also deduped `missing` (lastVersionTag is
in `routable` too, so it printed twice).

Release simulation now: WARN + exit 0 without --strict, exit 1 with. Normal
PR against the live index: exit 0.

SHIM DELETED - buildApiReferenceTags no longer pushes the legacy tag.

The premise is gone: the crawler retag is done. The live index holds
api-reference-latest with 4,376 pages and docs-default-current with one
stale record. The shim's only remaining effect was injecting that orphan
into every query on the served docs AND, by naming it always-on, making the
gate print it as reachable - hiding the drift the gate exists to surface. It
also degenerated after promotion, and would have pulled 616 unreleased-beta
pages into normal search if the crawler's /next/ exclusion ever changed.

The gate now reports it correctly: `1  UNREACHABLE  docs-default-current`.
Test MAP updated to match; it was modelling the shim.

FACTUAL CORRECTION - the cause was e92c1b1, not the 8.5.3 release.

Release 8.6.0-beta.1 set lastVersion: "8.5.2" and made `current` the
unreleased beta; that is what stopped the guides emitting
docs-default-current. 8.5.3 was a later patch bump. Corrected in the
config's SSOT block and the gate's docstring, and both now state that this
is a CYCLE - the root-served tag moves on production -> beta, on patches
during the beta window, and again on beta -> production - which is why the
gate has to stay green through the build that renames it. Commit messages
already merged cannot be corrected without a rewrite; the code comments are
where it will be read.

SMALLER FINDINGS

- tagsEmittedByBuild returns { found, exhausted }. Hitting the file cap now
  reports INCONCLUSIVE rather than blaming the config for a truncated walk.
- api-reference-7.6 / -6.28 print as `contextual`, not `typed-only`: they are
  reachable from those versions' pages with nothing typed.
- Transport errors exit 0 unless --strict, so an Algolia outage or a rate
  limit cannot block an unrelated docs PR.
- maxValuesPerFacet raised to Algolia's 1000, and pagesByTag fans out with a
  concurrency of 4 instead of one request per tag at once.
- effectiveLastVersion declared once beside DOCS_LAST_VERSION, replacing four
  copies of `isPreviewBuild ? "current" : DOCS_LAST_VERSION`.
- test-search-facets reads EMPTY_TAG_LIST and API_TAG_PREFIX out of the
  module instead of redeclaring them, so the eval'd functions no longer close
  over the test's copies.
- Workflow comments moved above the steps they describe.

Gates: test:search-facets (7), verify:search-tags, tsc --noEmit, full build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@moritzhartmeier moritzhartmeier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Re-verified 046d9720 by execution rather than by reading the commit message. Both blockers are genuinely fixed, and I have resolved 11 of the 12 threads.

rewriteVersionTag — my probe now returns two top-level entries with the duplicate gone. And the regression guard is real, not decorative: I ran the new test suite against the old SearchBar and it fails with actual: 3, expected: 2. The isTagOrGroup guard is a better fix than I suggested — it also blocks appending into a non-tag group, which I had not considered.

The gate — behaves exactly as claimed:

run result
current state OK, exit 0
8.5.3 -> 8.5.4 WARN x2, exit 0
8.5.4 with --strict FAIL x2, exit 1

missing prints once now, and a typo'd lastVersionTag is still caught by the separate build-emission check, so releasePending does not open the hole I was worried about.

Also confirmed: shim gone and the gate now prints 1 UNREACHABLE docs-default-current; the factual correction landed in both the config SSOT block and the gate docstring with the cycle explanation; tsc clean; 7 facet tests pass; all 5 CI checks pass.

One thing left, and I do think it should be fixed here

The --strict gap, inline. It is not a bug in what ships to readers, but it hollows out both of the fixes above: the release-PR fix and the transport-error fix are each justified by a strict run that does not exist anywhere. Fifteen lines of YAML and the contract the comments describe is actually true.

Left unresolved separately: the typed-only label, which is only partially fixed — report-only, your call.

Two figures for the record, since they moved: api-reference-latest is now 4,376 pages, not the 3,407 I measured on the 21st. The commit message has the current number; the code comment in test-search-facets.cjs still cites mine.

Comment on lines +42 to +44
# Not --strict on PRs: a crawl still catching up after a deploy, a release
# build that renames the served version, or an Algolia outage all warn
# rather than blocking unrelated work.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nothing in CI ever runs --strict, and there is no scheduled workflow — so both of the new downgrades are unconditional.

verify-search-tags.cjs justifies its two escape hatches on --strict existing somewhere, and refers to "the schedule" three times:

  • releasePending → "--strict fails either way so main and the schedule still see them"
  • transport errors → "Under --strict (main, or the schedule, where someone is watching) it still fails"

Neither exists. This workflow runs the same non-strict yarn verify:search-tags on pull_request and on push: main, and no workflow in .github/workflows/ has a schedule: trigger. grep -r -- --strict .github/ package.json matches only the comment above. So every condition the gate now downgrades is downgraded everywhere, permanently.

What that costs, using the incident this gate was built for: the release build WARNs (correctly). Push-to-main also runs pre-crawl, so it WARNs too. The unreachable API reference only becomes a FAIL on some later unrelated PR, once the crawl has caught up — and nobody reads WARN in a green log. The gate degrades from "catches it" to "might mention it eventually."

Adding --strict to the push-to-main run is not the fix — that run is also pre-crawl, so release merges would go red for exactly the reason releasePending exists. The fix is the daily --strict run the code already assumes:

on:
  schedule:
    - cron: "0 6 * * *"

with a job that builds and runs yarn verify:search-tags --strict. Post-crawl, once a day, someone watching — which is precisely the contract the comments describe.

Related, same theme: the two new escape hatches overlap. If the file cap is hit (INCONCLUSIVE, no failure) while releasePending is true (both index checks → WARN), a genuine config drift passes fully green with no signal at all. Low probability at ~2,250 pages against a 4,000 cap, but the holes line up, and --strict is the only thing that would catch it.

eugenia-scandit and others added 2 commits August 25, 2026 14:16
HIGH - releasePending tested the wrong thing.

It asked "is the served tag absent from the index", but on the beta ->
production build DOCS_LAST_VERSION becomes "current", so lastVersionTag
becomes docs-default-current - which the index already holds with ONE stale
record (a leftover /sdks/linux/barcode-capture/get-started/). releasePending
came out false, the outgoing docs-default-8.5.3 with its 559 pages tripped
`unreachable`, and the build failed with no path to green. That is one of
the three cycle events the header says the gate must survive.

Now tested as "holds no meaningful content" against THIN_PAGE_THRESHOLD.
Nothing is masked: a served tag genuinely below the threshold is what the
THIN check reports, and it reports it either way. Verified against the live
index: promotion warns and exits 0, with `WARN "docs-default-current" -> 1
pages` printed underneath.

MEDIUM

- --strict was never passed anywhere, so every "--strict still fails"
  promise in this script was empty: THIN, transport errors and Algolia-key
  drift could not fail any job. Now passed on refs/heads/main only.
- The served-major assertion silently no-opped whenever lastVersion is
  "current" - the entire production half of every cycle - because the major
  was parsed out of a tag that carries no number. The build now states
  lastVersionMajor in the manifest; the old derivation stays as a fallback
  and prints a NOTE. Verified: a manifest with lastVersionTag
  docs-default-current and deliberately wrong v8 routing now FAILs.
- A single empty API-reference tag only produced a NOTE, because
  alwaysOnEmpty required EVERY api tag to be empty and `missing` excluded
  them. If the crawler stops emitting api-reference-7.6, 2,501 pages leave
  7.6 search with CI green. Now any empty api tag fails, except while a
  release is pending. Verified with api-reference-8.0 in the manifest.
- assertConfigInSync drift and a missing manifest were swallowed as
  "transport" and exited 0, so the gate could degrade to a no-op while
  passing CI. Both now match FATAL_PATTERNS and always fail. Non-Error
  throws no longer print `undefined`.

LOW

- docs-default-current (the /next/ tree) and frozen docs-version tags now
  count as `contextual`, not UNREACHABLE: a reader on those pages reaches
  them through their own contextual filter, exactly like the API tags. It
  was tolerated only because the stale record count is 1 - at 50 crawled
  /next/ pages every PR would have hard-failed on a correct config.
- withApiReferenceTags gated on CONTENT rather than position. It latched on
  the first array it saw, which works only because
  useAlgoliaContextualFacetFilters returns the language filter as a bare
  string; if that ever gets wrapped, the API tags land in the language group
  and are silently dropped. No test caught it - all of them pass a string
  first.
- rewriteVersionTag's prefix filter now skips arrays. String() on a nested
  array joins its elements, so an OR group whose first element was an
  api-reference tag stringified to "docusaurus_tag:api-reference-…,…" and
  the whole group was dropped from the top-level AND - removing the
  docusaurus_tag filter entirely and returning every version's results.
- Remediation text pointed at `alwaysOnSearchTags`, which exists neither
  here nor in Docusaurus. It now names buildApiReferenceTags / docsVersions.
- Deleted the orphaned JSDoc block that described a constant removed with
  the migration shim and contradicted the very next block ("is NOT a
  Docusaurus docs version" vs "it IS versioned"). The history it carried is
  kept on buildApiReferenceTags, which is what survives the cycle.
- Corrected the preview-build comment: buildVersionTagByMajor reroutes only
  the major containing `current`, so majors 6 and 7 still resolve to frozen
  tags a preview has no pages for.

NOT fixed here, needs a decision: "See all results" goes to the stock
Docusaurus SearchPage, which refines docusaurus_tag on default + version
tags only, so the full-page results miss all ~4,376 API-reference pages and
its count disagrees with the modal's. Fixing it means either swizzling a
large third-party component or dropping searchPagePath. Both are product
calls, not something to slip into this PR.

Gates: test:search-facets (7), verify:search-tags across normal / patch
release / beta-promotion / --strict, tsc --noEmit, full build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
HIGH 1 - `unreachable` never consulted the `contextual` set I had just built.

A tag could print as `contextual` in the table and appear under "cannot
reach" in the same run. It bites at the next minor-production release:
docs-default-8.5.x loses major 8 to docs-default-current in
versionTagByMajor, drops out of `routable`, stays in `contextual`, and
hard-fails every PR once the crawler populates the new served tag and
releasePending stops masking it. Added `&& !contextual.has(tag)`.

HIGH 2 - the API tag was keyed off who is SERVED, not off what the pages
LINK to.

Counted in the sources, which settles it:

  docs/ (current)   2258 links to /data-capture-sdk,     0 versioned
  version-8.5.3     2258 links to /data-capture-sdk,     0 versioned
  version-7.6.14       0 unversioned, 2883 to /7.6/data-capture-sdk
  version-6.28.11      0 unversioned, 3430 to /6.28/data-capture-sdk

A snapshot keeps linking to the unversioned tree until the freeze process
rewrites it. `servedAtRoot` was right only by coincidence: the moment 8.5.x
stops being served it emits api-reference-8.5 - a tag nothing links to and
the index does not hold - so 8.5 readers get an OR-branch matching zero
records. Exactly the regression this file exists to prevent.

Now driven by UNVERSIONED_API_TREE_VERSIONS, with the counts recorded beside
it and the condition for removing an entry stated. It lives outside
docsVersions because that object goes to the docs plugin, which rejects
unknown keys - "versions.current.apiTree is not allowed" failed the build
when I first put it there.

MEDIUM

- --strict on main turned every post-release build red until the crawl
  finished, because it overrode the releasePending exemption. releasePending
  is a fact about the index, not a severity preference, so --strict no
  longer overrides it, and the THIN check is skipped while it holds - a thin
  served tag IS that state, not a separate problem. Verified: promotion
  exits 0 with and without --strict; an empty API tag still exits 1.
- A preview build contains only `current`, so expecting the frozen majors'
  tags produced "the config names tags this build never emits" against a
  correct config. The manifest now records isPreviewBuild and the check
  narrows accordingly.

LOW

- `--manifest` with no value left manifestArg undefined, silently falling
  back to the local build AND re-enabling the emission scan, so a CI
  invocation with an empty path variable checked a different source and
  printed OK. Now validated.
- assertConfigInSync matched the first appId/apiKey/indexName anywhere in the
  config, comments included. Scoped to the `algolia:` block, and a missing
  block is an error rather than a silent pass - this check is in
  FATAL_PATTERNS, so a false positive fails CI unconditionally.
- current.label is load-bearing (the only source of the served major once
  lastVersion is "current", and buildApiReferenceTags skips a version whose
  number it cannot resolve). Asserted non-empty at config load.
- Corrected the test's MAP comment, which claimed docs-default-current was
  absent while the line below added it as a key.
- extract() counts braces with no string/comment awareness. Documented, and
  the three extractions are now asserted to be functions, so a truncated
  read fails by name instead of as a confusing SyntaxError inside eval.

AND A DEFECT THE REVIEW SURFACED INDIRECTLY

Lifting the THIN check onto releasePending created a temporal-dead-zone read
- `thin` is computed before releasePending is declared - so the gate threw
`Cannot access 'releasePending' before initialization`. The throw was then
SWALLOWED as a transport failure and exited 0: the gate crashed and CI
stayed green. Block moved above its first use, and a ReferenceError /
TypeError / SyntaxError is now always fatal, because a defect in this script
is never an Algolia outage.

Scenario matrix, all against the live index:

  normal PR                          exit 0
  patch release            / --strict exit 0 / 0
  beta -> production       / --strict exit 0 / 0
  preview build                      exit 0
  frozen version out of routable     exit 0
  empty api-reference tag            exit 1
  --manifest with no value           exit 1

Gates: test:search-facets (7), tsc --noEmit, full build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

2 participants