mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-11 13:07:51 +01:00
./web.sh serves health, the configuration and the photo album on 127.0.0.1:8140 -- the port tool/tagger.py vacates. React 19, Vite 8, TypeScript 6.0.3, ESLint 10 with strictTypeChecked + stylisticTypeChecked + unicorn + sonarjs + perfectionist, Vitest 4 at 100% on all four metrics. No coverage exclude list, including for main.tsx. This repository treats a coverage omit as a suppression, and "the DOM bootstrap is awkward to assert" is the same sentence as "this code is untested" -- so the bootstrap is a shim thin enough to test in jsdom, and the album view holds its file list apart from the album so the update path stopped carrying a "what if it is null" branch that cannot happen. An untestable branch is an untested one. TypeScript stays at 6.0.3: typescript-eslint 8.68.0 declares typescript >=4.8.4 <6.1.0, and the fleet-wide allowlist entry says the compiler waits for the linter. The freshness gate then caught two pins the reference repo was carrying stale -- eslint-plugin-unicorn 73 -> 74 and a floating node-version: "24" in CI -- both bumped rather than allowlisted. Traps the gates found, all of them real: - .gitignore has a bare lib/, which matches at any depth, so web/src/lib/ would have been silently untracked. The source lives in src/api/. - CI's shellcheck glob was `run.sh scripts/*.sh`, so tag.sh was unlinted in CI for its whole life and web.sh would have inherited that. Now ./*.sh. - tsconfig.json is strict JSON, not JSONC: check-json is generic and tsconfig being commentable is a convention, not a requirement. - The personal-data gate refused an invented phone number in a test, which is exactly what it is for. The album's warning that names a command moved with the command, and every path that pointed at tool/tagger.py now points at ./web.sh. The historical mentions stay: they explain why the code is shaped as it is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01416TegJ3sa8xL5qGGgrygP
184 lines
8.2 KiB
JavaScript
184 lines
8.2 KiB
JavaScript
// Maximally aggressive, type-aware flat config.
|
|
//
|
|
// Baseline is typescript-eslint's *type-checked* strict + stylistic presets, so
|
|
// anything they flag is an error, not a warning. On top of that: unicorn (all),
|
|
// sonarjs (cognitive complexity / duplicate logic), perfectionist (sorting), and
|
|
// react-hooks. Unused disable directives are themselves errors, so every
|
|
// `eslint-disable` in this repo must be load-bearing.
|
|
//
|
|
// Deliberately absent:
|
|
// - eslint-plugin-react : 7.37.x still calls context.getFilename(), removed in
|
|
// ESLint 10, so it crashes the run.
|
|
// - eslint-plugin-jsx-a11y: 6.10.2 declares peer ESLint "^3 || ... || ^9" — no
|
|
// ESLint 10 support yet. a11y is enforced by review +
|
|
// testing-library queries (getByLabelText) instead.
|
|
//
|
|
// TypeScript is pinned to 6.0.x, not 7.x: typescript-eslint requires
|
|
// "typescript >=4.8.4 <6.1.0", and losing every type-aware rule would be a far
|
|
// bigger regression than gaining the Go-native compiler.
|
|
import js from "@eslint/js";
|
|
import perfectionist from "eslint-plugin-perfectionist";
|
|
import reactHooks from "eslint-plugin-react-hooks";
|
|
import sonarjs from "eslint-plugin-sonarjs";
|
|
import unicorn from "eslint-plugin-unicorn";
|
|
import tseslint from "typescript-eslint";
|
|
|
|
export default tseslint.config(
|
|
{ ignores: ["dist", "coverage"] },
|
|
{ linterOptions: { reportUnusedDisableDirectives: "error" } },
|
|
|
|
js.configs.recommended,
|
|
...tseslint.configs.strictTypeChecked,
|
|
...tseslint.configs.stylisticTypeChecked,
|
|
unicorn.configs.all,
|
|
sonarjs.configs.recommended,
|
|
perfectionist.configs["recommended-natural"],
|
|
|
|
{
|
|
files: ["**/*.{ts,tsx}"],
|
|
languageOptions: {
|
|
parserOptions: {
|
|
projectService: true,
|
|
tsconfigRootDir: import.meta.dirname,
|
|
},
|
|
},
|
|
plugins: { "react-hooks": reactHooks },
|
|
rules: {
|
|
...reactHooks.configs.recommended.rules,
|
|
|
|
// Numbers in template literals are unambiguous and pervasive here
|
|
// (counts, star totals, grades).
|
|
"@typescript-eslint/restrict-template-expressions": [
|
|
"error",
|
|
{ allowNumber: true },
|
|
],
|
|
|
|
// Keep the rule, widen its vocabulary. `passesFilters(server, filter)`
|
|
// and `matchesQuery(...)` are predicates that read as English; forcing
|
|
// them to `isPassingFilters` would be worse prose in service of a prefix
|
|
// list. `passes`/`matches` are added, nothing is removed.
|
|
"unicorn/consistent-boolean-name": [
|
|
"error",
|
|
{ prefixes: { matches: true, passes: true } },
|
|
],
|
|
|
|
// Kebab-case files exporting PascalCase components: house convention.
|
|
"unicorn/filename-case": ["error", { case: "kebabCase" }],
|
|
|
|
// Keep the rule, drop one replacement: `props` is React's own word.
|
|
// `interface FilterBarProps` is the convention every React codebase and
|
|
// every reader expects; `FilterBarProperties` would be unidiomatic in
|
|
// service of a generic dictionary. Every other replacement stays on.
|
|
"unicorn/name-replacements": ["error", { replacements: { props: false } }],
|
|
},
|
|
},
|
|
|
|
// House overrides, applied to every file including this config itself.
|
|
//
|
|
// Each rule below is switched off deliberately: these are the cases where
|
|
// "more aggressive" makes the code worse rather than safer. A rule you must
|
|
// disable at every use site is worse than no rule at all, because
|
|
// reportUnusedDisableDirectives then turns the disables into their own debt.
|
|
{
|
|
rules: {
|
|
// Directly contradicts unicorn/consistent-class-member-order: one wants
|
|
// a private field before the getter that reads it, the other wants the
|
|
// getter first, and satisfying either re-triggers the other. unicorn's
|
|
// wins because "declare the field before the code that uses it" is a
|
|
// reason, whereas alphabetical order within a class is not.
|
|
"perfectionist/sort-classes": "off",
|
|
|
|
// Sorts Map entries by key. The marker table in parse-readme.ts is keyed
|
|
// by emoji codepoint, so "sorted" would mean ordered by
|
|
// U+1F396 < U+1F3CE < ... — meaningless to a reader, and it would shred
|
|
// the legend's own grouping (official, languages, scope, OS) into noise.
|
|
"perfectionist/sort-maps": "off",
|
|
|
|
// Alphabetises declarations within a file, which actively fights reading
|
|
// order: it would scatter `GRADES -> gradeRank -> isTripleA` into
|
|
// `compositeRank -> GRADES -> gradeCoverage`. Alphabetical order is not
|
|
// narrative order. Every *other* perfectionist rule stays on — sorting
|
|
// imports, union members and object keys destroys no narrative.
|
|
"perfectionist/sort-modules": "off",
|
|
|
|
// Wants JSDoc blocks written without leading `*`, which is neither the
|
|
// TypeScript convention nor what editors/typedoc render.
|
|
"unicorn/no-asterisk-prefix-in-documentation-comments": "off",
|
|
|
|
// Flags hand-wrapped comment prose. Comments here are wrapped to 80
|
|
// columns on purpose; the rule would force one long line per paragraph.
|
|
"unicorn/no-manually-wrapped-comments": "off",
|
|
|
|
// `null` is load-bearing here and cannot be replaced by `undefined`:
|
|
// 1. The pipeline's output is JSON, and JSON has no `undefined`. A
|
|
// 2. The two values mean different things in this domain — `null` is
|
|
// "Glama never indexed this server", `undefined` is "indexed, but
|
|
// this axis was not graded". Collapsing them would lose information
|
|
// the UI displays differently (not-indexed vs graded-partial).
|
|
"unicorn/no-null": "off",
|
|
|
|
// `Temporal` does not exist in this project's runtime: Node 26 reports
|
|
// `typeof Temporal === "undefined"`. The rule is unfollowable until it
|
|
// ships. The inputs here are GitHub's ISO-8601 timestamps, which
|
|
// Date.parse handles identically across engines — the cross-engine
|
|
// inconsistency the rule warns about is for non-ISO strings.
|
|
"unicorn/prefer-temporal": "off",
|
|
|
|
// The domain vocabulary is abbreviated on purpose: `os`, `spdx`, `repo`,
|
|
// `env` are the names the source data uses, and expanding them to
|
|
// `operatingSystem` / `repository` would obscure the mapping.
|
|
"unicorn/prevent-abbreviations": "off",
|
|
},
|
|
},
|
|
|
|
{
|
|
rules: {
|
|
// Caps a try block at ONE statement. Unfollowable where the whole point
|
|
// is that several steps can throw: loadPresets must wrap getItem (throws
|
|
// in private browsing), JSON.parse (throws on junk) and the shape check
|
|
// together, because any of them failing means the same thing — fall back
|
|
// to no presets. Splitting them into three try blocks would be strictly
|
|
// worse code. The rule has no maximum option, so it is all or nothing.
|
|
"unicorn/try-complexity": "off",
|
|
},
|
|
},
|
|
|
|
// Tests: relax the rules that fight with mocking and fixtures.
|
|
{
|
|
rules: {
|
|
"@typescript-eslint/no-empty-function": "off",
|
|
"@typescript-eslint/no-non-null-assertion": "off",
|
|
"@typescript-eslint/require-await": "off",
|
|
"@typescript-eslint/unbound-method": "off",
|
|
"sonarjs/no-duplicate-string": "off",
|
|
// Test helpers live beside the tests that use them; hoisting every
|
|
// fixture factory to module scope to satisfy a scoping rule would
|
|
// scatter each test's setup away from the test.
|
|
"unicorn/consistent-function-scoping": "off",
|
|
// A no-op stub is the point of a stub.
|
|
"unicorn/no-empty-file": "off",
|
|
},
|
|
},
|
|
|
|
// The vitest setup file exists *to* perform top-level side effects: it patches
|
|
// globals (ResizeObserver, layout metrics, pointer capture) that jsdom lacks,
|
|
// before any test imports run. These two rules cannot be satisfied by a file
|
|
// whose entire contract is "mutate the environment on import".
|
|
{
|
|
files: ["src/test/setup.ts"],
|
|
rules: {
|
|
// The stubs are property getters installed on DOM prototypes, so `this`
|
|
// is the element being measured — there is no class to put them in.
|
|
"unicorn/no-this-outside-of-class": "off",
|
|
"unicorn/no-top-level-assignment-in-function": "off",
|
|
"unicorn/no-top-level-side-effects": "off",
|
|
},
|
|
},
|
|
|
|
// Plain JS config files get no type-aware linting.
|
|
{
|
|
extends: [tseslint.configs.disableTypeChecked],
|
|
files: ["**/*.{js,mjs,cjs}"],
|
|
},
|
|
);
|