fix(gate): knip sees the watch entry, the build must emit watch.html, and two hand copies are held
- The raw-source contract tests read files from disk (rawSources.testkit) instead of import.meta.glob, which knip counted as importing every src file — its unused-files lane could never name an orphan. knip now names src/watchMain.tsx as an entry (registered in docs/gates.md). - vite build fails when index.html or watch.html is not emitted. - The watch-entry import walk follows dynamic import() too. - audit_metrics.py's plan-code patterns are held equal to the guard's. - CrashBoundary is domain-free: it moves into components/ui.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { Component, type ErrorInfo, type ReactNode } from "react";
|
||||
import { errorAttrs, log } from "../lib/log";
|
||||
import { reloadingForNewBuild } from "../lib/staleBuild";
|
||||
import { Button } from "./ui/Button";
|
||||
import { errorAttrs, log } from "../../lib/log";
|
||||
import { reloadingForNewBuild } from "../../lib/staleBuild";
|
||||
import { Button } from "./Button";
|
||||
|
||||
export interface CrashCopy {
|
||||
title: string;
|
||||
|
||||
@@ -41,7 +41,7 @@ Counter-examples, deliberately NOT here: `PosterTile` (knows about Plex montage
|
||||
| actions & inputs | `Button`, `IconButton`, `Pressable` (`selectable` when its text is a title or a name), `Chip` + `chipStyles`, `DragHandle` + `OrderButtons` / `useOrderItems` (the non-drag reorder, WCAG 2.5.7) (one per button family, docs/DESIGN.md; a raw `<button>` or a bare `FocusSafeButton` outside the control primitives — eslint `BUTTON_PRIMITIVES` — is a lint error), `Input` + `fieldStyles`, `NativeControls`, `MenuCheck` (the drawn check of a menu row or a whole-row checkbox button), `SelectTrigger` (the field family; a raw `<input>` is a lint error), `useReturnFocus`, `surfaces` (the B5 glass roles; a bare glass class is a lint error), `Badge` (a static label), `LinkButton` / `IconLink` (a link wearing the button face) over `buttonStyles`, `Skeleton` (B8 first loads, through `LoadingState shape`), `NavRail` (B7; `NavRailGroup` folds a set of rare pages into one row with a panel), `form`, `DraftSaveBar`, `Combobox`, `FilterSelect`, `SegmentedToggle`, `SortControl`, `ColumnSortControl`, `PriorityStepper`, `UndoToolbar` |
|
||||
| data display | `DataTable` (+ `progressiveRows`: a long page mounts in steps), `tableHeader` + `tableAlign`, `HiddenFilterCue` (names a filter whose column is off screen), `Pager` + `pagerContent`, `VirtualGrid` + `gridColumns` (the column/width maths, incl. the ultrawide `maxCols` cap), `readAhead`, `Carousel`, `MediaCard` + `mediaCardStyles` (the one media card, docs/DESIGN.md "Media card") + `MediaPreview` / `useMediaPreview` (its mouse-rest preview), `NavGrid` (a plain grid, list or table body the keyboard walks — `DataTable`, `VirtualGrid` and `Carousel` take `keyboardNav`), `Avatar`, `CountBadge`, `ItemCount`, `FilterChipGroup`, `RelativeTime` ("3d ago" with the exact minute on hover, docs/DESIGN.md "Copy"), `ClampedText` (a long text clamped to a few lines; More / Less only when it is cut), `StatTiles` + `StatTile` / `StatBars` (number tiles, a breakdown as horizontal bars) |
|
||||
| page & panel structure | `PageShell` + `pageToolbarSlot` + `pageHeading`, `PageScroller`, `Headings` (`PageHeading`, `SectionLabel`), `SkipLink` + `landmarks`, `FilterFold` (B4), `scrollFold` (`useScrollFold`: a fixed header that folds once its content scrolls), `PanelGroup`, `PanelGroups`, `Tabs`, `BackToTop` |
|
||||
| state & messaging | `QueryState`, `Banner`, `InlineNotice` |
|
||||
| state & messaging | `QueryState`, `Banner`, `InlineNotice`, `CrashBoundary` |
|
||||
|
||||
## The size contract (E10 S1c)
|
||||
|
||||
|
||||
@@ -13,15 +13,12 @@
|
||||
// z-overlay`) was never checked at all, and a new viewer copying it would have slipped past
|
||||
// (found by a later review).
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readSources } from "../../lib/rawSources.testkit";
|
||||
|
||||
// BOTH patterns: `../**/*.tsx` from this directory does not match the siblings in `ui/` itself, so
|
||||
// ui/Modal.tsx — the overlay behind every dialog in the app — was never checked by this guard at all
|
||||
// (found by a later review).
|
||||
const sources = import.meta.glob(["../**/*.tsx", "./*.tsx"], {
|
||||
query: "?raw",
|
||||
import: "default",
|
||||
eager: true,
|
||||
});
|
||||
const sources = readSources(__dirname, ["../**/*.tsx", "./*.tsx"]);
|
||||
|
||||
// Overlays with nothing a toast could be about: a lazy chunk's black placeholder while the player
|
||||
// loads, and the signed-out landing page's screenshot zoom (the app's Toaster is not even mounted
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readSources } from "../../lib/rawSources.testkit";
|
||||
|
||||
// docs/DESIGN.md "Background menu": which pages have one is the owner's call (2026-09-26, scope A) —
|
||||
// the list pages whose toolbar sorts, filters or switches a view: the feed, the Plex library, the
|
||||
// channel manager, a playlist and the notification center. The tab-only pages (settings, config,
|
||||
// stats, users, downloads) keep the browser's menu. A page that starts rendering one fails here until
|
||||
// that decision is taken again.
|
||||
const files = import.meta.glob(["../**/*.tsx", "!../**/*.test.*", "!../design-lab/**", "!./**"], {
|
||||
query: "?raw",
|
||||
import: "default",
|
||||
eager: true,
|
||||
}) as Record<string, string>;
|
||||
const files = readSources(__dirname, [
|
||||
"../**/*.tsx",
|
||||
"!../**/*.test.*",
|
||||
"!../design-lab/**",
|
||||
"!./**",
|
||||
]) as Record<string, string>;
|
||||
|
||||
describe("the pages with a background menu", () => {
|
||||
it("are the five the owner chose", () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readSources } from "../../lib/rawSources.testkit";
|
||||
|
||||
// docs/DESIGN.md "Text stays selectable": a click surface whose text is a title or a name (a card, a
|
||||
// row, a person, a channel, a playlist, a log line) is `<Pressable selectable`, so its words select and
|
||||
@@ -8,11 +9,11 @@ import { describe, expect, it } from "vitest";
|
||||
// pinned here with the reason: a new Pressable, or one that lost its flag, fails this test until
|
||||
// someone decides.
|
||||
|
||||
const files = import.meta.glob(["../**/*.tsx", "!../**/*.test.*", "!../design-lab/**"], {
|
||||
query: "?raw",
|
||||
import: "default",
|
||||
eager: true,
|
||||
}) as Record<string, string>;
|
||||
const files = readSources(__dirname, [
|
||||
"../**/*.tsx",
|
||||
"!../**/*.test.*",
|
||||
"!../design-lab/**",
|
||||
]) as Record<string, string>;
|
||||
|
||||
/** file → [how many plain Pressables, why they are not titles]. */
|
||||
const PLAIN: Record<string, [number, string]> = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readSources } from "../../lib/rawSources.testkit";
|
||||
|
||||
// The platform layer (`components/ui/`) may not import a feature component or a domain API slice —
|
||||
// eslint's `no-restricted-imports` enforces that. It cannot see an i18n KEY, though, and two
|
||||
@@ -17,11 +18,7 @@ const PLATFORM_NAMESPACES = new Set([
|
||||
"time",
|
||||
]);
|
||||
|
||||
const files = import.meta.glob("./**/*.{ts,tsx}", {
|
||||
query: "?raw",
|
||||
import: "default",
|
||||
eager: true,
|
||||
});
|
||||
const files = readSources(__dirname, "./**/*.{ts,tsx}");
|
||||
// `t("ns.…")` and `t(\`ns.…\`)` — the namespace is everything before the first dot.
|
||||
const KEY = /\bt\(\s*["`]([A-Za-z]+)\./g;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user