Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
74 changes: 74 additions & 0 deletions docs/adr-013-frontend-linting-strategy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Architectural decision record (ADR) 013: Frontend linting strategy

## Status

Accepted on 26 October 2026. The frontend lint command runs Biome followed by
Oxlint, with the recommended Unicorn rules and the `ai-deslop` plugin enabled.

## Date

2026-10-26.

## Context and problem statement

The repository-owned frontend already uses Biome and project-specific checks.
It needs additional JavaScript and TypeScript rules for code quality and safer
handling of data received from APIs, configuration files, and tools. The added
checks must preserve existing Biome requirements, remain reproducible, and
surface type-aware findings in the same frontend lint command.

## Decision drivers

- Apply the recommended Unicorn profile to the frontend workspace.
- Add the requested `maharshi365/deslop` rules without weakening existing
Biome or TypeDoc checks.
- Pin the lint tools and external plugin so rule changes are reviewed with
version updates.
- Keep exceptions narrow and record direct rule conflicts explicitly.
- Validate untrusted input at its boundary rather than relying on unchecked
shape assumptions.

## Options considered

- Keep Biome alone. This does not provide the requested Unicorn and deslop
rules.
- Add Oxlint with floating versions. This would allow rule and plugin changes
without a reviewed dependency update.
- Add Oxlint with exact versions and enumerate the adopted rules. This keeps
the selected profile reviewable and makes exceptions visible in the config.

## Decision outcome

`frontend-pwa/package.json` pins Oxlint at `1.82.0`, `ai-deslop` at `0.4.9`, and
`oxlint-tsgolint` at `7.0.2004`. `ai-deslop` is loaded as an Oxlint JavaScript
plugin. Oxlint runs with type-aware rules enabled. The frontend `lint` script
runs `biome check .` first and `oxlint .` second.

`frontend-pwa/.oxlintrc.json` explicitly enables the recommended Unicorn rule
set and the selected deslop rules at error severity. Explicit rule entries make
the adopted profile and its review surface visible next to the tool
configuration. The single disabled rule is `deslop/no-reflect-apply`, because
it conflicts with `unicorn/prefer-reflect-apply`; Unicorn's recommendation
takes precedence. No other rules are disabled.

Oxlint's type-aware checks use the pinned `oxlint-tsgolint` package. Runtime
schemas belong at the boundary where the frontend or its tooling reads external
data. The task HTTP adapter, audit parser, and frontend configuration scripts
use Zod schemas so validated values have an explicit contract before the
application inspects them. The task schema retains unknown fields so additive
API changes remain available to callers.

Biome remains the first frontend lint stage and retains its existing rules and
plugins. TypeDoc remains a separate zero-tolerance documentation gate; neither
tool is weakened to accommodate Oxlint.

## Consequences

