Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f8c241d
chore(webapp): replace Biome's linter with oxlint
FelixTJDietrich Aug 21, 2026
85b7568
ci: bound the webapp unit-test leg with a timeout
FelixTJDietrich Aug 21, 2026
89656a7
refactor(webapp): fix the lint findings behind the suppressions
FelixTJDietrich Aug 21, 2026
83323a4
feat(webapp): enforce 168 more lint rules oxlint had switched off by …
FelixTJDietrich Aug 22, 2026
279ca51
fix(webapp): drop a lint rule that silently checks nothing
FelixTJDietrich Aug 22, 2026
d2cdecf
feat(webapp): enable type-aware linting and handle every floating pro…
FelixTJDietrich Aug 22, 2026
b2bf61b
fix(webapp): fix the correctness bugs type-aware linting surfaced
FelixTJDietrich Aug 22, 2026
284bb42
refactor(webapp): drop the type assertions the checker does not need
FelixTJDietrich Aug 22, 2026
d67eb0d
refactor(webapp): validate at runtime where the code was asserting
FelixTJDietrich Aug 22, 2026
d0c2372
refactor(webapp): fix every suppressed lint finding at its source
FelixTJDietrich Aug 22, 2026
9ab24d3
feat(webapp): close the gaps the type-aware pass left open
FelixTJDietrich Aug 22, 2026
140d3c5
feat(webapp): enforce the any boundary, deprecations and layering
FelixTJDietrich Aug 22, 2026
e78c8ba
feat(webapp): index safety, dead conditions and named option lists
FelixTJDietrich Aug 22, 2026
39c53f0
Merge remote-tracking branch 'origin/main' into issue-1463-replace-bi…
FelixTJDietrich Aug 22, 2026
baa2684
refactor(webapp): replace what the framework already provides, and te…
FelixTJDietrich Aug 22, 2026
00fc238
docs(webapp): cut the commentary back to what a reader cannot infer
FelixTJDietrich Aug 22, 2026
279ab28
Merge remote-tracking branch 'origin/main' into issue-1463-replace-bi…
FelixTJDietrich Aug 22, 2026
a896604
fix(server): report an unearned achievement as having no unlock time
FelixTJDietrich Aug 22, 2026
9f9bb9e
Merge remote-tracking branch 'origin/main' into issue-1463-replace-bi…
FelixTJDietrich Aug 22, 2026
4e255af
feat(webapp): enforce the story and query-key conventions in the linter
FelixTJDietrich Aug 22, 2026
d3c0d4b
refactor(webapp): close the gaps an adversarial audit found in the li…
FelixTJDietrich Aug 23, 2026
5ce2304
refactor(config): require the path alias instead of parent-relative i…
FelixTJDietrich Aug 23, 2026
3eae248
feat(webapp): enforce the conventions that were only ever review notes
FelixTJDietrich Aug 23, 2026
16ecaac
Merge remote-tracking branch 'origin/main' into issue-1463-replace-bi…
FelixTJDietrich Aug 23, 2026
9b4f3e7
Merge remote-tracking branch 'origin/main' into issue-1463-replace-bi…
FelixTJDietrich Aug 23, 2026
63efa4c
docs: cut the comments that restate what the linter already says
FelixTJDietrich Aug 23, 2026
25f8062
fix(webapp): type-check the whole .storybook directory, not just preview
FelixTJDietrich Aug 23, 2026
4c63aef
fix: arm the three gates that reported success without checking anything
FelixTJDietrich Aug 23, 2026
b00c877
fix(server): give PMD the classpath it needs to resolve types
FelixTJDietrich Aug 23, 2026
ba8414d
Merge remote-tracking branch 'origin/main' into issue-1463-replace-bi…
FelixTJDietrich Aug 23, 2026
feb9ade
docs(release): say the fixes came from the whole codebase, not the we…
FelixTJDietrich Aug 23, 2026
4f82368
docs: cut the comments that restate the code, and correct the ones th…
FelixTJDietrich Aug 24, 2026
67a3740
feat(webapp): keep Storybook titles derived from the file tree
FelixTJDietrich Aug 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/oxlint-replaces-biomes-linter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
---

