Turning on jsx-a11y and react in #91 pointed tabstop's linting at tabstop's own frontend. One finding is still open.
A placeholder href="#" in the footer — jsx-a11y/anchor-is-valid
web/src/screens/modules/audit/pages/Home/landing.tsx:637
{/* The footer targets do not exist yet; the placeholder keeps these
rendering as links so the layout and focus order are final. */}
{FOOTER_LINKS.map((label) => (
// oxlint-disable-next-line jsx-a11y/anchor-is-valid
<Button key={label} variant="link" size="sm" render={<a href="#" />}>
href="#" is a link to nowhere. It takes focus, appears in a screen reader's link list, and moves the caret to the top of the document when activated — the failure mode of an inert control rather than a styling detail.
This is the same inert-control problem already noted informally against the landing page (theme toggle, "Try it free", "View full report", these footer links). Fixing it means deciding what those links are: real destinations, or removed until they have one. A <button> with no href would at least stop claiming to be navigation.
The linter no longer reports this. #93 added the inline disable above, so nothing fails until someone reads the source. The suppression's reasoning is defensible if the footer is deliberately provisional — but it means this issue is now the only record.
Resolved: array index in a React key — react/no-array-index-key
ViolationList/index.tsx:105 now carries the justification the acceptance asked for, so this half is closed:
{/* Audit nodes are render-once data that never reorders, and
two nodes can share the same selector and markup, so
content cannot make a unique key. */}
Triage: what did NOT survive
Recorded so the next person to run this does not re-investigate. Eight further findings were false positives from two causes:
Ariakit's render prop (5 findings). jsx-a11y/anchor-has-content and control-has-associated-label read render={<a href="#x" />} as an empty anchor. The element is a prop that Ariakit merges children into at runtime, so the rendered output has content and every link in the app trips both rules. Structural incompatibility, not a threshold — both are off in .oxlintrc.json with that reason recorded.
prefer-tag-over-role (2 findings). Suggests <output> in place of role="status" on AuditStatus and RouteAnnouncer. <output> means "the result of a calculation"; a route announcer is not one. Off, with the reason recorded.
Also worth recording
react/rules-of-hooks and react/exhaustive-deps are now enabled and find nothing. Neither is in oxlint's correctness category, so neither was running before. For a screen built on polling with useEffect, a clean exhaustive-deps is a real result rather than an absence of one.
Acceptance
Turning on
jsx-a11yandreactin #91 pointed tabstop's linting at tabstop's own frontend. One finding is still open.A placeholder
href="#"in the footer —jsx-a11y/anchor-is-validweb/src/screens/modules/audit/pages/Home/landing.tsx:637href="#"is a link to nowhere. It takes focus, appears in a screen reader's link list, and moves the caret to the top of the document when activated — the failure mode of an inert control rather than a styling detail.This is the same inert-control problem already noted informally against the landing page (theme toggle, "Try it free", "View full report", these footer links). Fixing it means deciding what those links are: real destinations, or removed until they have one. A
<button>with no href would at least stop claiming to be navigation.The linter no longer reports this. #93 added the inline disable above, so nothing fails until someone reads the source. The suppression's reasoning is defensible if the footer is deliberately provisional — but it means this issue is now the only record.
Resolved: array index in a React key —
react/no-array-index-keyViolationList/index.tsx:105now carries the justification the acceptance asked for, so this half is closed:Triage: what did NOT survive
Recorded so the next person to run this does not re-investigate. Eight further findings were false positives from two causes:
Ariakit's
renderprop (5 findings).jsx-a11y/anchor-has-contentandcontrol-has-associated-labelreadrender={<a href="#x" />}as an empty anchor. The element is a prop that Ariakit merges children into at runtime, so the rendered output has content and every link in the app trips both rules. Structural incompatibility, not a threshold — both are off in.oxlintrc.jsonwith that reason recorded.prefer-tag-over-role(2 findings). Suggests<output>in place ofrole="status"onAuditStatusandRouteAnnouncer.<output>means "the result of a calculation"; a route announcer is not one. Off, with the reason recorded.Also worth recording
react/rules-of-hooksandreact/exhaustive-depsare now enabled and find nothing. Neither is in oxlint'scorrectnesscategory, so neither was running before. For a screen built on polling withuseEffect, a cleanexhaustive-depsis a real result rather than an absence of one.Acceptance
oxlint-disable-next-line jsx-a11y/anchor-is-validinlanding.tsxis gone, not moved