Files
siftlode/frontend/src/lib/sourceSize.contract.test.ts
T
peter cca3315836 feat!: remove direct messaging (chat)
E2EE direct messages, the chat dock, the Messages module, the message
WebSocket and its realtime registry, and the messages / message_keys
tables (migration 0097; the downgrade restores the empty schema). Prod
held two welcome notes, no message between people and one key — never
used (owner decision, E34). WebSocket now has no owner module: the lint
bans it everywhere. Privacy policy updated: it no longer promises E2EE
for a feature that is gone.
2026-10-05 00:19:38 +02:00

149 lines
6.3 KiB
TypeScript

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<string, number> = {
"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<string, number> = {
"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"]);
});
});