- Contributors run `make frontend-lint` to execute Biome and Oxlint in order.
- Changes to either exact Oxlint package version require reviewing the
configured rule set and its exceptions.
- Type-aware linting relies on the pinned `oxlint-tsgolint` executable and the
TypeScript project configuration supplied by the frontend workspace.
- A future Unicorn or deslop rule conflict must be handled with one narrow
documented exception, preferring Unicorn when the conflict is between those
two plugins.
7 changes: 4 additions & 3 deletions docs/contents.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ the first stop when choosing which document to read or update.
runtime expectations.
- [Developers' guide](developers-guide.md) explains maintainer workflows,
quality gates, local services, and implementation conventions.
- [ADR 012: Rust build standard](adr-012-rust-build-standard.md) records why
development builds use the fast flags while coverage and release builds stay
off them.
- [Roadmap](roadmap.md) tracks planned delivery work, dependencies, and
acceptance criteria.

Expand Down Expand Up @@ -59,6 +56,10 @@ the first stop when choosing which document to read or update.
records migration and coexistence policy.
- [ADR 011: Corbusier and Frankie review adapter role segregation](adr-011-corbusier-frankie-review-adapter-role-segregation.md)
records adapter responsibility boundaries.
- [ADR 012: Rust build standard](adr-012-rust-build-standard.md) records the
development build flags and release-build boundary.
- [ADR 013: Frontend linting strategy](adr-013-frontend-linting-strategy.md)
records frontend lint tools, selected rules, and the narrow plugin conflict.

## Guides and reference material

Expand Down
40 changes: 38 additions & 2 deletions docs/developers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,14 +194,48 @@ transport code directly.
gateway error mapping.
- `createFixtureTaskGateway` is the default local and test adapter. It keeps
task state in memory, serializes queued operations over that shared state,
and preserves fixture-first development until the live HTTP gateway is
selected in later roadmap work.
and supports fixture-first development. The HTTP gateway is also available as
an opt-in mode for the browser flow described below.

Run `make frontend-e2e` to exercise the browser paths. Playwright starts the
fixture app on port 4173 for the `chromium` project and an HTTP-mode app on
port 4174 for `http-chromium`, with `VITE_TASK_GATEWAY_MODE=http` and
`VITE_CORBUSIER_API_BASE_URL=/api/v1`. The HTTP flow intercepts API requests
with Playwright, so it needs no live backend. The fixture remains the default
gateway mode. Roadmap item 4.4.2 is complete; live task UI work remains in
pending item 4.4.3.

The shipped `4.4.1` slice boundary is recorded in
[`corbusier-design.md`](corbusier-design.md#713-repository-owned-frontend-workspace-boundary).
The Whitaker user's guide remains a tooling guide for the Rust lint runner and
does not describe these frontend-only APIs.

### Frontend linting

`make frontend-lint` runs Biome and then Oxlint in `frontend-pwa`. Oxlint uses
the recommended Unicorn profile and the pinned `ai-deslop` plugin with
type-aware rules enabled. The exact versions and the one rule conflict are
recorded in [ADR 013](adr-013-frontend-linting-strategy.md).

Runtime schemas belong at input boundaries. Frontend transport responses and
tooling configuration are parsed before callers rely on their shape; keep
unchecked shape tests and type assertions out of these paths.

The lint command supplements the existing Biome checks. Run
`make frontend-typecheck`, `make frontend-docs-check`, and `make frontend-test`
for the separate TypeScript, TypeDoc, and behavioural validation gates.

The semantic command regression suite creates temporary project fixtures and
copies the script under test into each fixture. Its setup helpers belong only
to that suite; production scripts do not depend on them. The audit command
tests provide a temporary `bun` executable through the child process's path to
exercise exit and signal handling without changing the parent environment.

The HTTP task gateway unit tests use a file-local envelope replacement helper
for cases that vary one JSON field. It creates a fresh response for each test,
while callers state their expected results. Keep it in that test module; other
adapter and command tests use their own fixtures.

### TypeScript type checking for tests

`frontend-pwa/tsconfig.test.json` extends the base `tsconfig.json` and widens
Expand All @@ -215,6 +249,8 @@ The separate test pass is required because tests and test configuration use
different inclusion patterns and ambient types from the production application
sources, while still depending on the application `src/` tree.

Vitest includes `.test.mjs` files under `src/` alongside TypeScript tests.

`make frontend-docs-check` runs `bun run docs:check` inside `frontend-pwa`:
TypeDoc's `notDocumented` validation over `frontend-pwa/src`
(`frontend-pwa/typedoc.json`), requiring a JSDoc block on every exported
Expand Down
183 changes: 183 additions & 0 deletions frontend-pwa/.oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["eslint", "typescript", "oxc", "unicorn"],
"jsPlugins": ["ai-deslop"],
"ignorePatterns": [
"node_modules/**",
"dist/**",
"coverage/**",
"playwright-report/**",
"test-results/**"
],
"categories": {
"correctness": "off",
"pedantic": "off",
"perf": "off",
"restriction": "off",
"style": "off",
"nursery": "off",
"suspicious": "off"
},
"options": {
"typeAware": true
},
"rules": {
"unicorn/catch-error-name": "error",
"unicorn/consistent-assert": "error",
"unicorn/consistent-date-clone": "error",
"unicorn/consistent-empty-array-spread": "error",
"unicorn/consistent-existence-index-check": "error",
"unicorn/consistent-function-scoping": "error",
"unicorn/consistent-template-literal-escape": "error",
"unicorn/empty-brace-spaces": "error",
"unicorn/error-message": "error",
"unicorn/escape-case": "error",
"unicorn/explicit-length-check": "error",
"unicorn/explicit-timer-delay": "error",
"unicorn/filename-case": "error",
"unicorn/import-style": "error",
"unicorn/max-nested-calls": "error",
"unicorn/new-for-builtins": "error",
"unicorn/no-abusive-eslint-disable": "error",
"unicorn/no-accessor-recursion": "error",
"unicorn/no-anonymous-default-export": "error",
"unicorn/no-array-callback-reference": "error",
"unicorn/no-array-fill-with-reference-type": "error",
"unicorn/no-array-method-this-argument": "error",
"unicorn/no-array-reduce": "error",
"unicorn/no-array-reverse": "error",
"unicorn/no-array-sort": "error",
"unicorn/no-await-expression-member": "error",
"unicorn/no-await-in-promise-methods": "error",
"unicorn/no-confusing-array-with": "error",
"unicorn/no-console-spaces": "error",
"unicorn/no-document-cookie": "error",
"unicorn/no-empty-file": "error",
"unicorn/no-immediate-mutation": "error",
"unicorn/no-instanceof-builtins": "error",
"unicorn/no-invalid-fetch-options": "error",
"unicorn/no-invalid-remove-event-listener": "error",
"unicorn/no-lonely-if": "error",
"unicorn/no-magic-array-flat-depth": "error",
"unicorn/no-negated-condition": "error",
"unicorn/no-negation-in-equality-check": "error",
"unicorn/no-nested-ternary": "error",
"unicorn/no-new-array": "error",
"unicorn/no-new-buffer": "error",
"unicorn/no-object-as-default-parameter": "error",
"unicorn/no-process-exit": "error",
"unicorn/no-single-promise-in-promise-methods": "error",
"unicorn/no-static-only-class": "error",
"unicorn/no-thenable": "error",
"unicorn/no-this-assignment": "error",
"unicorn/no-typeof-undefined": "error",
"unicorn/no-unnecessary-array-flat-depth": "error",
"unicorn/no-unnecessary-array-splice-count": "error",
"unicorn/no-unnecessary-await": "error",
"unicorn/no-unnecessary-slice-end": "error",
"unicorn/no-unreadable-array-destructuring": "error",
"unicorn/no-unreadable-iife": "error",
"unicorn/no-useless-collection-argument": "error",
"unicorn/no-useless-error-capture-stack-trace": "error",
"unicorn/no-useless-fallback-in-spread": "error",
"unicorn/no-useless-iterator-to-array": "error",
"unicorn/no-useless-length-check": "error",
"unicorn/no-useless-promise-resolve-reject": "error",
"unicorn/no-useless-spread": "error",
"unicorn/no-useless-switch-case": "error",
"unicorn/no-useless-undefined": "error",
"unicorn/no-null": "error",
"unicorn/no-zero-fractions": "error",
"unicorn/number-literal-case": "error",
"unicorn/numeric-separators-style": "error",
"unicorn/prefer-add-event-listener": "error",
"unicorn/prefer-array-find": "error",
"unicorn/prefer-array-flat": "error",
"unicorn/prefer-array-flat-map": "error",
"unicorn/prefer-array-index-of": "error",
"unicorn/prefer-array-some": "error",
"unicorn/prefer-at": "error",
"unicorn/prefer-bigint-literals": "error",
"unicorn/prefer-blob-reading-methods": "error",
"unicorn/prefer-class-fields": "error",
"unicorn/prefer-classlist-toggle": "error",
"unicorn/prefer-code-point": "error",
"unicorn/prefer-date-now": "error",
"unicorn/prefer-default-parameters": "error",
"unicorn/prefer-dom-node-append": "error",
"unicorn/prefer-dom-node-remove": "error",
"unicorn/prefer-dom-node-text-content": "error",
"unicorn/prefer-event-target": "error",
"unicorn/prefer-export-from": "error",
"unicorn/prefer-global-this": "error",
"unicorn/prefer-includes": "error",
"unicorn/prefer-keyboard-event-key": "error",
"unicorn/prefer-logical-operator-over-ternary": "error",
"unicorn/prefer-math-min-max": "error",
"unicorn/prefer-math-trunc": "error",
"unicorn/prefer-modern-dom-apis": "error",
"unicorn/prefer-modern-math-apis": "error",
"unicorn/prefer-module": "error",
"unicorn/prefer-native-coercion-functions": "error",
"unicorn/prefer-negative-index": "error",
"unicorn/prefer-node-protocol": "error",
"unicorn/prefer-number-coercion": "error",
"unicorn/prefer-number-properties": "error",
"unicorn/prefer-object-from-entries": "error",
"unicorn/prefer-optional-catch-binding": "error",
"unicorn/prefer-prototype-methods": "error",
"unicorn/prefer-query-selector": "error",
"unicorn/prefer-reflect-apply": "error",
// This conflicts with unicorn/prefer-reflect-apply; prefer Unicorn as requested.
"deslop/no-reflect-apply": "off",
"unicorn/prefer-regexp-test": "error",
"unicorn/prefer-response-static-json": "error",
"unicorn/prefer-set-has": "error",
"unicorn/prefer-set-size": "error",
"unicorn/prefer-single-call": "error",
"unicorn/prefer-spread": "error",
"unicorn/prefer-string-raw": "error",
"unicorn/prefer-string-replace-all": "error",
"unicorn/prefer-string-slice": "error",
"unicorn/prefer-string-starts-ends-with": "error",
"unicorn/prefer-string-trim-start-end": "error",
"unicorn/prefer-structured-clone": "error",
"unicorn/prefer-ternary": "error",
"unicorn/prefer-top-level-await": "error",
"unicorn/prefer-type-error": "error",
"unicorn/relative-url-style": "error",
"unicorn/require-array-join-separator": "error",
"unicorn/require-module-attributes": "error",
"unicorn/require-module-specifiers": "error",
"unicorn/require-number-to-fixed-digits-argument": "error",
"unicorn/switch-case-braces": "error",
"unicorn/switch-case-break-position": "error",
"unicorn/text-encoding-identifier-case": "error",
"unicorn/throw-new-error": "error",
"deslop/no-array-filter-map": "error",
"deslop/no-reduce-accumulator-copy": "error",
"deslop/no-chained-type-assertions": "error",
"deslop/no-conditional-empty-object-spread": "error",
"deslop/no-known-value-widening": "error",
"deslop/no-module-mocking": "error",
"deslop/no-object-parameters": "error",
"deslop/no-reflect-get": "error",
"deslop/no-runtime-typeof": "error",
"deslop/no-shape-in-symbol-names": "error",
"deslop/no-unknown-parameters": "error",
"deslop/no-unknown-returns": "error",
"deslop/no-unknown-type-aliases": "error",
"deslop/no-unsafe-dictionary-type": "error",
"deslop/no-widen-then-assert": "error",
"deslop/require-safety-comment-for-type-assertion": "error",
"deslop/no-call-only-assertions": "error",
"deslop/no-poor-substring-tests": "error",
"deslop/no-repeated-expect-call": "error",
"deslop/no-pass-through-type-alias": "error",
"typescript/no-deprecated": "error",
"typescript/no-require-imports": "error",
"typescript/no-floating-promises": "error",
"typescript/no-unnecessary-condition": "error",
"no-unreachable": "error",
}
}
Loading
Loading