Files
jellytau/eslint.config.js
dtourolle 8f5c9023d0 ci: make the frontend gates real, and fix the coverage script
The repo configured four frontend gates and enforced one of them. eslint
and prettier ran in no workflow and no hook; `bun run check` ran only in
build-release.yml, so a type error could sit on master until somebody cut
a tag; and `bun run test:coverage` had been dead for months.

CI (build-and-test.yml) now runs format:check, lint, check and coverage
alongside the existing boundary and doc-link tripwires.

The coverage script failure was a version mismatch, not a config problem:
@vitest/coverage-v8 resolved to 4.1.10, whose peer range pins vitest
exactly, while package.json asked for ">=1.0.0 <5.0.0" and got 4.0.16 --
every run died on a missing BaseCoverageProvider export. The loose range
is what allowed the pair to drift, so it is now ^4.1.10.

Two ratchets, same policy as MIN_THRESHOLD in traceability-check.yml:

  eslint  --max-warnings=159   (0 errors; 159 is today's backlog, only
                                ever lower it)
  vitest  thresholds           (statements 51 / branches 45 /
                                functions 46 / lines 52, measured at
                                54.6 / 48.7 / 49.6 / 55.1)

no-console is promoted from "off" to "error": the logger-facade
migration it was waiting on is finished -- 8 calls remained, 2 of them
real stragglers in the settings page, now on the facade the file already
imported. The sink itself, tests, and scripts/ are exempted; a CLI whose
stdout is the product is not a stray debug statement.

The threshold was verified to bite by raising it to 99 and watching the
run go red, not by assuming an unfailed gate works.

DR-205 moves to Done; the coverage gate is DR-215.
2026-08-21 18:11:26 +02:00

200 lines
7.8 KiB
JavaScript

