From 1ae137eb84f639d13e88f4cf5b051d49ae84af17 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Sun, 2 Aug 2026 16:21:19 +0200
Subject: [PATCH 1/3] test(guards): sweep the directories the structural guards
were skipping
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
`fs.globSync` never matches a leading dot, so every guard built on
`**/*.{ts,tsx}` walked past `src/app/.well-known` — the two OAuth
discovery documents and the apple-app-site-association handler sat
outside fourteen frozen-set sweeps. An unlisted `getSession()` planted
in the authorization-server route left the session-surface guard at nine
passed; it fails now.
Move those fourteen onto one shared walker built on `readdirSync` with
`recursive`, which has no dot rule, and give it a floor every caller has
to state, so a walk that comes back too small throws instead of
reporting a clean tree.
Give the same floor to the guards that compared a discovered set against
a frozen list with nothing asserting the set had been read: gutting the
walker in the inventory-write guard left its invariant test green,
because zero offenders reads exactly like no offenders. Raise the
migration-count floor off one, and assert the locale bundles are found
before the per-locale cases are generated from them.
Document, at each walker that prunes on purpose, what is pruned and that
dot-prefixed directories are not.
---
.../account-payload-consumer-guard.test.ts | 6 ++
.../acting-account-boundary-guard.test.ts | 12 ++-
.../ai-consent-enforcement-guard.test.ts | 12 ++-
.../bearer-scope-enforcement-guard.test.ts | 12 ++-
...cycle-verdict-single-decider-guard.test.ts | 6 ++
.../daily-reads-refetch-guard.test.ts | 19 +++--
.../destructive-control-guard.test.ts | 9 ++-
.../display-transform-rounding-guard.test.ts | 6 +-
.../dynamic-key-exhaustiveness.test.ts | 6 ++
src/__tests__/e2e-route-cleanup-guard.test.ts | 10 +--
.../external-id-stability-guard.test.ts | 12 ++-
src/__tests__/helpers/source-files.ts | 75 +++++++++++++++++++
src/__tests__/i18n-call-site-coverage.test.ts | 6 ++
src/__tests__/i18n-drift-guard.test.ts | 6 ++
src/__tests__/i18n-reverse-coverage.test.ts | 6 ++
.../inventory-write-isolation.test.ts | 13 ++++
.../medications-efficacy-verb-guard.test.ts | 6 ++
.../migration-number-uniqueness.test.ts | 5 +-
.../report-selection-egress-guard.test.ts | 13 ++++
src/__tests__/session-surface-guard.test.ts | 9 ++-
src/__tests__/step-up-elevation-guard.test.ts | 19 +++--
.../success-affordance-guard.test.ts | 7 ++
.../unit-preference-display-guard.test.ts | 11 ++-
...sorge-due-bucket-single-home-guard.test.ts | 16 +++-
.../workout-canonical-identity-guard.test.ts | 15 +++-
.../workout-sport-provenance-guard.test.ts | 19 +++--
26 files changed, 259 insertions(+), 77 deletions(-)
create mode 100644 src/__tests__/helpers/source-files.ts
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()) {
From 5575d4ca2e7b822ae5f488ac5083829d8d963043 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Sun, 2 Aug 2026 16:39:16 +0200
Subject: [PATCH 2/3] build(knip): say what the dead-code gate proves and what
it cannot
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Vitest is a knip plugin, so every file under `src/**/__tests__/**` is an
entry, and a module whose only importer is its own test counts as used
for as long as the test survives. Planting a module with a single test
importing it left `pnpm knip` at exit 0 — the gate reports green on code
production stopped reaching.
Knip can answer the question, just not in the pass CI runs: production
mode drops the plugin's test entries and keeps the entry and project
patterns suffixed with `!`. Mark them, which is inert in the default
pass, so `pnpm knip --production --include files` is one command away.
It stays a review tool rather than a second gate, and the config says
why: twenty-nine files today, some test-only by construction and some
unreachable from production on purpose, plus at least one false positive
where a script's `@/` import does not resolve. Baselining that set
unread would encode noise as an invariant.
Move the config to JSONC so all of the above lives next to the settings
it describes instead of in a commit message nobody will run `git log`
for.
---
.github/workflows/knip.yml | 6 +-
knip.json | 77 ----------------------
knip.jsonc | 131 +++++++++++++++++++++++++++++++++++++
3 files changed, 136 insertions(+), 78 deletions(-)
delete mode 100644 knip.json
create mode 100644 knip.jsonc
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/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"],
+ },
+}
From 89532ee48e44d18fbbf9d9f73e6aa1e8f35aceb8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Sun, 2 Aug 2026 16:45:51 +0200
Subject: [PATCH 3/3] docs(about): state the GeoLite2 licence in full
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The credits page named CC BY-SA 4.0 alone. MaxMind stopped distributing
GeoLite2 under that licence by itself: the databases ship under the
GeoLite End User License Agreement, which incorporates CC BY-SA 4.0 by
reference and controls wherever the two disagree. Name both, in that
order, and link the EULA. The attribution string was already there and
is unchanged; the e2e page check now asserts both licences rather than
the one that no longer stands alone.
Reconcile the self-hosting guide with the same wording, point it at the
current EULA URL, and say why the `/about` page must stay reachable — it
is where an operator's own attribution lives.
Correct the project licence line while on the page: it still claimed
AGPL-3.0, which the page's own footer contradicts. The trunk has been
PolyForm Noncommercial 1.0.0 since v1.15.19.
---
docs/self-hosting/geolite2.md | 13 ++++++++++---
e2e/v1427-public-pages.spec.ts | 6 ++++++
src/app/about/page.tsx | 28 +++++++++++++++++++++-------
3 files changed, 37 insertions(+), 10 deletions(-)
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/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.