diff --git a/.github/workflows/knip.yml b/.github/workflows/knip.yml index 713650633..1b8518654 100644 --- a/.github/workflows/knip.yml +++ b/.github/workflows/knip.yml @@ -30,7 +30,11 @@ jobs: - run: pnpm db:generate # v1.4.42 — exports + types tiers promoted to enforcing. The - # `knip.json` ignore-block carries the shadcn UI surface and the + # `knip.jsonc` ignore-block carries the shadcn UI surface and the # `src/lib/validations/**` contract types; everything else has # to clear or get justified there. + # + # This pass cannot see a module whose only importer is its own test — + # vitest is a knip plugin, so test files are entries. The header of + # `knip.jsonc` demonstrates that and names the pass that does see it. - run: pnpm knip --reporter compact diff --git a/docs/self-hosting/geolite2.md b/docs/self-hosting/geolite2.md index 8de132921..b9dacd0a0 100644 --- a/docs/self-hosting/geolite2.md +++ b/docs/self-hosting/geolite2.md @@ -100,9 +100,16 @@ not immediately. A weekly `geoipupdate` followed by The databases are downloaded under your own MaxMind account, so their terms are between you and MaxMind — read what your account agrees to at -. HealthLog's own attribution -("this product includes GeoLite2 data created by MaxMind") is already on -the `/about` page and covers an instance that uses them. +. That agreement, the GeoLite +End User License Agreement, incorporates CC BY-SA 4.0 by reference and +controls wherever the two disagree; both require you to attribute the +data to MaxMind if you make it available to anyone else. + +HealthLog's own attribution ("This product includes GeoLite2 data +created by MaxMind, available from https://www.maxmind.com") is already +on the `/about` page and covers an instance that uses them. Keep that +page reachable — it is what carries the attribution for your deployment, +and it is served without a session for exactly that reason. ## Troubleshooting diff --git a/e2e/v1427-public-pages.spec.ts b/e2e/v1427-public-pages.spec.ts index 8fbb313fa..a9e62b753 100644 --- a/e2e/v1427-public-pages.spec.ts +++ b/e2e/v1427-public-pages.spec.ts @@ -41,6 +41,12 @@ test.describe("v1.4.27 — public pages", () => { // GeoLite2 attribution is the load-bearing reason this page exists. await expect(page.getByText(/GeoLite2/).first()).toBeVisible(); await expect(page.getByText(/MaxMind/).first()).toBeVisible(); + // Both licences, because naming only the Creative Commons one understates + // the terms: the GeoLite EULA is what governs and CC BY-SA 4.0 rides + // inside it. + await expect( + page.getByText(/GeoLite End User License Agreement/i), + ).toBeVisible(); await expect(page.getByText(/Attribution-ShareAlike 4\.0/i)).toBeVisible(); }); diff --git a/knip.json b/knip.json deleted file mode 100644 index 4e233cf9a..000000000 --- a/knip.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "$schema": "https://unpkg.com/knip@6/schema.json", - "entry": [ - "src/app/**/page.tsx", - "src/app/**/layout.tsx", - "src/app/**/route.ts", - "src/app/**/loading.tsx", - "src/app/**/not-found.tsx", - "src/app/**/error.tsx", - "src/app/**/global-error.tsx", - "src/app/**/template.tsx", - "src/app/**/opengraph-image.tsx", - "src/app/**/twitter-image.tsx", - "src/app/**/icon.tsx", - "src/app/**/apple-icon.tsx", - "src/app/**/manifest.ts", - "src/app/**/robots.ts", - "src/app/**/sitemap.ts", - "src/app/**/default.tsx", - "src/proxy.ts", - "src/instrumentation.ts", - "src/lib/jobs/**/*.ts", - "src/lib/ai/coach/eval/judge.ts", - "scripts/**/*.ts", - "scripts/**/*.mjs", - "e2e/**/*.spec.ts", - "e2e/**/*.test.ts", - "tests/**/*.test.ts", - "tests/**/setup.ts", - "vitest.config.ts", - "vitest.config.integration.ts", - "playwright.config.ts", - "next.config.ts", - "eslint.config.*", - "knip.json" - ], - "project": [ - "src/**/*.{ts,tsx}", - "scripts/**/*.{ts,mjs}", - "tests/**/*.ts", - "e2e/**/*.ts" - ], - "ignore": [ - "src/generated/**", - "prisma/**", - "public/**", - ".next/**", - "**/__mocks__/**", - "e2e/setup/test-helpers.ts", - "src/lib/logging/index.ts" - ], - "ignoreDependencies": [ - "@types/node", - "@types/react", - "@types/react-dom", - "@hookform/resolvers", - "react-hook-form", - "@prisma/client", - "next-themes", - "@vitejs/plugin-react", - "shadcn", - "tailwindcss", - "testcontainers", - "tw-animate-css", - "@napi-rs/canvas", - "@openai/codex-security", - "postcss" - ], - "ignoreBinaries": ["docker", "docker-compose", "pg_isready", "tsx", "cosign"], - "ignoreExportsUsedInFile": true, - "ignoreIssues": { - "src/components/ui/**": ["exports", "types"], - "src/lib/validations/**": ["types"], - "src/hooks/_internal/use-query-client-safe.ts": ["exports"], - "src/lib/google-health/sync.ts": ["exports"] - } -} diff --git a/knip.jsonc b/knip.jsonc new file mode 100644 index 000000000..e6bac4e8f --- /dev/null +++ b/knip.jsonc @@ -0,0 +1,131 @@ +// Dead-code configuration. JSONC rather than JSON so this can say what the +// gate proves — the previous JSON file could not, and the difference matters +// more here than anywhere else in the repo. +// +// ## What `pnpm knip` (the CI gate in .github/workflows/knip.yml) proves +// +// A module, export or dependency that nothing in the repository reaches is a +// build failure. That is a real property and it catches the ordinary case: a +// helper left behind after its last caller was rewritten. +// +// ## What it does NOT prove, demonstrated +// +// It does not prove a module is still reachable from production. Vitest is a +// knip plugin here, so `src/**/__tests__/**` are entry files, and a module +// whose ONLY importer is its own test counts as used — forever. Planting +// `src/lib/dead-probe.ts` with a single test importing it left `pnpm knip` at +// exit 0. Nothing in the default pass can see that class of dead code, so do +// not read a green Dead-code gate as "every module is still wired to +// something that ships". +// +// ## The pass that does see it +// +// pnpm knip --production --include files +// +// Production mode drops the plugin-supplied test entries and keeps only the +// entry and project patterns suffixed with `!` below — the app router tree, +// the proxy, instrumentation, the pg-boss workers, and `scripts/` (maintainer +// tooling is not production, but it is a legitimate non-test consumer, so a +// module reached only from a script is not the thing we are hunting). The `!` +// suffix is inert in the default pass, so both modes read one config. +// +// This is a review tool, not a second gate, for two reasons written down so +// the next reader does not have to rediscover them: +// +// 1. It reports 29 files today. Some are test-only by construction (the +// guard helpers under `src/__tests__/helpers/`, the settings section +// harness) and some are deliberately unreachable from production +// (`src/lib/ai/mock-client.ts` is excluded structurally by the +// `PROVIDER_CHAIN_TYPES` allowlist). Baselining the rest without +// triaging each one would encode noise as an invariant. +// 2. It has false positives. `src/lib/mood/note-backfill.ts` is imported by +// `scripts/backfill-mood-note-column.ts` through the `@/` alias and is +// still reported, because `scripts/` sits outside the tsconfig that +// defines the alias. The default pass hides that resolution failure +// behind the module's test importer. +// +// Run it in the deep-audit step, the same way +// `scripts/audit-column-readers.ts` is run: read the list, judge each entry, +// do not treat the exit code as a verdict. +{ + "$schema": "https://unpkg.com/knip@6/schema.json", + // `!` marks an entry as production. Read the block above before adding or + // removing one — the suffix is what separates the two passes. + "entry": [ + "src/app/**/page.tsx!", + "src/app/**/layout.tsx!", + "src/app/**/route.ts!", + "src/app/**/loading.tsx!", + "src/app/**/not-found.tsx!", + "src/app/**/error.tsx!", + "src/app/**/global-error.tsx!", + "src/app/**/template.tsx!", + "src/app/**/opengraph-image.tsx!", + "src/app/**/twitter-image.tsx!", + "src/app/**/icon.tsx!", + "src/app/**/apple-icon.tsx!", + "src/app/**/manifest.ts!", + "src/app/**/robots.ts!", + "src/app/**/sitemap.ts!", + "src/app/**/default.tsx!", + "src/proxy.ts!", + "src/instrumentation.ts!", + "src/lib/jobs/**/*.ts!", + "src/lib/ai/coach/eval/judge.ts!", + "scripts/**/*.ts!", + "scripts/**/*.mjs!", + "e2e/**/*.spec.ts", + "e2e/**/*.test.ts", + "tests/**/*.test.ts", + "tests/**/setup.ts", + "vitest.config.ts", + "vitest.config.integration.ts", + "playwright.config.ts", + "next.config.ts!", + "eslint.config.*", + "knip.jsonc", + ], + "project": [ + "src/**/*.{ts,tsx}!", + "scripts/**/*.{ts,mjs}!", + "tests/**/*.ts", + "e2e/**/*.ts", + ], + "ignore": [ + "src/generated/**", + "prisma/**", + "public/**", + ".next/**", + "**/__mocks__/**", + "e2e/setup/test-helpers.ts", + "src/lib/logging/index.ts", + ], + "ignoreDependencies": [ + "@types/node", + "@types/react", + "@types/react-dom", + "@hookform/resolvers", + "react-hook-form", + "@prisma/client", + "next-themes", + "@vitejs/plugin-react", + "shadcn", + "tailwindcss", + "testcontainers", + "tw-animate-css", + "@napi-rs/canvas", + "@openai/codex-security", + "postcss", + ], + "ignoreBinaries": ["docker", "docker-compose", "pg_isready", "tsx", "cosign"], + // Scoped to a single file: an export used only inside its own module is not + // a finding. It is also why the default pass cannot answer the question the + // production pass answers — this setting is about exports, not reachability. + "ignoreExportsUsedInFile": true, + "ignoreIssues": { + "src/components/ui/**": ["exports", "types"], + "src/lib/validations/**": ["types"], + "src/hooks/_internal/use-query-client-safe.ts": ["exports"], + "src/lib/google-health/sync.ts": ["exports"], + }, +} diff --git a/src/__tests__/account-payload-consumer-guard.test.ts b/src/__tests__/account-payload-consumer-guard.test.ts index 86cb77d27..e4c2fda78 100644 --- a/src/__tests__/account-payload-consumer-guard.test.ts +++ b/src/__tests__/account-payload-consumer-guard.test.ts @@ -55,6 +55,12 @@ const CONSUMER_ROOTS = [ join(SRC, "app"), ]; +/** + * Pruned on purpose: the generated Prisma client, build output, vendored + * dependencies, and the suites themselves are not the tree under audit. Note + * what is NOT pruned — this walk descends into dot-prefixed directories, so + * `src/app/.well-known` is inside the sweep. `fs.globSync` would drop it. + */ const SKIP_DIRS = new Set([ "__tests__", "__mocks__", diff --git a/src/__tests__/acting-account-boundary-guard.test.ts b/src/__tests__/acting-account-boundary-guard.test.ts index 3d9690e76..7081f57cc 100644 --- a/src/__tests__/acting-account-boundary-guard.test.ts +++ b/src/__tests__/acting-account-boundary-guard.test.ts @@ -15,19 +15,17 @@ */ import { describe, it, expect } from "vitest"; import { readFileSync } from "node:fs"; -import { globSync } from "node:fs"; -import { join, sep } from "node:path"; +import { join } from "node:path"; + +import { walkSourceFiles } from "./helpers/source-files"; const SRC = join(process.cwd(), "src"); function sourceFiles(): string[] { - return globSync("**/*.{ts,tsx}", { cwd: SRC }) - .filter( - (p) => !p.startsWith(`generated${sep}`) && !p.startsWith("generated/"), - ) + return walkSourceFiles(SRC, { floor: 3000 }) + .filter((p) => !p.startsWith("generated/")) .filter((p) => !p.includes("__tests__")) .filter((p) => !p.endsWith(".test.ts") && !p.endsWith(".test.tsx")) - .map((p) => p.split(sep).join("/")) .sort(); } diff --git a/src/__tests__/ai-consent-enforcement-guard.test.ts b/src/__tests__/ai-consent-enforcement-guard.test.ts index ccdb21426..25b5173ab 100644 --- a/src/__tests__/ai-consent-enforcement-guard.test.ts +++ b/src/__tests__/ai-consent-enforcement-guard.test.ts @@ -33,20 +33,18 @@ */ import { describe, it, expect } from "vitest"; import { readFileSync } from "node:fs"; -import { join, sep } from "node:path"; -import { globSync } from "node:fs"; +import { join } from "node:path"; + +import { walkSourceFiles } from "./helpers/source-files"; const SRC = join(process.cwd(), "src"); /** Every non-test `.ts` / `.tsx` under `src/`, minus the generated client. */ function sourceFiles(): string[] { - return globSync("**/*.{ts,tsx}", { cwd: SRC }) - .filter( - (p) => !p.startsWith(`generated${sep}`) && !p.startsWith("generated/"), - ) + return walkSourceFiles(SRC, { floor: 3000 }) + .filter((p) => !p.startsWith("generated/")) .filter((p) => !p.includes("__tests__")) .filter((p) => !p.endsWith(".test.ts") && !p.endsWith(".test.tsx")) - .map((p) => p.split(sep).join("/")) .sort(); } diff --git a/src/__tests__/bearer-scope-enforcement-guard.test.ts b/src/__tests__/bearer-scope-enforcement-guard.test.ts index b1c721cbb..ffd5175ee 100644 --- a/src/__tests__/bearer-scope-enforcement-guard.test.ts +++ b/src/__tests__/bearer-scope-enforcement-guard.test.ts @@ -14,8 +14,9 @@ */ import { describe, it, expect } from "vitest"; import { readFileSync } from "node:fs"; -import { join, sep } from "node:path"; -import { globSync } from "node:fs"; +import { join } from "node:path"; + +import { walkSourceFiles } from "./helpers/source-files"; const SRC = join(process.cwd(), "src"); @@ -24,13 +25,10 @@ const SRC = join(process.cwd(), "src"); * client (9 MB; never read it) and test files themselves. */ function sourceFiles(): string[] { - return globSync("**/*.{ts,tsx}", { cwd: SRC }) - .filter( - (p) => !p.startsWith(`generated${sep}`) && !p.startsWith("generated/"), - ) + return walkSourceFiles(SRC, { floor: 3000 }) + .filter((p) => !p.startsWith("generated/")) .filter((p) => !p.includes("__tests__")) .filter((p) => !p.endsWith(".test.ts") && !p.endsWith(".test.tsx")) - .map((p) => p.split(sep).join("/")) .sort(); } diff --git a/src/__tests__/cycle-verdict-single-decider-guard.test.ts b/src/__tests__/cycle-verdict-single-decider-guard.test.ts index 4d39d23bc..4214e2a0d 100644 --- a/src/__tests__/cycle-verdict-single-decider-guard.test.ts +++ b/src/__tests__/cycle-verdict-single-decider-guard.test.ts @@ -28,6 +28,12 @@ const ROOT = process.cwd(); /** Client-side code: the component tree, the hooks, and the app's pages. */ const CLIENT_ROOTS = ["src/components", "src/hooks", "src/app"]; +/** + * Pruned on purpose: the generated Prisma client, build output, vendored + * dependencies, and the suites themselves are not the tree under audit. Note + * what is NOT pruned — this walk descends into dot-prefixed directories, so + * `src/app/.well-known` is inside the sweep. `fs.globSync` would drop it. + */ const SKIP_DIRS = new Set(["__tests__", "node_modules", ".next", "generated"]); function walk(dir: string, out: string[] = []): string[] { diff --git a/src/__tests__/daily-reads-refetch-guard.test.ts b/src/__tests__/daily-reads-refetch-guard.test.ts index 3bc244c37..a466e4f23 100644 --- a/src/__tests__/daily-reads-refetch-guard.test.ts +++ b/src/__tests__/daily-reads-refetch-guard.test.ts @@ -25,8 +25,9 @@ */ import { describe, it, expect } from "vitest"; import { readFileSync } from "node:fs"; -import { join, sep } from "node:path"; -import { globSync } from "node:fs"; +import { join } from "node:path"; + +import { walkSourceFiles } from "./helpers/source-files"; const SRC = join(process.cwd(), "src"); @@ -49,13 +50,10 @@ const INVALIDATION = const REFETCH = /refetchInactiveDailyReads\(/g; function sourceFiles(): string[] { - return globSync("**/*.{ts,tsx}", { cwd: SRC }) - .filter( - (p) => !p.startsWith(`generated${sep}`) && !p.startsWith("generated/"), - ) + return walkSourceFiles(SRC, { floor: 3000 }) + .filter((p) => !p.startsWith("generated/")) .filter((p) => !p.includes("__tests__")) .filter((p) => !p.endsWith(".test.ts") && !p.endsWith(".test.tsx")) - .map((p) => p.split(sep).join("/")) .sort(); } @@ -108,6 +106,13 @@ const NO_REFETCH_ALLOWLIST = [ ].sort(); describe("daily-reads refetch guard", () => { + // A sweep that finds nothing agrees with every allowlist, so the size of + // the tree being read is asserted before anything is concluded from it. + // Pinned below the real source-file count with headroom, not at one. + it("reads the tree it claims to sweep", () => { + expect(sourceFiles().length).toBeGreaterThan(1500); + }); + it("the blessed helper pairs the bundle invalidation with the inactive refetch", () => { const src = read(FACTORY); // The one canonical entry point every intake surface routes through. diff --git a/src/__tests__/destructive-control-guard.test.ts b/src/__tests__/destructive-control-guard.test.ts index a23fdcdd8..bbface639 100644 --- a/src/__tests__/destructive-control-guard.test.ts +++ b/src/__tests__/destructive-control-guard.test.ts @@ -49,8 +49,10 @@ * red at the call site. */ import { describe, it, expect } from "vitest"; -import { readFileSync, globSync } from "node:fs"; -import { join, sep } from "node:path"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { walkSourceFiles } from "./helpers/source-files"; const SRC = join(process.cwd(), "src"); @@ -558,8 +560,7 @@ const CONTROL_ROOTS = ["app/", "components/", "hooks/"]; const EXCLUDED_PREFIXES = ["app/api/", "generated/", "lib/"]; function sourceFiles(): string[] { - return globSync("**/*.{ts,tsx}", { cwd: SRC }) - .map((p) => p.split(sep).join("/")) + return walkSourceFiles(SRC, { floor: 3000 }) .filter((p) => !p.includes("__tests__")) .filter((p) => !p.endsWith(".test.ts") && !p.endsWith(".test.tsx")) .sort(); diff --git a/src/__tests__/display-transform-rounding-guard.test.ts b/src/__tests__/display-transform-rounding-guard.test.ts index ce02fa091..d0d44e943 100644 --- a/src/__tests__/display-transform-rounding-guard.test.ts +++ b/src/__tests__/display-transform-rounding-guard.test.ts @@ -23,9 +23,11 @@ * are pinned to a named allowlist, so a new converted-value surface * cannot quietly join them. */ -import { readFileSync, globSync } from "node:fs"; +import { readFileSync } from "node:fs"; import { join } from "node:path"; +import { walkSourceFiles } from "./helpers/source-files"; + import { describe, it, expect } from "vitest"; import { @@ -69,7 +71,7 @@ const RAW_SCALE_READ = /\b\w*[Tt]ransform(?:For\([^)]*\))?\??\.(factor|offset)\b/; function sourceFiles(): string[] { - return globSync("**/*.{ts,tsx}", { cwd: SRC }).filter( + return walkSourceFiles(SRC, { floor: 3000 }).filter( (rel) => // Never walk the Prisma client, and skip the suites that legitimately // exercise the raw arithmetic they are asserting about. diff --git a/src/__tests__/dynamic-key-exhaustiveness.test.ts b/src/__tests__/dynamic-key-exhaustiveness.test.ts index d5d2af4ac..7b5a539d9 100644 --- a/src/__tests__/dynamic-key-exhaustiveness.test.ts +++ b/src/__tests__/dynamic-key-exhaustiveness.test.ts @@ -132,6 +132,12 @@ const REGISTRY: readonly KeySpace[] = [ ]; describe("dynamic-key exhaustiveness (enum-derived)", () => { + // Six shipped locales. An empty read of `messages/` would register zero + // per-locale cases and the suite would report green over nothing. + it("reads every shipped locale bundle", () => { + expect(LOCALES.length).toBeGreaterThanOrEqual(6); + }); + it("discovers all six shipped locales", () => { expect(LOCALES.map((l) => l.locale).sort()).toEqual([ "de", diff --git a/src/__tests__/e2e-route-cleanup-guard.test.ts b/src/__tests__/e2e-route-cleanup-guard.test.ts index 0e7613914..0a9d7af58 100644 --- a/src/__tests__/e2e-route-cleanup-guard.test.ts +++ b/src/__tests__/e2e-route-cleanup-guard.test.ts @@ -14,8 +14,10 @@ * a runtime property, proven by the suite itself. */ import { describe, it, expect } from "vitest"; -import { readFileSync, globSync } from "node:fs"; -import { join, sep } from "node:path"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { walkSourceFiles } from "./helpers/source-files"; const E2E = join(process.cwd(), "e2e"); @@ -33,9 +35,7 @@ const DIRECT_IMPORT_ALLOWLIST: Record = { }; function e2eFiles(): string[] { - return globSync("**/*.ts", { cwd: E2E }) - .map((p) => p.split(sep).join("/")) - .sort(); + return walkSourceFiles(E2E, { floor: 40, extensions: [".ts"] }).sort(); } function read(rel: string): string { diff --git a/src/__tests__/external-id-stability-guard.test.ts b/src/__tests__/external-id-stability-guard.test.ts index 8e3abe525..c21c1ead7 100644 --- a/src/__tests__/external-id-stability-guard.test.ts +++ b/src/__tests__/external-id-stability-guard.test.ts @@ -23,20 +23,18 @@ */ import { describe, it, expect } from "vitest"; import { readFileSync } from "node:fs"; -import { join, sep } from "node:path"; -import { globSync } from "node:fs"; +import { join } from "node:path"; + +import { walkSourceFiles } from "./helpers/source-files"; const SRC = join(process.cwd(), "src"); const VALIDATOR_MODULE = "@/lib/validations/external-id"; function sourceFiles(): string[] { - return globSync("**/*.{ts,tsx}", { cwd: SRC }) - .filter( - (p) => !p.startsWith(`generated${sep}`) && !p.startsWith("generated/"), - ) + return walkSourceFiles(SRC, { floor: 3000 }) + .filter((p) => !p.startsWith("generated/")) .filter((p) => !p.includes("__tests__")) .filter((p) => !p.endsWith(".test.ts") && !p.endsWith(".test.tsx")) - .map((p) => p.split(sep).join("/")) .sort(); } diff --git a/src/__tests__/helpers/source-files.ts b/src/__tests__/helpers/source-files.ts new file mode 100644 index 000000000..0fb711277 --- /dev/null +++ b/src/__tests__/helpers/source-files.ts @@ -0,0 +1,75 @@ +/** + * The one file walker the structural guards share. + * + * ## Why this exists rather than `fs.globSync` + * + * A guard that freezes a discovered set against an allowlist is only as + * honest as its idea of "every source file". `fs.globSync` has a rule that + * silently narrows that idea: a glob's `*` never matches a leading dot, so + * `**\/*.{ts,tsx}` skips every dot-prefixed directory without saying so. + * + * `src/app/.well-known/` holds three live route modules — the two OAuth + * discovery documents the MCP server publishes and the + * apple-app-site-association handler. Every guard built on `globSync` walked + * straight past all three and reported a clean sweep over a tree with a hole + * in it. Demonstrated, not assumed: an unlisted `getSession()` call planted + * in `app/.well-known/oauth-authorization-server/route.ts` left + * `session-surface-guard` at 9 passed, and fails it once the walk goes + * through here. + * + * `readdirSync(root, { recursive: true })` has no dot rule. It is the reason + * `delegable-surface-guard.test.ts` already walks by hand; this helper makes + * that the shared behaviour instead of one guard's private correction. + * + * ## Why `floor` is required and not optional + * + * The other way a sweep goes quiet is by finding nothing at all: an empty + * match set agrees with an empty allowlist, and the guard passes on a tree it + * never read. A wrong root, a renamed directory, or a walk that throws away + * every entry all look exactly like compliance. So every caller states the + * size it expects to walk, pinned below the real count with headroom, and a + * walk that comes back smaller throws instead of reporting a clean sweep. + * The floor covers the walk; a guard that filters further is still on the + * hook for a floor over its own narrowed set. + * + * ## What it returns + * + * Posix-separated paths relative to `root`, sorted, so a guard's output and + * its failure message read the same on every platform. Directory entries are + * dropped by the extension filter. + * + * ## What it does not do + * + * It applies no exclusions of its own — not `generated/`, not `__tests__`, + * not `.test.ts`. Each guard states its own, because what counts as out of + * scope is the guard's claim to make, not the walker's. + * + * And it must not be rooted at the repository root. Descending into + * dot-prefixed directories is the whole point, but at the root that means + * `.git` and — on a maintainer's machine mid-release — the `.wt-*` sibling + * worktrees, each a full second copy of the tree that would double every + * match. Every caller roots at `src/`, `e2e/` or a subtree of one. + */ +import { readdirSync } from "node:fs"; +import { sep } from "node:path"; + +export function walkSourceFiles( + root: string, + options: { floor: number; extensions?: readonly string[] }, +): string[] { + const extensions = options.extensions ?? [".ts", ".tsx"]; + const files = readdirSync(root, { recursive: true }) + .map((entry) => String(entry).split(sep).join("/")) + .filter((rel) => extensions.some((ext) => rel.endsWith(ext))) + .sort(); + + if (files.length < options.floor) { + throw new Error( + `walkSourceFiles(${root}) found ${files.length} file(s) matching ` + + `${extensions.join(", ")}, below the stated floor of ${options.floor}. ` + + `A sweep this small is a broken walk, not a clean tree — check the ` + + `root before lowering the floor.`, + ); + } + return files; +} diff --git a/src/__tests__/i18n-call-site-coverage.test.ts b/src/__tests__/i18n-call-site-coverage.test.ts index e236f05eb..c4969259c 100644 --- a/src/__tests__/i18n-call-site-coverage.test.ts +++ b/src/__tests__/i18n-call-site-coverage.test.ts @@ -27,6 +27,12 @@ const ROOT = join(__dirname, "../.."); const SRC = join(ROOT, "src"); const EN_BUNDLE_PATH = join(ROOT, "messages/en.json"); +/** + * Pruned on purpose: the generated Prisma client, build output, vendored + * dependencies, and the suites themselves are not the tree under audit. Note + * what is NOT pruned — this walk descends into dot-prefixed directories, so + * `src/app/.well-known` is inside the sweep. `fs.globSync` would drop it. + */ const SKIP_DIRS = new Set(["__tests__", "node_modules", ".next", "generated"]); function walk(dir: string, out: string[] = []): string[] { diff --git a/src/__tests__/i18n-drift-guard.test.ts b/src/__tests__/i18n-drift-guard.test.ts index f1348fc4f..e373f1380 100644 --- a/src/__tests__/i18n-drift-guard.test.ts +++ b/src/__tests__/i18n-drift-guard.test.ts @@ -115,6 +115,12 @@ const REQUIRED_KEY_SETS: Array<{ ]; describe("v1.4.27 B6 i18n drift-guard — required keys present in every locale", () => { + // Six shipped locales. An empty read of `messages/` would register zero + // per-locale cases and the suite would report green over nothing. + it("reads every shipped locale bundle", () => { + expect(LOCALES.length).toBeGreaterThanOrEqual(6); + }); + for (const { label, required } of REQUIRED_KEY_SETS) { describe(label, () => { it.each(LOCALES)( diff --git a/src/__tests__/i18n-reverse-coverage.test.ts b/src/__tests__/i18n-reverse-coverage.test.ts index c55e5dae6..1feac2a38 100644 --- a/src/__tests__/i18n-reverse-coverage.test.ts +++ b/src/__tests__/i18n-reverse-coverage.test.ts @@ -35,6 +35,12 @@ const ROOT = join(__dirname, "../.."); const SRC = join(ROOT, "src"); const EN_BUNDLE_PATH = join(ROOT, "messages/en.json"); +/** + * Pruned on purpose: the generated Prisma client, build output, vendored + * dependencies, and the suites themselves are not the tree under audit. Note + * what is NOT pruned — this walk descends into dot-prefixed directories, so + * `src/app/.well-known` is inside the sweep. `fs.globSync` would drop it. + */ const SKIP_DIRS = new Set(["__tests__", "node_modules", ".next", "generated"]); /** diff --git a/src/__tests__/inventory-write-isolation.test.ts b/src/__tests__/inventory-write-isolation.test.ts index 6e38fb16b..b24c43bcb 100644 --- a/src/__tests__/inventory-write-isolation.test.ts +++ b/src/__tests__/inventory-write-isolation.test.ts @@ -22,6 +22,12 @@ import { join, relative, sep } from "node:path"; const ROOT = join(__dirname, "../.."); const SRC = join(ROOT, "src"); +/** + * Pruned on purpose: the generated Prisma client, build output, vendored + * dependencies, and the suites themselves are not the tree under audit. Note + * what is NOT pruned — this walk descends into dot-prefixed directories, so + * `src/app/.well-known` is inside the sweep. `fs.globSync` would drop it. + */ const SKIP_DIRS = new Set(["__tests__", "node_modules", ".next", "generated"]); /** @@ -61,6 +67,13 @@ function walk(dir: string, out: string[] = []): string[] { } describe("medicationInventoryItem write isolation", () => { + // A sweep that finds nothing agrees with every allowlist, so the size of + // the tree being read is asserted before anything is concluded from it. + // Pinned below the real source-file count with headroom, not at one. + it("reads the tree it claims to sweep", () => { + expect(walk(SRC).length).toBeGreaterThan(1500); + }); + it("no file outside the consumption module + inventory surfaces writes inventory items", () => { const offenders: string[] = []; for (const file of walk(SRC)) { diff --git a/src/__tests__/medications-efficacy-verb-guard.test.ts b/src/__tests__/medications-efficacy-verb-guard.test.ts index a85e3db8c..867491e68 100644 --- a/src/__tests__/medications-efficacy-verb-guard.test.ts +++ b/src/__tests__/medications-efficacy-verb-guard.test.ts @@ -80,6 +80,12 @@ function flatten(obj: unknown, prefix: string, out: [string, string][]): void { } describe("medications.efficacy — banned verdict/advice verbs", () => { + // Six shipped locales. An empty read of `messages/` would register zero + // per-locale cases and the suite would report green over nothing. + it("reads every shipped locale bundle", () => { + expect(LOCALES.length).toBeGreaterThanOrEqual(6); + }); + it.each(LOCALES)("%s carries no forbidden verb", (file) => { const bundle = JSON.parse(readFileSync(join(MESSAGES, file), "utf8")) as { medications?: { efficacy?: unknown }; diff --git a/src/__tests__/migration-number-uniqueness.test.ts b/src/__tests__/migration-number-uniqueness.test.ts index de7ea3c53..14bc18ed4 100644 --- a/src/__tests__/migration-number-uniqueness.test.ts +++ b/src/__tests__/migration-number-uniqueness.test.ts @@ -64,6 +64,9 @@ describe("prisma migrations — numeric prefixes are unique", () => { const numbered = readdirSync(MIGRATIONS_DIR, { withFileTypes: true, }).filter((e) => e.isDirectory() && NUMBERED_MIGRATION.test(e.name)); - expect(numbered.length).toBeGreaterThan(0); + // Not `> 0`: a walk that returns a single directory is as broken as one + // that returns none, and both would pass a zero floor. Pinned below the + // real migration count with headroom. + expect(numbered.length).toBeGreaterThan(200); }); }); diff --git a/src/__tests__/report-selection-egress-guard.test.ts b/src/__tests__/report-selection-egress-guard.test.ts index 0601165d1..8f516f69c 100644 --- a/src/__tests__/report-selection-egress-guard.test.ts +++ b/src/__tests__/report-selection-egress-guard.test.ts @@ -19,6 +19,12 @@ import { readFileSync, readdirSync, statSync } from "node:fs"; import { join } from "node:path"; const SRC = join(__dirname, ".."); +/** + * Pruned on purpose: the generated Prisma client, build output, vendored + * dependencies, and the suites themselves are not the tree under audit. Note + * what is NOT pruned — this walk descends into dot-prefixed directories, so + * `src/app/.well-known` is inside the sweep. `fs.globSync` would drop it. + */ const SKIP_DIRS = new Set(["__tests__", "node_modules", ".next", "generated"]); /** @@ -52,6 +58,13 @@ function stripComments(source: string): string { } describe("doctor-report aggregator egress", () => { + // A sweep that finds nothing agrees with every allowlist, so the size of + // the tree being read is asserted before anything is concluded from it. + // Pinned below the real source-file count with headroom, not at one. + it("reads the tree it claims to sweep", () => { + expect(walk(SRC).length).toBeGreaterThan(1500); + }); + it("keeps the set of aggregator callers frozen", () => { const found: string[] = []; for (const file of walk(SRC)) { diff --git a/src/__tests__/session-surface-guard.test.ts b/src/__tests__/session-surface-guard.test.ts index 067bd97d3..0d49280d7 100644 --- a/src/__tests__/session-surface-guard.test.ts +++ b/src/__tests__/session-surface-guard.test.ts @@ -34,8 +34,9 @@ */ import { describe, it, expect } from "vitest"; import { readFileSync } from "node:fs"; -import { join, sep } from "node:path"; -import { globSync } from "node:fs"; +import { join } from "node:path"; + +import { walkSourceFiles } from "./helpers/source-files"; const SRC = join(process.cwd(), "src"); @@ -46,8 +47,8 @@ const SRC = join(process.cwd(), "src"); * `app/`) and never read — see CLAUDE.md. */ function appFiles(): string[] { - return globSync("app/**/*.{ts,tsx}", { cwd: SRC }) - .map((p) => p.split(sep).join("/")) + return walkSourceFiles(SRC, { floor: 3000 }) + .filter((p) => p.startsWith("app/")) .filter((p) => !p.includes("__tests__")) .filter((p) => !p.endsWith(".test.ts") && !p.endsWith(".test.tsx")) .sort(); diff --git a/src/__tests__/step-up-elevation-guard.test.ts b/src/__tests__/step-up-elevation-guard.test.ts index 7b0581c8f..a8c4ec8b6 100644 --- a/src/__tests__/step-up-elevation-guard.test.ts +++ b/src/__tests__/step-up-elevation-guard.test.ts @@ -24,19 +24,17 @@ */ import { describe, it, expect } from "vitest"; import { readFileSync } from "node:fs"; -import { join, sep } from "node:path"; -import { globSync } from "node:fs"; +import { join } from "node:path"; + +import { walkSourceFiles } from "./helpers/source-files"; const SRC = join(process.cwd(), "src"); function sourceFiles(): string[] { - return globSync("**/*.{ts,tsx}", { cwd: SRC }) - .filter( - (p) => !p.startsWith(`generated${sep}`) && !p.startsWith("generated/"), - ) + return walkSourceFiles(SRC, { floor: 3000 }) + .filter((p) => !p.startsWith("generated/")) .filter((p) => !p.includes("__tests__")) .filter((p) => !p.endsWith(".test.ts") && !p.endsWith(".test.tsx")) - .map((p) => p.split(sep).join("/")) .sort(); } @@ -120,6 +118,13 @@ const ELEVATION_ROUTES = [ ].sort(); describe("S1 — the elevation-accepting route set is frozen", () => { + // A sweep that finds nothing agrees with every allowlist, so the size of + // the tree being read is asserted before anything is concluded from it. + // Pinned below the real source-file count with headroom, not at one. + it("reads the tree it claims to sweep", () => { + expect(sourceFiles().length).toBeGreaterThan(1500); + }); + it("only the known MFA-management mutations call the gate", () => { const found = callers("requireMfaManagementAuth", API_HANDLER).filter( (rel) => rel !== "lib/api-handler.ts", diff --git a/src/__tests__/success-affordance-guard.test.ts b/src/__tests__/success-affordance-guard.test.ts index d200f6a9a..75d1669f8 100644 --- a/src/__tests__/success-affordance-guard.test.ts +++ b/src/__tests__/success-affordance-guard.test.ts @@ -493,6 +493,13 @@ function markerCounts( } describe("success affordance — owned by the outcome module, pinned everywhere else", () => { + // A sweep that finds nothing agrees with every allowlist, so the size of + // the tree being read is asserted before anything is concluded from it. + // Pinned below the real component-file count with headroom, not at one. + it("reads the tree it claims to sweep", () => { + expect(componentSourceFiles().length).toBeGreaterThan(400); + }); + it("lets no unlisted component reach for the success affordance", () => { const unlisted = componentSourceFiles() .filter((path) => !path.startsWith(PRESENTATION_DIR)) diff --git a/src/__tests__/unit-preference-display-guard.test.ts b/src/__tests__/unit-preference-display-guard.test.ts index 10e0598d7..b42229280 100644 --- a/src/__tests__/unit-preference-display-guard.test.ts +++ b/src/__tests__/unit-preference-display-guard.test.ts @@ -11,9 +11,10 @@ * Mutation check: add `unit="kg"` back to any swept page and this goes red. */ import { readFileSync } from "node:fs"; -import { globSync } from "node:fs"; import { join } from "node:path"; +import { walkSourceFiles } from "./helpers/source-files"; + import { describe, it, expect } from "vitest"; import { @@ -74,9 +75,11 @@ const HEIGHT_CONTROL = join( function sweptFiles(): string[] { return [ join(SRC, "app", "page-client.tsx"), - ...globSync("app/insights/**/page.tsx", { cwd: SRC }).map((rel) => - join(SRC, rel), - ), + ...walkSourceFiles(SRC, { floor: 700, extensions: [".tsx"] }) + .filter( + (rel) => rel.startsWith("app/insights/") && rel.endsWith("/page.tsx"), + ) + .map((rel) => join(SRC, rel)), ...TARGET_SURFACES, ]; } diff --git a/src/__tests__/vorsorge-due-bucket-single-home-guard.test.ts b/src/__tests__/vorsorge-due-bucket-single-home-guard.test.ts index 19811de76..bc960b9cf 100644 --- a/src/__tests__/vorsorge-due-bucket-single-home-guard.test.ts +++ b/src/__tests__/vorsorge-due-bucket-single-home-guard.test.ts @@ -18,8 +18,10 @@ * settles is that there is only one of it. */ import { describe, it, expect } from "vitest"; -import { readFileSync, globSync } from "node:fs"; -import { join, sep } from "node:path"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { walkSourceFiles } from "./helpers/source-files"; const SRC = join(process.cwd(), "src"); @@ -71,8 +73,7 @@ const FIX_THE_ZONE = [ ].join(" "); function sourceFiles(): string[] { - return globSync("**/*.{ts,tsx}", { cwd: SRC }) - .map((p) => p.split(sep).join("/")) + return walkSourceFiles(SRC, { floor: 3000 }) .filter((p) => !p.startsWith("generated/")) .filter((p) => !p.includes("__tests__/")) .sort(); @@ -94,6 +95,13 @@ function declaresTheBucket(source: string): boolean { } describe("the checkup due-day bucket has one home", () => { + // A sweep that finds nothing agrees with every allowlist, so the size of + // the tree being read is asserted before anything is concluded from it. + // Pinned below the real source-file count with headroom, not at one. + it("reads the tree it claims to sweep", () => { + expect(sourceFiles().length).toBeGreaterThan(1500); + }); + it("is declared in exactly one file", () => { const declaring = sourceFiles().filter((rel) => declaresTheBucket(read(rel)), diff --git a/src/__tests__/workout-canonical-identity-guard.test.ts b/src/__tests__/workout-canonical-identity-guard.test.ts index 7c976d1ec..73fb493fb 100644 --- a/src/__tests__/workout-canonical-identity-guard.test.ts +++ b/src/__tests__/workout-canonical-identity-guard.test.ts @@ -21,8 +21,9 @@ */ import { describe, it, expect } from "vitest"; import { readFileSync } from "node:fs"; -import { globSync } from "node:fs"; -import { join, sep } from "node:path"; +import { join } from "node:path"; + +import { walkSourceFiles } from "./helpers/source-files"; const SRC = join(process.cwd(), "src"); @@ -52,8 +53,7 @@ const IDENTITY_OWNER: Record = { const EXPECTED_CALL_SITES = Object.keys(IDENTITY_OWNER).sort(); function sourceFiles(): string[] { - return globSync("**/*.{ts,tsx}", { cwd: SRC }) - .map((p) => p.split(sep).join("/")) + return walkSourceFiles(SRC, { floor: 3000 }) .filter((p) => !p.startsWith("generated/")) .filter((p) => !p.includes("__tests__")) .filter((p) => !p.endsWith(".test.ts") && !p.endsWith(".test.tsx")) @@ -81,6 +81,13 @@ function invokesPicker(source: string): boolean { } describe("the picker's call-site set is frozen", () => { + // A sweep that finds nothing agrees with every allowlist, so the size of + // the tree being read is asserted before anything is concluded from it. + // Pinned below the real source-file count with headroom, not at one. + it("reads the tree it claims to sweep", () => { + expect(sourceFiles().length).toBeGreaterThan(1500); + }); + it("has exactly the call sites this file acknowledges", () => { const found = sourceFiles() .filter((rel) => rel !== PICKER) diff --git a/src/__tests__/workout-sport-provenance-guard.test.ts b/src/__tests__/workout-sport-provenance-guard.test.ts index d0a34e2a3..47af62008 100644 --- a/src/__tests__/workout-sport-provenance-guard.test.ts +++ b/src/__tests__/workout-sport-provenance-guard.test.ts @@ -20,20 +20,18 @@ */ import { describe, it, expect } from "vitest"; import { readFileSync } from "node:fs"; -import { join, sep } from "node:path"; -import { globSync } from "node:fs"; +import { join } from "node:path"; + +import { walkSourceFiles } from "./helpers/source-files"; const SRC = join(process.cwd(), "src"); /** Every non-test `.ts` / `.tsx` under `src/`, minus the generated client. */ function sourceFiles(): string[] { - return globSync("**/*.{ts,tsx}", { cwd: SRC }) - .filter( - (p) => !p.startsWith(`generated${sep}`) && !p.startsWith("generated/"), - ) + return walkSourceFiles(SRC, { floor: 3000 }) + .filter((p) => !p.startsWith("generated/")) .filter((p) => !p.includes("__tests__")) .filter((p) => !p.endsWith(".test.ts") && !p.endsWith(".test.tsx")) - .map((p) => p.split(sep).join("/")) .sort(); } @@ -131,6 +129,13 @@ const READ_TIME_NARROWERS = [ ]; describe("T1 — the set of sport-label mappers is frozen", () => { + // A sweep that finds nothing agrees with every allowlist, so the size of + // the tree being read is asserted before anything is concluded from it. + // Pinned below the real source-file count with headroom, not at one. + it("reads the tree it claims to sweep", () => { + expect(sourceFiles().length).toBeGreaterThan(1500); + }); + it("every exported sport-type resolver is registered", () => { const found = new Set(); for (const rel of sourceFiles()) { diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index 0c499ef03..45d3e0e4e 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -8,9 +8,11 @@ import Link from "next/link"; * * 1. The runtime image bundles MaxMind GeoLite2-City and * GeoLite2-ASN databases for offline IP-to-location and - * IP-to-ASN lookups. The CC BY-SA 4.0 licence requires the - * attribution to be reachable from the running application, - * not just the source repository. + * IP-to-ASN lookups. They ship under the GeoLite End User + * License Agreement, which incorporates CC BY-SA 4.0 by + * reference and controls over it; both want the attribution + * reachable from the running application, not just from the + * source repository. * 2. A general home for open-source credits as the project * accumulates more third-party data sources (already true for * the upcoming ICD-10 reference table in the iOS Health import @@ -35,7 +37,7 @@ import Link from "next/link"; // weakening the CSP with `unsafe-inline`. export const dynamic = "force-dynamic"; -const LAST_UPDATED = "2026-05-15"; +const LAST_UPDATED = "2026-08-02"; export const metadata: Metadata = { title: "About — HealthLog", @@ -119,7 +121,9 @@ export default function AboutPage() { > github.com/MBombeck/HealthLog {" "} - and is licensed under the GNU Affero General Public License v3.0. + and is licensed under the PolyForm Noncommercial License 1.0.0. + Releases up to and including v1.15.18 were published under AGPL-3.0 + and stay available under it.

@@ -156,6 +160,15 @@ export default function AboutPage() { ) power the offline IP-to-location and IP-to-carrier lookups that decorate the admin login-overview audit table. They are distributed under the{" "} + + GeoLite End User License Agreement + + , which incorporates the{" "} Creative Commons Attribution-ShareAlike 4.0 International License - - . + {" "} + by reference and controls wherever the two disagree. The attribution + above is what that licence asks for.