// ESLint flat config for the JellyTau frontend (Svelte 5 + TypeScript strict).
//
// TRACES: | DR-205
//
// Scope: `src/` (the presentation layer), `scripts/` (build tooling), and the
// root config files. The Rust backend is linted by clippy, not by this config.
//
// Formatting is NOT ESLint's job here — `eslint-config-prettier` is applied last
// and switches off every stylistic rule that would fight `prettier`. Run
// `bun run format` / `bun run format:check` for layout.
import js from "@eslint/js";
import ts from "typescript-eslint";
import svelte from "eslint-plugin-svelte";
import globals from "globals";
import prettier from "eslint-config-prettier";
import svelteConfig from "./svelte.config.js";
export default ts.config(
{
// Kept in one place so `npx eslint .` and editor integrations agree.
ignores: [
"node_modules/",
".svelte-kit/",
// Scratch worktrees (git-ignored) hold full checkouts of this repo,
// including their own generated .svelte-kit trees. Without this, `eslint .`
// lints every in-flight branch and reports its generated code as ours.
".claude/",
"build/",
"dist/",
"coverage/",
"package/",
"src-tauri/",
// Generated by tauri-specta on every Rust build — never hand-edited, and
// its shape is dictated by the Rust command definitions.
"src/lib/api/bindings.ts",
],
},
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
prettier,
...svelte.configs.prettier,
{
languageOptions: {
globals: {
...globals.browser,
...globals.es2021,
},
},
rules: {
// The logger-facade migration this rule was waiting on is done: the ~468
// `console.*` calls that used to live in `src/` are gone, replaced by
// `createLogger(...)` from src/lib/utils/logger.ts (DR-204), which is now
// the single sink. Nothing is allowed through — not even warn/error —
// because the facade's own `warn`/`error` levels are always emitted, so a
// raw call has no capability the facade lacks. It only loses the scope tag
// and the runtime level control.
//
// The sink itself is exempted below, as are tests (a test that asserts on
// logging has to be able to talk about `console`).
"no-console": "error",
// Unused values are a real signal, but `_`-prefixed args are the
// established way to say "this parameter exists for the signature".
//
// ⚠️ warn, not error: the tree carries ~94 genuinely dead bindings (stale
// imports, `$state` left over from refactors, unused `catch (e)`). Every
// one is a real finding, but fixing them here would mean ~50 unrelated
// files in this tooling commit. Clear the backlog, then promote to
// "error".
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
},
],
// Warn-only rules: each flags something real, but the existing tree has
// more instances than can be fixed without swamping unrelated diffs.
// Drive these to zero and promote them to "error" — do not delete them.
//
// `any` at the Tauri IPC boundary, mostly in code predating the
// tauri-specta bindings (~25 sites outside tests).
"@typescript-eslint/no-explicit-any": "warn",
// Empty catch/if bodies that swallow an error.
"no-empty": ["warn", { allowEmptyCatch: true }],
// Prefer `import type` so type-only imports are erased cleanly by the
// bundler instead of pulling a module in at run time.
"@typescript-eslint/consistent-type-imports": "off",
// Not applicable to this app (~130 hits, all no-ops). SvelteKit's
// `resolve()` exists so hrefs keep working under a non-empty
// `kit.paths.base`; JellyTau is an adapter-static SPA served from the
// Tauri webview root and svelte.config.js sets no `base`. Re-enable this
// the day a base path is introduced — the rule is otherwise correct.
// (Declared here, not in the *.svelte block: `goto()` is also called from
// plain .ts modules such as src/lib/utils/navigation.ts.)
"svelte/no-navigation-without-resolve": "off",
},
},
{
// Svelte components: the parser needs the project's svelte.config.js so it
// resolves preprocessors and Svelte 5 runes the same way the build does.
files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"],
languageOptions: {
parserOptions: {
parser: ts.parser,
svelteConfig,
},
},
rules: {
// Warn-only — real findings, but each fix is a behavioural refactor that
// does not belong in a tooling commit:
// require-each-key keyed {#each} changes DOM reuse semantics
// prefer-svelte-reactivity Set/Map -> SvelteSet/SvelteMap changes
// reactivity, not just syntax
// prefer-writable-derived $state + $effect -> writable $derived
// no-at-html-tags {@html} sites need an XSS review each
"svelte/require-each-key": "warn",
"svelte/prefer-svelte-reactivity": "warn",
"svelte/prefer-writable-derived": "warn",
"svelte/no-at-html-tags": "warn",
// Warn-only: this rule cannot see the Svelte *compiler's* warning set, so
// it reports `<!-- svelte-ignore a11y_… -->` as unused when the compiler
// may still be emitting the warning it suppresses. Verify against a real
// `bun run check` before deleting any of them.
"svelte/no-unused-svelte-ignore": "warn",
},
},
{
// The logging facade is the one place allowed to touch `console` — it *is*
// the sink every other module reaches it through (see the `no-console`
// comment above). `createLogger`'s `console[method](...)` dispatch is a
// computed member access, which the rule flags like any other.
files: ["src/lib/utils/logger.ts"],
rules: {
"no-console": "off",
},
},
{
// Node-side tooling: build/test scripts and root config files run under
// Bun/Node, not in the webview.
files: [
"scripts/**/*.{ts,js}",
"*.config.{ts,js}",
"*.config.*.{ts,js}",
"svelte.config.js",
"eslint.config.js",
],
languageOptions: {
globals: {
...globals.node,
},
},
rules: {
// These are command-line tools (extract-traces, release-notes, ...) whose
// stdout IS the product — `bun run traces:markdown > docs/traceability.md`
// depends on it. The logging facade is a webview concern; a CLI printing
// its result is not a stray debug statement.
"no-console": "off",
},
},
{
// Test files: vitest globals are enabled in vitest.config.ts.
files: ["**/*.{test,spec}.{ts,js}", "src/test/**/*.{ts,js}"],
languageOptions: {
globals: {
...globals.node,
...globals.vitest,
},
},
rules: {
// Tests are allowed to talk about `console` — several spy on it to assert
// what the logging facade emits, and scripts/ tooling tests capture output.
"no-console": "off",
// Test doubles legitimately use `any` for partial mocks.
"@typescript-eslint/no-explicit-any": "off",
// `vi.mock` factories are hoisted above the import graph, so a lazy
// `require()` inside one is the documented escape hatch.
"@typescript-eslint/no-require-imports": "off",
// Several tests deliberately replay a production assignment sequence
// (`currentStreamUrl = newStreamUrl; hasSeeked = false;`) to document the
// `$effect` they stand in for. The "useless" write is the subject under
// test, not dead code.
"no-useless-assignment": "off",
},
},
);