Development tooling only: oxlint replaces Biome's linter over `webapp/`, and Biome keeps the
formatter and import sort. No endpoint, configuration or screen changes for operators or users. Two
shipped files were adjusted to satisfy rules Biome had no equivalent for — the mentor chat hook now
defers two `Date.now()` calls until TanStack Query needs them, and the sync-events stream detaches
its listeners through an `AbortController` — both behaviour-preserving and covered by tests.
2 changes: 1 addition & 1 deletion .claude/skills/fix-ci/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Before fixing anything, categorize every failure into this table.
| Priority | Category | Symptoms | Fix Command |
|----------|----------|----------|-------------|
| 1 | Formatting | "Formatting failed", biome/prettier diff | `pnpm run format` |
| 2 | Lint | Biome lint errors | `pnpm run check:fix` |
| 2 | Lint | oxlint errors | `pnpm run check:webapp:fix` |
| 3 | TypeScript | TS2xxx errors, type mismatch | Fix the type error in source |
| 4 | Build failure | Compilation errors, missing exports | Fix imports/exports, verify with `pnpm run build:webapp` |
| 5 | Webapp tests | "FAIL" in webapp test output | Fix test or source, verify with `pnpm run test:webapp` |
Expand Down
12 changes: 6 additions & 6 deletions .claude/skills/storybook-components/RUBRIC.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ is a **pass condition, not an achievement**.
## Dimension 4 — Do the Controls drive the real component?

- **D** — `meta` names a `component` but is typed bare `Meta`. (Already a build failure:
`webapp/.biome/typed-story-meta.grit`.)
`hephaestus/typed-story-meta`.)
- **C** — `render: () => <Thing fixed={…} />` — the args object is ignored, so the Controls panel edits
nothing and `autodocs` publishes an API nobody can exercise.
- **B** — Every story is `args`-driven, or `render: (args) => <Harness {...args} />`; at least one story
Expand Down Expand Up @@ -113,8 +113,8 @@ gap. They are vendored shadcn and editing them is forbidden. Do not open that as

## Dimension 6 — Can the play function fail?

- **D** — `expect(canvas.getByRole(…)).toBeInTheDocument()` or a bare `await expect(getBy(…))`. (Already
a build failure: `webapp/.biome/no-redundant-in-the-document.grit`.)
- **D** — `expect(canvas.getByRole(…)).toBeInTheDocument()` (`hephaestus/no-redundant-in-the-document`)
or a bare `await expect(getBy(…))` (`vitest/valid-expect`). Both are already build failures.
- **C** — The play renders and asserts presence of text the story itself supplied — true whatever the
component does with it.
- **B** — The assertion names a value the component **derived**: a label the registry produced, a count it
Expand Down Expand Up @@ -183,10 +183,10 @@ have one.

- **D** — A new rule added to prose that contradicts existing code, with no migration and no gate.
- **C** — Rule in prose, no gate.
- **B** — Rule in prose plus a mechanical check — a GritQL plugin in `webapp/.biome/`, or a node gate in
`scripts/` wired into `pnpm run check`.
- **B** — Rule in prose plus a mechanical check — an oxlint rule in `webapp/tools/oxlint/rules/`, or a
node gate in `scripts/` wired into `pnpm run check`.
- **A** — B, and the gate's own comment explains the two neighbouring shapes it deliberately does *not*
match, so nobody widens it into a nuisance (both existing plugins do this).
match, so nobody widens it into a nuisance (all three house rules do this).
- **A+** — The gate is shrink-only: an allowlist entry that scans clean fails the build, so it cannot go
stale (`scripts/check-presentational-components.mjs`).

Expand Down
10 changes: 6 additions & 4 deletions .claude/skills/storybook-components/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ records what this repo decided, and what it has already been burned by.
These fail `pnpm run check`. Treat a violation as a build error, not a style opinion, and do not
write a guideline that repeats one.

