mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
Merge branch 'release/v1.45.0' into feat/v145-testkit
# Conflicts: # src/app/api/tokens/__tests__/route.test.ts
This commit is contained in:
commit
ae2636c9e5
84 files changed
+9286
-532
No files matched your search
@@ -93,11 +93,12 @@ jobs:
|
||||
- run: NODE_OPTIONS=--max-old-space-size=8192 pnpm build
|
||||
|
||||
# Client-bundle budget gate — rides this job because it is the one
|
||||
# place CI already has a production build on disk. Catches the
|
||||
# regressions the dead webpack-era tooling (ANALYZE=1, the old
|
||||
# bundle-report) let through: a statically imported message catalog
|
||||
# (~100 KB gz on every route) or a chart import bypassing the shared
|
||||
# chart-runtime boundary (a duplicated ~92 KB gz recharts chunk).
|
||||
# place CI already has a production build on disk. Measures the first
|
||||
# load of every page (files deduplicated by content hash) against its
|
||||
# baseline, the p95 and max across pages, and the distinct client code
|
||||
# (modules deduplicated by id); see the script header. Catches a
|
||||
# statically imported message catalog, a duplicated recharts chunk, or
|
||||
# a heavy module (Zod) pulled into a page that had none.
|
||||
- run: pnpm bundle-check
|
||||
|
||||
- run: pnpm exec playwright test --shard=${{ matrix.shard }}/3
|
||||
|
||||
@@ -16,3 +16,12 @@ docs/api/openapi.yaml
|
||||
|
||||
# Build-generated service worker (the PWA build emits it).
|
||||
public/sw.js
|
||||
|
||||
# Ratchet baselines, written by the guards themselves (RATCHET_WRITE=1). The
|
||||
# writer's JSON layout is the canonical one, so a write-back never fails the
|
||||
# format check.
|
||||
src/__tests__/baselines
|
||||
|
||||
# Benchmark results, written by scripts/bench/run.ts and committed as they
|
||||
# come out, so a re-run diffs cleanly against the last one.
|
||||
scripts/bench/results
|
||||
+117
-10
@@ -1,14 +1,121 @@
|
||||
{
|
||||
"$comment": "Client-bundle budgets, enforced by scripts/check-bundle-budget.mjs (--check) after `next build` in CI. Values are KB gzip and include every emitted lazy route and locale chunk, while route budgets include the shared rootMainFiles baseline. Re-measured at v1.37.2: / 455, /insights 443, /measurements 441, /insights/mood 453, total 3328, baseline 130, lazy catalogs 849. Every per-route ceiling is unchanged. The /insights overview crept a kilobyte over its 445 ceiling when the ECG teaser began importing its overview cap from the classification module, dragging that module's label map and helpers into the eager page; the cap now lives in its own value-only module, and the Health Score's single provenance entry is split out of the full metric-provenance map, so the overview imports the bare number and citation it renders rather than every metric's label map behind them. Both are back under 445 with room to spare. The 42 KB aggregate rise from 3286 (v1.35.0) is all locale-catalog strings: v1.36 and v1.37 added the encounter, mood-context, mood-prognosis, practitioner, workout-batch and account-sharing surfaces, and their copy lands across the six locale catalogs the whole-build sum counts independently (804 to 849 KB). The non-catalog client chunks are flat against v1.35.0, so nothing a person downloads on a given route grew beyond its own ceiling. The aggregate cap therefore moves from 3310 to 3352 (24 KB headroom, matching the previous margin); per-route ceilings, the static-catalog guard and the one-Recharts-chunk guard remain unchanged. v1.37.4's honest-error-state charge then added recoverable read-failure handling to thirteen surfaces (the shared QueryErrorCard and QueryErrorRow primitives plus an isError branch per surface) and rewrote the MCP scope note and the managed-record error copy across the six locales; the measured total rose to 3404 while every per-route ceiling stayed met (/page 457, /insights 445, /measurements 443, /insights/mood 455). The aggregate cap moves from 3352 to 3428, keeping the same margin; nothing a person downloads on a given route grew beyond its ceiling. v1.37.15's async-truth charge then added the refresh settling state with its two-locale-key copy, the index-outcome plain-words line (seven keys across the six catalogs) and the reindex-count wiring; the measured total rose to 3429 while every per-route ceiling stayed met (/page 456, /insights 445, /measurements 442, /insights/mood 455). The aggregate cap moves from 3428 to 3452, keeping the same margin; nothing a person downloads on a given route grew beyond its ceiling. v1.37.27 found /insights sitting a few hundred bytes under 445 on main and a few hundred over it on the release branch — the page had been at its ceiling for three releases and the release only had to breathe on it. The weight it was carrying was not its own: the authenticated shell imported one destination predicate from the `@/lib/record-settings` barrel, and the barrel also re-exports the managed settings-patch schemas, so Zod, the module registry and three validation modules rode into the eager bundle of every authenticated route for a function that reads a URL slug. The shell imports the `classification` leaf now. Measured across all 585 client-rendered routes: none grew by a byte, 100 fell, by 4.7 KB at the median and 8.1 KB at the most; /insights 444 to 437, /measurements 442 to 435, / 456 to 450, /insights/mood 455 to 450. The whole-build sum moved the other way, 3451 to 3476, and that direction is the metric rather than the change: the aggregate counts each emitted chunk once, so a graph that leaves the chunk group every route shares gets counted again wherever it lands — here inside the deferred settings chunks that actually use it. Bytes on disk rose 25 KB; bytes a reader downloads only ever fell. The aggregate cap moves from 3452 to 3500, the same 24 KB headroom as the four re-measurements before it. Per-route ceilings keep their old values rather than tightening to the new floor: the CI runner measures each route about 1.5 KB heavier than a local arm64 build does, and a ceiling set from a local number would fail on a build that had not regressed. The static-catalog guard and the one-Recharts-chunk guard remain unchanged. Raised to 3700 at v1.37.32 for the seventh interface language (Korean): the lazy message catalogs went 6->7 chunks (1026 KB gz measured), pushing the aggregate to 3631 against the old 3500. The raise is entirely the new catalog; no route budget moved, and the four eager route ceilings are unchanged and still passing. Most of the aggregate is the dynamic locale-switch and fallback path that a first paint never loads (see issue #253), so this ceiling guards bytes almost no visitor requests; 3700 leaves ~69 KB for ordinary drift until that double-count is cleaned up. v1.38.14 re-measures the four eager routes and changes what the number means. The ceilings had been re-stated but never re-based since v1.37.4, so /inLine truncated
|
||||
"routeBaselineKbGz": {
|
||||
"/page": 355,
|
||||
"/insights/page": 323,
|
||||
"/measurements/page": 335,
|
||||
"/insights/mood/page": 334,
|
||||
"/settings/[section]/page": 307
|
||||
},
|
||||
"$comment": "Client-bundle budgets, enforced by scripts/check-bundle-budget.mjs --check after `pnpm build` in CI; the script header describes the method. Numbers are KB gzip. routeBaselineKbGz is the measured first load of every page (the shared rootMainFiles plus the page's client-reference chunks, each file counted once by content hash), written with --write-baselines; a page fails when it is more than routeDriftAllowanceKbGz over its baseline, and a page without a baseline fails until one is written in the same change. firstLoadP95KbGz and firstLoadMaxKbGz cap the distribution across pages. uniqueClientKbGz caps the distinct client code: every module counted once by its module id, the lazy message catalogs excluded. maxCatalogKbGz holds each catalog, maxRechartsChunks the one shared chart chunk. The summed-chunk aggregate the gate used up to v1.44.0 (3875 KB there, capped at 3900) is still printed but no longer gated: it counted a module once per chunk group it landed in, so it rose whenever a page moved code into on-demand chunks and needed a raise in most releases while no reader downloaded more. Re-based at v1.45.0 from the v1.44.0 build, measured on a local arm64 machine: 110 pages, first load p50 336, p95 437, max 451 KB; unique client code 1904 KB in 1224 modules. The five pages watched before keep their numbers within a kilobyte (/ 359, /measurements 339, /insights/mood 337, /insights 323, /settings/[section] 311), and the metric pages that no watched route covered before are gated now too (/insights/weight 339, /insights/pulse 339). Caps: p95 447 and max 461 KB (10 KB over the measurement), unique client code 1930 KB (about 25 KB, roughly two releases of new client code). A raise or a re-base is a reviewed change with the measured numbers and the reason written here; earlier re-bases are in the history of this file.",
|
||||
"routeDriftAllowanceKbGz": 20,
|
||||
"totalClientKbGz": 3900,
|
||||
"firstLoadP95KbGz": 447,
|
||||
"firstLoadMaxKbGz": 461,
|
||||
"uniqueClientKbGz": 1930,
|
||||
"maxRechartsChunks": 1,
|
||||
"maxCatalogKbGz": 210
|
||||
"maxCatalogKbGz": 210,
|
||||
"routeBaselineKbGz": {
|
||||
"/_global-error/page": 133,
|
||||
"/_not-found/page": 282,
|
||||
"/about/page": 282,
|
||||
"/achievements/page": 292,
|
||||
"/admin/[section]/page": 451,
|
||||
"/admin/page": 297,
|
||||
"/auth/claim/page": 298,
|
||||
"/auth/login/page": 290,
|
||||
"/auth/register/page": 288,
|
||||
"/c/[token]/page": 284,
|
||||
"/checkups/page": 365,
|
||||
"/checkups/practitioners/page": 289,
|
||||
"/claim/[token]/page": 282,
|
||||
"/coach/conversations/page": 294,
|
||||
"/coach/page": 355,
|
||||
"/coach/plans/page": 288,
|
||||
"/confirm-access/page": 287,
|
||||
"/custom-metrics/[id]/page": 303,
|
||||
"/custom-metrics/[id]/values/page": 311,
|
||||
"/cycle/page": 412,
|
||||
"/documents/page": 378,
|
||||
"/enroll-mfa/page": 305,
|
||||
"/illness/[id]/page": 447,
|
||||
"/illness/page": 421,
|
||||
"/insights/active-energy/page": 336,
|
||||
"/insights/audio-events/page": 337,
|
||||
"/insights/blood-glucose/page": 340,
|
||||
"/insights/blood-pressure/page": 336,
|
||||
"/insights/bmi/page": 331,
|
||||
"/insights/body-fat/page": 336,
|
||||
"/insights/body-temperature/page": 336,
|
||||
"/insights/body-water/page": 336,
|
||||
"/insights/bone-mass/page": 336,
|
||||
"/insights/breathing-disturbances/page": 337,
|
||||
"/insights/cardio-fitness/page": 336,
|
||||
"/insights/cardio-recovery/page": 337,
|
||||
"/insights/catalog/page": 317,
|
||||
"/insights/daylight/page": 336,
|
||||
"/insights/double-support-time/page": 336,
|
||||
"/insights/ecg/[id]/page": 316,
|
||||
"/insights/ecg/page": 316,
|
||||
"/insights/environmental-audio/page": 337,
|
||||
"/insights/falls/page": 336,
|
||||
"/insights/fat-free-mass/page": 336,
|
||||
"/insights/fat-mass/page": 336,
|
||||
"/insights/flights-climbed/page": 336,
|
||||
"/insights/grip-strength/page": 337,
|
||||
"/insights/headphone-audio/page": 336,
|
||||
"/insights/health-score/page": 315,
|
||||
"/insights/hrv/page": 337,
|
||||
"/insights/lean-body-mass/page": 336,
|
||||
"/insights/medications/page": 338,
|
||||
"/insights/mood/page": 337,
|
||||
"/insights/muscle-mass/page": 337,
|
||||
"/insights/nutrients/page": 319,
|
||||
"/insights/oxygen/page": 337,
|
||||
"/insights/page": 323,
|
||||
"/insights/pain/page": 336,
|
||||
"/insights/pulse-wave-velocity/page": 336,
|
||||
"/insights/pulse/page": 339,
|
||||
"/insights/recovery/page": 330,
|
||||
"/insights/respiratory-rate/page": 336,
|
||||
"/insights/resting-pulse/page": 337,
|
||||
"/insights/scores/[metric]/page": 328,
|
||||
"/insights/six-minute-walk/page": 336,
|
||||
"/insights/skin-temperature/page": 338,
|
||||
"/insights/sleep/page": 337,
|
||||
"/insights/sources/page": 321,
|
||||
"/insights/stair-ascent-speed/page": 336,
|
||||
"/insights/stair-descent-speed/page": 336,
|
||||
"/insights/step-length/page": 336,
|
||||
"/insights/steps/page": 337,
|
||||
"/insights/values/[type]/page": 350,
|
||||
"/insights/vascular-age/page": 337,
|
||||
"/insights/visceral-fat/page": 336,
|
||||
"/insights/waist-to-height/page": 336,
|
||||
"/insights/waist/page": 336,
|
||||
"/insights/walking-asymmetry/page": 336,
|
||||
"/insights/walking-distance/page": 336,
|
||||
"/insights/walking-heart-rate/page": 337,
|
||||
"/insights/walking-speed/page": 336,
|
||||
"/insights/walking-steadiness/page": 336,
|
||||
"/insights/weight/page": 339,
|
||||
"/insights/workouts/[id]/page": 437,
|
||||
"/insights/workouts/page": 437,
|
||||
"/insights/wrist-temperature/page": 336,
|
||||
"/invite/[token]/page": 282,
|
||||
"/labs/[biomarkerId]/page": 327,
|
||||
"/labs/[biomarkerId]/values/page": 335,
|
||||
"/labs/page": 423,
|
||||
"/measurements/page": 339,
|
||||
"/medications/[id]/history/page": 283,
|
||||
"/medications/[id]/page": 402,
|
||||
"/medications/new/page": 282,
|
||||
"/medications/page": 355,
|
||||
"/mental-wellbeing/page": 297,
|
||||
"/mood/page": 438,
|
||||
"/notifications/page": 289,
|
||||
"/onboarding/[step]/page": 445,
|
||||
"/onboarding/handover/page": 295,
|
||||
"/onboarding/page": 286,
|
||||
"/page": 359,
|
||||
"/privacy/page": 282,
|
||||
"/profile/page": 285,
|
||||
"/settings/[section]/page": 311,
|
||||
"/settings/layout/[module]/page": 380,
|
||||
"/settings/modules/[module]/page": 374,
|
||||
"/settings/page": 282,
|
||||
"/timeline/page": 331,
|
||||
"/vaccinations/page": 327
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,9 @@ notes, and machine-readable specs that ship alongside the source:
|
||||
- [`contributing/ui-standards.md`](./contributing/ui-standards.md) — the
|
||||
binding UI standards: card anatomy, spacing, text colour, tokens and
|
||||
which primitive to use where. Read before touching any surface.
|
||||
- [`contributing/benchmarks.md`](./contributing/benchmarks.md) — the
|
||||
large-account benchmark (latency and statements per request) and the
|
||||
statement budgets on the hot routes.
|
||||
- [`ui-guidelines.md`](./ui-guidelines.md) — UI / a11y house style; where
|
||||
it disagrees with the UI standards, the standards win.
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# Benchmarks
|
||||
|
||||
Two tools measure the server's read and write paths. Use them before and after any change to a hot path, and put both numbers in the PR.
|
||||
|
||||
- **`scripts/bench/run.ts`** measures latency (p50, p95, max) and SQL statements per request against a seeded account of about 1.26 million measurement rows, through the production build. It is a local tool, not a CI gate: latency depends on the machine.
|
||||
- **`tests/integration/query-budget-hot-routes.test.ts`** holds a statement budget per hot route on a small fixture. Statement counts are exact on a fixed fixture, so this one is a gate: a change that adds a query per row, or a second read of something already loaded, fails it.
|
||||
|
||||
## The large-account benchmark
|
||||
|
||||
### 1. A throwaway Postgres with pg_stat_statements
|
||||
|
||||
Never point the seed or the benchmark at a database that holds real data. They write under a synthetic user, and the seed's `--reset` deletes that user with everything it owns.
|
||||
|
||||
```sh
|
||||
docker run -d --name hl-bench-db --shm-size=256m \
|
||||
-e POSTGRES_USER=bench -e POSTGRES_PASSWORD=bench -e POSTGRES_DB=healthlog_bench \
|
||||
-p 55432:5432 postgres:16.15-alpine \
|
||||
postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=top
|
||||
```
|
||||
|
||||
The image and the two settings are the ones `docker-compose.yml` runs in production.
|
||||
|
||||
### 2. Environment, migrations, seed
|
||||
|
||||
```sh
|
||||
export DATABASE_URL="postgresql://bench:bench@127.0.0.1:55432/healthlog_bench?schema=public"
|
||||
export ENCRYPTION_KEYS='{"v1":"<64 hex>"}' ENCRYPTION_ACTIVE_KEY_ID=v1
|
||||
export API_TOKEN_HMAC_KEY="<64 hex>"
|
||||
|
||||
pnpm db:migrate:deploy
|
||||
pnpm dlx tsx scripts/bench/seed-large-account.ts --reset --anchor 2026-10-22
|
||||
```
|
||||
|
||||
Any throwaway key values work, as long as the seed, the benchmark and the server see the same ones.
|
||||
|
||||
The seed writes ten years for one user: Apple Health dense rows (heart rate about every ten minutes, HRV, SpO2, respiratory rate, gait, audio), Withings blood pressure, pulse and scale, a weekly manual weight, three years of Oura resting heart rate and RMSSD, 90 days of Nightscout CGM, `stats:` and per-sample cumulative rows, nightly sleep stages, about 3,000 workouts, three years of mood and of three medications with their intake events. It then folds the rollup tier the way the boot backfill does and runs `ANALYZE`. It takes about 80 seconds on the Mac mini.
|
||||
|
||||
The seed is deterministic for a given `--anchor`: values and timestamps are functions of the row index and that day. Without `--anchor` it anchors on today, which is what you want when the benchmark should see recent data.
|
||||
|
||||
### 3. A production build
|
||||
|
||||
```sh
|
||||
pnpm build
|
||||
```
|
||||
|
||||
### 4. Run
|
||||
|
||||
```sh
|
||||
pnpm dlx tsx scripts/bench/run.ts --out scripts/bench/results/<label>
|
||||
```
|
||||
|
||||
The script boots the build once with every role, so the worker creates its queues as it does in production, drops whatever that boot scheduled, and then runs the measurement against a web-only process (`HEALTHLOG_PROCESS_TYPE=web`) under the production heap ceiling (`--max-old-space-size=792`, the value Node derives in the 1.5 GB production container). Without the worker, its polling does not land in every statement count. It prints the server's idle statement rate so the remaining background is visible.
|
||||
|
||||
For each route it sends three warm-up requests, then twenty measured ones in each of two modes, one request at a time:
|
||||
|
||||
- **cold**: before every request a one-row ingest marked as a manual sync evicts the user's caches, so the request rebuilds what it serves. This is a reader's first look after a sync.
|
||||
- **warm**: back to back, what a cache hit costs.
|
||||
|
||||
The batch ingest posts 5,000 fresh heart-rate rows as ten requests of 500, the route's per-request limit, and removes them again at the end.
|
||||
|
||||
Statements are the change in `sum(calls)` of `pg_stat_statements` over the database, read before and after each request, once no statement is in flight and the counter holds still. Fire-and-forget writes a request starts are therefore counted with it.
|
||||
|
||||
Options: `--iterations`, `--warmup`, `--user`, `--heap-mb`, `--port`, and `--base-url` to measure a server you started yourself.
|
||||
|
||||
The output is `<label>.json` (every number, plus the commit, Node and Postgres versions and the row count) and `<label>.md` (the table). Commit the pair when a change is meant to move a number, so the PR shows before and after.
|
||||
|
||||
### Reading the numbers
|
||||
|
||||
Run on a quiet machine; a parallel build or test run moves latency by tens of percent and leaves the statement counts alone. Compare statement counts across machines, latency only across runs on the same machine. A local number is only valid under the production heap ceiling, which the script sets.
|
||||
|
||||
## Statement budgets in the integration suite
|
||||
|
||||
`tests/integration/query-budget-hot-routes.test.ts` seeds a small account (four months of the main types with overlapping sources, sleep, glucose, a medication, workouts and mood), and counts the statements of one request per hot route on a cache miss: `/api/auth/me`, the dashboard summary and snapshot, the pulse series over 30, 365 and 3650 days, the day view, insights health status, correlations, and a 500-row batch ingest.
|
||||
|
||||
The counter (`tests/integration/query-counter.ts`) wraps `pg`'s `Client.prototype.query` on the copy of `pg` the Prisma adapter loads, and settles background tasks before it closes the window.
|
||||
|
||||
Each budget is the measured count. When a change lowers one, lower the budget in the same change. When a change has to raise one, raise it with the reason in the PR. A failure prints every statement the request sent, grouped by text, so an added loop shows up as one statement with a count next to it.
|
||||
|
||||
Run it like any integration file:
|
||||
|
||||
```sh
|
||||
pnpm test:integration tests/integration/query-budget-hot-routes.test.ts
|
||||
```
|
||||
@@ -5,7 +5,7 @@ import type { APIRequestContext } from "@playwright/test";
|
||||
|
||||
import { ADMIN_SECTION_SLUGS } from "@/components/admin/section-slugs";
|
||||
import { MODULE_SETTINGS_PAGE_IDS } from "@/components/settings/module-settings-page-ids";
|
||||
import { SETTINGS_SECTION_SLUGS } from "@/components/settings/section-slugs";
|
||||
import { SETTINGS_SECTION_SLUGS } from "@/lib/settings/section-slugs";
|
||||
|
||||
import { expect, test } from "./setup/test";
|
||||
import { MOBILE_ROUTES_STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Unit tests for the `healthlog/import-boundaries` rule: src/lib never
|
||||
* imports components or app, and a "use client" file never makes a value
|
||||
* import of a server-only module. Covers alias and relative specifiers,
|
||||
* type-only imports, dynamic imports, re-exports, the allow map and its
|
||||
* stale-entry report.
|
||||
*/
|
||||
|
||||
import { describe, it } from "vitest";
|
||||
import { RuleTester } from "eslint";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
import rule from "../import-boundaries.js";
|
||||
|
||||
// The TypeScript parser the app lints with, taken from the same config so
|
||||
// `import type` reaches the rule as it does in `pnpm lint`.
|
||||
const tsParser = nextTs.find((c) => c.languageOptions?.parser).languageOptions
|
||||
.parser;
|
||||
|
||||
RuleTester.describe = describe;
|
||||
RuleTester.it = it;
|
||||
RuleTester.itOnly = it.only;
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
ecmaVersion: 2023,
|
||||
sourceType: "module",
|
||||
parserOptions: { ecmaFeatures: { jsx: true } },
|
||||
},
|
||||
});
|
||||
|
||||
const LIB_FILE = "/repo/src/lib/medications/example.ts";
|
||||
const CLIENT_FILE = "/repo/src/components/medications/example.tsx";
|
||||
|
||||
ruleTester.run("import-boundaries", rule, {
|
||||
valid: [
|
||||
// lib importing lib, a package, and itself relatively.
|
||||
{ code: 'import { a } from "@/lib/x";', filename: LIB_FILE },
|
||||
{ code: 'import { z } from "zod";', filename: LIB_FILE },
|
||||
{ code: 'import { b } from "../y";', filename: LIB_FILE },
|
||||
// components importing components and lib is the right direction.
|
||||
{
|
||||
code: 'import { C } from "@/components/ui/card";',
|
||||
filename: CLIENT_FILE,
|
||||
},
|
||||
// Tests are out of scope.
|
||||
{
|
||||
code: 'import { C } from "@/components/ui/card";',
|
||||
filename: "/repo/src/lib/medications/__tests__/x.test.ts",
|
||||
},
|
||||
// An allowed lib file that still imports upward.
|
||||
{
|
||||
code: 'import { C } from "@/components/ui/card";',
|
||||
filename: LIB_FILE,
|
||||
options: [{ allow: { "src/lib/medications/example.ts": "reason" } }],
|
||||
},
|
||||
// A server component (no directive) may import the database client.
|
||||
{
|
||||
code: 'import { prisma } from "@/lib/db";',
|
||||
filename: "/repo/src/app/page.tsx",
|
||||
},
|
||||
// A client file may import types from server modules and the enums entry.
|
||||
{
|
||||
code: '"use client";\nimport type { MeasurementType } from "@/generated/prisma/client";',
|
||||
filename: CLIENT_FILE,
|
||||
},
|
||||
{
|
||||
code: '"use client";\nimport { type BackupRestoreJobView } from "@/lib/jobs/backup-restore";',
|
||||
filename: CLIENT_FILE,
|
||||
},
|
||||
{
|
||||
code: '"use client";\nimport { MeasurementType } from "@/generated/prisma/enums";',
|
||||
filename: CLIENT_FILE,
|
||||
},
|
||||
// A string that only looks like the directive is not one.
|
||||
{
|
||||
code: 'const x = "use client";\nimport { prisma } from "@/lib/db";',
|
||||
filename: CLIENT_FILE,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: 'import { C } from "@/components/ui/card";',
|
||||
filename: LIB_FILE,
|
||||
errors: [{ messageId: "libUpward" }],
|
||||
},
|
||||
{
|
||||
code: 'import type { P } from "@/app/settings/page";',
|
||||
filename: LIB_FILE,
|
||||
errors: [{ messageId: "libUpward" }],
|
||||
},
|
||||
// Relative specifiers resolve to the same layer check.
|
||||
{
|
||||
code: 'import { C } from "../../components/ui/card";',
|
||||
filename: LIB_FILE,
|
||||
errors: [{ messageId: "libUpward" }],
|
||||
},
|
||||
{
|
||||
code: 'export { C } from "@/components/ui/card";',
|
||||
filename: LIB_FILE,
|
||||
errors: [{ messageId: "libUpward" }],
|
||||
},
|
||||
{
|
||||
code: 'const m = await import("@/components/ui/card");',
|
||||
filename: LIB_FILE,
|
||||
errors: [{ messageId: "libUpward" }],
|
||||
},
|
||||
// An allowed file that stopped importing upward must leave the list.
|
||||
{
|
||||
code: 'import { a } from "@/lib/x";',
|
||||
filename: LIB_FILE,
|
||||
options: [{ allow: { "src/lib/medications/example.ts": "reason" } }],
|
||||
errors: [{ messageId: "staleAllow" }],
|
||||
},
|
||||
// Server-only modules in a client file.
|
||||
{
|
||||
code: '"use client";\nimport { prisma } from "@/lib/db";',
|
||||
filename: CLIENT_FILE,
|
||||
errors: [{ messageId: "clientServerOnly" }],
|
||||
},
|
||||
{
|
||||
code: '"use client";\nimport { encrypt } from "@/lib/crypto";',
|
||||
filename: CLIENT_FILE,
|
||||
errors: [{ messageId: "clientServerOnly" }],
|
||||
},
|
||||
{
|
||||
code: '"use client";\nimport { MeasurementType } from "@/generated/prisma/client";',
|
||||
filename: CLIENT_FILE,
|
||||
errors: [{ messageId: "clientServerOnly" }],
|
||||
},
|
||||
{
|
||||
code: '"use client";\nimport { enqueue } from "@/lib/jobs/boss-instance";',
|
||||
filename: CLIENT_FILE,
|
||||
errors: [{ messageId: "clientServerOnly" }],
|
||||
},
|
||||
{
|
||||
code: '"use client";\nimport { cookies } from "next/headers";',
|
||||
filename: CLIENT_FILE,
|
||||
errors: [{ messageId: "clientServerOnly" }],
|
||||
},
|
||||
{
|
||||
code: '"use client";\nimport { Pool } from "pg";',
|
||||
filename: CLIENT_FILE,
|
||||
errors: [{ messageId: "clientServerOnly" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* @fileoverview ESLint rule — import direction between the layers of `src`.
|
||||
*
|
||||
* Two boundaries, both checked per import statement so the mistake shows up
|
||||
* in the editor rather than in a later sweep:
|
||||
*
|
||||
* 1. `src/lib/**` never imports from `src/components/**` or `src/app/**`.
|
||||
* The direction is `app → components → hooks → lib`. A lib module that
|
||||
* reaches upward drags a component folder into server jobs and makes
|
||||
* the layers impossible to split. Type-only imports count too: the
|
||||
* dependency is the same, only erased at build time.
|
||||
*
|
||||
* 2. A `"use client"` file never makes a value import of a server-only
|
||||
* module: the database client, the crypto module, the Prisma client
|
||||
* entry, the job queue, `pg`, `next/headers`. Type-only imports are
|
||||
* erased and stay legal, and the Prisma `enums` entry is browser-safe.
|
||||
* The transitive version of this check is
|
||||
* `src/__tests__/client-bundle-server-import-guard.test.ts`, which walks
|
||||
* the whole client graph; this rule is the direct, local half.
|
||||
*
|
||||
* Existing lib → components imports are listed in the `allow` option, file
|
||||
* by file, each with the reason it has not moved yet. A listed file that no
|
||||
* longer imports upward is reported too, so the list can only shrink.
|
||||
*
|
||||
* WHAT THIS DOES NOT CATCH
|
||||
*
|
||||
* A dependency through a re-export in a package outside `src`, and a
|
||||
* `require()` call. Neither occurs in the tree today.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const path = require("node:path");
|
||||
|
||||
function toPosix(filename) {
|
||||
return filename.replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
function isTestFile(filename) {
|
||||
return (
|
||||
/\/__tests__\//.test(filename) ||
|
||||
/\.(test|spec)\.[cm]?[jt]sx?$/.test(filename) ||
|
||||
/\/__mocks__\//.test(filename)
|
||||
);
|
||||
}
|
||||
|
||||
/** Path of `filename` relative to the repository's `src/`, or null. */
|
||||
function srcRelative(filename) {
|
||||
const idx = filename.lastIndexOf("/src/");
|
||||
return idx === -1 ? null : filename.slice(idx + 1);
|
||||
}
|
||||
|
||||
/** Where a specifier lands inside `src/`, or null for a package. */
|
||||
function targetInSrc(spec, filename) {
|
||||
if (spec.startsWith("@/")) return `src/${spec.slice(2)}`;
|
||||
if (spec.startsWith(".")) {
|
||||
const abs = toPosix(path.resolve(path.dirname(filename), spec));
|
||||
return srcRelative(abs);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const SERVER_ONLY_PACKAGES = [
|
||||
/^pg$/,
|
||||
/^pg-boss$/,
|
||||
/^@prisma\//,
|
||||
/^next\/headers$/,
|
||||
];
|
||||
|
||||
const SERVER_ONLY_SRC = [
|
||||
/^src\/lib\/db(-session)?$/,
|
||||
/^src\/lib\/crypto(\/|$)/,
|
||||
/^src\/lib\/jobs\//,
|
||||
/^src\/generated\/prisma\/(client|internal|models)(\/|$)/,
|
||||
];
|
||||
|
||||
function isServerOnly(spec, filename) {
|
||||
if (SERVER_ONLY_PACKAGES.some((re) => re.test(spec))) return true;
|
||||
const target = targetInSrc(spec, filename);
|
||||
if (!target) return false;
|
||||
const bare = target.replace(/\.(ts|tsx|js|mjs)$/, "");
|
||||
return SERVER_ONLY_SRC.some((re) => re.test(bare));
|
||||
}
|
||||
|
||||
function isTypeOnly(node) {
|
||||
if (node.importKind === "type" || node.exportKind === "type") return true;
|
||||
const specifiers = node.specifiers ?? [];
|
||||
return (
|
||||
specifiers.length > 0 &&
|
||||
specifiers.every((s) => s.importKind === "type" || s.exportKind === "type")
|
||||
);
|
||||
}
|
||||
|
||||
function hasUseClient(program) {
|
||||
for (const stmt of program.body) {
|
||||
if (stmt.type !== "ExpressionStatement" || !stmt.directive) break;
|
||||
if (stmt.directive === "use client") return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @type {import("eslint").Rule.RuleModule} */
|
||||
const importBoundariesRule = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"Keep src/lib below components and app, and server-only modules out of client files.",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allow: {
|
||||
type: "object",
|
||||
additionalProperties: { type: "string", minLength: 1 },
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
libUpward:
|
||||
"src/lib must not import {{target}}. Move the shared piece into src/lib (or the importer into the component folder); an exception goes into the rule's `allow` map in eslint.config.mjs with its reason.",
|
||||
staleAllow:
|
||||
"This file is in the import-boundaries `allow` map but no longer imports from src/components or src/app. Remove the entry so the list keeps shrinking.",
|
||||
clientServerOnly:
|
||||
'A "use client" file must not import {{spec}}: it is server-only and would ship to the browser. Import a type with `import type`, or move the call behind an API route or a server component.',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const filename = toPosix(context.filename ?? context.getFilename?.() ?? "");
|
||||
const rel = srcRelative(filename);
|
||||
if (!rel || isTestFile(filename)) return {};
|
||||
|
||||
const allow = (context.options[0] && context.options[0].allow) || {};
|
||||
const inLib = rel.startsWith("src/lib/");
|
||||
const allowed = inLib && Object.hasOwn(allow, rel);
|
||||
let upwardSeen = false;
|
||||
let isClient = false;
|
||||
|
||||
function check(node, source) {
|
||||
if (!source || typeof source.value !== "string") return;
|
||||
const spec = source.value;
|
||||
if (inLib) {
|
||||
const target = targetInSrc(spec, filename);
|
||||
if (
|
||||
target &&
|
||||
(target.startsWith("src/components/") ||
|
||||
target.startsWith("src/app/"))
|
||||
) {
|
||||
upwardSeen = true;
|
||||
if (!allowed) {
|
||||
context.report({
|
||||
node: source,
|
||||
messageId: "libUpward",
|
||||
data: { target: spec },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isClient && !isTypeOnly(node) && isServerOnly(spec, filename)) {
|
||||
context.report({
|
||||
node: source,
|
||||
messageId: "clientServerOnly",
|
||||
data: { spec },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
Program(program) {
|
||||
isClient = hasUseClient(program);
|
||||
},
|
||||
ImportDeclaration(node) {
|
||||
check(node, node.source);
|
||||
},
|
||||
ExportNamedDeclaration(node) {
|
||||
if (node.source) check(node, node.source);
|
||||
},
|
||||
ExportAllDeclaration(node) {
|
||||
check(node, node.source);
|
||||
},
|
||||
ImportExpression(node) {
|
||||
if (node.source && node.source.type === "Literal") {
|
||||
check(node, node.source);
|
||||
}
|
||||
},
|
||||
"Program:exit"(program) {
|
||||
if (allowed && !upwardSeen) {
|
||||
context.report({ node: program, messageId: "staleAllow" });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = importBoundariesRule;
|
||||
@@ -21,6 +21,8 @@
|
||||
* toLocaleString().
|
||||
* - `no-raw-card-shell` — a card surface is a <Card> or a shell
|
||||
* primitive, not bg-card + border + rounded-xl.
|
||||
* - `import-boundaries` — src/lib never imports components or app, and
|
||||
* a "use client" file imports no server module.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
@@ -36,6 +38,7 @@ const noUtcDayKey = require("./no-utc-day-key.js");
|
||||
const noPlanningTags = require("./no-planning-tags.js");
|
||||
const noUnlocalisedDecimal = require("./no-unlocalised-decimal.js");
|
||||
const noRawCardShell = require("./no-raw-card-shell.js");
|
||||
const importBoundaries = require("./import-boundaries.js");
|
||||
|
||||
module.exports = {
|
||||
rules: {
|
||||
@@ -55,5 +58,6 @@ module.exports = {
|
||||
"no-planning-tags": noPlanningTags,
|
||||
"no-unlocalised-decimal": noUnlocalisedDecimal,
|
||||
"no-raw-card-shell": noRawCardShell,
|
||||
"import-boundaries": importBoundaries,
|
||||
},
|
||||
};
|
||||
@@ -176,6 +176,27 @@ const eslintConfig = defineConfig([
|
||||
],
|
||||
},
|
||||
],
|
||||
// Layer direction: src/lib never imports src/components or src/app, and
|
||||
// a "use client" file makes no value import of the database client,
|
||||
// crypto, the Prisma client entry, the job queue, pg or next/headers.
|
||||
// The files below still import upward; each says why it has not moved.
|
||||
// A listed file that stops importing upward fails until it is removed,
|
||||
// so the map only shrinks. See the rule header.
|
||||
"healthlog/import-boundaries": [
|
||||
"error",
|
||||
{
|
||||
allow: {
|
||||
"src/lib/insights/metric-catalog.ts":
|
||||
"reads SUB_PAGE_TABS, which is declared inside the client tab strip component together with its labels and order",
|
||||
"src/lib/insights/target-adjust-context.tsx":
|
||||
"a React provider that renders TargetEditSheet; it belongs under components but has importers across 21 insight pages",
|
||||
"src/lib/errors/error-toast.tsx":
|
||||
"a React toast renderer that renders ErrorIdLine; it belongs under components but is imported by most client surfaces",
|
||||
"src/lib/module-list-prefs.ts":
|
||||
"a client hook module that raises the shared outcome toast; it moves with the module list settings surface",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// v1.28.17 — every recharts-rendering component funnels through the
|
||||
|
||||
@@ -28,6 +28,7 @@ typecheck.
|
||||
- **`repair-intake-anomalies.ts`** — repair historic medication-ledger defects (duplicate dose-slot rows, implausible `taken_at`); dry-run by default, see `docs/ops/intake-repair.md`.
|
||||
- **`backfill-rollups.ts`**, **`drain-per-sample-cumulative.ts`**, **`backfill-mood-note-column.ts`** — historical data backfills (note: the boot-time `rollup-full-backfill` queue handles new accounts automatically; the CLI is the manual fallback).
|
||||
- **`seed-demo.ts`** + **`seed-demo-v15.sql`** — populate a demo tenant.
|
||||
- **`bench/`** — `seed-large-account.ts` (a synthetic ten-year account of about 1.26 million measurement rows) and `run.ts` (latency and SQL statements per request against it); see `docs/contributing/benchmarks.md`. Results land in `bench/results/`.
|
||||
- **`test-notifications.ts`** — exercise the notification dispatcher + reminder check.
|
||||
- **`fetch-geolite2.sh`** — download the MaxMind GeoLite2 MMDBs (needs a licence key) before `docker build`.
|
||||
- **`install-hooks.sh`** — install the local git hooks.
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
{
|
||||
"meta": {
|
||||
"label": "v1.44.0-baseline",
|
||||
"commit": "d640eea32",
|
||||
"date": "2026-10-22T03:54:05.746Z",
|
||||
"node": "v22.23.2",
|
||||
"postgres": "16.15",
|
||||
"cpus": 14,
|
||||
"loadAverage": [
|
||||
9.4,
|
||||
20.1,
|
||||
20
|
||||
],
|
||||
"measurements": 1266549,
|
||||
"iterations": 20,
|
||||
"warmup": 3,
|
||||
"heapMb": 792,
|
||||
"idleStatementsPerSecond": 0,
|
||||
"batch5k": {
|
||||
"totalMs": 1722,
|
||||
"totalStatements": 424
|
||||
}
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"name": "/api/auth/me",
|
||||
"mode": "cold",
|
||||
"request": "GET /api/auth/me",
|
||||
"n": 20,
|
||||
"p50Ms": 12.1,
|
||||
"p95Ms": 17.4,
|
||||
"maxMs": 18.4,
|
||||
"statementsMedian": 19,
|
||||
"statementsMax": 19,
|
||||
"failures": 0
|
||||
},
|
||||
{
|
||||
"name": "/api/auth/me",
|
||||
"mode": "warm",
|
||||
"request": "GET /api/auth/me",
|
||||
"n": 20,
|
||||
"p50Ms": 16.6,
|
||||
"p95Ms": 22.2,
|
||||
"maxMs": 24.4,
|
||||
"statementsMedian": 19,
|
||||
"statementsMax": 19,
|
||||
"failures": 0
|
||||
},
|
||||
{
|
||||
"name": "dashboard summary",
|
||||
"mode": "cold",
|
||||
"request": "GET /api/dashboard/summary",
|
||||
"n": 20,
|
||||
"p50Ms": 205.6,
|
||||
"p95Ms": 233.2,
|
||||
"maxMs": 275.3,
|
||||
"statementsMedian": 31,
|
||||
"statementsMax": 32,
|
||||
"failures": 0
|
||||
},
|
||||
{
|
||||
"name": "dashboard summary",
|
||||
"mode": "warm",
|
||||
"request": "GET /api/dashboard/summary",
|
||||
"n": 20,
|
||||
"p50Ms": 12.8,
|
||||
"p95Ms": 14.3,
|
||||
"maxMs": 27.5,
|
||||
"statementsMedian": 7,
|
||||
"statementsMax": 7,
|
||||
"failures": 0
|
||||
},
|
||||
{
|
||||
"name": "dashboard snapshot",
|
||||
"mode": "cold",
|
||||
"request": "GET /api/dashboard/snapshot",
|
||||
"n": 20,
|
||||
"p50Ms": 2157.8,
|
||||
"p95Ms": 2252.7,
|
||||
"maxMs": 2292.9,
|
||||
"statementsMedian": 158,
|
||||
"statementsMax": 161,
|
||||
"failures": 0
|
||||
},
|
||||
{
|
||||
"name": "dashboard snapshot",
|
||||
"mode": "warm",
|
||||
"request": "GET /api/dashboard/snapshot",
|
||||
"n": 20,
|
||||
"p50Ms": 15.6,
|
||||
"p95Ms": 23.5,
|
||||
"maxMs": 25.6,
|
||||
"statementsMedian": 13,
|
||||
"statementsMax": 16,
|
||||
"failures": 0
|
||||
},
|
||||
{
|
||||
"name": "series pulse 30 d",
|
||||
"mode": "cold",
|
||||
"request": "GET /api/measurements/series?kind=pulse&days=30",
|
||||
"n": 20,
|
||||
"p50Ms": 50.8,
|
||||
"p95Ms": 58.7,
|
||||
"maxMs": 58.9,
|
||||
"statementsMedian": 8,
|
||||
"statementsMax": 9,
|
||||
"failures": 0
|
||||
},
|
||||
{
|
||||
"name": "series pulse 30 d",
|
||||
"mode": "warm",
|
||||
"request": "GET /api/measurements/series?kind=pulse&days=30",
|
||||
"n": 20,
|
||||
"p50Ms": 52.3,
|
||||
"p95Ms": 59.5,
|
||||
"maxMs": 59.9,
|
||||
"statementsMedian": 8,
|
||||
"statementsMax": 8,
|
||||
"failures": 0
|
||||
},
|
||||
{
|
||||
"name": "series pulse 365 d",
|
||||
"mode": "cold",
|
||||
"request": "GET /api/measurements/series?kind=pulse&days=365",
|
||||
"n": 20,
|
||||
"p50Ms": 289,
|
||||
"p95Ms": 311.3,
|
||||
"maxMs": 363.7,
|
||||
"statementsMedian": 8,
|
||||
"statementsMax": 9,
|
||||
"failures": 0
|
||||
},
|
||||
{
|
||||
"name": "series pulse 365 d",
|
||||
"mode": "warm",
|
||||
"request": "GET /api/measurements/series?kind=pulse&days=365",
|
||||
"n": 20,
|
||||
"p50Ms": 274.3,
|
||||
"p95Ms": 355.8,
|
||||
"maxMs": 366.8,
|
||||
"statementsMedian": 8,
|
||||
"statementsMax": 8,
|
||||
"failures": 0
|
||||
},
|
||||
{
|
||||
"name": "series pulse 3650 d",
|
||||
"mode": "cold",
|
||||
"request": "GET /api/measurements/series?kind=pulse&days=3650",
|
||||
"n": 20,
|
||||
"p50Ms": 1502.6,
|
||||
"p95Ms": 2117.6,
|
||||
"maxMs": 2122.9,
|
||||
"statementsMedian": 8,
|
||||
"statementsMax": 9,
|
||||
"failures": 0
|
||||
},
|
||||
{
|
||||
"name": "series pulse 3650 d",
|
||||
"mode": "warm",
|
||||
"request": "GET /api/measurements/series?kind=pulse&days=3650",
|
||||
"n": 20,
|
||||
"p50Ms": 1490.3,
|
||||
"p95Ms": 1505.5,
|
||||
"maxMs": 1555.2,
|
||||
"statementsMedian": 8,
|
||||
"statementsMax": 11,
|
||||
"failures": 0
|
||||
},
|
||||
{
|
||||
"name": "day view",
|
||||
"mode": "cold",
|
||||
"request": "GET /api/day/2026-10-21",
|
||||
"n": 20,
|
||||
"p50Ms": 61.7,
|
||||
"p95Ms": 77.5,
|
||||
"maxMs": 86.1,
|
||||
"statementsMedian": 49,
|
||||
"statementsMax": 50,
|
||||
"failures": 0
|
||||
},
|
||||
{
|
||||
"name": "day view",
|
||||
"mode": "warm",
|
||||
"request": "GET /api/day/2026-10-21",
|
||||
"n": 20,
|
||||
"p50Ms": 49,
|
||||
"p95Ms": 55.6,
|
||||
"maxMs": 62.5,
|
||||
"statementsMedian": 48,
|
||||
"statementsMax": 48,
|
||||
"failures": 0
|
||||
},
|
||||
{
|
||||
"name": "insights health status",
|
||||
"mode": "cold",
|
||||
"request": "GET /api/insights/health-status",
|
||||
"n": 20,
|
||||
"p50Ms": 124.1,
|
||||
"p95Ms": 139.5,
|
||||
"maxMs": 141.5,
|
||||
"statementsMedian": 68,
|
||||
"statementsMax": 68,
|
||||
"failures": 0
|
||||
},
|
||||
{
|
||||
"name": "insights health status",
|
||||
"mode": "warm",
|
||||
"request": "GET /api/insights/health-status",
|
||||
"n": 20,
|
||||
"p50Ms": 124.7,
|
||||
"p95Ms": 157.8,
|
||||
"maxMs": 165.3,
|
||||
"statementsMedian": 68,
|
||||
"statementsMax": 68,
|
||||
"failures": 0
|
||||
},
|
||||
{
|
||||
"name": "insights correlations",
|
||||
"mode": "cold",
|
||||
"request": "GET /api/insights/correlations",
|
||||
"n": 20,
|
||||
"p50Ms": 95.4,
|
||||
"p95Ms": 105,
|
||||
"maxMs": 108.8,
|
||||
"statementsMedian": 45,
|
||||
"statementsMax": 45,
|
||||
"failures": 0
|
||||
},
|
||||
{
|
||||
"name": "insights correlations",
|
||||
"mode": "warm",
|
||||
"request": "GET /api/insights/correlations",
|
||||
"n": 20,
|
||||
"p50Ms": 13,
|
||||
"p95Ms": 15.4,
|
||||
"maxMs": 15.9,
|
||||
"statementsMedian": 8,
|
||||
"statementsMax": 8,
|
||||
"failures": 0
|
||||
},
|
||||
{
|
||||
"name": "batch ingest 5k (per 500)",
|
||||
"mode": "write",
|
||||
"request": "POST /api/measurements/batch ×10",
|
||||
"n": 10,
|
||||
"p50Ms": 163,
|
||||
"p95Ms": 220.7,
|
||||
"maxMs": 220.7,
|
||||
"statementsMedian": 42,
|
||||
"statementsMax": 45,
|
||||
"failures": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# Benchmark v1.44.0-baseline
|
||||
|
||||
Commit d640eea32, 2026-10-22T03:54:05.746Z. 1266549 measurement rows on the bench account, 20 measured requests per row after 3 warm-up requests, one at a time. Server: production build, web process, heap ceiling 792 MB. Postgres 16.15, 14 cores, load average 9.4,20.1,20 at the end. Idle background: 0 statements/s.
|
||||
|
||||
| Request | Mode | n | p50 ms | p95 ms | max ms | statements (median) | statements (max) |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| /api/auth/me | cold | 20 | 12.1 | 17.4 | 18.4 | 19 | 19 |
|
||||
| /api/auth/me | warm | 20 | 16.6 | 22.2 | 24.4 | 19 | 19 |
|
||||
| dashboard summary | cold | 20 | 205.6 | 233.2 | 275.3 | 31 | 32 |
|
||||
| dashboard summary | warm | 20 | 12.8 | 14.3 | 27.5 | 7 | 7 |
|
||||
| dashboard snapshot | cold | 20 | 2157.8 | 2252.7 | 2292.9 | 158 | 161 |
|
||||
| dashboard snapshot | warm | 20 | 15.6 | 23.5 | 25.6 | 13 | 16 |
|
||||
| series pulse 30 d | cold | 20 | 50.8 | 58.7 | 58.9 | 8 | 9 |
|
||||
| series pulse 30 d | warm | 20 | 52.3 | 59.5 | 59.9 | 8 | 8 |
|
||||
| series pulse 365 d | cold | 20 | 289 | 311.3 | 363.7 | 8 | 9 |
|
||||
| series pulse 365 d | warm | 20 | 274.3 | 355.8 | 366.8 | 8 | 8 |
|
||||
| series pulse 3650 d | cold | 20 | 1502.6 | 2117.6 | 2122.9 | 8 | 9 |
|
||||
| series pulse 3650 d | warm | 20 | 1490.3 | 1505.5 | 1555.2 | 8 | 11 |
|
||||
| day view | cold | 20 | 61.7 | 77.5 | 86.1 | 49 | 50 |
|
||||
| day view | warm | 20 | 49 | 55.6 | 62.5 | 48 | 48 |
|
||||
| insights health status | cold | 20 | 124.1 | 139.5 | 141.5 | 68 | 68 |
|
||||
| insights health status | warm | 20 | 124.7 | 157.8 | 165.3 | 68 | 68 |
|
||||
| insights correlations | cold | 20 | 95.4 | 105 | 108.8 | 45 | 45 |
|
||||
| insights correlations | warm | 20 | 13 | 15.4 | 15.9 | 8 | 8 |
|
||||
| batch ingest 5k (per 500) | write | 10 | 163 | 220.7 | 220.7 | 42 | 45 |
|
||||
@@ -0,0 +1,478 @@
|
||||
/**
|
||||
* Request benchmark against a seeded account: latency (p50, p95, max) and
|
||||
* SQL statements per request for the hot read and write paths.
|
||||
*
|
||||
* DATABASE_URL=postgresql://… API_TOKEN_HMAC_KEY=… ENCRYPTION_KEYS=… \
|
||||
* ENCRYPTION_ACTIVE_KEY_ID=… pnpm dlx tsx scripts/bench/run.ts \
|
||||
* [--user bench-large] [--iterations 20] [--warmup 3] \
|
||||
* [--base-url http://127.0.0.1:3100] [--out scripts/bench/results/<label>]
|
||||
*
|
||||
* See docs/contributing/benchmarks.md for the whole setup (a throwaway
|
||||
* Postgres with pg_stat_statements, migrations, the seed, a production
|
||||
* build). Never point it at a database that holds real data: it mints a
|
||||
* token for the bench user and writes measurements under it.
|
||||
*
|
||||
* Without `--base-url` the script starts the production build itself
|
||||
* (`next start` on `.next`) as a web-only process under the production heap
|
||||
* ceiling, and stops it at the end.
|
||||
*
|
||||
* Statements are counted with pg_stat_statements: the sum of `calls` over
|
||||
* the database, read before and after each request, once no statement is
|
||||
* in flight and the count has stopped moving (so fire-and-forget writes a
|
||||
* request starts are counted with it). The bench's own reads of the two
|
||||
* views are excluded. Requests run one at a time, so nothing else is in the
|
||||
* delta except the server's idle background, which is measured first and
|
||||
* reported. Before the measurement the build boots once with every role, so
|
||||
* the worker creates its queues as it does in production; whatever that
|
||||
* boot scheduled is dropped.
|
||||
*
|
||||
* Each read runs twice: cold, with the user's caches evicted before every
|
||||
* request by a one-row manual-sync ingest (excluded from the numbers), so
|
||||
* the request rebuilds what it serves; and warm, back to back, which is
|
||||
* what a cache hit costs. The batch ingest posts 5,000 fresh rows as ten
|
||||
* requests of 500, the route's per-request limit, and removes them again.
|
||||
*
|
||||
* Output: `<out>.json` and `<out>.md`. Exit 2 on any non-2xx response.
|
||||
*/
|
||||
import { execSync, spawn, type ChildProcess } from "node:child_process";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { cpus, loadavg } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
import { Pool } from "pg";
|
||||
|
||||
import { issueApiToken } from "@/lib/auth/issue-token";
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
function arg(name: string, fallback: string): string {
|
||||
const i = process.argv.indexOf(`--${name}`);
|
||||
return i >= 0 && process.argv[i + 1] ? process.argv[i + 1]! : fallback;
|
||||
}
|
||||
|
||||
const USER_ID = arg("user", "bench-large");
|
||||
const ITERATIONS = Number(arg("iterations", "20"));
|
||||
const WARMUP = Number(arg("warmup", "3"));
|
||||
const PORT = Number(arg("port", "3100"));
|
||||
const BASE_URL = arg("base-url", "");
|
||||
const OUT = arg("out", join("scripts", "bench", "results", "latest"));
|
||||
const HEAP_MB = Number(arg("heap-mb", "792"));
|
||||
|
||||
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 1 });
|
||||
|
||||
async function statementCount(): Promise<number> {
|
||||
const { rows } = await pool.query<{ n: string | null }>(
|
||||
`SELECT sum(calls)::bigint AS n FROM pg_stat_statements
|
||||
WHERE dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
|
||||
AND query NOT ILIKE '%pg_stat_statements%'
|
||||
AND query NOT ILIKE '%pg_stat_activity%'`,
|
||||
);
|
||||
return Number(rows[0]?.n ?? 0);
|
||||
}
|
||||
|
||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
/**
|
||||
* Wait until the server has no statement in flight and the counter holds
|
||||
* still for 150 ms (or 10 s pass), then read it. pg_stat_statements counts a
|
||||
* statement when it ends, so a long fold in a background refresh would look
|
||||
* settled by the counter alone; pg_stat_activity says it is still running.
|
||||
*/
|
||||
async function settledCount(): Promise<number> {
|
||||
let last = -1;
|
||||
const deadline = Date.now() + 10_000;
|
||||
while (Date.now() < deadline) {
|
||||
const { rows } = await pool.query<{ active: string }>(
|
||||
`SELECT count(*)::int AS active FROM pg_stat_activity
|
||||
WHERE datname = current_database() AND pid <> pg_backend_pid()
|
||||
AND state NOT IN ('idle')`,
|
||||
);
|
||||
const now = await statementCount();
|
||||
if (Number(rows[0]?.active ?? 0) === 0 && now === last) return now;
|
||||
last = now;
|
||||
await sleep(150);
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
function percentile(sorted: number[], p: number): number {
|
||||
if (sorted.length === 0) return 0;
|
||||
const i = Math.ceil((p / 100) * sorted.length) - 1;
|
||||
return sorted[Math.min(sorted.length - 1, Math.max(0, i))]!;
|
||||
}
|
||||
|
||||
interface Sample {
|
||||
ms: number;
|
||||
statements: number;
|
||||
status: number;
|
||||
}
|
||||
|
||||
interface ScenarioResult {
|
||||
name: string;
|
||||
mode: "cold" | "warm" | "write";
|
||||
request: string;
|
||||
n: number;
|
||||
p50Ms: number;
|
||||
p95Ms: number;
|
||||
maxMs: number;
|
||||
statementsMedian: number;
|
||||
statementsMax: number;
|
||||
failures: number;
|
||||
}
|
||||
|
||||
let token = "";
|
||||
const failures: string[] = [];
|
||||
|
||||
async function call(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: unknown,
|
||||
): Promise<Sample> {
|
||||
const before = await settledCount();
|
||||
const started = performance.now();
|
||||
const res = await fetch(`${base()}${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
...(body ? { "content-type": "application/json" } : {}),
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
await res.arrayBuffer();
|
||||
const ms = performance.now() - started;
|
||||
const after = await settledCount();
|
||||
if (!res.ok) failures.push(`${method} ${path}: ${res.status}`);
|
||||
return { ms, statements: after - before, status: res.status };
|
||||
}
|
||||
|
||||
function summarise(
|
||||
name: string,
|
||||
mode: ScenarioResult["mode"],
|
||||
request: string,
|
||||
samples: Sample[],
|
||||
): ScenarioResult {
|
||||
const ms = samples.map((s) => s.ms).sort((a, b) => a - b);
|
||||
const st = samples.map((s) => s.statements).sort((a, b) => a - b);
|
||||
const r = (n: number) => Math.round(n * 10) / 10;
|
||||
return {
|
||||
name,
|
||||
mode,
|
||||
request,
|
||||
n: samples.length,
|
||||
p50Ms: r(percentile(ms, 50)),
|
||||
p95Ms: r(percentile(ms, 95)),
|
||||
maxMs: r(ms[ms.length - 1] ?? 0),
|
||||
statementsMedian: percentile(st, 50),
|
||||
statementsMax: st[st.length - 1] ?? 0,
|
||||
failures: samples.filter((s) => s.status >= 300).length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The per-user rate limits would refuse a benchmark long before a person
|
||||
* meets them (one run sends several hundred requests in minutes), so the
|
||||
* bench user's buckets are cleared before each scenario. The limiter's own
|
||||
* cost stays in every count.
|
||||
*/
|
||||
async function resetRateLimits(): Promise<void> {
|
||||
await pool.query(`DELETE FROM rate_limits WHERE key LIKE '%:' || $1`, [
|
||||
USER_ID,
|
||||
]);
|
||||
}
|
||||
|
||||
let bustCounter = 0;
|
||||
/**
|
||||
* Evict the user's caches with a real change, outside the numbers: a one-row
|
||||
* ingest marked as a manual sync, which hard-evicts instead of marking
|
||||
* stale, so the next read is a miss and rebuilds in the request.
|
||||
*/
|
||||
async function bust(anchorDay: string): Promise<void> {
|
||||
bustCounter += 1;
|
||||
const res = await fetch(`${base()}/api/measurements/batch`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
syncTrigger: "manual",
|
||||
entries: [
|
||||
{
|
||||
hkIdentifier: "HKQuantityTypeIdentifierFlightsClimbed",
|
||||
value: 10 + (bustCounter % 2),
|
||||
unit: "count",
|
||||
startDate: `${anchorDay}T08:00:00.000Z`,
|
||||
endDate: `${anchorDay}T18:00:00.000Z`,
|
||||
externalId: `stats:HKQuantityTypeIdentifierFlightsClimbed:${anchorDay}`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
const body = (await res.json()) as {
|
||||
data?: { inserted?: number; updated?: number };
|
||||
};
|
||||
// A bust that wrote nothing evicted nothing, and every "cold" number
|
||||
// after it would be a cache hit.
|
||||
if ((body.data?.inserted ?? 0) + (body.data?.updated ?? 0) !== 1) {
|
||||
throw new Error(`cache bust wrote no row: ${JSON.stringify(body)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function base(): string {
|
||||
return BASE_URL || `http://127.0.0.1:${PORT}`;
|
||||
}
|
||||
|
||||
async function waitForHealth(
|
||||
timeoutMs: number,
|
||||
server: ChildProcess | null,
|
||||
): Promise<void> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
if (server && server.exitCode !== null) {
|
||||
throw new Error(`the server exited with code ${server.exitCode}`);
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`${base()}/api/health`);
|
||||
if (res.ok) return;
|
||||
} catch {
|
||||
// not listening yet
|
||||
}
|
||||
await sleep(500);
|
||||
}
|
||||
throw new Error(`server at ${base()} did not answer /api/health`);
|
||||
}
|
||||
|
||||
function startServer(role: "web" | "all"): ChildProcess {
|
||||
// `next start` rather than the standalone server: the standalone tree
|
||||
// lacks the externalised runtime packages the image installs separately
|
||||
// (see the Dockerfile), while the checkout has them. It serves the same
|
||||
// build output.
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
[
|
||||
"node_modules/next/dist/bin/next",
|
||||
"start",
|
||||
"-p",
|
||||
String(PORT),
|
||||
"-H",
|
||||
"127.0.0.1",
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: "production",
|
||||
PORT: String(PORT),
|
||||
HOSTNAME: "127.0.0.1",
|
||||
HEALTHLOG_PROCESS_TYPE: role,
|
||||
SESSION_COOKIE_SECURE: "false",
|
||||
NODE_OPTIONS: `--max-old-space-size=${HEAP_MB}`,
|
||||
},
|
||||
stdio: ["ignore", "ignore", "inherit"],
|
||||
},
|
||||
);
|
||||
return child;
|
||||
}
|
||||
|
||||
async function stopServer(child: ChildProcess): Promise<void> {
|
||||
if (child.exitCode !== null) return;
|
||||
const exited = new Promise((resolve) => child.once("exit", resolve));
|
||||
child.kill("SIGTERM");
|
||||
await Promise.race([exited, sleep(20_000)]);
|
||||
if (child.exitCode === null) child.kill("SIGKILL");
|
||||
}
|
||||
|
||||
function freshBatch(run: string, chunk: number): unknown[] {
|
||||
// 500 heart-rate readings a minute apart, on a day inside the seeded
|
||||
// window, with ids no earlier run used.
|
||||
const day = Date.UTC(2026, 0, 1) + chunk * 86_400_000;
|
||||
return Array.from({ length: 500 }, (_, i) => {
|
||||
const at = new Date(day + i * 60_000).toISOString();
|
||||
return {
|
||||
hkIdentifier: "HKQuantityTypeIdentifierHeartRate",
|
||||
value: 55 + (i % 40),
|
||||
unit: "count/min",
|
||||
startDate: at,
|
||||
endDate: at,
|
||||
externalId: `bench-${run}-${chunk}-${i}`,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function markdown(meta: Record<string, unknown>, rows: ScenarioResult[]) {
|
||||
const lines = [
|
||||
`# Benchmark ${String(meta.label)}`,
|
||||
"",
|
||||
`Commit ${String(meta.commit)}, ${String(meta.date)}. ${String(meta.measurements)} measurement rows on the bench account, ${ITERATIONS} measured requests per row after ${WARMUP} warm-up requests, one at a time. Server: production build, web process, heap ceiling ${HEAP_MB} MB. Postgres ${String(meta.postgres)}, ${String(meta.cpus)} cores, load average ${String(meta.loadAverage)} at the end. Idle background: ${String(meta.idleStatementsPerSecond)} statements/s.`,
|
||||
"",
|
||||
"| Request | Mode | n | p50 ms | p95 ms | max ms | statements (median) | statements (max) |",
|
||||
"| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |",
|
||||
...rows.map(
|
||||
(r) =>
|
||||
`| ${r.name} | ${r.mode} | ${r.n} | ${r.p50Ms} | ${r.p95Ms} | ${r.maxMs} | ${r.statementsMedian} | ${r.statementsMax} |`,
|
||||
),
|
||||
"",
|
||||
];
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL is required");
|
||||
await pool.query("CREATE EXTENSION IF NOT EXISTS pg_stat_statements");
|
||||
await statementCount(); // fails here when the library is not preloaded
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: USER_ID },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!user) throw new Error(`no user ${USER_ID}; run the seed first`);
|
||||
const measurements = await prisma.measurement.count({
|
||||
where: { userId: USER_ID },
|
||||
});
|
||||
token = (
|
||||
await issueApiToken({
|
||||
userId: USER_ID,
|
||||
name: "benchmark",
|
||||
permissions: ["*"],
|
||||
expiresInDays: 1,
|
||||
})
|
||||
).token;
|
||||
|
||||
// A web-only process only sends jobs; the queues are created by the
|
||||
// worker role. Boot the full process once so every queue exists (as it
|
||||
// does in production), then run the measurement against a web-only
|
||||
// process, so the worker's polling is not in every delta.
|
||||
if (!BASE_URL) {
|
||||
const full = startServer("all");
|
||||
await waitForHealth(180_000, full);
|
||||
await stopServer(full);
|
||||
// Drop whatever that boot scheduled: nothing in the web-only process
|
||||
// runs jobs, and a fold left queued would change the fixture on the
|
||||
// next full boot.
|
||||
await pool.query("TRUNCATE pgboss.job");
|
||||
}
|
||||
const server = BASE_URL ? null : startServer("web");
|
||||
try {
|
||||
await waitForHealth(180_000, server);
|
||||
|
||||
const idleBefore = await settledCount();
|
||||
await sleep(5_000);
|
||||
const idleStatementsPerSecond =
|
||||
Math.round(((await statementCount()) - idleBefore) / 0.5) / 10;
|
||||
|
||||
const yesterday = new Date(Date.now() - 86_400_000)
|
||||
.toISOString()
|
||||
.slice(0, 10);
|
||||
const reads: Array<[string, string]> = [
|
||||
["/api/auth/me", "/api/auth/me"],
|
||||
["dashboard summary", "/api/dashboard/summary"],
|
||||
["dashboard snapshot", "/api/dashboard/snapshot"],
|
||||
["series pulse 30 d", "/api/measurements/series?kind=pulse&days=30"],
|
||||
["series pulse 365 d", "/api/measurements/series?kind=pulse&days=365"],
|
||||
["series pulse 3650 d", "/api/measurements/series?kind=pulse&days=3650"],
|
||||
["day view", `/api/day/${yesterday}`],
|
||||
["insights health status", "/api/insights/health-status"],
|
||||
["insights correlations", "/api/insights/correlations"],
|
||||
];
|
||||
|
||||
const results: ScenarioResult[] = [];
|
||||
for (const [name, path] of reads) {
|
||||
await resetRateLimits();
|
||||
for (let i = 0; i < WARMUP; i++) await call("GET", path);
|
||||
const cold: Sample[] = [];
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
await bust(yesterday);
|
||||
cold.push(await call("GET", path));
|
||||
}
|
||||
results.push(summarise(name, "cold", `GET ${path}`, cold));
|
||||
await resetRateLimits();
|
||||
const warm: Sample[] = [];
|
||||
for (let i = 0; i < ITERATIONS; i++) warm.push(await call("GET", path));
|
||||
results.push(summarise(name, "warm", `GET ${path}`, warm));
|
||||
console.log(
|
||||
`${name.padEnd(26)} cold p95 ${results.at(-2)!.p95Ms} ms / ${results.at(-2)!.statementsMedian} st, warm p95 ${results.at(-1)!.p95Ms} ms / ${results.at(-1)!.statementsMedian} st`,
|
||||
);
|
||||
}
|
||||
|
||||
// Rows an earlier run wrote would turn this run's inserts into
|
||||
// reconciles of the same natural key, so they go first, and again at
|
||||
// the end, leaving the seeded fixture as it was.
|
||||
const clean = () =>
|
||||
pool.query(
|
||||
`DELETE FROM measurements WHERE user_id = $1 AND external_id LIKE 'bench-%'`,
|
||||
[USER_ID],
|
||||
);
|
||||
await clean();
|
||||
await resetRateLimits();
|
||||
const run = Date.now().toString(36);
|
||||
const writes: Sample[] = [];
|
||||
for (let chunk = 0; chunk < 10; chunk++) {
|
||||
writes.push(
|
||||
await call("POST", "/api/measurements/batch", {
|
||||
entries: freshBatch(run, chunk),
|
||||
}),
|
||||
);
|
||||
}
|
||||
const batch = summarise(
|
||||
"batch ingest 5k (per 500)",
|
||||
"write",
|
||||
"POST /api/measurements/batch ×10",
|
||||
writes,
|
||||
);
|
||||
results.push(batch);
|
||||
await clean();
|
||||
const totalMs = writes.reduce((a, s) => a + s.ms, 0);
|
||||
const totalStatements = writes.reduce((a, s) => a + s.statements, 0);
|
||||
console.log(
|
||||
`batch ingest 5k total ${Math.round(totalMs)} ms / ${totalStatements} st`,
|
||||
);
|
||||
|
||||
let commit = "unknown";
|
||||
try {
|
||||
commit = execSync("git rev-parse --short HEAD").toString().trim();
|
||||
} catch {
|
||||
// not a checkout
|
||||
}
|
||||
const meta = {
|
||||
label: OUT.split("/").pop(),
|
||||
commit,
|
||||
date: new Date().toISOString(),
|
||||
node: process.version,
|
||||
postgres: (
|
||||
await pool.query<{ v: string }>(
|
||||
"SELECT current_setting('server_version') AS v",
|
||||
)
|
||||
).rows[0]?.v,
|
||||
cpus: cpus().length,
|
||||
loadAverage: loadavg().map((n) => Math.round(n * 10) / 10),
|
||||
measurements,
|
||||
iterations: ITERATIONS,
|
||||
warmup: WARMUP,
|
||||
heapMb: HEAP_MB,
|
||||
idleStatementsPerSecond,
|
||||
batch5k: { totalMs: Math.round(totalMs), totalStatements },
|
||||
};
|
||||
mkdirSync(dirname(OUT), { recursive: true });
|
||||
writeFileSync(
|
||||
`${OUT}.json`,
|
||||
`${JSON.stringify({ meta, results }, null, 2)}\n`,
|
||||
);
|
||||
writeFileSync(`${OUT}.md`, markdown(meta, results));
|
||||
console.log(`wrote ${OUT}.json and ${OUT}.md`);
|
||||
} finally {
|
||||
if (server) await stopServer(server);
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error(`non-2xx responses:\n ${failures.join("\n ")}`);
|
||||
process.exitCode = 2;
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exitCode = 1;
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
await pool.end();
|
||||
});
|
||||
@@ -0,0 +1,659 @@
|
||||
/**
|
||||
* Seed one synthetic account the size of a heavy real one, for the benchmark
|
||||
* harness (`scripts/bench/run.ts`) and for reproducing the maintenance jobs:
|
||||
* about 1.26 million measurement rows over ten years, plus workouts, mood
|
||||
* entries and medications with their intake history.
|
||||
*
|
||||
* DATABASE_URL=postgresql://… pnpm dlx tsx scripts/bench/seed-large-account.ts \
|
||||
* [--user bench-large] [--years 10] [--anchor 2026-10-22] [--reset]
|
||||
* [--no-rollups]
|
||||
*
|
||||
* Never point this at a database that holds real data: it inserts rows under
|
||||
* the given user id and, with `--reset`, deletes that user (and, through the
|
||||
* cascades, everything it owns) first.
|
||||
*
|
||||
* Deterministic: every value and timestamp is a function of the row index and
|
||||
* the anchor day (`--anchor`, UTC midnight; default today), and ids are
|
||||
* derived from the user id, the series and the index. Two runs with the same
|
||||
* arguments write the same rows.
|
||||
*
|
||||
* The shape follows what the integrations write, with overlapping sources so
|
||||
* the canonical-source pick has work to do:
|
||||
*
|
||||
* - Apple Health dense spot rows (heart rate about every ten minutes, HRV,
|
||||
* SpO2, respiratory rate, gait metrics, audio exposure) keyed by a hashed
|
||||
* sample id;
|
||||
* - Withings morning and evening blood pressure with its pulse, and daily
|
||||
* weight, body fat and muscle mass; a weekly manual weight beside it;
|
||||
* - Oura nightly resting heart rate and RMSSD for the last three years,
|
||||
* beside Apple Health's resting heart rate;
|
||||
* - Nightscout CGM glucose every five minutes for the last 90 days;
|
||||
* - one `stats:` daily row per cumulative type and day (the export import)
|
||||
* and per-sample cumulative rows with opaque ids for the last 90 days
|
||||
* (the iOS sync);
|
||||
* - sleep-stage segments every night;
|
||||
* - about 3,000 workouts, a mood entry a day for three years, and three
|
||||
* daily medications with their intake events for three years.
|
||||
*
|
||||
* Everything goes in through set-based `INSERT … SELECT … generate_series`,
|
||||
* so a million rows take seconds rather than a million round trips. The
|
||||
* rollup tier is then folded with `recomputeUserRollups`, the same path the
|
||||
* boot backfill takes for an imported account, so reads see the state a real
|
||||
* account is in. `ANALYZE` runs at the end: a planner reading pre-seed
|
||||
* statistics picks plans no production table of this size would get.
|
||||
*/
|
||||
import { prisma } from "@/lib/db";
|
||||
import {
|
||||
ALL_GRANULARITIES,
|
||||
recomputeUserRollups,
|
||||
} from "@/lib/rollups/measurement-rollups";
|
||||
|
||||
function arg(name: string, fallback: string): string {
|
||||
const i = process.argv.indexOf(`--${name}`);
|
||||
return i >= 0 && process.argv[i + 1] ? process.argv[i + 1]! : fallback;
|
||||
}
|
||||
|
||||
const USER_ID = arg("user", "bench-large");
|
||||
const YEARS = Number(arg("years", "10"));
|
||||
const ANCHOR = arg("anchor", new Date().toISOString().slice(0, 10));
|
||||
const RESET = process.argv.includes("--reset");
|
||||
const ROLLUPS = !process.argv.includes("--no-rollups");
|
||||
const ZONE = "Europe/Berlin";
|
||||
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(ANCHOR)) {
|
||||
throw new Error(`--anchor must be YYYY-MM-DD, got ${ANCHOR}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* A spot series: rows evenly spaced over the last `days` days before the
|
||||
* anchor. `perDay` above one spaces rows through the day; `everyDays` above
|
||||
* one writes one row every that many days at `offsetSec` past midnight UTC.
|
||||
*/
|
||||
interface SpotSeries {
|
||||
type: string;
|
||||
unit: string;
|
||||
source: string;
|
||||
base: number;
|
||||
spread: number;
|
||||
days: number;
|
||||
perDay?: number;
|
||||
everyDays?: number;
|
||||
offsetSec?: number;
|
||||
/** externalId prefix; `stats:` rows are generated separately. */
|
||||
idPrefix: string;
|
||||
}
|
||||
|
||||
async function insertSpot(s: SpotSeries): Promise<number> {
|
||||
const step = s.everyDays
|
||||
? s.everyDays * 86_400
|
||||
: Math.floor(86_400 / (s.perDay ?? 1));
|
||||
const count = Math.floor((s.days * 86_400) / step);
|
||||
// Jitter stays below the step so every measured_at is distinct per type
|
||||
// and source (the (user, type, measured_at, source, sleep_stage) unique
|
||||
// index). Series that share a step and offset (BP systolic, diastolic and
|
||||
// the Withings pulse) share their timestamps, as a cuff reading does.
|
||||
const jitter = Math.max(1, Math.min(59, step - 1));
|
||||
return prisma.$executeRawUnsafe(
|
||||
`
|
||||
INSERT INTO measurements
|
||||
(id, user_id, type, value, unit, source, measured_at, external_id, created_at, updated_at)
|
||||
SELECT
|
||||
'seed-' || $1 || '-' || $2 || '-' || $10 || '-' || g,
|
||||
$1,
|
||||
$2::measurement_type,
|
||||
round(($3 + $4 * sin(g / 37.0) + ($4 / 2) * ((g * 7919) % 100) / 100.0)::numeric, 2),
|
||||
$5,
|
||||
$10::measurement_source,
|
||||
$11::date::timestamp - make_interval(secs => $6 * $7::bigint)
|
||||
+ make_interval(secs => g * $6 + $12 + (g * 31) % $8),
|
||||
$9 || md5($2 || $10 || g::text),
|
||||
now(), now()
|
||||
FROM generate_series(0, $7::bigint - 1) AS g
|
||||
`,
|
||||
USER_ID,
|
||||
s.type,
|
||||
s.base,
|
||||
s.spread,
|
||||
s.unit,
|
||||
step,
|
||||
count,
|
||||
jitter,
|
||||
s.idPrefix,
|
||||
s.source,
|
||||
ANCHOR,
|
||||
s.offsetSec ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
function log(label: string, n: number): void {
|
||||
console.log(`${label.padEnd(40)} ${String(n).padStart(9)}`);
|
||||
}
|
||||
|
||||
async function seedMeasurements(days: number): Promise<number> {
|
||||
const recent = Math.min(days, 90);
|
||||
const threeYears = Math.min(days, 3 * 365);
|
||||
const spot: SpotSeries[] = [
|
||||
// Apple Health, the dense tier.
|
||||
{
|
||||
type: "PULSE",
|
||||
unit: "bpm",
|
||||
source: "APPLE_HEALTH",
|
||||
perDay: 147,
|
||||
base: 62,
|
||||
spread: 25,
|
||||
days,
|
||||
idPrefix: "hk-",
|
||||
},
|
||||
{
|
||||
type: "HEART_RATE_VARIABILITY",
|
||||
unit: "ms",
|
||||
source: "APPLE_HEALTH",
|
||||
perDay: 12,
|
||||
base: 45,
|
||||
spread: 20,
|
||||
days,
|
||||
idPrefix: "hk-",
|
||||
},
|
||||
{
|
||||
type: "OXYGEN_SATURATION",
|
||||
unit: "%",
|
||||
source: "APPLE_HEALTH",
|
||||
perDay: 24,
|
||||
base: 96,
|
||||
spread: 2,
|
||||
days,
|
||||
idPrefix: "hk-",
|
||||
},
|
||||
{
|
||||
type: "RESPIRATORY_RATE",
|
||||
unit: "breaths/min",
|
||||
source: "APPLE_HEALTH",
|
||||
perDay: 24,
|
||||
base: 14,
|
||||
spread: 3,
|
||||
days,
|
||||
idPrefix: "hk-",
|
||||
},
|
||||
{
|
||||
type: "WALKING_SPEED",
|
||||
unit: "m/s",
|
||||
source: "APPLE_HEALTH",
|
||||
perDay: 12,
|
||||
base: 1.3,
|
||||
spread: 0.2,
|
||||
days,
|
||||
idPrefix: "hk-",
|
||||
},
|
||||
{
|
||||
type: "WALKING_STEP_LENGTH",
|
||||
unit: "m",
|
||||
source: "APPLE_HEALTH",
|
||||
perDay: 12,
|
||||
base: 0.72,
|
||||
spread: 0.06,
|
||||
days,
|
||||
idPrefix: "hk-",
|
||||
},
|
||||
{
|
||||
type: "WALKING_ASYMMETRY",
|
||||
unit: "%",
|
||||
source: "APPLE_HEALTH",
|
||||
perDay: 12,
|
||||
base: 3,
|
||||
spread: 2,
|
||||
days,
|
||||
idPrefix: "hk-",
|
||||
},
|
||||
{
|
||||
type: "WALKING_DOUBLE_SUPPORT",
|
||||
unit: "%",
|
||||
source: "APPLE_HEALTH",
|
||||
perDay: 12,
|
||||
base: 28,
|
||||
spread: 3,
|
||||
days,
|
||||
idPrefix: "hk-",
|
||||
},
|
||||
{
|
||||
type: "AUDIO_EXPOSURE_ENV",
|
||||
unit: "dBA",
|
||||
source: "APPLE_HEALTH",
|
||||
perDay: 24,
|
||||
base: 62,
|
||||
spread: 12,
|
||||
days,
|
||||
idPrefix: "hk-",
|
||||
},
|
||||
{
|
||||
type: "RESTING_HEART_RATE",
|
||||
unit: "bpm",
|
||||
source: "APPLE_HEALTH",
|
||||
perDay: 1,
|
||||
offsetSec: 6 * 3600,
|
||||
base: 56,
|
||||
spread: 4,
|
||||
days,
|
||||
idPrefix: "hk-",
|
||||
},
|
||||
{
|
||||
type: "TIME_IN_DAYLIGHT",
|
||||
unit: "minutes",
|
||||
source: "APPLE_HEALTH",
|
||||
perDay: 1,
|
||||
offsetSec: 18 * 3600,
|
||||
base: 60,
|
||||
spread: 40,
|
||||
days,
|
||||
idPrefix: "hk-",
|
||||
},
|
||||
// The iOS sync's per-sample cumulative rows: dense, recent, opaque ids.
|
||||
{
|
||||
type: "ACTIVITY_STEPS",
|
||||
unit: "steps",
|
||||
source: "APPLE_HEALTH",
|
||||
perDay: 100,
|
||||
base: 90,
|
||||
spread: 60,
|
||||
days: recent,
|
||||
idPrefix: "uuid-",
|
||||
},
|
||||
{
|
||||
type: "ACTIVE_ENERGY_BURNED",
|
||||
unit: "kcal",
|
||||
source: "APPLE_HEALTH",
|
||||
perDay: 400,
|
||||
base: 1.2,
|
||||
spread: 0.9,
|
||||
days: recent,
|
||||
idPrefix: "uuid-",
|
||||
},
|
||||
{
|
||||
type: "WALKING_RUNNING_DISTANCE",
|
||||
unit: "m",
|
||||
source: "APPLE_HEALTH",
|
||||
perDay: 100,
|
||||
base: 70,
|
||||
spread: 50,
|
||||
days: recent,
|
||||
idPrefix: "uuid-",
|
||||
},
|
||||
// Withings: a cuff reading morning and evening, and the scale.
|
||||
{
|
||||
type: "BLOOD_PRESSURE_SYS",
|
||||
unit: "mmHg",
|
||||
source: "WITHINGS",
|
||||
perDay: 2,
|
||||
offsetSec: 6 * 3600,
|
||||
base: 126,
|
||||
spread: 10,
|
||||
days,
|
||||
idPrefix: "wi-",
|
||||
},
|
||||
{
|
||||
type: "BLOOD_PRESSURE_DIA",
|
||||
unit: "mmHg",
|
||||
source: "WITHINGS",
|
||||
perDay: 2,
|
||||
offsetSec: 6 * 3600,
|
||||
base: 81,
|
||||
spread: 7,
|
||||
days,
|
||||
idPrefix: "wi-",
|
||||
},
|
||||
{
|
||||
type: "PULSE",
|
||||
unit: "bpm",
|
||||
source: "WITHINGS",
|
||||
perDay: 2,
|
||||
offsetSec: 6 * 3600,
|
||||
base: 66,
|
||||
spread: 8,
|
||||
days,
|
||||
idPrefix: "wi-",
|
||||
},
|
||||
{
|
||||
type: "WEIGHT",
|
||||
unit: "kg",
|
||||
source: "WITHINGS",
|
||||
perDay: 1,
|
||||
offsetSec: 5 * 3600,
|
||||
base: 82,
|
||||
spread: 1.5,
|
||||
days,
|
||||
idPrefix: "wi-",
|
||||
},
|
||||
{
|
||||
type: "BODY_FAT",
|
||||
unit: "%",
|
||||
source: "WITHINGS",
|
||||
perDay: 1,
|
||||
offsetSec: 5 * 3600,
|
||||
base: 22,
|
||||
spread: 2,
|
||||
days,
|
||||
idPrefix: "wi-",
|
||||
},
|
||||
{
|
||||
type: "MUSCLE_MASS",
|
||||
unit: "kg",
|
||||
source: "WITHINGS",
|
||||
perDay: 1,
|
||||
offsetSec: 5 * 3600,
|
||||
base: 61,
|
||||
spread: 1,
|
||||
days,
|
||||
idPrefix: "wi-",
|
||||
},
|
||||
// A weekly manual weight beside the scale.
|
||||
{
|
||||
type: "WEIGHT",
|
||||
unit: "kg",
|
||||
source: "MANUAL",
|
||||
everyDays: 7,
|
||||
offsetSec: 19 * 3600,
|
||||
base: 82.4,
|
||||
spread: 1.5,
|
||||
days,
|
||||
idPrefix: "man-",
|
||||
},
|
||||
// Oura, nightly, the last three years.
|
||||
{
|
||||
type: "RESTING_HEART_RATE",
|
||||
unit: "bpm",
|
||||
source: "OURA",
|
||||
perDay: 1,
|
||||
offsetSec: 4 * 3600,
|
||||
base: 54,
|
||||
spread: 4,
|
||||
days: threeYears,
|
||||
idPrefix: "oura-",
|
||||
},
|
||||
{
|
||||
type: "HRV_RMSSD",
|
||||
unit: "ms",
|
||||
source: "OURA",
|
||||
perDay: 1,
|
||||
offsetSec: 4 * 3600,
|
||||
base: 48,
|
||||
spread: 15,
|
||||
days: threeYears,
|
||||
idPrefix: "oura-",
|
||||
},
|
||||
// Nightscout CGM, every five minutes, the last 90 days.
|
||||
{
|
||||
type: "BLOOD_GLUCOSE",
|
||||
unit: "mg/dL",
|
||||
source: "NIGHTSCOUT",
|
||||
perDay: 288,
|
||||
base: 105,
|
||||
spread: 30,
|
||||
days: recent,
|
||||
idPrefix: "ns-",
|
||||
},
|
||||
];
|
||||
|
||||
let total = 0;
|
||||
for (const s of spot) {
|
||||
const n = await insertSpot(s);
|
||||
total += n;
|
||||
log(`${s.type} (${s.source})`, n);
|
||||
}
|
||||
|
||||
// One export-estimate `stats:` row per cumulative type and local day, at
|
||||
// local noon — what `flushCumulativeBuckets` in the export import writes —
|
||||
// for every day older than the per-sample window.
|
||||
for (const [type, hk, unit, base] of [
|
||||
["ACTIVITY_STEPS", "HKQuantityTypeIdentifierStepCount", "steps", 8000],
|
||||
[
|
||||
"ACTIVE_ENERGY_BURNED",
|
||||
"HKQuantityTypeIdentifierActiveEnergyBurned",
|
||||
"kcal",
|
||||
550,
|
||||
],
|
||||
[
|
||||
"WALKING_RUNNING_DISTANCE",
|
||||
"HKQuantityTypeIdentifierDistanceWalkingRunning",
|
||||
"m",
|
||||
6000,
|
||||
],
|
||||
["FLIGHTS_CLIMBED", "HKQuantityTypeIdentifierFlightsClimbed", "flights", 9],
|
||||
] as const) {
|
||||
const n = await prisma.$executeRawUnsafe(
|
||||
`
|
||||
INSERT INTO measurements
|
||||
(id, user_id, type, value, unit, source, measured_at, external_id,
|
||||
aggregation_provenance, created_at, updated_at)
|
||||
SELECT
|
||||
'seed-' || $1 || '-' || $2 || '-stats-' || d,
|
||||
$1, $2::measurement_type, $5 + (d * 7919) % ($5 / 2), $4, 'APPLE_HEALTH',
|
||||
(($7::date - d)::timestamp + interval '12 hours')
|
||||
AT TIME ZONE $8 AT TIME ZONE 'UTC',
|
||||
'stats:' || $3 || ':' || to_char($7::date - d, 'YYYY-MM-DD'),
|
||||
'EXPORT_XML_SOURCE_MAX', now(), now()
|
||||
FROM generate_series(91, $6::int) AS d
|
||||
`,
|
||||
USER_ID,
|
||||
type,
|
||||
hk,
|
||||
unit,
|
||||
base,
|
||||
days,
|
||||
ANCHOR,
|
||||
ZONE,
|
||||
);
|
||||
total += n;
|
||||
log(`${type} (stats:)`, n);
|
||||
}
|
||||
|
||||
// Sleep: 30 segments a night from 23:00 local, 16 minutes apart, rotating
|
||||
// through the stages. Distinct measured_at per segment.
|
||||
const sleep = await prisma.$executeRawUnsafe(
|
||||
`
|
||||
INSERT INTO measurements
|
||||
(id, user_id, type, value, unit, source, measured_at, external_id,
|
||||
sleep_stage, created_at, updated_at)
|
||||
SELECT
|
||||
'seed-' || $1 || '-sleep-' || d || '-' || s,
|
||||
$1, 'SLEEP_DURATION', 16, 'minutes', 'APPLE_HEALTH',
|
||||
(($3::date - d)::timestamp - interval '1 hour' + s * interval '16 minutes')
|
||||
AT TIME ZONE $4 AT TIME ZONE 'UTC',
|
||||
'hk-' || md5('sleep' || d || '-' || s),
|
||||
(ARRAY['CORE','DEEP','CORE','REM','AWAKE','CORE']::sleep_stage[])[1 + s % 6],
|
||||
now(), now()
|
||||
FROM generate_series(1, $2::int) AS d, generate_series(0, 29) AS s
|
||||
`,
|
||||
USER_ID,
|
||||
days,
|
||||
ANCHOR,
|
||||
ZONE,
|
||||
);
|
||||
total += sleep;
|
||||
log("SLEEP_DURATION (APPLE_HEALTH)", sleep);
|
||||
return total;
|
||||
}
|
||||
|
||||
async function seedWorkouts(days: number): Promise<number> {
|
||||
// About one workout every 29 hours, rotating through five sports.
|
||||
const count = Math.floor((days * 24) / 29);
|
||||
return prisma.$executeRawUnsafe(
|
||||
`
|
||||
INSERT INTO workouts
|
||||
(id, user_id, sport_type, started_at, ended_at, duration_sec,
|
||||
total_energy_kcal, total_distance_m, avg_heart_rate, max_heart_rate,
|
||||
source, external_id, created_at, updated_at)
|
||||
SELECT
|
||||
'seed-' || $1 || '-workout-' || g,
|
||||
$1,
|
||||
(ARRAY['walking','running','cycling','hiking','swimming'])[1 + g % 5],
|
||||
$3::date::timestamp - make_interval(hours => (g * 29)::int) + interval '17 hours',
|
||||
$3::date::timestamp - make_interval(hours => (g * 29)::int) + interval '17 hours'
|
||||
+ make_interval(secs => (1800 + (g * 97) % 3600)::int),
|
||||
(1800 + (g * 97) % 3600)::int,
|
||||
round((250 + (g * 13) % 400)::numeric, 1),
|
||||
round((3000 + (g * 211) % 9000)::numeric, 1),
|
||||
(115 + (g * 7) % 40)::int,
|
||||
(150 + (g * 5) % 35)::int,
|
||||
'APPLE_HEALTH',
|
||||
'hk-workout-' || md5($1 || g::text),
|
||||
now(), now()
|
||||
FROM generate_series(0, $2::int - 1) AS g
|
||||
`,
|
||||
USER_ID,
|
||||
count,
|
||||
ANCHOR,
|
||||
);
|
||||
}
|
||||
|
||||
async function seedMood(days: number): Promise<number> {
|
||||
const span = Math.min(days, 3 * 365);
|
||||
return prisma.$executeRawUnsafe(
|
||||
`
|
||||
INSERT INTO mood_entries
|
||||
(id, user_id, date, mood, score, mood_a1, stress_a2, energy_a3,
|
||||
source, mood_logged_at, tz, synced_at, created_at, updated_at)
|
||||
SELECT
|
||||
'seed-' || $1 || '-mood-' || d,
|
||||
$1,
|
||||
to_char($3::date - d, 'YYYY-MM-DD'),
|
||||
(ARRAY['LAUSIG','SCHLECHT','OKAY','GUT','SUPER_GUT'])[1 + (d * 7) % 5],
|
||||
1 + (d * 7) % 5,
|
||||
2 * ((d * 7) % 5) + 1,
|
||||
(d * 3) % 11,
|
||||
(d * 5) % 11,
|
||||
'MANUAL',
|
||||
(($3::date - d)::timestamp + interval '20 hours') AT TIME ZONE $4 AT TIME ZONE 'UTC',
|
||||
$4,
|
||||
now(), now(), now()
|
||||
FROM generate_series(1, $2::int) AS d
|
||||
`,
|
||||
USER_ID,
|
||||
span,
|
||||
ANCHOR,
|
||||
ZONE,
|
||||
);
|
||||
}
|
||||
|
||||
async function seedMedications(days: number): Promise<number> {
|
||||
const span = Math.min(days, 3 * 365);
|
||||
const meds = [
|
||||
{ key: "a", name: "Synthetic A", dose: "5 mg", window: ["08:00", "09:00"] },
|
||||
{
|
||||
key: "b",
|
||||
name: "Synthetic B",
|
||||
dose: "100 mg",
|
||||
window: ["08:00", "09:00"],
|
||||
},
|
||||
{
|
||||
key: "c",
|
||||
name: "Synthetic C",
|
||||
dose: "20 mg",
|
||||
window: ["20:00", "21:00"],
|
||||
},
|
||||
];
|
||||
let events = 0;
|
||||
for (const m of meds) {
|
||||
const medId = `seed-${USER_ID}-med-${m.key}`;
|
||||
await prisma.$executeRawUnsafe(
|
||||
`
|
||||
INSERT INTO medications
|
||||
(id, user_id, name, dose, starts_on, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, ($5::date - $6::int)::timestamp, now(), now())
|
||||
`,
|
||||
medId,
|
||||
USER_ID,
|
||||
m.name,
|
||||
m.dose,
|
||||
ANCHOR,
|
||||
span,
|
||||
);
|
||||
await prisma.$executeRawUnsafe(
|
||||
`
|
||||
INSERT INTO medication_schedules
|
||||
(id, medication_id, window_start, window_end, schedule_type)
|
||||
VALUES ($1, $2, $3, $4, 'SCHEDULED')
|
||||
`,
|
||||
`${medId}-schedule`,
|
||||
medId,
|
||||
m.window[0],
|
||||
m.window[1],
|
||||
);
|
||||
// One intake event per day; about one in twelve skipped, the rest taken
|
||||
// a few minutes into the window.
|
||||
events += await prisma.$executeRawUnsafe(
|
||||
`
|
||||
INSERT INTO medication_intake_events
|
||||
(id, user_id, medication_id, scheduled_for, taken_at, skipped,
|
||||
attribution_source, source, created_at, updated_at)
|
||||
SELECT
|
||||
$1 || '-intake-' || d,
|
||||
$2,
|
||||
$1,
|
||||
(($3::date - d)::timestamp + $5::time) AT TIME ZONE $6 AT TIME ZONE 'UTC',
|
||||
CASE WHEN d % 12 = 0 THEN NULL
|
||||
ELSE (($3::date - d)::timestamp + $5::time
|
||||
+ make_interval(mins => (d * 7) % 40)) AT TIME ZONE $6 AT TIME ZONE 'UTC'
|
||||
END,
|
||||
d % 12 = 0,
|
||||
'AUTO',
|
||||
'WEB',
|
||||
now(), now()
|
||||
FROM generate_series(1, $4::int) AS d
|
||||
`,
|
||||
medId,
|
||||
USER_ID,
|
||||
ANCHOR,
|
||||
span,
|
||||
m.window[0],
|
||||
ZONE,
|
||||
);
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const days = Math.round(YEARS * 365);
|
||||
const started = Date.now();
|
||||
|
||||
if (RESET) {
|
||||
await prisma.$executeRawUnsafe(`DELETE FROM users WHERE id = $1`, USER_ID);
|
||||
}
|
||||
await prisma.$executeRawUnsafe(
|
||||
`INSERT INTO users (id, username, timezone, onboarding_completed_at, updated_at)
|
||||
VALUES ($1, $1, $2, now(), now())
|
||||
ON CONFLICT (id) DO NOTHING`,
|
||||
USER_ID,
|
||||
ZONE,
|
||||
);
|
||||
|
||||
const measurements = await seedMeasurements(days);
|
||||
log("measurements total", measurements);
|
||||
log("workouts", await seedWorkouts(days));
|
||||
log("mood entries", await seedMood(days));
|
||||
log("medication intake events", await seedMedications(days));
|
||||
|
||||
if (ROLLUPS) {
|
||||
const folded = await recomputeUserRollups(USER_ID, {
|
||||
granularities: ALL_GRANULARITIES,
|
||||
statementTimeoutMs: 600_000,
|
||||
});
|
||||
log("rollup rows", folded.rowsUpserted);
|
||||
}
|
||||
|
||||
for (const table of [
|
||||
"measurements",
|
||||
"measurement_rollups",
|
||||
"workouts",
|
||||
"mood_entries",
|
||||
"medication_intake_events",
|
||||
]) {
|
||||
await prisma.$executeRawUnsafe(`ANALYZE ${table}`);
|
||||
}
|
||||
console.log(`done in ${((Date.now() - started) / 1000).toFixed(1)} s`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exitCode = 1;
|
||||
})
|
||||
.finally(() => prisma.$disconnect());
|
||||
+263
-128
@@ -2,55 +2,90 @@
|
||||
/**
|
||||
* Client-bundle report + budget gate for the Turbopack build.
|
||||
*
|
||||
* The webpack-era tooling died on Next 16: `ANALYZE=1` is a silent no-op
|
||||
* under Turbopack and the old `.next/analyze/client.json` reader had
|
||||
* nothing to read — so bundle regressions (a statically imported message
|
||||
* catalog, a duplicated recharts chunk group) shipped invisibly. This
|
||||
* script derives the signal straight from the build output instead:
|
||||
* What a reader downloads is a set of chunk files: the shared
|
||||
* `rootMainFiles` from `build-manifest.json` plus the chunks a page's
|
||||
* `*_client-reference-manifest.js` names. This script measures that set per
|
||||
* page and gates it, and separately measures how much distinct client code
|
||||
* the build holds.
|
||||
*
|
||||
* - per-route client-JS totals (gzip) from the Turbopack
|
||||
* `*_client-reference-manifest.js` files plus the shared
|
||||
* `rootMainFiles` baseline from `build-manifest.json`;
|
||||
* - the total gzip weight of every emitted client chunk;
|
||||
* - a recharts-duplication guard: the number of chunks carrying the
|
||||
* recharts library fingerprint must stay exactly 1 (the shared
|
||||
* chart-runtime chunk group);
|
||||
* - a catalog guard: no message-catalog-fingerprinted chunk may be
|
||||
* referenced by any route's client-reference manifest or by the
|
||||
* shared baseline (catalogs load only lazily / via /i18n/<locale>).
|
||||
* METHOD
|
||||
*
|
||||
* First load per page. The union of `rootMainFiles` and the page's
|
||||
* client-reference chunks, deduplicated by content hash (sha-256 of the
|
||||
* file), each file gzipped at level 9 and summed. A file that two
|
||||
* manifests list under different names counts once. Every page in the
|
||||
* build is measured, not a hand-picked few: a heavy import into a page
|
||||
* nobody watched is how Zod sat in fifty metric pages for three months.
|
||||
*
|
||||
* Across pages. p50, p95 and max of the per-page first loads.
|
||||
*
|
||||
* Unique client code. Every emitted chunk is evaluated in a `node:vm`
|
||||
* sandbox with a stub `TURBOPACK` array, which collects the module
|
||||
* factories the chunk registers without running any of them. Each factory
|
||||
* is keyed by its Turbopack module id, which is global to the build, so a
|
||||
* module the bundler copied into forty chunk groups counts once. (Not by
|
||||
* the factory's text: each chunk is minified on its own, so the copies of
|
||||
* one module differ in their mangled local names.) One copy per module,
|
||||
* from the first chunk in file-name order, without the lazy message
|
||||
* catalogs, is concatenated in id order and gzipped once. A file that
|
||||
* registers no factories (the runtime, the polyfills) counts as one unit
|
||||
* by its content hash.
|
||||
*
|
||||
* The old aggregate (every emitted chunk gzipped and summed) is still
|
||||
* printed, for continuity, but no longer gated: it counts a module once
|
||||
* per chunk group it lands in, so it rose every time a page got lighter
|
||||
* by moving code into on-demand chunks.
|
||||
*
|
||||
* GATES (bundle-budget.json)
|
||||
*
|
||||
* - routeBaselineKbGz: measured first load per page; a page fails when it
|
||||
* is more than routeDriftAllowanceKbGz over its baseline, or when it is
|
||||
* a page with no baseline (write one with --write-baselines).
|
||||
* - firstLoadP95KbGz / firstLoadMaxKbGz: caps on the distribution.
|
||||
* - uniqueClientKbGz: cap on the distinct client code.
|
||||
* - maxCatalogKbGz: per lazy message catalog.
|
||||
* - maxRechartsChunks: chunks carrying the Recharts fingerprint (one, the
|
||||
* shared chart-runtime group).
|
||||
* - no message catalog referenced by a page or the shared baseline.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm bundle-report # print the report table
|
||||
* pnpm bundle-report --check # enforce bundle-budget.json (CI gate)
|
||||
* pnpm bundle-report # print the report
|
||||
* pnpm bundle-check # enforce bundle-budget.json
|
||||
* node scripts/check-bundle-budget.mjs --write-baselines
|
||||
* # rewrite routeBaselineKbGz from
|
||||
* # this build (a reviewed change)
|
||||
* node scripts/check-bundle-budget.mjs --dir <build dir> [--json <file>]
|
||||
*
|
||||
* Budgets live in `bundle-budget.json` (repo root). Numbers are KB gzip.
|
||||
*
|
||||
* Per-route the file states a MEASURED baseline and one drift allowance
|
||||
* over it, and a route fails on the delta rather than on a ceiling it is
|
||||
* already touching. Flat ceilings had been re-stated but never re-based, so
|
||||
* three routes ended up sitting within a kilobyte of theirs and any
|
||||
* unrelated dependency bump failed the e2e job on a regression nobody had
|
||||
* made. The delta is printed on every run, so a route that has crept most of
|
||||
* the way through its allowance is readable in the log before it is red.
|
||||
*
|
||||
* The gate still exists to catch step changes (a ~100 KB statically imported
|
||||
* catalog, a ~90 KB duplicate library), which are several times the
|
||||
* allowance. Re-base consciously, in the same PR that pays the cost, with
|
||||
* the reason written into `$comment`.
|
||||
* Numbers are KB gzip. A local arm64 build measures each page about 1.5 KB
|
||||
* lighter than the CI runner; the drift allowance covers that and ordinary
|
||||
* week-to-week movement, while the step changes the gate exists for (a
|
||||
* statically imported catalog, a duplicated chart library, Zod on a page
|
||||
* that had none: 60 to 100 KB each) are several times larger.
|
||||
*/
|
||||
|
||||
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
||||
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { join } from "node:path";
|
||||
import vm from "node:vm";
|
||||
import { gzipSync } from "node:zlib";
|
||||
|
||||
const CHECK = process.argv.includes("--check");
|
||||
const BUILD_DIR = join(process.cwd(), ".next");
|
||||
const argv = process.argv.slice(2);
|
||||
const flag = (name) => argv.includes(name);
|
||||
const option = (name) => {
|
||||
const i = argv.indexOf(name);
|
||||
return i >= 0 ? argv[i + 1] : undefined;
|
||||
};
|
||||
|
||||
const CHECK = flag("--check");
|
||||
const WRITE_BASELINES = flag("--write-baselines");
|
||||
const BUILD_DIR = option("--dir") ?? join(process.cwd(), ".next");
|
||||
const JSON_OUT = option("--json");
|
||||
const APP_DIR = join(BUILD_DIR, "server", "app");
|
||||
const BUDGET_PATH = join(process.cwd(), "bundle-budget.json");
|
||||
|
||||
if (!existsSync(join(BUILD_DIR, "build-manifest.json"))) {
|
||||
console.error(
|
||||
"check-bundle-budget: no .next/build-manifest.json — run `pnpm build` first.",
|
||||
`check-bundle-budget: no ${join(BUILD_DIR, "build-manifest.json")} — run \`pnpm build\` first.`,
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
@@ -58,30 +93,47 @@ if (!existsSync(join(BUILD_DIR, "build-manifest.json"))) {
|
||||
/** Recharts library fingerprint (present once in the shared runtime chunk). */
|
||||
const RECHARTS_MARK = "CartesianGrid";
|
||||
/**
|
||||
* Message-catalog fingerprint. The key set is identical across all six
|
||||
* locales, so a KEY-position match ("typeTimeInDaylight" directly followed
|
||||
* by a colon, in either the plain or the JSON.parse-escaped chunk encoding)
|
||||
* marks a bundled catalog. Source modules only ever carry the namespaced
|
||||
* call-site form (`measurements.typeTimeInDaylight`), which matches neither.
|
||||
* Message-catalog fingerprint. The key set is identical across all locales,
|
||||
* so a KEY-position match ("typeTimeInDaylight" directly followed by a colon,
|
||||
* in either the plain or the JSON.parse-escaped chunk encoding) marks a
|
||||
* bundled catalog. Source modules only ever carry the namespaced call-site
|
||||
* form (`measurements.typeTimeInDaylight`), which matches neither.
|
||||
*/
|
||||
const CATALOG_MARKS = ['"typeTimeInDaylight":', 'typeTimeInDaylight\\":'];
|
||||
|
||||
const sizeCache = new Map();
|
||||
const sha = (buf) => createHash("sha256").update(buf).digest("hex");
|
||||
const gz = (buf) => gzipSync(buf, { level: 9 }).length;
|
||||
const kb = (bytes) => bytes / 1024;
|
||||
const fmt = (bytes) => `${kb(bytes).toFixed(0)} KB`;
|
||||
const round1 = (n) => Math.round(n * 10) / 10;
|
||||
|
||||
const fileCache = new Map();
|
||||
function chunkInfo(file) {
|
||||
if (!sizeCache.has(file)) {
|
||||
if (!fileCache.has(file)) {
|
||||
let info = {
|
||||
hash: `missing:${file}`,
|
||||
raw: 0,
|
||||
gz: 0,
|
||||
recharts: false,
|
||||
catalog: false,
|
||||
};
|
||||
try {
|
||||
const buf = readFileSync(join(BUILD_DIR, file));
|
||||
sizeCache.set(file, {
|
||||
info = {
|
||||
hash: sha(buf),
|
||||
raw: buf.length,
|
||||
gz: gzipSync(buf, { level: 9 }).length,
|
||||
gz: gz(buf),
|
||||
recharts: buf.includes(RECHARTS_MARK),
|
||||
catalog: CATALOG_MARKS.some((m) => buf.includes(m)),
|
||||
});
|
||||
buf,
|
||||
};
|
||||
} catch {
|
||||
sizeCache.set(file, { raw: 0, gz: 0, recharts: false, catalog: false });
|
||||
// A manifest naming a file the build did not emit: counted as zero and
|
||||
// reported by its missing hash rather than crashing the report.
|
||||
}
|
||||
fileCache.set(file, info);
|
||||
}
|
||||
return sizeCache.get(file);
|
||||
return fileCache.get(file);
|
||||
}
|
||||
|
||||
function routeChunks(manifestFile) {
|
||||
@@ -97,24 +149,54 @@ function routeChunks(manifestFile) {
|
||||
return files;
|
||||
}
|
||||
|
||||
function findRouteManifests(dir, prefix = "") {
|
||||
function findPageManifests(dir, prefix = "") {
|
||||
let out = [];
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const p = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
out = out.concat(findRouteManifests(p, `${prefix}/${entry.name}`));
|
||||
} else if (entry.name.endsWith("_client-reference-manifest.js")) {
|
||||
out.push({
|
||||
route: `${prefix}/${entry.name.replace("_client-reference-manifest.js", "")}`,
|
||||
file: p,
|
||||
});
|
||||
out = out.concat(findPageManifests(p, `${prefix}/${entry.name}`));
|
||||
} else if (entry.name === "page_client-reference-manifest.js") {
|
||||
out.push({ route: `${prefix}/page`, file: p });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const kb = (bytes) => bytes / 1024;
|
||||
const fmt = (bytes) => `${kb(bytes).toFixed(0)} KB`;
|
||||
function percentile(sorted, p) {
|
||||
if (sorted.length === 0) return 0;
|
||||
const i = Math.min(
|
||||
sorted.length - 1,
|
||||
Math.ceil((p / 100) * sorted.length) - 1,
|
||||
);
|
||||
return sorted[Math.max(0, i)];
|
||||
}
|
||||
|
||||
/**
|
||||
* The `[module id, factory source]` pairs a Turbopack chunk registers,
|
||||
* without running them: `TURBOPACK.push([script, id, factory, id, factory,
|
||||
* …])`. Null when the file is not in that shape.
|
||||
*/
|
||||
function chunkFactories(buf) {
|
||||
const sandbox = { TURBOPACK: [] };
|
||||
sandbox.globalThis = sandbox;
|
||||
sandbox.self = sandbox;
|
||||
try {
|
||||
vm.runInNewContext(buf.toString("utf8"), sandbox, { timeout: 5000 });
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const modules = [];
|
||||
for (const entry of sandbox.TURBOPACK) {
|
||||
if (!Array.isArray(entry)) continue;
|
||||
for (let i = 1; i < entry.length; i++) {
|
||||
if (typeof entry[i] !== "function") continue;
|
||||
const id = entry[i - 1];
|
||||
if (typeof id !== "number" && typeof id !== "string") continue;
|
||||
modules.push([String(id), entry[i].toString()]);
|
||||
}
|
||||
}
|
||||
return modules.length > 0 ? modules : null;
|
||||
}
|
||||
|
||||
// ── Shared baseline ─────────────────────────────────────────────────────────
|
||||
const buildManifest = JSON.parse(
|
||||
@@ -123,88 +205,135 @@ const buildManifest = JSON.parse(
|
||||
const rootFiles = buildManifest.rootMainFiles ?? [];
|
||||
let rootGz = 0;
|
||||
for (const f of rootFiles) rootGz += chunkInfo(f).gz;
|
||||
const rootCatalogRef = rootFiles.some((f) => chunkInfo(f).catalog);
|
||||
|
||||
// ── Per-route totals ────────────────────────────────────────────────────────
|
||||
const manifests = findRouteManifests(APP_DIR);
|
||||
const routeTotals = new Map();
|
||||
for (const { route, file } of manifests) {
|
||||
let gz = 0;
|
||||
// ── Per-page first load, deduplicated by content hash ──────────────────────
|
||||
const pages = new Map();
|
||||
for (const { route, file } of findPageManifests(APP_DIR)) {
|
||||
const byHash = new Map();
|
||||
let catalogRef = false;
|
||||
for (const chunk of routeChunks(file)) {
|
||||
for (const chunk of [...rootFiles, ...routeChunks(file)]) {
|
||||
const info = chunkInfo(chunk);
|
||||
gz += info.gz;
|
||||
if (info.catalog) catalogRef = true;
|
||||
byHash.set(info.hash, info.gz);
|
||||
if (info.catalog && !rootFiles.includes(chunk)) catalogRef = true;
|
||||
}
|
||||
routeTotals.set(route, { gz: gz + rootGz, catalogRef });
|
||||
let total = 0;
|
||||
for (const size of byHash.values()) total += size;
|
||||
pages.set(route, { gz: total, catalogRef });
|
||||
}
|
||||
const firstLoads = [...pages.values()].map((p) => p.gz).sort((a, b) => a - b);
|
||||
const p50 = percentile(firstLoads, 50);
|
||||
const p95 = percentile(firstLoads, 95);
|
||||
const max = firstLoads[firstLoads.length - 1] ?? 0;
|
||||
|
||||
// ── Whole-build chunk scan ──────────────────────────────────────────────────
|
||||
// ── Whole build: old aggregate, catalogs, recharts, unique modules ─────────
|
||||
const chunkDir = join(BUILD_DIR, "static", "chunks");
|
||||
let totalGz = 0;
|
||||
let summedGz = 0;
|
||||
let rechartsChunks = 0;
|
||||
let catalogChunks = 0;
|
||||
let catalogGz = 0;
|
||||
let largestCatalog = { file: "-", gz: 0 };
|
||||
let largest = { file: "-", gz: 0 };
|
||||
for (const f of readdirSync(chunkDir)) {
|
||||
const uniqueUnits = new Map();
|
||||
for (const f of readdirSync(chunkDir).sort()) {
|
||||
if (!f.endsWith(".js")) continue;
|
||||
const info = chunkInfo(join("static", "chunks", f));
|
||||
totalGz += info.gz;
|
||||
summedGz += info.gz;
|
||||
if (info.recharts) rechartsChunks += 1;
|
||||
if (info.catalog) {
|
||||
catalogChunks += 1;
|
||||
catalogGz += info.gz;
|
||||
if (info.gz > largestCatalog.gz) largestCatalog = { file: f, gz: info.gz };
|
||||
continue;
|
||||
}
|
||||
const modules = info.buf ? chunkFactories(info.buf) : null;
|
||||
if (modules) {
|
||||
for (const [id, source] of modules) {
|
||||
if (!uniqueUnits.has(`m:${id}`)) uniqueUnits.set(`m:${id}`, source);
|
||||
}
|
||||
} else if (info.buf) {
|
||||
uniqueUnits.set(`f:${info.hash}`, info.buf.toString("utf8"));
|
||||
}
|
||||
if (info.gz > largest.gz) largest = { file: f, gz: info.gz };
|
||||
}
|
||||
const rootCatalogRef = rootFiles.some((f) => chunkInfo(f).catalog);
|
||||
const uniqueKeys = [...uniqueUnits.keys()].sort();
|
||||
const uniqueGz = gz(
|
||||
Buffer.from(uniqueKeys.map((k) => uniqueUnits.get(k)).join("\n"), "utf8"),
|
||||
);
|
||||
|
||||
// ── Report ──────────────────────────────────────────────────────────────────
|
||||
const budget = existsSync(BUDGET_PATH)
|
||||
? JSON.parse(readFileSync(BUDGET_PATH, "utf8"))
|
||||
: null;
|
||||
const baselines = budget?.routeBaselineKbGz ?? {};
|
||||
const allowanceKb = budget?.routeDriftAllowanceKbGz ?? 0;
|
||||
|
||||
console.log(`pages measured: ${pages.size}`);
|
||||
console.log(`shared baseline (rootMainFiles): ${fmt(rootGz)} gz`);
|
||||
console.log(`all client chunks: ${fmt(totalGz)} gz`);
|
||||
console.log(
|
||||
` lazy message catalogs: ${fmt(catalogGz)} gz (${catalogChunks} chunks)`,
|
||||
);
|
||||
console.log(` other client chunks: ${fmt(totalGz - catalogGz)} gz`);
|
||||
console.log(
|
||||
` largest message catalog: ${fmt(largestCatalog.gz)} gz (${largestCatalog.file})`,
|
||||
`first load across pages: p50 ${fmt(p50)}, p95 ${fmt(p95)}, max ${fmt(max)} gz`,
|
||||
);
|
||||
console.log(
|
||||
`largest chunk: ${fmt(largest.gz)} gz ${largest.file}`,
|
||||
`unique client code: ${fmt(uniqueGz)} gz (${uniqueKeys.length} distinct modules, catalogs excluded)`,
|
||||
);
|
||||
console.log(
|
||||
`lazy message catalogs: ${fmt(catalogGz)} gz (${catalogChunks} chunks, largest ${fmt(largestCatalog.gz)} ${largestCatalog.file})`,
|
||||
);
|
||||
console.log(
|
||||
`summed chunks, not gated: ${fmt(summedGz - catalogGz)} gz (every emitted chunk once, catalogs excluded)`,
|
||||
);
|
||||
console.log(`recharts-fingerprint chunks: ${rechartsChunks}`);
|
||||
console.log("");
|
||||
|
||||
const baselines = budget?.routeBaselineKbGz ?? {};
|
||||
const allowanceKb = budget?.routeDriftAllowanceKbGz ?? 0;
|
||||
/** The ceiling a route's measured baseline plus the drift allowance buys it. */
|
||||
const capFor = (route) =>
|
||||
baselines[route] == null ? null : baselines[route] + allowanceKb;
|
||||
|
||||
const watched = Object.keys(baselines);
|
||||
const rows = [...routeTotals.entries()]
|
||||
.filter(([route]) => !watched.length || watched.includes(route))
|
||||
.sort((a, b) => b[1].gz - a[1].gz);
|
||||
for (const [route, { gz }] of rows.slice(0, watched.length || 15)) {
|
||||
const baseline = baselines[route];
|
||||
if (baseline == null) {
|
||||
console.log(`${fmt(gz).padStart(8)} gz ${route}`);
|
||||
continue;
|
||||
}
|
||||
const delta = kb(gz) - baseline;
|
||||
const sign = delta >= 0 ? "+" : "−";
|
||||
const rows = [...pages.entries()]
|
||||
.map(([route, { gz: size }]) => {
|
||||
const baseline = baselines[route];
|
||||
return {
|
||||
route,
|
||||
kb: round1(kb(size)),
|
||||
baseline: baseline ?? null,
|
||||
delta: baseline == null ? null : round1(kb(size) - baseline),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => (b.delta ?? Infinity) - (a.delta ?? Infinity) || b.kb - a.kb);
|
||||
console.log("largest movers against their baseline:");
|
||||
for (const r of rows.slice(0, 15)) {
|
||||
const delta =
|
||||
r.delta == null
|
||||
? "no baseline"
|
||||
: `${r.delta >= 0 ? "+" : "−"}${Math.abs(r.delta).toFixed(1)} KB of ${allowanceKb}`;
|
||||
console.log(
|
||||
`${fmt(gz).padStart(8)} gz ${route} (baseline ${baseline} KB, ` +
|
||||
`${sign}${Math.abs(delta).toFixed(1)} KB of ${allowanceKb} KB allowed, ` +
|
||||
`cap ${capFor(route)} KB)`,
|
||||
`${`${r.kb.toFixed(0)} KB`.padStart(8)} gz ${r.route} (${delta})`,
|
||||
);
|
||||
}
|
||||
|
||||
const report = {
|
||||
pages: Object.fromEntries(rows.map((r) => [r.route, r.kb])),
|
||||
firstLoadKbGz: {
|
||||
p50: round1(kb(p50)),
|
||||
p95: round1(kb(p95)),
|
||||
max: round1(kb(max)),
|
||||
},
|
||||
uniqueClientKbGz: round1(kb(uniqueGz)),
|
||||
summedChunksKbGz: round1(kb(summedGz - catalogGz)),
|
||||
catalogKbGz: round1(kb(catalogGz)),
|
||||
largestCatalogKbGz: round1(kb(largestCatalog.gz)),
|
||||
rechartsChunks,
|
||||
};
|
||||
if (JSON_OUT) writeFileSync(JSON_OUT, `${JSON.stringify(report, null, 2)}\n`);
|
||||
|
||||
if (WRITE_BASELINES) {
|
||||
if (!budget) {
|
||||
console.error("check-bundle-budget: bundle-budget.json missing.");
|
||||
process.exit(2);
|
||||
}
|
||||
budget.routeBaselineKbGz = Object.fromEntries(
|
||||
rows
|
||||
.map((r) => [r.route, Math.round(r.kb)])
|
||||
.sort(([a], [b]) => a.localeCompare(b)),
|
||||
);
|
||||
writeFileSync(BUDGET_PATH, `${JSON.stringify(budget, null, 2)}\n`);
|
||||
console.log(`\nwrote ${rows.length} page baselines to bundle-budget.json`);
|
||||
}
|
||||
|
||||
if (!CHECK) process.exit(0);
|
||||
|
||||
// ── Budget gate ─────────────────────────────────────────────────────────────
|
||||
@@ -212,42 +341,48 @@ if (!budget) {
|
||||
console.error("check-bundle-budget: bundle-budget.json missing.");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
// A budget file that states no baselines gates nothing, and would do it
|
||||
// quietly — the loop below would simply have nothing to walk. Say so instead.
|
||||
if (watched.length === 0) {
|
||||
// A budget that states no page baselines gates nothing, and would do it
|
||||
// quietly. A build with no pages is a broken build, not a light one.
|
||||
if (Object.keys(baselines).length === 0 || pages.size === 0) {
|
||||
console.error(
|
||||
"check-bundle-budget: bundle-budget.json states no routeBaselineKbGz " +
|
||||
"entries, so no route is gated. Measure the eager routes and write " +
|
||||
"them in, with a routeDriftAllowanceKbGz over them.",
|
||||
"check-bundle-budget: no page baselines in bundle-budget.json, or no " +
|
||||
"pages in the build. Measure them with --write-baselines.",
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const failures = [];
|
||||
for (const [route, baselineKb] of Object.entries(baselines)) {
|
||||
const actual = routeTotals.get(route);
|
||||
if (!actual) {
|
||||
failures.push(`route ${route} not found in the build output`);
|
||||
continue;
|
||||
}
|
||||
const delta = kb(actual.gz) - baselineKb;
|
||||
if (delta > allowanceKb) {
|
||||
for (const r of rows) {
|
||||
if (r.baseline == null) {
|
||||
failures.push(
|
||||
`${route}: ${fmt(actual.gz)} gz is ${delta.toFixed(1)} KB over the ` +
|
||||
`${baselineKb} KB baseline, past the ${allowanceKb} KB drift ` +
|
||||
`allowance (cap ${capFor(route)} KB)`,
|
||||
`${r.route}: ${r.kb} KB gz and no baseline. A new page gets one with --write-baselines in the same change.`,
|
||||
);
|
||||
} else if (r.delta > allowanceKb) {
|
||||
failures.push(
|
||||
`${r.route}: ${r.kb} KB gz is ${r.delta.toFixed(1)} KB over its ${r.baseline} KB baseline, past the ${allowanceKb} KB drift allowance`,
|
||||
);
|
||||
}
|
||||
}
|
||||
// The aggregate leaves the lazy message catalogs out. A reader downloads one
|
||||
// catalog, the one for their language, so counting all of them made every new
|
||||
// interface language look like a regression. Each catalog is held to its own
|
||||
// ceiling instead.
|
||||
const nonCatalogGz = totalGz - catalogGz;
|
||||
if (budget.totalClientKbGz && kb(nonCatalogGz) > budget.totalClientKbGz) {
|
||||
for (const route of Object.keys(baselines)) {
|
||||
if (!pages.has(route)) {
|
||||
failures.push(
|
||||
`${route} has a baseline but is not in the build; remove it with --write-baselines`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (budget.firstLoadP95KbGz && kb(p95) > budget.firstLoadP95KbGz) {
|
||||
failures.push(
|
||||
`client JS without the lazy catalogs ${fmt(nonCatalogGz)} gz exceeds the ${budget.totalClientKbGz} KB budget`,
|
||||
`first-load p95 ${kb(p95).toFixed(1)} KB gz exceeds the ${budget.firstLoadP95KbGz} KB budget`,
|
||||
);
|
||||
}
|
||||
if (budget.firstLoadMaxKbGz && kb(max) > budget.firstLoadMaxKbGz) {
|
||||
failures.push(
|
||||
`largest first load ${kb(max).toFixed(1)} KB gz exceeds the ${budget.firstLoadMaxKbGz} KB budget`,
|
||||
);
|
||||
}
|
||||
if (budget.uniqueClientKbGz && kb(uniqueGz) > budget.uniqueClientKbGz) {
|
||||
failures.push(
|
||||
`unique client code ${kb(uniqueGz).toFixed(1)} KB gz exceeds the ${budget.uniqueClientKbGz} KB budget`,
|
||||
);
|
||||
}
|
||||
if (budget.maxCatalogKbGz && kb(largestCatalog.gz) > budget.maxCatalogKbGz) {
|
||||
@@ -263,12 +398,12 @@ if (
|
||||
`${rechartsChunks} recharts-fingerprint chunks (budget ${budget.maxRechartsChunks}) — a chart import bypassed the shared chart-runtime boundary`,
|
||||
);
|
||||
}
|
||||
const catalogRefRoutes = [...routeTotals.entries()]
|
||||
const catalogRefPages = [...pages.entries()]
|
||||
.filter(([, v]) => v.catalogRef)
|
||||
.map(([r]) => r);
|
||||
if (catalogRefRoutes.length > 0 || rootCatalogRef) {
|
||||
if (catalogRefPages.length > 0 || rootCatalogRef) {
|
||||
failures.push(
|
||||
`message catalog statically referenced by ${rootCatalogRef ? "the shared baseline" : catalogRefRoutes.join(", ")} — catalogs must stay lazy (/i18n/<locale> + dynamic import)`,
|
||||
`message catalog statically referenced by ${rootCatalogRef ? "the shared baseline" : catalogRefPages.join(", ")} — catalogs must stay lazy (/i18n/<locale> + dynamic import)`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,296 +0,0 @@
|
||||
/**
|
||||
* Seed one account shaped like a multi-year Apple Health `export.xml` import,
|
||||
* for reproducing the maintenance jobs at the size a real heavy account
|
||||
* reaches (issue #1031: about 1.26 million measurement rows).
|
||||
*
|
||||
* DATABASE_URL=postgresql://… pnpm dlx tsx scripts/seed-large-account.ts \
|
||||
* [--user bench-large] [--years 4] [--pulse-seconds 165]
|
||||
*
|
||||
* Never point this at a database that holds real data: it inserts rows under
|
||||
* the given user id and, with `--reset`, deletes that user first.
|
||||
*
|
||||
* The shape follows what the import and the iOS sync actually write:
|
||||
*
|
||||
* - dense spot rows (heart rate every `--pulse-seconds`, HRV, SpO2) keyed by
|
||||
* a hashed sample id, the tier the dense-intraday retention folds;
|
||||
* - high-frequency mean rows (respiratory rate, gait metrics, audio
|
||||
* exposure), the tier the daily-mean consolidation folds;
|
||||
* - one `stats:` daily row per cumulative type and day (what the export
|
||||
* import writes for steps, energy, distance and flights);
|
||||
* - per-sample cumulative rows with opaque sample ids for the most recent
|
||||
* 90 days (what the iOS sync writes, and what the cumulative drain folds);
|
||||
* - sleep-stage segments every night.
|
||||
*
|
||||
* Everything is generated inside Postgres with `generate_series`, so a million
|
||||
* rows take seconds rather than a million round trips. `ANALYZE` runs at the
|
||||
* end: a planner reading pre-seed statistics picks plans no production table
|
||||
* of this size would get.
|
||||
*/
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
function arg(name: string, fallback: string): string {
|
||||
const i = process.argv.indexOf(`--${name}`);
|
||||
return i >= 0 && process.argv[i + 1] ? process.argv[i + 1]! : fallback;
|
||||
}
|
||||
|
||||
const USER_ID = arg("user", "bench-large");
|
||||
const YEARS = Number(arg("years", "4"));
|
||||
const PULSE_SECONDS = Number(arg("pulse-seconds", "165"));
|
||||
const RESET = process.argv.includes("--reset");
|
||||
|
||||
/** A spot series: `perDay` evenly spaced rows per day over the whole span. */
|
||||
interface SpotSeries {
|
||||
type: string;
|
||||
unit: string;
|
||||
perDay: number;
|
||||
base: number;
|
||||
spread: number;
|
||||
/** Days back from now the series covers. Defaults to the whole span. */
|
||||
days?: number;
|
||||
/** externalId prefix; `stats:` rows are generated separately. */
|
||||
idPrefix: string;
|
||||
}
|
||||
|
||||
async function insertSpot(s: SpotSeries, days: number): Promise<number> {
|
||||
const step = Math.floor(86_400 / s.perDay);
|
||||
const count = Math.floor((days * 86_400) / step);
|
||||
// Jitter stays below the step so every measured_at is distinct per type
|
||||
// (the (user, type, measured_at, source, sleep_stage) unique index).
|
||||
const jitter = Math.max(1, Math.min(59, step - 1));
|
||||
return prisma.$executeRawUnsafe(
|
||||
`
|
||||
INSERT INTO measurements
|
||||
(id, user_id, type, value, unit, source, measured_at, external_id, created_at, updated_at)
|
||||
SELECT
|
||||
'seed-' || $1 || '-' || $2 || '-' || g,
|
||||
$1,
|
||||
$2::measurement_type,
|
||||
round(($3 + $4 * sin(g / 37.0) + ($4 / 2) * ((g * 7919) % 100) / 100.0)::numeric, 2),
|
||||
$5,
|
||||
'APPLE_HEALTH',
|
||||
date_trunc('minute', now()) - make_interval(secs => $6 * $7::bigint)
|
||||
+ make_interval(secs => g * $6 + (g * 31) % $8),
|
||||
$9 || md5($2 || g::text),
|
||||
now(), now()
|
||||
FROM generate_series(0, $7::bigint - 1) AS g
|
||||
`,
|
||||
USER_ID,
|
||||
s.type,
|
||||
s.base,
|
||||
s.spread,
|
||||
s.unit,
|
||||
step,
|
||||
count,
|
||||
jitter,
|
||||
s.idPrefix,
|
||||
);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const days = Math.round(YEARS * 365);
|
||||
|
||||
if (RESET) {
|
||||
await prisma.$executeRawUnsafe(`DELETE FROM users WHERE id = $1`, USER_ID);
|
||||
}
|
||||
await prisma.$executeRawUnsafe(
|
||||
`INSERT INTO users (id, username, timezone, updated_at)
|
||||
VALUES ($1, $1, 'Europe/Berlin', now())
|
||||
ON CONFLICT (id) DO NOTHING`,
|
||||
USER_ID,
|
||||
);
|
||||
|
||||
const pulsePerDay = Math.floor(86_400 / PULSE_SECONDS);
|
||||
const spot: SpotSeries[] = [
|
||||
{
|
||||
type: "PULSE",
|
||||
unit: "bpm",
|
||||
perDay: pulsePerDay,
|
||||
base: 62,
|
||||
spread: 25,
|
||||
idPrefix: "hk-",
|
||||
},
|
||||
{
|
||||
type: "HEART_RATE_VARIABILITY",
|
||||
unit: "ms",
|
||||
perDay: 12,
|
||||
base: 45,
|
||||
spread: 20,
|
||||
idPrefix: "hk-",
|
||||
},
|
||||
{
|
||||
type: "OXYGEN_SATURATION",
|
||||
unit: "%",
|
||||
perDay: 24,
|
||||
base: 96,
|
||||
spread: 2,
|
||||
idPrefix: "hk-",
|
||||
},
|
||||
{
|
||||
type: "RESPIRATORY_RATE",
|
||||
unit: "breaths/min",
|
||||
perDay: 48,
|
||||
base: 14,
|
||||
spread: 3,
|
||||
idPrefix: "hk-",
|
||||
},
|
||||
{
|
||||
type: "WALKING_SPEED",
|
||||
unit: "km/h",
|
||||
perDay: 30,
|
||||
base: 4.8,
|
||||
spread: 0.8,
|
||||
idPrefix: "hk-",
|
||||
},
|
||||
{
|
||||
type: "WALKING_STEP_LENGTH",
|
||||
unit: "cm",
|
||||
perDay: 30,
|
||||
base: 72,
|
||||
spread: 6,
|
||||
idPrefix: "hk-",
|
||||
},
|
||||
{
|
||||
type: "WALKING_ASYMMETRY",
|
||||
unit: "%",
|
||||
perDay: 30,
|
||||
base: 3,
|
||||
spread: 2,
|
||||
idPrefix: "hk-",
|
||||
},
|
||||
{
|
||||
type: "WALKING_DOUBLE_SUPPORT",
|
||||
unit: "%",
|
||||
perDay: 30,
|
||||
base: 28,
|
||||
spread: 3,
|
||||
idPrefix: "hk-",
|
||||
},
|
||||
{
|
||||
type: "AUDIO_EXPOSURE_ENV",
|
||||
unit: "dB",
|
||||
perDay: 60,
|
||||
base: 62,
|
||||
spread: 12,
|
||||
idPrefix: "hk-",
|
||||
},
|
||||
// The iOS sync's per-sample cumulative rows: dense, recent, opaque ids.
|
||||
{
|
||||
type: "ACTIVITY_STEPS",
|
||||
unit: "steps",
|
||||
perDay: 100,
|
||||
base: 90,
|
||||
spread: 60,
|
||||
days: 90,
|
||||
idPrefix: "uuid-",
|
||||
},
|
||||
{
|
||||
type: "ACTIVE_ENERGY_BURNED",
|
||||
unit: "kcal",
|
||||
perDay: 400,
|
||||
base: 1.2,
|
||||
spread: 0.9,
|
||||
days: 90,
|
||||
idPrefix: "uuid-",
|
||||
},
|
||||
{
|
||||
type: "WALKING_RUNNING_DISTANCE",
|
||||
unit: "m",
|
||||
perDay: 100,
|
||||
base: 70,
|
||||
spread: 50,
|
||||
days: 90,
|
||||
idPrefix: "uuid-",
|
||||
},
|
||||
{
|
||||
type: "WEIGHT",
|
||||
unit: "kg",
|
||||
perDay: 1,
|
||||
base: 82,
|
||||
spread: 1.5,
|
||||
idPrefix: "hk-",
|
||||
},
|
||||
];
|
||||
|
||||
let total = 0;
|
||||
for (const s of spot) {
|
||||
const n = await insertSpot(s, s.days ?? days);
|
||||
total += n;
|
||||
console.log(`${s.type.padEnd(28)} ${String(n).padStart(9)}`);
|
||||
}
|
||||
|
||||
// One export-estimate `stats:` row per cumulative type and local day, at
|
||||
// local noon — what `flushCumulativeBuckets` in the export import writes.
|
||||
for (const [type, hk, unit, base] of [
|
||||
["ACTIVITY_STEPS", "HKQuantityTypeIdentifierStepCount", "steps", 8000],
|
||||
[
|
||||
"ACTIVE_ENERGY_BURNED",
|
||||
"HKQuantityTypeIdentifierActiveEnergyBurned",
|
||||
"kcal",
|
||||
550,
|
||||
],
|
||||
[
|
||||
"WALKING_RUNNING_DISTANCE",
|
||||
"HKQuantityTypeIdentifierDistanceWalkingRunning",
|
||||
"m",
|
||||
6000,
|
||||
],
|
||||
["FLIGHTS_CLIMBED", "HKQuantityTypeIdentifierFlightsClimbed", "count", 9],
|
||||
] as const) {
|
||||
const n = await prisma.$executeRawUnsafe(
|
||||
`
|
||||
INSERT INTO measurements
|
||||
(id, user_id, type, value, unit, source, measured_at, external_id,
|
||||
aggregation_provenance, created_at, updated_at)
|
||||
SELECT
|
||||
'seed-' || $1 || '-' || $2 || '-stats-' || d,
|
||||
$1, $2::measurement_type, $5 + (d * 7919) % ($5 / 2), $4, 'APPLE_HEALTH',
|
||||
((current_date - d)::timestamp + interval '12 hours')
|
||||
AT TIME ZONE 'Europe/Berlin' AT TIME ZONE 'UTC',
|
||||
'stats:' || $3 || ':' || to_char(current_date - d, 'YYYY-MM-DD'),
|
||||
'EXPORT_XML_SOURCE_MAX', now(), now()
|
||||
FROM generate_series(91, $6::int) AS d
|
||||
`,
|
||||
USER_ID,
|
||||
type,
|
||||
hk,
|
||||
unit,
|
||||
base,
|
||||
days,
|
||||
);
|
||||
total += n;
|
||||
console.log(`${`${type} (stats:)`.padEnd(28)} ${String(n).padStart(9)}`);
|
||||
}
|
||||
|
||||
// Sleep: 30 segments a night from 23:00 local, 16 minutes apart, rotating
|
||||
// through the stages. Distinct measured_at per segment.
|
||||
const sleep = await prisma.$executeRawUnsafe(
|
||||
`
|
||||
INSERT INTO measurements
|
||||
(id, user_id, type, value, unit, source, measured_at, external_id,
|
||||
sleep_stage, created_at, updated_at)
|
||||
SELECT
|
||||
'seed-' || $1 || '-sleep-' || d || '-' || s,
|
||||
$1, 'SLEEP_DURATION', 16, 'minutes', 'APPLE_HEALTH',
|
||||
((current_date - d)::timestamp - interval '1 hour' + s * interval '16 minutes')
|
||||
AT TIME ZONE 'Europe/Berlin' AT TIME ZONE 'UTC',
|
||||
'hk-' || md5('sleep' || d || '-' || s),
|
||||
(ARRAY['CORE','DEEP','CORE','REM','AWAKE','CORE']::sleep_stage[])[1 + s % 6],
|
||||
now(), now()
|
||||
FROM generate_series(1, $2::int) AS d, generate_series(0, 29) AS s
|
||||
`,
|
||||
USER_ID,
|
||||
days,
|
||||
);
|
||||
total += sleep;
|
||||
console.log(`${"SLEEP_DURATION".padEnd(28)} ${String(sleep).padStart(9)}`);
|
||||
|
||||
await prisma.$executeRawUnsafe(`ANALYZE measurements`);
|
||||
console.log(`${"total".padEnd(28)} ${String(total).padStart(9)}`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exitCode = 1;
|
||||
})
|
||||
.finally(() => prisma.$disconnect());
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
convertHkValue,
|
||||
isConvertibleHkUnit,
|
||||
} from "@/lib/measurements/hk-units";
|
||||
import { readSource } from "@/__tests__/helpers/source-files";
|
||||
|
||||
const ENTRIES = Object.values(APPLE_HEALTH_TYPE_MAP);
|
||||
|
||||
@@ -193,10 +194,10 @@ describe("Apple Health mapping — the record's own unit (issue #944)", () => {
|
||||
"utf8",
|
||||
);
|
||||
expect(archive).toContain(optIn);
|
||||
const batch = readFileSync(
|
||||
join(process.cwd(), "src/app/api/measurements/batch/route.ts"),
|
||||
"utf8",
|
||||
);
|
||||
const batch = readSource("src/app/api/measurements/batch/route.ts", [
|
||||
"export const POST",
|
||||
"hkIdentifier",
|
||||
]);
|
||||
expect(
|
||||
batch,
|
||||
"the batch route opted into record-unit conversion",
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"$comment": "Runtime import cycles under src/: the back-edge count and the strongly connected components. Maintained by src/__tests__/import-cycle-guard.test.ts.",
|
||||
"cycles": 13,
|
||||
"components": [
|
||||
[
|
||||
"components/command-palette/command-palette.tsx",
|
||||
"components/command-palette/palette-store.ts",
|
||||
"components/layout/global-shortcuts.tsx"
|
||||
],
|
||||
[
|
||||
"lib/analytics/compliance.ts",
|
||||
"lib/analytics/compliance/display.ts",
|
||||
"lib/analytics/compliance/ledger.ts",
|
||||
"lib/analytics/compliance/occurrences.ts",
|
||||
"lib/analytics/compliance/windows.ts",
|
||||
"lib/medications/scheduling/band-minter.ts",
|
||||
"lib/medications/scheduling/cadence.ts",
|
||||
"lib/medications/scheduling/compliance.ts",
|
||||
"lib/medications/scheduling/schedule-eras.ts"
|
||||
],
|
||||
[
|
||||
"lib/auth/audit.ts",
|
||||
"lib/geo.ts",
|
||||
"lib/integrations/oauth-kit/connect-target.ts",
|
||||
"lib/jobs/offhost-purge.ts",
|
||||
"lib/managed-profiles/lifecycle.ts",
|
||||
"lib/notifications/channel-state.ts",
|
||||
"lib/notifications/delivery-identity.ts",
|
||||
"lib/notifications/dispatch-localised.ts",
|
||||
"lib/notifications/dispatcher.ts",
|
||||
"lib/sharing/grants.ts"
|
||||
],
|
||||
[
|
||||
"lib/cycle/gate.ts",
|
||||
"lib/modules/gate.ts"
|
||||
],
|
||||
[
|
||||
"lib/insights/features.ts",
|
||||
"lib/insights/signals-of-day.ts"
|
||||
],
|
||||
[
|
||||
"lib/insights/mood-aggregates.ts",
|
||||
"lib/insights/mood-patterns.ts"
|
||||
],
|
||||
[
|
||||
"lib/jobs/boss-instance.ts",
|
||||
"lib/jobs/boss-ready.ts"
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$comment": "Modules under src/ whose every statement is an export … from. Maintained by src/__tests__/import-cycle-guard.test.ts.",
|
||||
"count": 16,
|
||||
"total": 16,
|
||||
"entries": {
|
||||
"components/data-list/index.ts": 1,
|
||||
"components/insights/derived/index.ts": 1,
|
||||
"components/insights/workout-detail/index.ts": 1,
|
||||
"lib/ai/coach/tools/index.ts": 1,
|
||||
"lib/analytics/compliance.ts": 1,
|
||||
"lib/cycle/index.ts": 1,
|
||||
"lib/doctor-report-data.ts": 1,
|
||||
"lib/fhir/resources/index.ts": 1,
|
||||
"lib/insights/derived/index.ts": 1,
|
||||
"lib/links/index.ts": 1,
|
||||
"lib/logging/index.ts": 1,
|
||||
"lib/mcp/index.ts": 1,
|
||||
"lib/openapi/routes/insights/index.ts": 1,
|
||||
"lib/openapi/routes/medications/index.ts": 1,
|
||||
"lib/record-settings/index.ts": 1,
|
||||
"lib/validations/medication/index.ts": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
{
|
||||
"$comment": "route tests that mock @/lib/db wholesale, per test file under src/. Maintained by src/__tests__/test-realism-ratchet.test.ts.",
|
||||
"count": 321,
|
||||
"total": 321,
|
||||
"entries": {
|
||||
"app/api/account/grants/__tests__/invite-access-level.test.ts": 1,
|
||||
"app/api/account/grants/__tests__/invite-recent-proof.test.ts": 1,
|
||||
"app/api/account/grants/__tests__/invite-scope-and-manage.test.ts": 1,
|
||||
"app/api/activity/__tests__/routes.test.ts": 1,
|
||||
"app/api/admin/ai-settings/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/audit-log/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/audit-log/actions/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/backups/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/backups/[id]/summary/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/import-apple-health-export/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/maintenance/measurements/__tests__/status.test.ts": 1,
|
||||
"app/api/admin/maintenance/pulse-day-mean/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/notifications/diagnostic/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/notifications/health/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/notifications/reminder-check/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/notifications/test/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/provider-health/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/settings/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/settings/assistant-flags/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/settings/module-availability/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/settings/web-push-vapid/generate/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/status/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/support-report/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/users/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/users/[id]/__tests__/route.test.ts": 1,
|
||||
"app/api/analytics/__tests__/route.test.ts": 1,
|
||||
"app/api/anamnesis/facts/__tests__/idempotency-conflict.test.ts": 1,
|
||||
"app/api/anamnesis/facts/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/claim/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/login/__tests__/constant-work.test.ts": 1,
|
||||
"app/api/auth/login/__tests__/native-token.test.ts": 1,
|
||||
"app/api/auth/login/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/login/__tests__/trusted-device.test.ts": 1,
|
||||
"app/api/auth/me/ai-usage/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/me/coach-prefs/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/me/devices/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/me/devices/[id]/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/me/diabetes/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/me/disable-coach/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/me/disable-coach/__tests__/unknown-key-strict.test.ts": 1,
|
||||
"app/api/auth/me/documents-auto-ai-read/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/me/glucose-unit/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/me/health-score-config/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/me/mfa/disable/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/me/mfa/webauthn/register/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/me/modules/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/me/notification-prefs/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/me/security-activity/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/me/sessions/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/me/source-priority/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/me/timezone/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/me/unit-preference/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/me/workout-routes/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/mfa/webauthn/verify/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/native/complete/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/native/login/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/native/token/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/oidc/callback/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/oidc/native/token/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/passkey/login-verify/__tests__/native-token.test.ts": 1,
|
||||
"app/api/auth/passkey/login-verify/__tests__/oidc-only.test.ts": 1,
|
||||
"app/api/auth/passkey/login-verify/__tests__/refusals.test.ts": 1,
|
||||
"app/api/auth/passkeys/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/passkeys/[id]/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/password/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/register/__tests__/route.test.ts": 1,
|
||||
"app/api/biomarkers/__tests__/error-contract.test.ts": 1,
|
||||
"app/api/biomarkers/[id]/__tests__/delete-record-auth.test.ts": 1,
|
||||
"app/api/biomarkers/[id]/__tests__/route.test.ts": 1,
|
||||
"app/api/c/[token]/unlock/__tests__/route.test.ts": 1,
|
||||
"app/api/coach/about-me/__tests__/route-concurrency.test.ts": 1,
|
||||
"app/api/coach/about-me/__tests__/route-module-gate.test.ts": 1,
|
||||
"app/api/coach/about-me/adopt/__tests__/route.test.ts": 1,
|
||||
"app/api/coach/plans/__tests__/route.test.ts": 1,
|
||||
"app/api/coach/reminders/__tests__/route.test.ts": 1,
|
||||
"app/api/coach/suggested-actions/__tests__/route.test.ts": 1,
|
||||
"app/api/consent/ai/__tests__/route.test.ts": 1,
|
||||
"app/api/consent/ai/latest/__tests__/route.test.ts": 1,
|
||||
"app/api/custom-metrics/__tests__/route.test.ts": 1,
|
||||
"app/api/custom-metrics/[id]/__tests__/route.test.ts": 1,
|
||||
"app/api/custom-metrics/[id]/entries/__tests__/note-encryption.test.ts": 1,
|
||||
"app/api/custom-metrics/[id]/entries/__tests__/route.test.ts": 1,
|
||||
"app/api/custom-metrics/[id]/entries/__tests__/soft-delete-restore.test.ts": 1,
|
||||
"app/api/cycle/__tests__/external-id-floor.test.ts": 1,
|
||||
"app/api/cycle/calendar/__tests__/day-entries.test.ts": 1,
|
||||
"app/api/cycle/calendar/__tests__/verdict-round-trip.test.ts": 1,
|
||||
"app/api/cycle/day-logs/bulk/__tests__/route.test.ts": 1,
|
||||
"app/api/cycle/insights/__tests__/route-locale.test.ts": 1,
|
||||
"app/api/cycle/symptoms/custom/__tests__/custom-crud.test.ts": 1,
|
||||
"app/api/daily/digest/__tests__/route.test.ts": 1,
|
||||
"app/api/daily/digest/dismiss/__tests__/route.test.ts": 1,
|
||||
"app/api/dashboard/chart-overlay-prefs/__tests__/route.test.ts": 1,
|
||||
"app/api/dashboard/snapshot/__tests__/route.test.ts": 1,
|
||||
"app/api/dashboard/summary/__tests__/module-gate.test.ts": 1,
|
||||
"app/api/dashboard/summary/__tests__/route.test.ts": 1,
|
||||
"app/api/dashboard/widgets/__tests__/route.test.ts": 1,
|
||||
"app/api/dawarich/places/__tests__/route.test.ts": 1,
|
||||
"app/api/devices/__tests__/route.test.ts": 1,
|
||||
"app/api/devices/[id]/__tests__/route.test.ts": 1,
|
||||
"app/api/documents/inbound/__tests__/route.test.ts": 1,
|
||||
"app/api/documents/inbound/[id]/__tests__/route.test.ts": 1,
|
||||
"app/api/documents/inbound/[id]/chat/__tests__/route.test.ts": 1,
|
||||
"app/api/documents/inbound/[id]/extract/__tests__/route.test.ts": 1,
|
||||
"app/api/documents/inbound/[id]/index/__tests__/route.test.ts": 1,
|
||||
"app/api/documents/inbound/[id]/original/__tests__/route.test.ts": 1,
|
||||
"app/api/documents/inbound/[id]/suggest/__tests__/route.test.ts": 1,
|
||||
"app/api/documents/inbound/[id]/summary/__tests__/route.test.ts": 1,
|
||||
"app/api/documents/inbound/[id]/thumbnail/__tests__/route.test.ts": 1,
|
||||
"app/api/documents/inbound/layout/__tests__/route.test.ts": 1,
|
||||
"app/api/documents/inbound/reindex/__tests__/route.test.ts": 1,
|
||||
"app/api/documents/inbound/usage/__tests__/route.test.ts": 1,
|
||||
"app/api/encounters/procedures/__tests__/route.test.ts": 1,
|
||||
"app/api/environment/backfill/__tests__/route.test.ts": 1,
|
||||
"app/api/export/__tests__/measurement-streaming.test.ts": 1,
|
||||
"app/api/export/__tests__/per-type-routes.test.ts": 1,
|
||||
"app/api/export/__tests__/soft-delete-filter.test.ts": 1,
|
||||
"app/api/export/encrypted/__tests__/route.test.ts": 1,
|
||||
"app/api/export/health-record/__tests__/route.test.ts": 1,
|
||||
"app/api/feature-flags/__tests__/route.test.ts": 1,
|
||||
"app/api/fhir/__tests__/capability-and-everything.test.ts": 1,
|
||||
"app/api/fhir/__tests__/module-gate.test.ts": 1,
|
||||
"app/api/fitbit/connect/__tests__/route.test.ts": 1,
|
||||
"app/api/fitbit/credentials/__tests__/route.test.ts": 1,
|
||||
"app/api/gamification/achievements/__tests__/hidden-redaction.test.ts": 1,
|
||||
"app/api/gamification/achievements/__tests__/ios-format.test.ts": 1,
|
||||
"app/api/gamification/achievements/__tests__/module-gate.test.ts": 1,
|
||||
"app/api/gamification/achievements/__tests__/writes-nothing.test.ts": 1,
|
||||
"app/api/google-health/credentials/__tests__/route.test.ts": 1,
|
||||
"app/api/health/__tests__/route.test.ts": 1,
|
||||
"app/api/illness/episodes/[id]/__tests__/body-site.test.ts": 1,
|
||||
"app/api/illness/episodes/[id]/day-logs/__tests__/route.test.ts": 1,
|
||||
"app/api/illness/episodes/[id]/restore/__tests__/route.test.ts": 1,
|
||||
"app/api/import/__tests__/route.test.ts": 1,
|
||||
"app/api/import/apple-health-export/__tests__/route.test.ts": 1,
|
||||
"app/api/import/apple-health-export/[jobId]/status/__tests__/route.test.ts": 1,
|
||||
"app/api/import/csv/__tests__/route.test.ts": 1,
|
||||
"app/api/import/health-connect-export/__tests__/route.test.ts": 1,
|
||||
"app/api/ingest/medication/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/biomarker-assessment/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/blood-pressure-status/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/cards/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/golden-transcripts.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-activity.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-cancelled-turn.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-capability-gate.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-fence-guard.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-followups.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-guard-activation.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-guard-ledger.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-guard.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-list-query.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-snapshot-once.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-tool-mode.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-workout-scope.test.ts": 1,
|
||||
"app/api/insights/chat/[id]/attachments/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/chat/[id]/messages/[messageId]/trail/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/chat/fenced/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/coach-read/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/coach/facts/__tests__/route-memory.test.ts": 1,
|
||||
"app/api/insights/coach/facts/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/coach/nudge-status/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/comprehensive/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/correlations/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/derived/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/derived/batch/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/ecg/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/ecg/[id]/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/generate/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/glp1-timeline/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/layout/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/metric-status/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/mood-status/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/patterns/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/patterns/[id]/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/pregenerate/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/provider-chain/__tests__/put.test.ts": 1,
|
||||
"app/api/insights/provider-chain/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/rhythm-events/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/targets/__tests__/route.test.ts": 1,
|
||||
"app/api/integrations/fitbit/test/__tests__/route.test.ts": 1,
|
||||
"app/api/integrations/healthkit/__tests__/route.test.ts": 1,
|
||||
"app/api/integrations/status/__tests__/route.test.ts": 1,
|
||||
"app/api/integrations/whoop/test/__tests__/route.test.ts": 1,
|
||||
"app/api/integrations/withings/test/__tests__/route.test.ts": 1,
|
||||
"app/api/internal/deploy-webhook/__tests__/route.test.ts": 1,
|
||||
"app/api/labs/__tests__/error-contract.test.ts": 1,
|
||||
"app/api/labs/ocr/commit/__tests__/route.test.ts": 1,
|
||||
"app/api/labs/ocr/extract/__tests__/route.test.ts": 1,
|
||||
"app/api/labs/restore/__tests__/route.test.ts": 1,
|
||||
"app/api/mcp/oauth/__tests__/authorize.test.ts": 1,
|
||||
"app/api/mcp/oauth/__tests__/token.test.ts": 1,
|
||||
"app/api/mcp/tokens/__tests__/route.test.ts": 1,
|
||||
"app/api/measurement-reminders/[id]/__tests__/route.test.ts": 1,
|
||||
"app/api/measurement-reminders/[id]/__tests__/skip-snooze-history.test.ts": 1,
|
||||
"app/api/measurement-reminders/[id]/complete/__tests__/route.test.ts": 1,
|
||||
"app/api/measurement-reminders/[id]/satisfy/__tests__/route.test.ts": 1,
|
||||
"app/api/measurements/__tests__/arrival-wiring.test.ts": 1,
|
||||
"app/api/measurements/__tests__/group-by-day.test.ts": 1,
|
||||
"app/api/measurements/__tests__/put-duplicate-timestamp.test.ts": 1,
|
||||
"app/api/measurements/__tests__/range-aggregation-route.test.ts": 1,
|
||||
"app/api/measurements/__tests__/zod-multi-issue.test.ts": 1,
|
||||
"app/api/measurements/[id]/__tests__/route.test.ts": 1,
|
||||
"app/api/measurements/batch/__tests__/aggregated-hr-wire.test.ts": 1,
|
||||
"app/api/measurements/batch/__tests__/duplicate-prefilter.test.ts": 1,
|
||||
"app/api/measurements/batch/__tests__/hr-bucket-observability.test.ts": 1,
|
||||
"app/api/measurements/batch/__tests__/insert-returning-arrivals.test.ts": 1,
|
||||
"app/api/measurements/batch/__tests__/issue-paths.test.ts": 1,
|
||||
"app/api/measurements/batch/__tests__/pr-detection-hook.test.ts": 1,
|
||||
"app/api/measurements/batch/__tests__/refusal-detail.test.ts": 1,
|
||||
"app/api/measurements/batch/__tests__/sync-trigger.test.ts": 1,
|
||||
"app/api/measurements/batch/__tests__/timestamp-plausibility.test.ts": 1,
|
||||
"app/api/measurements/batch/__tests__/unstable-external-id.test.ts": 1,
|
||||
"app/api/measurements/bulk-delete/__tests__/route.test.ts": 1,
|
||||
"app/api/measurements/by-external-ids/__tests__/route.test.ts": 1,
|
||||
"app/api/measurements/restore/__tests__/route.test.ts": 1,
|
||||
"app/api/measurements/series/__tests__/route.test.ts": 1,
|
||||
"app/api/measurements/series/__tests__/unknown-query-param-open.test.ts": 1,
|
||||
"app/api/medications/__tests__/route.test.ts": 1,
|
||||
"app/api/medications/[id]/__tests__/route.test.ts": 1,
|
||||
"app/api/medications/[id]/cadence/__tests__/route.test.ts": 1,
|
||||
"app/api/medications/[id]/compliance/__tests__/route.test.ts": 1,
|
||||
"app/api/medications/[id]/dose-history/__tests__/route.test.ts": 1,
|
||||
"app/api/medications/[id]/glp1/__tests__/route.test.ts": 1,
|
||||
"app/api/medications/[id]/intake/__tests__/route.test.ts": 1,
|
||||
"app/api/medications/[id]/intake/__tests__/source-provenance.test.ts": 1,
|
||||
"app/api/medications/[id]/intake/[eventId]/__tests__/route.test.ts": 1,
|
||||
"app/api/medications/[id]/intake/bulk-delete/__tests__/route.test.ts": 1,
|
||||
"app/api/medications/[id]/intake/import/__tests__/route.test.ts": 1,
|
||||
"app/api/medications/[id]/intake/import/[jobId]/status/__tests__/route.test.ts": 1,
|
||||
"app/api/medications/[id]/intake/purge/__tests__/route.test.ts": 1,
|
||||
"app/api/medications/[id]/inventory/__tests__/route.test.ts": 1,
|
||||
"app/api/medications/[id]/inventory/[itemId]/__tests__/route.test.ts": 1,
|
||||
"app/api/medications/[id]/phase-config/__tests__/route.test.ts": 1,
|
||||
"app/api/medications/[id]/schedule-revisions/__tests__/route.test.ts": 1,
|
||||
"app/api/medications/[id]/side-effects/__tests__/route.test.ts": 1,
|
||||
"app/api/medications/compliance/__tests__/route.test.ts": 1,
|
||||
"app/api/medications/intake/__tests__/route.test.ts": 1,
|
||||
"app/api/medications/intake/bulk/__tests__/dose-arrival-scope.test.ts": 1,
|
||||
"app/api/medications/intake/bulk/__tests__/route.test.ts": 1,
|
||||
"app/api/medications/intake/dose-history-import/__tests__/route.test.ts": 1,
|
||||
"app/api/medications/layout/__tests__/route.test.ts": 1,
|
||||
"app/api/mental-health/assessments/__tests__/route.test.ts": 1,
|
||||
"app/api/mental-health/assessments/[id]/__tests__/route.test.ts": 1,
|
||||
"app/api/meta/capabilities/__tests__/route.test.ts": 1,
|
||||
"app/api/monitoring/glitchtip/__tests__/route.test.ts": 1,
|
||||
"app/api/mood-entries/__tests__/route.test.ts": 1,
|
||||
"app/api/mood-entries/[id]/__tests__/route.test.ts": 1,
|
||||
"app/api/mood-entries/bulk-delete/__tests__/route.test.ts": 1,
|
||||
"app/api/mood-entries/bulk/__tests__/route.test.ts": 1,
|
||||
"app/api/mood-entries/restore/__tests__/route.test.ts": 1,
|
||||
"app/api/mood/analytics/__tests__/route.test.ts": 1,
|
||||
"app/api/mood/tags/__tests__/custom-crud.test.ts": 1,
|
||||
"app/api/mood/tags/__tests__/groups-crud.test.ts": 1,
|
||||
"app/api/mood/tags/__tests__/layout.test.ts": 1,
|
||||
"app/api/mood/tags/__tests__/route.test.ts": 1,
|
||||
"app/api/mood/tags/layout/__tests__/route.test.ts": 1,
|
||||
"app/api/nightscout/connect/__tests__/route.test.ts": 1,
|
||||
"app/api/notifications/action/[token]/__tests__/route.test.ts": 1,
|
||||
"app/api/notifications/status/__tests__/route.test.ts": 1,
|
||||
"app/api/notifications/web-push/__tests__/route.test.ts": 1,
|
||||
"app/api/notifications/web-push/test/__tests__/route.test.ts": 1,
|
||||
"app/api/nutrients/__tests__/route.test.ts": 1,
|
||||
"app/api/nutrients/batch/__tests__/route.test.ts": 1,
|
||||
"app/api/nutrients/daily/__tests__/route.test.ts": 1,
|
||||
"app/api/nutrients/water/__tests__/route.test.ts": 1,
|
||||
"app/api/onboarding/__tests__/error-contract.test.ts": 1,
|
||||
"app/api/onboarding/complete/__tests__/route.test.ts": 1,
|
||||
"app/api/oura/callback/__tests__/route.test.ts": 1,
|
||||
"app/api/oura/credentials/__tests__/route.test.ts": 1,
|
||||
"app/api/oura/disconnect/__tests__/route.test.ts": 1,
|
||||
"app/api/oura/status/__tests__/route.test.ts": 1,
|
||||
"app/api/personal-records/__tests__/route.test.ts": 1,
|
||||
"app/api/polar/callback/__tests__/route.test.ts": 1,
|
||||
"app/api/polar/credentials/__tests__/route.test.ts": 1,
|
||||
"app/api/polar/disconnect/__tests__/route.test.ts": 1,
|
||||
"app/api/polar/status/__tests__/route.test.ts": 1,
|
||||
"app/api/search/__tests__/route.test.ts": 1,
|
||||
"app/api/settings/__tests__/channel-enabled-routes.test.ts": 1,
|
||||
"app/api/settings/__tests__/channel-global-switch-refusal.test.ts": 1,
|
||||
"app/api/settings/__tests__/email-test-route.test.ts": 1,
|
||||
"app/api/settings/__tests__/private-origin-routes.test.ts": 1,
|
||||
"app/api/settings/__tests__/telegram-test-locale.test.ts": 1,
|
||||
"app/api/settings/__tests__/webhook-format-route.test.ts": 1,
|
||||
"app/api/settings/reminder-thresholds/__tests__/route.test.ts": 1,
|
||||
"app/api/settings/webhook/test/__tests__/route.test.ts": 1,
|
||||
"app/api/share-links/__tests__/route.test.ts": 1,
|
||||
"app/api/sleep/night/__tests__/route.test.ts": 1,
|
||||
"app/api/sleep/rhythm/__tests__/route.test.ts": 1,
|
||||
"app/api/strava/callback/__tests__/route.test.ts": 1,
|
||||
"app/api/strava/credentials/__tests__/route.test.ts": 1,
|
||||
"app/api/strava/disconnect/__tests__/route.test.ts": 1,
|
||||
"app/api/sync/changes/__tests__/module-gate.test.ts": 1,
|
||||
"app/api/sync/state/__tests__/route.test.ts": 1,
|
||||
"app/api/telegram/webhook/__tests__/route.test.ts": 1,
|
||||
"app/api/tokens/__tests__/route.test.ts": 1,
|
||||
"app/api/tokens/documents/__tests__/route.test.ts": 1,
|
||||
"app/api/tokens/measurements/__tests__/route.test.ts": 1,
|
||||
"app/api/tokens/workouts/__tests__/route.test.ts": 1,
|
||||
"app/api/user/ai-provider/__tests__/legacy-private-host.test.ts": 1,
|
||||
"app/api/user/ai-provider/__tests__/route.test.ts": 1,
|
||||
"app/api/user/avatar/__tests__/route.test.ts": 1,
|
||||
"app/api/user/avatar/[id]/__tests__/route.test.ts": 1,
|
||||
"app/api/user/profile/__tests__/route.test.ts": 1,
|
||||
"app/api/user/thresholds/__tests__/route.test.ts": 1,
|
||||
"app/api/whoop/callback/__tests__/route.test.ts": 1,
|
||||
"app/api/whoop/connect/__tests__/route.test.ts": 1,
|
||||
"app/api/whoop/credentials/__tests__/route.test.ts": 1,
|
||||
"app/api/whoop/disconnect/__tests__/route.test.ts": 1,
|
||||
"app/api/whoop/status/__tests__/route.test.ts": 1,
|
||||
"app/api/whoop/webhook/[token]/__tests__/route.test.ts": 1,
|
||||
"app/api/withings/callback/__tests__/route.test.ts": 1,
|
||||
"app/api/withings/connect/__tests__/route.test.ts": 1,
|
||||
"app/api/withings/credentials/__tests__/route.test.ts": 1,
|
||||
"app/api/withings/webhook/__tests__/route.test.ts": 1,
|
||||
"app/api/withings/webhook/[token]/__tests__/route.test.ts": 1,
|
||||
"app/api/workouts/__tests__/batch-create.test.ts": 1,
|
||||
"app/api/workouts/__tests__/batch-scope.test.ts": 1,
|
||||
"app/api/workouts/__tests__/canonical-dedup.test.ts": 1,
|
||||
"app/api/workouts/__tests__/delete.test.ts": 1,
|
||||
"app/api/workouts/__tests__/detail.test.ts": 1,
|
||||
"app/api/workouts/batch/__tests__/unstable-external-id.test.ts": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
{
|
||||
"$comment": "route tests that stub @/lib/api-handler, per test file under src/. Maintained by src/__tests__/test-realism-ratchet.test.ts.",
|
||||
"count": 138,
|
||||
"total": 138,
|
||||
"entries": {
|
||||
"app/api/account/grants/__tests__/invite-access-level.test.ts": 1,
|
||||
"app/api/account/grants/__tests__/invite-scope-and-manage.test.ts": 1,
|
||||
"app/api/activity/__tests__/routes.test.ts": 1,
|
||||
"app/api/admin/ai-settings/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/app-logs/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/audit-log/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/audit-log/actions/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/backups/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/backups/[id]/summary/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/notifications/diagnostic/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/notifications/health/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/notifications/reminder-check/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/notifications/test/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/provider-health/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/settings/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/settings/web-push-vapid/generate/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/status/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/support-report/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/users/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/users/[id]/__tests__/route.test.ts": 1,
|
||||
"app/api/ai/test/__tests__/route.test.ts": 1,
|
||||
"app/api/anamnesis/facts/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/me/mfa/disable/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/me/mfa/webauthn/register/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/mfa/webauthn/verify/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/native/complete/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/native/login/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/native/token/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/oidc/callback/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/oidc/login/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/oidc/native/token/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/passkeys/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/passkeys/[id]/__tests__/route.test.ts": 1,
|
||||
"app/api/auth/password/__tests__/route.test.ts": 1,
|
||||
"app/api/biomarkers/[id]/__tests__/delete-record-auth.test.ts": 1,
|
||||
"app/api/coach/about-me/__tests__/route-concurrency.test.ts": 1,
|
||||
"app/api/coach/about-me/__tests__/route-module-gate.test.ts": 1,
|
||||
"app/api/coach/about-me/adopt/__tests__/route.test.ts": 1,
|
||||
"app/api/coach/about-me/questions/__tests__/route-module-gate.test.ts": 1,
|
||||
"app/api/dawarich/places/__tests__/route.test.ts": 1,
|
||||
"app/api/documents/inbound/usage/__tests__/route.test.ts": 1,
|
||||
"app/api/encounters/procedures/__tests__/route.test.ts": 1,
|
||||
"app/api/environment/backfill/__tests__/route.test.ts": 1,
|
||||
"app/api/fitbit/connect/__tests__/route.test.ts": 1,
|
||||
"app/api/fitbit/credentials/__tests__/route.test.ts": 1,
|
||||
"app/api/fitbit/sync/__tests__/route.test.ts": 1,
|
||||
"app/api/google-health/credentials/__tests__/route.test.ts": 1,
|
||||
"app/api/google-health/sync/__tests__/route.test.ts": 1,
|
||||
"app/api/google-health/sync/status/__tests__/route.test.ts": 1,
|
||||
"app/api/health/__tests__/route.test.ts": 1,
|
||||
"app/api/import/__tests__/record-csv-routes.test.ts": 1,
|
||||
"app/api/import/__tests__/route.test.ts": 1,
|
||||
"app/api/import/csv/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/golden-transcripts.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-activity.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-cancelled-turn.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-capability-gate.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-fence-guard.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-followups.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-guard-activation.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-guard-ledger.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-guard.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-list-query.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-snapshot-once.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-tool-mode.test.ts": 1,
|
||||
"app/api/insights/chat/__tests__/route-workout-scope.test.ts": 1,
|
||||
"app/api/insights/chat/[id]/__tests__/route-patch.test.ts": 1,
|
||||
"app/api/insights/chat/[id]/messages/[messageId]/results/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/chat/[id]/messages/[messageId]/trail/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/coach/nudge-status/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/generate/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/glp1-timeline/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/provider-chain/__tests__/put.test.ts": 1,
|
||||
"app/api/insights/provider-chain/__tests__/route.test.ts": 1,
|
||||
"app/api/integrations/fitbit/test/__tests__/route.test.ts": 1,
|
||||
"app/api/integrations/status/__tests__/route.test.ts": 1,
|
||||
"app/api/integrations/whoop/test/__tests__/route.test.ts": 1,
|
||||
"app/api/integrations/withings/resume/__tests__/route.test.ts": 1,
|
||||
"app/api/integrations/withings/test/__tests__/route.test.ts": 1,
|
||||
"app/api/internal/deploy-webhook/__tests__/route.test.ts": 1,
|
||||
"app/api/labs/ocr/extract/__tests__/route.test.ts": 1,
|
||||
"app/api/measurement-reminders/[id]/__tests__/route.test.ts": 1,
|
||||
"app/api/measurement-reminders/[id]/__tests__/skip-snooze-history.test.ts": 1,
|
||||
"app/api/measurement-reminders/[id]/complete/__tests__/route.test.ts": 1,
|
||||
"app/api/measurement-reminders/[id]/satisfy/__tests__/route.test.ts": 1,
|
||||
"app/api/measurements/__tests__/arrival-wiring.test.ts": 1,
|
||||
"app/api/medications/extract/__tests__/route.test.ts": 1,
|
||||
"app/api/medications/intake/bulk/__tests__/dose-arrival-scope.test.ts": 1,
|
||||
"app/api/monitoring/glitchtip/__tests__/route.test.ts": 1,
|
||||
"app/api/monitoring/umami-script/__tests__/route.test.ts": 1,
|
||||
"app/api/monitoring/umami/[...path]/__tests__/route.test.ts": 1,
|
||||
"app/api/nightscout/connect/__tests__/route.test.ts": 1,
|
||||
"app/api/nightscout/sync/__tests__/route.test.ts": 1,
|
||||
"app/api/notifications/web-push/__tests__/route.test.ts": 1,
|
||||
"app/api/notifications/web-push/test/__tests__/route.test.ts": 1,
|
||||
"app/api/oura/callback/__tests__/route.test.ts": 1,
|
||||
"app/api/oura/connect/__tests__/route.test.ts": 1,
|
||||
"app/api/oura/credentials/__tests__/route.test.ts": 1,
|
||||
"app/api/oura/disconnect/__tests__/route.test.ts": 1,
|
||||
"app/api/oura/status/__tests__/route.test.ts": 1,
|
||||
"app/api/oura/sync/__tests__/route.test.ts": 1,
|
||||
"app/api/polar/callback/__tests__/route.test.ts": 1,
|
||||
"app/api/polar/connect/__tests__/route.test.ts": 1,
|
||||
"app/api/polar/credentials/__tests__/route.test.ts": 1,
|
||||
"app/api/polar/disconnect/__tests__/route.test.ts": 1,
|
||||
"app/api/polar/status/__tests__/route.test.ts": 1,
|
||||
"app/api/polar/sync/__tests__/route.test.ts": 1,
|
||||
"app/api/record-settings/integrations/[provider]/__tests__/route.test.ts": 1,
|
||||
"app/api/search/__tests__/route.test.ts": 1,
|
||||
"app/api/settings/__tests__/telegram-test-locale.test.ts": 1,
|
||||
"app/api/strava/callback/__tests__/route.test.ts": 1,
|
||||
"app/api/strava/connect/__tests__/route.test.ts": 1,
|
||||
"app/api/strava/credentials/__tests__/route.test.ts": 1,
|
||||
"app/api/strava/disconnect/__tests__/route.test.ts": 1,
|
||||
"app/api/strava/sync/__tests__/route.test.ts": 1,
|
||||
"app/api/sync/state/__tests__/route.test.ts": 1,
|
||||
"app/api/telegram/webhook/__tests__/route.test.ts": 1,
|
||||
"app/api/tokens/__tests__/route.test.ts": 1,
|
||||
"app/api/tokens/documents/__tests__/route.test.ts": 1,
|
||||
"app/api/tokens/measurements/__tests__/route.test.ts": 1,
|
||||
"app/api/tokens/workouts/__tests__/route.test.ts": 1,
|
||||
"app/api/user/ai-provider/__tests__/legacy-private-host.test.ts": 1,
|
||||
"app/api/user/ai-provider/__tests__/route.test.ts": 1,
|
||||
"app/api/version/__tests__/route.test.ts": 1,
|
||||
"app/api/version/check-updates/__tests__/route.test.ts": 1,
|
||||
"app/api/whoop/callback/__tests__/route.test.ts": 1,
|
||||
"app/api/whoop/connect/__tests__/route.test.ts": 1,
|
||||
"app/api/whoop/connect/ticket/__tests__/route.test.ts": 1,
|
||||
"app/api/whoop/credentials/__tests__/route.test.ts": 1,
|
||||
"app/api/whoop/disconnect/__tests__/route.test.ts": 1,
|
||||
"app/api/whoop/status/__tests__/route.test.ts": 1,
|
||||
"app/api/whoop/sync/__tests__/route.test.ts": 1,
|
||||
"app/api/whoop/webhook/[token]/__tests__/route.test.ts": 1,
|
||||
"app/api/withings/callback/__tests__/route.test.ts": 1,
|
||||
"app/api/withings/connect/__tests__/route.test.ts": 1,
|
||||
"app/api/withings/sync/__tests__/route.test.ts": 1,
|
||||
"app/api/withings/webhook/__tests__/route.test.ts": 1,
|
||||
"app/api/withings/webhook/[token]/__tests__/route.test.ts": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"$comment": "Production files over 800 lines and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
|
||||
"count": 129,
|
||||
"total": 156466,
|
||||
"entries": {
|
||||
"app/api/dashboard/summary/route.ts": 1032,
|
||||
"app/api/documents/inbound/route.ts": 811,
|
||||
"app/api/insights/generate/route.ts": 1209,
|
||||
"app/api/measurements/batch/route.ts": 1576,
|
||||
"app/api/measurements/route.ts": 938,
|
||||
"app/api/measurements/series/route.ts": 879,
|
||||
"app/api/medications/[id]/route.ts": 1281,
|
||||
"app/api/medications/intake/bulk/route.ts": 1044,
|
||||
"app/api/workouts/batch/route.ts": 971,
|
||||
"app/medications/page-client.tsx": 839,
|
||||
"app/page-client.tsx": 2326,
|
||||
"components/admin/backups-section.tsx": 1462,
|
||||
"components/charts/health-chart.tsx": 2517,
|
||||
"components/charts/mood-chart.tsx": 1166,
|
||||
"components/cycle/log-day-sheet.tsx": 1639,
|
||||
"components/documents/document-detail-sheet.tsx": 1305,
|
||||
"components/documents/documents-view.tsx": 1212,
|
||||
"components/insights/coach-panel/chat-bubble.tsx": 888,
|
||||
"components/insights/coach-panel/coach-conversation.tsx": 1286,
|
||||
"components/insights/coach-panel/use-coach.ts": 1486,
|
||||
"components/insights/derived/vitals-dashboard.tsx": 1033,
|
||||
"components/insights/health-score-card.tsx": 1023,
|
||||
"components/insights/insights-tab-strip.tsx": 1086,
|
||||
"components/layout/sidebar-nav.tsx": 858,
|
||||
"components/measurement-reminders/vorsorge-section.tsx": 1536,
|
||||
"components/measurements/measurement-form.tsx": 815,
|
||||
"components/measurements/measurement-list.tsx": 2019,
|
||||
"components/medications/detail/medication-detail-tabs.tsx": 1176,
|
||||
"components/medications/dose-history-ledger.tsx": 1052,
|
||||
"components/medications/medication-table.tsx": 853,
|
||||
"components/medications/wizard/wizard-payload.ts": 1414,
|
||||
"components/mood/mood-list.tsx": 1408,
|
||||
"components/records/life-circumstances-manager.tsx": 1080,
|
||||
"components/settings/dashboard-layout-section.tsx": 1033,
|
||||
"components/settings/managed-record-settings-section.tsx": 1164,
|
||||
"components/timeline/timeline-chart.tsx": 1174,
|
||||
"components/timeline/timeline-geometry.ts": 1395,
|
||||
"lib/ai/anthropic-client.ts": 804,
|
||||
"lib/ai/coach/about-me.ts": 1012,
|
||||
"lib/ai/coach/coach-prose-grounding.ts": 1148,
|
||||
"lib/ai/coach/persistence.ts": 1219,
|
||||
"lib/ai/coach/results/metric-table-tool.ts": 1102,
|
||||
"lib/ai/coach/snapshot.ts": 1362,
|
||||
"lib/ai/coach/stream-events.ts": 817,
|
||||
"lib/ai/coach/tools/executor.ts": 1523,
|
||||
"lib/ai/coach/turn/model.ts": 820,
|
||||
"lib/ai/coach/types.ts": 1011,
|
||||
"lib/ai/codex-client.ts": 1094,
|
||||
"lib/ai/prompts/insight-generator.ts": 1121,
|
||||
"lib/ai/provider.ts": 1644,
|
||||
"lib/ai/safety/outbound-screen.ts": 1463,
|
||||
"lib/analytics/score/reader.ts": 998,
|
||||
"lib/analytics/sleep-night.ts": 1345,
|
||||
"lib/analytics/summaries-slice.ts": 1314,
|
||||
"lib/api-handler.ts": 1978,
|
||||
"lib/auth/session.ts": 811,
|
||||
"lib/cache/server-cache.ts": 851,
|
||||
"lib/cycle/prediction.ts": 883,
|
||||
"lib/daily/digest.ts": 1317,
|
||||
"lib/daily/load-digest.ts": 1000,
|
||||
"lib/daily/today-overview.ts": 838,
|
||||
"lib/dashboard-layout.ts": 971,
|
||||
"lib/dashboard/snapshot.ts": 1549,
|
||||
"lib/day/records.ts": 1175,
|
||||
"lib/doctor-report/collect.ts": 803,
|
||||
"lib/export/backup-plan.ts": 1089,
|
||||
"lib/export/coach-backup.ts": 1070,
|
||||
"lib/export/full-backup-payload.ts": 1520,
|
||||
"lib/export/profile-backup.ts": 1019,
|
||||
"lib/export/records-backup.ts": 834,
|
||||
"lib/export/restore-backup.ts": 3208,
|
||||
"lib/fitbit/client.ts": 1529,
|
||||
"lib/fitbit/sync-core.ts": 968,
|
||||
"lib/gamification/achievements-result.ts": 1094,
|
||||
"lib/gamification/achievements.ts": 913,
|
||||
"lib/google-health/client.ts": 1875,
|
||||
"lib/google-health/mappers.ts": 1295,
|
||||
"lib/google-health/sync-core.ts": 1634,
|
||||
"lib/idempotency.ts": 879,
|
||||
"lib/illness/correlation.ts": 1059,
|
||||
"lib/import/health-connect/import.ts": 1689,
|
||||
"lib/import/unzip-export-xml.ts": 825,
|
||||
"lib/insights/comprehensive-aggregator.ts": 943,
|
||||
"lib/insights/comprehensive-generate.ts": 1669,
|
||||
"lib/insights/correlation-channel-series.ts": 1339,
|
||||
"lib/insights/correlation-discovery.ts": 1240,
|
||||
"lib/insights/correlations.ts": 1026,
|
||||
"lib/insights/features.ts": 2009,
|
||||
"lib/insights/mood-aggregates.ts": 1055,
|
||||
"lib/insights/mood-status.ts": 814,
|
||||
"lib/insights/status-cache.ts": 812,
|
||||
"lib/insights/strain-score.ts": 832,
|
||||
"lib/integrations/status.ts": 1445,
|
||||
"lib/jobs/apple-health-import-worker.ts": 863,
|
||||
"lib/jobs/backup-restore.ts": 888,
|
||||
"lib/jobs/coach-nudge.ts": 1089,
|
||||
"lib/jobs/insight-pregenerate.ts": 1266,
|
||||
"lib/jobs/medication-intake-import.ts": 822,
|
||||
"lib/jobs/offhost-backup.ts": 1168,
|
||||
"lib/jobs/reaction-line.ts": 824,
|
||||
"lib/jobs/reminder/medication-reminder-check.ts": 818,
|
||||
"lib/jobs/reminder/register-integration-sync.ts": 1051,
|
||||
"lib/jobs/reminder/register-maintenance.ts": 2197,
|
||||
"lib/jobs/reminder/register-rollup.ts": 1156,
|
||||
"lib/jobs/reminder/register-status.ts": 878,
|
||||
"lib/managed-profiles/handover.ts": 986,
|
||||
"lib/mcp/record-search.ts": 1467,
|
||||
"lib/mcp/rich-reads.ts": 1468,
|
||||
"lib/mcp/tools.ts": 2654,
|
||||
"lib/measurements/dense-intraday-retention.ts": 1057,
|
||||
"lib/measurements/import-apple-health-export.ts": 1729,
|
||||
"lib/medications/scheduling/cadence.ts": 978,
|
||||
"lib/medications/scheduling/recurrence.ts": 1108,
|
||||
"lib/medications/scheduling/slot-upsert.ts": 926,
|
||||
"lib/notifications/senders/apns.ts": 1100,
|
||||
"lib/oura/client.ts": 1360,
|
||||
"lib/oura/sync.ts": 917,
|
||||
"lib/polar/client.ts": 1010,
|
||||
"lib/rollups/measurement-rollups.ts": 1496,
|
||||
"lib/search/search-records.ts": 996,
|
||||
"lib/sharing/grants.ts": 1046,
|
||||
"lib/support/report.ts": 905,
|
||||
"lib/telegram-webhook-handlers.ts": 1384,
|
||||
"lib/validations/backup.ts": 2418,
|
||||
"lib/validations/inbound-documents.ts": 1130,
|
||||
"lib/whoop/client.ts": 917,
|
||||
"lib/withings/sync-sleep.ts": 903,
|
||||
"lib/withings/sync.ts": 978,
|
||||
"proxy.ts": 870
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
{
|
||||
"$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
|
||||
"count": 415,
|
||||
"total": 154643,
|
||||
"entries": {
|
||||
"app/achievements/page.tsx :: AchievementsPage": 206,
|
||||
"app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 314,
|
||||
"app/api/admin/notifications/reminder-check/route.ts :: apiHandler(arg)": 208,
|
||||
"app/api/admin/settings/route.ts :: apiHandler(arg) #2": 217,
|
||||
"app/api/analytics/route.ts :: buildAnalyticsResponse": 446,
|
||||
"app/api/auth/login/route.ts :: apiHandler(arg)": 262,
|
||||
"app/api/auth/me/route.ts :: apiHandler(arg)": 386,
|
||||
"app/api/auth/oidc/callback/route.ts :: apiHandler(arg)": 499,
|
||||
"app/api/auth/register/route.ts :: apiHandler(arg)": 275,
|
||||
"app/api/coach/about-me/route.ts :: apiHandler(arg) #2": 309,
|
||||
"app/api/cycle/calendar/route.ts :: apiHandler(arg)": 230,
|
||||
"app/api/cycle/day-logs/bulk/route.ts :: postBulk": 213,
|
||||
"app/api/cycle/insights/route.ts :: apiHandler(arg)": 226,
|
||||
"app/api/dashboard/summary/route.ts :: buildDashboardSummary": 606,
|
||||
"app/api/devices/route.ts :: apiHandler(arg)": 237,
|
||||
"app/api/documents/inbound/bulk/route.ts :: apiHandler(arg)": 212,
|
||||
"app/api/documents/inbound/route.ts :: processUpload": 309,
|
||||
"app/api/encounters/[id]/route.ts :: apiHandler(arg) #2": 275,
|
||||
"app/api/environment/route.ts :: apiHandler(arg)": 201,
|
||||
"app/api/export/health-record/route.ts :: apiHandler(arg)": 246,
|
||||
"app/api/import/csv/route.ts :: apiHandler(arg)": 407,
|
||||
"app/api/import/route.ts :: apiHandler(arg)": 364,
|
||||
"app/api/ingest/medication/route.ts :: apiHandler(arg)": 269,
|
||||
"app/api/insights/cards/route.ts :: apiHandler(arg)": 211,
|
||||
"app/api/insights/comprehensive/route.ts :: buildComprehensiveResponse": 505,
|
||||
"app/api/insights/generate/route.ts :: apiHandler(arg) #2": 862,
|
||||
"app/api/insights/generate/route.ts :: apiHandler(arg) > withFeatureCacheScope(arg)": 858,
|
||||
"app/api/integrations/status/route.ts :: apiHandler(arg)": 451,
|
||||
"app/api/labs/[id]/route.ts :: apiHandler(arg) #2": 244,
|
||||
"app/api/labs/ocr/commit/route.ts :: commitOcrRows": 290,
|
||||
"app/api/labs/route.ts :: postLabResult": 248,
|
||||
"app/api/mcp/oauth/token/route.ts :: POST": 250,
|
||||
"app/api/mcp/oauth/token/route.ts :: POST > withBackgroundEvent(arg)": 248,
|
||||
"app/api/measurement-reminders/[id]/route.ts :: apiHandler(arg) #2": 240,
|
||||
"app/api/measurements/[id]/route.ts :: apiHandler(arg) #2": 203,
|
||||
"app/api/measurements/batch/route.ts :: postBatch": 1277,
|
||||
"app/api/measurements/route.ts :: apiHandler(arg)": 286,
|
||||
"app/api/measurements/route.ts :: postMeasurement": 377,
|
||||
"app/api/measurements/series/route.ts :: apiHandler(arg)": 670,
|
||||
"app/api/medications/[id]/dose-history/route.ts :: apiHandler(arg)": 256,
|
||||
"app/api/medications/[id]/intake/[eventId]/route.ts :: apiHandler(arg)": 390,
|
||||
"app/api/medications/[id]/intake/route.ts :: postIntake": 607,
|
||||
"app/api/medications/[id]/inventory/[itemId]/route.ts :: apiHandler(arg)": 210,
|
||||
"app/api/medications/[id]/route.ts :: apiHandler(arg)": 214,
|
||||
"app/api/medications/[id]/route.ts :: apiHandler(arg) #2": 892,
|
||||
"app/api/medications/[id]/route.ts :: apiHandler(arg) > prisma.$transaction(arg)": 293,
|
||||
"app/api/medications/[id]/schedule-revisions/[revisionId]/route.ts :: apiHandler(arg)": 254,
|
||||
"app/api/medications/extract/route.ts :: handleExtract": 203,
|
||||
"app/api/medications/intake/bulk/route.ts :: postBulk": 846,
|
||||
"app/api/medications/intake/route.ts :: apiHandler(arg) #2": 432,
|
||||
"app/api/medications/route.ts :: postMedication": 403,
|
||||
"app/api/mental-health/assessments/route.ts :: postAssessment": 267,
|
||||
"app/api/mood-entries/[id]/route.ts :: apiHandler(arg) #2": 302,
|
||||
"app/api/mood-entries/bulk/route.ts :: postBulk": 479,
|
||||
"app/api/mood-entries/route.ts :: postMoodEntry": 329,
|
||||
"app/api/nutrients/batch/route.ts :: postBatch": 304,
|
||||
"app/api/onboarding/complete/route.ts :: completeNeedsFlow": 218,
|
||||
"app/api/record-settings/[family]/route.ts :: apiHandler(arg) #2": 262,
|
||||
"app/api/sync/changes/route.ts :: apiHandler(arg)": 427,
|
||||
"app/api/user/ai-provider/route.ts :: apiHandler(arg) #2": 252,
|
||||
"app/api/withings/callback/route.ts :: apiHandler(arg)": 247,
|
||||
"app/api/workouts/[id]/route.ts :: apiHandler(arg)": 313,
|
||||
"app/api/workouts/batch/route.ts :: postBatch": 722,
|
||||
"app/auth/login/page.tsx :: LoginPage": 576,
|
||||
"app/auth/register/page.tsx :: RegisterPage": 285,
|
||||
"app/coach/conversations/page.tsx :: CoachConversationsBody": 249,
|
||||
"app/coach/plans/page.tsx :: CoachPlansBody": 216,
|
||||
"app/insights/catalog/page.tsx :: MetricCatalogPage": 204,
|
||||
"app/insights/medications/page.tsx :: InsightsMedikamentePage": 265,
|
||||
"app/insights/page-client.tsx :: InsightsPageClient": 376,
|
||||
"app/labs/page.tsx :: LabsPage": 228,
|
||||
"app/medications/page-client.tsx :: MedicationsPageClient": 591,
|
||||
"app/notifications/page.tsx :: NotificationsPage": 327,
|
||||
"app/page-client.tsx :: DashboardPageClient": 2133,
|
||||
"app/page-client.tsx :: DashboardPageClient > (anonymous)": 1279,
|
||||
"components/activity/activity-calendar-card.tsx :: ActivityCalendarCard": 470,
|
||||
"components/admin/admin-shell.tsx :: AdminShell": 254,
|
||||
"components/admin/api-token-overview-section.tsx :: ApiTokenOverviewSection": 212,
|
||||
"components/admin/app-log-preview-section.tsx :: AppLogPreviewSection": 341,
|
||||
"components/admin/backups-section.tsx :: BackupsSection": 618,
|
||||
"components/admin/backups-section.tsx :: RestoreRowDialog": 226,
|
||||
"components/admin/central-codex-section.tsx :: CentralCodexSection": 241,
|
||||
"components/admin/encryption-section.tsx :: EncryptionSection": 303,
|
||||
"components/admin/general-settings-section.tsx :: GeneralSettingsSection": 220,
|
||||
"components/admin/glitchtip-section.tsx :: GlitchtipSection": 233,
|
||||
"components/admin/host-metrics-chart.tsx :: HostMetricsChart": 232,
|
||||
"components/admin/invite-tokens-section.tsx :: InviteTokensSection": 525,
|
||||
"components/admin/key-backup-card.tsx :: KeyBackupCard": 238,
|
||||
"components/admin/login-overview-section.tsx :: LoginOverviewSection": 520,
|
||||
"components/admin/pulse-day-mean-card.tsx :: PulseDayMeanCard": 305,
|
||||
"components/admin/reminders-section.tsx :: RemindersSection": 309,
|
||||
"components/admin/user-management-section.tsx :: UserManagementSection": 634,
|
||||
"components/admin/web-push-vapid-section.tsx :: WebPushVapidSection": 217,
|
||||
"components/auth/claim-profile.tsx :: ClaimProfile": 429,
|
||||
"components/auth/mfa-login-step.tsx :: MfaLoginStep": 217,
|
||||
"components/charts/compliance-heatmap.tsx :: ComplianceHeatmap": 325,
|
||||
"components/charts/health-chart.tsx :: HealthChart": 1929,
|
||||
"components/charts/medication-compliance-chart.tsx :: MedicationComplianceChart": 543,
|
||||
"components/charts/mood-chart.tsx :: MoodChart": 896,
|
||||
"components/charts/mood-heatmap.tsx :: MoodHeatmap": 296,
|
||||
"components/charts/trend-card.tsx :: TrendCard": 365,
|
||||
"components/clinician/clinician-view.tsx :: ClinicianView": 223,
|
||||
"components/command-palette/command-palette.tsx :: PaletteBody": 333,
|
||||
"components/custom-metrics/custom-metric-chart.tsx :: CustomMetricChart": 231,
|
||||
"components/custom-metrics/custom-metric-detail.tsx :: CustomMetricDetail": 277,
|
||||
"components/custom-metrics/custom-metric-form.tsx :: CustomMetricForm": 214,
|
||||
"components/custom-metrics/custom-metric-history-list.tsx :: CustomMetricHistoryList": 302,
|
||||
"components/cycle/bbt-chart.tsx :: BbtChart": 244,
|
||||
"components/cycle/cycle-calendar.tsx :: CycleCalendar": 303,
|
||||
"components/cycle/cycle-settings.tsx :: CycleSettings": 228,
|
||||
"components/cycle/cycle-view.tsx :: CycleView": 408,
|
||||
"components/cycle/log-day-sheet.tsx :: LogDaySheet": 753,
|
||||
"components/daily/today-hero.tsx :: TodayHero": 490,
|
||||
"components/dashboard/dashboard-header.tsx :: DashboardHeader": 215,
|
||||
"components/dashboard/medication-intake-quick-add.tsx :: MedicationIntakeQuickAdd": 384,
|
||||
"components/day/day-layer.tsx :: DayLayer": 375,
|
||||
"components/day/day-view.tsx :: DayView": 300,
|
||||
"components/documents/document-ai-section.tsx :: DocumentAiSection": 262,
|
||||
"components/documents/document-bulk-bar.tsx :: DocumentBulkBar": 204,
|
||||
"components/documents/document-card.tsx :: DocumentCard": 354,
|
||||
"components/documents/document-detail-sheet.tsx :: DocumentDetailSheet": 936,
|
||||
"components/documents/document-facts-review.tsx :: DocumentFactsSection": 252,
|
||||
"components/documents/document-facts-review.tsx :: FactRow": 262,
|
||||
"components/documents/document-filter-bar.tsx :: DocumentFilterBar": 348,
|
||||
"components/documents/document-record-links.tsx :: DocumentRecordLinks": 202,
|
||||
"components/documents/document-timeline.tsx :: DocumentTimeline": 314,
|
||||
"components/documents/documents-view.tsx :: DocumentsView": 964,
|
||||
"components/documents/sources/document-source-picker.tsx :: DocumentSourcePicker": 565,
|
||||
"components/illness/log-day-sheet.tsx :: LogDaySheet": 265,
|
||||
"components/illness/new-episode-sheet.tsx :: NewEpisodeSheet": 264,
|
||||
"components/insights/coach-panel/attachment-picker.tsx :: AttachmentPicker": 208,
|
||||
"components/insights/coach-panel/chat-bubble.tsx :: ChatBubbleImpl": 419,
|
||||
"components/insights/coach-panel/coach-conversation.tsx :: CoachConversation": 1012,
|
||||
"components/insights/coach-panel/coach-input.tsx :: CoachInput": 259,
|
||||
"components/insights/coach-panel/coach-model-picker.tsx :: CoachModelPicker": 212,
|
||||
"components/insights/coach-panel/conversations-panel.tsx :: ConversationsPanel": 317,
|
||||
"components/insights/coach-panel/message-thread.tsx :: MessageThread": 390,
|
||||
"components/insights/coach-panel/sources-rail.tsx :: SourcesRail": 203,
|
||||
"components/insights/coach-panel/use-coach.ts :: useSendCoachMessage": 436,
|
||||
"components/insights/coach-panel/use-coach.ts :: useSendCoachMessage > useCallback(arg) #2": 340,
|
||||
"components/insights/daily-briefing.tsx :: DailyBriefing": 224,
|
||||
"components/insights/derived/coach-read-strip.tsx :: CoachReadStrip": 202,
|
||||
"components/insights/derived/composite-score-anatomy.tsx :: CompositeScoreAnatomy": 251,
|
||||
"components/insights/derived/score-trend-chart.tsx :: ScoreTrendChart": 255,
|
||||
"components/insights/derived/vitals-dashboard.tsx :: VitalsDashboard": 239,
|
||||
"components/insights/derived/wellness-scores.tsx :: WellnessScores": 273,
|
||||
"components/insights/glucose/glucose-clinical-panel.tsx :: GlucoseClinicalPanel": 213,
|
||||
"components/insights/health-score-card.tsx :: HealthScoreCard": 561,
|
||||
"components/insights/healthkit-metric-page.tsx :: HealthKitMetricPage": 322,
|
||||
"components/insights/insight-status-card.tsx :: InsightStatusCard": 205,
|
||||
"components/insights/insights-edit-mode.tsx :: InsightsEditMode": 270,
|
||||
"components/insights/insights-tab-strip.tsx :: InsightsTabStripImpl": 410,
|
||||
"components/insights/intraday-pulse-chart.tsx :: IntradayPulseChart": 283,
|
||||
"components/insights/layout-coach-fab.tsx :: LayoutCoachFab": 230,
|
||||
"components/insights/metric-target-summary.tsx :: TargetReferencePanel": 223,
|
||||
"components/insights/mood/mood-dimension-trends.tsx :: MoodDimensionTrends": 227,
|
||||
"components/insights/sleep-stage-stacked-bar.tsx :: SleepStageStackedBar": 354,
|
||||
"components/insights/sub-page-shell.tsx :: SubPageShell": 266,
|
||||
"components/insights/trends-row.tsx :: TrendsRow": 215,
|
||||
"components/labs/biomarker-form.tsx :: BiomarkerForm": 244,
|
||||
"components/labs/biomarker-manager.tsx :: BiomarkerManager": 242,
|
||||
"components/labs/biomarker-merge-sheet.tsx :: BiomarkerMergeSheet": 250,
|
||||
"components/labs/lab-biomarker-chart.tsx :: LabBiomarkerChart": 370,
|
||||
"components/labs/lab-biomarker-detail.tsx :: LabBiomarkerDetail": 481,
|
||||
"components/labs/lab-form.tsx :: LabForm": 473,
|
||||
"components/labs/lab-history-list.tsx :: LabHistoryList": 534,
|
||||
"components/labs/lab-list.tsx :: LabList": 308,
|
||||
"components/labs/ocr-review-dialog.tsx :: OcrReviewDialog": 359,
|
||||
"components/labs/ocr-row-editor.tsx :: OcrRowEditor": 393,
|
||||
"components/layout/auth-shell.tsx :: AuthShell": 520,
|
||||
"components/layout/bottom-nav.tsx :: BottomNav": 260,
|
||||
"components/layout/capture-picker.tsx :: CapturePicker": 235,
|
||||
"components/layout/sidebar-nav.tsx :: SidebarNav": 372,
|
||||
"components/layout/top-bar.tsx :: TopBar": 212,
|
||||
"components/links/entity-link-picker.tsx :: EntityLinkPicker": 392,
|
||||
"components/measurement-reminders/vorsorge-dashboard-card.tsx :: VorsorgeDashboardCard": 215,
|
||||
"components/measurement-reminders/vorsorge-section.tsx :: VorsorgeCard": 635,
|
||||
"components/measurement-reminders/vorsorge-section.tsx :: VorsorgeSection": 618,
|
||||
"components/measurements/measurement-form.tsx :: MeasurementForm": 500,
|
||||
"components/measurements/measurement-list.tsx :: MeasurementList": 1586,
|
||||
"components/measurements/measurement-list.tsx :: MeasurementList > data.measurements.map(arg)": 223,
|
||||
"components/measurements/measurement-list.tsx :: MeasurementList > data.measurements.map(arg) #2": 229,
|
||||
"components/medications/detail/efficacy/efficacy-chart.tsx :: EfficacyChart": 361,
|
||||
"components/medications/detail/medication-detail-tabs.tsx :: MedicationDetailTabs": 787,
|
||||
"components/medications/dose-history-add-dialog.tsx :: LedgerAddDialog": 223,
|
||||
"components/medications/dose-history-ledger.tsx :: DoseHistoryLedger": 404,
|
||||
"components/medications/dose-history-ledger.tsx :: LedgerRowItem": 314,
|
||||
"components/medications/dose-strength-curve.tsx :: DoseStrengthCurve": 266,
|
||||
"components/medications/glp1-medication-card.tsx :: Glp1MedicationCard": 405,
|
||||
"components/medications/injection-site-picker.tsx :: InjectionSitePicker": 287,
|
||||
"components/medications/intake-history-editable.tsx :: IntakeHistoryEditable": 224,
|
||||
"components/medications/intake-history-list-v2.tsx :: IntakeHistoryListV2": 435,
|
||||
"components/medications/intake-import-dialog.tsx :: IntakeImportDialog": 208,
|
||||
"components/medications/log-intake-dialog.tsx :: LogIntakeDialog": 267,
|
||||
"components/medications/medication-card.tsx :: MedicationCard": 413,
|
||||
"components/medications/medication-table.tsx :: MedicationTableRowItem": 377,
|
||||
"components/medications/scheduling/natural-language-extractor.tsx :: NaturalLanguageExtractor": 214,
|
||||
"components/medications/scheduling/schedule-history-timeline.tsx :: EraDialog": 219,
|
||||
"components/medications/scheduling/schedule-history-timeline.tsx :: ScheduleHistoryTimeline": 256,
|
||||
"components/medications/sections/api-tokens-row.tsx :: ApiTokensRow": 296,
|
||||
"components/medications/sections/composition-section.tsx :: CompositionEditor": 362,
|
||||
"components/medications/sections/inventory-dialogs.tsx :: AddInventoryDialog": 238,
|
||||
"components/medications/sections/inventory-section.tsx :: InventorySection": 368,
|
||||
"components/medications/side-effects-section.tsx :: SideEffectsSection": 516,
|
||||
"components/medications/wizard/medication-wizard-dialog.tsx :: WizardDialogShell": 516,
|
||||
"components/medications/wizard/steps/step3-dose.tsx :: Step3Dose": 222,
|
||||
"components/medications/wizard/steps/step8-summary.tsx :: Step8Summary": 230,
|
||||
"components/mental-health/check-in-wizard.tsx :: CheckInWizard": 214,
|
||||
"components/mental-health/mental-wellbeing.tsx :: MentalWellbeing": 210,
|
||||
"components/mood/manage/tag-groups-card.tsx :: TagGroupsCard": 256,
|
||||
"components/mood/manage/tag-manager-card.tsx :: TagManagerCard": 270,
|
||||
"components/mood/mood-form.tsx :: MoodForm": 513,
|
||||
"components/mood/mood-list.tsx :: MoodList": 1109,
|
||||
"components/mood/mood-tag-picker.tsx :: MoodTagPicker": 257,
|
||||
"components/onboarding/confirm-screen.tsx :: ConfirmScreen": 255,
|
||||
"components/onboarding/done-screen.tsx :: DoneScreen": 408,
|
||||
"components/onboarding/getting-started-checklist.tsx :: GettingStartedChecklist": 425,
|
||||
"components/onboarding/tour.tsx :: OnboardingTour": 544,
|
||||
"components/practitioners/practitioner-list.tsx :: PractitionerList": 230,
|
||||
"components/provenance/source-provenance-view.tsx :: SourceProvenanceView": 363,
|
||||
"components/records/allergy-form.tsx :: AllergyForm": 225,
|
||||
"components/records/audit-c-panel.tsx :: AuditCPanel": 285,
|
||||
"components/records/life-circumstances-manager.tsx :: SectionBody": 482,
|
||||
"components/settings/about-section.tsx :: AboutSection": 236,
|
||||
"components/settings/access/grant-invite-card.tsx :: GrantInviteCard": 311,
|
||||
"components/settings/access/managed-profile-create-form.tsx :: ManagedProfileCreateForm": 235,
|
||||
"components/settings/access/managed-profile-edit-form.tsx :: ManagedProfileEditFields": 208,
|
||||
"components/settings/access/managed-profile-guardians.tsx :: ManagedProfileGuardians": 234,
|
||||
"components/settings/account-section/index.tsx :: AccountSection": 659,
|
||||
"components/settings/ai/codex-provider-form.tsx :: CodexProviderForm": 281,
|
||||
"components/settings/ai/fallback-chain-card.tsx :: FallbackChainCard": 228,
|
||||
"components/settings/ai/runtime-actions-row.tsx :: RuntimeActionsRow": 282,
|
||||
"components/settings/api-section.tsx :: ApiTokensCard": 366,
|
||||
"components/settings/coach-nudge-card.tsx :: CoachNudgeCard": 251,
|
||||
"components/settings/coach-prefs-section.tsx :: CoachPrefsSection": 350,
|
||||
"components/settings/dashboard-layout-section.tsx :: DashboardLayoutSection": 620,
|
||||
"components/settings/environment-section.tsx :: EnvironmentSection": 666,
|
||||
"components/settings/import-panel/apple-health-import-card.tsx :: AppleHealthImportCard": 298,
|
||||
"components/settings/import-panel/csv-import-card.tsx :: CsvImportCard": 212,
|
||||
"components/settings/import-panel/dose-history-import-card.tsx :: DoseHistoryImportCard": 225,
|
||||
"components/settings/import-panel/health-connect-import-card.tsx :: HealthConnectImportCard": 293,
|
||||
"components/settings/import-panel/json-import-card.tsx :: JsonImportCard": 223,
|
||||
"components/settings/import-panel/record-csv-import-card.tsx :: RecordCsvImportSheet": 460,
|
||||
"components/settings/insights-pill-order-section.tsx :: InsightsPillOrderSection": 255,
|
||||
"components/settings/integrations/connections-panel.tsx :: ConnectionsPanel": 236,
|
||||
"components/settings/integrations/document-sources-card.tsx :: SourceForm": 306,
|
||||
"components/settings/integrations/google-health-card.tsx :: GoogleHealthCard": 300,
|
||||
"components/settings/integrations/integration-card.tsx :: IntegrationCard": 209,
|
||||
"components/settings/integrations/oauth-integration-card.tsx :: OAuthIntegrationCardView": 321,
|
||||
"components/settings/low-stock-card.tsx :: LowStockCard": 296,
|
||||
"components/settings/mcp-section.tsx :: McpTokensCard": 257,
|
||||
"components/settings/mood-reminder-card.tsx :: MoodReminderCard": 234,
|
||||
"components/settings/report-selection/selection-panel.tsx :: HealthRecordExportPanel": 370,
|
||||
"components/settings/score-section.tsx :: ScoreSection": 383,
|
||||
"components/settings/security-section/passkey-list-section.tsx :: PasskeyListSection": 410,
|
||||
"components/settings/security-section/security-keys-card.tsx :: SecurityKeysCard": 288,
|
||||
"components/settings/security-section/totp-card.tsx :: TotpCard": 533,
|
||||
"components/settings/security-sessions-card.tsx :: SecuritySessionsCard": 245,
|
||||
"components/settings/settings-shell.tsx :: SettingsShell": 254,
|
||||
"components/settings/share-link-create-form.tsx :: ShareLinkCreateForm": 451,
|
||||
"components/settings/sharing-section.tsx :: ShareLinksCard": 260,
|
||||
"components/settings/sources-section.tsx :: SourcesSection": 601,
|
||||
"components/settings/thresholds-editor-section.tsx :: MetricRow": 241,
|
||||
"components/settings/web-push-card.tsx :: WebPushCard": 215,
|
||||
"components/symptoms/symptom-entry-form.tsx :: SymptomEntryForm": 317,
|
||||
"components/symptoms/symptoms-section.tsx :: SymptomsSection": 205,
|
||||
"components/targets/range-bar.tsx :: RangeBar": 220,
|
||||
"components/targets/target-edit-sheet.tsx :: TargetEditSheetBody": 474,
|
||||
"components/timeline/life-event-form.tsx :: LifeEventForm": 281,
|
||||
"components/timeline/series-point-tips.tsx :: SeriesPointTips": 212,
|
||||
"components/timeline/timeline-chart.tsx :: ChartBody": 243,
|
||||
"components/timeline/timeline-chronicle.tsx :: TimelineChronicle": 274,
|
||||
"components/timeline/timeline-view.tsx :: TimelineView": 502,
|
||||
"components/ui/responsive-sheet.tsx :: ResponsiveSheet": 237,
|
||||
"components/vaccinations/catalog-picker.tsx :: CatalogPicker": 221,
|
||||
"components/vaccinations/custom-vaccine-sheet.tsx :: CustomVaccineSheet": 246,
|
||||
"components/vaccinations/vaccination-form.tsx :: VaccinationForm": 218,
|
||||
"components/workouts/manual-workout-form.tsx :: ManualWorkoutForm": 323,
|
||||
"lib/activity/calendar-math.ts :: buildActivityCalendar": 204,
|
||||
"lib/ai/anthropic-client.ts :: generateCompletion": 294,
|
||||
"lib/ai/coach/cycle-snapshot.ts :: buildCycleSnapshotBlock": 218,
|
||||
"lib/ai/coach/glp1-snapshot.ts :: buildGlp1SnapshotBlock": 218,
|
||||
"lib/ai/coach/snapshot.ts :: buildCoachSnapshotImpl": 898,
|
||||
"lib/ai/coach/tools/loop.ts :: runCoachToolLoop": 438,
|
||||
"lib/ai/coach/turn/model.ts :: runTurnModel": 560,
|
||||
"lib/ai/coach/turn/pipeline.ts :: runTurn": 274,
|
||||
"lib/ai/coach/turn/reply-guards.ts :: guardReply": 331,
|
||||
"lib/ai/codex-client.ts :: parseResponse": 302,
|
||||
"lib/ai/local-client.ts :: generateCompletionStream": 288,
|
||||
"lib/ai/openai-client.ts :: generateCompletion": 346,
|
||||
"lib/ai/safety/outbound-screen.ts :: buildDoseChangeClass": 275,
|
||||
"lib/analytics/bp-in-target-fast-path.ts :: computeFromRollups": 203,
|
||||
"lib/analytics/compliance/display.ts :: calculateCompliance": 212,
|
||||
"lib/analytics/correlations-fast-path.ts :: computeCorrelationHypothesesFastPath": 256,
|
||||
"lib/analytics/intraday-pulse-io.ts :: loadIntradayPulse": 206,
|
||||
"lib/analytics/score/reader.ts :: computeUserHealthScore": 324,
|
||||
"lib/analytics/summaries-slice.ts :: computeFromRollups": 441,
|
||||
"lib/api-handler.ts :: apiHandler": 207,
|
||||
"lib/api-handler.ts :: apiHandler > wrapped": 201,
|
||||
"lib/auth/profile-update.ts :: applyProfileUpdate": 353,
|
||||
"lib/cache/server-cache.ts :: createServerCaches": 230,
|
||||
"lib/cycle/backup.ts :: restoreCycleData": 243,
|
||||
"lib/cycle/day-log-write.ts :: writeDayLog": 311,
|
||||
"lib/daily/digest.ts :: buildDailyDigest": 227,
|
||||
"lib/daily/load-digest.ts :: loadDailyDigest": 452,
|
||||
"lib/dashboard/snapshot.ts :: buildDashboardSnapshot": 312,
|
||||
"lib/dashboard/snapshot.ts :: buildExtras": 240,
|
||||
"lib/day/records.ts :: medicationsPart": 306,
|
||||
"lib/doctor-report-pdf/clinical-records-notes-section.ts :: buildClinicalRecordsNotesSection": 648,
|
||||
"lib/doctor-report-pdf/measurements-charts-section.ts :: buildMeasurementsChartsSection": 398,
|
||||
"lib/doctor-report-pdf/medication-mood-wellness-section.ts :: buildMedicationMoodWellnessSection": 509,
|
||||
"lib/doctor-report/collect.ts :: collectDoctorReportData": 678,
|
||||
"lib/doctor-report/dense-buckets.ts :: loadDenseMeasurementBuckets": 210,
|
||||
"lib/documents/fenced-chat.ts :: streamFencedReply": 309,
|
||||
"lib/documents/ingest.ts :: ingestDocument": 272,
|
||||
"lib/export/backup-json-scan.ts :: scanBackupJson": 225,
|
||||
"lib/export/full-backup-payload.ts :: buildFullBackupPayload": 804,
|
||||
"lib/export/full-backup-payload.ts :: buildFullBackupPayload > medications.map(arg) #3": 268,
|
||||
"lib/export/profile-backup.ts :: buildProfileBackupSection": 270,
|
||||
"lib/export/profile-backup.ts :: restoreProfileData": 291,
|
||||
"lib/export/records-backup.ts :: buildRecordsBackupSection": 308,
|
||||
"lib/export/restore-backup.ts :: restoreBackup": 2833,
|
||||
"lib/export/restore-backup.ts :: restoreBackup > prisma.$transaction(arg)": 2163,
|
||||
"lib/export/vaccinations-backup.ts :: restoreVaccinationsData": 216,
|
||||
"lib/export/visits-backup.ts :: restoreVisitsData": 261,
|
||||
"lib/fhir/build-bundle.ts :: buildFhirDocumentBundle": 363,
|
||||
"lib/fitbit/sync-core.ts :: upsertFitbitMeasurements": 370,
|
||||
"lib/gamification/achievements-result.ts :: buildAchievementsResult": 586,
|
||||
"lib/google-health/client.ts :: forEachDataPointPage": 288,
|
||||
"lib/google-health/client.ts :: forEachDataPointPage > walk": 223,
|
||||
"lib/google-health/sync-core.ts :: upsertGoogleHealthMeasurements": 463,
|
||||
"lib/google-health/sync.ts :: runGoogleHealthSyncCycle": 203,
|
||||
"lib/illness/correlation.ts :: computeIllnessCorrelation": 233,
|
||||
"lib/import/labs-csv.ts :: importLabsCsv": 309,
|
||||
"lib/import/labs-csv.ts :: importLabsCsv > flush": 231,
|
||||
"lib/insights/biomarker-status.ts :: generateBiomarkerStatus": 290,
|
||||
"lib/insights/blood-pressure-status.ts :: prepareBloodPressureStatusForUser": 635,
|
||||
"lib/insights/bmi-status.ts :: prepareBmiStatusForUser": 387,
|
||||
"lib/insights/comprehensive-aggregator.ts :: buildFromRollups": 220,
|
||||
"lib/insights/comprehensive-generate.ts :: generateComprehensiveInsight": 872,
|
||||
"lib/insights/features.ts :: extractFeatures": 796,
|
||||
"lib/insights/general-status.ts :: prepareGeneralStatusForUser": 449,
|
||||
"lib/insights/medication-compliance-status.ts :: prepareMedicationComplianceStatusForUser": 485,
|
||||
"lib/insights/metric-status.ts :: generateMetricStatus": 477,
|
||||
"lib/insights/mood-aggregates.ts :: fetchMoodAggregates": 246,
|
||||
"lib/insights/mood-status.ts :: prepareMoodStatusForUser": 646,
|
||||
"lib/insights/pulse-status.ts :: preparePulseStatusForUser": 414,
|
||||
"lib/insights/status-provider.ts :: runStatusCompletion": 241,
|
||||
"lib/insights/weight-status.ts :: prepareWeightStatusForUser": 598,
|
||||
"lib/integrations/connection-health-load.ts :: loadConnectionFacts": 227,
|
||||
"lib/integrations/status.ts :: recordSyncFailure": 292,
|
||||
"lib/jobs/apple-health-import-worker.ts :: handleAppleHealthImport": 357,
|
||||
"lib/jobs/coach-nudge.ts :: findTriggerForUser": 279,
|
||||
"lib/jobs/coach-nudge.ts :: runCoachNudgeTick": 292,
|
||||
"lib/jobs/coach-reminder-sweep.ts :: runCoachReminderSweep": 222,
|
||||
"lib/jobs/cycle-reminder.ts :: runCycleReminderTick": 243,
|
||||
"lib/jobs/document-summary.ts :: runDocumentSummaryJob": 239,
|
||||
"lib/jobs/google-health-backfill.ts :: runGoogleHealthBackfillForUser": 276,
|
||||
"lib/jobs/insight-pregenerate.ts :: forceWarmUser": 282,
|
||||
"lib/jobs/insight-pregenerate.ts :: forceWarmUser > withFeatureCacheScope(arg)": 252,
|
||||
"lib/jobs/insight-pregenerate.ts :: runInsightPregenerate": 383,
|
||||
"lib/jobs/insight-pregenerate.ts :: runInsightPregenerate > pregenerateCandidate": 233,
|
||||
"lib/jobs/measurement-reminder.ts :: runMeasurementReminderTick": 286,
|
||||
"lib/jobs/medication-intake-import.ts :: processNextChunk": 220,
|
||||
"lib/jobs/medication-intake-import.ts :: processNextChunk > client.$transaction(arg)": 212,
|
||||
"lib/jobs/medication-low-stock.ts :: runMedicationLowStockTick": 286,
|
||||
"lib/jobs/oura-backfill.ts :: runOuraBackfillForUser": 236,
|
||||
"lib/jobs/reaction-line.ts :: runReactionLine": 337,
|
||||
"lib/jobs/reminder/medication-reminder-check.ts :: handleReminderCheck": 619,
|
||||
"lib/jobs/reminder/medication-reminder-check.ts :: handleReminderCheck > withBackgroundEvent(arg)": 614,
|
||||
"lib/jobs/reminder/register-integration-sync.ts :: enqueueIntegrationSyncBootDiscovery": 224,
|
||||
"lib/jobs/reminder/register-integration-sync.ts :: registerIntegrationSyncQueues": 320,
|
||||
"lib/jobs/reminder/register-maintenance.ts :: enqueueMaintenanceBootDiscovery": 232,
|
||||
"lib/jobs/reminder/register-maintenance.ts :: registerMaintenanceQueues": 997,
|
||||
"lib/jobs/reminder/register-rollup.ts :: enqueueRollupBootDiscovery": 238,
|
||||
"lib/jobs/reminder/register-rollup.ts :: registerRollupQueues": 616,
|
||||
"lib/jobs/reminder/register-status.ts :: registerStatusQueues": 542,
|
||||
"lib/jobs/workout-insight-generate.ts :: runWorkoutInsightGenerate": 324,
|
||||
"lib/labs/biomarker-merge.ts :: runMerge": 254,
|
||||
"lib/labs/ocr-extract.ts :: runOcrExtraction": 207,
|
||||
"lib/labs/ocr-run.ts :: executeOcrExtraction": 206,
|
||||
"lib/mcp/tools.ts :: searchAndFetchTools": 421,
|
||||
"lib/mcp/tools.ts :: searchAndFetchTools > run": 205,
|
||||
"lib/measurements/consolidate-daily-mean.ts :: consolidateDailyMean": 278,
|
||||
"lib/measurements/consolidate-legacy-steps.ts :: consolidateLegacySteps": 263,
|
||||
"lib/measurements/dense-intraday-retention.ts :: runDenseIntradayRetention": 537,
|
||||
"lib/measurements/dense-intraday-retention.ts :: runDenseIntradayRetention > walkSource": 447,
|
||||
"lib/measurements/dense-intraday-retention.ts :: runDenseIntradayRetention > walkSource > writeDay": 373,
|
||||
"lib/measurements/dense-intraday-retention.ts :: runDenseIntradayRetention > walkSource > writeDay > foldOnce": 212,
|
||||
"lib/measurements/drain-per-sample-cumulative.ts :: drainPerSampleCumulative": 344,
|
||||
"lib/measurements/drain-per-sample-cumulative.ts :: drainPerSampleCumulative > writeDay": 205,
|
||||
"lib/measurements/import-apple-health-export.ts :: streamParseExportXml": 1256,
|
||||
"lib/measurements/import-apple-health-export.ts :: streamParseExportXml > (anonymous) #2": 323,
|
||||
"lib/medications/efficacy/build-efficacy.ts :: buildMedicationEfficacy": 268,
|
||||
"lib/medications/intake-slot-dedup.ts :: dedupeUserIntakeSlots": 321,
|
||||
"lib/medications/list-read.ts :: buildMedicationsList": 310,
|
||||
"lib/medications/scheduling/dose-history.ts :: reconstructDoseHistory": 218,
|
||||
"lib/multipart/stream-to-disk.ts :: streamMultipartToDisk": 272,
|
||||
"lib/notifications/dispatcher.ts :: dispatchNotification": 378,
|
||||
"lib/notifications/senders/apns.ts :: sendViaApns": 299,
|
||||
"lib/notifications/senders/web-push.ts :: sendViaWebPush": 268,
|
||||
"lib/personal-records/pr-detection-worker.ts :: detectPersonalRecordsForUser": 239,
|
||||
"lib/personal-records/pr-direction.ts :: getPRDirection": 202,
|
||||
"lib/record-activity/activity-verb.ts :: recordActivityVerbLine": 395,
|
||||
"lib/strava/sync.ts :: runStravaSync": 219,
|
||||
"lib/support/report.ts :: collectSupportReportFacts": 332,
|
||||
"lib/targets/build-response.ts :: buildTargetsResponse": 330,
|
||||
"lib/targets/vitals-builder.ts :: buildVitalTargets": 324,
|
||||
"lib/telegram-webhook-handlers.ts :: handleCallback": 599,
|
||||
"lib/telegram-webhook-handlers.ts :: handleTextMessage": 337,
|
||||
"lib/withings/sync-activity.ts :: syncUserActivity": 391,
|
||||
"lib/withings/sync-ecg.ts :: syncUserEcg": 213,
|
||||
"lib/withings/sync-sleep.ts :: syncUserSleep": 429,
|
||||
"lib/withings/sync.ts :: syncUserMeasurements": 373,
|
||||
"proxy.ts :: proxy": 499
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,577 @@
|
||||
{
|
||||
"$comment": "as never casts in tests, per test file under src/. Maintained by src/__tests__/test-realism-ratchet.test.ts.",
|
||||
"count": 570,
|
||||
"total": 6014,
|
||||
"entries": {
|
||||
"__tests__/api/auth/login/mfa-branch.test.ts": 10,
|
||||
"__tests__/api/auth/mfa/verify.test.ts": 9,
|
||||
"__tests__/api/auth/registration-status/route.test.ts": 4,
|
||||
"__tests__/derived-wellness-value-contract.test.ts": 2,
|
||||
"__tests__/notification-delivery-identity.test.ts": 13,
|
||||
"__tests__/profile-email-rate-limit-contract.test.ts": 3,
|
||||
"__tests__/record-write-rate-limit-contract.test.ts": 3,
|
||||
"__tests__/shared-record-navigation.test.tsx": 2,
|
||||
"__tests__/sharing-legacy-client-contract.test.ts": 2,
|
||||
"__tests__/tz-day-paths-harness.test.ts": 12,
|
||||
"app/api/account/grants/__tests__/invite-access-level.test.ts": 2,
|
||||
"app/api/account/grants/__tests__/invite-recent-proof.test.ts": 4,
|
||||
"app/api/account/grants/__tests__/invite-scope-and-manage.test.ts": 2,
|
||||
"app/api/admin/ai-settings/__tests__/route.test.ts": 9,
|
||||
"app/api/admin/app-logs/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/audit-log/__tests__/route.test.ts": 1,
|
||||
"app/api/admin/audit-log/actions/__tests__/route.test.ts": 2,
|
||||
"app/api/admin/import-apple-health-export/__tests__/route.test.ts": 5,
|
||||
"app/api/admin/maintenance/measurements/__tests__/status.test.ts": 11,
|
||||
"app/api/admin/maintenance/pulse-day-mean/__tests__/route.test.ts": 7,
|
||||
"app/api/admin/notifications/diagnostic/__tests__/route.test.ts": 15,
|
||||
"app/api/admin/notifications/health/__tests__/route.test.ts": 5,
|
||||
"app/api/admin/notifications/reminder-check/__tests__/route.test.ts": 4,
|
||||
"app/api/admin/notifications/test/__tests__/route.test.ts": 15,
|
||||
"app/api/admin/provider-health/__tests__/route.test.ts": 29,
|
||||
"app/api/admin/settings/__tests__/route.test.ts": 12,
|
||||
"app/api/admin/settings/assistant-flags/__tests__/route.test.ts": 21,
|
||||
"app/api/admin/settings/module-availability/__tests__/route.test.ts": 13,
|
||||
"app/api/admin/settings/web-push-vapid/generate/__tests__/route.test.ts": 4,
|
||||
"app/api/admin/status/__tests__/route.test.ts": 3,
|
||||
"app/api/admin/users/__tests__/route.test.ts": 3,
|
||||
"app/api/admin/users/[id]/__tests__/route.test.ts": 8,
|
||||
"app/api/ai/test/__tests__/route.test.ts": 26,
|
||||
"app/api/analytics/__tests__/route.test.ts": 25,
|
||||
"app/api/auth/claim/__tests__/route.test.ts": 13,
|
||||
"app/api/auth/login/__tests__/constant-work.test.ts": 4,
|
||||
"app/api/auth/login/__tests__/native-token.test.ts": 8,
|
||||
"app/api/auth/login/__tests__/route.test.ts": 5,
|
||||
"app/api/auth/login/__tests__/trusted-device.test.ts": 2,
|
||||
"app/api/auth/me/ai-usage/__tests__/route.test.ts": 3,
|
||||
"app/api/auth/me/coach-prefs/__tests__/route.test.ts": 9,
|
||||
"app/api/auth/me/devices/__tests__/route.test.ts": 8,
|
||||
"app/api/auth/me/devices/[id]/__tests__/route.test.ts": 13,
|
||||
"app/api/auth/me/diabetes/__tests__/route.test.ts": 13,
|
||||
"app/api/auth/me/disable-coach/__tests__/route.test.ts": 19,
|
||||
"app/api/auth/me/disable-coach/__tests__/unknown-key-strict.test.ts": 2,
|
||||
"app/api/auth/me/documents-auto-ai-read/__tests__/route.test.ts": 5,
|
||||
"app/api/auth/me/glucose-unit/__tests__/route.test.ts": 19,
|
||||
"app/api/auth/me/health-score-config/__tests__/route.test.ts": 25,
|
||||
"app/api/auth/me/mfa/disable/__tests__/route.test.ts": 7,
|
||||
"app/api/auth/me/mfa/webauthn/register/__tests__/route.test.ts": 8,
|
||||
"app/api/auth/me/modules/__tests__/route.test.ts": 29,
|
||||
"app/api/auth/me/notification-prefs/__tests__/route.test.ts": 44,
|
||||
"app/api/auth/me/security-activity/__tests__/route.test.ts": 2,
|
||||
"app/api/auth/me/sessions/__tests__/route.test.ts": 13,
|
||||
"app/api/auth/me/source-priority/__tests__/route.test.ts": 11,
|
||||
"app/api/auth/me/timezone/__tests__/route.test.ts": 15,
|
||||
"app/api/auth/me/unit-preference/__tests__/route.test.ts": 19,
|
||||
"app/api/auth/me/workout-routes/__tests__/route.test.ts": 3,
|
||||
"app/api/auth/mfa/webauthn/verify/__tests__/route.test.ts": 11,
|
||||
"app/api/auth/native/complete/__tests__/route.test.ts": 2,
|
||||
"app/api/auth/native/login/__tests__/route.test.ts": 2,
|
||||
"app/api/auth/native/token/__tests__/route.test.ts": 12,
|
||||
"app/api/auth/oidc/callback/__tests__/route.test.ts": 18,
|
||||
"app/api/auth/oidc/login/__tests__/route.test.ts": 4,
|
||||
"app/api/auth/oidc/native/token/__tests__/route.test.ts": 11,
|
||||
"app/api/auth/passkey/login-options/__tests__/oidc-only.test.ts": 2,
|
||||
"app/api/auth/passkey/login-verify/__tests__/native-token.test.ts": 7,
|
||||
"app/api/auth/passkey/login-verify/__tests__/oidc-only.test.ts": 4,
|
||||
"app/api/auth/passkey/login-verify/__tests__/refusals.test.ts": 6,
|
||||
"app/api/auth/passkeys/__tests__/route.test.ts": 3,
|
||||
"app/api/auth/passkeys/[id]/__tests__/route.test.ts": 17,
|
||||
"app/api/auth/password/__tests__/route.test.ts": 5,
|
||||
"app/api/auth/register/__tests__/route.test.ts": 25,
|
||||
"app/api/biomarkers/__tests__/error-contract.test.ts": 3,
|
||||
"app/api/biomarkers/[id]/__tests__/delete-record-auth.test.ts": 3,
|
||||
"app/api/biomarkers/[id]/__tests__/route.test.ts": 23,
|
||||
"app/api/coach/plans/__tests__/route.test.ts": 23,
|
||||
"app/api/coach/reminders/__tests__/route.test.ts": 15,
|
||||
"app/api/coach/suggested-actions/__tests__/route.test.ts": 7,
|
||||
"app/api/consent/ai/__tests__/route.test.ts": 10,
|
||||
"app/api/consent/ai/latest/__tests__/route.test.ts": 42,
|
||||
"app/api/consent/ai/web/__tests__/route.test.ts": 6,
|
||||
"app/api/custom-metrics/__tests__/route.test.ts": 14,
|
||||
"app/api/custom-metrics/[id]/__tests__/route.test.ts": 23,
|
||||
"app/api/custom-metrics/[id]/entries/__tests__/note-encryption.test.ts": 13,
|
||||
"app/api/custom-metrics/[id]/entries/__tests__/route.test.ts": 12,
|
||||
"app/api/custom-metrics/[id]/entries/__tests__/soft-delete-restore.test.ts": 6,
|
||||
"app/api/cycle/__tests__/external-id-floor.test.ts": 9,
|
||||
"app/api/cycle/calendar/__tests__/day-entries.test.ts": 9,
|
||||
"app/api/cycle/calendar/__tests__/verdict-round-trip.test.ts": 26,
|
||||
"app/api/cycle/day-logs/bulk/__tests__/route.test.ts": 6,
|
||||
"app/api/cycle/symptoms/custom/__tests__/custom-crud.test.ts": 4,
|
||||
"app/api/daily/digest/__tests__/route.test.ts": 2,
|
||||
"app/api/daily/digest/dismiss/__tests__/route.test.ts": 2,
|
||||
"app/api/dashboard/chart-overlay-prefs/__tests__/route.test.ts": 2,
|
||||
"app/api/dashboard/snapshot/__tests__/route.test.ts": 5,
|
||||
"app/api/dashboard/summary/__tests__/module-gate.test.ts": 6,
|
||||
"app/api/dashboard/summary/__tests__/route.test.ts": 86,
|
||||
"app/api/dashboard/widgets/__tests__/route.test.ts": 49,
|
||||
"app/api/devices/__tests__/route.test.ts": 32,
|
||||
"app/api/devices/[id]/__tests__/route.test.ts": 10,
|
||||
"app/api/documents/inbound/__tests__/route.test.ts": 62,
|
||||
"app/api/documents/inbound/[id]/__tests__/route.test.ts": 56,
|
||||
"app/api/documents/inbound/[id]/chat/__tests__/route.test.ts": 42,
|
||||
"app/api/documents/inbound/[id]/extract/__tests__/route.test.ts": 38,
|
||||
"app/api/documents/inbound/[id]/index/__tests__/route.test.ts": 40,
|
||||
"app/api/documents/inbound/[id]/original/__tests__/route.test.ts": 7,
|
||||
"app/api/documents/inbound/[id]/suggest/__tests__/route.test.ts": 12,
|
||||
"app/api/documents/inbound/[id]/summary/__tests__/route.test.ts": 31,
|
||||
"app/api/documents/inbound/[id]/thumbnail/__tests__/route.test.ts": 5,
|
||||
"app/api/documents/inbound/layout/__tests__/route.test.ts": 11,
|
||||
"app/api/documents/inbound/reindex/__tests__/route.test.ts": 9,
|
||||
"app/api/export/__tests__/measurement-streaming.test.ts": 9,
|
||||
"app/api/export/__tests__/per-type-routes.test.ts": 100,
|
||||
"app/api/export/__tests__/soft-delete-filter.test.ts": 39,
|
||||
"app/api/export/encrypted/__tests__/route.test.ts": 20,
|
||||
"app/api/export/health-record/__tests__/route.test.ts": 32,
|
||||
"app/api/feature-flags/__tests__/route.test.ts": 6,
|
||||
"app/api/fhir/__tests__/capability-and-everything.test.ts": 4,
|
||||
"app/api/fhir/__tests__/module-gate.test.ts": 6,
|
||||
"app/api/fhir/__tests__/observation-series.test.ts": 8,
|
||||
"app/api/fhir/__tests__/route.test.ts": 9,
|
||||
"app/api/fitbit/connect/__tests__/route.test.ts": 4,
|
||||
"app/api/gamification/achievements/__tests__/hidden-redaction.test.ts": 12,
|
||||
"app/api/gamification/achievements/__tests__/ios-format.test.ts": 13,
|
||||
"app/api/gamification/achievements/__tests__/module-gate.test.ts": 17,
|
||||
"app/api/gamification/achievements/__tests__/writes-nothing.test.ts": 1,
|
||||
"app/api/google-health/credentials/__tests__/route.test.ts": 1,
|
||||
"app/api/google-health/sync/__tests__/route.test.ts": 3,
|
||||
"app/api/google-health/sync/status/__tests__/route.test.ts": 1,
|
||||
"app/api/illness/episodes/[id]/__tests__/body-site.test.ts": 9,
|
||||
"app/api/illness/episodes/[id]/day-logs/__tests__/route.test.ts": 8,
|
||||
"app/api/illness/episodes/[id]/restore/__tests__/route.test.ts": 9,
|
||||
"app/api/import/__tests__/route.test.ts": 47,
|
||||
"app/api/import/apple-health-export/__tests__/route.test.ts": 8,
|
||||
"app/api/import/apple-health-export/[jobId]/status/__tests__/route.test.ts": 4,
|
||||
"app/api/import/csv/__tests__/route.test.ts": 4,
|
||||
"app/api/import/health-connect-export/__tests__/route.test.ts": 2,
|
||||
"app/api/ingest/medication/__tests__/route.test.ts": 28,
|
||||
"app/api/insights/biomarker-assessment/__tests__/route.test.ts": 6,
|
||||
"app/api/insights/blood-pressure-status/__tests__/route.test.ts": 3,
|
||||
"app/api/insights/cards/__tests__/route.test.ts": 18,
|
||||
"app/api/insights/chat/[id]/attachments/__tests__/route.test.ts": 23,
|
||||
"app/api/insights/chat/fenced/__tests__/route.test.ts": 18,
|
||||
"app/api/insights/coach/facts/__tests__/route-memory.test.ts": 9,
|
||||
"app/api/insights/coach/facts/__tests__/route.test.ts": 12,
|
||||
"app/api/insights/comprehensive/__tests__/route.test.ts": 20,
|
||||
"app/api/insights/correlations/__tests__/route.test.ts": 12,
|
||||
"app/api/insights/derived/__tests__/route.test.ts": 12,
|
||||
"app/api/insights/derived/batch/__tests__/route.test.ts": 21,
|
||||
"app/api/insights/ecg/__tests__/route.test.ts": 5,
|
||||
"app/api/insights/ecg/[id]/__tests__/route.test.ts": 5,
|
||||
"app/api/insights/generate/__tests__/route.test.ts": 54,
|
||||
"app/api/insights/layout/__tests__/route.test.ts": 23,
|
||||
"app/api/insights/metric-status/__tests__/route.test.ts": 7,
|
||||
"app/api/insights/mood-status/__tests__/route.test.ts": 10,
|
||||
"app/api/insights/patterns/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/patterns/[id]/__tests__/route.test.ts": 4,
|
||||
"app/api/insights/pregenerate/__tests__/route.test.ts": 4,
|
||||
"app/api/insights/provider-chain/__tests__/put.test.ts": 4,
|
||||
"app/api/insights/provider-chain/__tests__/route.test.ts": 13,
|
||||
"app/api/insights/rhythm-events/__tests__/route.test.ts": 5,
|
||||
"app/api/insights/targets/__tests__/route.test.ts": 1,
|
||||
"app/api/integrations/fitbit/test/__tests__/route.test.ts": 18,
|
||||
"app/api/integrations/healthkit/__tests__/route.test.ts": 48,
|
||||
"app/api/integrations/status/__tests__/route.test.ts": 1,
|
||||
"app/api/integrations/whoop/test/__tests__/route.test.ts": 15,
|
||||
"app/api/integrations/withings/resume/__tests__/route.test.ts": 2,
|
||||
"app/api/integrations/withings/test/__tests__/route.test.ts": 9,
|
||||
"app/api/internal/deploy-webhook/__tests__/route.test.ts": 9,
|
||||
"app/api/labs/__tests__/error-contract.test.ts": 4,
|
||||
"app/api/labs/ocr/commit/__tests__/route.test.ts": 9,
|
||||
"app/api/labs/ocr/extract/__tests__/route.test.ts": 17,
|
||||
"app/api/labs/restore/__tests__/route.test.ts": 5,
|
||||
"app/api/mcp/oauth/__tests__/authorize.test.ts": 29,
|
||||
"app/api/mcp/oauth/__tests__/register.test.ts": 8,
|
||||
"app/api/mcp/oauth/__tests__/token.test.ts": 14,
|
||||
"app/api/mcp/tokens/__tests__/route.test.ts": 3,
|
||||
"app/api/measurement-categories/__tests__/route.test.ts": 3,
|
||||
"app/api/measurements/__tests__/arrival-wiring.test.ts": 7,
|
||||
"app/api/measurements/__tests__/group-by-day.test.ts": 10,
|
||||
"app/api/measurements/__tests__/put-duplicate-timestamp.test.ts": 9,
|
||||
"app/api/measurements/__tests__/range-aggregation-route.test.ts": 10,
|
||||
"app/api/measurements/__tests__/zod-multi-issue.test.ts": 2,
|
||||
"app/api/measurements/[id]/__tests__/route.test.ts": 8,
|
||||
"app/api/measurements/batch/__tests__/aggregated-hr-wire.test.ts": 9,
|
||||
"app/api/measurements/batch/__tests__/duplicate-prefilter.test.ts": 1,
|
||||
"app/api/measurements/batch/__tests__/hr-bucket-observability.test.ts": 3,
|
||||
"app/api/measurements/batch/__tests__/insert-returning-arrivals.test.ts": 2,
|
||||
"app/api/measurements/batch/__tests__/issue-paths.test.ts": 1,
|
||||
"app/api/measurements/batch/__tests__/pr-detection-hook.test.ts": 3,
|
||||
"app/api/measurements/batch/__tests__/refusal-detail.test.ts": 1,
|
||||
"app/api/measurements/batch/__tests__/sync-trigger.test.ts": 6,
|
||||
"app/api/measurements/batch/__tests__/timestamp-plausibility.test.ts": 2,
|
||||
"app/api/measurements/batch/__tests__/unstable-external-id.test.ts": 2,
|
||||
"app/api/measurements/bulk-delete/__tests__/route.test.ts": 10,
|
||||
"app/api/measurements/by-external-ids/__tests__/route.test.ts": 6,
|
||||
"app/api/measurements/restore/__tests__/route.test.ts": 10,
|
||||
"app/api/measurements/series-batch/__tests__/route.test.ts": 10,
|
||||
"app/api/measurements/series/__tests__/route.test.ts": 96,
|
||||
"app/api/measurements/series/__tests__/unknown-query-param-open.test.ts": 6,
|
||||
"app/api/medications/__tests__/route.test.ts": 43,
|
||||
"app/api/medications/[id]/__tests__/route.test.ts": 52,
|
||||
"app/api/medications/[id]/cadence/__tests__/route.test.ts": 12,
|
||||
"app/api/medications/[id]/compliance/__tests__/route.test.ts": 18,
|
||||
"app/api/medications/[id]/dose-history/__tests__/route.test.ts": 15,
|
||||
"app/api/medications/[id]/glp1/__tests__/route.test.ts": 47,
|
||||
"app/api/medications/[id]/intake/__tests__/route.test.ts": 45,
|
||||
"app/api/medications/[id]/intake/__tests__/source-provenance.test.ts": 12,
|
||||
"app/api/medications/[id]/intake/[eventId]/__tests__/route.test.ts": 63,
|
||||
"app/api/medications/[id]/intake/bulk-delete/__tests__/route.test.ts": 13,
|
||||
"app/api/medications/[id]/intake/import/__tests__/route.test.ts": 9,
|
||||
"app/api/medications/[id]/intake/import/[jobId]/status/__tests__/route.test.ts": 2,
|
||||
"app/api/medications/[id]/intake/purge/__tests__/route.test.ts": 4,
|
||||
"app/api/medications/[id]/inventory/__tests__/route.test.ts": 45,
|
||||
"app/api/medications/[id]/inventory/[itemId]/__tests__/route.test.ts": 12,
|
||||
"app/api/medications/[id]/phase-config/__tests__/route.test.ts": 2,
|
||||
"app/api/medications/[id]/schedule-revisions/__tests__/route.test.ts": 24,
|
||||
"app/api/medications/[id]/side-effects/__tests__/route.test.ts": 32,
|
||||
"app/api/medications/compliance/__tests__/route.test.ts": 10,
|
||||
"app/api/medications/extract/__tests__/route.test.ts": 38,
|
||||
"app/api/medications/intake/__tests__/route.test.ts": 59,
|
||||
"app/api/medications/intake/bulk/__tests__/dose-arrival-scope.test.ts": 9,
|
||||
"app/api/medications/intake/bulk/__tests__/route.test.ts": 68,
|
||||
"app/api/medications/intake/dose-history-import/__tests__/route.test.ts": 7,
|
||||
"app/api/medications/layout/__tests__/route.test.ts": 17,
|
||||
"app/api/mental-health/assessments/__tests__/route.test.ts": 12,
|
||||
"app/api/mental-health/assessments/[id]/__tests__/route.test.ts": 7,
|
||||
"app/api/meta/capabilities/__tests__/route.test.ts": 1,
|
||||
"app/api/monitoring/glitchtip/__tests__/route.test.ts": 2,
|
||||
"app/api/monitoring/umami-script/__tests__/route.test.ts": 2,
|
||||
"app/api/monitoring/umami/[...path]/__tests__/route.test.ts": 4,
|
||||
"app/api/mood-entries/__tests__/route.test.ts": 6,
|
||||
"app/api/mood-entries/[id]/__tests__/route.test.ts": 8,
|
||||
"app/api/mood-entries/bulk-delete/__tests__/route.test.ts": 10,
|
||||
"app/api/mood-entries/bulk/__tests__/route.test.ts": 17,
|
||||
"app/api/mood-entries/restore/__tests__/route.test.ts": 11,
|
||||
"app/api/mood/analytics/__tests__/route.test.ts": 15,
|
||||
"app/api/mood/insights/__tests__/route.test.ts": 3,
|
||||
"app/api/mood/tags/__tests__/custom-crud.test.ts": 2,
|
||||
"app/api/mood/tags/__tests__/groups-crud.test.ts": 1,
|
||||
"app/api/mood/tags/__tests__/layout.test.ts": 1,
|
||||
"app/api/mood/tags/__tests__/route.test.ts": 1,
|
||||
"app/api/mood/tags/layout/__tests__/route.test.ts": 11,
|
||||
"app/api/nightscout/sync/__tests__/route.test.ts": 3,
|
||||
"app/api/notifications/action/[token]/__tests__/route.test.ts": 5,
|
||||
"app/api/notifications/status/__tests__/route.test.ts": 4,
|
||||
"app/api/notifications/web-push/__tests__/route.test.ts": 5,
|
||||
"app/api/notifications/web-push/test/__tests__/route.test.ts": 9,
|
||||
"app/api/nutrients/__tests__/route.test.ts": 5,
|
||||
"app/api/nutrients/batch/__tests__/route.test.ts": 11,
|
||||
"app/api/nutrients/daily/__tests__/route.test.ts": 7,
|
||||
"app/api/nutrients/water/__tests__/route.test.ts": 8,
|
||||
"app/api/onboarding/__tests__/error-contract.test.ts": 1,
|
||||
"app/api/onboarding/complete/__tests__/route.test.ts": 4,
|
||||
"app/api/oura/callback/__tests__/route.test.ts": 1,
|
||||
"app/api/oura/sync/__tests__/route.test.ts": 3,
|
||||
"app/api/personal-records/__tests__/route.test.ts": 15,
|
||||
"app/api/polar/callback/__tests__/route.test.ts": 1,
|
||||
"app/api/polar/sync/__tests__/route.test.ts": 3,
|
||||
"app/api/settings/__tests__/channel-enabled-routes.test.ts": 14,
|
||||
"app/api/settings/__tests__/channel-global-switch-refusal.test.ts": 15,
|
||||
"app/api/settings/__tests__/email-test-route.test.ts": 2,
|
||||
"app/api/settings/__tests__/private-origin-routes.test.ts": 4,
|
||||
"app/api/settings/__tests__/telegram-test-locale.test.ts": 9,
|
||||
"app/api/settings/__tests__/webhook-format-route.test.ts": 4,
|
||||
"app/api/settings/reminder-thresholds/__tests__/route.test.ts": 7,
|
||||
"app/api/settings/webhook/test/__tests__/route.test.ts": 2,
|
||||
"app/api/share-links/__tests__/route.test.ts": 19,
|
||||
"app/api/sleep/night/__tests__/route.test.ts": 24,
|
||||
"app/api/sleep/rhythm/__tests__/route.test.ts": 6,
|
||||
"app/api/strava/sync/__tests__/route.test.ts": 3,
|
||||
"app/api/sync/changes/__tests__/module-gate.test.ts": 7,
|
||||
"app/api/sync/state/__tests__/route.test.ts": 8,
|
||||
"app/api/telegram/webhook/__tests__/route.test.ts": 43,
|
||||
"app/api/tokens/__tests__/route.test.ts": 5,
|
||||
"app/api/tokens/documents/__tests__/route.test.ts": 25,
|
||||
"app/api/tokens/measurements/__tests__/route.test.ts": 25,
|
||||
"app/api/tokens/workouts/__tests__/route.test.ts": 25,
|
||||
"app/api/user/ai-provider/__tests__/legacy-private-host.test.ts": 2,
|
||||
"app/api/user/ai-provider/__tests__/route.test.ts": 12,
|
||||
"app/api/user/avatar/__tests__/route.test.ts": 13,
|
||||
"app/api/user/avatar/[id]/__tests__/route.test.ts": 5,
|
||||
"app/api/user/profile/__tests__/route.test.ts": 29,
|
||||
"app/api/user/thresholds/__tests__/route.test.ts": 7,
|
||||
"app/api/whoop/callback/__tests__/route.test.ts": 2,
|
||||
"app/api/whoop/webhook/[token]/__tests__/route.test.ts": 5,
|
||||
"app/api/withings/callback/__tests__/route.test.ts": 9,
|
||||
"app/api/withings/connect/__tests__/route.test.ts": 4,
|
||||
"app/api/withings/credentials/__tests__/route.test.ts": 4,
|
||||
"app/api/withings/webhook/__tests__/route.test.ts": 26,
|
||||
"app/api/withings/webhook/[token]/__tests__/route.test.ts": 8,
|
||||
"app/api/workouts/__tests__/batch-create.test.ts": 14,
|
||||
"app/api/workouts/__tests__/batch-scope.test.ts": 10,
|
||||
"app/api/workouts/__tests__/canonical-dedup.test.ts": 20,
|
||||
"app/api/workouts/__tests__/delete.test.ts": 7,
|
||||
"app/api/workouts/__tests__/detail.test.ts": 36,
|
||||
"app/api/workouts/batch/__tests__/unstable-external-id.test.ts": 4,
|
||||
"app/c/[token]/__tests__/report-download.test.ts": 11,
|
||||
"app/c/[token]/d/[id]/__tests__/route.test.ts": 8,
|
||||
"app/mcp/__tests__/route.test.ts": 1,
|
||||
"components/day/__tests__/day-view.test.tsx": 1,
|
||||
"components/documents/__tests__/document-record-links.test.tsx": 1,
|
||||
"components/insights/__tests__/coincident-deviation-card.test.tsx": 1,
|
||||
"components/insights/coach-panel/__tests__/chat-bubble-memo.test.tsx": 1,
|
||||
"components/insights/coach-panel/__tests__/message-thread.test.tsx": 1,
|
||||
"components/insights/coach-panel/__tests__/open-at-end.test.ts": 1,
|
||||
"components/insights/coach-panel/__tests__/result-chart-compare.test.tsx": 2,
|
||||
"components/insights/coach-panel/__tests__/turn-activity.test.tsx": 1,
|
||||
"components/links/__tests__/vault-document-options.test.ts": 1,
|
||||
"components/measurements/__tests__/measurement-list-url-sync.test.tsx": 1,
|
||||
"components/medications/__tests__/side-effects-section.test.tsx": 3,
|
||||
"components/medications/__tests__/use-medication-intake.test.ts": 1,
|
||||
"components/medications/detail/efficacy/__tests__/change-at-start-summary.test.tsx": 1,
|
||||
"components/mood/__tests__/mood-list-url-sync.test.tsx": 1,
|
||||
"components/settings/__tests__/settings-shell.test.tsx": 1,
|
||||
"components/settings/integrations/__tests__/dawarich-card.test.tsx": 1,
|
||||
"components/timeline/__tests__/medication-rows.test.ts": 1,
|
||||
"lib/__tests__/acting-account-resolver.test.ts": 5,
|
||||
"lib/__tests__/auth-error-codes.test.ts": 7,
|
||||
"lib/__tests__/db.test.ts": 2,
|
||||
"lib/__tests__/doctor-report-data.test.ts": 2,
|
||||
"lib/__tests__/doctor-report-module-backstop.test.ts": 3,
|
||||
"lib/__tests__/doctor-report-recovery-canonical.test.ts": 1,
|
||||
"lib/__tests__/idempotency.test.ts": 29,
|
||||
"lib/__tests__/rate-limit-auth-surface.test.ts": 3,
|
||||
"lib/__tests__/require-auth-bearer.test.ts": 20,
|
||||
"lib/__tests__/require-fresh-mfa-if-enrolled.test.ts": 11,
|
||||
"lib/__tests__/require-fresh-mfa-or-elevation-if-enrolled.test.ts": 15,
|
||||
"lib/__tests__/require-fresh-mfa.test.ts": 32,
|
||||
"lib/__tests__/require-recent-proof.test.ts": 16,
|
||||
"lib/__tests__/require-record-auth-scope.test.ts": 8,
|
||||
"lib/ai/__tests__/briefing-grounding.test.ts": 9,
|
||||
"lib/ai/__tests__/codex-oauth.test.ts": 5,
|
||||
"lib/ai/__tests__/openai-compatible-provider.test.ts": 2,
|
||||
"lib/ai/__tests__/provider-admin-host.test.ts": 2,
|
||||
"lib/ai/__tests__/provider-health-ledger.test.ts": 5,
|
||||
"lib/ai/__tests__/provider-legacy-private-host.test.ts": 3,
|
||||
"lib/ai/__tests__/provider-presence.test.ts": 2,
|
||||
"lib/ai/__tests__/provider-reasoning-effort.test.ts": 5,
|
||||
"lib/ai/__tests__/provider-response-timeout.test.ts": 2,
|
||||
"lib/ai/__tests__/provider.test.ts": 35,
|
||||
"lib/ai/__tests__/resolve-chain.test.ts": 12,
|
||||
"lib/ai/__tests__/server-provider-health.test.ts": 1,
|
||||
"lib/ai/capabilities/__tests__/egress.test.ts": 1,
|
||||
"lib/ai/capabilities/__tests__/gate.test.ts": 1,
|
||||
"lib/ai/coach/__tests__/budget.test.ts": 1,
|
||||
"lib/ai/coach/__tests__/coach-memory-refresh-worker.test.ts": 1,
|
||||
"lib/ai/coach/__tests__/context-reminders.test.ts": 5,
|
||||
"lib/ai/coach/__tests__/conversation-summary.test.ts": 2,
|
||||
"lib/ai/coach/__tests__/facts-deterministic.test.ts": 3,
|
||||
"lib/ai/coach/__tests__/persistence-trail.test.ts": 1,
|
||||
"lib/ai/coach/__tests__/plans.test.ts": 6,
|
||||
"lib/ai/coach/__tests__/reminders.test.ts": 2,
|
||||
"lib/ai/coach/__tests__/series-long-window.test.ts": 1,
|
||||
"lib/ai/coach/__tests__/suggest-gate.test.ts": 1,
|
||||
"lib/ai/coach/__tests__/tools-executor.test.ts": 1,
|
||||
"lib/ai/coach/__tests__/tools-inventory.test.ts": 1,
|
||||
"lib/ai/coach/__tests__/tools-loop.test.ts": 1,
|
||||
"lib/ai/coach/results/__tests__/project.test.ts": 2,
|
||||
"lib/ai/coach/snapshot-blocks/__tests__/core-metrics-pulse-day.test.ts": 2,
|
||||
"lib/ai/coach/turn/__tests__/ledger-reasoning.test.ts": 2,
|
||||
"lib/ai/coach/turn/__tests__/model-stable-prefix.test.ts": 2,
|
||||
"lib/ai/prompts/__tests__/output-language-adoption.test.ts": 1,
|
||||
"lib/ai/safety/__tests__/outbound-screen-guard2.test.ts": 1,
|
||||
"lib/analytics/__tests__/mood-series.test.ts": 5,
|
||||
"lib/analytics/__tests__/sleep-stage-composition.test.ts": 2,
|
||||
"lib/analytics/score/__tests__/config-change-is-not-a-health-event.test.ts": 1,
|
||||
"lib/analytics/score/__tests__/record.test.ts": 2,
|
||||
"lib/apple-health/__tests__/route-import.test.ts": 4,
|
||||
"lib/arrivals/__tests__/reaction-line-shared.test.ts": 1,
|
||||
"lib/auth/__tests__/audit.test.ts": 2,
|
||||
"lib/auth/__tests__/bearer.test.ts": 16,
|
||||
"lib/auth/__tests__/enrollment-proof-second-factor.test.ts": 12,
|
||||
"lib/auth/__tests__/login-alert.test.ts": 9,
|
||||
"lib/auth/__tests__/login-throttle.test.ts": 4,
|
||||
"lib/auth/__tests__/mfa-enrollment.test.ts": 8,
|
||||
"lib/auth/__tests__/native-handoff-flow.test.ts": 10,
|
||||
"lib/auth/__tests__/oidc-native-handoff.test.ts": 13,
|
||||
"lib/auth/__tests__/oidc.test.ts": 5,
|
||||
"lib/auth/__tests__/passkey.test.ts": 7,
|
||||
"lib/auth/__tests__/profile-update-email-limit.test.ts": 5,
|
||||
"lib/auth/__tests__/profile-update.test.ts": 4,
|
||||
"lib/auth/__tests__/session-locale-reconcile.test.ts": 1,
|
||||
"lib/auth/__tests__/session.test.ts": 15,
|
||||
"lib/auth/__tests__/trusted-device.test.ts": 10,
|
||||
"lib/auth/__tests__/webauthn-library-contract.test.ts": 5,
|
||||
"lib/auth/mfa/__tests__/challenge.test.ts": 14,
|
||||
"lib/auth/mfa/__tests__/recovery-codes.test.ts": 8,
|
||||
"lib/auth/mfa/__tests__/verify-factor.test.ts": 1,
|
||||
"lib/cache/__tests__/server-cache.test.ts": 2,
|
||||
"lib/consent/__tests__/receipts.test.ts": 11,
|
||||
"lib/consent/__tests__/web-grant.test.ts": 9,
|
||||
"lib/consent/__tests__/withdrawal.test.ts": 4,
|
||||
"lib/cycle/__tests__/backup.test.ts": 3,
|
||||
"lib/cycle/__tests__/prediction-refresh.test.ts": 9,
|
||||
"lib/daily/__tests__/load-digest.test.ts": 32,
|
||||
"lib/dashboard/__tests__/snapshot.test.ts": 1,
|
||||
"lib/devices/__tests__/revoke.test.ts": 17,
|
||||
"lib/doctor-report/__tests__/leaf-gating-sweep.test.ts": 2,
|
||||
"lib/documents/__tests__/assist-kinds.test.ts": 1,
|
||||
"lib/documents/__tests__/commit.test.ts": 9,
|
||||
"lib/documents/__tests__/fenced-chat-pipeline.test.ts": 6,
|
||||
"lib/documents/__tests__/index-document.test.ts": 23,
|
||||
"lib/documents/__tests__/index-outcome-record.test.ts": 14,
|
||||
"lib/documents/__tests__/prepare-vision-raster-failure.test.ts": 6,
|
||||
"lib/documents/__tests__/provider-order.test.ts": 1,
|
||||
"lib/documents/__tests__/rasterize-pdf-font-data.test.ts": 1,
|
||||
"lib/documents/__tests__/upload-policy.test.ts": 8,
|
||||
"lib/ecg/__tests__/persist-recording.test.ts": 7,
|
||||
"lib/export/__tests__/account-settings-backup.test.ts": 1,
|
||||
"lib/export/__tests__/coach-backup-title.test.ts": 3,
|
||||
"lib/export/__tests__/full-backup-payload.test.ts": 25,
|
||||
"lib/export/__tests__/intraday-profile-backup.test.ts": 3,
|
||||
"lib/export/__tests__/paged-measurements.test.ts": 4,
|
||||
"lib/export/__tests__/profile-backup-decrypt-failures.test.ts": 1,
|
||||
"lib/export/__tests__/records-backup.test.ts": 8,
|
||||
"lib/export/__tests__/reminders-backup-appointment-address.test.ts": 1,
|
||||
"lib/export/__tests__/restore-backup-owner-mismatch.test.ts": 4,
|
||||
"lib/export/__tests__/restore-backup-round-trip.test.ts": 5,
|
||||
"lib/fhir/__tests__/conformance.test.ts": 3,
|
||||
"lib/fhir/__tests__/rest-module-backstop.test.ts": 4,
|
||||
"lib/fitbit/__tests__/sync-dead-token.test.ts": 10,
|
||||
"lib/google-health/__tests__/sync-failsoft.test.ts": 6,
|
||||
"lib/illness/__tests__/red-flag-notify.test.ts": 2,
|
||||
"lib/import/__tests__/record-import-route.test.ts": 1,
|
||||
"lib/insights/__tests__/background-reasoning-wire.test.ts": 2,
|
||||
"lib/insights/__tests__/biomarker-status.test.ts": 23,
|
||||
"lib/insights/__tests__/blood-pressure-status.test.ts": 32,
|
||||
"lib/insights/__tests__/bmi-status.test.ts": 21,
|
||||
"lib/insights/__tests__/briefing-provider-budget.test.ts": 2,
|
||||
"lib/insights/__tests__/comprehensive-briefing-reroll.test.ts": 1,
|
||||
"lib/insights/__tests__/comprehensive-generate-force.test.ts": 2,
|
||||
"lib/insights/__tests__/feature-cache.test.ts": 2,
|
||||
"lib/insights/__tests__/general-status.test.ts": 30,
|
||||
"lib/insights/__tests__/glp1-plateau.test.ts": 17,
|
||||
"lib/insights/__tests__/locale-pipeline.test.ts": 6,
|
||||
"lib/insights/__tests__/medication-compliance-status.test.ts": 19,
|
||||
"lib/insights/__tests__/memory.test.ts": 5,
|
||||
"lib/insights/__tests__/metric-correlation-context.test.ts": 10,
|
||||
"lib/insights/__tests__/metric-status.test.ts": 30,
|
||||
"lib/insights/__tests__/mood-status.test.ts": 14,
|
||||
"lib/insights/__tests__/payload-budget.test.ts": 13,
|
||||
"lib/insights/__tests__/pulse-status-timeout.test.ts": 8,
|
||||
"lib/insights/__tests__/pulse-status.test.ts": 33,
|
||||
"lib/insights/__tests__/status-batch.test.ts": 33,
|
||||
"lib/insights/__tests__/status-cache-delegated.test.ts": 2,
|
||||
"lib/insights/__tests__/status-cache.test.ts": 39,
|
||||
"lib/insights/__tests__/status-provider-budget.test.ts": 4,
|
||||
"lib/insights/__tests__/unit-change-refresh.test.ts": 4,
|
||||
"lib/insights/__tests__/weight-status.test.ts": 33,
|
||||
"lib/insights/derived/__tests__/baseline.test.ts": 3,
|
||||
"lib/insights/derived/__tests__/coach-read.test.ts": 1,
|
||||
"lib/insights/derived/__tests__/derived-assessment-ai.test.ts": 3,
|
||||
"lib/insights/derived/__tests__/recovery-resolve.test.ts": 1,
|
||||
"lib/insights/derived/__tests__/sleep-score-oura-rhythm.test.ts": 2,
|
||||
"lib/insights/derived/__tests__/sleep-score.test.ts": 3,
|
||||
"lib/insights/derived/__tests__/trajectory.test.ts": 2,
|
||||
"lib/insights/derived/__tests__/wellness-scores.test.ts": 2,
|
||||
"lib/insights/narrative/__tests__/period-narrative-deterministic.test.ts": 1,
|
||||
"lib/insights/narrative/__tests__/period-narrative-locale.test.ts": 3,
|
||||
"lib/integrations/__tests__/admin-alert.test.ts": 5,
|
||||
"lib/integrations/__tests__/apple-health-progress.test.ts": 2,
|
||||
"lib/integrations/__tests__/sources-in-use.test.ts": 4,
|
||||
"lib/integrations/__tests__/status.test.ts": 85,
|
||||
"lib/jobs/__tests__/achievement-unlock-sweep.test.ts": 11,
|
||||
"lib/jobs/__tests__/apple-health-import-reconcile.test.ts": 4,
|
||||
"lib/jobs/__tests__/async-state-contract.test.ts": 32,
|
||||
"lib/jobs/__tests__/coach-nudge.test.ts": 15,
|
||||
"lib/jobs/__tests__/coach-reminder-sweep.test.ts": 11,
|
||||
"lib/jobs/__tests__/compaction-tombstone-purge-failure.test.ts": 4,
|
||||
"lib/jobs/__tests__/cycle-prediction-refresh.test.ts": 3,
|
||||
"lib/jobs/__tests__/data-arrival-worker.test.ts": 18,
|
||||
"lib/jobs/__tests__/document-ai-run.test.ts": 1,
|
||||
"lib/jobs/__tests__/document-content-index-backfill.test.ts": 29,
|
||||
"lib/jobs/__tests__/document-index.test.ts": 6,
|
||||
"lib/jobs/__tests__/document-summary-reaper.test.ts": 1,
|
||||
"lib/jobs/__tests__/document-summary.test.ts": 19,
|
||||
"lib/jobs/__tests__/geo-backfill.test.ts": 1,
|
||||
"lib/jobs/__tests__/insight-pregenerate.test.ts": 45,
|
||||
"lib/jobs/__tests__/insight-status-generate.test.ts": 2,
|
||||
"lib/jobs/__tests__/measurement-maintenance-schedule.test.ts": 2,
|
||||
"lib/jobs/__tests__/measurement-reminder-byhour.test.ts": 2,
|
||||
"lib/jobs/__tests__/measurement-reminder.test.ts": 33,
|
||||
"lib/jobs/__tests__/medication-intake-import.test.ts": 1,
|
||||
"lib/jobs/__tests__/medication-reminder-check.test.ts": 27,
|
||||
"lib/jobs/__tests__/mood-reminder.test.ts": 15,
|
||||
"lib/jobs/__tests__/morning-digest-refresh.test.ts": 6,
|
||||
"lib/jobs/__tests__/oauth-callback-claim-cleanup.test.ts": 1,
|
||||
"lib/jobs/__tests__/offhost-backup.test.ts": 5,
|
||||
"lib/jobs/__tests__/offhost-purge.test.ts": 14,
|
||||
"lib/jobs/__tests__/pr-detection.test.ts": 2,
|
||||
"lib/jobs/__tests__/pulse-day-mean-rebuild-state.test.ts": 1,
|
||||
"lib/jobs/__tests__/reminder-satisfy.test.ts": 9,
|
||||
"lib/jobs/__tests__/workout-insight-generate-shared.test.ts": 2,
|
||||
"lib/jobs/__tests__/workout-insight-generate.test.ts": 37,
|
||||
"lib/jobs/__tests__/workout-insight-no-backfill-spend.test.ts": 3,
|
||||
"lib/jobs/reminder/__tests__/queue-retention.test.ts": 2,
|
||||
"lib/jobs/reminder/__tests__/registrar-shared.test.ts": 5,
|
||||
"lib/labs/__tests__/ocr-capability.test.ts": 4,
|
||||
"lib/labs/__tests__/ocr-rate-limit.test.ts": 1,
|
||||
"lib/labs/__tests__/ocr-run.test.ts": 17,
|
||||
"lib/labs/__tests__/reading-conversion.test.ts": 3,
|
||||
"lib/labs/__tests__/resolve-or-mint-biomarker.test.ts": 1,
|
||||
"lib/life-events/__tests__/service.test.ts": 1,
|
||||
"lib/mcp/__tests__/admin-boundary.test.ts": 1,
|
||||
"lib/mcp/__tests__/auth.test.ts": 1,
|
||||
"lib/mcp/__tests__/nutrients-read.test.ts": 6,
|
||||
"lib/mcp/__tests__/prompts.test.ts": 16,
|
||||
"lib/mcp/__tests__/resource-templates.test.ts": 17,
|
||||
"lib/mcp/__tests__/resources.test.ts": 8,
|
||||
"lib/mcp/__tests__/rich-reads.test.ts": 1,
|
||||
"lib/mcp/__tests__/search-fetch.test.ts": 12,
|
||||
"lib/mcp/__tests__/server-resources.test.ts": 3,
|
||||
"lib/mcp/__tests__/server-write-registration.test.ts": 3,
|
||||
"lib/mcp/__tests__/tools.test.ts": 64,
|
||||
"lib/mcp/__tests__/writes.test.ts": 1,
|
||||
"lib/mcp/oauth/__tests__/connections.test.ts": 13,
|
||||
"lib/measurement-reminders/__tests__/resolve.test.ts": 7,
|
||||
"lib/measurement-reminders/__tests__/satisfy.test.ts": 18,
|
||||
"lib/measurements/__tests__/categories.test.ts": 1,
|
||||
"lib/measurements/__tests__/pick-canonical-workout-rows.test.ts": 2,
|
||||
"lib/measurements/__tests__/series-canonical.test.ts": 1,
|
||||
"lib/medications/__tests__/compliance-payload-heatmap.test.ts": 1,
|
||||
"lib/medications/__tests__/compliance-payload-pre-creation.test.ts": 1,
|
||||
"lib/medications/__tests__/intake-import-job-status.test.ts": 2,
|
||||
"lib/medications/__tests__/intake-slot-dedup-converge.test.ts": 12,
|
||||
"lib/medications/__tests__/intake-slot-dedup-inventory.test.ts": 7,
|
||||
"lib/medications/__tests__/intake-slot-dedup-isolation.test.ts": 9,
|
||||
"lib/medications/__tests__/intake-slot-dedup-pin.test.ts": 8,
|
||||
"lib/medications/efficacy/__tests__/segmented-effect.test.ts": 1,
|
||||
"lib/medications/ingredients/__tests__/label-extract.test.ts": 2,
|
||||
"lib/medications/inventory/__tests__/consumption.test.ts": 8,
|
||||
"lib/medications/inventory/__tests__/service.test.ts": 3,
|
||||
"lib/medications/inventory/__tests__/summary.test.ts": 1,
|
||||
"lib/medications/scheduling/__tests__/band-minter.test.ts": 1,
|
||||
"lib/medications/scheduling/__tests__/dose-history.test.ts": 1,
|
||||
"lib/medications/scheduling/__tests__/pin-conflict.test.ts": 2,
|
||||
"lib/modules/__tests__/measurement-scope.test.ts": 3,
|
||||
"lib/notifications/__tests__/admin-locale.test.ts": 7,
|
||||
"lib/notifications/__tests__/client-managed-apns.test.ts": 9,
|
||||
"lib/notifications/__tests__/dispatch-localised-cache.test.ts": 5,
|
||||
"lib/notifications/__tests__/dispatcher-global-channel-switch.test.ts": 8,
|
||||
"lib/notifications/__tests__/dispatcher.test.ts": 23,
|
||||
"lib/notifications/__tests__/event-defaults.test.ts": 2,
|
||||
"lib/notifications/__tests__/reminder-dedup.test.ts": 2,
|
||||
"lib/notifications/senders/__tests__/apns.test.ts": 24,
|
||||
"lib/notifications/senders/__tests__/ntfy.test.ts": 1,
|
||||
"lib/notifications/senders/__tests__/push-attempt-record.test.ts": 14,
|
||||
"lib/notifications/senders/__tests__/webhook.test.ts": 1,
|
||||
"lib/onboarding/__tests__/first-result-config.test.ts": 1,
|
||||
"lib/oura/__tests__/client.test.ts": 3,
|
||||
"lib/polar/__tests__/client.test.ts": 2,
|
||||
"lib/profile/__tests__/health-facts.test.ts": 1,
|
||||
"lib/queries/__tests__/use-medication-list-layout.test.ts": 5,
|
||||
"lib/sharing/__tests__/account-access.test.ts": 5,
|
||||
"lib/sharing/__tests__/record-session-fence.test.ts": 1,
|
||||
"lib/tz/__tests__/resolver.test.ts": 14,
|
||||
"lib/vaccinations/__tests__/document-suggestion.test.ts": 1,
|
||||
"lib/whoop/__tests__/sync-dead-token.test.ts": 6,
|
||||
"lib/withings/__tests__/subscription-state.test.ts": 8,
|
||||
"lib/withings/__tests__/sync-activity.test.ts": 37,
|
||||
"lib/withings/__tests__/sync-ecg.test.ts": 12,
|
||||
"lib/withings/__tests__/sync-measure-watermark.test.ts": 14,
|
||||
"lib/withings/__tests__/sync-plan.test.ts": 13,
|
||||
"lib/withings/__tests__/sync-plausibility.test.ts": 10,
|
||||
"lib/withings/__tests__/sync-sleep.test.ts": 23,
|
||||
"lib/withings/__tests__/sync-typed-classification.test.ts": 1,
|
||||
"lib/workouts/__tests__/climb-backfill.test.ts": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
{
|
||||
"$comment": "tests that read a source file by path, per test file under src/. Maintained by src/__tests__/test-realism-ratchet.test.ts.",
|
||||
"count": 400,
|
||||
"total": 400,
|
||||
"entries": {
|
||||
"__tests__/account-payload-consumer-guard.test.ts": 1,
|
||||
"__tests__/acting-account-boundary-guard.test.ts": 1,
|
||||
"__tests__/admin-data-wipe-completeness.test.ts": 1,
|
||||
"__tests__/aging-fixture-dates-guard.test.ts": 1,
|
||||
"__tests__/ai-call-timeout-guard.test.ts": 1,
|
||||
"__tests__/ai-capability-surface-guard.test.ts": 1,
|
||||
"__tests__/ai-consent-enforcement-guard.test.ts": 1,
|
||||
"__tests__/ai-egress-capability-guard.test.ts": 1,
|
||||
"__tests__/ai-reasoning-admin-guard.test.ts": 1,
|
||||
"__tests__/ai-reasoning-effort-guard.test.ts": 1,
|
||||
"__tests__/analysis-depth-no-ai-guard.test.ts": 1,
|
||||
"__tests__/api-errors-leaf-guard.test.ts": 1,
|
||||
"__tests__/apple-health-unit-audit-guard.test.ts": 1,
|
||||
"__tests__/backfill-preserves-updated-at-guard.test.ts": 1,
|
||||
"__tests__/background-reasoning-inventory.test.ts": 1,
|
||||
"__tests__/background-task-settle-guard.test.ts": 1,
|
||||
"__tests__/backup-plan-classification.test.ts": 1,
|
||||
"__tests__/backup-readers-stream-guard.test.ts": 1,
|
||||
"__tests__/backup-schema-lazy-guard.test.ts": 1,
|
||||
"__tests__/bearer-scope-enforcement-guard.test.ts": 1,
|
||||
"__tests__/client-bundle-server-import-guard.test.ts": 1,
|
||||
"__tests__/client-fetch-limit-guard.test.ts": 1,
|
||||
"__tests__/client-poll-inventory-guard.test.ts": 1,
|
||||
"__tests__/client-random-id-guard.test.ts": 1,
|
||||
"__tests__/client-route-zod-guard.test.ts": 1,
|
||||
"__tests__/client-threshold-literal-guard.test.ts": 1,
|
||||
"__tests__/coach-focus-ring-guard.test.ts": 1,
|
||||
"__tests__/coach-stream-lifecycle-guard.test.ts": 1,
|
||||
"__tests__/coach-usage-owner-columns-guard.test.ts": 1,
|
||||
"__tests__/compose-env-whitelist-guard.test.ts": 1,
|
||||
"__tests__/confirm-destructive-style-guard.test.ts": 1,
|
||||
"__tests__/consent-grant-explicit-guard.test.ts": 1,
|
||||
"__tests__/container-scripts-ship-guard.test.ts": 1,
|
||||
"__tests__/contract-stub-inventory.test.ts": 1,
|
||||
"__tests__/copy-matches-behaviour-guard.test.ts": 1,
|
||||
"__tests__/correlation-single-implementation-guard.test.ts": 1,
|
||||
"__tests__/customize-shortcut-guard.test.ts": 1,
|
||||
"__tests__/cycle-verdict-single-decider-guard.test.ts": 1,
|
||||
"__tests__/daily-reads-refetch-guard.test.ts": 1,
|
||||
"__tests__/dashboard-summary-read-plan-guard.test.ts": 1,
|
||||
"__tests__/dashboard-tile-eligibility-bucket-guard.test.ts": 1,
|
||||
"__tests__/date-order-explicit-formatter-guard.test.ts": 1,
|
||||
"__tests__/datetime-helper-guard.test.ts": 1,
|
||||
"__tests__/dawarich-backup-completeness.test.ts": 1,
|
||||
"__tests__/dense-measurement-read-guard.test.ts": 1,
|
||||
"__tests__/derived-response-consumer-guard.test.ts": 1,
|
||||
"__tests__/destructive-control-guard.test.ts": 1,
|
||||
"__tests__/discovery-matrix-guard.test.ts": 1,
|
||||
"__tests__/display-transform-rounding-guard.test.ts": 1,
|
||||
"__tests__/doctor-report-control-gating-guard.test.ts": 1,
|
||||
"__tests__/document-sources-owner-only-guard.test.ts": 1,
|
||||
"__tests__/domain-write-support-guard.test.ts": 1,
|
||||
"__tests__/dose-ledger-single-source-guard.test.ts": 1,
|
||||
"__tests__/dynamic-key-exhaustiveness.test.ts": 1,
|
||||
"__tests__/e2e-conditional-skip-guard.test.ts": 1,
|
||||
"__tests__/e2e-measurement-gating-guard.test.ts": 1,
|
||||
"__tests__/e2e-route-cleanup-guard.test.ts": 1,
|
||||
"__tests__/e2e-shared-session-guard.test.ts": 1,
|
||||
"__tests__/empty-state-ratchet.test.ts": 1,
|
||||
"__tests__/encounter-reminder-exclusion.test.ts": 1,
|
||||
"__tests__/env-empty-is-unset-guard.test.ts": 1,
|
||||
"__tests__/environment-backup-completeness.test.ts": 1,
|
||||
"__tests__/error-code-catalogue.test.ts": 1,
|
||||
"__tests__/error-display-helper-guard.test.ts": 1,
|
||||
"__tests__/error-level-discipline-guard.test.ts": 1,
|
||||
"__tests__/external-id-stability-guard.test.ts": 1,
|
||||
"__tests__/factor-verification-surface-guard.test.ts": 1,
|
||||
"__tests__/feature-flags-mock-guard.test.ts": 1,
|
||||
"__tests__/greeting-hour-single-source.test.ts": 1,
|
||||
"__tests__/guard-floor-guard.test.ts": 1,
|
||||
"__tests__/healthcheck-boot-storm.test.ts": 1,
|
||||
"__tests__/i18n-call-site-coverage.test.ts": 1,
|
||||
"__tests__/i18n-catalog-cache-key.test.ts": 1,
|
||||
"__tests__/i18n-english-leak-guard.test.ts": 1,
|
||||
"__tests__/i18n-reverse-coverage.test.ts": 1,
|
||||
"__tests__/idempotency-contract-publication.test.ts": 1,
|
||||
"__tests__/inline-action-guard.test.ts": 1,
|
||||
"__tests__/insights-cached-text-access-guard.test.ts": 1,
|
||||
"__tests__/insights-unit-literal-guard.test.ts": 1,
|
||||
"__tests__/intake-toast-single-decision.test.ts": 1,
|
||||
"__tests__/integration-callback-url-guard.test.ts": 1,
|
||||
"__tests__/integration-failure-response-consumer-guard.test.ts": 1,
|
||||
"__tests__/inventory-write-isolation.test.ts": 1,
|
||||
"__tests__/job-locale-resolution-guard.test.ts": 1,
|
||||
"__tests__/job-queue-runtime-guard.test.ts": 1,
|
||||
"__tests__/job-retention-floor-guard.test.ts": 1,
|
||||
"__tests__/key-mismatch-refusal-guard.test.ts": 1,
|
||||
"__tests__/latest-per-type-read-guard.test.ts": 1,
|
||||
"__tests__/linear-fit-single-implementation-guard.test.ts": 1,
|
||||
"__tests__/list-cap-reader-guard.test.ts": 1,
|
||||
"__tests__/list-page-guard.test.ts": 1,
|
||||
"__tests__/malformed-json-status-guard.test.ts": 1,
|
||||
"__tests__/manage-activity-inventory.test.ts": 1,
|
||||
"__tests__/managed-profile-contract.test.ts": 1,
|
||||
"__tests__/measurement-backup-completeness.test.ts": 1,
|
||||
"__tests__/measurement-write-plausibility-guard.test.ts": 1,
|
||||
"__tests__/measurement-write-tail-guard.test.ts": 1,
|
||||
"__tests__/medication-ingredient-backup-completeness.test.ts": 1,
|
||||
"__tests__/medication-intake-tracking-guard.test.ts": 1,
|
||||
"__tests__/memory-block-egress-guard.test.ts": 1,
|
||||
"__tests__/migration-lock-timeout-guard.test.ts": 1,
|
||||
"__tests__/module-gate-record-scoped.test.ts": 1,
|
||||
"__tests__/mood-backup-completeness.test.ts": 1,
|
||||
"__tests__/mood-prediction-write-surface-guard.test.ts": 1,
|
||||
"__tests__/moodlog-removal-guard.test.ts": 1,
|
||||
"__tests__/no-control-bytes-guard.test.ts": 1,
|
||||
"__tests__/no-server-actions-guard.test.ts": 1,
|
||||
"__tests__/oauth-kit-guard.test.ts": 1,
|
||||
"__tests__/oidc-native-error-reasons-documented.test.ts": 1,
|
||||
"__tests__/openapi-account-sharing-components.test.ts": 1,
|
||||
"__tests__/openapi-route-coverage-guard.test.ts": 1,
|
||||
"__tests__/openapi-sharing-denial.test.ts": 1,
|
||||
"__tests__/page-header-h1-guard.test.ts": 1,
|
||||
"__tests__/page-walk-guard.test.ts": 1,
|
||||
"__tests__/password-breach-check-switch.test.ts": 1,
|
||||
"__tests__/prefetch-page-caching-guard.test.ts": 1,
|
||||
"__tests__/prisma-enum-mapping-guard.test.ts": 1,
|
||||
"__tests__/profile-rejection-surface-guard.test.ts": 1,
|
||||
"__tests__/provider-rounding-guard.test.ts": 1,
|
||||
"__tests__/proxy-body-limit.test.ts": 1,
|
||||
"__tests__/proxy-demo-mode-allowlist.test.ts": 1,
|
||||
"__tests__/proxy-page-marker.test.ts": 1,
|
||||
"__tests__/proxy-relative-redirect-guard.test.ts": 1,
|
||||
"__tests__/published-env-reaches-the-container.test.ts": 1,
|
||||
"__tests__/pulse-day-mean-reader-guard.test.ts": 1,
|
||||
"__tests__/pwa-mobile-shell-guard.test.ts": 1,
|
||||
"__tests__/query-error-error-prop-guard.test.ts": 1,
|
||||
"__tests__/read-failure-markup-guard.test.ts": 1,
|
||||
"__tests__/read-over-post-guard.test.ts": 1,
|
||||
"__tests__/recent-proof-surface-guard.test.ts": 1,
|
||||
"__tests__/record-fence-headerless-transport-guard.test.ts": 1,
|
||||
"__tests__/record-session-fence-acceptance-map.test.ts": 1,
|
||||
"__tests__/record-session-fence-compat.test.ts": 1,
|
||||
"__tests__/record-session-fence-guard.test.ts": 1,
|
||||
"__tests__/record-settings-route-guard.test.ts": 1,
|
||||
"__tests__/record-settings-ui.test.tsx": 1,
|
||||
"__tests__/record-write-rate-limit-contract.test.ts": 1,
|
||||
"__tests__/recurrence-course-anchor-guard.test.ts": 1,
|
||||
"__tests__/relation-links-backup-completeness.test.ts": 1,
|
||||
"__tests__/report-selection-egress-guard.test.ts": 1,
|
||||
"__tests__/resolved-field-consumer-guard.test.ts": 1,
|
||||
"__tests__/restore-lock-consolidation-guard.test.ts": 1,
|
||||
"__tests__/rotation-walk-keyset-guard.test.ts": 1,
|
||||
"__tests__/score-pill-guard.test.ts": 1,
|
||||
"__tests__/self-gating-child-frame-guard.test.ts": 1,
|
||||
"__tests__/service-worker-version-anchor.test.ts": 1,
|
||||
"__tests__/service-worker.test.ts": 1,
|
||||
"__tests__/session-surface-guard.test.ts": 1,
|
||||
"__tests__/session-user-projection-guard.test.ts": 1,
|
||||
"__tests__/settings-legacy-routes.test.ts": 1,
|
||||
"__tests__/sharing-accessibility-states.test.tsx": 1,
|
||||
"__tests__/sharing-native-client-contract.test.ts": 1,
|
||||
"__tests__/sharing-record-fence-contract.test.ts": 1,
|
||||
"__tests__/sharing-recoverable-errors.test.tsx": 1,
|
||||
"__tests__/sharing-surface-guard.test.ts": 1,
|
||||
"__tests__/spread-single-implementation-guard.test.ts": 1,
|
||||
"__tests__/sql-now-utc-guard.test.ts": 1,
|
||||
"__tests__/step-up-elevation-guard.test.ts": 1,
|
||||
"__tests__/step-up-web-caller-guard.test.ts": 1,
|
||||
"__tests__/success-affordance-guard.test.ts": 1,
|
||||
"__tests__/sw-security-hardening.test.ts": 1,
|
||||
"__tests__/text-field-focus-guard.test.ts": 1,
|
||||
"__tests__/timezone-explicit-formatter-guard.test.ts": 1,
|
||||
"__tests__/tombstone-read-guard.test.ts": 1,
|
||||
"__tests__/translate-probe-guard.test.ts": 1,
|
||||
"__tests__/ui-code-no-dash-separator.test.ts": 1,
|
||||
"__tests__/unit-preference-display-guard.test.ts": 1,
|
||||
"__tests__/unit-suite-browser-install-guard.test.ts": 1,
|
||||
"__tests__/utc-day-key-allowlist-guard.test.ts": 1,
|
||||
"__tests__/v1341-locale-parity.test.ts": 1,
|
||||
"__tests__/visit-link-guard.test.ts": 1,
|
||||
"__tests__/vorsorge-due-bucket-single-home-guard.test.ts": 1,
|
||||
"__tests__/weight-tile-direction-guard.test.ts": 1,
|
||||
"__tests__/workout-canonical-identity-guard.test.ts": 1,
|
||||
"__tests__/workout-sport-provenance-guard.test.ts": 1,
|
||||
"__tests__/write-rate-limit-guard.test.ts": 1,
|
||||
"__tests__/written-outcome-response-consumer-guard.test.ts": 1,
|
||||
"__tests__/zod-refusal-issue-list-guard.test.ts": 1,
|
||||
"app/__tests__/dashboard-analytics-split.test.ts": 1,
|
||||
"app/__tests__/dashboard-chart-reveal.test.tsx": 1,
|
||||
"app/__tests__/dashboard-demo-checklist.test.ts": 1,
|
||||
"app/__tests__/dashboard-hero-wiring.test.ts": 1,
|
||||
"app/__tests__/dashboard-hrv-rmssd-fallback.test.tsx": 1,
|
||||
"app/__tests__/dashboard-suspense-boundaries.test.ts": 1,
|
||||
"app/__tests__/dashboard-tile-strip-skeleton.test.ts": 1,
|
||||
"app/__tests__/i18n-boot-script-strategy.test.ts": 1,
|
||||
"app/__tests__/insights-b3-wiring.test.ts": 1,
|
||||
"app/__tests__/insights-polish.test.ts": 1,
|
||||
"app/__tests__/not-found-copy.test.ts": 1,
|
||||
"app/__tests__/quick-add-labels.test.ts": 1,
|
||||
"app/__tests__/root-loading-silhouette.test.tsx": 1,
|
||||
"app/api/__tests__/ai-capability-route-inventory.test.ts": 1,
|
||||
"app/api/__tests__/inline-schema-strictness-inventory.test.ts": 1,
|
||||
"app/api/__tests__/module-route-gate-inventory.test.ts": 1,
|
||||
"app/api/__tests__/read-routes-write-nothing.test.ts": 1,
|
||||
"app/api/__tests__/webhook-verb-rate-limit-guard.test.ts": 1,
|
||||
"app/api/auth/native/__tests__/invariants.test.ts": 1,
|
||||
"app/api/auth/passkeys/__tests__/route.test.ts": 1,
|
||||
"app/api/insights/__tests__/coach-route-gate-inventory.test.ts": 1,
|
||||
"app/api/mood-entries/__tests__/bulk-dimension-bounds.test.ts": 1,
|
||||
"app/api/polar/sync/__tests__/route.test.ts": 1,
|
||||
"app/api/settings/__tests__/private-origin-routes.test.ts": 1,
|
||||
"app/api/user/thresholds/__tests__/route.test.ts": 1,
|
||||
"app/insights/__tests__/assessment-spine-order.test.ts": 1,
|
||||
"app/insights/__tests__/explainer-metric-keys.test.ts": 1,
|
||||
"app/insights/__tests__/header-add-pattern.test.ts": 1,
|
||||
"app/insights/__tests__/page-skeletons.test.ts": 1,
|
||||
"app/insights/sleep/__tests__/empty-state-href.test.ts": 1,
|
||||
"app/measurements/__tests__/add-token-resolution.test.ts": 1,
|
||||
"app/medications/__tests__/detail-page-history-surface.test.ts": 1,
|
||||
"components/__tests__/design-consistency-guards.test.tsx": 1,
|
||||
"components/__tests__/failed-read-never-empty.test.tsx": 1,
|
||||
"components/__tests__/motion-reduce-spin-coverage.test.ts": 1,
|
||||
"components/admin/__tests__/admin-shell.test.tsx": 1,
|
||||
"components/admin/__tests__/notification-test-failures.test.ts": 1,
|
||||
"components/charts/__tests__/chart-error-state.test.tsx": 1,
|
||||
"components/charts/__tests__/health-chart-overlay-defaults.test.tsx": 1,
|
||||
"components/charts/__tests__/health-chart-target-link-removed.test.ts": 1,
|
||||
"components/charts/__tests__/mood-chart-polish.test.tsx": 1,
|
||||
"components/charts/__tests__/touch-action-guard.test.ts": 1,
|
||||
"components/cycle/__tests__/use-cycle-ring-has-dial.test.tsx": 1,
|
||||
"components/dashboard/__tests__/medication-intake-quick-add.test.tsx": 1,
|
||||
"components/dashboard/__tests__/tile-destinations.test.ts": 1,
|
||||
"components/day/__tests__/day-door-coverage-guard.test.ts": 1,
|
||||
"components/day/__tests__/day-not-here-guard.test.ts": 1,
|
||||
"components/documents/__tests__/document-coach-launch-wiring.test.ts": 1,
|
||||
"components/documents/__tests__/document-detail-download.test.ts": 1,
|
||||
"components/documents/__tests__/documents-delete-confirm-guard.test.ts": 1,
|
||||
"components/forms/__tests__/use-seeded-form-dismissal.test.ts": 1,
|
||||
"components/i18n/__tests__/maintainership-banner.test.tsx": 1,
|
||||
"components/insights/__tests__/coach-metric-scope-inventory.test.ts": 1,
|
||||
"components/insights/__tests__/correlation-card-skeleton.test.ts": 1,
|
||||
"components/insights/__tests__/health-score-card-geometry.test.tsx": 1,
|
||||
"components/insights/__tests__/health-score-coverage-axis-guard.test.tsx": 1,
|
||||
"components/insights/__tests__/insight-section-card.test.tsx": 1,
|
||||
"components/insights/__tests__/sleep-overview-empty-href.test.ts": 1,
|
||||
"components/insights/__tests__/tile-header-wrap.test.tsx": 1,
|
||||
"components/insights/coach-panel/__tests__/coach-settings-overlay.test.tsx": 1,
|
||||
"components/insights/coach-panel/__tests__/conversations-panel-fold.test.tsx": 1,
|
||||
"components/insights/coach-panel/__tests__/message-thread.test.tsx": 1,
|
||||
"components/insights/coach-panel/__tests__/open-at-end.test.ts": 1,
|
||||
"components/insights/coach-panel/__tests__/panel-touch-targets.test.ts": 1,
|
||||
"components/insights/coach-panel/__tests__/scroll-to-bottom-button.test.tsx": 1,
|
||||
"components/insights/mood/__tests__/mood-notes-timeline-removed.test.ts": 1,
|
||||
"components/layout/__tests__/auth-shell-coach-hoist.test.ts": 1,
|
||||
"components/layout/__tests__/auth-shell-shared-record-doors.test.tsx": 1,
|
||||
"components/layout/__tests__/offline-banner.test.tsx": 1,
|
||||
"components/layout/__tests__/sidebar-nav.test.tsx": 1,
|
||||
"components/measurements/__tests__/duration-read-as.test.tsx": 1,
|
||||
"components/measurements/__tests__/measurement-form-glucose-unit.test.tsx": 1,
|
||||
"components/measurements/__tests__/measurement-list-glucose-unit.test.tsx": 1,
|
||||
"components/medications/__tests__/add-inventory-dialog.test.tsx": 1,
|
||||
"components/medications/__tests__/detail-page-cascade.test.tsx": 1,
|
||||
"components/medications/__tests__/inventory-section-dates.test.tsx": 1,
|
||||
"components/medications/__tests__/log-injection-site-dialog.test.tsx": 1,
|
||||
"components/medications/__tests__/medication-order-editor.test.tsx": 1,
|
||||
"components/medications/__tests__/medication-table.test.tsx": 1,
|
||||
"components/medications/__tests__/take-all-due.test.tsx": 1,
|
||||
"components/mood/__tests__/linked-day-timezone.test.ts": 1,
|
||||
"components/mood/__tests__/mood-note-fulltext.test.tsx": 1,
|
||||
"components/mood/__tests__/mood-qol-fixes.test.tsx": 1,
|
||||
"components/onboarding/__tests__/tour-launcher-gate.test.ts": 1,
|
||||
"components/records/__tests__/emergency-profile-save.test.tsx": 1,
|
||||
"components/settings/__tests__/action-feedback.test.ts": 1,
|
||||
"components/settings/__tests__/card-description-length.test.ts": 1,
|
||||
"components/settings/__tests__/confirm-style-guard.test.ts": 1,
|
||||
"components/settings/__tests__/dashboard-layout-snapshot-invalidation.test.ts": 1,
|
||||
"components/settings/__tests__/destructive-controls-confirm.test.tsx": 1,
|
||||
"components/settings/__tests__/glucose-unit-select.test.tsx": 1,
|
||||
"components/settings/__tests__/kept-grant-revoke-confirm.test.ts": 1,
|
||||
"components/settings/__tests__/module-description-length.test.ts": 1,
|
||||
"components/settings/__tests__/mood-reminder-card.test.tsx": 1,
|
||||
"components/settings/__tests__/oauth-return-outcome.test.ts": 1,
|
||||
"components/settings/__tests__/one-piece-anatomy.test.tsx": 1,
|
||||
"components/settings/__tests__/password-card-placement.test.tsx": 1,
|
||||
"components/settings/__tests__/save-model-guard.test.ts": 1,
|
||||
"components/settings/__tests__/score-refusal-consumer-guard.test.ts": 1,
|
||||
"components/settings/__tests__/settings-card-header-coverage.test.ts": 1,
|
||||
"components/settings/__tests__/settings-shell.test.tsx": 1,
|
||||
"components/settings/__tests__/test-connection-button-response.test.tsx": 1,
|
||||
"components/settings/__tests__/token-revoke-labels.test.ts": 1,
|
||||
"components/settings/access/__tests__/grant-access-level.test.tsx": 1,
|
||||
"components/settings/access/__tests__/grant-invite-expiry.test.tsx": 1,
|
||||
"components/settings/access/__tests__/grant-invite-scope.test.tsx": 1,
|
||||
"components/settings/access/__tests__/managed-profile-affordances.test.tsx": 1,
|
||||
"components/settings/account-section/__tests__/account-section-field-error-clearing.test.ts": 1,
|
||||
"components/settings/account-section/__tests__/display-name-field.test.tsx": 1,
|
||||
"components/settings/account-section/__tests__/display-preferences-card.test.tsx": 1,
|
||||
"components/settings/import-panel/__tests__/import-docs-link.test.ts": 1,
|
||||
"components/settings/integrations/__tests__/cards-render-health.test.ts": 1,
|
||||
"components/settings/integrations/__tests__/google-health-card.test.tsx": 1,
|
||||
"components/settings/security-section/__tests__/factor-enrollment-reauth.test.tsx": 1,
|
||||
"components/settings/security-section/__tests__/passkey-enrollment-reauth.test.tsx": 1,
|
||||
"components/settings/security-section/__tests__/passkey-removal-step-up.test.tsx": 1,
|
||||
"components/settings/security-section/__tests__/recent-proof-callers.test.tsx": 1,
|
||||
"components/settings/security-section/__tests__/totp-card.test.tsx": 1,
|
||||
"components/timeline/__tests__/label-keys.test.ts": 1,
|
||||
"components/timeline/__tests__/no-template-key-guard.test.ts": 1,
|
||||
"components/ui/__tests__/card-density.test.tsx": 1,
|
||||
"components/ui/__tests__/card-header-rows.test.ts": 1,
|
||||
"components/ui/__tests__/filter-bar.test.tsx": 1,
|
||||
"components/ui/__tests__/segmented.test.tsx": 1,
|
||||
"hooks/__tests__/use-insight-status.test.ts": 1,
|
||||
"lib/__tests__/geo-offline-detection.test.ts": 1,
|
||||
"lib/__tests__/query-keys.test.ts": 1,
|
||||
"lib/__tests__/ssrf-pin-inventory.test.ts": 1,
|
||||
"lib/ai/__tests__/provider-state-never-persisted.test.ts": 1,
|
||||
"lib/ai/coach/__tests__/coach-availability-window-honesty.test.ts": 1,
|
||||
"lib/ai/coach/__tests__/coach-history-reach-guard.test.ts": 1,
|
||||
"lib/ai/coach/__tests__/coach-locale-no-german-default.test.ts": 1,
|
||||
"lib/ai/coach/__tests__/data-field-injection.test.ts": 1,
|
||||
"lib/ai/coach/__tests__/snapshot.test.ts": 1,
|
||||
"lib/ai/coach/memory/__tests__/contract.test.ts": 1,
|
||||
"lib/ai/coach/tools/__tests__/turn-budget.test.ts": 1,
|
||||
"lib/ai/coach/turn/__tests__/steps.test.ts": 1,
|
||||
"lib/analytics/__tests__/glucose-panel-window.test.ts": 1,
|
||||
"lib/analytics/compliance/__tests__/as-needed-compliance-exclusion.test.ts": 1,
|
||||
"lib/analytics/mood-prognosis/__tests__/gates.test.ts": 1,
|
||||
"lib/analytics/score/__tests__/reference-citation-guard.test.ts": 1,
|
||||
"lib/auth/__tests__/step-up.test.ts": 1,
|
||||
"lib/charts/__tests__/axis-unit.test.ts": 1,
|
||||
"lib/charts/__tests__/data-ready-latch.test.ts": 1,
|
||||
"lib/charts/__tests__/fetch-window.test.ts": 1,
|
||||
"lib/command-palette/__tests__/settings-cards.test.ts": 1,
|
||||
"lib/crypto/__tests__/encrypted-column-writers.test.ts": 1,
|
||||
"lib/crypto/__tests__/encrypted-columns.test.ts": 1,
|
||||
"lib/dashboard/__tests__/snapshot.test.ts": 1,
|
||||
"lib/documents/__tests__/fenced-chat-module-graph.test.ts": 1,
|
||||
"lib/documents/__tests__/rasterize-pdf-font-data.test.ts": 1,
|
||||
"lib/export/__tests__/full-backup-stream-heap-gauge.test.ts": 1,
|
||||
"lib/export/__tests__/intraday-profile-backup.test.ts": 1,
|
||||
"lib/feature-flags/__tests__/no-dead-assistant-flag.test.ts": 1,
|
||||
"lib/fitbit/__tests__/sync-module-graph.test.ts": 1,
|
||||
"lib/format/__tests__/bytes.test.ts": 1,
|
||||
"lib/google-health/__tests__/sync-module-graph.test.ts": 1,
|
||||
"lib/http/__tests__/deprecation.test.ts": 1,
|
||||
"lib/i18n/__tests__/locale-first-paint-single-write.test.tsx": 1,
|
||||
"lib/i18n/__tests__/plural-locale-completeness.test.ts": 1,
|
||||
"lib/import/__tests__/record-import-rows.test.ts": 1,
|
||||
"lib/insights/__tests__/prompt-soft-delete-filter.test.ts": 1,
|
||||
"lib/insights/eval/__tests__/tone-harness.test.ts": 1,
|
||||
"lib/jobs/__tests__/ai-job-capability-inventory.test.ts": 1,
|
||||
"lib/jobs/__tests__/apple-health-import-ecg.test.ts": 1,
|
||||
"lib/jobs/__tests__/apple-health-import-export-date.test.ts": 1,
|
||||
"lib/jobs/__tests__/apple-health-import-reconcile.test.ts": 1,
|
||||
"lib/jobs/__tests__/apple-health-import-send-policy.test.ts": 1,
|
||||
"lib/jobs/__tests__/coach-memory-refresh-queue.test.ts": 1,
|
||||
"lib/jobs/__tests__/coach-message-cleanup-queue.test.ts": 1,
|
||||
"lib/jobs/__tests__/cumulative-pr-rederive-queue.test.ts": 1,
|
||||
"lib/jobs/__tests__/cycle-reminder-queue.test.ts": 1,
|
||||
"lib/jobs/__tests__/daily-briefing.test.ts": 1,
|
||||
"lib/jobs/__tests__/data-arrival-provider-isolation.test.ts": 1,
|
||||
"lib/jobs/__tests__/data-arrival-queue.test.ts": 1,
|
||||
"lib/jobs/__tests__/data-backup-send-policy.test.ts": 1,
|
||||
"lib/jobs/__tests__/drain-cumulative-queue.test.ts": 1,
|
||||
"lib/jobs/__tests__/fitbit-queues.test.ts": 1,
|
||||
"lib/jobs/__tests__/geo-backfill.test.ts": 1,
|
||||
"lib/jobs/__tests__/geolite2-fetch-queue.test.ts": 1,
|
||||
"lib/jobs/__tests__/google-health-queues.test.ts": 1,
|
||||
"lib/jobs/__tests__/insight-pregenerate.test.ts": 1,
|
||||
"lib/jobs/__tests__/insight-status-generate.test.ts": 1,
|
||||
"lib/jobs/__tests__/intake-slot-dedup-queue.test.ts": 1,
|
||||
"lib/jobs/__tests__/integration-backfill-admission.test.ts": 1,
|
||||
"lib/jobs/__tests__/job-failures-retention.test.ts": 1,
|
||||
"lib/jobs/__tests__/job-outcome-source-inventory.test.ts": 1,
|
||||
"lib/jobs/__tests__/mcp-token-cleanup-queue.test.ts": 1,
|
||||
"lib/jobs/__tests__/mean-consolidation-queue.test.ts": 1,
|
||||
"lib/jobs/__tests__/measurement-reminder-queue.test.ts": 1,
|
||||
"lib/jobs/__tests__/measurement-tombstone-cleanup.test.ts": 1,
|
||||
"lib/jobs/__tests__/nightscout-queues.test.ts": 1,
|
||||
"lib/jobs/__tests__/period-narrative-warm.test.ts": 1,
|
||||
"lib/jobs/__tests__/queue-policy-tables.test.ts": 1,
|
||||
"lib/jobs/__tests__/read-side-effect-queues.test.ts": 1,
|
||||
"lib/jobs/__tests__/recovery-score-queue.test.ts": 1,
|
||||
"lib/jobs/__tests__/sleep-timeline-queues.test.ts": 1,
|
||||
"lib/jobs/__tests__/status-cron-candidates.test.ts": 1,
|
||||
"lib/jobs/__tests__/step-consolidation-repair-queue.test.ts": 1,
|
||||
"lib/jobs/__tests__/stress-strain-retention-queue.test.ts": 1,
|
||||
"lib/jobs/__tests__/subsystem-surface.test.ts": 1,
|
||||
"lib/jobs/__tests__/tls-pin-monitor-queue.test.ts": 1,
|
||||
"lib/jobs/__tests__/whoop-queues.test.ts": 1,
|
||||
"lib/jobs/__tests__/withings-ecg-worker.test.ts": 1,
|
||||
"lib/jobs/__tests__/withings-queues.test.ts": 1,
|
||||
"lib/jobs/__tests__/workout-insight-queue.test.ts": 1,
|
||||
"lib/jobs/reminder/__tests__/integration-cohort-outcomes.test.ts": 1,
|
||||
"lib/labs/__tests__/local-ocr.test.ts": 1,
|
||||
"lib/links/__tests__/link-surface-guard.test.ts": 1,
|
||||
"lib/links/__tests__/relation-matrix.test.ts": 1,
|
||||
"lib/medications/__tests__/auto-mark-taken.test.ts": 1,
|
||||
"lib/medications/__tests__/glp1-knowledge-drift.test.ts": 1,
|
||||
"lib/medications/inventory/__tests__/summary.test.ts": 1,
|
||||
"lib/modules/__tests__/surface-module-registry.test.ts": 1,
|
||||
"lib/mood/__tests__/context-vocabulary.test.ts": 1,
|
||||
"lib/notifications/__tests__/event-defaults.test.ts": 1,
|
||||
"lib/pwa/__tests__/sw-cache-policy.test.ts": 1,
|
||||
"lib/record-activity/__tests__/verb-map-covers-routes.test.ts": 1,
|
||||
"lib/sharing/__tests__/record-session-fence.test.ts": 1,
|
||||
"lib/sources/__tests__/source-priority-whoop-applehealth.test.ts": 1,
|
||||
"lib/whoop/__tests__/sync-module-graph.test.ts": 1,
|
||||
"lib/whoop/__tests__/sync-user-whoop.test.ts": 1
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -171,5 +171,16 @@ export function readDeclaredSchema(
|
||||
};
|
||||
});
|
||||
|
||||
// Every consumer compares this against an allowlist or a "none of these"
|
||||
// rule, which an empty parse satisfies. For the real schema, this little is
|
||||
// a broken read (a moved file, a block regex that stopped matching), not a
|
||||
// schema. A synthetic schema a test writes itself is exempt.
|
||||
if (schemaPath === SCHEMA_PATH && (models.length < 50 || enums.length < 10)) {
|
||||
throw new Error(
|
||||
`readDeclaredSchema(${schemaPath}): parsed ${models.length} model(s) and ` +
|
||||
`${enums.length} enum(s), below the floor of 50 and 10. Fix the parser ` +
|
||||
`before trusting a guard built on it.`,
|
||||
);
|
||||
}
|
||||
return { enums, models };
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* A ratchet: a committed baseline of counts that may only fall.
|
||||
*
|
||||
* Each ratchet guard measures something per key (lines per file, `as never`
|
||||
* per test file, one per file on a list) and compares it with a JSON baseline
|
||||
* under `src/__tests__/baselines/`. Three things fail:
|
||||
*
|
||||
* - a key that is not in the baseline (a new offender);
|
||||
* - a key whose value rose above its baseline (an offender that grew);
|
||||
* - a key that fell below its baseline by more than the ratchet's `slack`,
|
||||
* or disappeared (an improvement nobody wrote down).
|
||||
*
|
||||
* The third is what makes it a ratchet rather than a ceiling: an improvement
|
||||
* that is not written back leaves room for the next regression to hide in.
|
||||
* Writing back is one command, printed in every failure:
|
||||
*
|
||||
* RATCHET_WRITE=1 pnpm vitest run <guard file>
|
||||
*
|
||||
* which rewrites the baseline with the lower of the recorded and the
|
||||
* measured value for every key, drops keys that reached zero, and never
|
||||
* raises anything. Raising a value, or adding a key, is a hand edit of the
|
||||
* JSON in a reviewed change, with the reason in the PR.
|
||||
*/
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
export const BASELINE_DIR = join(process.cwd(), "src/__tests__/baselines");
|
||||
|
||||
export interface RatchetBaseline {
|
||||
$comment: string;
|
||||
/** Number of keys, kept in the file so a reader sees the headline. */
|
||||
count: number;
|
||||
/** Sum of `entries`. */
|
||||
total: number;
|
||||
entries: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface RatchetResult {
|
||||
count: number;
|
||||
total: number;
|
||||
baselineCount: number;
|
||||
baselineTotal: number;
|
||||
/** New keys and keys above their baseline, as readable lines. */
|
||||
grew: string[];
|
||||
/** Keys that fell below their baseline beyond the slack, or vanished. */
|
||||
stale: string[];
|
||||
}
|
||||
|
||||
function sum(entries: Record<string, number>): number {
|
||||
return Object.values(entries).reduce((a, b) => a + b, 0);
|
||||
}
|
||||
|
||||
export function readBaseline(file: string): RatchetBaseline {
|
||||
return JSON.parse(
|
||||
readFileSync(join(BASELINE_DIR, file), "utf8"),
|
||||
) as RatchetBaseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare measured counts against the baseline in `file`. With
|
||||
* `RATCHET_WRITE=1` in the environment the lower numbers are written back
|
||||
* first, so the comparison that follows passes on everything but growth.
|
||||
*/
|
||||
export function runRatchet(
|
||||
file: string,
|
||||
measured: Record<string, number>,
|
||||
options: { slack?: number; comment?: string } = {},
|
||||
): RatchetResult {
|
||||
const slack = options.slack ?? 0;
|
||||
const current = Object.fromEntries(
|
||||
Object.entries(measured).filter(([, v]) => v > 0),
|
||||
);
|
||||
const write = process.env.RATCHET_WRITE === "1";
|
||||
// The one case where writing records the measured numbers as they are: a
|
||||
// baseline that does not exist yet. Deleting a baseline to re-seed it
|
||||
// higher shows up as a whole-file diff in review.
|
||||
if (write && !existsSync(join(BASELINE_DIR, file))) {
|
||||
mkdirSync(BASELINE_DIR, { recursive: true });
|
||||
writeFileSync(
|
||||
join(BASELINE_DIR, file),
|
||||
`${JSON.stringify({ $comment: options.comment ?? "", count: 0, total: 0, entries: current }, null, 2)}\n`,
|
||||
);
|
||||
}
|
||||
let baseline = readBaseline(file);
|
||||
|
||||
if (write) {
|
||||
const lowered: Record<string, number> = {};
|
||||
for (const [key, recorded] of Object.entries(baseline.entries)) {
|
||||
const now = current[key] ?? 0;
|
||||
const value = Math.min(recorded, now);
|
||||
if (value > 0) lowered[key] = value;
|
||||
}
|
||||
const sorted = Object.fromEntries(
|
||||
Object.entries(lowered).sort(([a], [b]) => a.localeCompare(b)),
|
||||
);
|
||||
baseline = {
|
||||
$comment: baseline.$comment,
|
||||
count: Object.keys(sorted).length,
|
||||
total: sum(sorted),
|
||||
entries: sorted,
|
||||
};
|
||||
writeFileSync(
|
||||
join(BASELINE_DIR, file),
|
||||
`${JSON.stringify(baseline, null, 2)}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
const grew: string[] = [];
|
||||
const stale: string[] = [];
|
||||
for (const [key, value] of Object.entries(current)) {
|
||||
const recorded = baseline.entries[key];
|
||||
if (recorded === undefined) grew.push(`${key}: new, ${value}`);
|
||||
else if (value > recorded) grew.push(`${key}: ${recorded} → ${value}`);
|
||||
}
|
||||
for (const [key, recorded] of Object.entries(baseline.entries)) {
|
||||
const value = current[key] ?? 0;
|
||||
if (value < recorded - slack) stale.push(`${key}: ${recorded} → ${value}`);
|
||||
}
|
||||
return {
|
||||
count: Object.keys(current).length,
|
||||
total: sum(current),
|
||||
baselineCount: baseline.count,
|
||||
baselineTotal: baseline.total,
|
||||
grew: grew.sort(),
|
||||
stale: stale.sort(),
|
||||
};
|
||||
}
|
||||
|
||||
export function writeBackHint(guardFile: string): string {
|
||||
return (
|
||||
`The baseline records a higher number than the tree now has. Write the ` +
|
||||
`lower numbers back so the ratchet tightens: ` +
|
||||
`RATCHET_WRITE=1 pnpm vitest run ${guardFile}`
|
||||
);
|
||||
}
|
||||
|
||||
/** Read a baseline of another shape than counts per key, or null if absent. */
|
||||
export function readBaselineJson<T>(file: string): T | null {
|
||||
const path = join(BASELINE_DIR, file);
|
||||
return existsSync(path)
|
||||
? (JSON.parse(readFileSync(path, "utf8")) as T)
|
||||
: null;
|
||||
}
|
||||
|
||||
export function writeBaselineJson(file: string, value: unknown): void {
|
||||
mkdirSync(BASELINE_DIR, { recursive: true });
|
||||
writeFileSync(
|
||||
join(BASELINE_DIR, file),
|
||||
`${JSON.stringify(value, null, 2)}\n`,
|
||||
);
|
||||
}
|
||||
@@ -50,7 +50,7 @@
|
||||
* 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, readFileSync } from "node:fs";
|
||||
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
||||
import { join, sep } from "node:path";
|
||||
|
||||
export function walkSourceFiles(
|
||||
@@ -229,3 +229,82 @@ export function scanSourceMatches(
|
||||
options.matchFloor,
|
||||
) as SourceMatch[];
|
||||
}
|
||||
|
||||
/** Something a file must still contain for a read of it to mean anything. */
|
||||
export type SourceAnchor = string | RegExp;
|
||||
|
||||
function anchorCount(text: string, anchor: SourceAnchor): number {
|
||||
if (typeof anchor === "string") return text.split(anchor).length - 1;
|
||||
const flags = anchor.flags.includes("g") ? anchor.flags : `${anchor.flags}g`;
|
||||
return [...text.matchAll(new RegExp(anchor.source, flags))].length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read one source file by path, and prove it is still the file the guard is
|
||||
* about.
|
||||
*
|
||||
* A guard that reads a fixed path and asserts that something is ABSENT
|
||||
* (`expect(source).not.toMatch(…)`) is green on a file that has been split:
|
||||
* the code it watched moved next door, the old path holds whatever is left,
|
||||
* and the absence is true for the wrong reason. A missing file at least
|
||||
* throws; a file that kept its name but lost its contents does not.
|
||||
*
|
||||
* So the caller names at least one anchor: a string or pattern the watched
|
||||
* code itself carries (the function the guard is about, the call it pins).
|
||||
* Every anchor must match at least once, or this throws and names the file,
|
||||
* which is the moment to point the guard at the new home (or at a folder
|
||||
* with `readSourceFolder`). `rel` is relative to the repository root.
|
||||
*/
|
||||
export function readSource(
|
||||
rel: string,
|
||||
anchors: SourceAnchor | readonly SourceAnchor[],
|
||||
): string {
|
||||
const list = (Array.isArray(anchors) ? anchors : [anchors]) as SourceAnchor[];
|
||||
if (list.length === 0) {
|
||||
throw new Error(
|
||||
`readSource(${rel}): name at least one anchor the watched code carries.`,
|
||||
);
|
||||
}
|
||||
const path = join(process.cwd(), rel);
|
||||
if (!existsSync(path)) {
|
||||
throw new Error(
|
||||
`readSource(${rel}): the file does not exist. If it was split or moved, ` +
|
||||
`point the guard at the new location or scan the folder with ` +
|
||||
`readSourceFolder and a floor.`,
|
||||
);
|
||||
}
|
||||
const text = readFileSync(path, "utf8");
|
||||
const lost = list.filter((anchor) => anchorCount(text, anchor) === 0);
|
||||
if (lost.length > 0) {
|
||||
throw new Error(
|
||||
`readSource(${rel}): the file no longer contains ${lost
|
||||
.map(String)
|
||||
.join(", ")}. The code this guard watches has moved; a negative ` +
|
||||
`assertion on this file would now pass for the wrong reason.`,
|
||||
);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read every source file under a folder (relative to the repository root),
|
||||
* with a floor on how many it found: the folder-scan form of `readSource`
|
||||
* for a guard whose subject was split into several files. Returns
|
||||
* `[path relative to the folder, text]` pairs, sorted.
|
||||
*/
|
||||
export function readSourceFolder(
|
||||
relDir: string,
|
||||
options: {
|
||||
floor: number;
|
||||
include?: (rel: string) => boolean;
|
||||
extensions?: readonly string[];
|
||||
},
|
||||
): Array<[string, string]> {
|
||||
const root = join(process.cwd(), relDir);
|
||||
const files = walkSourceFiles(root, {
|
||||
floor: options.floor,
|
||||
extensions: options.extensions,
|
||||
}).filter(options.include ?? (() => true));
|
||||
requireFloor(`readSourceFolder(${relDir})`, files, options.floor);
|
||||
return files.map((rel) => [rel, readFileSync(join(root, rel), "utf8")]);
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
/**
|
||||
* Measurements over the production source tree, shared by the ratchet
|
||||
* guards: which files count as production code, how long each file and each
|
||||
* function is, and the value-import graph between modules.
|
||||
*
|
||||
* Everything is read through the TypeScript compiler, not grepped, so a
|
||||
* function is a function node (declaration, expression, arrow, method,
|
||||
* constructor, accessor) and an import's kind is the parser's answer, not a
|
||||
* regex's guess. `typescript` is already a dependency; nothing new is pulled
|
||||
* in.
|
||||
*
|
||||
* Production code means every `.ts`/`.tsx` file under `src/` outside
|
||||
* `src/generated/`, test files, `__tests__/`, `__mocks__/`, `test-utils` and
|
||||
* declaration files. The walk goes through `walkSourceFiles`, so dot-prefixed
|
||||
* directories (`app/.well-known/`) are included and an empty walk throws.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import ts from "typescript";
|
||||
|
||||
import { requireFloor, walkSourceFiles } from "./source-files";
|
||||
|
||||
export const SRC = join(process.cwd(), "src");
|
||||
|
||||
const TEST_PATH =
|
||||
/(^|\/)__tests__\/|(^|\/)__mocks__\/|\.(test|spec)\.tsx?$|(^|\/)test-utils(\/|\.)/;
|
||||
|
||||
export function isTestPath(rel: string): boolean {
|
||||
return TEST_PATH.test(rel);
|
||||
}
|
||||
|
||||
/** Production source files, relative to `src/`, sorted. */
|
||||
export function productionSources(): string[] {
|
||||
const files = walkSourceFiles(SRC, { floor: 3000 }).filter(
|
||||
(rel) =>
|
||||
!rel.startsWith("generated/") &&
|
||||
!rel.endsWith(".d.ts") &&
|
||||
!isTestPath(rel),
|
||||
);
|
||||
return requireFloor("production source files", files, 3000) as string[];
|
||||
}
|
||||
|
||||
/** Test files under `src/`, relative to `src/`, sorted. */
|
||||
export function testSources(): string[] {
|
||||
const files = walkSourceFiles(SRC, { floor: 3000 }).filter(
|
||||
(rel) => !rel.startsWith("generated/") && /\.(test|spec)\.tsx?$/.test(rel),
|
||||
);
|
||||
return requireFloor("test files", files, 2000) as string[];
|
||||
}
|
||||
|
||||
export function readSrc(rel: string): string {
|
||||
return readFileSync(join(SRC, rel), "utf8");
|
||||
}
|
||||
|
||||
/** Line count the way an editor shows it: a trailing newline adds none. */
|
||||
export function lineCount(text: string): number {
|
||||
if (text.length === 0) return 0;
|
||||
const lines = text.split("\n").length;
|
||||
return text.endsWith("\n") ? lines - 1 : lines;
|
||||
}
|
||||
|
||||
export function parse(rel: string, text: string): ts.SourceFile {
|
||||
return ts.createSourceFile(
|
||||
rel,
|
||||
text,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
rel.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
||||
);
|
||||
}
|
||||
|
||||
type FunctionNode =
|
||||
| ts.FunctionDeclaration
|
||||
| ts.FunctionExpression
|
||||
| ts.ArrowFunction
|
||||
| ts.MethodDeclaration
|
||||
| ts.ConstructorDeclaration
|
||||
| ts.GetAccessorDeclaration
|
||||
| ts.SetAccessorDeclaration;
|
||||
|
||||
function isFunctionNode(node: ts.Node): node is FunctionNode {
|
||||
return (
|
||||
ts.isFunctionDeclaration(node) ||
|
||||
ts.isFunctionExpression(node) ||
|
||||
ts.isArrowFunction(node) ||
|
||||
ts.isMethodDeclaration(node) ||
|
||||
ts.isConstructorDeclaration(node) ||
|
||||
ts.isGetAccessorDeclaration(node) ||
|
||||
ts.isSetAccessorDeclaration(node)
|
||||
);
|
||||
}
|
||||
|
||||
function propertyNameText(name: ts.PropertyName): string | null {
|
||||
if (
|
||||
ts.isIdentifier(name) ||
|
||||
ts.isStringLiteral(name) ||
|
||||
ts.isNumericLiteral(name) ||
|
||||
ts.isPrivateIdentifier(name)
|
||||
) {
|
||||
return name.text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function calleeText(call: ts.CallExpression, sf: ts.SourceFile): string {
|
||||
const text = call.expression.getText(sf).replace(/\s+/g, "");
|
||||
return text.length > 40 ? `${text.slice(0, 40)}…` : text;
|
||||
}
|
||||
|
||||
/**
|
||||
* A readable, stable name for a function node: its own name, the variable or
|
||||
* property it is assigned to, or the call it is passed to
|
||||
* (`apiHandler(arg)`, `$transaction(arg)`). Positions are deliberately not
|
||||
* part of it, so an edit above a function does not rename it.
|
||||
*/
|
||||
function ownName(node: FunctionNode, sf: ts.SourceFile): string {
|
||||
if (ts.isConstructorDeclaration(node)) return "constructor";
|
||||
if (
|
||||
(ts.isFunctionDeclaration(node) ||
|
||||
ts.isFunctionExpression(node) ||
|
||||
ts.isMethodDeclaration(node) ||
|
||||
ts.isGetAccessorDeclaration(node) ||
|
||||
ts.isSetAccessorDeclaration(node)) &&
|
||||
node.name
|
||||
) {
|
||||
const text = ts.isIdentifier(node.name)
|
||||
? node.name.text
|
||||
: propertyNameText(node.name as ts.PropertyName);
|
||||
if (text) return text;
|
||||
}
|
||||
const parent = node.parent;
|
||||
if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
|
||||
return parent.name.text;
|
||||
}
|
||||
if (ts.isPropertyAssignment(parent)) {
|
||||
return propertyNameText(parent.name) ?? "(property)";
|
||||
}
|
||||
if (ts.isPropertyDeclaration(parent)) {
|
||||
return propertyNameText(parent.name) ?? "(property)";
|
||||
}
|
||||
if (ts.isExportAssignment(parent)) return "default";
|
||||
if (ts.isCallExpression(parent)) return `${calleeText(parent, sf)}(arg)`;
|
||||
if (ts.isJsxExpression(parent)) return "(jsx callback)";
|
||||
return "(anonymous)";
|
||||
}
|
||||
|
||||
export interface MeasuredFunction {
|
||||
/** `outer > inner` chain of names, unique within the file. */
|
||||
name: string;
|
||||
/** 1-based first line. */
|
||||
line: number;
|
||||
lines: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every function in the file with its length in lines, nested functions
|
||||
* included (a 2,800-line function holding a 2,100-line callback reports
|
||||
* both). Names that collide within a file get a `#2`, `#3` suffix in source
|
||||
* order.
|
||||
*/
|
||||
export function measureFunctions(sf: ts.SourceFile): MeasuredFunction[] {
|
||||
const out: MeasuredFunction[] = [];
|
||||
const seen = new Map<string, number>();
|
||||
const visit = (node: ts.Node, chain: string[]): void => {
|
||||
let next = chain;
|
||||
if (isFunctionNode(node)) {
|
||||
const base = [...chain, ownName(node, sf)].join(" > ");
|
||||
const n = (seen.get(base) ?? 0) + 1;
|
||||
seen.set(base, n);
|
||||
const name = n === 1 ? base : `${base} #${n}`;
|
||||
const start = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line;
|
||||
const end = sf.getLineAndCharacterOfPosition(node.getEnd()).line;
|
||||
out.push({ name, line: start + 1, lines: end - start + 1 });
|
||||
next = [...chain, ownName(node, sf)];
|
||||
}
|
||||
ts.forEachChild(node, (child) => visit(child, next));
|
||||
};
|
||||
visit(sf, []);
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── Value-import graph ──────────────────────────────────────────────────────
|
||||
|
||||
const COMPILER_OPTIONS: ts.CompilerOptions = {
|
||||
baseUrl: process.cwd(),
|
||||
paths: { "@/*": ["./src/*"] },
|
||||
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
||||
module: ts.ModuleKind.ESNext,
|
||||
jsx: ts.JsxEmit.ReactJSX,
|
||||
resolveJsonModule: true,
|
||||
allowJs: true,
|
||||
};
|
||||
|
||||
export interface ModuleImports {
|
||||
/** Specifiers this module evaluates at runtime, static and dynamic. */
|
||||
value: string[];
|
||||
/** True when every statement is an `export … from` (or an import). */
|
||||
reexportOnly: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The specifiers a module needs at runtime: static imports and re-exports
|
||||
* that are not type-only, side-effect imports, and dynamic `import("…")`
|
||||
* with a literal specifier. `import type`, `export type` and lists whose
|
||||
* every name is `type` are erased by the compiler and left out.
|
||||
*/
|
||||
export function moduleImports(sf: ts.SourceFile): ModuleImports {
|
||||
const value: string[] = [];
|
||||
let reexports = 0;
|
||||
let other = 0;
|
||||
for (const st of sf.statements) {
|
||||
if (ts.isImportDeclaration(st)) {
|
||||
const clause = st.importClause;
|
||||
const typeOnly =
|
||||
!!clause &&
|
||||
(clause.isTypeOnly ||
|
||||
(!clause.name &&
|
||||
!!clause.namedBindings &&
|
||||
ts.isNamedImports(clause.namedBindings) &&
|
||||
clause.namedBindings.elements.length > 0 &&
|
||||
clause.namedBindings.elements.every((e) => e.isTypeOnly)));
|
||||
if (!typeOnly && ts.isStringLiteral(st.moduleSpecifier)) {
|
||||
value.push(st.moduleSpecifier.text);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (ts.isExportDeclaration(st) && st.moduleSpecifier) {
|
||||
reexports += 1;
|
||||
const typeOnly =
|
||||
st.isTypeOnly ||
|
||||
(!!st.exportClause &&
|
||||
ts.isNamedExports(st.exportClause) &&
|
||||
st.exportClause.elements.length > 0 &&
|
||||
st.exportClause.elements.every((e) => e.isTypeOnly));
|
||||
if (!typeOnly && ts.isStringLiteral(st.moduleSpecifier)) {
|
||||
value.push(st.moduleSpecifier.text);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
other += 1;
|
||||
}
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (
|
||||
ts.isCallExpression(node) &&
|
||||
node.expression.kind === ts.SyntaxKind.ImportKeyword &&
|
||||
node.arguments[0] &&
|
||||
ts.isStringLiteralLike(node.arguments[0])
|
||||
) {
|
||||
value.push(node.arguments[0].text);
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(sf);
|
||||
return { value, reexportOnly: reexports > 0 && other === 0 };
|
||||
}
|
||||
|
||||
export interface ImportGraph {
|
||||
/** Module → modules it imports at runtime, both relative to `src/`. */
|
||||
edges: Map<string, string[]>;
|
||||
/** Modules whose every statement is a re-export. */
|
||||
reexportOnly: string[];
|
||||
/** Specifiers resolved to a module inside `src/`. */
|
||||
resolved: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The runtime import graph over the production sources, resolved with the
|
||||
* compiler's own module resolution under the app's `@/*` path mapping.
|
||||
* Imports of `src/generated/` and of packages are not edges.
|
||||
*/
|
||||
export function buildImportGraph(files: readonly string[]): ImportGraph {
|
||||
const cache = ts.createModuleResolutionCache(
|
||||
process.cwd(),
|
||||
(x) => x,
|
||||
COMPILER_OPTIONS,
|
||||
);
|
||||
const known = new Set(files);
|
||||
const edges = new Map<string, string[]>();
|
||||
const reexportOnly: string[] = [];
|
||||
let resolved = 0;
|
||||
for (const rel of files) {
|
||||
const abs = join(SRC, rel);
|
||||
const sf = parse(rel, readFileSync(abs, "utf8"));
|
||||
const imports = moduleImports(sf);
|
||||
if (imports.reexportOnly) reexportOnly.push(rel);
|
||||
const deps = new Set<string>();
|
||||
for (const spec of imports.value) {
|
||||
const hit = ts.resolveModuleName(
|
||||
spec,
|
||||
abs,
|
||||
COMPILER_OPTIONS,
|
||||
ts.sys,
|
||||
cache,
|
||||
).resolvedModule;
|
||||
if (!hit || hit.isExternalLibraryImport) continue;
|
||||
const target = hit.resolvedFileName;
|
||||
if (!target.startsWith(`${SRC}/`)) continue;
|
||||
const targetRel = target.slice(SRC.length + 1);
|
||||
if (!known.has(targetRel)) continue;
|
||||
deps.add(targetRel);
|
||||
resolved += 1;
|
||||
}
|
||||
edges.set(rel, [...deps].sort());
|
||||
}
|
||||
return { edges, reexportOnly, resolved };
|
||||
}
|
||||
|
||||
/** Strongly connected components with more than one module, or a self-loop. */
|
||||
export function stronglyConnected(edges: Map<string, string[]>): string[][] {
|
||||
let index = 0;
|
||||
const idx = new Map<string, number>();
|
||||
const low = new Map<string, number>();
|
||||
const onStack = new Set<string>();
|
||||
const stack: string[] = [];
|
||||
const out: string[][] = [];
|
||||
|
||||
const strong = (v: string): void => {
|
||||
idx.set(v, index);
|
||||
low.set(v, index);
|
||||
index += 1;
|
||||
stack.push(v);
|
||||
onStack.add(v);
|
||||
for (const w of edges.get(v) ?? []) {
|
||||
if (!idx.has(w)) {
|
||||
strong(w);
|
||||
low.set(v, Math.min(low.get(v)!, low.get(w)!));
|
||||
} else if (onStack.has(w)) {
|
||||
low.set(v, Math.min(low.get(v)!, idx.get(w)!));
|
||||
}
|
||||
}
|
||||
if (low.get(v) === idx.get(v)) {
|
||||
const component: string[] = [];
|
||||
let w: string;
|
||||
do {
|
||||
w = stack.pop()!;
|
||||
onStack.delete(w);
|
||||
component.push(w);
|
||||
} while (w !== v);
|
||||
if (component.length > 1 || (edges.get(v) ?? []).includes(v)) {
|
||||
out.push(component.sort());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const v of [...edges.keys()].sort()) if (!idx.has(v)) strong(v);
|
||||
return out.sort((a, b) => a[0].localeCompare(b[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cycles the way a depth-first walk in sorted order meets them: one cycle
|
||||
* per back edge, each listed from the module the walk re-entered. This is
|
||||
* the count `madge --circular` reports, so the number reads the same as the
|
||||
* audits that measured it.
|
||||
*/
|
||||
export function backEdgeCycles(edges: Map<string, string[]>): string[][] {
|
||||
const visited = new Set<string>();
|
||||
const cycles: string[][] = [];
|
||||
const path: string[] = [];
|
||||
const onPath = new Set<string>();
|
||||
const walk = (v: string): void => {
|
||||
if (onPath.has(v)) {
|
||||
cycles.push([...path.slice(path.indexOf(v)), v]);
|
||||
return;
|
||||
}
|
||||
if (visited.has(v)) return;
|
||||
visited.add(v);
|
||||
path.push(v);
|
||||
onPath.add(v);
|
||||
for (const w of edges.get(v) ?? []) walk(w);
|
||||
path.pop();
|
||||
onPath.delete(v);
|
||||
};
|
||||
for (const v of [...edges.keys()].sort()) walk(v);
|
||||
return cycles;
|
||||
}
|
||||
@@ -38,6 +38,12 @@ export function parseWipeSchema(
|
||||
const blocks = [...schema.matchAll(/^model\s+(\w+)\s*\{([\s\S]*?)^\}/gm)].map(
|
||||
(m) => ({ name: m[1], body: m[2] }),
|
||||
);
|
||||
if (schemaPath === WIPE_SCHEMA_PATH && blocks.length < 50) {
|
||||
throw new Error(
|
||||
`parseWipeSchema(${schemaPath}): parsed ${blocks.length} model(s), below ` +
|
||||
`the floor of 50. A wipe guard over an empty parse passes on nothing.`,
|
||||
);
|
||||
}
|
||||
const modelNames = new Set(blocks.map((b) => b.name));
|
||||
|
||||
const out = new Map<string, ModelShape>();
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* No new runtime import cycle, and no new re-export-only module.
|
||||
*
|
||||
* A value-import cycle means module evaluation order decides whether a
|
||||
* binding is initialised when it is first read, and it ties the modules in
|
||||
* it into one unit nobody can load or split alone. The graph here is the
|
||||
* runtime one: static imports and re-exports that are not type-only,
|
||||
* side-effect imports, and dynamic `import("…")` with a literal specifier
|
||||
* (a lazy edge still loads the module into the same process). `import type`
|
||||
* is erased and left out. Modules are resolved with the TypeScript compiler
|
||||
* under the app's `@/*` mapping (`helpers/source-metrics.ts`).
|
||||
*
|
||||
* Two baselines in `baselines/import-cycles.json`:
|
||||
*
|
||||
* - the cycle count, as a depth-first walk in sorted order meets them (one
|
||||
* per back edge, the number `madge --circular` reports);
|
||||
* - the strongly connected components, member by member.
|
||||
*
|
||||
* A component that is new, or gained a member, fails: that is a new cycle,
|
||||
* whatever the count says. A count above the baseline fails. A count below
|
||||
* it, or a component that lost members, fails until the lower state is
|
||||
* written back with
|
||||
* `RATCHET_WRITE=1 pnpm vitest run src/__tests__/import-cycle-guard.test.ts`.
|
||||
* The target is zero.
|
||||
*
|
||||
* A re-export-only module (every statement an `export … from`) is a facade:
|
||||
* it is where most of the cycles above came from, because children of a
|
||||
* split module import back through the old file. Existing ones are listed
|
||||
* in `baselines/reexport-only-modules.json`; a new one fails. Import the
|
||||
* module that defines the binding instead.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { requireFloor } from "./helpers/source-files";
|
||||
import {
|
||||
backEdgeCycles,
|
||||
buildImportGraph,
|
||||
productionSources,
|
||||
stronglyConnected,
|
||||
} from "./helpers/source-metrics";
|
||||
import {
|
||||
readBaselineJson,
|
||||
runRatchet,
|
||||
writeBackHint,
|
||||
writeBaselineJson,
|
||||
} from "./helpers/ratchet";
|
||||
|
||||
const GUARD = "src/__tests__/import-cycle-guard.test.ts";
|
||||
const CYCLES_FILE = "import-cycles.json";
|
||||
|
||||
interface CycleBaseline {
|
||||
$comment: string;
|
||||
cycles: number;
|
||||
components: string[][];
|
||||
}
|
||||
|
||||
describe("runtime import cycles", () => {
|
||||
const files = productionSources();
|
||||
const graph = buildImportGraph(files);
|
||||
const components = stronglyConnected(graph.edges);
|
||||
const cycles = backEdgeCycles(graph.edges);
|
||||
|
||||
it("builds a graph of the whole tree", () => {
|
||||
expect(graph.edges.size).toBeGreaterThan(3000);
|
||||
expect(graph.resolved).toBeGreaterThan(15_000);
|
||||
});
|
||||
|
||||
it("adds no cycle and leaves no improvement unrecorded", () => {
|
||||
if (process.env.RATCHET_WRITE === "1") {
|
||||
const old = readBaselineJson<CycleBaseline>(CYCLES_FILE) ?? {
|
||||
$comment: `Runtime import cycles under src/: the back-edge count and the strongly connected components. Maintained by ${GUARD}.`,
|
||||
cycles: cycles.length,
|
||||
components,
|
||||
};
|
||||
const kept = old.components
|
||||
.map((c) => c.filter((m) => components.some((now) => now.includes(m))))
|
||||
.filter((c) => c.length > 1);
|
||||
writeBaselineJson(CYCLES_FILE, {
|
||||
$comment: old.$comment,
|
||||
cycles: Math.min(old.cycles, cycles.length),
|
||||
components: kept,
|
||||
});
|
||||
}
|
||||
const baseline = readBaselineJson<CycleBaseline>(CYCLES_FILE);
|
||||
if (!baseline) throw new Error(`${CYCLES_FILE} is missing`);
|
||||
|
||||
const grown = components.filter(
|
||||
(c) => !baseline.components.some((b) => c.every((m) => b.includes(m))),
|
||||
);
|
||||
expect(
|
||||
grown.map((c) => c.join(" <-> ")),
|
||||
"A new runtime import cycle (or a known one that drew in another " +
|
||||
"module). Break it: move the shared piece into a leaf module, turn " +
|
||||
"the edge into `import type` if only a type crosses, or import the " +
|
||||
"defining module instead of a facade.",
|
||||
).toEqual([]);
|
||||
expect(
|
||||
cycles.length,
|
||||
`Runtime import cycles: ${cycles.map((c) => c.join(" -> ")).join("; ")}`,
|
||||
).toBeLessThanOrEqual(baseline.cycles);
|
||||
|
||||
const shrunk = baseline.components.filter(
|
||||
(b) => !components.some((c) => b.every((m) => c.includes(m))),
|
||||
);
|
||||
expect(
|
||||
shrunk.map((c) => c.join(" <-> ")),
|
||||
writeBackHint(GUARD),
|
||||
).toEqual([]);
|
||||
expect(cycles.length, writeBackHint(GUARD)).toBe(baseline.cycles);
|
||||
});
|
||||
|
||||
it("adds no re-export-only module", () => {
|
||||
requireFloor("re-export-only modules", graph.reexportOnly, 1);
|
||||
const r = runRatchet(
|
||||
"reexport-only-modules.json",
|
||||
Object.fromEntries(graph.reexportOnly.map((rel) => [rel, 1])),
|
||||
{
|
||||
comment: `Modules under src/ whose every statement is an export … from. Maintained by ${GUARD}.`,
|
||||
},
|
||||
);
|
||||
expect(
|
||||
r.grew,
|
||||
"A new re-export-only module. Import the module that defines the binding instead of adding a facade.",
|
||||
).toEqual([]);
|
||||
expect(r.stale, writeBackHint(GUARD)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the cycle finder", () => {
|
||||
const graphOf = (edges: Record<string, string[]>) =>
|
||||
new Map(Object.entries(edges));
|
||||
|
||||
it("finds a two-module and a three-module cycle, and nothing in a tree", () => {
|
||||
const g = graphOf({
|
||||
a: ["b"],
|
||||
b: ["a", "c"],
|
||||
c: ["d"],
|
||||
d: ["e"],
|
||||
e: ["c"],
|
||||
f: ["a"],
|
||||
});
|
||||
expect(stronglyConnected(g)).toEqual([
|
||||
["a", "b"],
|
||||
["c", "d", "e"],
|
||||
]);
|
||||
expect(backEdgeCycles(g)).toHaveLength(2);
|
||||
expect(stronglyConnected(graphOf({ a: ["b"], b: ["c"], c: [] }))).toEqual(
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import { INTEGRATION_CADENCE } from "@/lib/integrations/sync-verdict";
|
||||
import { INTEGRATION_DISPLAY_NAMES } from "@/components/settings/integrations/integration-fallback-row";
|
||||
import { readSource } from "@/__tests__/helpers/source-files";
|
||||
|
||||
const ROOT = path.resolve(__dirname, "../..");
|
||||
|
||||
@@ -65,9 +66,9 @@ describe("moodLog integration stays removed", () => {
|
||||
});
|
||||
|
||||
it("registers no moodlog queue, cron or handler binding", () => {
|
||||
const registrar = readFileSync(
|
||||
path.join(ROOT, "src/lib/jobs/reminder/register-integration-sync.ts"),
|
||||
"utf8",
|
||||
const registrar = readSource(
|
||||
"src/lib/jobs/reminder/register-integration-sync.ts",
|
||||
"export async function registerIntegrationSyncQueues",
|
||||
);
|
||||
expect(registrar).not.toContain("moodlog");
|
||||
expect(registrar).not.toContain("MoodLog");
|
||||
|
||||
@@ -19,7 +19,7 @@ import { dirname, join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import nextConfig from "../../next.config";
|
||||
import { SETTINGS_SECTION_SLUGS } from "@/components/settings/section-slugs";
|
||||
import { SETTINGS_SECTION_SLUGS } from "@/lib/settings/section-slugs";
|
||||
import { LAYOUT_GROUP_IDS } from "@/components/settings/layout-groups";
|
||||
import { MODULE_SETTINGS_PAGE_IDS } from "@/components/settings/module-settings-page-ids";
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* `readSource` and `readSourceFolder`: the reads a path-pinned guard goes
|
||||
* through, so a split file fails the guard instead of leaving it green.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { readSource, readSourceFolder } from "./helpers/source-files";
|
||||
|
||||
const HERE = "src/__tests__/helpers/source-files.ts";
|
||||
|
||||
describe("readSource", () => {
|
||||
it("returns the text when every anchor is present", () => {
|
||||
const text = readSource(HERE, [
|
||||
"export function readSource(",
|
||||
/export function walkSourceFiles\(/,
|
||||
]);
|
||||
expect(text.length).toBeGreaterThan(1000);
|
||||
});
|
||||
|
||||
it("throws on a file that does not exist", () => {
|
||||
expect(() => readSource("src/lib/does-not-exist.ts", "anything")).toThrow(
|
||||
/does not exist/,
|
||||
);
|
||||
});
|
||||
|
||||
it("throws when an anchor no longer matches", () => {
|
||||
expect(() =>
|
||||
readSource(HERE, ["export function readSource(", "noSuchAnchorHere"]),
|
||||
).toThrow(/no longer contains noSuchAnchorHere/);
|
||||
expect(() => readSource(HERE, /export function neverDeclared\(/)).toThrow(
|
||||
/no longer contains/,
|
||||
);
|
||||
});
|
||||
|
||||
it("refuses a read without an anchor", () => {
|
||||
expect(() => readSource(HERE, [])).toThrow(/at least one anchor/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("readSourceFolder", () => {
|
||||
it("returns the files with their text, above the floor", () => {
|
||||
const files = readSourceFolder("src/__tests__/helpers", { floor: 5 });
|
||||
expect(files.length).toBeGreaterThanOrEqual(5);
|
||||
expect(files.map(([rel]) => rel)).toContain("source-files.ts");
|
||||
});
|
||||
|
||||
it("throws when the folder holds fewer files than the floor", () => {
|
||||
expect(() =>
|
||||
readSourceFolder("src/__tests__/helpers", {
|
||||
floor: 1,
|
||||
include: (rel) => rel === "nothing-matches.ts",
|
||||
}),
|
||||
).toThrow(/below the stated floor/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* File and function size may only shrink.
|
||||
*
|
||||
* Two caps over the production source (see `helpers/source-metrics.ts` for
|
||||
* what counts): a file over 800 lines, and a function over 200 lines. Most of
|
||||
* the large files are one large function, so both are measured, through the
|
||||
* TypeScript compiler.
|
||||
*
|
||||
* Every file and function over its cap today is in a baseline under
|
||||
* `baselines/` with its current length. From here:
|
||||
*
|
||||
* - a new file or function over the cap fails;
|
||||
* - a baselined one that grows past its recorded length fails. If the
|
||||
* growth is deliberate, raising the number is a hand edit of the JSON in
|
||||
* a reviewed change, with the reason in the PR;
|
||||
* - a baselined one that shrank by more than the slack (or fell under the
|
||||
* cap) fails until the lower number is written back with
|
||||
* `RATCHET_WRITE=1 pnpm vitest run src/__tests__/source-size-ratchet.test.ts`.
|
||||
* The slack (25 lines for a file, 10 for a function) keeps a small edit
|
||||
* from forcing a baseline commit; a real split always crosses it.
|
||||
*
|
||||
* Catalog and data files are exempt from both caps: their length is the
|
||||
* length of the thing they list, and splitting them changes navigation, not
|
||||
* cohesion. Each is named below with its reason, and a named file that no
|
||||
* longer exists fails, so the list cannot outlive its entries.
|
||||
*/
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { requireFloor } from "./helpers/source-files";
|
||||
import {
|
||||
SRC,
|
||||
lineCount,
|
||||
measureFunctions,
|
||||
parse,
|
||||
productionSources,
|
||||
readSrc,
|
||||
} from "./helpers/source-metrics";
|
||||
import { runRatchet, writeBackHint } from "./helpers/ratchet";
|
||||
|
||||
const FILE_CAP = 800;
|
||||
const FUNCTION_CAP = 200;
|
||||
const GUARD = "src/__tests__/source-size-ratchet.test.ts";
|
||||
|
||||
/**
|
||||
* Exempt from both caps. A key ending in `/` covers the folder. Paths are
|
||||
* relative to `src/`.
|
||||
*/
|
||||
const CATALOGS: Readonly<Record<string, string>> = {
|
||||
"lib/openapi/routes/":
|
||||
"the OpenAPI contract: one schema and one path entry per route, grows with the API surface",
|
||||
"lib/signals/registry.ts": "the SIGNALS record, one entry per signal",
|
||||
"lib/ai/coach/eval/golden-cases.ts": "Coach evaluation fixtures",
|
||||
"lib/ai/coach/system-prompt.ts": "prompt text",
|
||||
"lib/measurements/apple-health-mapping.ts":
|
||||
"the HealthKit identifier mapping table",
|
||||
"lib/labs/biomarker-catalog.ts": "the biomarker catalogue",
|
||||
"app/privacy/page.tsx":
|
||||
"legal copy rendered as React text, one long static component",
|
||||
};
|
||||
|
||||
function isCatalog(rel: string): boolean {
|
||||
return Object.keys(CATALOGS).some((key) =>
|
||||
key.endsWith("/") ? rel.startsWith(key) : rel === key,
|
||||
);
|
||||
}
|
||||
|
||||
function measure() {
|
||||
const files = productionSources();
|
||||
const fileLines: Record<string, number> = {};
|
||||
const functionLines: Record<string, number> = {};
|
||||
let functionsSeen = 0;
|
||||
for (const rel of files) {
|
||||
if (isCatalog(rel)) continue;
|
||||
const text = readSrc(rel);
|
||||
const lines = lineCount(text);
|
||||
if (lines > FILE_CAP) fileLines[rel] = lines;
|
||||
for (const fn of measureFunctions(parse(rel, text))) {
|
||||
functionsSeen += 1;
|
||||
if (fn.lines > FUNCTION_CAP)
|
||||
functionLines[`${rel} :: ${fn.name}`] = fn.lines;
|
||||
}
|
||||
}
|
||||
return { files: files.length, functionsSeen, fileLines, functionLines };
|
||||
}
|
||||
|
||||
describe("source size ratchet", () => {
|
||||
const m = measure();
|
||||
|
||||
it("reads the whole tree", () => {
|
||||
expect(m.files).toBeGreaterThan(3000);
|
||||
expect(m.functionsSeen).toBeGreaterThan(20_000);
|
||||
requireFloor("files over the cap", Object.keys(m.fileLines), 100);
|
||||
requireFloor("functions over the cap", Object.keys(m.functionLines), 300);
|
||||
});
|
||||
|
||||
it(`no file over ${FILE_CAP} lines grows, and none is added`, () => {
|
||||
const r = runRatchet("source-file-lines.json", m.fileLines, {
|
||||
slack: 25,
|
||||
comment: `Production files over ${FILE_CAP} lines and their length. Maintained by ${GUARD}.`,
|
||||
});
|
||||
expect(
|
||||
r.grew,
|
||||
`Files over ${FILE_CAP} lines that are new or longer than their baseline. Split the file, or raise its number in src/__tests__/baselines/source-file-lines.json in a reviewed change with the reason.`,
|
||||
).toEqual([]);
|
||||
expect(r.stale, writeBackHint(GUARD)).toEqual([]);
|
||||
});
|
||||
|
||||
it(`no function over ${FUNCTION_CAP} lines grows, and none is added`, () => {
|
||||
const r = runRatchet("source-function-lines.json", m.functionLines, {
|
||||
slack: 10,
|
||||
comment: `Production functions over ${FUNCTION_CAP} lines, keyed "file :: name", and their length. Maintained by ${GUARD}.`,
|
||||
});
|
||||
expect(
|
||||
r.grew,
|
||||
`Functions over ${FUNCTION_CAP} lines that are new or longer than their baseline. Extract a part, or raise its number in src/__tests__/baselines/source-function-lines.json in a reviewed change with the reason.`,
|
||||
).toEqual([]);
|
||||
expect(r.stale, writeBackHint(GUARD)).toEqual([]);
|
||||
});
|
||||
|
||||
it("names no catalog that is gone", () => {
|
||||
const missing = Object.keys(CATALOGS).filter(
|
||||
(key) => !existsSync(join(SRC, key)),
|
||||
);
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the function measure", () => {
|
||||
const fns = (text: string) => measureFunctions(parse("probe.ts", text));
|
||||
|
||||
it("names declarations, assignments, methods and call arguments", () => {
|
||||
const names = fns(
|
||||
[
|
||||
"export function a() {}",
|
||||
"const b = () => {};",
|
||||
"const o = { c() {}, d: function () {} };",
|
||||
"class K { constructor() {} e() {} }",
|
||||
"wrap(async () => {});",
|
||||
"function f() { return () => 1; }",
|
||||
].join("\n"),
|
||||
).map((f) => f.name);
|
||||
expect(names).toEqual([
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d",
|
||||
"constructor",
|
||||
"e",
|
||||
"wrap(arg)",
|
||||
"f",
|
||||
"f > (anonymous)",
|
||||
]);
|
||||
});
|
||||
|
||||
it("counts lines from the first to the last line of the node", () => {
|
||||
const [fn] = fns("function a() {\n x();\n y();\n}\n");
|
||||
expect(fn.lines).toBe(4);
|
||||
expect(fn.line).toBe(1);
|
||||
});
|
||||
|
||||
it("keeps two same-named functions apart", () => {
|
||||
const names = fns("call(() => 1);\ncall(() => 2);").map((f) => f.name);
|
||||
expect(names).toEqual(["call(arg)", "call(arg) #2"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Test shapes that cannot see the bugs this repo keeps shipping may only
|
||||
* become rarer.
|
||||
*
|
||||
* Four ratchets over the test files under `src/`, each against a baseline in
|
||||
* `baselines/` (see `helpers/ratchet.ts` for the mechanics):
|
||||
*
|
||||
* - route tests that replace the database client wholesale
|
||||
* (`vi.mock("@/lib/db", …)`): they assert the query the route sends, not
|
||||
* what Postgres does with it, so a `where` that matches the wrong rows
|
||||
* passes. Behaviour belongs in `tests/integration/`;
|
||||
* - route tests that stub `@/lib/api-handler`: they skip auth, scope, the
|
||||
* envelope and idempotency, which is where the handler arity class hid;
|
||||
* - `as never` casts in tests: they switch the type checker off for exactly
|
||||
* the fixture rows that would otherwise drift loudly when a model
|
||||
* changes;
|
||||
* - tests that read a source file by path and assert on its text. A split
|
||||
* or rename leaves such a test reading the wrong file, and a negative
|
||||
* assertion then passes for the wrong reason. New ones go through
|
||||
* `readSource(path, anchors)` or `readSourceFolder(dir, { floor })` in
|
||||
* `helpers/source-files.ts`, which this count does not include.
|
||||
*
|
||||
* A new offender fails; one that disappears (or loses casts) fails until
|
||||
* the lower number is written back with
|
||||
* `RATCHET_WRITE=1 pnpm vitest run src/__tests__/test-realism-ratchet.test.ts`.
|
||||
*
|
||||
* Limits, stated: the database mock is matched on `vi.mock`/`vi.doMock` with
|
||||
* the `@/lib/db` specifier or a relative path ending in `lib/db`; a mock of
|
||||
* the Prisma client entry itself, or a spy on every delegate, is not a
|
||||
* pattern in use and is not matched. The source-read check is a text
|
||||
* heuristic: a file that calls `readFileSync`/`readFile` and names a source
|
||||
* path (a literal ending in `.ts`, `.tsx`, `.js` or `.mjs`).
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
import ts from "typescript";
|
||||
|
||||
import { requireFloor } from "./helpers/source-files";
|
||||
import { parse, readSrc, testSources } from "./helpers/source-metrics";
|
||||
import { runRatchet, writeBackHint } from "./helpers/ratchet";
|
||||
|
||||
const GUARD = "src/__tests__/test-realism-ratchet.test.ts";
|
||||
|
||||
const DB_MOCK =
|
||||
/vi\.(?:do)?[mM]ock\(\s*["'`](?:@\/lib\/db|(?:\.\.?\/)+(?:[\w-]+\/)*lib\/db)["'`]/;
|
||||
const HANDLER_STUB = /vi\.(?:do)?[mM]ock\(\s*["'`]@\/lib\/api-handler["'`]/;
|
||||
const READS_FILE = /\b(?:readFileSync|readFile)\s*\(/;
|
||||
const SOURCE_PATH = /["'`][^"'`\n]*\.(?:tsx?|mjs|js)["'`]/;
|
||||
|
||||
function asNeverCount(rel: string, text: string): number {
|
||||
if (!text.includes("as never")) return 0;
|
||||
let n = 0;
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (
|
||||
ts.isAsExpression(node) &&
|
||||
node.type.kind === ts.SyntaxKind.NeverKeyword
|
||||
) {
|
||||
n += 1;
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(parse(rel, text));
|
||||
return n;
|
||||
}
|
||||
|
||||
function classifyTest(rel: string, text: string) {
|
||||
const routeTest = rel.startsWith("app/api/");
|
||||
return {
|
||||
dbMock: routeTest && DB_MOCK.test(text),
|
||||
handlerStub: routeTest && HANDLER_STUB.test(text),
|
||||
asNever: asNeverCount(rel, text),
|
||||
sourceRead: READS_FILE.test(text) && SOURCE_PATH.test(text),
|
||||
};
|
||||
}
|
||||
|
||||
function measure() {
|
||||
const tests = testSources();
|
||||
const dbMocks: Record<string, number> = {};
|
||||
const handlerStubs: Record<string, number> = {};
|
||||
const asNever: Record<string, number> = {};
|
||||
const sourceReads: Record<string, number> = {};
|
||||
let routeTests = 0;
|
||||
for (const rel of tests) {
|
||||
const c = classifyTest(rel, readSrc(rel));
|
||||
if (rel.startsWith("app/api/")) routeTests += 1;
|
||||
if (c.dbMock) dbMocks[rel] = 1;
|
||||
if (c.handlerStub) handlerStubs[rel] = 1;
|
||||
if (c.asNever > 0) asNever[rel] = c.asNever;
|
||||
if (c.sourceRead) sourceReads[rel] = 1;
|
||||
}
|
||||
return {
|
||||
tests: tests.length,
|
||||
routeTests,
|
||||
dbMocks,
|
||||
handlerStubs,
|
||||
asNever,
|
||||
sourceReads,
|
||||
};
|
||||
}
|
||||
|
||||
describe("test realism ratchet", () => {
|
||||
const m = measure();
|
||||
|
||||
it("reads the whole test tree", () => {
|
||||
expect(m.tests).toBeGreaterThan(2000);
|
||||
expect(m.routeTests).toBeGreaterThan(300);
|
||||
requireFloor("route tests mocking the db", Object.keys(m.dbMocks), 100);
|
||||
requireFloor("test files with as never", Object.keys(m.asNever), 100);
|
||||
requireFloor("source-reading tests", Object.keys(m.sourceReads), 100);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"route tests that mock @/lib/db wholesale",
|
||||
"route-test-db-mocks.json",
|
||||
"dbMocks",
|
||||
"Drive the route against Postgres in tests/integration/ instead.",
|
||||
],
|
||||
[
|
||||
"route tests that stub @/lib/api-handler",
|
||||
"route-test-handler-stubs.json",
|
||||
"handlerStubs",
|
||||
"Call the real handler, so auth, scope and the envelope run.",
|
||||
],
|
||||
[
|
||||
"as never casts in tests",
|
||||
"test-as-never.json",
|
||||
"asNever",
|
||||
"Type the fixture (a typed builder or the Prisma payload type) instead of casting it away.",
|
||||
],
|
||||
[
|
||||
"tests that read a source file by path",
|
||||
"test-source-reads.json",
|
||||
"sourceReads",
|
||||
"Read through readSource(path, anchors) or readSourceFolder(dir, { floor }) from src/__tests__/helpers/source-files.ts.",
|
||||
],
|
||||
] as const)("%s only become rarer", (label, file, key, advice) => {
|
||||
const r = runRatchet(file, m[key], {
|
||||
comment: `${label}, per test file under src/. Maintained by ${GUARD}.`,
|
||||
});
|
||||
expect(r.grew, `New or grown: ${label}. ${advice}`).toEqual([]);
|
||||
expect(r.stale, writeBackHint(GUARD)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
const READ_CALL = ["read", "File", "Sync"].join("");
|
||||
|
||||
describe("the classifier", () => {
|
||||
it("sees the shapes it counts", () => {
|
||||
const c = classifyTest(
|
||||
"app/api/x/__tests__/route.test.ts",
|
||||
[
|
||||
'vi.mock("@/lib/db", () => ({ prisma: {} }));',
|
||||
'vi.mock("@/lib/api-handler", () => ({}));',
|
||||
"const a = {} as never; const b = [] as never;",
|
||||
// Split so this file does not count itself as a source reader.
|
||||
`const src = ${READ_CALL}(join(ROOT, "src/app/api/x/route.ts"), "utf8");`,
|
||||
].join("\n"),
|
||||
);
|
||||
expect(c).toEqual({
|
||||
dbMock: true,
|
||||
handlerStub: true,
|
||||
asNever: 2,
|
||||
sourceRead: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores neighbours that only look alike", () => {
|
||||
const c = classifyTest(
|
||||
"app/api/x/__tests__/route.test.ts",
|
||||
[
|
||||
'vi.mock("@/lib/db-compat", () => ({}));',
|
||||
"// cast with as never is banned",
|
||||
'const s = "as never";',
|
||||
`const fixture = ${READ_CALL}(join(DIR, "sample.xml"), "utf8");`,
|
||||
].join("\n"),
|
||||
);
|
||||
expect(c).toEqual({
|
||||
dbMock: false,
|
||||
handlerStub: false,
|
||||
asNever: 0,
|
||||
sourceRead: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("counts a relative database mock and only under app/api", () => {
|
||||
expect(
|
||||
classifyTest(
|
||||
"app/api/x/__tests__/a.test.ts",
|
||||
'vi.mock("../../../lib/db")',
|
||||
).dbMock,
|
||||
).toBe(true);
|
||||
expect(
|
||||
classifyTest("lib/x/__tests__/a.test.ts", 'vi.mock("@/lib/db")').dbMock,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* No new comment that opens with a release version.
|
||||
*
|
||||
* `// v1.4.39.1 — wipe the rollup partition … Pre-fix the partition kept …`
|
||||
* is a history note: it describes a state of the code the reader cannot see,
|
||||
* and it repeats what CHANGELOG and `git log` already hold. About ten
|
||||
* thousand comment lines in the tree open that way. They stay until their
|
||||
* file is split or rewritten anyway; a mass edit would be churn. New ones do
|
||||
* not get added: say what the code does and why, and leave when it changed
|
||||
* to the history.
|
||||
*
|
||||
* A comment line counts when, after its marker (`//`, `/*`, or the `*` of a
|
||||
* block comment), it opens with `vMAJOR.MINOR[.PATCH…]`. A version later in
|
||||
* the sentence ("since v1.39 the gate …") is not an opener and is not
|
||||
* counted. The count is per file, against `baselines/version-stamp-comments.json`:
|
||||
* a file that gains one fails, a new file with one fails, and a file that
|
||||
* sheds some fails until the lower count is written back with
|
||||
* `RATCHET_WRITE=1 pnpm vitest run src/__tests__/version-stamp-ratchet.test.ts`.
|
||||
*
|
||||
* Limit, stated: it reads lines, not the parse tree, so a line inside a
|
||||
* template string that happens to start with `* v1.2` would count. None does
|
||||
* today.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { requireFloor, walkSourceFiles } from "./helpers/source-files";
|
||||
import { SRC, readSrc } from "./helpers/source-metrics";
|
||||
import { runRatchet, writeBackHint } from "./helpers/ratchet";
|
||||
|
||||
const GUARD = "src/__tests__/version-stamp-ratchet.test.ts";
|
||||
|
||||
const STAMP_OPENER = /^\s*(?:\/\/+|\/\*+|\*+)\s*\(?v\d+\.\d+(?:\.\d+){0,3}\b/;
|
||||
|
||||
function stampCount(text: string): number {
|
||||
let n = 0;
|
||||
for (const line of text.split("\n")) if (STAMP_OPENER.test(line)) n += 1;
|
||||
return n;
|
||||
}
|
||||
|
||||
describe("version-stamp comments", () => {
|
||||
const files = walkSourceFiles(SRC, { floor: 5000 }).filter(
|
||||
(rel) => !rel.startsWith("generated/") && !rel.endsWith(".d.ts"),
|
||||
);
|
||||
const counts: Record<string, number> = {};
|
||||
for (const rel of files) {
|
||||
const n = stampCount(readSrc(rel));
|
||||
if (n > 0) counts[rel] = n;
|
||||
}
|
||||
|
||||
it("finds the stamps that are there", () => {
|
||||
requireFloor("files with a version-stamp opener", Object.keys(counts), 500);
|
||||
});
|
||||
|
||||
it("gains none, and records every one that goes", () => {
|
||||
const r = runRatchet("version-stamp-comments.json", counts, {
|
||||
comment: `Comment lines that open with a release version, per file under src/. Maintained by ${GUARD}.`,
|
||||
});
|
||||
expect(
|
||||
r.grew,
|
||||
"New comment lines opening with a release version. Say what the code does and why; when it changed belongs in CHANGELOG and git.",
|
||||
).toEqual([]);
|
||||
expect(r.stale, writeBackHint(GUARD)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the opener matcher", () => {
|
||||
it.each([
|
||||
"// v1.4.39.1 — wipe the partition",
|
||||
" // v1.23: notes are encrypted",
|
||||
" * v1.37.19 (#219 parity) — per-slot units",
|
||||
"/* v1.5.0 — one-shot backfill */",
|
||||
"/** v1.16.12 — stale window widened */",
|
||||
])("counts %s", (line) => {
|
||||
expect(stampCount(line)).toBe(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"// Since v1.39 every AI decision is one resolution",
|
||||
"const v = 'v1.2.3';",
|
||||
"// version 1.2",
|
||||
"// ve1.2",
|
||||
])("does not count %s", (line) => {
|
||||
expect(stampCount(line)).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { moduleForMeasurementType } from "@/lib/modules/measurement-scope";
|
||||
import { locales } from "@/lib/i18n/config";
|
||||
import type { DataSummary } from "@/lib/analytics/trends";
|
||||
import { readSource } from "@/__tests__/helpers/source-files";
|
||||
|
||||
/**
|
||||
* The dashboard HRV tile must accept either HRV type.
|
||||
@@ -250,10 +251,10 @@ describe("the tile the user actually sees", () => {
|
||||
// current formatting of the replacement, so reformatting cannot break
|
||||
// it. Freshness is per-type and captioning a live RMSSD tile from the
|
||||
// SDNN lookup is the failure it guards.
|
||||
const page = readFileSync(
|
||||
join(process.cwd(), "src/app/page-client.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
const page = readSource("src/app/page-client.tsx", [
|
||||
"const tileStaleDays",
|
||||
"HEART_RATE_VARIABILITY",
|
||||
]);
|
||||
expect(page).not.toContain('tileStaleDays("HEART_RATE_VARIABILITY")');
|
||||
expect(page).not.toContain(
|
||||
"const hrvSummary = data?.summaries?.HEART_RATE_VARIABILITY",
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
estimateRunwayDays,
|
||||
lowStockTriggerDays,
|
||||
type RunwaySchedule,
|
||||
} from "@/components/medications/detail/supply-runway";
|
||||
} from "@/lib/medications/supply-runway";
|
||||
import {
|
||||
resolveLowStockRunwayDays,
|
||||
resolveReorderLeadDays,
|
||||
|
||||
@@ -54,7 +54,7 @@ import { assertMedicationOwnership } from "@/lib/medications/route-guards";
|
||||
import {
|
||||
effectiveUnitsPerDose,
|
||||
estimateUnitsRunwayDays,
|
||||
} from "@/components/medications/detail/supply-runway";
|
||||
} from "@/lib/medications/supply-runway";
|
||||
import { serializeScheduleUnitsPerDose } from "@/lib/medications/schedule-units-dto";
|
||||
import { hhmmToMinutesOrNull } from "@/lib/medications/scheduling/hhmm";
|
||||
import { getUserTodayBounds } from "@/lib/tz/local-day";
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { join, relative } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readSource } from "@/__tests__/helpers/source-files";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const INSIGHTS = join(ROOT, "src/app/insights");
|
||||
@@ -52,7 +53,10 @@ describe("Insights add actions", () => {
|
||||
});
|
||||
|
||||
it("the mood page has no text links into logging or the check-in", () => {
|
||||
const source = readFileSync(join(INSIGHTS, "mood/page-client.tsx"), "utf8");
|
||||
const source = readSource(
|
||||
"src/app/insights/mood/page-client.tsx",
|
||||
"<SubPageAddButton",
|
||||
);
|
||||
expect(source).not.toMatch(/<Link\s+href="\/mood"\s+data-slot/);
|
||||
expect(source).not.toContain('href="/mental-wellbeing"');
|
||||
expect(source).not.toContain("insights.mood.mentalWellbeingLink");
|
||||
|
||||
@@ -10,7 +10,8 @@ import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { isSettingsSectionSlug } from "@/components/settings/section-slugs";
|
||||
import { isSettingsSectionSlug } from "@/lib/settings/section-slugs";
|
||||
import { readSource } from "@/__tests__/helpers/source-files";
|
||||
|
||||
describe("sleep empty-state CTA href", () => {
|
||||
it("links to /settings/integrations (a real settings slug)", () => {
|
||||
@@ -29,9 +30,9 @@ describe("sleep empty-state CTA href", () => {
|
||||
});
|
||||
|
||||
it("never links to the removed /settings/data-sources slug", () => {
|
||||
const source = readFileSync(
|
||||
join(process.cwd(), "src/app/insights/sleep/page.tsx"),
|
||||
"utf8",
|
||||
const source = readSource(
|
||||
"src/app/insights/sleep/page.tsx",
|
||||
"<MetricEmptyState",
|
||||
);
|
||||
expect(source).not.toContain("/settings/data-sources");
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { notFound } from "next/navigation";
|
||||
import {
|
||||
SETTINGS_SECTION_SLUGS,
|
||||
isSettingsSectionSlug,
|
||||
} from "@/components/settings/section-slugs";
|
||||
} from "@/lib/settings/section-slugs";
|
||||
import { SettingsShell } from "@/components/settings/settings-shell";
|
||||
import {
|
||||
SettingsSectionLoader,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Server-safe constants for the v1.5 admin split. Mirrors the pattern in
|
||||
* `src/components/settings/section-slugs.ts` so server-rendered code paths
|
||||
* `src/lib/settings/section-slugs.ts` so server-rendered code paths
|
||||
* (`generateStaticParams()`, the page guard, route-existence tests) can
|
||||
* import the slug list without dragging the `<AdminShell>` client bundle
|
||||
* into the server graph.
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildPaletteIndex,
|
||||
type PaletteIndexInput,
|
||||
} from "@/lib/command-palette/build-index";
|
||||
} from "@/components/command-palette/build-index";
|
||||
|
||||
const t = (key: string) => key;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { Activity, Moon, Plus, Smile } from "lucide-react";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import type { PaletteEntry } from "@/lib/command-palette/build-index";
|
||||
import type { PaletteEntry } from "@/components/command-palette/build-index";
|
||||
import type { SearchResponse } from "@/lib/search/contract";
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
|
||||
+2
-2
@@ -24,8 +24,8 @@ import {
|
||||
SETTINGS_CARDS,
|
||||
SETTINGS_LAYOUT_PAGES,
|
||||
SETTINGS_MODULE_PAGES,
|
||||
} from "./settings-cards";
|
||||
import { PALETTE_SYNONYMS } from "./synonyms";
|
||||
} from "@/lib/command-palette/settings-cards";
|
||||
import { PALETTE_SYNONYMS } from "@/lib/command-palette/synonyms";
|
||||
|
||||
/**
|
||||
* The command palette's index: every place and action this session may
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
EMPTY_STATE_ACTIONS,
|
||||
type PaletteEntry,
|
||||
type PaletteGroup,
|
||||
} from "@/lib/command-palette/build-index";
|
||||
} from "@/components/command-palette/build-index";
|
||||
import { rankEntries } from "@/lib/command-palette/rank";
|
||||
import { pushRecent, readRecent } from "@/lib/command-palette/recent";
|
||||
import { scrollToAnchorWhenReady } from "@/lib/dom/scroll-to-anchor";
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
|
||||
import {
|
||||
buildPaletteIndex,
|
||||
type PaletteEntry,
|
||||
} from "@/lib/command-palette/build-index";
|
||||
} from "@/components/command-palette/build-index";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import {
|
||||
MODULE_REGISTRY,
|
||||
|
||||
@@ -11,7 +11,8 @@ import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { isSettingsSectionSlug } from "@/components/settings/section-slugs";
|
||||
import { isSettingsSectionSlug } from "@/lib/settings/section-slugs";
|
||||
import { readSource } from "@/__tests__/helpers/source-files";
|
||||
|
||||
describe("sleep-overview empty-state CTA href", () => {
|
||||
it("links to /settings/integrations (a real settings slug)", () => {
|
||||
@@ -30,9 +31,9 @@ describe("sleep-overview empty-state CTA href", () => {
|
||||
});
|
||||
|
||||
it("never links to the non-existent /settings/devices slug", () => {
|
||||
const source = readFileSync(
|
||||
join(process.cwd(), "src/components/insights/sleep-overview.tsx"),
|
||||
"utf8",
|
||||
const source = readSource(
|
||||
"src/components/insights/sleep-overview.tsx",
|
||||
"<EmptyState",
|
||||
);
|
||||
expect(source).not.toContain("/settings/devices");
|
||||
});
|
||||
|
||||
@@ -66,6 +66,7 @@ import {
|
||||
import { ADMIN_SECTIONS } from "@/components/admin/admin-shell";
|
||||
import { visibleUtilityDestinations } from "../nav-model";
|
||||
import { delegatedDomains } from "@/lib/sharing/domain-write-support";
|
||||
import { readSource } from "@/__tests__/helpers/source-files";
|
||||
|
||||
function render({
|
||||
pathname = "/",
|
||||
@@ -565,9 +566,9 @@ describe("<SidebarNav> carries no rules for a viewport it never shows on", () =>
|
||||
// A touch screen at most 520 px tall gets the phone shell (the
|
||||
// `shell-mobile` variant), so the rail is hidden there and a class keyed
|
||||
// on that query can never apply.
|
||||
const source = readFileSync(
|
||||
join(__dirname, "..", "sidebar-nav.tsx"),
|
||||
"utf8",
|
||||
const source = readSource(
|
||||
"src/components/layout/sidebar-nav.tsx",
|
||||
"export function SidebarNav",
|
||||
);
|
||||
expect(source).not.toMatch(/pointer:coarse\)_and_\(height<=520px\)/);
|
||||
});
|
||||
|
||||
@@ -24,10 +24,8 @@ import {
|
||||
weekdayIndexesToTokens,
|
||||
weekdayIndexToToken,
|
||||
} from "@/components/medications/scheduling/legacy-bridge";
|
||||
import {
|
||||
DEFAULT_SUB_CONTROLS,
|
||||
type WeekdayToken,
|
||||
} from "@/components/medications/scheduling/types";
|
||||
import { DEFAULT_SUB_CONTROLS } from "@/components/medications/scheduling/types";
|
||||
import { type WeekdayToken } from "@/lib/medications/scheduling/weekday-tokens";
|
||||
|
||||
describe("weekdayIndexToToken", () => {
|
||||
it("maps Sunday-anchored 0..6 to SU/MO/TU/WE/TH/FR/SA", () => {
|
||||
|
||||
@@ -88,7 +88,7 @@ import { PhaseConfigSheet } from "@/components/medications/sections/phase-config
|
||||
import { SideEffectsSection } from "@/components/medications/side-effects-section";
|
||||
import { ChartSkeleton } from "@/components/charts/chart-skeleton";
|
||||
import { TitrationTimeline } from "@/components/medications/titration-timeline";
|
||||
import { estimateRunwayDays } from "@/components/medications/detail/supply-runway";
|
||||
import { estimateRunwayDays } from "@/lib/medications/supply-runway";
|
||||
import type { SupplySummary } from "@/lib/medications/inventory/summary";
|
||||
import { MedicationWizardDialog } from "@/components/medications/wizard/medication-wizard-dialog";
|
||||
import { parseScheduleRecurrence } from "@/lib/medication-schedule";
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
estimateDailyDoseCount,
|
||||
estimateRunwayDays,
|
||||
lowStockTriggerDays,
|
||||
} from "@/components/medications/detail/supply-runway";
|
||||
} from "@/lib/medications/supply-runway";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useMedicationComplianceSummary } from "@/lib/queries/use-medication-compliance-summary";
|
||||
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
estimateDailyDoseCount,
|
||||
estimateRunwayDays,
|
||||
lowStockTriggerDays,
|
||||
} from "@/components/medications/detail/supply-runway";
|
||||
} from "@/lib/medications/supply-runway";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import {
|
||||
invalidateKeys,
|
||||
|
||||
@@ -28,7 +28,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { MedicationStatusPill } from "@/components/medications/card-parts/medication-status-pill";
|
||||
import { useWeekdayLabel } from "@/components/medications/card-parts/medication-next-last-slot";
|
||||
import { resolveDisplayedSlotInstant } from "@/components/medications/card-parts/displayed-slot-instant";
|
||||
import { estimateRunwayDays } from "@/components/medications/detail/supply-runway";
|
||||
import { estimateRunwayDays } from "@/lib/medications/supply-runway";
|
||||
import { useMedicationIntake } from "@/components/medications/use-medication-intake";
|
||||
import { reduceCurrentWindowStatus } from "@/lib/medications/window-status";
|
||||
import { resolveNextDueDayLabel } from "@/lib/medications/next-due-day-label";
|
||||
|
||||
@@ -59,9 +59,11 @@ import {
|
||||
type CadenceSubControls,
|
||||
type CadenceValue,
|
||||
DEFAULT_SUB_CONTROLS,
|
||||
} from "./types";
|
||||
import {
|
||||
type WeekdayToken,
|
||||
WEEKDAY_TOKENS,
|
||||
} from "./types";
|
||||
} from "@/lib/medications/scheduling/weekday-tokens";
|
||||
|
||||
export interface CadencePickerProps {
|
||||
value: CadenceValue;
|
||||
|
||||
@@ -17,9 +17,11 @@ import {
|
||||
type CadenceSubControls,
|
||||
type CadenceValue,
|
||||
DEFAULT_SUB_CONTROLS,
|
||||
} from "./types";
|
||||
import {
|
||||
type WeekdayToken,
|
||||
WEEKDAY_TOKENS,
|
||||
} from "./types";
|
||||
} from "@/lib/medications/scheduling/weekday-tokens";
|
||||
|
||||
/**
|
||||
* Legacy schedule shape the page passes to the form. Sunday-anchored
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
* structured values; the wizard / form maps them to the wire format.
|
||||
*/
|
||||
|
||||
import type { WeekdayToken } from "@/lib/medications/scheduling/weekday-tokens";
|
||||
|
||||
/**
|
||||
* The eight cadence kinds the wizard exposes. Maps to the engine's
|
||||
* dispatch tiers in `src/lib/medications/scheduling/recurrence.ts`:
|
||||
@@ -71,18 +73,6 @@ export interface CadenceSubControls {
|
||||
rollingDays: number;
|
||||
}
|
||||
|
||||
/** RFC 5545 BYDAY tokens — the order the chip row renders. */
|
||||
export const WEEKDAY_TOKENS = [
|
||||
"MO",
|
||||
"TU",
|
||||
"WE",
|
||||
"TH",
|
||||
"FR",
|
||||
"SA",
|
||||
"SU",
|
||||
] as const;
|
||||
export type WeekdayToken = (typeof WEEKDAY_TOKENS)[number];
|
||||
|
||||
/** Sensible defaults so a brand-new picker has valid sub-controls. */
|
||||
export const DEFAULT_SUB_CONTROLS: CadenceSubControls = {
|
||||
weekdays: ["MO"],
|
||||
|
||||
@@ -16,8 +16,8 @@ import { isoStringToDate } from "@/components/medications/scheduling/course-wind
|
||||
import {
|
||||
DEFAULT_SUB_CONTROLS,
|
||||
type CadenceSubControls,
|
||||
type WeekdayToken,
|
||||
} from "@/components/medications/scheduling/types";
|
||||
import { type WeekdayToken } from "@/lib/medications/scheduling/weekday-tokens";
|
||||
import type { WizardPayload as ExtractorPayload } from "@/components/medications/scheduling/natural-language-extractor";
|
||||
|
||||
import type { WizardPayload } from "./wizard-payload";
|
||||
|
||||
@@ -5,11 +5,11 @@ import { useMemo } from "react";
|
||||
import { encodeCadence } from "@/components/medications/scheduling/cadence-picker";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { type CadenceSubControls } from "@/components/medications/scheduling/types";
|
||||
import {
|
||||
type CadenceSubControls,
|
||||
type WeekdayToken,
|
||||
WEEKDAY_TOKENS,
|
||||
} from "@/components/medications/scheduling/types";
|
||||
} from "@/lib/medications/scheduling/weekday-tokens";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
import type { StepProps } from "./step1-name";
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
* component tests render server-side and cannot click.
|
||||
*/
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import {
|
||||
defaultOnboardingSteps,
|
||||
@@ -27,6 +25,7 @@ import {
|
||||
|
||||
import { runBaselineAttempt } from "../baseline-form";
|
||||
import { runConfirmFinish } from "../confirm-screen";
|
||||
import { readSource } from "@/__tests__/helpers/source-files";
|
||||
|
||||
type PassableStep = Parameters<
|
||||
Parameters<typeof runConfirmFinish>[0]["passStep"]
|
||||
@@ -92,10 +91,10 @@ describe("runBaselineAttempt", () => {
|
||||
it("is the form's only way to move the pending flag", () => {
|
||||
// A second, inline `setSaving(false)` is how the flag stopped being
|
||||
// reset on one of the three exits in the first place.
|
||||
const source = readFileSync(
|
||||
join(process.cwd(), "src/components/onboarding/baseline-form.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
const source = readSource("src/components/onboarding/baseline-form.tsx", [
|
||||
"export function BaselineForm",
|
||||
"setSaving",
|
||||
]);
|
||||
expect(source).not.toContain("setSaving(false)");
|
||||
expect(source).not.toContain("setSaving(true)");
|
||||
});
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type React from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { AnamnesisSection } from "../anamnesis-section";
|
||||
import { readSource } from "@/__tests__/helpers/source-files";
|
||||
|
||||
/**
|
||||
* #159 — the "About me" note and the free-text allergies line moved from the
|
||||
@@ -144,9 +143,9 @@ describe("Anamnese — the About-me note card (#159)", () => {
|
||||
|
||||
describe("Account settings — the About-me card is gone (#159)", () => {
|
||||
it("the account section carries no about-me mount any more", () => {
|
||||
const source = readFileSync(
|
||||
join(process.cwd(), "src/components/settings/account-section/index.tsx"),
|
||||
"utf8",
|
||||
const source = readSource(
|
||||
"src/components/settings/account-section/index.tsx",
|
||||
"export function AccountSection",
|
||||
);
|
||||
expect(source).not.toContain("AboutMeSection");
|
||||
expect(source).not.toContain("about-me-section");
|
||||
|
||||
@@ -41,6 +41,7 @@ vi.mock("@/hooks/use-auth", () => ({
|
||||
|
||||
import { PasswordCard } from "../password-card";
|
||||
import { SecuritySection } from "../security-section";
|
||||
import { readSource } from "@/__tests__/helpers/source-files";
|
||||
|
||||
const SLOT = 'data-slot="settings-password-card"';
|
||||
|
||||
@@ -108,9 +109,9 @@ describe("change-password card placement", () => {
|
||||
});
|
||||
|
||||
it("left no password form behind in the Account section", () => {
|
||||
const account = readFileSync(
|
||||
join(ROOT, "src/components/settings/account-section/index.tsx"),
|
||||
"utf8",
|
||||
const account = readSource(
|
||||
"src/components/settings/account-section/index.tsx",
|
||||
"export function AccountSection",
|
||||
);
|
||||
for (const marker of [
|
||||
"settings.passwordTitle",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import en from "../../../../messages/en.json" with { type: "json" };
|
||||
import de from "../../../../messages/de.json" with { type: "json" };
|
||||
import { SETTINGS_SECTION_SLUGS } from "../section-slugs";
|
||||
import { SETTINGS_SECTION_SLUGS } from "@/lib/settings/section-slugs";
|
||||
|
||||
type Bag = Record<string, unknown>;
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ vi.mock("@/components/settings/thresholds-editor-section", () => ({
|
||||
}));
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { SettingsSectionFrame } from "./section-frame-harness";
|
||||
import type { SettingsSectionSlug } from "../section-slugs";
|
||||
import type { SettingsSectionSlug } from "@/lib/settings/section-slugs";
|
||||
import { AboutSection } from "../about-section";
|
||||
import { AccountSection } from "../account-section";
|
||||
import { AdvancedSection } from "../advanced-section";
|
||||
|
||||
@@ -11,7 +11,7 @@ import { classifySettingsDestination } from "@/lib/record-settings/classificatio
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { ManagedIntegrationStatus } from "@/components/settings/managed-integration-status";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import type { SettingsSectionSlug } from "@/components/settings/section-slugs";
|
||||
import type { SettingsSectionSlug } from "@/lib/settings/section-slugs";
|
||||
|
||||
// The two record-scoped surfaces load on demand. Both are forms that validate
|
||||
// with Zod (the managed settings patch, the allergy and family-history
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useEffect, type ComponentType } from "react";
|
||||
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import type { SettingsSectionSlug } from "@/components/settings/section-slugs";
|
||||
import type { SettingsSectionSlug } from "@/lib/settings/section-slugs";
|
||||
import type { IntegrationCallbackUrls } from "@/lib/integrations/callback-urls";
|
||||
|
||||
/**
|
||||
|
||||
@@ -55,7 +55,7 @@ import {
|
||||
SETTINGS_SECTION_SLUGS,
|
||||
isSettingsSectionSlug,
|
||||
type SettingsSectionSlug,
|
||||
} from "./section-slugs";
|
||||
} from "@/lib/settings/section-slugs";
|
||||
import { SettingsHubBackLink } from "./settings-hub-back-link";
|
||||
import { SettingsMobileNav } from "./settings-mobile-nav";
|
||||
import { SettingsAnchorForward } from "./settings-anchor-forward";
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { WEEKDAY_TOKENS } from "@/components/medications/scheduling/types";
|
||||
import { WEEKDAY_TOKENS } from "@/lib/medications/scheduling/weekday-tokens";
|
||||
|
||||
/** Closed list — matches the design-synthesis dose-unit dropdown. */
|
||||
export const DOSE_UNITS = [
|
||||
|
||||
@@ -559,6 +559,14 @@ describe("extractFeatures — v1.22 workouts block", () => {
|
||||
});
|
||||
|
||||
it("counts a session synced by two services once", async () => {
|
||||
// The cross-source picker groups sessions into fixed five-minute slots
|
||||
// of the epoch (`pick-canonical-workout-rows.ts`). With a start taken
|
||||
// from the real clock, the 30 s gap between the two copies crossed a
|
||||
// slot edge whenever the test ran in the last 30 s of a slot, and the
|
||||
// pair counted twice. The clock is pinned to an instant well inside a
|
||||
// slot, so the case tests the collapse rather than the time of day.
|
||||
vi.useFakeTimers({ toFake: ["Date"] });
|
||||
vi.setSystemTime(Date.parse("2026-10-22T12:02:00Z"));
|
||||
const startedAt = new Date(Date.now() - 1 * DAY_MS);
|
||||
prismaMock.workout.findMany.mockResolvedValue([
|
||||
{
|
||||
@@ -576,10 +584,14 @@ describe("extractFeatures — v1.22 workouts block", () => {
|
||||
totalDistanceM: 5010,
|
||||
},
|
||||
]);
|
||||
const f = await extractFeatures("user-1", false);
|
||||
expect(f.workouts?.last7.count).toBe(1);
|
||||
expect(f.workouts?.last7.totalDurationMin).toBe(30);
|
||||
expect(f.workouts?.last7.totalDistanceKm).toBe(5);
|
||||
try {
|
||||
const f = await extractFeatures("user-1", false);
|
||||
expect(f.workouts?.last7.count).toBe(1);
|
||||
expect(f.workouts?.last7.totalDurationMin).toBe(30);
|
||||
expect(f.workouts?.last7.totalDistanceKm).toBe(5);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ import {
|
||||
lowStockTriggerDays,
|
||||
supplyRunwayDates,
|
||||
type RunwaySchedule,
|
||||
} from "@/components/medications/detail/supply-runway";
|
||||
} from "@/lib/medications/supply-runway";
|
||||
import {
|
||||
detectSupplyUnderflow,
|
||||
summariseSupply,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { readSource } from "@/__tests__/helpers/source-files";
|
||||
|
||||
type Provider = "google_health" | "withings";
|
||||
type UserStatus = "complete" | "partial" | "failed" | "parked" | "skipped";
|
||||
@@ -45,9 +46,9 @@ const googleHandlerSource = readFileSync(
|
||||
join(process.cwd(), "src/lib/jobs/reminder/google-health-sync.ts"),
|
||||
"utf8",
|
||||
);
|
||||
const googleSyncSource = readFileSync(
|
||||
join(process.cwd(), "src/lib/google-health/sync.ts"),
|
||||
"utf8",
|
||||
const googleSyncSource = readSource(
|
||||
"src/lib/google-health/sync.ts",
|
||||
"export async function syncUserGoogleHealth(",
|
||||
);
|
||||
const withingsHandlerSource = readFileSync(
|
||||
join(process.cwd(), "src/lib/jobs/reminder/withings-sync.ts"),
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import {
|
||||
lowStockTriggerDays,
|
||||
supplyRunwayDates,
|
||||
type RunwaySchedule,
|
||||
} from "@/components/medications/detail/supply-runway";
|
||||
} from "@/lib/medications/supply-runway";
|
||||
|
||||
function schedule(partial: Partial<RunwaySchedule>): RunwaySchedule {
|
||||
return {
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
import {
|
||||
effectiveUnitsPerDose,
|
||||
estimateUnitsRunwayDays,
|
||||
} from "@/components/medications/detail/supply-runway";
|
||||
} from "@/lib/medications/supply-runway";
|
||||
import { serializeScheduleUnitsPerDose } from "@/lib/medications/schedule-units-dto";
|
||||
import {
|
||||
computeDisplayDue,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* RFC 5545 BYDAY tokens in the order the weekday chip row renders them.
|
||||
*
|
||||
* Shared by the scheduling wizard and the server-side medication extraction
|
||||
* prompt, so it lives beside the recurrence engine rather than in a
|
||||
* component folder.
|
||||
*/
|
||||
export const WEEKDAY_TOKENS = [
|
||||
"MO",
|
||||
"TU",
|
||||
"WE",
|
||||
"TH",
|
||||
"FR",
|
||||
"SA",
|
||||
"SU",
|
||||
] as const;
|
||||
export type WeekdayToken = (typeof WEEKDAY_TOKENS)[number];
|
||||
File renamed without changes.
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
SETTINGS_SECTION_SLUGS,
|
||||
type SettingsSectionSlug,
|
||||
} from "@/components/settings/section-slugs";
|
||||
} from "@/lib/settings/section-slugs";
|
||||
import type {
|
||||
AccountAccessLevel,
|
||||
AccountRecordKind,
|
||||
|
||||
File renamed without changes.
@@ -4,7 +4,7 @@
|
||||
* A self-hoster with 1.25 million measurement rows (a multi-year Apple Health
|
||||
* export) saw the dense intra-day retention, the nightly drain tick and the
|
||||
* data backup all fail with `job timed out`. Reproduced against a seeded
|
||||
* account of the same size (`scripts/seed-large-account.ts`): the drains held
|
||||
* account of the same size (`scripts/bench/seed-large-account.ts`): the drains held
|
||||
* a type's whole history in memory before folding a day, spent about 90
|
||||
* statements per folded day, and paged with a cursor Postgres could not use as
|
||||
* an index condition; the backup added about 640 MB to the process to store
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
/**
|
||||
* Statement budgets for the hot routes, against a real Postgres.
|
||||
*
|
||||
* Latency on a testcontainer says little; the number of statements a request
|
||||
* sends says a lot, and it is exact on a fixed fixture. A loop that reads per
|
||||
* row instead of per set (an N+1), a second read of something already loaded,
|
||||
* or a fan-out that grew with the data all show up here as a count over its
|
||||
* budget, on a change that every other test would let through.
|
||||
*
|
||||
* Each budget is the count measured on this fixture when it was written. A
|
||||
* route that needs fewer statements passes; lower the budget in the same
|
||||
* change so the gain is held. A route that needs more fails with every
|
||||
* statement it sent, grouped, so the new one is visible. Raising a budget is
|
||||
* a reviewed change with the reason beside the number.
|
||||
*
|
||||
* Every read runs on a miss: the user's caches are evicted before the
|
||||
* measured call, after one warm-up call that fills the process-level caches
|
||||
* a running server already holds. The large-account numbers (latency and
|
||||
* statements on 1.26 million rows) come from `scripts/bench/run.ts`; see
|
||||
* docs/contributing/benchmarks.md.
|
||||
*/
|
||||
import { NextRequest } from "next/server";
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type {
|
||||
MeasurementSource,
|
||||
MeasurementType,
|
||||
Prisma,
|
||||
SleepStage,
|
||||
} from "@/generated/prisma/client";
|
||||
|
||||
import { cookieJar } from "./mock-next-headers";
|
||||
import { countStatements, summariseStatements } from "./query-counter";
|
||||
import { getPrismaClient, truncateAllTables } from "./setup";
|
||||
|
||||
vi.mock("next/headers", async () => {
|
||||
const { cookieJar, headerJar } = await import("./mock-next-headers");
|
||||
return {
|
||||
headers: vi.fn(async () => ({
|
||||
get: (name: string) => headerJar.get(name.toLowerCase()) ?? null,
|
||||
})),
|
||||
cookies: vi.fn(async () => ({
|
||||
get: (name: string) => {
|
||||
const value = cookieJar.get(name);
|
||||
return value ? { name, value } : undefined;
|
||||
},
|
||||
set: (name: string, value: string) => {
|
||||
cookieJar.set(name, value);
|
||||
},
|
||||
delete: (name: string) => {
|
||||
cookieJar.delete(name);
|
||||
},
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/lib/db-compat", () => ({
|
||||
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const USER_ID = "user-query-budget";
|
||||
const DAY_MS = 86_400_000;
|
||||
|
||||
/**
|
||||
* Statements per request on this fixture. Each is the measured count; see
|
||||
* the header for how to move one.
|
||||
*/
|
||||
const BUDGET = {
|
||||
authMe: 18,
|
||||
dashboardSummary: 86,
|
||||
dashboardSnapshot: 103,
|
||||
series30: 6,
|
||||
series365: 6,
|
||||
series3650: 6,
|
||||
dayView: 48,
|
||||
healthStatus: 60,
|
||||
correlations: 44,
|
||||
batchIngest500: 31,
|
||||
} as const;
|
||||
|
||||
type Handler = (
|
||||
request: NextRequest,
|
||||
context: { params: Promise<Record<string, string>> },
|
||||
) => Promise<Response>;
|
||||
|
||||
/** Midnight UTC `n` days before today. */
|
||||
function daysAgo(n: number): Date {
|
||||
const d = new Date(Date.now() - n * DAY_MS);
|
||||
d.setUTCHours(0, 0, 0, 0);
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* A small account with the shape of a real one: dense and sparse types,
|
||||
* overlapping sources, cumulative `stats:` rows, sleep stages, glucose, a
|
||||
* medication with its intake history, workouts and mood.
|
||||
*/
|
||||
async function seed(): Promise<void> {
|
||||
const db = getPrismaClient();
|
||||
await db.user.create({
|
||||
data: {
|
||||
id: USER_ID,
|
||||
username: "query-budget",
|
||||
email: "query-budget@example.test",
|
||||
timezone: "Europe/Berlin",
|
||||
locale: "en",
|
||||
onboardingCompletedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
const rows: Prisma.MeasurementCreateManyInput[] = [];
|
||||
const push = (
|
||||
type: MeasurementType,
|
||||
unit: string,
|
||||
source: MeasurementSource,
|
||||
at: Date,
|
||||
value: number,
|
||||
sleepStage?: SleepStage,
|
||||
) =>
|
||||
rows.push({
|
||||
userId: USER_ID,
|
||||
type,
|
||||
unit,
|
||||
source,
|
||||
measuredAt: at,
|
||||
value,
|
||||
externalId: `${source}-${type}-${at.getTime()}${sleepStage ?? ""}`,
|
||||
...(sleepStage ? { sleepStage } : {}),
|
||||
});
|
||||
|
||||
for (let d = 1; d <= 120; d++) {
|
||||
const day = daysAgo(d).getTime();
|
||||
for (let h = 0; h < 24; h += 2) {
|
||||
push(
|
||||
"PULSE",
|
||||
"bpm",
|
||||
"APPLE_HEALTH",
|
||||
new Date(day + h * 3_600_000),
|
||||
60 + ((d + h) % 20),
|
||||
);
|
||||
}
|
||||
push(
|
||||
"HEART_RATE_VARIABILITY",
|
||||
"ms",
|
||||
"APPLE_HEALTH",
|
||||
new Date(day + 3 * 3_600_000),
|
||||
40 + (d % 15),
|
||||
);
|
||||
push(
|
||||
"RESTING_HEART_RATE",
|
||||
"bpm",
|
||||
"APPLE_HEALTH",
|
||||
new Date(day + 6 * 3_600_000),
|
||||
55 + (d % 5),
|
||||
);
|
||||
push(
|
||||
"BLOOD_PRESSURE_SYS",
|
||||
"mmHg",
|
||||
"WITHINGS",
|
||||
new Date(day + 7 * 3_600_000),
|
||||
120 + (d % 12),
|
||||
);
|
||||
push(
|
||||
"BLOOD_PRESSURE_DIA",
|
||||
"mmHg",
|
||||
"WITHINGS",
|
||||
new Date(day + 7 * 3_600_000),
|
||||
78 + (d % 8),
|
||||
);
|
||||
push(
|
||||
"PULSE",
|
||||
"bpm",
|
||||
"WITHINGS",
|
||||
new Date(day + 7 * 3_600_000),
|
||||
66 + (d % 6),
|
||||
);
|
||||
push(
|
||||
"WEIGHT",
|
||||
"kg",
|
||||
"WITHINGS",
|
||||
new Date(day + 6 * 3_600_000),
|
||||
82 + (d % 10) / 10,
|
||||
);
|
||||
if (d % 7 === 0) {
|
||||
push("WEIGHT", "kg", "MANUAL", new Date(day + 19 * 3_600_000), 82.3);
|
||||
}
|
||||
push(
|
||||
"OXYGEN_SATURATION",
|
||||
"%",
|
||||
"APPLE_HEALTH",
|
||||
new Date(day + 4 * 3_600_000),
|
||||
96 + (d % 3),
|
||||
);
|
||||
for (let s = 0; s < 8; s++) {
|
||||
push(
|
||||
"SLEEP_DURATION",
|
||||
"minutes",
|
||||
"APPLE_HEALTH",
|
||||
new Date(day - 3_600_000 + s * 45 * 60_000),
|
||||
45,
|
||||
(
|
||||
[
|
||||
"CORE",
|
||||
"DEEP",
|
||||
"REM",
|
||||
"CORE",
|
||||
"AWAKE",
|
||||
"CORE",
|
||||
"REM",
|
||||
"CORE",
|
||||
] as const
|
||||
)[s],
|
||||
);
|
||||
}
|
||||
const key = new Date(day).toISOString().slice(0, 10);
|
||||
rows.push({
|
||||
userId: USER_ID,
|
||||
type: "ACTIVITY_STEPS",
|
||||
unit: "steps",
|
||||
source: "APPLE_HEALTH",
|
||||
measuredAt: new Date(day + 11 * 3_600_000),
|
||||
value: 7000 + (d % 9) * 300,
|
||||
externalId: `stats:HKQuantityTypeIdentifierStepCount:${key}`,
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < 7 * 96; i++) {
|
||||
push(
|
||||
"BLOOD_GLUCOSE",
|
||||
"mg/dL",
|
||||
"NIGHTSCOUT",
|
||||
new Date(daysAgo(7).getTime() + i * 15 * 60_000),
|
||||
95 + (i % 30),
|
||||
);
|
||||
}
|
||||
await db.measurement.createMany({ data: rows });
|
||||
|
||||
await db.workout.createMany({
|
||||
data: Array.from({ length: 20 }, (_, i) => ({
|
||||
userId: USER_ID,
|
||||
sportType: ["running", "walking", "cycling"][i % 3]!,
|
||||
startedAt: new Date(daysAgo(i * 3 + 1).getTime() + 17 * 3_600_000),
|
||||
endedAt: new Date(daysAgo(i * 3 + 1).getTime() + 18 * 3_600_000),
|
||||
durationSec: 3600,
|
||||
totalEnergyKcal: 400,
|
||||
source: "APPLE_HEALTH" as const,
|
||||
externalId: `workout-${i}`,
|
||||
})),
|
||||
});
|
||||
await db.moodEntry.createMany({
|
||||
data: Array.from({ length: 60 }, (_, i) => {
|
||||
const at = new Date(daysAgo(i + 1).getTime() + 18 * 3_600_000);
|
||||
return {
|
||||
userId: USER_ID,
|
||||
date: at.toISOString().slice(0, 10),
|
||||
mood: ["GUT", "OKAY", "SUPER_GUT"][i % 3]!,
|
||||
score: [4, 3, 5][i % 3]!,
|
||||
source: "MANUAL",
|
||||
moodLoggedAt: at,
|
||||
tz: "Europe/Berlin",
|
||||
};
|
||||
}),
|
||||
});
|
||||
const med = await db.medication.create({
|
||||
data: {
|
||||
userId: USER_ID,
|
||||
name: "Budget A",
|
||||
dose: "5 mg",
|
||||
schedules: { create: { windowStart: "08:00", windowEnd: "09:00" } },
|
||||
},
|
||||
});
|
||||
await db.medicationIntakeEvent.createMany({
|
||||
data: Array.from({ length: 60 }, (_, i) => {
|
||||
const at = new Date(daysAgo(i + 1).getTime() + 6 * 3_600_000);
|
||||
return {
|
||||
userId: USER_ID,
|
||||
medicationId: med.id,
|
||||
scheduledFor: at,
|
||||
takenAt: i % 10 === 0 ? null : new Date(at.getTime() + 300_000),
|
||||
skipped: i % 10 === 0,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
const { recomputeUserRollups } =
|
||||
await import("@/lib/rollups/measurement-rollups");
|
||||
await recomputeUserRollups(USER_ID);
|
||||
|
||||
const session = await db.session.create({
|
||||
data: { userId: USER_ID, expiresAt: new Date(Date.now() + 3_600_000) },
|
||||
});
|
||||
cookieJar.set("healthlog_session", session.id);
|
||||
}
|
||||
|
||||
async function evictUserCaches(): Promise<void> {
|
||||
const { invalidateUserMeasurements } = await import("@/lib/cache/invalidate");
|
||||
invalidateUserMeasurements(USER_ID, { evict: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a route once to fill the process-level caches, evict the user's, and
|
||||
* count the statements of a second call: the miss a reader meets after a
|
||||
* sync on a server that is already running.
|
||||
*/
|
||||
async function measureRead(
|
||||
handler: Handler,
|
||||
url: string,
|
||||
params: Record<string, string> = {},
|
||||
) {
|
||||
const call = () =>
|
||||
handler(new NextRequest(url), { params: Promise.resolve(params) });
|
||||
const warm = await call();
|
||||
expect(warm.status, `${url} warm-up`).toBe(200);
|
||||
await evictUserCaches();
|
||||
const measured = await countStatements(async () => {
|
||||
const res = await call();
|
||||
await res.arrayBuffer();
|
||||
return res.status;
|
||||
});
|
||||
expect(measured.result, url).toBe(200);
|
||||
return measured;
|
||||
}
|
||||
|
||||
function expectWithin(
|
||||
label: string,
|
||||
measured: { count: number; statements: string[] },
|
||||
budget: number,
|
||||
) {
|
||||
expect(
|
||||
measured.count,
|
||||
`${label}: ${measured.count} statements against a budget of ${budget}.\n` +
|
||||
summariseStatements(measured.statements),
|
||||
).toBeLessThanOrEqual(budget);
|
||||
}
|
||||
|
||||
describe("statement budgets on the hot routes (real Postgres)", () => {
|
||||
beforeAll(async () => {
|
||||
await truncateAllTables(getPrismaClient());
|
||||
cookieJar.clear();
|
||||
await seed();
|
||||
});
|
||||
|
||||
it("GET /api/auth/me", async () => {
|
||||
const { GET } = await import("@/app/api/auth/me/route");
|
||||
const m = await measureRead(
|
||||
GET as unknown as Handler,
|
||||
"http://localhost/api/auth/me",
|
||||
);
|
||||
expectWithin("/api/auth/me", m, BUDGET.authMe);
|
||||
});
|
||||
|
||||
it("GET /api/dashboard/summary", async () => {
|
||||
const { GET } = await import("@/app/api/dashboard/summary/route");
|
||||
const m = await measureRead(
|
||||
GET as unknown as Handler,
|
||||
"http://localhost/api/dashboard/summary",
|
||||
);
|
||||
expectWithin("/api/dashboard/summary", m, BUDGET.dashboardSummary);
|
||||
});
|
||||
|
||||
it("GET /api/dashboard/snapshot", async () => {
|
||||
const { GET } = await import("@/app/api/dashboard/snapshot/route");
|
||||
const m = await measureRead(
|
||||
GET as unknown as Handler,
|
||||
"http://localhost/api/dashboard/snapshot",
|
||||
);
|
||||
expectWithin("/api/dashboard/snapshot", m, BUDGET.dashboardSnapshot);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[30, BUDGET.series30],
|
||||
[365, BUDGET.series365],
|
||||
[3650, BUDGET.series3650],
|
||||
])("GET /api/measurements/series, %i days", async (days, budget) => {
|
||||
const { GET } = await import("@/app/api/measurements/series/route");
|
||||
const m = await measureRead(
|
||||
GET as unknown as Handler,
|
||||
`http://localhost/api/measurements/series?kind=pulse&days=${days}`,
|
||||
);
|
||||
expectWithin(`/api/measurements/series ${days} d`, m, budget);
|
||||
});
|
||||
|
||||
it("GET /api/day/[date]", async () => {
|
||||
const { GET } = await import("@/app/api/day/[date]/route");
|
||||
const date = daysAgo(2).toISOString().slice(0, 10);
|
||||
const m = await measureRead(
|
||||
GET as unknown as Handler,
|
||||
`http://localhost/api/day/${date}`,
|
||||
{ date },
|
||||
);
|
||||
expectWithin("/api/day/[date]", m, BUDGET.dayView);
|
||||
});
|
||||
|
||||
it("GET /api/insights/health-status", async () => {
|
||||
const { GET } = await import("@/app/api/insights/health-status/route");
|
||||
const m = await measureRead(
|
||||
GET as unknown as Handler,
|
||||
"http://localhost/api/insights/health-status",
|
||||
);
|
||||
expectWithin("/api/insights/health-status", m, BUDGET.healthStatus);
|
||||
});
|
||||
|
||||
it("GET /api/insights/correlations", async () => {
|
||||
const { GET } = await import("@/app/api/insights/correlations/route");
|
||||
const m = await measureRead(
|
||||
GET as unknown as Handler,
|
||||
"http://localhost/api/insights/correlations",
|
||||
);
|
||||
expectWithin("/api/insights/correlations", m, BUDGET.correlations);
|
||||
});
|
||||
|
||||
it("POST /api/measurements/batch, 500 fresh rows", async () => {
|
||||
const { POST } = await import("@/app/api/measurements/batch/route");
|
||||
const start = daysAgo(3).getTime() + 30_000;
|
||||
const entries = Array.from({ length: 500 }, (_, i) => {
|
||||
const at = new Date(start + i * 60_000).toISOString();
|
||||
return {
|
||||
hkIdentifier: "HKQuantityTypeIdentifierHeartRate",
|
||||
value: 55 + (i % 40),
|
||||
unit: "count/min",
|
||||
startDate: at,
|
||||
endDate: at,
|
||||
externalId: `budget-hr-${i}`,
|
||||
};
|
||||
});
|
||||
const m = await countStatements(async () => {
|
||||
const res = await (POST as unknown as Handler)(
|
||||
new NextRequest("http://localhost/api/measurements/batch", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ entries }),
|
||||
}),
|
||||
{ params: Promise.resolve({}) },
|
||||
);
|
||||
return {
|
||||
status: res.status,
|
||||
body: (await res.json()) as { data: { inserted: number } },
|
||||
};
|
||||
});
|
||||
expect(m.result.status).toBe(200);
|
||||
expect(m.result.body.data.inserted).toBe(500);
|
||||
expectWithin(
|
||||
"POST /api/measurements/batch (500)",
|
||||
m,
|
||||
BUDGET.batchIngest500,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Counts the SQL statements the application sends to Postgres while a
|
||||
* function runs.
|
||||
*
|
||||
* Every statement the Prisma client sends goes through `pg`'s
|
||||
* `Client.prototype.query` (the driver adapter's pool hands out `pg`
|
||||
* clients, raw queries included, transactions' BEGIN/COMMIT included). The
|
||||
* prototype is patched once, on the `pg` instance the adapter itself
|
||||
* resolves, so a second copy of `pg` elsewhere in node_modules cannot leave
|
||||
* the counter reading zero; `countStatements` refuses a window that saw no
|
||||
* statement at all for the same reason.
|
||||
*
|
||||
* Background work a request starts and does not await (the fire-and-forget
|
||||
* tasks) is settled before the window closes, so it is counted with the
|
||||
* request that caused it.
|
||||
*/
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
interface PgClientPrototype {
|
||||
query: (...args: unknown[]) => unknown;
|
||||
}
|
||||
|
||||
const requireHere = createRequire(import.meta.url);
|
||||
const adapterEntry = requireHere.resolve("@prisma/adapter-pg");
|
||||
const pg = createRequire(adapterEntry)("pg") as {
|
||||
Client: { prototype: PgClientPrototype };
|
||||
};
|
||||
|
||||
const state = { active: false, statements: [] as string[] };
|
||||
const PATCHED = Symbol.for("healthlog.queryCounter");
|
||||
|
||||
const proto = pg.Client.prototype as PgClientPrototype & {
|
||||
[PATCHED]?: boolean;
|
||||
};
|
||||
if (!proto[PATCHED]) {
|
||||
const original = proto.query;
|
||||
proto.query = function patchedQuery(this: unknown, ...args: unknown[]) {
|
||||
if (state.active) {
|
||||
const first = args[0] as string | { text?: string } | undefined;
|
||||
const text = typeof first === "string" ? first : (first?.text ?? "?");
|
||||
state.statements.push(text.replace(/\s+/g, " ").trim().slice(0, 160));
|
||||
}
|
||||
return original.apply(this, args);
|
||||
};
|
||||
proto[PATCHED] = true;
|
||||
}
|
||||
|
||||
export interface StatementCount<T> {
|
||||
result: T;
|
||||
count: number;
|
||||
statements: string[];
|
||||
}
|
||||
|
||||
export async function countStatements<T>(
|
||||
fn: () => Promise<T>,
|
||||
): Promise<StatementCount<T>> {
|
||||
const { settleBackgroundTasks } =
|
||||
await import("@/lib/logging/background-tasks");
|
||||
await settleBackgroundTasks();
|
||||
state.statements = [];
|
||||
state.active = true;
|
||||
let result: T;
|
||||
try {
|
||||
result = await fn();
|
||||
await settleBackgroundTasks();
|
||||
} finally {
|
||||
state.active = false;
|
||||
}
|
||||
const statements = state.statements;
|
||||
if (statements.length === 0) {
|
||||
throw new Error(
|
||||
"countStatements saw no statement at all. Either the call reads " +
|
||||
"nothing from Postgres, or the counter is patched on a different " +
|
||||
"copy of pg than the one the Prisma adapter uses.",
|
||||
);
|
||||
}
|
||||
return { result, count: statements.length, statements };
|
||||
}
|
||||
|
||||
/** The statements grouped by text, most frequent first, for a failure message. */
|
||||
export function summariseStatements(statements: readonly string[]): string {
|
||||
const counts = new Map<string, number>();
|
||||
for (const s of statements) counts.set(s, (counts.get(s) ?? 0) + 1);
|
||||
return [...counts.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([s, n]) => `${String(n).padStart(4)} × ${s}`)
|
||||
.join("\n");
|
||||
}
|
||||
Reference in new issue
Block a user