Captures the gate-flip outcome: 35 unused exports → 0, 52 unused types
→ 0, exit code 0 on the final `pnpm knip` run. Documents the
`ignoreExportsUsedInFile` + `ignoreIssues` ignore-block rationale,
inventories every dropped symbol, and flags the two W3 / W4 reconcile
callouts (`describeInjectionSite` re-export, `listSupportedTimezones`
re-export) along with the merge-order constraint they imply.
Drop the `--include files,dependencies,binaries,unlisted` scope on the
Knip workflow. The tree is now at zero unused exports + zero unused
types, so the gate enforces every issue type knip reports by default.
The `knip.json` ignore-block documents the two muted surfaces
(`src/components/ui/**` for shadcn parity, `src/lib/validations/**`
for the contract-type API surface) so any new addition that breaks
either invariant trips CI immediately.
Every `z.infer<>` source-of-truth type the residual flagged had no
external consumer:
- `MetricSource`, `AIRecommendation`, `AIRecommendationRationale`,
`AICitation`, `AIWarning`, `StoryboardAnnotation` in
`src/lib/ai/schema.ts`
- `InsightFinding` in `src/lib/ai/types.ts`
- `CoachChatRequest` in `src/lib/ai/coach/types.ts`
- `ApplyProfileInput` in `src/lib/auth/profile-update.ts`
- `CreateSideEffectInput`, `ListSideEffectsInput` in
`src/lib/medications/side-effects/validators.ts`
- `AnalyticsSlicePayload` in `src/lib/queries/use-analytics-query.ts`
- `WithingsWebhookAuthOutcome` in `src/lib/withings/webhook-handler.ts`
Plus three stragglers — `ThresholdOverride` (the persisted shape is the
sibling `ThresholdOverridesJson`), `ChartRangePreset` + the now-orphaned
`CHART_RANGE_PRESETS` constant it was derived from (no chart reads the
preset list), and the `WebPushChannelConfig` + `ApnsChannelConfig` +
`ChannelConfig` triplet in `src/lib/notifications/types.ts` (the
dispatcher imports the two real channel configs by name; the empty
records and the union added nothing).
`src/lib/validations/**` keeps its `z.infer<>` types — that's the
external API surface the iOS client matches against. Muted via
`ignoreIssues` so the gate can enforce while the contract surface
stays self-documenting.
Every shadcn UI re-export the simplifier residual flagged
(`AlertDialogMedia/Overlay/Portal`, `AvatarBadge/Group/GroupCount`,
`badgeVariants`, `buttonVariants`, the `Card*` / `Dialog*` /
`DropdownMenu*` / `Select*` / `Sheet*` / `Table*` / `tabsListVariants`
tier) is part of the upstream `new-york-v4` registry — confirmed
against `https://ui.shadcn.com/r/styles/new-york-v4/*.json`. Keeping
them in tree preserves library-shape parity so the next shadcn refresh
applies cleanly. Mute the whole `src/components/ui/**` surface for
the `exports` + `types` issue tiers via `ignoreIssues`.
`ignoreExportsUsedInFile: true` covers the schema-source-of-truth
pattern across `src/lib/ai/**` + `src/lib/validations/**`: the schemas
are exported for the inferred `z.infer<>` type but only referenced
internally for composition. Knip's default behaviour flagged them as
unused exports; the option teaches it that same-file references count.
Two genuine dead helpers go: `requireAdmin` in `src/lib/auth/session.ts`
shadowed the real implementation in `src/lib/api-handler.ts` and had
no callers, and `readDailyMeans` in `src/lib/rollups/measurement-read.ts`
was an unused convenience wrapper around `readRollupBuckets`.
`tokenKind`, `withBackgroundEventSafe`, and `isRollupFresh` had no
callers anywhere in the tree, so the symbol + JSDoc + index re-export
are removed. `PROGRESS_TICK_RECORDS`, `MAINTAINED_LOCALES`, and
`SUB_PAGE_METRIC` lose the `export` keyword — every reader lives in the
same module, so the constants stay local. `isRollupFresh` was deferred
in v1.4.41 pending the persistent-rollup reader landing on develop; the
read-path now carries its own per-tier freshness probe and never
imported the helper.
No behaviour change. Knip drops from 35 unused exports to 29.
W1-KNIP's reconcile callouts identified two re-exports that no caller
ever consumed:
- `src/lib/tz/resolver.ts:51` re-exported `listSupportedTimezones` from
`./format`, but both call-sites (`components/settings/timezone-picker.tsx`
and `components/admin/general-settings-section.tsx`) import directly
from `@/lib/tz/format`.
- `src/components/medications/glp1-medication-card.tsx:595-596`
re-exported `describeInjectionSite` "so the parent doesn't need to
import it", but every real consumer (`therapy-timeline.tsx`,
`injection-site-picker.tsx`, the unit test) imports the source from
`@/lib/medications/injection-sites` directly.
Drops both re-exports and the now-orphaned `describeInjectionSite`
import in `glp1-medication-card.tsx`. Clears the last knip findings so
the gate flip in the next commit lands clean.
The settings / medications / admin / hooks tree still declared
bare-literal `queryKey: [ … ]` arrays at ~70 call sites. Centralise
them through `queryKeys.<entry>()` so mutation-invalidation prefixes
stay symmetrical and a future rename only touches one location.
New factory entries cover per-medication compliance / titration /
cadence / glp1-details / drug-level-chart / intake-list, the
Withings status read, the paginated admin audit-log filter, and the
workouts-recent list opts. Existing entries cover everything else
(passkeys, tokens, settings, telegram, integrations, research-mode,
admin status / users / backups / feedback / host-metrics / app-logs /
ai-quality / assistant-flags, coach-prefs, feature-flags, etc.).
Lockstep updates:
- `eslint-plugins/healthlog/queryKey-factory.js` extends
`GUARDED_DIRECTORIES` to cover `src/components/{settings,
medications,admin}` + `src/hooks`, plus the three guarded files
under `src/app/medications/**` and `src/app/targets/page.tsx`.
- `src/lib/__tests__/query-keys.test.ts` mirrors the guard's
`guardedRoots` and pins the shape of every new factory entry.
Tests stay green: 4 736 unit; the factory walker reports zero
bare-literal `queryKey:` in any newly-guarded surface.
Adds `pnpm check-env` — a CLI tool that reads
`scripts/env-manifest.json` and verifies every required env var is
present + non-empty. Exit code 0 on green, 1 on missing required vars,
2 on a malformed manifest.
Two modes:
pnpm check-env # against process.env
pnpm check-env --file <path> # against a Coolify export / .env.production
The manifest is JSON so it's git-trackable and reviewable. Groups can
be marked `required: true` (boot-blocking) or `required: false`
(silently disable a feature). An `allOrNone: true` group emits a
synthetic <all-or-none> required row when partially populated — this
is the v1.4.40 AP-2 silent-disable pattern (three of four APNS_* set,
.p8 missing) made into a hard failure.
Documents the workflow in docs/ops/env-check.md including the planned
v1.4.43 CI integration. 16 unit tests pin parseEnvFile + checkEnv;
renderResults stays untested as a thin shell over the pure helpers.
This is a CLI tool, not a runtime gate — the app still boots without
this check having run. The point is to make the operator-visible step
between "Coolify env block edited" and "deploy fires" loud about
missing vars before the deploy ships.
Withings sometimes replies HTTP 200 with a non-zero body status, empty
measuregrps, or a transient 503. Pre-v1.4.42 the client surfaced all of
those as identical `Error("Withings <verb> error: <status>")` throws,
and the sync layer used a message regex to bucket reauth vs transient.
That made rate-limit (601) and contract-mismatch (293/294) responses
silently retry forever without ever reaching an admin alert.
Adds `src/lib/withings/response-classifier.ts` — a pure
`(httpStatus, body) -> {success | transient | reauth_required |
persistent}` function plus a `WithingsApiError` subclass carrying the
verdict. The four client entrypoints (exchangeCode, refreshAccessToken,
fetchMeasurements, subscribeWebhook) and both v2-sync paths
(sync-activity, sync-sleep) now throw the typed error; sync.ts reads
err.classification directly via classifyError() and falls back to the
legacy regex when the prototype is lost on a pg-boss retry.
Extends FailureKind with `persistent`. Persistent failures audit with
kind="persistent" and surface a distinct admin-alert label
("persistent error" / "investigate the upstream contract") but keep
state=error_transient so the next sync still runs once the upstream
side fixes the mismatch.
Three test branches pinned (37 new tests, 103 -> 140 passing in
src/lib/withings + src/lib/integrations).
The v1.4.41 W-FRONTEND-FACTORY placeholder rendered as the per-tile
Suspense fallback shipped without the trend-card's flex column or
min-height. The all-suspend edge case (every tile suspending
simultaneously, e.g. a future RSC hoist of every cell) collapsed the
grid row to zero height and the placeholder chrome diverged from the
live tile shell by a couple of layout utilities.
Match the trend-card's container classes byte-for-byte: add flex /
min-w-0 / flex-col for chrome parity and min-h-[6rem] so the fallback
holds the row open during a synchronous all-suspend transition.
Structural Suspense test pin keys on the aria-hidden marker, not the
className, so it stays green without modification.
iOS HealthKit observer queue routinely carries the same logical workout
from two paired sensors (Apple Watch + Withings ScanWatch) in a single
ingest batch. Without write-time dedup both rows persist (their
externalIds differ, so the (userId, source, externalId) unique index
never fires) and the read-time picker has to carry the duplicate
forward on every dashboard render.
Add pickCanonicalWorkoutRows() at src/lib/workouts/canonical-rows.ts as
a pure pre-createMany pass invoked by POST /api/workouts/batch. Groups
by (userId, activityType, startedAt +-90 s); prefers the canonical
source ladder (APPLE_HEALTH > WITHINGS > MANUAL > IMPORT) shared with
the read-time helper; breaks ties on calories > earliest createdAt >
input order. Losers surface to the iOS client as duplicate so the sync
cursor advances past them identically to the externalId case.
pr-detection-worker.ts ran its warm-up count and best-measurement scan
across every Measurement row including soft-deleted ones. A user who
deleted a row that happened to be their PR would not get the next-best
row promoted on the next detection pass. Add `deletedAt: null` to both
where clauses to align with the v1.4.40 soft-delete completeness sweep.
Also document the offhost-backup DR-intent at the matching findMany —
that path deliberately includes soft-deleted rows because the offhost
upload is a disaster-recovery snapshot, not a user-facing export.
src/lib/doctor-report-data.ts was checked in as raw bytes (file(1)
reported "data", git diff reported "Binary files differ") because the
control-character regex carried literal NUL + US + DEL bytes inside the
character class. Rewrite the class as `[\x00-\x1F\x7F]` so the file is
UTF-8 clean and future diffs are readable. Same runtime behaviour — the
existing 18-case test suite still passes.
The trend-card row in src/app/page.tsx carried two adjacent comment
blocks describing the same `<Suspense>` boundary — one from the v1.4.40
W-RSC seed and a second from the v1.4.41 W-FRONTEND-FACTORY fallback
hoist. Collapse into one block keyed to current behaviour with a
one-line history trailer. The sibling chart-row Suspense stayed in
single-block shape so no second consolidation was needed.
Each of the seven src/lib/insights/*-status.ts files carried an identical
20-LOC block defining BERLIN_DAY_FORMATTER plus toBerlinDayKey. Extract
the helper into src/lib/tz/resolver.ts (next to dayKeyForScheduledFor and
friends) and import it everywhere. Same runtime behaviour, 100-LOC drop.
Records the helper landing + widgets-route swap and stages the
v1.4.43 rollout queue (41 sites in src/app/api/** still on the
issues[0].message pattern). Includes the rewrite recipe + iOS-contract
prioritisation so the follow-up wave can pick up without re-discovery.
The PUT /api/dashboard/widgets route surfaced only the first Zod issue
on 422, which forced iOS clients debugging the widget-layout contract
into one round-trip per wrong field. The route now wires the shared
returnAllZodIssues helper so every issue lands in details.issues at
once.
Adds an append-only audit-ledger breadcrumb keyed
dashboard.widgets.validation-failed so the operator can grep
/api/admin/audit for the same trail outside the iOS dev console. The
write is fire-and-forget — the validation response is the contract,
the audit row is best-effort.
Tests cover 2 + 3 simultaneous issues, audit-row shape, params-leak
guard, and audit-write-rejection survival on both the unit and
integration tiers.
Historic pattern was apiError(parsed.error.issues[0].message, 422) which
dropped every issue past the first. Mobile callers debugging a contract
needed one round-trip per wrong field. The new helper returns every
issue under details.issues, additive to the existing envelope so any
client that only reads `error` keeps working.
Sanitises issues to {path, code, message} — issue.params can carry the
raw rejected user input for some Zod codes and must not leak.
Carry-over from the v1.4.41 marathon reconcile — the six W10 reviewer
reports (code-reviewer, security, design, senior-dev, product-lead,
simplifier-residual) were not committed before the squash-merge to
main. Lands them on develop so the next marathon's audit-trail has
the inputs that gated the v1.4.41 tag in one place.
Eight touch-disjoint waves landed on develop before this release:
- W-INSIGHTS-HOT + W-RECONCILE-INSIGHTS — extract the timeout-stub
persist helper and wire all seven *-status routes through it, closing
the recurring ~14 s warm response the iOS client sees on dashboard
open once the AI provider has stalled once that day.
- W-DELETED-2 — close the remaining three soft-delete reader-tier gaps
(export bundle, gamification achievements, doctor-report aggregator)
with a five-route integration-test pin.
- W-ORG — types consolidation into src/types/analytics.ts and
src/types/backups.ts; prompt directory unification.
- W-FRONTEND-FACTORY — queryKey factory expansion + ESLint rule lift.
- W-IOS-COORD — POST /api/auth/check-user four-branch onboarding
discovery, identifier matched verbatim, per-IP rate-limited.
- W-SIMPLIFIER — today-intake projection helper extract; unused `tx?`
params dropped; 13 knip-flagged dead exports trimmed.
- W-PERF-OPS — v1.4.39 W-SUM legacy-NULL UNION discovery arm retired.
- W-PROCESS-DOCS — pg.Pool sizing operator doc; ESLint rule
formalisation; knip CI gate staged for v1.4.42 flip.
No migration, no env-var change, no API contract break for iOS v0.5.4.
The new check-user endpoint is an additive surface. AP-2 .p8 key is
installed; SB-5 time-sensitive APNs is live as of this deploy.
pnpm test --run green at 4732 passing / 1 skipped.
pnpm typecheck, pnpm lint, pnpm knip green.
Captures the helper extract + 6 sibling-route wire + cache-shape
reconcile + test pins. Notes the medication-compliance stub-row
recogniser fix and the pulse-status-timeout assertion flip as
risk callouts.
Bring blood-pressure, weight, general, pulse, mood, and
medication-compliance into parity with bmi-status. On a 20 s
provider stall every route now persists a sentinel auditLog row
keyed to today's Berlin day so the next mount short-circuits at
the cache lookup instead of re-racing the same provider call on
every cold visit. Eliminates the ~14 s warm response on every
dashboard open after a one-off provider hiccup.
The seven routes share one persist path through
`persistTimeoutStubAndReturn`; ~120 LOC of duplicate
`auditLog.create` calls collapse into the helper.
medication-compliance returns a richer `{summary, medications}`
envelope rather than the standard `{text}` shape, so it calls
the helper for the persist side-effect and maps `summary` ←
`text` on the way out. Its cache-read also gains a stub-row
recogniser so the second-mount short-circuit fires on the
helper's `text` + `timeout: true` payload.
Tests cover both arms per route: the persist-on-timeout path
asserts a single auditLog.create with `timeout: true`, model
`timeout-stub`, and today's Berlin dateKey; the
short-circuit-on-stub path asserts a subsequent mount returns
the cached stub text without invoking the provider.
Pull the 40-line auditLog.create block out of bmi-status into a
dedicated `persist-timeout-stub.ts` helper so every InsightStatus
route can share one persist path instead of copy-pasting the
sentinel-row shape. The bmi route now calls the helper in a
6-line return; behaviour and persisted body are byte-equivalent
to the inline form.
Sets up the next commit, which wires the six remaining sibling
routes (blood-pressure, weight, general, pulse, mood,
medication-compliance) to the same helper and closes the
last bare-fallback shape on the dashboard hot path.
Two small follow-ups to the v1.4.41 inline-QA reconcile:
- eslint.config.mjs gains `.claude/**` in the globalIgnores list so a
mid-marathon Claude Code worktree on an older base commit can no
longer poison `pnpm lint` locally with pre-fix warnings. CI never
walks that path; the change is purely for the dev environment.
- The check-user route test mock objects for `checkRateLimit` carried
a stray `limit: 30` field that does not exist on `RateLimitResult`
(the real type is `{ allowed, remaining, resetAt }`). The literal
was harmless at runtime but flagged by `tsc --noEmit` on the
test-file declarations. Drops the field at all three mock sites.
The W-DELETED-2 integration-test file header promises coverage for five
reader tiers; v1.4.41 shipped pin assertions for three (`/api/export`,
`/api/export/full-backup`, `/api/export/measurements`) but never asserted
the two routes that the W-DELETED-2 fix actually touched:
- `/api/gamification/achievements` — reads recent measurements via
`findMany` to compute streak / badge progress. A tombstoned row must
never count toward a milestone.
- `/api/doctor-report/availability` — four parallel `measurement.count`
probes drive the section-availability switch. A soft-deleted history
must not light up a section the user has since wiped.
Adds one `it()` block per route asserting that every measurement
read scopes to `deletedAt: null`. Mocks already in place; no
test-infra work needed.
Two clarity regressions surfaced by the v1.4.41 QA round.
1. `src/types/analytics.ts:37-38` imported `DataSummary` twice from the
same module — once bare, once aliased as `DataSummaryType` — with
the alias used on a single line. The duplicate shape was carried
verbatim from `src/app/page.tsx` where both names existed
historically; in a fresh module there's no reason for two import
lines. Collapses to a single import; `glucoseByContext` now uses
the canonical `DataSummary`. Same fix replayed in `src/app/page.tsx`.
2. `src/app/insights/page.tsx:100` and
`src/components/onboarding/getting-started-checklist.tsx:75` carried
their W-ORG alias-back-import in the middle of the file, after
non-import statements. Lifts both into the top-of-file import block
alongside their peers; the aliased name (`as AnalyticsData`) stays
for call-site stability, the W-ORG header comment moves with the
import.
The v1.4.41 W-PROCESS-DOCS promotion of the test-guard substitute to a
real ESLint rule shipped with a whitelist (`charts`, `comparison`,
`page.tsx`, `use-auth.ts`) narrower than the companion test-guard's
`guardedRoots` (which also covers `app/auth`, `app/notifications`, and
`components/settings/about-section.tsx` — the three surfaces W-FRONTEND-
FACTORY migrated in the same release).
Without the mirror, a bare-literal `queryKey: [ ... ]` regression in any
of the three v1.4.41-migrated dirs would slip past IDE / `pnpm lint`
and only be caught by `pnpm test`. The lint rule and the test-guard
should fail at the same boundary.
Extends GUARDED_DIRECTORIES + GUARDED_FILES to match the test-guard
list, with an inline comment cross-referencing the migration wave.
Two correctness gaps on the v1.4.41 onboarding-discovery endpoint,
caught in the v1.4.41 QA round:
1. The route called `identifier.toLowerCase()` before the OR-match on
`username` / `email`, but `registerSchema` applies no transform on
write — both columns are stored exactly as typed. A user registered
as `MixedCase@Example.com` would never resolve through this route,
and iOS onboarding would route them to the sign-up branch despite
an existing account.
2. The route header claimed an edge-level limit on `/api/auth/*`
covered brute-force enumeration; no such middleware exists. Sibling
`/api/auth/passkey/login-options` carries a per-route throttle. We
mirror that shape — `checkRateLimit("auth:check-user:<ip>", 30,
15 * 60 * 1000)` — and rewrite the header comment so the rationale
matches reality.
Tests pin both invariants: a `MixedCase@Example.com` probe hits
`findFirst` with the case preserved, and a 429 short-circuits the
`findFirst` call entirely.
v1.4.41 W-FRONTEND-FACTORY (UX M1) swapped the tile-strip Suspense
fallback from `null` to an `aria-hidden` placeholder div that mirrors
the trend-card chrome, preventing CLS when a future RSC hoist makes
the tile body suspend. The literal regex pin in the boundary test was
not updated alongside, leaving a red on develop tip.
Updates the regex to match the new structural contract and refreshes
the surrounding comment so future contributors see why the fallback is
no longer `null`.
Narrow each helper's visibility to its actual reach instead of leaving
it exported "just in case":
- Drop the `export` keyword on file-local helpers / constants:
`getTrendSentiment`, `isPRWithin30Days`, `buildCoachMetricSourceType`,
`countPassedSchedules`, `getGithubConfig`, `isComparisonBaseline`,
`DASHBOARD_LAYOUT_VERSION`, `INTL_LOCALE_MAP`, `SIDE_EFFECT_NOTES_MAX`,
`MEDICATION_CATEGORIES`, `openApiBase`, `ALL_MOOD_GRANULARITIES`.
- Remove never-used dead code:
`CodexOAuthNotConfiguredError` (declared as a backwards-compat shim
no caller ever consumed), `editMessageReplyMarkup` (Telegram method
nothing on the bot dispatch tree calls), and the stale
`DISPLAY_TIMEZONE` re-export from `src/lib/format.ts`.
Knip drops from 48 -> 35 unused exports. Pure narrowing / deletion;
no behaviour change.
`recomputeMoodBucketsForEntry`, `recomputeMedicationComplianceForDay`,
and `recomputeMedicationComplianceForEvent` all declared an optional
`tx?` Prisma transaction client but no call site has ever passed one —
every caller (routes, jobs, worker, tests) invokes them outside a
`$transaction` block. The dead surface area kept dragging a
`PrismaLike` / `PrismaTxOrClient` alias plus a top-level `Prisma`
import along with it. With the param gone the helpers simply close
over the singleton `prisma` client; the alias and the
type-only import follow.
No call-site changes needed — the param was optional everywhere.
Both `/api/medications/intake?scope=today` and `/api/dashboard/summary`
carried their own copy of the v1.4.39 projection + idempotent
`createMany` + per-`(med, day)` compliance recompute. The duplicate
implementations had already drifted twice; fold them into one tested
location at `src/lib/medications/scheduling/project-today-intakes.ts`
so future changes (new IntakeSource literal, new schedule cadence,
audit metadata) only land once.
The helper stays read-shape-neutral — the two routes return different
projections of the resulting events, so the caller still owns its
typed re-read. Returns `{ projected, backfilled }` for the existing
`annotate` telemetry.
Net: ~200 lines collapsed into 145 (helper) + two short call sites.
The insights page kept `CorrelationResult` + `DataSummary` type imports
left over from an earlier refactor, and the slim summaries slice still
referenced `RollupGranularity`, `aggregateWmyBuckets`, and
`RollupBucketRow` after the v1.4.37.2 GROUP BY rewrite folded that work
into the narrow aggregate. Pure deletions — both helpers stay exported
for the test suite that still exercises them.
Mirror the bmi-status (v1.4.37) timeout-stub persist into the two
sibling endpoints. On a 20 s provider stall the route now writes a
sentinel auditLog row keyed to today's Berlin day so the next mount
short-circuits at the cache lookup instead of re-racing the same
provider call for the rest of the day.
This eliminates the ~14 s warm response on every iOS dashboard open
when the provider has stalled once that day. Response envelope
unchanged; iOS v0.5.4 contract preserved.
Tests cover both arms: the persist-on-timeout path (one create with
timeout:true, model:"timeout-stub", today's dateKey) and the
short-circuit-on-stub path (subsequent mount returns cached text,
provider never invoked).
The `sum_value IS NULL` UNION arm in `enqueueBootTimeRollupBackfill`
was added in v1.4.39 W-SUM to converge DAY rollup rows that pre-dated
the writer change. Production data has since converged, so the arm is
a permanent no-op seq scan over `measurement_rollups`. Per-day missing
coverage remains the sole discovery anchor.
Drops the legacy text-anchor unit test. The read-side `mean * count`
fallback in `/api/dashboard/summary` retains legacy-row readability
and stays covered by its dedicated route test.
Append a Postgres connection-pool sizing section to the self-hosting
scaling guide. Documents the v1.4.40 W-POOL default (20 slots per
container), the container_count × DATABASE_POOL_MAX × max_connections
envelope (with a 1-5 container ready-reckoner table), the env-var
override path, and PgBouncer as the escape hatch for deployments that
outgrow the per-process pool model. Cross-references src/lib/db.ts and
the empirical trace that motivated the bump.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-v1.4.41 prompts lived in two homes — `src/lib/ai/prompts/` (per-
metric system prompts + safety contracts) and `src/lib/insights/prompt
*.ts` (user-prompt builder + comparison snapshot + the `compactSections`
helper). A new reader could not tell from filenames which file to
extend; both directories looked equally authoritative. Audit rec #3
flagged the split as the only undiscoverable seam in the AI layer.
Move both files into `src/lib/ai/prompts/`:
- `lib/insights/prompt.ts` → `lib/ai/prompts/insight-system-prompt.ts`
- `lib/insights/prompt-compact.ts` → `lib/ai/prompts/compact-sections.ts`
- co-located tests follow
Three importers updated: `app/api/insights/generate/route.ts`,
`lib/ai/coach/snapshot.ts`, the generate route test (the
`vi.mock('@/lib/insights/prompt')` path moved too). Prompts now have
ONE home and ONE mental model.
v1.4.41 W-DELETED-2 — assert that /api/export, /api/export/full-backup,
and /api/export/measurements all scope the measurement read to
deletedAt: null. Shape-level guard so a future refactor cannot silently
drop the filter and resurrect deleted rows in user-downloaded files.
Routes the registration-status, auth invalidation, notifications
preferences, and Settings about-section api-version reads through
the centralised queryKeys factory. Adds factory entries for the
remaining settings/admin surfaces a follow-up will migrate, and
extends the walker test to fail on any future bare-literal
queryKey in src/app/auth, src/app/notifications, and the
about-section. Also swaps the dashboard tile Suspense fallback
from null to a layout-stable card placeholder so a future RSC
hoist of any tile slot can't trigger CLS.
Captures SB-7 follow-up (check-user endpoint shipped), SB-8 (merge
semantics documented, no code change), SB-9 (already retired, no
streak surface exists), and the dashboard-widgets PUT 422 root cause:
the Zod validator is correct and additive-safe; the live 422s from
iOS v0.5.4 are an iOS-side payload bug (likely an unknown widget id
or an out-of-range order value). No server fix viable.
ls src/app/api/streak returns No such file or directory. No web
frontend code paths reference /api/streak/...; the only streak
references in the API tree are derived fields inside
/api/insights/targets (streakDays, streakHighlight).
Decision: nothing to deprecate. The route family either never existed
or was removed before the audit. iOS can safely treat /api/streak/*
as 404. Document filed so a future audit does not re-open the
question.
The dashboard layout resolver in src/lib/dashboard-layout.ts is
append-only on read: new widget ids added to DASHBOARD_WIDGET_IDS
automatically appear for every existing user on next GET, with
visible / tileVisible / order taken from DEFAULT_DASHBOARD_LAYOUT. No
database migration is ever required when introducing a new widget.
Documents the merge contract so a future widget-introduction does not
ship a one-shot SQL migration that would clobber every user's
customisation. PUT preservation rules captured: chartOverlayPrefs is
preserved-on-omit, comparisonBaseline is not. Investigation-only — no
code change.