- `webapp/.biome/typed-story-meta.grit` — a `meta` naming a `component` must be
`satisfies Meta<typeof X>`; a gallery meta naming no component may be bare `Meta`.
- `webapp/.biome/no-redundant-in-the-document.grit` — `expect(getBy…).toBeInTheDocument()` and
bare `await expect(getBy…)`.
- `hephaestus/typed-story-meta` (`webapp/tools/oxlint/rules/typed-story-meta.ts`) — a `meta` naming a
`component` must be `satisfies Meta<typeof X>`; a gallery meta naming no component may be bare `Meta`.
- `hephaestus/no-redundant-in-the-document` — `expect(getBy…).toBeInTheDocument()`. A bare
`await expect(getBy…)` is `vitest/valid-expect`, which catches it for every subject.
- `hephaestus/no-within-canvas-element` — `within(canvasElement)` when the play function was handed
`canvas`.
- `scripts/check-story-prose.mjs` (`check:stories`) — `<p>` in a comment Storybook publishes.
- `scripts/check-presentational-components.mjs` (`check:components`) — a component importing the
query layer, and a story installing MSW handlers. Its allowlist is shrink-only.
Expand Down
27 changes: 18 additions & 9 deletions .claude/skills/storybook-components/traps.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,23 @@ matches, and forcing every duration to 1ms does not fix it. Use `expectSettledVi
animation routinely. Any settle helper must `.catch()` the rejection and treat it as an outcome
(`webapp/src/test/overlay.ts:60-66`). Without the catch the helper throws on the ordinary path.

## 3. A GritQL capture group degrades a Biome plugin to a no-op that exits 0
## 3. An `exhaustive-deps` suppression silently disables `set-state-in-effect` too

A capturing group in a `r"…"` pattern binds a Grit variable the pattern never declared; the plugin then
reports `info` instead of `error` and fails nothing, while `biome check` still exits 0. Use
non-capturing `(?: … )`. This is documented in place at
`webapp/.biome/no-redundant-in-the-document.grit:31-32` — read that comment before editing either plugin.
oxlint runs the React hook rules as one fused pass, so `// oxlint-disable-next-line
react-hooks/exhaustive-deps` above a dependency array turns off **every** hook rule for that
`useEffect` — including `react/set-state-in-effect`, which reports on the `setState` line *above* the
directive. `options.reportUnusedDisableDirectives` does not save you: it calls the directive unused
while it is actively suppressing an error. One real finding in `TimeframeFilter.tsx` hid this way
until the directive was removed. Prefer letting a hook finding land in `oxlint-suppressions.json`,
where it is counted and reviewable, over a directive that takes its neighbours with it.

## 4. One story's MSW handlers answer for the whole Docs page
## 4. `oxlint-suppressions.json` counts findings per file, not per line

Fix one violation and introduce another of the same rule in the same file and the count is unchanged,
so CI stays green and the diff is empty. The baseline gates *new files and new rules*, not churn
inside an already-baselined file. Read a count that went up as a new bug, never as noise.

## 5. One story's MSW handlers answer for the whole Docs page

Autodocs mounts every story of a file into **one** document, and `msw-storybook-addon` installs on a
single global worker — so the last story's handlers serve every story on that page. One error story
Expand All @@ -32,21 +41,21 @@ snapshot, stays green. That is not hypothetical: it is what made a screen's Docs
"Couldn't load this feedback". A story file installs no handlers at all;
`scripts/check-presentational-components.mjs` enforces it.

## 5. `test:storybook` does not run the README-export check — CI does, right after
## 6. `test:storybook` does not run the README-export check — CI does, right after

`.github/workflows/ci-tests.yml` runs `pnpm run export:readme-assets` after `test:storybook` and fails
the job if `docs/images/readme` is dirty. So the storybook job can go red printing "1382 passed". If a
change moves or renames a story that exports a README asset, run
`pnpm --filter webapp run export:readme-assets` and commit the result.

## 6. Storybook subcomponents get no Controls, and their `argTypes` cannot be overridden
## 7. Storybook subcomponents get no Controls, and their `argTypes` cannot be overridden

*"Subcomponents are only intended for documentation purposes and have some limitations: 1. The
`argTypes` of subcomponents are inferred … and cannot be manually defined or overridden. 2. The table
for each documented subcomponent does *not* include controls"*. This is the cost that decides whether a
part becomes a compound subcomponent or stays a prop — see `rules/composition-and-slots.md` rule 3.

## 7. A hand-rolled stateful wrapper swallows the spy in `meta.args`
## 8. A hand-rolled stateful wrapper swallows the spy in `meta.args`

If the wrapper passes its own `onChange` instead of `{...args}`, the `fn()` declared in `meta.args` can
never be called, never be asserted, and never appears in the Actions panel — while the file looks fully
Expand Down
2 changes: 1 addition & 1 deletion .github/instructions/tsx.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Follow the [general coding guidelines](./general-coding.instructions.md) in addi
## Tooling integrations
- Keep stories colocated (`Component.stories.tsx`) and represent the real UI states that Chromatic validates.
- The React Compiler is enabled in `webapp/vite.shared.ts`. Write pure components, avoid conditional hooks, and remove hand-written memoization unless profiling demands it. Use `'use no memo'` only while debugging.
- Format and lint with Biome (`pnpm run check`) before pushing. Fix warnings or explain them in the pull request.
- Format with Biome and lint with oxlint (`pnpm run check`) before pushing. Fix warnings or explain them in the pull request.
- Keep Vitest configs minimal. Suites that need a browser runtime get their own config, as `vitest.config.storybook.ts` does.

## Testing and stories
Expand Down
2 changes: 1 addition & 1 deletion .github/prompts/fix-ci.prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Before fixing anything, categorize every failure into this table.
| Priority | Category | Symptoms | Fix Command |
|----------|----------|----------|-------------|
| 1 | Formatting | "Formatting failed", biome/prettier diff | `pnpm run format` |
| 2 | Lint | Biome lint errors | `pnpm run check:fix` |
| 2 | Lint | oxlint errors | `pnpm run check:webapp:fix` |
| 3 | TypeScript | TS2xxx errors, type mismatch | Fix the type error in source |
| 4 | Build failure | Compilation errors, missing exports | Fix imports/exports, verify with `pnpm run build:webapp` |
| 5 | Webapp tests | "FAIL" in webapp test output | Fix test or source, verify with `pnpm run test:webapp` |
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci-quality-gates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,14 @@ jobs:
cd webapp

ISSUES_FOUND=()
BIOME_OK=true
LINT_OK=true
TYPES_OK=true
# The webapp-scoped tree gates run here as well as in the pre-push hook, because a hook is
# skippable and a merge is not. check:env stays out: it reads server files this leg omits.
COMPONENTS_OK=true
STORIES_OK=true

pnpm run check || { BIOME_OK=false; ISSUES_FOUND+=("Biome checks failed. Run: cd webapp && pnpm run check -- --write"); }
pnpm run check || { LINT_OK=false; ISSUES_FOUND+=("Lint or formatting failed. Run: cd webapp && pnpm run check:fix"); }
pnpm run typecheck || { TYPES_OK=false; ISSUES_FOUND+=("TypeScript errors found. Run: cd webapp && pnpm run typecheck"); }
node ../scripts/check-presentational-components.mjs || { COMPONENTS_OK=false; ISSUES_FOUND+=("Components must take data as props. Run: pnpm run check:components"); }
node ../scripts/check-story-prose.mjs || { STORIES_OK=false; ISSUES_FOUND+=("Story prose renders HTML paragraphs. Run: pnpm run check:stories"); }
Expand All @@ -139,7 +139,7 @@ jobs:
echo "### Webapp Quality" >> $GITHUB_STEP_SUMMARY
echo "| Check | Status | Fix |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|-----|" >> $GITHUB_STEP_SUMMARY
[[ "$BIOME_OK" == "false" ]] && echo "| Biome (lint + format) | :x: Failed | \`cd webapp && pnpm run check -- --write\` |" >> $GITHUB_STEP_SUMMARY
[[ "$LINT_OK" == "false" ]] && echo "| Lint + format (oxlint + Biome) | :x: Failed | \`cd webapp && pnpm run check:fix\` |" >> $GITHUB_STEP_SUMMARY
[[ "$TYPES_OK" == "false" ]] && echo "| TypeScript | :x: Failed | \`cd webapp && pnpm run typecheck\` |" >> $GITHUB_STEP_SUMMARY
[[ "$COMPONENTS_OK" == "false" ]] && echo "| Presentational components | :x: Failed | \`pnpm run check:components\` |" >> $GITHUB_STEP_SUMMARY
[[ "$STORIES_OK" == "false" ]] && echo "| Story prose | :x: Failed | \`pnpm run check:stories\` |" >> $GITHUB_STEP_SUMMARY
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/ci-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,10 @@ jobs:

case "${{ matrix.test-type }}" in
"webapp-unit")
pnpm run test
# Bound runner deadlocks without coupling CI to the current suite duration, as the
# storybook leg below already does. Without it a hung suite burns the whole job
# timeout and GitHub retains no log for the step.
timeout --kill-after=30s 10m pnpm run test
;;
"webapp-storybook")
# Install Playwright browsers (cached in ~/.cache/ms-playwright by setup-caches)
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ jobs:
echo "|-------|-------------|" >> $GITHUB_STEP_SUMMARY
echo "| Formatting errors | \`pnpm run format\` |" >> $GITHUB_STEP_SUMMARY
echo "| TypeScript errors | \`pnpm run typecheck:webapp\` |" >> $GITHUB_STEP_SUMMARY
echo "| Biome lint errors | \`pnpm run lint:fix\` |" >> $GITHUB_STEP_SUMMARY
echo "| Lint errors | \`pnpm run check:webapp:fix\` |" >> $GITHUB_STEP_SUMMARY
echo "| Java formatting | \`pnpm run format:java\` |" >> $GITHUB_STEP_SUMMARY
echo "| OpenAPI out of sync | \`pnpm run generate:api\` |" >> $GITHUB_STEP_SUMMARY
echo "| Database schema drift | \`pnpm run db:draft-changelog\` |" >> $GITHUB_STEP_SUMMARY
Expand Down
2 changes: 1 addition & 1 deletion .opencode/commands/fix-ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Before fixing anything, categorize every failure into this table.
| Priority | Category | Symptoms | Fix Command |
|----------|----------|----------|-------------|
| 1 | Formatting | "Formatting failed", biome/prettier diff | `pnpm run format` |
| 2 | Lint | Biome lint errors | `pnpm run check:fix` |
| 2 | Lint | oxlint errors | `pnpm run check:webapp:fix` |
| 3 | TypeScript | TS2xxx errors, type mismatch | Fix the type error in source |
| 4 | Build failure | Compilation errors, missing exports | Fix imports/exports, verify with `pnpm run build:webapp` |
| 5 | Webapp tests | "FAIL" in webapp test output | Fix test or source, verify with `pnpm run test:webapp` |
Expand Down
2 changes: 1 addition & 1 deletion docs/contributor/ci-cd.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Before any release, code must pass:
| OpenAPI sync | Diff check | Client ↔ Server sync |
| Java formatting | Prettier (`prettier-plugin-java`) | Code style |
| Java lint | PMD | Static analysis |
| TypeScript | Biome + tsc | Lint + typecheck |
| TypeScript | oxlint + Biome + tsc | Lint + format + typecheck |

## 🔒 Security

Expand Down
2 changes: 1 addition & 1 deletion docs/contributor/coding-guidelines.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ These guidelines keep every service aligned. Keep changes focused. Mention any d
- TanStack Query v5 is initialised in `src/integrations/tanstack-query/root-provider.tsx`. OpenAPI helpers live in `src/api/@tanstack/react-query.gen.ts`.
- Global state lives in Zustand stores under `src/stores/**`.
- Tailwind CSS 4 powers styling. Tokens sit in `src/styles.css`. shadcn/ui primitives live in `src/components/ui/**`.
- Storybook, Chromatic, Vitest, and Biome cover tooling.
- Storybook, Chromatic, Vitest, oxlint, and Biome cover tooling.

### Building features

Expand Down
2 changes: 1 addition & 1 deletion docs/contributor/local-development.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Open the repository using the `project.code-workspace` file in VS Code and insta
- Java Extension Pack
- Spring Boot tools
- Node.js + TypeScript tooling
- Biome (lint + format)
- oxlint (lint) and Biome (format)
- Tailwind CSS IntelliSense

JetBrains alternatives such as IntelliJ (Java) and WebStorm (React/TypeScript) work equally well.
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"check:webapp": "pnpm --filter webapp run check",
"check:webapp:fix": "pnpm --filter webapp run check:fix",
"typecheck:webapp": "pnpm --filter webapp run typecheck",
"ci:webapp": "pnpm --filter webapp run ci",
"test:webapp": "pnpm --filter webapp run test",
"build:webapp": "pnpm --filter webapp run build",
"nats:extract-examples": "node --import tsx scripts/nats-extract-examples.ts",
Expand Down
Loading
Loading