Skip to content

feat(FR-3773): validate WEBUIHelpButton manual anchors in verify.sh - #9269

Open
yomybaby wants to merge 3 commits into
mainfrom
feat/FR-3773-help-anchor-check
Open

feat(FR-3773): validate WEBUIHelpButton manual anchors in verify.sh#9269
yomybaby wants to merge 3 commits into
mainfrom
feat/FR-3773-help-anchor-check

Conversation

@yomybaby

@yomybaby yomybaby commented Aug 30, 2026

Copy link
Copy Markdown
Member

Resolves #9262 (FR-3773)

Part of the bai-agent page→feature map plan (epic FR-3740, step A): make the page→manual link map a validated data source.

What changed

  • WEBUIHelpButton's two inline tables (URLMatchingTable, TabMatchingTable) move into react/src/helper/helpAnchors.json + a typed helpAnchors.ts (resolveHelpDocPath(menuKey, activeTab)); the button's behaviour is unchanged (helpAnchors.test.ts pins the lookup order).
  • New scripts/check-help-anchors.mjs resolves every docPage#anchor against the English manual sources using a line-for-line port of the docs toolkit's slug rules (slugify / slugFromNavPath, setext + ATX headings, explicit <a id> markers); parity is pinned by scripts/check-help-anchors.test.ts against real headings and the toolkit's own functions.
  • scripts/verify.sh gains a hard Help anchors (user manual) gate; docs-checks.yml runs the checker in CI and vitest-root.yml watches the data file.
  • The one dead target it found is fixed: admin_menu-manage-user39s-keypairsadmin_menu-manage-users-keypairs (the toolkit decodes &#39; before slugifying). Every other entry resolves.

Verification

  • node scripts/check-help-anchors.mjsChecked 59 help-anchor entries against 28 en manual pages. (exit 0); a deliberately broken entry exits 1 with the dead target listed.
  • root vitest 121 passed; react src/helper 581 passed.
  • bash scripts/verify.sh=== ALL PASS ===

🤖 Generated with Claude Code

https://claude.ai/code/session_018BG9zkxxCdqjzDgBgfhXtW

Follow-up (2026-08-31, review): scripts/check-help-anchors.test.ts now imports the toolkit source (slugify, slugFromNavPath, stripHtmlTags, decodeHtmlEntities) and compares the ports over every English manual heading (357, raw and marked-escaped) and navigation path (28); the earlier version only checked hard-coded outputs. pnpm exec vitest run scripts/check-help-anchors.test.ts 33/33.

The header's "?" button opened a hard-coded manual `page#anchor` picked from
two literal tables inside WEBUIHelpButton.tsx. Nothing tied those tables to
the manual, so renaming a heading in packages/backend.ai-webui-docs silently
turned the button into a no-op scroll — the page still loaded, the anchor just
resolved to nothing.

Data move. URLMatchingTable and TabMatchingTable become one flat, typed list
in react/src/helper/helpAnchors.json ({ path, tab?, docPage, anchor? }), read
by react/src/helper/helpAnchors.ts. The button now calls
`resolveHelpDocPath(menuKey, activeTab)`; lookup order is unchanged (a
tab-specific entry wins, then the page-level entry, then the index page), and
helpAnchors.test.ts pins that behaviour. JSON rather than a TS literal so the
checker can read the data with plain `node`, no transpile step.

The gate. scripts/check-help-anchors.mjs resolves every entry against the
ENGLISH manual sources: chapter slugs come from book.config.yaml's `navigation`
paths, in-page ids from each chapter's headings and its explicit `<a id="…">`
markers. It reports both dead anchors and docPage values that map to no source
file, and exits 1. verify.sh runs it as "Help anchors (user manual)" next to
the other hard gates.

backend.ai-docs-toolkit owns the slug rules but is TypeScript with no build
output in this workspace, so `slugify`, `slugFromNavPath`, `stripHtmlTags` and
`decodeHtmlEntities` are ported line-for-line from markdown-processor.ts /
markdown-extensions.ts. check-help-anchors.test.ts pins that parity against
real headings from the manual, plus a deliberately broken entry and an unknown
page.

Anchors fixed: one, the known dead one. `credential?tab=credentials` pointed at
`admin_menu.html#admin_menu-manage-user39s-keypairs`, a pre-FR-2825 id from
before the toolkit decoded `&#39;` ahead of slugifying; the live heading
"Manage user's keypairs" is `admin_menu-manage-users-keypairs`. No entry had to
be dropped — every other page and anchor still resolves.

CI teeth. verify.sh is the local/agent harness and no workflow runs it, so the
gate is also wired into the two workflows that fire on the inputs that can
break it: docs-checks.yml runs the checker on any manual PR, and
vitest-root.yml's path filter gains helpAnchors.json so check-help-anchors.test.ts
resolves the table whenever it is edited.

The `slugFromNavPath` port now really is line-for-line: it was missing the
toolkit's dash-collapse guard, so a nav path with `--` would have keyed the page
`foo--bar.html` while the site serves `foo-bar.html`. The empty-basename throw is
restored too, and the reserved `index` home slug is rejected in buildManualIndex
(where website-generator.ts checks it), not inside the slug function.
collectAnchorIds also indexes setext headings, which the web build gives ids —
missing them made the checker hard-fail a live anchor.

helpAnchors.ts keyed its tab lookup with a literal NUL in a template string,
which made the whole module a binary blob to git and grep. The two tables are a
nested Map now, so the file is text and reviewable.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018BG9zkxxCdqjzDgBgfhXtW
@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for root-coverage

Status Category Percentage Covered / Total
🔵 Lines 8.99% 65 / 723
🔵 Statements 9.25% 70 / 756
🔵 Functions 12.63% 12 / 95
🔵 Branches 9.11% 42 / 461
File CoverageNo changed files found.
Generated in workflow #76 for commit f7e8ae2 by the Vitest Coverage Report Action

Comment thread scripts/check-help-anchors.mjs Fixed
@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for react-coverage (./react)

Status Category Percentage Covered / Total
🔵 Lines 14.97% 5404 / 36097
🔵 Statements 12.46% 6532 / 52393
🔵 Functions 13.17% 848 / 6437
🔵 Branches 8.83% 4437 / 50243
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
react/src/components/WEBUIHelpButton.tsx 5.12% 0% 0% 10% 28-58
react/src/helper/helpAnchors.ts 100% 100% 100% 100%
Generated in workflow #396 for commit f7e8ae2 by the Vitest Coverage Report Action

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Extracts help-anchor mappings into validated shared data and adds CI safeguards against dead manual links.

Changes:

  • Adds typed help-anchor resolution backed by JSON.
  • Adds anchor validation and regression tests.
  • Integrates validation into local and CI checks.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
scripts/verify.sh Adds the help-anchor gate.
scripts/check-help-anchors.mjs Validates manual pages and anchors.
scripts/check-help-anchors.test.ts Tests checker behavior and slug rules.
react/src/helper/helpAnchors.json Stores route-to-manual mappings.
react/src/helper/helpAnchors.ts Provides typed mapping resolution.
react/src/helper/helpAnchors.test.ts Tests lookup precedence and data shape.
react/src/components/WEBUIHelpButton.tsx Uses the extracted resolver.
.github/workflows/vitest-root.yml Triggers tests for mapping changes.
.github/workflows/docs-checks.yml Validates links after documentation changes.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread scripts/check-help-anchors.test.ts
…tions

The parity tests only compared the checker's copied slug rules against
hard-coded outputs, and the "real headings" test built its index with
that same copy, so a change to the canonical toolkit logic left both
green. The new "parity with the docs toolkit source" suite imports
`slugify` / `slugFromNavPath` from
`packages/backend.ai-docs-toolkit/src/markdown-processor.ts` and
`stripHtmlTags` / `decodeHtmlEntities` from `markdown-extensions.ts`
directly (vitest transpiles the toolkit TypeScript that the plain-node
checker cannot load) and compares every port — and the composed
heading-id pipeline — against the toolkit over every ATX/setext heading
and every navigation path of the English manual, plus the marked-escaped
form of each heading so the entity-decoding branch runs on real input.
The corpus must be non-trivial (> 20 paths, > 100 headings, at least one
`&#39;` and one `&amp;`) so an empty corpus cannot pass silently. The
hard-coded cases stay; the file and checker header comments now describe
the suite accurately.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HCRVJAbdPPFsqDM4BXxis3
@yomybaby
yomybaby marked this pull request as ready for review August 31, 2026 03:07
@yomybaby
yomybaby requested a review from agatha197 as a code owner August 31, 2026 03:07
CodeQL (default setup) failed the required check on this PR with its one new
high-severity alert:

  check-help-anchors.mjs:47  js/incomplete-multi-character-sanitization,
                             "This string may still contain <script"

The query is syntactic about the strip itself: any single-pass
`.replace(/<[^>]+>/g, "")` is reported, regardless of what happens
downstream. Here nothing can survive anyway -- the next `.replace` drops
every character that is not a letter, digit, whitespace or dash, and the
output is used as a URL fragment id, never interpolated into HTML. So this
was never an injection path; the code shape is what has to change.

`stripTagSpans` restates that regex's match rule as an index scan: a match
can only start at `<`, `[^>]+` can never cross a `>`, so the span ends at
the first `>` after the `<` and is dropped only when that `>` is at least
two characters away (`[^>]+` cannot match empty). Removing the regex clears
the alert -- the query anchors on the StringReplaceCall -- and incidentally
removes a genuine quadratic: '<'.repeat(50000) went from 3649 ms to 0.01 ms.

This is the same helper, with the same equivalence argument, that
FR-3762 applies to the `slug.ts` port of the same function, so the two ports
stay recognisably identical. Equivalence was proven, not assumed: 0 output
differences over an exhaustive enumeration of all 797,161 strings up to
length 12 on {<,>,a}, all 120 manual files / 1,504 headings / 30,577 lines
in every language, and 400k fuzz inputs.

The toolkit original is untouched -- and check-help-anchors.test.ts, which
pins these ports against the toolkit's own functions over every heading and
navigation path of the manual, still passes 33/33.

Verified: 33/33 pinning tests, `node scripts/check-help-anchors.mjs` resolves
all 59 entries against 28 pages, prettier.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y3Ky5jYEW7Uk4ybDKnCxoe

@agatha197 agatha197 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

please resolve conflicts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bai-agent] Docs anchor validation — extract WEBUIHelpButton's page→manual tables to data and gate them in verify.sh

4 participants