diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d27686..94da993 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,9 +10,14 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + with: + fetch-depth: 2 - uses: actions/setup-node@v6 with: node-version: '22' + - uses: actions/setup-python@v6 + with: + python-version: '3.12' - name: Prepare pnpm run: | corepack enable @@ -22,6 +27,5 @@ jobs: corepack prepare pnpm@10.32.1 --activate fi - run: pnpm install --frozen-lockfile - - run: pnpm lint - - run: pnpm test - - run: pnpm docs:check + - run: python -m pip install lizard==1.23.0 + - run: pnpm quality diff --git a/AGENTS.md b/AGENTS.md index d83b4d8..9556b15 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,6 +43,7 @@ pnpm build # Astro production static build pnpm test # vitest run pnpm lint # biome check . pnpm typecheck # astro check +pnpm quality # complete CI code-health gate # Catalog (CI-only credentials; checked-in catalog needs no key) pnpm run build:catalog # fetch + process (no NER) diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md index a2600a8..6fe0cc0 100644 --- a/PROJECT_STATUS.md +++ b/PROJECT_STATUS.md @@ -1,6 +1,6 @@ # LoopTV — PROJECT STATUS -Last updated: 2026-08-09 +Last updated: 2026-08-12 > Detailed feature inventory and timeline live in > [docs/product/features.md](docs/product/features.md). @@ -22,6 +22,10 @@ catalog freshness, and documentation hygiene. ## Timeline +- **2026-08-12:** Adopted the Fleet code-health contract with truthful + whole-library coverage, unused-code, cycle, complexity, duplication, + dependency, suppression, build, docs, and repository-hygiene ratchets in CI; + removed three unused public exports and recorded existing debt in GitHub. - **2026-08-09:** Adopted the verified Ultracite-backed Biome baseline through the existing read-only check, with explicit compatibility exceptions and no source rewrite, catalog rebuild, production dependency, playback, or deploy @@ -43,6 +47,7 @@ catalog freshness, and documentation hygiene. - Owned editorial product changelog at `/changelog`. - Exact Ultracite-backed Biome presets with explicit local compatibility exceptions; `pnpm check` remains non-writing. +- One `pnpm quality` command reproduces the complete hosted code-health gate. - The complete shipped feature inventory lives in [docs/product/features.md](docs/product/features.md). diff --git a/docs/development/setup.md b/docs/development/setup.md index 780e9a2..f8599f7 100644 --- a/docs/development/setup.md +++ b/docs/development/setup.md @@ -35,6 +35,7 @@ pnpm lint # biome check . pnpm format # biome format --write . pnpm typecheck # astro check pnpm check # biome check . (alias of lint) +pnpm quality # full code-health gate used by CI ``` `pnpm build` prerenders every public route and generated endpoint into `dist/`. diff --git a/docs/development/testing.md b/docs/development/testing.md index f716abd..33ec012 100644 --- a/docs/development/testing.md +++ b/docs/development/testing.md @@ -12,6 +12,7 @@ pnpm test # vitest run (unit) pnpm test:coverage # vitest with v8 coverage pnpm lint # biome check . pnpm typecheck # tsc --noEmit +pnpm quality # complete local/CI code-health gate ``` Playwright browser tests exist (`tests/example.spec.ts`, `tests/mobile.spec.ts`, @@ -25,8 +26,16 @@ Vitest is configured in `vitest.config.ts` and includes: - `src/**/*.test.ts` - `scripts/__tests__/**/*.test.ts` -Coverage targets `src/lib/**/*.ts` with thresholds -`lines: 80, functions: 80, branches: 70, statements: 80`. +Coverage targets the complete `src/lib/**/*.ts` surface, including modules with +no current tests. The ratchet starts at the coverage the suite proves: +`lines: 70, functions: 67, branches: 58, statements: 63`. Thresholds may move +up, never silently down. + +`pnpm quality` also prevents regressions in unused code, import cycles, +complexity, exact duplication, critical/high dependency advisories, suppression +markers, formatting, documentation, build output, and repository hygiene. +Existing measurable debt is recorded in GitHub issue #38 and checked as a +ceiling rather than described as clean. ## What's tested diff --git a/docs/operations/jobs/ci.md b/docs/operations/jobs/ci.md index dc43601..0648fcb 100644 --- a/docs/operations/jobs/ci.md +++ b/docs/operations/jobs/ci.md @@ -16,17 +16,15 @@ Single job, `ubuntu-latest`, Node 22: 2. Setup Node 22. 3. Prepare pnpm (corepack, pinned version from `packageManager`). 4. `pnpm install --frozen-lockfile`. -5. `pnpm lint` (Biome check). -6. `pnpm test` (Vitest run). - -No build step — that's covered by [weekly-quality.md](weekly-quality.md) and -the deploy workflows. CI is the fast feedback gate. +5. Install the pinned Lizard complexity analyzer. +6. `pnpm quality`: formatting, lint, types, coverage, unused code, complexity, + duplication, cycles, dependency advisories, suppressions, docs, build, and + repository hygiene. ## What's not here - No catalog rebuild (that's [fetch-catalog-sources.md](fetch-catalog-sources.md) + [build-catalog.md](build-catalog.md)). - No deploy (that's [deploy.md](deploy.md)). -- No docs validation in this workflow — `pnpm docs:check` runs as a CI - step here, and the dedicated `docs.yml` workflow runs the strict check on - docs-only changes. See [../runbooks/](../runbooks/). +- The dedicated `docs.yml` workflow still runs the strict documentation check + on docs-only changes. See [../runbooks/](../runbooks/). diff --git a/knip.json b/knip.json deleted file mode 100644 index 00a93d7..0000000 --- a/knip.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "$schema": "https://unpkg.com/knip@latest/schema.json", - "entry": ["src/pages/**/*.astro"], - "project": ["**/*.{astro,ts,tsx,js,jsx,mjs,cjs,mts}"], - "ignore": [ - "**/dist/**", - "**/build/**", - "**/.next/**", - "**/.astro/**", - "**/.output/**", - "**/node_modules/**", - "**/coverage/**", - "**/public/**", - "**/*.config.{js,ts,mjs,cjs}", - "**/eslint.config.*", - "**/blume.config.*", - "**/docs-blume/**", - "scripts/**", - "tests/e2e/**", - "test/**", - "**/*.test.{ts,tsx,js,mjs}", - "**/*.spec.{ts,tsx,js,mjs}", - "**/*.d.mts", - "**/*.d.ts", - "**/__tests__/**", - "**/playground/**", - "**/examples/**" - ], - "ignoreDependencies": [ - "typescript", - "@types/*", - "biome", - "@biomejs/biome", - "prettier", - "prettier-plugin-tailwindcss", - "husky", - "lint-staged", - "vitest", - "@vitest/*", - "@cloudflare/vitest-pool-workers", - "playwright", - "@playwright/test", - "drizzle-kit", - "wrangler", - "opennextjs-cloudflare", - "tailwindcss", - "@tailwindcss/*", - "lightningcss", - "tsx", - "size-limit", - "knip", - "blume", - "@blume/*", - "stockfish", - "astro", - "@astrojs/*", - "@shikijs/*", - "shiki", - "@vercel/node", - "three-stdlib", - "expo", - "expo-*", - "@expo/*", - "react-native", - "@react-native/*" - ], - "ignoreBinaries": [ - "next", - "astro", - "vite", - "wrangler", - "drizzle-kit", - "tsx", - "biome", - "playwright", - "vitest", - "knip", - "eslint", - "prettier", - "blume", - "yt-dlp", - "tailscale", - "gemini", - "fallow", - "security" - ], - "ignoreIssues": { - "src/app/api/**/route.ts": ["exports", "types"], - "src/lib/actions/**": ["exports", "types"], - "lib/actions/**": ["exports", "types"], - "**/schema.ts": ["exports", "types"], - "**/db/schema*.ts": ["exports", "types"], - "**/commands/**": ["exports", "types"], - "**/agent-edge.*": ["exports", "types"], - "**/foundry-monitoring.ts": ["exports", "types"], - "**/analytics.ts": ["exports", "types"], - "**/api-timing.ts": ["exports", "types"], - "scripts/**": ["exports", "types"] - } -} diff --git a/knip.jsonc b/knip.jsonc new file mode 100644 index 0000000..9a48bc3 --- /dev/null +++ b/knip.jsonc @@ -0,0 +1,20 @@ +{ + "$schema": "https://unpkg.com/knip@6/schema.json", + "entry": [ + "src/pages/**/*.{astro,ts}", + "scripts/*.mjs", + "astro.config.ts", + "vitest.config.ts" + ], + "project": ["**/*.{astro,ts,tsx,js,jsx,mjs,cjs,mts}"], + "ignore": [ + // Optional docs presentation config and the runtime-copied yt-dlp boundary. + "blume.config.ts" + ], + "ignoreDependencies": [ + // CSS-only dependency and the checker-spawned CLI are not visible in the import graph. + "jscpd", + "tailwindcss" + ], + "ignoreBinaries": ["blume", "yt-dlp"] +} diff --git a/package.json b/package.json index e9da907..7731d06 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,15 @@ "docs:build": "blume build", "docs:dev": "blume dev", "knip": "knip --no-exit-code --reporter symbols", - "knip:strict": "knip --reporter symbols" + "knip:strict": "knip --reporter symbols", + "quality:unused": "node scripts/check-code-health.mjs unused", + "quality:complexity": "node scripts/check-code-health.mjs complexity", + "quality:duplication": "node scripts/check-code-health.mjs duplication", + "quality:cycles": "node scripts/check-code-health.mjs cycles", + "quality:dependencies": "node scripts/check-code-health.mjs dependencies", + "quality:suppressions": "node scripts/check-code-health.mjs suppressions", + "quality:hygiene": "node scripts/check-code-health.mjs hygiene", + "quality": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm test:coverage && pnpm quality:unused && pnpm quality:complexity && pnpm quality:duplication && pnpm quality:cycles && pnpm quality:dependencies && pnpm quality:suppressions && pnpm docs:check && pnpm build && pnpm quality:hygiene" }, "dependencies": { "@saas-maker/feedback": "^0.2.1", @@ -49,6 +57,7 @@ "@vitejs/plugin-react": "4.7.0", "@vitest/coverage-v8": "^4.1.10", "astro": "^5.16.6", + "jscpd": "5.0.14", "knip": "^6.29.0", "lightningcss": "^1.33.0", "tailwindcss": "^4.3.3", @@ -58,5 +67,16 @@ "vitest": "4.1.10", "wrangler": "4.114.0" }, - "packageManager": "pnpm@10.33.2" + "packageManager": "pnpm@10.33.2", + "pnpm": { + "overrides": { + "dompurify@<=3.4.12": "3.4.13", + "fast-uri@<3.1.5": "3.1.5", + "js-yaml@>=4.0.0 <4.3.1": "4.3.1", + "nanoid@<3.3.17": "3.3.17", + "postcss@<=8.5.22": "8.5.23", + "sharp@<0.35.0": "0.35.3", + "undici@>=7.0.0 <7.29.0": "7.29.0" + } + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2fec7cc..c0392ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,15 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + dompurify@<=3.4.12: 3.4.13 + fast-uri@<3.1.5: 3.1.5 + js-yaml@>=4.0.0 <4.3.1: 4.3.1 + nanoid@<3.3.17: 3.3.17 + postcss@<=8.5.22: 8.5.23 + sharp@<0.35.0: 0.35.3 + undici@>=7.0.0 <7.29.0: 7.29.0 + importers: .: @@ -63,6 +72,9 @@ importers: astro: specifier: ^5.16.6 version: 5.18.2(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.33.0)(rollup@4.62.2)(typescript@6.0.3)(yaml@2.9.0) + jscpd: + specifier: 5.0.14 + version: 5.0.14 knip: specifier: ^6.29.0 version: 6.29.0 @@ -376,9 +388,6 @@ packages: '@emnapi/core@1.11.2': resolution: {integrity: sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==} - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@emnapi/runtime@1.11.2': resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} @@ -860,22 +869,16 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - '@img/sharp-darwin-arm64@0.35.2': resolution: {integrity: sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] + '@img/sharp-darwin-arm64@0.35.3': + resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] os: [darwin] '@img/sharp-darwin-x64@0.35.2': @@ -884,24 +887,30 @@ packages: cpu: [x64] os: [darwin] + '@img/sharp-darwin-x64@0.35.3': + resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + '@img/sharp-freebsd-wasm32@0.35.2': resolution: {integrity: sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==} engines: {node: '>=20.9.0'} os: [freebsd] - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} - cpu: [arm64] - os: [darwin] + '@img/sharp-freebsd-wasm32@0.35.3': + resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==} + engines: {node: '>=20.9.0'} + os: [freebsd] '@img/sharp-libvips-darwin-arm64@1.3.1': resolution: {integrity: sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} - cpu: [x64] + '@img/sharp-libvips-darwin-arm64@1.3.2': + resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==} + cpu: [arm64] os: [darwin] '@img/sharp-libvips-darwin-x64@1.3.1': @@ -909,11 +918,10 @@ packages: cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} - cpu: [arm64] - os: [linux] - libc: [glibc] + '@img/sharp-libvips-darwin-x64@1.3.2': + resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==} + cpu: [x64] + os: [darwin] '@img/sharp-libvips-linux-arm64@1.3.1': resolution: {integrity: sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==} @@ -921,9 +929,9 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} - cpu: [arm] + '@img/sharp-libvips-linux-arm64@1.3.2': + resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} + cpu: [arm64] os: [linux] libc: [glibc] @@ -933,9 +941,9 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} - cpu: [ppc64] + '@img/sharp-libvips-linux-arm@1.3.2': + resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} + cpu: [arm] os: [linux] libc: [glibc] @@ -945,9 +953,9 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} - cpu: [riscv64] + '@img/sharp-libvips-linux-ppc64@1.3.2': + resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} + cpu: [ppc64] os: [linux] libc: [glibc] @@ -957,9 +965,9 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} - cpu: [s390x] + '@img/sharp-libvips-linux-riscv64@1.3.2': + resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} + cpu: [riscv64] os: [linux] libc: [glibc] @@ -969,9 +977,9 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} - cpu: [x64] + '@img/sharp-libvips-linux-s390x@1.3.2': + resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} + cpu: [s390x] os: [linux] libc: [glibc] @@ -981,11 +989,11 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} - cpu: [arm64] + '@img/sharp-libvips-linux-x64@1.3.2': + resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} + cpu: [x64] os: [linux] - libc: [musl] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.3.1': resolution: {integrity: sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==} @@ -993,9 +1001,9 @@ packages: os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} - cpu: [x64] + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} + cpu: [arm64] os: [linux] libc: [musl] @@ -1005,12 +1013,11 @@ packages: os: [linux] libc: [musl] - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] + '@img/sharp-libvips-linuxmusl-x64@1.3.2': + resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} + cpu: [x64] os: [linux] - libc: [glibc] + libc: [musl] '@img/sharp-linux-arm64@0.35.2': resolution: {integrity: sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==} @@ -1019,10 +1026,10 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] + '@img/sharp-linux-arm64@0.35.3': + resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] os: [linux] libc: [glibc] @@ -1033,10 +1040,10 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ppc64] + '@img/sharp-linux-arm@0.35.3': + resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==} + engines: {node: '>=20.9.0'} + cpu: [arm] os: [linux] libc: [glibc] @@ -1047,10 +1054,10 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [riscv64] + '@img/sharp-linux-ppc64@0.35.3': + resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] os: [linux] libc: [glibc] @@ -1061,10 +1068,10 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] + '@img/sharp-linux-riscv64@0.35.3': + resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] os: [linux] libc: [glibc] @@ -1075,10 +1082,10 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] + '@img/sharp-linux-s390x@0.35.3': + resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} + engines: {node: '>=20.9.0'} + cpu: [s390x] os: [linux] libc: [glibc] @@ -1089,12 +1096,12 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] + '@img/sharp-linux-x64@0.35.3': + resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} + engines: {node: '>=20.9.0'} + cpu: [x64] os: [linux] - libc: [musl] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.35.2': resolution: {integrity: sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==} @@ -1103,10 +1110,10 @@ packages: os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] + '@img/sharp-linuxmusl-arm64@0.35.3': + resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} + engines: {node: '>=20.9.0'} + cpu: [arm64] os: [linux] libc: [musl] @@ -1117,25 +1124,30 @@ packages: os: [linux] libc: [musl] - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] + '@img/sharp-linuxmusl-x64@0.35.3': + resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] '@img/sharp-wasm32@0.35.2': resolution: {integrity: sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==} engines: {node: '>=20.9.0'} + '@img/sharp-wasm32@0.35.3': + resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} + engines: {node: '>=20.9.0'} + '@img/sharp-webcontainers-wasm32@0.35.2': resolution: {integrity: sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==} engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [win32] + '@img/sharp-webcontainers-wasm32@0.35.3': + resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] '@img/sharp-win32-arm64@0.35.2': resolution: {integrity: sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==} @@ -1143,10 +1155,10 @@ packages: cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] + '@img/sharp-win32-arm64@0.35.3': + resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==} + engines: {node: '>=20.9.0'} + cpu: [arm64] os: [win32] '@img/sharp-win32-ia32@0.35.2': @@ -1155,10 +1167,10 @@ packages: cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] + '@img/sharp-win32-ia32@0.35.3': + resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==} + engines: {node: ^20.9.0} + cpu: [ia32] os: [win32] '@img/sharp-win32-x64@0.35.2': @@ -1167,6 +1179,12 @@ packages: cpu: [x64] os: [win32] + '@img/sharp-win32-x64@0.35.3': + resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -2154,8 +2172,8 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.4.12: - resolution: {integrity: sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==} + dompurify@3.4.13: + resolution: {integrity: sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==} domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -2245,8 +2263,8 @@ packages: fast-string-width@3.0.2: resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - fast-uri@3.1.4: - resolution: {integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==} + fast-uri@3.1.5: + resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} @@ -2419,8 +2437,46 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.3.0: - resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} + hasBin: true + + jscpd-darwin-arm64@5.0.14: + resolution: {integrity: sha512-Ojjl79SBuj9tEW6WbjZ1a/1ZOR89dneH9yLQYQu8WyWaQownttnx7RYFEHU6aGhS4jIvwUEbr+1wxzFTb37cwg==} + cpu: [arm64] + os: [darwin] + + jscpd-darwin-x64@5.0.14: + resolution: {integrity: sha512-DxFg5XvjMZ81iVeqillnM5apqcGCfNTbroNF+mPLr7RkHLGH6mudLgtO+ILL/hfpZXy1bF9oIY5BSudPmN/k9A==} + cpu: [x64] + os: [darwin] + + jscpd-linux-arm64-gnu@5.0.14: + resolution: {integrity: sha512-1uw+XBHEt9pONXNICSp5HpaVWPjG6mQ6deDXaq9Yb0xCNJkX4/8gmn0vhzekIyZD2DspRYKPUolbDsqm/HEdYg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + jscpd-linux-x64-gnu@5.0.14: + resolution: {integrity: sha512-dFTbyyrm+Z9pcXIVzJQCw8QAgiNqIiO69sm4AfA7/wFdPoizoVzjhaXsYXcSV4bs0aoPiWbNazg0J0HgslT/5A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + jscpd-linux-x64-musl@5.0.14: + resolution: {integrity: sha512-SayS7qQJvixyy9eR0+UjepkTsUUwqvlsiuSxfIdHgG2qzqoh/thnkgiu4By8fsiiDpQONsQrRrZDwHRQ3GDrBQ==} + cpu: [x64] + os: [linux] + libc: [musl] + + jscpd-windows-x64-msvc@5.0.14: + resolution: {integrity: sha512-DqjxlVkUanlahGgY2lY7Zkrau4BUTI+AwWky+bPGK4kSK2AIOaUziY9Q19u8b58idXmJA9FKK98Fuu4ajNXVjQ==} + cpu: [x64] + os: [win32] + + jscpd@5.0.14: + resolution: {integrity: sha512-zge+FPZZAymt2Do5Z0+QHyIn4/XcUhrO/W7of9HcHZfx2AK8++dYhLA1uWtwXj47ml3Of8PbcUW4wUWvYMCc3w==} + engines: {node: '>=18'} hasBin: true jsesc@3.1.0: @@ -2778,8 +2834,8 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + nanoid@3.3.17: + resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -2901,8 +2957,8 @@ packages: engines: {node: '>=20'} hasBin: true - postcss@8.5.10: - resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} engines: {node: ^10 || ^12 || >=14} posthog-js@1.407.3: @@ -3051,14 +3107,19 @@ packages: engines: {node: '>=10'} hasBin: true - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - sharp@0.35.2: resolution: {integrity: sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==} engines: {node: '>=20.9.0'} + sharp@0.35.3: + resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3225,8 +3286,8 @@ packages: undici-types@7.19.2: resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} - undici@7.28.0: - resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} + undici@7.29.0: + resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} engines: {node: '>=20.18.1'} unenv@2.0.0-rc.24: @@ -3719,7 +3780,7 @@ snapshots: hast-util-from-html: 2.0.3 hast-util-to-text: 4.0.2 import-meta-resolve: 4.2.0 - js-yaml: 4.3.0 + js-yaml: 4.3.1 mdast-util-definitions: 6.0.0 rehype-raw: 7.0.0 rehype-stringify: 10.0.1 @@ -4010,11 +4071,6 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.11.2': dependencies: tslib: 2.8.1 @@ -4266,19 +4322,14 @@ snapshots: '@img/colour@1.1.0': {} - '@img/sharp-darwin-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 - optional: true - '@img/sharp-darwin-arm64@0.35.2': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.3.1 optional: true - '@img/sharp-darwin-x64@0.34.5': + '@img/sharp-darwin-arm64@0.35.3': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-darwin-arm64': 1.3.2 optional: true '@img/sharp-darwin-x64@0.35.2': @@ -4286,74 +4337,79 @@ snapshots: '@img/sharp-libvips-darwin-x64': 1.3.1 optional: true + '@img/sharp-darwin-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.2 + optional: true + '@img/sharp-freebsd-wasm32@0.35.2': dependencies: '@img/sharp-wasm32': 0.35.2 optional: true - '@img/sharp-libvips-darwin-arm64@1.2.4': + '@img/sharp-freebsd-wasm32@0.35.3': + dependencies: + '@img/sharp-wasm32': 0.35.3 optional: true '@img/sharp-libvips-darwin-arm64@1.3.1': optional: true - '@img/sharp-libvips-darwin-x64@1.2.4': + '@img/sharp-libvips-darwin-arm64@1.3.2': optional: true '@img/sharp-libvips-darwin-x64@1.3.1': optional: true - '@img/sharp-libvips-linux-arm64@1.2.4': + '@img/sharp-libvips-darwin-x64@1.3.2': optional: true '@img/sharp-libvips-linux-arm64@1.3.1': optional: true - '@img/sharp-libvips-linux-arm@1.2.4': + '@img/sharp-libvips-linux-arm64@1.3.2': optional: true '@img/sharp-libvips-linux-arm@1.3.1': optional: true - '@img/sharp-libvips-linux-ppc64@1.2.4': + '@img/sharp-libvips-linux-arm@1.3.2': optional: true '@img/sharp-libvips-linux-ppc64@1.3.1': optional: true - '@img/sharp-libvips-linux-riscv64@1.2.4': + '@img/sharp-libvips-linux-ppc64@1.3.2': optional: true '@img/sharp-libvips-linux-riscv64@1.3.1': optional: true - '@img/sharp-libvips-linux-s390x@1.2.4': + '@img/sharp-libvips-linux-riscv64@1.3.2': optional: true '@img/sharp-libvips-linux-s390x@1.3.1': optional: true - '@img/sharp-libvips-linux-x64@1.2.4': + '@img/sharp-libvips-linux-s390x@1.3.2': optional: true '@img/sharp-libvips-linux-x64@1.3.1': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + '@img/sharp-libvips-linux-x64@1.3.2': optional: true '@img/sharp-libvips-linuxmusl-arm64@1.3.1': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.4': + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': optional: true '@img/sharp-libvips-linuxmusl-x64@1.3.1': optional: true - '@img/sharp-linux-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64@1.3.2': optional: true '@img/sharp-linux-arm64@0.35.2': @@ -4361,9 +4417,9 @@ snapshots: '@img/sharp-libvips-linux-arm64': 1.3.1 optional: true - '@img/sharp-linux-arm@0.34.5': + '@img/sharp-linux-arm64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.3.2 optional: true '@img/sharp-linux-arm@0.35.2': @@ -4371,9 +4427,9 @@ snapshots: '@img/sharp-libvips-linux-arm': 1.3.1 optional: true - '@img/sharp-linux-ppc64@0.34.5': + '@img/sharp-linux-arm@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.3.2 optional: true '@img/sharp-linux-ppc64@0.35.2': @@ -4381,9 +4437,9 @@ snapshots: '@img/sharp-libvips-linux-ppc64': 1.3.1 optional: true - '@img/sharp-linux-riscv64@0.34.5': + '@img/sharp-linux-ppc64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.3.2 optional: true '@img/sharp-linux-riscv64@0.35.2': @@ -4391,9 +4447,9 @@ snapshots: '@img/sharp-libvips-linux-riscv64': 1.3.1 optional: true - '@img/sharp-linux-s390x@0.34.5': + '@img/sharp-linux-riscv64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.3.2 optional: true '@img/sharp-linux-s390x@0.35.2': @@ -4401,9 +4457,9 @@ snapshots: '@img/sharp-libvips-linux-s390x': 1.3.1 optional: true - '@img/sharp-linux-x64@0.34.5': + '@img/sharp-linux-s390x@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.3.2 optional: true '@img/sharp-linux-x64@0.35.2': @@ -4411,9 +4467,9 @@ snapshots: '@img/sharp-libvips-linux-x64': 1.3.1 optional: true - '@img/sharp-linuxmusl-arm64@0.34.5': + '@img/sharp-linux-x64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.3.2 optional: true '@img/sharp-linuxmusl-arm64@0.35.2': @@ -4421,9 +4477,9 @@ snapshots: '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 optional: true - '@img/sharp-linuxmusl-x64@0.34.5': + '@img/sharp-linuxmusl-arm64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 optional: true '@img/sharp-linuxmusl-x64@0.35.2': @@ -4431,9 +4487,9 @@ snapshots: '@img/sharp-libvips-linuxmusl-x64': 1.3.1 optional: true - '@img/sharp-wasm32@0.34.5': - dependencies: - '@emnapi/runtime': 1.10.0 + '@img/sharp-linuxmusl-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 optional: true '@img/sharp-wasm32@0.35.2': @@ -4441,29 +4497,39 @@ snapshots: '@emnapi/runtime': 1.11.3 optional: true + '@img/sharp-wasm32@0.35.3': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + '@img/sharp-webcontainers-wasm32@0.35.2': dependencies: '@img/sharp-wasm32': 0.35.2 optional: true - '@img/sharp-win32-arm64@0.34.5': + '@img/sharp-webcontainers-wasm32@0.35.3': + dependencies: + '@img/sharp-wasm32': 0.35.3 optional: true '@img/sharp-win32-arm64@0.35.2': optional: true - '@img/sharp-win32-ia32@0.34.5': + '@img/sharp-win32-arm64@0.35.3': optional: true '@img/sharp-win32-ia32@0.35.2': optional: true - '@img/sharp-win32-x64@0.34.5': + '@img/sharp-win32-ia32@0.35.3': optional: true '@img/sharp-win32-x64@0.35.2': optional: true + '@img/sharp-win32-x64@0.35.3': + optional: true + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -5057,7 +5123,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.4 + fast-uri: 3.1.5 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -5127,7 +5193,7 @@ snapshots: html-escaper: 3.0.3 http-cache-semantics: 4.2.0 import-meta-resolve: 4.2.0 - js-yaml: 4.3.0 + js-yaml: 4.3.1 magic-string: 0.30.21 magicast: 0.5.3 mrmime: 2.0.1 @@ -5160,7 +5226,7 @@ snapshots: zod-to-json-schema: 3.25.2(zod@3.25.76) zod-to-ts: 1.2.0(typescript@6.0.3)(zod@3.25.76) optionalDependencies: - sharp: 0.34.5 + sharp: 0.35.3(@types/node@25.6.0) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -5375,7 +5441,7 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.4.12: + dompurify@3.4.13: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -5524,7 +5590,7 @@ snapshots: dependencies: fast-string-truncated-width: 3.0.3 - fast-uri@3.1.4: {} + fast-uri@3.1.5: {} fast-wrap-ansi@0.2.2: dependencies: @@ -5732,10 +5798,37 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@4.3.0: + js-yaml@4.3.1: dependencies: argparse: 2.0.1 + jscpd-darwin-arm64@5.0.14: + optional: true + + jscpd-darwin-x64@5.0.14: + optional: true + + jscpd-linux-arm64-gnu@5.0.14: + optional: true + + jscpd-linux-x64-gnu@5.0.14: + optional: true + + jscpd-linux-x64-musl@5.0.14: + optional: true + + jscpd-windows-x64-msvc@5.0.14: + optional: true + + jscpd@5.0.14: + optionalDependencies: + jscpd-darwin-arm64: 5.0.14 + jscpd-darwin-x64: 5.0.14 + jscpd-linux-arm64-gnu: 5.0.14 + jscpd-linux-x64-gnu: 5.0.14 + jscpd-linux-x64-musl: 5.0.14 + jscpd-windows-x64-msvc: 5.0.14 + jsesc@3.1.0: {} json-schema-traverse@1.0.0: {} @@ -6207,7 +6300,7 @@ snapshots: dependencies: '@cspotcode/source-map-support': 0.8.1 sharp: 0.35.2 - undici: 7.28.0 + undici: 7.29.0 workerd: 1.20260722.1 ws: 8.21.0 youch: 4.1.0-beta.10 @@ -6227,7 +6320,7 @@ snapshots: muggle-string@0.4.1: {} - nanoid@3.3.11: {} + nanoid@3.3.17: {} neotraverse@0.6.18: {} @@ -6375,9 +6468,9 @@ snapshots: optionalDependencies: fsevents: 2.3.2 - postcss@8.5.10: + postcss@8.5.23: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.17 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -6387,7 +6480,7 @@ snapshots: '@posthog/core': 1.45.1 '@posthog/types': 1.398.0 core-js: 3.49.0 - dompurify: 3.4.12 + dompurify: 3.4.13 fflate: 0.4.9 preact: 10.29.7 query-selector-shadow-dom: 1.0.1 @@ -6577,38 +6670,6 @@ snapshots: semver@7.8.5: {} - sharp@0.34.5: - dependencies: - '@img/colour': 1.1.0 - detect-libc: 2.1.2 - semver: 7.7.4 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 - optional: true - sharp@0.35.2: dependencies: '@img/colour': 1.1.0 @@ -6641,6 +6702,40 @@ snapshots: '@img/sharp-win32-ia32': 0.35.2 '@img/sharp-win32-x64': 0.35.2 + sharp@0.35.3(@types/node@25.6.0): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.3 + '@img/sharp-darwin-x64': 0.35.3 + '@img/sharp-freebsd-wasm32': 0.35.3 + '@img/sharp-libvips-darwin-arm64': 1.3.2 + '@img/sharp-libvips-darwin-x64': 1.3.2 + '@img/sharp-libvips-linux-arm': 1.3.2 + '@img/sharp-libvips-linux-arm64': 1.3.2 + '@img/sharp-libvips-linux-ppc64': 1.3.2 + '@img/sharp-libvips-linux-riscv64': 1.3.2 + '@img/sharp-libvips-linux-s390x': 1.3.2 + '@img/sharp-libvips-linux-x64': 1.3.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + '@img/sharp-linux-arm': 0.35.3 + '@img/sharp-linux-arm64': 0.35.3 + '@img/sharp-linux-ppc64': 0.35.3 + '@img/sharp-linux-riscv64': 0.35.3 + '@img/sharp-linux-s390x': 0.35.3 + '@img/sharp-linux-x64': 0.35.3 + '@img/sharp-linuxmusl-arm64': 0.35.3 + '@img/sharp-linuxmusl-x64': 0.35.3 + '@img/sharp-webcontainers-wasm32': 0.35.3 + '@img/sharp-win32-arm64': 0.35.3 + '@img/sharp-win32-ia32': 0.35.3 + '@img/sharp-win32-x64': 0.35.3 + '@types/node': 25.6.0 + optional: true + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -6779,7 +6874,7 @@ snapshots: undici-types@7.19.2: {} - undici@7.28.0: {} + undici@7.29.0: {} unenv@2.0.0-rc.24: dependencies: @@ -6880,7 +6975,7 @@ snapshots: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.10 + postcss: 8.5.23 rollup: 4.62.2 tinyglobby: 0.2.17 optionalDependencies: diff --git a/scripts/check-code-health.mjs b/scripts/check-code-health.mjs new file mode 100644 index 0000000..8856203 --- /dev/null +++ b/scripts/check-code-health.mjs @@ -0,0 +1,279 @@ +#!/usr/bin/env node + +import { spawnSync } from 'node:child_process'; +import { mkdtempSync, readFileSync, readdirSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, extname, join, resolve } from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; + +const currentFile = fileURLToPath(import.meta.url); +const projectRoot = resolve(dirname(currentFile), '..'); +const productionPaths = ['src', 'scripts', 'astro.config.ts', 'vitest.config.ts']; +const sourceExtensions = new Set(['.js', '.jsx', '.mjs', '.mts', '.ts', '.tsx']); + +const baselines = { + complexity: { violations: 21, maxCcn: 48, maxLength: 665, maxParams: 37 }, + duplication: { clones: 4, duplicatedLines: 62 }, + unused: { + files: 0, + exports: 0, + types: 0, + dependencies: 0, + devDependencies: 0, + unlisted: 0, + unresolved: 0, + }, + suppressions: 9, +}; + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { + cwd: options.cwd ?? projectRoot, + encoding: 'utf8', + env: { ...process.env, ...(options.env ?? {}) }, + maxBuffer: 64 * 1024 * 1024, + }); + if (result.error) throw result.error; + if (result.status !== 0 && !options.allowFailure) { + process.stdout.write(result.stdout ?? ''); + process.stderr.write(result.stderr ?? ''); + throw new Error(`${command} exited with status ${result.status}`); + } + return { status: result.status ?? 1, stdout: result.stdout ?? '', stderr: result.stderr ?? '' }; +} + +function parseJson(result, label) { + try { + return JSON.parse(result.stdout); + } catch (error) { + process.stderr.write(result.stderr); + throw new Error(`${label} did not return valid JSON`, { cause: error }); + } +} + +function commandWithUvx(command, uvxArgs) { + const probe = spawnSync(command, ['--version'], { encoding: 'utf8' }); + return probe.status === 0 ? { command, prefix: [] } : { command: 'uvx', prefix: uvxArgs }; +} + +function issueCount(issues, key) { + return issues.reduce((sum, issue) => sum + (issue[key]?.length ?? 0), 0); +} + +function failRegressions(label, observed, baseline) { + const regressions = Object.entries(baseline).filter(([key, maximum]) => observed[key] > maximum); + if (regressions.length > 0) { + throw new Error( + regressions + .map(([key, maximum]) => `${label} ${key} regressed: ${observed[key]} > ${maximum}`) + .join('\n') + ); + } + if (Object.entries(baseline).some(([key, maximum]) => observed[key] < maximum)) { + console.log(`${label} improved; lower the checked-in baseline intentionally.`); + } +} + +function checkUnused() { + const report = parseJson( + run('pnpm', ['exec', 'knip', '--reporter', 'json', '--no-exit-code', '--no-progress'], { + allowFailure: true, + }), + 'Knip' + ); + const issues = report.issues ?? []; + const observed = Object.fromEntries( + Object.keys(baselines.unused).map((key) => [key, issueCount(issues, key)]) + ); + console.log( + `Unused: files=${observed.files}, exports=${observed.exports}, types=${observed.types}, ` + + `dependencies=${observed.dependencies}, devDependencies=${observed.devDependencies}, ` + + `unlisted=${observed.unlisted}, unresolved=${observed.unresolved}.` + ); + failRegressions('Unused', observed, baselines.unused); +} + +function checkComplexity() { + const lizard = commandWithUvx('lizard', ['--from', 'lizard==1.23.0', 'lizard']); + const result = run(lizard.command, [ + ...lizard.prefix, + ...productionPaths, + '-x', + '**/*.test.*', + '-x', + 'scripts/check-code-health.mjs', + '-x', + '*.d.ts', + '--csv', + ]); + const rows = result.stdout + .trim() + .split('\n') + .map((line) => line.match(/^(\d+),(\d+),(\d+),(\d+),(\d+),/u)) + .filter(Boolean) + .map((match) => match.slice(1).map(Number)); + const observed = { + functions: rows.length, + nloc: rows.reduce((sum, row) => sum + row[0], 0), + violations: rows.filter((row) => row[1] > 15 || row[4] > 100 || row[3] > 7).length, + maxCcn: Math.max(...rows.map((row) => row[1])), + maxLength: Math.max(...rows.map((row) => row[4])), + maxParams: Math.max(...rows.map((row) => row[3])), + }; + console.log( + `Complexity: ${observed.functions} functions, ${observed.nloc} NLOC, ` + + `${observed.violations} violations; max CCN ${observed.maxCcn}, ` + + `max length ${observed.maxLength}, max params ${observed.maxParams}.` + ); + failRegressions('Complexity', observed, baselines.complexity); +} + +function checkDuplication() { + const outputDirectory = mkdtempSync(join(tmpdir(), 'looptv-jscpd-')); + run('pnpm', [ + 'exec', + 'jscpd', + ...productionPaths, + '--min-lines', + '8', + '--min-tokens', + '60', + '--mode', + 'strict', + '--ignore', + '**/*.test.*,**/*.d.ts,**/node_modules/**,**/dist/**,**/coverage/**,scripts/check-code-health.mjs', + '--reporters', + 'json', + '--output', + outputDirectory, + '--silent', + '--no-tips', + ]); + const observed = JSON.parse(readFileSync(join(outputDirectory, 'jscpd-report.json'), 'utf8')) + .statistics.total; + console.log( + `Duplication: ${observed.clones} groups, ${observed.duplicatedLines}/${observed.lines} lines ` + + `(${observed.percentage.toFixed(4)}%) across ${observed.sources} files.` + ); + failRegressions('Duplication', observed, baselines.duplication); +} + +function checkCycles() { + const report = parseJson( + run( + 'pnpm', + ['exec', 'knip', '--cycles', '--reporter', 'json', '--no-exit-code', '--no-progress'], + { allowFailure: true } + ), + 'Knip cycle analysis' + ); + const cycles = (report.issues ?? []).flatMap((issue) => issue.cycles ?? []); + if (cycles.length > 0) throw new Error(`Dependency cycles detected: ${cycles.length}`); + console.log('Cycles: zero JavaScript or TypeScript import cycles.'); +} + +function checkDependencies() { + const report = parseJson(run('pnpm', ['audit', '--json'], { allowFailure: true }), 'pnpm audit'); + // Astro 5 debt accepted only until the major-upgrade issue is resolved: #38. + const accepted = new Set(['GHSA-8hv8-536x-4wqp', 'GHSA-2pvr-wf23-7pc7']); + const severe = Object.values(report.advisories ?? {}).filter((advisory) => + ['critical', 'high'].includes(advisory.severity) + ); + const unexpected = severe.filter((advisory) => !accepted.has(advisory.github_advisory_id)); + const critical = severe.filter((advisory) => advisory.severity === 'critical').length; + const high = severe.filter((advisory) => advisory.severity === 'high').length; + console.log( + `Dependencies: ${critical} critical, ${high} high, ${unexpected.length} unexpected; ` + + `${severe.length - unexpected.length} accepted Astro 5 advisories.` + ); + if (unexpected.length > 0) { + throw new Error( + `Unexpected critical/high advisories: ${unexpected + .map((advisory) => advisory.github_advisory_id) + .join(', ')}` + ); + } +} + +const suppressionPattern = + /biome-ignore|eslint-disable|@ts-ignore|@ts-expect-error|istanbul ignore|c8 ignore|(?:test|base)\.skip\(|\bTODO\b|\bFIXME\b/u; + +function sourceFiles(root) { + const files = []; + for (const entry of readdirSync(root, { withFileTypes: true })) { + const path = join(root, entry.name); + if (entry.isDirectory()) files.push(...sourceFiles(path)); + else if (entry.isFile() && sourceExtensions.has(extname(entry.name))) files.push(path); + } + return files; +} + +function checkSuppressions() { + const files = ['src', 'scripts', 'tests'].flatMap((root) => + sourceFiles(resolve(projectRoot, root)) + ); + const matches = files + .filter((file) => file !== currentFile) + .flatMap((file) => + readFileSync(file, 'utf8') + .split('\n') + .filter((line) => suppressionPattern.test(line)) + ); + console.log(`Suppressions: ${matches.length} justified source/test markers.`); + if (matches.length > baselines.suppressions) { + throw new Error(`Suppressions regressed: ${matches.length} > ${baselines.suppressions}.`); + } + if (matches.length < baselines.suppressions) { + console.log('Suppressions improved; lower the checked-in baseline intentionally.'); + } +} + +function checkHygiene() { + const parent = run('git', ['rev-parse', '--verify', 'HEAD^'], { allowFailure: true }); + if (parent.status === 0) run('git', ['diff', '--check', 'HEAD^', 'HEAD']); + else run('git', ['diff-tree', '--check', '--root', '-r', 'HEAD']); + run('git', ['diff', '--check', 'HEAD', '--', '.']); + const conflicts = run('git', ['grep', '-nE', '^(<<<<<<< |=======|>>>>>>> )', '--', '.'], { + allowFailure: true, + }); + if (conflicts.status === 0) throw new Error(`Conflict markers found:\n${conflicts.stdout}`); + if (conflicts.status > 1) throw new Error(`git grep failed with status ${conflicts.status}`); + + const generated = run('git', ['ls-files', '--others', '--exclude-standard']) + .stdout.trim() + .split('\n') + .filter(Boolean) + .filter((file) => + /(^|\/)(?:coverage|dist|build|\.next|\.open-next|\.wrangler)(?:\/|$)|(?:^|\/)\.DS_Store$|\.tsbuildinfo$/u.test( + file + ) + ); + if (generated.length > 0) { + throw new Error(`Untracked generated artifacts found: ${generated.join(', ')}`); + } + console.log('Repository hygiene: whitespace, conflicts, and generated outputs pass.'); +} + +const checks = { + unused: checkUnused, + complexity: checkComplexity, + duplication: checkDuplication, + cycles: checkCycles, + dependencies: checkDependencies, + suppressions: checkSuppressions, + hygiene: checkHygiene, +}; +const selected = process.argv[2]; + +if (!Object.hasOwn(checks, selected)) { + console.error(`Usage: check-code-health.mjs <${Object.keys(checks).join('|')}>`); + process.exit(2); +} + +try { + checks[selected](); +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); +} diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts index 9c3e12d..16c79e5 100644 --- a/src/lib/analytics.ts +++ b/src/lib/analytics.ts @@ -39,7 +39,7 @@ interface AnalyticsEventMap { returned: { project_id: typeof PROJECT }; } -export function trackEvent(event: string, properties: Record = {}): void { +function trackEvent(event: string, properties: Record = {}): void { try { if (typeof window === 'undefined') return; posthog.capture(event, { project_id: PROJECT, ...properties }); diff --git a/src/lib/api-timing.ts b/src/lib/api-timing.ts index 14b859a..3367606 100644 --- a/src/lib/api-timing.ts +++ b/src/lib/api-timing.ts @@ -158,12 +158,3 @@ export function initApiTiming(options?: ApiTimingOptions): void { if (document.visibilityState === 'hidden') collectAndFlush(); }); } - -/** Stop sampling and flush remaining entries. */ -export function stopApiTiming(): void { - if (_flushTimer) { - clearInterval(_flushTimer); - _flushTimer = null; - } - collectAndFlush(); -} diff --git a/src/lib/foundry-monitoring.ts b/src/lib/foundry-monitoring.ts index eac3833..3d92d76 100644 --- a/src/lib/foundry-monitoring.ts +++ b/src/lib/foundry-monitoring.ts @@ -15,33 +15,7 @@ function messageFrom(error: unknown) { return String(error); } -type ErrorBoundaryScope = 'root' | 'global' | 'unknown'; - -/** - * Emits an "error_captured" event for an error surfaced by a React error - * boundary (error.tsx / global-error.tsx). Safe to call from the client — - * no-ops gracefully if PostHog is not ready and never throws. - */ -export function captureError( - error: unknown, - options: { scope?: ErrorBoundaryScope; digest?: string; source?: string } = {} -) { - try { - posthog.capture('error_captured', { - project_id: PROJECT_SLUG, - route: route(), - scope: options.scope ?? 'unknown', - digest: options.digest, - source: options.source ?? 'error_boundary', - message: messageFrom(error), - stack: error instanceof Error ? error.stack : undefined, - }); - } catch { - // Never let monitoring throw inside an error boundary. - } -} - -export function capturePageCrash(error: unknown, source: 'window_error' | 'unhandled_rejection') { +function capturePageCrash(error: unknown, source: 'window_error' | 'unhandled_rejection') { posthog.capture('foundry_page_crash', { project_id: PROJECT_SLUG, route: route(), diff --git a/vitest.config.ts b/vitest.config.ts index 6ca5778..e002872 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -27,7 +27,8 @@ export default defineConfig({ '.next', '.wrangler', ], - thresholds: { lines: 80, functions: 80, branches: 70, statements: 80 }, + // Ratchet the whole src/lib surface from the coverage the suite actually proves. + thresholds: { lines: 70, functions: 67, branches: 58, statements: 63 }, }, }, resolve: { alias: { '@': path.resolve(__dirname, 'src') } },