From 7e681e112e24d8ea656cfc650a35d1502fe74944 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sun, 4 Oct 2026 21:58:26 +0200 Subject: [PATCH] test(gates)!: the size/complexity ratchet leaves the gate; audit_metrics reports it sourceSize.contract.test.ts, its test kit and backend/tests/test_source_size.py held 13 FE / 9 BE file ceilings and 64 FE / 32 BE function ceilings. No real hit (E34 S2/S4): its history is its own birth (751e1ae5), two rounds of fixes to its own logic (e432dc27, d1df76e8), the snapshot move (182ec311) and ceiling edits inside feature fixes (5806adda, df4a79a1, and the S2/S3 cuts here). It caught no defect, went red 8 times on its own limits and drew 5 review findings about itself. The numbers stay visible: audit_metrics reports file size and complexity, and an epic compares before and after. docs/gates.md: the two size rows go; the C901 row says where it is read now. --- backend/tests/test_source_size.py | 144 ------------------ docs/development.md | 2 +- docs/gates.md | 4 +- frontend/src/lib/gates.snapshot.json | 2 - frontend/src/lib/sourceSize.contract.test.ts | 148 ------------------- frontend/src/lib/sourceSize.testkit.ts | 141 ------------------ scripts/audit_metrics.py | 4 +- scripts/fe_complexity.mjs | 2 +- 8 files changed, 5 insertions(+), 442 deletions(-) delete mode 100644 backend/tests/test_source_size.py delete mode 100644 frontend/src/lib/sourceSize.contract.test.ts delete mode 100644 frontend/src/lib/sourceSize.testkit.ts diff --git a/backend/tests/test_source_size.py b/backend/tests/test_source_size.py deleted file mode 100644 index f83a658e..00000000 --- a/backend/tests/test_source_size.py +++ /dev/null @@ -1,144 +0,0 @@ -"""The size / complexity RATCHET for the backend (E32, owner 2026-10-03 — "class size" from the quality -checklist, measured rather than argued). The frontend's twin is src/lib/sourceSize.contract.test.ts. - -Two limits, each with today's offenders as a CEILING: - * a module over MAX_LINES lines — a listed one may not grow past its line count; any other module - may not cross the limit; - * a function over ruff's C901 limit (cyclomatic complexity, MAX_COMPLEXITY) — a listed one, by its - NAME (`module::function`), may not rise; no other function may cross the limit. (Ranked per - module until the E32 review: one function could grow by what another in the same module lost.) - A rename reads as a new offender plus a stale entry: move its row. -Shrinking is free; a module or function that drops UNDER the limit must be taken off its list (red -until it is), so the lists only get shorter. Raising a ceiling is a decision for the owner, with the -reason in docs/gates.md. - -C901 is measured here rather than switched on in ruff.toml because ruff has no per-function -baseline: on, it would need a `noqa` on each of today's offenders, and a `noqa` lets the function -grow without limit. This keeps the ceiling on the number. -""" - -import json -import re -import subprocess -import sys -from pathlib import Path - -BACKEND = Path(__file__).resolve().parent.parent -MAX_LINES = 800 -MAX_COMPLEXITY = 10 # ruff's C901 default (mccabe max-complexity) - -BIG_MODULES: dict[str, int] = { - "app/downloads/edit.py": 833, - "app/downloads/pipeline.py": 870, - "app/logs.py": 839, - "app/models.py": 1525, - "app/plex/stream.py": 1128, - "app/plex/subtitles.py": 1092, - "app/routes/auth.py": 1273, - "app/routes/downloads.py": 1232, -} - -COMPLEX: dict[str, int] = { - "app/bodylimit.py::__call__": 11, - "app/discover/hero.py::pick": 14, - "app/discover/pins.py::_shelf_card": 12, - "app/downloads/edit.py::normalize_edit_spec": 16, - "app/downloads/edit.py::run_ffmpeg": 15, - "app/downloads/formats.py::build_ydl_opts": 11, - "app/downloads/gc.py::_sweep_orphan_files": 15, - "app/downloads/gc.py::run_download_gc": 11, - "app/downloads/og.py::_jpeg_size": 12, - "app/downloads/og.py::_watch_tags": 11, - "app/downloads/pipeline.py::_download": 14, - "app/downloads/pipeline.py::_process_edit": 12, - "app/feedquery.py::feed_summary": 14, - "app/feedquery.py::filtered_query": 28, - "app/plex/query.py::meta_filter_conds": 13, - "app/plex/stream.py::start_session": 14, - "app/plex/subtitles.py::_ffmpeg_extract": 12, - "app/plex/sync.py::sync": 13, - "app/plex/tmdb.py::parse_tmdb": 11, - "app/plex/watch_sync.py::run_plex_watch_sync": 12, - "app/routes/auth.py::callback": 19, - "app/routes/debug_sessions.py::send_issue": 12, - "app/routes/downloads.py::resolve_source": 11, - "app/routes/plex/browse.py::facets": 25, - "app/routes/plex/browse.py::per_table": 13, - "app/routes/plex/browse.py::unified_library": 18, - "app/routes/plex/detail.py::item_detail": 13, - "app/routes/search.py::search_youtube": 15, - "app/sync/playlists.py::merge_playlist": 19, - "app/sync/runner.py::run_rss_poll": 13, - "app/sync/tagsuggest.py::compute_tag_suggestions": 14, - "app/worker.py::_run_supervised": 18, -} - - -def _line_counts() -> dict[str, int]: - return { - p.relative_to(BACKEND).as_posix(): len(p.read_text(encoding="utf-8").split("\n")) - for p in sorted((BACKEND / "app").rglob("*.py")) - } - - -def _complexity() -> dict[str, int]: - """`module::function` -> its complexity; a name taken twice in a module gets ` 2`, ` 3` in - source order.""" - out = subprocess.run( - [ - sys.executable, - "-m", - "ruff", - "check", - "--config", - str(BACKEND / "ruff.toml"), - "--select", - "C901", - "--output-format", - "json", - "--exit-zero", - str(BACKEND / "app"), - ], - capture_output=True, - text=True, - check=True, - ).stdout - found: dict[str, int] = {} - for m in sorted(json.loads(out), key=lambda m: (m["filename"], m["location"]["row"])): - value = re.search(r"\((\d+) > \d+\)", m["message"]) - name = re.search(r"`([^`]+)`", m["message"]) - assert value and name, m["message"] - rel = Path(m["filename"]).resolve().relative_to(BACKEND).as_posix() - key, n = f"{rel}::{name.group(1)}", 2 - while key in found: - key, n = f"{rel}::{name.group(1)} {n}", n + 1 - found[key] = int(value.group(1)) - return found - - -def test_no_module_crosses_the_line_limit_and_a_listed_one_does_not_grow(): - over = [ - f"{f}: {n} lines (ceiling {BIG_MODULES.get(f, MAX_LINES)})" - for f, n in _line_counts().items() - if n > BIG_MODULES.get(f, MAX_LINES) - ] - assert over == [] - - -def test_lists_no_module_that_is_back_under_the_limit(): - lines = _line_counts() - assert [f for f in BIG_MODULES if lines.get(f, 0) <= MAX_LINES] == [] - - -def test_no_function_crosses_the_complexity_limit_and_a_listed_one_does_not_rise(): - worse = [ - f"{fn}: {v} (ceiling {COMPLEX.get(fn, MAX_COMPLEXITY)})" - for fn, v in _complexity().items() - if v > COMPLEX.get(fn, MAX_COMPLEXITY) - ] - assert worse == [] - - -def test_lists_no_function_that_is_back_under_the_limit_or_gone(): - actual = _complexity() - assert [fn for fn in COMPLEX if fn not in actual] == [] diff --git a/docs/development.md b/docs/development.md index e23464b1..5356de25 100644 --- a/docs/development.md +++ b/docs/development.md @@ -98,7 +98,7 @@ npm run typecheck # tsc x3 (app / node / e2e) npm run lint # eslint (typed rules included; warnings fail) npm run format:check # prettier npm run knip # unused files/exports/deps — and unused by what SHIPS (--production) -npm run test # vitest (incl. the compiler, size/complexity and gate-register contracts) +npm run test # vitest (incl. the compiler and gate-register contracts) npx vite build # the production bundle (devOnlyGuard fails it on dev-only code) npm audit --omit=dev --audit-level=high # what ships to the browser, at high or above ``` diff --git a/docs/gates.md b/docs/gates.md index 096f327a..f90ae3e7 100644 --- a/docs/gates.md +++ b/docs/gates.md @@ -87,7 +87,7 @@ project checks needs a row. | Scope | Narrowed | Why | Measured | | --- | --- | --- | --- | | backend + scripts | `B008` — narrowed: `fastapi.Depends`, `fastapi.Query` declared immutable | FastAPI's dependency injection IS a call in a default argument, on every route. Every other call in a default still fails. | 474 sites, all of them FastAPI | -| backend/app | `C901` — not in ruff; measured by `tests/test_source_size.py` | Ruff has no per-function baseline, so turning it on would need a `noqa` per offender, and a `noqa` lets a function grow without limit. The test holds each offender, by name, at its current value. Tests and scripts are outside it: see the size / complexity rows under Lanes. | 32 functions over 10 | +| backend/app | `C901` — not in ruff; reported by `scripts/audit_metrics.py` | Ruff has no per-function baseline, so turning it on would need a `noqa` per offender, and a `noqa` lets a function grow without limit. Complexity is a number an epic reads before and after (E35), not a gate: the ratchet that held it caught no defect in E32–E34, went red 8 times on its own ceilings and drew 5 review findings about itself. | 32 functions over 10 (E34) | ## basedpyright (backend) @@ -121,8 +121,6 @@ project checks needs a row. | CVE gate | HIGH/CRITICAL with a fix available only | `--ignore-unfixed` + `--severity HIGH,CRITICAL`: a finding with no fix cannot be acted on by an upgrade; it shows in the scan output. A fixable one fails the publish, and the only way past it is `.trivyignore.yaml`. | 0 open (E32 S1) | | npm audit | `--omit=dev --audit-level=high` | What ships to the browser, at high or above. The dev toolchain's advisories were burned down by hand in E32 S1 (18 → 0). | 0 | | React Compiler | the functions it skips, pinned | `components/compilerCoverage.contract.test.ts` — the list may only shrink. End state: only VirtualGrid (TanStack Virtual, an IncompatibleLibrary) remains. | 93 (E32 S1) → 29 (E32 S3) → 1, VirtualGrid (E32 S4) | -| size / complexity | today's offenders as ceilings | `src/lib/sourceSize.contract.test.ts`, `backend/tests/test_source_size.py`: listed files and functions may not grow, nothing new may cross the limit, an entry back under it must be deleted. A function is held by its name, not its rank in the file. | files >800 lines: 13 FE / 9 BE; functions over the limit: 64 FE / 32 BE (E32 review) | -| size / complexity | shipped code only — not tests, test kits, e2e or `scripts/` | The ratchet guards what runs in production. A test file is long because it is a list of cases, and a test kit is test code; splitting either moves lines without making anything easier to change. `scripts/` is release and seed tooling, not in the image. | outside it: tests >800 lines 2 FE / 5 BE; functions over the limit 2 FE test code (`gates.contract.test.ts` `eslintNarrowings` 23, E32 review round 3; `compilerSkips.testkit.ts` 25), 1 BE test (11), 1 script (`e2e_seed.py` `main` 13) | ## Accepted findings diff --git a/frontend/src/lib/gates.snapshot.json b/frontend/src/lib/gates.snapshot.json index b2366411..ef2e43f2 100644 --- a/frontend/src/lib/gates.snapshot.json +++ b/frontend/src/lib/gates.snapshot.json @@ -1505,7 +1505,6 @@ "src/lib/saveLanguage.dom.test.ts", "src/lib/savedViews.test.ts", "src/lib/sortableA11y.contract.test.ts", - "src/lib/sourceSize.contract.test.ts", "src/lib/staleBuild.dom.test.ts", "src/lib/store.dom.test.tsx", "src/lib/subscriptionCache.test.ts", @@ -1948,7 +1947,6 @@ "src/lib/savedViews.ts", "src/lib/seekableEnd.ts", "src/lib/sortOptions.ts", - "src/lib/sourceSize.testkit.ts", "src/lib/staleBuild.ts", "src/lib/storage.ts", "src/lib/store.ts", diff --git a/frontend/src/lib/sourceSize.contract.test.ts b/frontend/src/lib/sourceSize.contract.test.ts deleted file mode 100644 index d759ba8f..00000000 --- a/frontend/src/lib/sourceSize.contract.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - boundNamesIn, - complexityOffenders, - lineCounts, - MAX_COMPLEXITY, - MAX_LINES, - shippedFiles, -} from "./sourceSize.testkit"; - -// The size / complexity RATCHET for the shipped frontend (E32, owner 2026-10-03 — "class size" from -// the quality checklist, measured rather than argued). Two limits, each with today's offenders as a -// CEILING: -// • a file over 800 lines — the listed ones may not grow past their line count; any other -// file may not cross the limit; -// • a function over complexity 20 (eslint `complexity`, cyclomatic) — a listed one, by its -// NAME, may not rise; no other function may cross the limit. (Ranked per file until the E32 -// review: one function could grow by what another in the same file lost.) A rename reads as a -// new offender plus a stale entry: move its row. -// Shrinking is free; a file or function that drops UNDER the limit must be taken off its list (red -// until it is), so the lists only get shorter. Raising a ceiling is a decision for the owner, with -// the reason in docs/gates.md. JSX branches count toward complexity, so a component scores high by -// construction — the limit catches the outliers, it does not grade style. - -const BIG_FILES: Record = { - "components/AdminUsers.tsx": 993, - "components/Channels.tsx": 904, - "components/Feed.tsx": 934, - "components/Home.tsx": 1136, - "components/IssueComposer.tsx": 861, - "components/PlayerModal.tsx": 1142, - "components/Playlists.tsx": 1069, - "components/PlexPlayer.tsx": 1984, - "components/PlexPlaylistView.tsx": 830, - "components/plexViews.tsx": 941, - "components/SettingsPanel.tsx": 885, - "components/Welcome.tsx": 944, - "lib/plexPlayerHooks.ts": 922, -}; - -const COMPLEX: Record = { - "App.tsx::App": 28, - "components/AccountPanel.tsx::AccountPanel": 29, - "components/AdminLogs.tsx::AdminLogs": 35, - "components/ChannelPage.tsx::ChannelPage": 91, - "components/Channels.tsx::Channels": 53, - "components/ConfigPanel.tsx::ConfigPanel": 24, - "components/ConfigPanel.tsx::Field": 28, - "components/DownloadCenter.tsx::DownloadCenter": 36, - "components/Feed.tsx::Feed": 64, - "components/Home.tsx::Hero": 32, - "components/Home.tsx::Home": 34, - "components/Home.tsx::useHomeCardMenu": 21, - "components/IssueComposer.tsx::ComposerBody": 34, - "components/NotificationsPanel.tsx::NotificationRow": 28, - "components/OnboardingWizard.tsx::OnboardingWizard": 21, - "components/PersonalViewsModal.tsx::PersonalViewsModal": 22, - "components/PlayerModal.tsx::PlayerModal": 98, - "components/Playlists.tsx::Playlists": 66, - "components/PlexBrowse.tsx::PlexBrowse": 45, - "components/PlexCollectionView.tsx::PlexCollectionView": 41, - "components/PlexDiscover.tsx::PlexDiscover": 21, - "components/PlexInfo.tsx::PlexInfo": 65, - "components/PlexPlayer.tsx::onKey": 22, - "components/PlexPlayer.tsx::PlexPlayer": 103, - "components/PlexPlaylistView.tsx::PlexPlaylistView": 22, - "components/PlexSidebar.tsx::PlexSidebar": 32, - "components/PlexStatsOverlay.tsx::sample": 25, - "components/Scheduler.tsx::JobRow": 21, - "components/Scheduler.tsx::Scheduler": 21, - "components/SetupWizard.tsx::SetupWizard": 21, - "components/SyncStatus.tsx::SyncStatus": 22, - "components/VideoCard.tsx::VideoCard": 39, - "components/VideoEditor.tsx::VideoEditor": 49, - "components/WatchPage.tsx::WatchPage": 30, - "components/Welcome.tsx::AuthCard": 39, - "components/Welcome.tsx::onSubmit": 24, - "components/auditColumns.tsx::localDetails": 24, - "components/downloadRow.tsx::DownloadRow": 25, - "components/plexCards.tsx::PlexPosterCard": 58, - "components/plexViews.tsx::PlexPersonView": 30, - "components/plexViews.tsx::PlexSeasonView": 21, - "components/plexViews.tsx::PlexShowView": 50, - "components/ui/AnchoredPopover.tsx::PopoverSurfaceInner": 22, - "components/ui/Combobox.tsx::Combobox": 22, - "components/ui/DataTable.tsx::DataTable": 42, - "components/ui/PanelGroup.tsx::PanelGroup": 23, - "components/ui/SidePanel.tsx::SidePanel": 45, - "components/ui/SortControl.tsx::SortControl": 23, - "components/ui/anchoredPopoverHooks.ts::useAnchoredPopover": 30, - "components/usePlaylistActions.tsx::pushSteps": 23, - "components/usePlexActiveFilters.ts::buildPlexActiveFilters": 25, - "components/useVideoActions.tsx::items": 25, - "lib/api/core.ts::localizeDetail": 48, - "lib/api/core.ts::req": 22, - "lib/api/plex.ts::appendPlexFilters": 26, - "lib/diagnostics.ts::applyDiagnostics": 22, - "lib/keymap.ts::dispatch": 28, - "lib/logHooks.ts::controlAttrs": 21, - "lib/plexDetail.ts::useDetailAside": 24, - "lib/plexPlayerHooks.ts::usePlexTracks": 30, - "lib/urlState.ts::filtersToParams": 24, - "lib/urlState.ts::paramsToFilters": 26, - "lib/useActiveFilters.ts::useActiveFilters": 25, -}; - -describe("file size", () => { - const lines = lineCounts(shippedFiles()); - - it(`no file crosses ${MAX_LINES} lines, and a listed one does not grow`, () => { - const over = Object.entries(lines) - .filter(([f, n]) => n > (BIG_FILES[f] ?? MAX_LINES)) - .map(([f, n]) => `${f}: ${n} lines (ceiling ${BIG_FILES[f] ?? MAX_LINES})`); - expect(over).toEqual([]); - }); - - it("lists no file that is back under the limit (delete it)", () => { - const back = Object.keys(BIG_FILES).filter((f) => (lines[f] ?? 0) <= MAX_LINES); - expect(back).toEqual([]); - }); -}); - -describe("function complexity", async () => { - const actual = await complexityOffenders(); - - it(`no function crosses ${MAX_COMPLEXITY}, and a listed one does not rise`, () => { - const worse = Object.entries(actual) - .filter(([fn, v]) => v > (COMPLEX[fn] ?? MAX_COMPLEXITY)) - .map(([fn, v]) => `${fn}: ${v} (ceiling ${COMPLEX[fn] ?? MAX_COMPLEXITY})`); - expect(worse).toEqual([]); - }); - - it("lists no function that is back under the limit, or gone (delete its row)", () => { - expect(Object.keys(COMPLEX).filter((fn) => !(fn in actual))).toEqual([]); - }); -}); - -describe("function names (the ratchet's keys)", () => { - it("names an unnamed function by what it is bound to, through a wrapping call", () => { - const code = [ - "const onSave = useCallback(() => 1);", - "useEffect(() => {});", - "const o = { k: function () {} };", - "const v = useMemo(() => g(() => 2));", - ].join(" "); - expect(boundNamesIn(code)).toEqual(["onSave", "useEffect callback", "k", "v", "g callback"]); - }); -}); diff --git a/frontend/src/lib/sourceSize.testkit.ts b/frontend/src/lib/sourceSize.testkit.ts deleted file mode 100644 index 9cf51a4c..00000000 --- a/frontend/src/lib/sourceSize.testkit.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { readdirSync, readFileSync } from "node:fs"; -import { fileURLToPath } from "node:url"; -import { ESLint, Linter, type Rule } from "eslint"; -import tseslint from "typescript-eslint"; - -// Measures the shipped frontend source for the size/complexity ratchet (E32): lines per file, and the -// cyclomatic complexity of every function over the limit. Used by sourceSize.contract.test.ts. - -export const MAX_LINES = 800; -export const MAX_COMPLEXITY = 20; - -const SRC = fileURLToPath(new URL("../", import.meta.url)); - -/** src-relative paths of everything the build ships (no tests, no test kits). */ -export function shippedFiles(): string[] { - return (readdirSync(SRC, { recursive: true }) as string[]) - .map((p) => p.split("\\").join("/")) - .filter( - (p) => /\.(ts|tsx)$/.test(p) && !/\.(test|testkit)\.tsx?$/.test(p) && !p.endsWith(".d.ts"), - ) - .sort(); -} - -export function lineCounts(files: string[]): Record { - const out: Record = {}; - for (const f of files) out[f] = readFileSync(SRC + f, "utf8").split("\n").length; - return out; -} - -/** `file::function` → its complexity, for every function over MAX_COMPLEXITY. Keyed by NAME, not by - * rank in the file: a ranked list let one function grow while another in the same file shrank by as - * much (E32 review). ESLint names declarations and methods; a function it leaves unnamed takes the - * name it is bound to (`const onSave = () => …`, `{ onSave: () => … }`), through a wrapping call - * (`const onSave = useCallback(() => …)`, round 3), else ` callback` (`useEffect - * callback`), else `(anonymous)`. A name taken twice in a file gets ` 2`, ` 3` in source order — - * left only for repeated unbound callbacks (two `useEffect`s), where the order IS the identity. A - * bare rule run (no type info, no other rules), so it costs a couple of seconds, not the full lint. */ -type Node = Rule.Node; -const ident = (n: { type: string; name?: string } | null | undefined): string | undefined => - n?.type === "Identifier" ? n.name : undefined; -function boundName(p: Node): string | undefined { - switch (p.type) { - case "VariableDeclarator": - return ident(p.id); - case "Property": - case "PropertyDefinition": - return ident(p.key); - case "AssignmentExpression": - return ident(p.left.type === "MemberExpression" ? p.left.property : p.left); - case "CallExpression": { - // a wrapped function takes the wrapper's binding: `const onSave = useCallback(() => …)` - const outer = p.parent as Node | null; - const bound = outer && outer.type !== "CallExpression" ? boundName(outer) : undefined; - if (bound) return bound; - const callee = ident(p.callee.type === "MemberExpression" ? p.callee.property : p.callee); - return callee && `${callee} callback`; - } - default: - return undefined; - } -} -/** The name each function is bound to, reported where `complexity` reports it (the function's HEAD: - * the `=>` of an arrow, the key of a method, else the function's start) so the two line up. */ -const functionNames: Rule.RuleModule = { - create(ctx) { - const visit = (node: Node) => { - const p = node.parent as Node | null; - const name = p && boundName(p); - if (!p || !name) return; - const head = - node.type === "ArrowFunctionExpression" - ? ctx.sourceCode.getTokenBefore(node.body, (t) => t.value === "=>")!.loc.start - : p.type === "Property" || - p.type === "MethodDefinition" || - p.type === "PropertyDefinition" - ? p.loc!.start - : node.loc!.start; - ctx.report({ loc: head, message: name }); - }; - return { ArrowFunctionExpression: visit, FunctionExpression: visit }; - }, -}; - -/** The name `functionNames` gives each unnamed function in `code`, in source order (its own test). */ -export function boundNamesIn(code: string): string[] { - return new Linter() - .verify( - code, - [ - { - files: ["**/*.tsx"], - languageOptions: { - parser: tseslint.parser, - parserOptions: { ecmaFeatures: { jsx: true } }, - }, - plugins: { size: { rules: { "function-names": functionNames } } }, - rules: { "size/function-names": "error" }, - }, - ], - "probe.tsx", - ) - .map((m) => m.message); -} - -export async function complexityOffenders(): Promise> { - const eslint = new ESLint({ - cwd: fileURLToPath(new URL("../../", import.meta.url)), - overrideConfigFile: true, - overrideConfig: [ - { - files: ["src/**/*.{ts,tsx}"], - ignores: ["**/*.test.*", "**/*.testkit.*"], - languageOptions: { - parser: tseslint.parser, - parserOptions: { ecmaFeatures: { jsx: true } }, - }, - plugins: { size: { rules: { "function-names": functionNames } } }, - rules: { complexity: ["error", MAX_COMPLEXITY], "size/function-names": "error" }, - }, - ], - }); - const out: Record = {}; - for (const r of await eslint.lintFiles(["src/**/*.{ts,tsx}"])) { - const file = r.filePath - .split("\\") - .join("/") - .replace(/^.*\/frontend\/src\//, ""); - const at = (m: { line: number; column: number }) => `${m.line}:${m.column}`; - const bound = new Map( - r.messages.filter((m) => m.ruleId === "size/function-names").map((m) => [at(m), m.message]), - ); - for (const m of r.messages) { - if (m.ruleId !== "complexity") continue; - const name = /'([^']+)'/.exec(m.message)?.[1] ?? bound.get(at(m)) ?? "(anonymous)"; - let key = `${file}::${name}`; - for (let n = 2; key in out; n++) key = `${file}::${name} ${n}`; - out[key] = Number(/complexity of (\d+)/.exec(m.message)?.[1]); - } - } - return out; -} diff --git a/scripts/audit_metrics.py b/scripts/audit_metrics.py index f7bc80ce..5a55670a 100644 --- a/scripts/audit_metrics.py +++ b/scripts/audit_metrics.py @@ -24,8 +24,8 @@ BE_APP = ROOT / "backend" / "app" BE_TESTS = ROOT / "backend" / "tests" FE_SRC = ROOT / "frontend" / "src" BIG_FILE = 800 -BE_COMPLEXITY = 10 # ruff's C901 default, the backend ratchet's limit -FE_COMPLEXITY = 20 # the frontend ratchet's limit +BE_COMPLEXITY = 10 # ruff's C901 default +FE_COMPLEXITY = 20 # the frontend's line (E32) EPIC_CODE = re.compile(r"\b(E\d{2}|R\d{1,2})(\s?S\d+)?\b") NARRATION = re.compile(r"\b(used to|no longer)\b", re.IGNORECASE) diff --git a/scripts/fe_complexity.mjs b/scripts/fe_complexity.mjs index 64f8a806..171e98a7 100644 --- a/scripts/fe_complexity.mjs +++ b/scripts/fe_complexity.mjs @@ -3,7 +3,7 @@ // one JSON object: {"limit": N, "over": count, "max": highest}. Shipped code only — tests and test // kits are lists of cases, not logic anyone has to change. // -// node scripts/fe_complexity.mjs [limit] (default 20, the frontend's ratchet limit) +// node scripts/fe_complexity.mjs [limit] (default 20, the frontend's line since E32) import { readdirSync, readFileSync } from "node:fs"; import { createRequire } from "node:module"; import { join } from "node:path";