Both guards timed out at five seconds under a loaded unit suite. The
timezone formatter guard grepped the whole of src, the generated client
included, once per scan; it now reads the tree once without the generated
client and keeps a file only when a plain substring says a pattern could
match. The threshold literal guard parsed every client file once per rule
and twice per run; each file is parsed once, each rule's hits are found
once, and a file is parsed for a rule only when it carries one of the
rule's markers. A sample that the markers would skip fails the matcher
case, and a planted literal still fails the allowlist case.
Measured alone: the timezone guard 5.5 s to 0.45 s (its floor case 1.9 s
to 2 ms), the threshold guard 4.6 s to 0.54 s (its non-empty case 1.1 s to
0 ms, reusing the hits it already found).
The regenerate button aborted its inline POST after a fixed 45 s, so on a
slow local model every regenerate was cut while the server still had
minutes left on the person's setting. It now uses clientAbortMs over the
route's sequential model calls, never less per call than the briefing's
own 180 s default, because the published value cannot say whether the
person set it.
A save of the about-me self-context derives the clarifying questions
inline and ran under the 15 s request default, which reported a failed
save while the server went on to commit it. All six save sites now take
their abort from the same setting. Every other browser request that waits
on a model either has no client timeout or only queues the work.
Refs #1267
The batch route counted arrivals inline and grew past its size baseline
with the new-sample rule. tallyHealthKitArrivals now owns the fold, the
route hands it each landed entry and hour with whether it brought a new
value, and the size baselines move down to what the route is now.
Refs #1259
A batch that carried a type with duplicates only moved its
lastReceivedAt, lastTrigger and lastBackgroundAt, so a type whose live
delivery had stalled kept reading as freshly arrived for as long as a
replay, a retry or an older client re-sent its old samples.
The arrival ledger now moves those stamps only for a type the batch
brought at least one new or changed row for. An unchanged stats:
re-post and an hour posted again with its stored value count as
duplicates here; the wire keeps answering updated for them. A
duplicates-only type stamps the new last_seen_duplicate_at column on
its existing row and nothing else. The intake bulk route's dose entry
follows the same rule through the shared recorder. lastSyncedAt and
healthKitLastSyncedAt are unchanged.
GET /api/integrations/healthkit serves the stamp additively as
metricFreshness[].lastSeenDuplicateAt, and the Apple Health card names a
re-send that came after the last arrival.
Refs #1259
Each of these asked the same question several times or did the expensive
part before the cheap one: the threshold guard parsed every client file
once per rule and twice per run, the i18n reverse check rebuilt its scan
three times and compared every leaf with every literal key, the step-up
caller guard stripped comments and searched every route path in every
file per case, the plausibility guard ran its writer sweep three times,
and the cycle verdict guard parsed every client file to find out whether
it mentions a cycle. Each now works a result out once, and a file is
parsed or searched only when a substring says it can match. A planted
violation still fails the threshold, plausibility, cycle verdict and
backup plan guards.
walkSourceFiles now lists each indexed folder (src, e2e, prisma) once
per worker and filters it by prefix, readSource and readSourceFolder read
through the same memoised file cache, and readSourceText is the cached
read for guards that read files themselves. The guards project shares a
module graph per worker, so a file is read once however many guards ask
for it. sourcesContaining adds the substring pre-filter for sweeps that
look for a few markers. Every guard over 1.5 s now reads through it;
anything outside the indexed folders is read from disk as before.
One bottom sheet on top of the Radix dialog, so focus, Escape, the
backdrop, aria and the page scroll lock stay where they were. The sheet
follows a touch with a transform, the backdrop lightens with it, and a
release past a quarter of the height or a flick down closes it; anything
less springs back. Grip and header always drag; content only from its
very top, and sideways gestures never count.
Every `side="bottom"` sheet and ResponsiveSheet on a phone render through
it: the More sheet, the capture and form sheets, the Coach drawer on a
phone and the day. The day opens on the new preview step and pulls up to
90 dvh, its footer held on the bottom edge. Focus now returns to whatever
opened a sheet, not only to a Radix trigger. The command palette fills
the window and keeps no grip or drag.
Each of the guard's scans started its own grep over the whole of src,
the generated client included, five times per run, and the floor case
timed out at five seconds under a loaded unit suite. The tree is read
once now, without the generated client, and a file is kept only when a
plain substring says a pattern could match; each scan's result is
reused by the cases that ask about it. The file runs in 1.9 s instead of
6.5 s alone, and the floor case in 2 ms instead of 2.3 s.
The profile section in docs/contributing/benchmarks.md says how to run
scripts/bench/boot-profile.ts, what the profile of the v1.44 runtime
found (the worker graph as one 753 ms block, a live heap of about
165 MB of which the query compiler holds 50 and the catalogs 33) and what
changed. scripts/bench/results/v1.45.0-*.md hold the before and after
numbers per area, from back-to-back runs on the same machine. The size
baselines take the lowered numbers.
Re-importing an Apple Health export the account already holds sent one
UPDATE per row the insert skipped, about two million statements for a
large export, nearly all writing back what was stored. One read per chunk
now finds the rows whose stored values equal what the update would write;
they count as updated as before, without the statement. A key the chunk
carries twice is written in order as before. The natural-key rescue moves
into the same module.
A long-window read of a dense series sent two statements over the same
canonical rows, the day or hour buckets and the strip's figures, so a
ten-year pulse read picked the canonical source per day and scanned the
readings twice. Both aggregates now run in one statement over the window
materialised once, with the same SQL each had (dense-series-read.ts).
An integration test keeps the two old statements as the reference and
requires the same buckets and figures for pulse (two ranked sources),
glucose and oxygen saturation, day and hour grains, in UTC,
Europe/Berlin, Asia/Kolkata and America/New_York, and the same empty
answer for a type without readings.
The two-ended checks ask every declared file about every model and each
of its relations, and read the schema and the files again for each
question. With the backup split into more files that took two seconds a
case and timed out on a loaded runner; the reads are memoised now.
Nothing renders through next/image, so /_next/image only offered a route
that fetches, decodes and re-encodes whatever local path a request names.
With images.unoptimized Next answers it with 404 before reading a
parameter. A guard fails when next/image is imported while the optimizer
is off, or when the optimizer returns with SVG input allowed.
The element schemas move out of validations/backup.ts into one
<domain>-backup-schema.ts per domain, beside its export and restore, with
the shared primitives in backup-schema-kit.ts. validations/backup.ts now
composes the slices into backupPayloadSchema in the same key order, with
the same defaults and the checks that span sections, so the parsed
payload, every validation message and the OpenAPI contract are unchanged.
The lazy-schema guard now covers the slices too: a slice is reached only
through the payload schema or another slice, never from a domain's export
or restore. The external-id guard carries the payload's exemption to its
slices, and mutation testing scores the slices with the payload schema.
buildFullBackupPayload now asks every domain for its slice at once and
lays the slices out in the file's key order (backup-export-sections.ts),
merging the manifest where it first appears. The measurement, medication,
mood, instance-settings and nutrient readers and serialisers move out of
the payload builder into their domain modules; the records section keeps
its composition and manifest, while labs, illness, allergies, family
history, workouts and documents each export from their own module. The
measurement restore half moves to measurements-restore.ts so the export
graph does not pull in the restore's dependencies.
The file, its counts and their order are unchanged; the golden copies
still match. The measurement completeness guard now reads the domain
module and also checks that every carried column is written back.
next/font/google downloads Inter from fonts.googleapis.com and
fonts.gstatic.com while the app builds, and when that request fails the
build fails with it, which is what broke the arm64 image build and
several CI jobs. The files it downloaded now live in src/app/fonts/inter
under the SIL Open Font License, and next/font/local declares them.
They are the same bytes: Inter 4.001, one variable file per script
subset (Cyrillic and its extension, Greek and its extension,
Vietnamese, Latin Extended, Latin), each used for weights 400 to 700.
The declarations rebuild the stylesheet Google answered, the twenty-eight
faces with Google's unicode ranges in Google's subset order, swap
display, and only the Latin file preloaded. The local loader emits one
unicode range per call, so each subset is its own call under the
family Inter.
The Arial fallback face and the font-sans stack move into globals.css
with the metric overrides next/font/google shipped for Inter; the local
loader would have computed slightly different ones from the file.
A production build with every outbound request sent to a dead proxy
completes, where the same build with next/font/google fails on the
fonts.googleapis.com request. The content security policy already
allowed fonts from self only. A guard refuses a next/font/google import
anywhere under src.
Every sync marked all of an account's analytics cells stale, so the slice
re-read the pre-fold remainder (every reading older than five years,
folded per source and day, the slice's largest statement on a long
history) and the day view re-read its past history blocks, although a
sync of today's readings cannot change either.
A cache key can now name the instant its value's readings end before
(closedBlockKeyPart). invalidateUserMeasurements takes touchedFrom, the
earliest measuredAt a write created, changed or removed, and leaves such
blocks in place when it is not earlier; without it every block goes as
before. The batch route passes it from the identities it wrote. The
pre-fold remainder moves into its own module and is cached as a closed
block; the day view's past history blocks carry their end, with a day of
margin for a night across midnight.
The batch route's two up-front reads move into batch-stored-rows.ts with
the once-per-batch key rule they share.
POST /api/admin/backups/upload stores a backup file as a new copy, the
copy whose id the published restore takes, and it refuses a file for
reasons an operator has to be able to look up: a key this server does
not hold (backup.key.missing, with the key ids), an owner account that
does not exist here, a newer schema, a copy too large to store
(backup.upload.too_large). It was exempted from the contract as console
plumbing while its two siblings were published for exactly that kind
of refusal.
It is registered with the three bodies it reads (raw JSON, raw gzip, a
small multipart form) and every status it answers; the summary is the
three identity fields plus one integer count per class, so a class the
backup format gains later does not break a decoder. The round-trip
test calls it through the contract now.
restoreBackup becomes an orchestrator: the checks before the first delete
move to restore-preflight.ts, and the transaction runs the steps listed in
backup-domains.ts in dependency order, emptying the account first and then
rebuilding it domain by domain. The inline restores for measurements,
medications, mood, labs, illness, allergies, family history, workouts,
nutrient days, documents, the instance settings and the cleared
notification channels move into one module per domain, beside the
existing section modules. Cross-domain references travel through one
typed context instead of local variables.
The order of every write, the refusals and their audit rows, the skip
report, the cleared counts and the reported phases are unchanged; the
golden copies of the restored account still match. Guards that pinned
the restore by path now point at the domain files.
The SETTING verdicts are now the keys of the codec table, so a column
becomes a setting by getting a codec and nothing else has to name it. The
verdicts for every other User column move beside it into
account-settings-classes.ts, and the restore half into
account-settings-restore.ts. The backup plan keeps the model verdicts.
The classification guard now also refuses a column that has both a codec
and another verdict, since the composed table would quietly carry it as a
setting.
The mocked route tests moved onto the kit take the ratchets down: route
tests mocking @/lib/db wholesale 321 to 274, route tests stubbing
apiHandler 138 to 133, as never casts in tests 6 014 to 5 496, and version
stamp comments 10 634 to 10 592 with the deleted files.
Vitest's --shard cut the file list into slices of equal length, which gave
the integration shards 954 s to 1 449 s. A sequencer now deals files out by
their measured duration, plus a fixed per-file cost for import and setup,
longest first to the shard with the least work, and starts the longest files
first inside a shard. Timings live in tests/durations/*.json and are
regenerated from Vitest JSON reports with scripts/test-durations.mjs; every
CI shard uploads its report.
Integration shards run three workers each, one cloned database per worker.
The unit suite runs as three projects decided per file from its source:
guards and unit share a module graph inside a worker, isolated keeps a fresh
one for every file that mocks, spies, stubs or resets. In CI it is three
duration-balanced shards next to a job for lint, format, types and the
OpenAPI drift check.
The required check names stay as they were. "Lint, Typecheck & Test" is now
the job that answers for the static checks and every unit shard, the way
"integration" already answers for its shards. The browser-install guard
recognises a direct, flags-only vitest run as a run of the unit suite.
Global setup migrates one template database and reads what a fresh install
holds: its schemas, its tables and the rows the migrations seed. Each Vitest
worker clones the template on its first file with CREATE DATABASE ... TEMPLATE
and keeps the clone for the rest of the run, so files run in parallel without
seeing each other's rows, advisory locks or queue schema. Schemas created at
runtime (pg-boss, migration probes) are dropped when the next file starts.
The truncate list is the catalogue now, every table in public except the
migration bookkeeping, instead of a hand-kept list of 42 of 166 tables. The
seeded catalogues are put back after each truncate in foreign-key order, so a
test starts from what a fresh install has. The settings singleton is the one
seeded table a test starts without, declared with its reason.
next/headers and the boot compatibility probe are mocked once in the setup
file. Two lock-wait probes now look at their own database only, since other
workers' backends share pg_stat_activity.
The runtime import graph (static value imports and re-exports, side-effect
imports, dynamic import() with a literal; `import type` erased) is built
with the TypeScript compiler's module resolution under the `@/*` mapping,
over every production module. No new dependency.
- Cycles: 13, counted one per back edge of a sorted depth-first walk (the
number `madge --circular` reports), in 7 strongly connected components:
the audit/geo/notifications/managed-profiles cluster (10 modules), the
compliance facade cluster (9), command palette ↔ store ↔ shortcuts,
modules ↔ cycle gate, boss-instance ↔ boss-ready, mood aggregates ↔
patterns, features ↔ signals-of-day. A new component or a component that
draws in another module fails whatever the count says; a lower count or a
shrunk component fails until written back. The target is zero.
- Re-export-only modules (every statement an `export … from`): 16 listed;
a new one fails.
Proof: two planted modules importing each other failed with
"lib/zz-probe/a.ts <-> lib/zz-probe/b.ts", a planted `export * from "./a"`
module failed the re-export ratchet, and a baseline raised to 14 cycles
failed with the write-back message until RATCHET_WRITE=1 restored it.
Add a ratchet mechanism (helpers/ratchet.ts): per-key counts against a JSON
baseline under src/__tests__/baselines/. A new key or a higher count fails;
a lower count fails until it is written back with
`RATCHET_WRITE=1 pnpm vitest run <guard>`, which only ever lowers numbers.
Raising one is a hand edit in a reviewed change. The baselines are
machine-written, so prettier leaves them alone.
helpers/source-metrics.ts measures the production tree through the
TypeScript compiler: the file list (dot directories included, tests and
generated code out), line counts, every function node with a stable
"outer > inner" name and its length, and the runtime import graph.
Guards and their baselines, measured on this tree:
- source-size-ratchet: 129 files over 800 lines and 415 functions over 200
lines (catalog and data files exempt, each named with its reason; slack
25 and 10 lines before a shrink must be written back).
- test-realism-ratchet: 321 route tests mocking @/lib/db wholesale, 138
stubbing @/lib/api-handler, 6014 `as never` casts in 570 test files, and
400 test files that read a source file by path (reads through readSource
are not counted).
- version-stamp-ratchet: 10634 comment lines in 2985 files that open with a
release version; none may be added.
Proof each can fail: a planted 953-line file with a 253-line function failed
both size ratchets, and one line appended to app/page-client.tsx failed the
file ratchet (2326 → 2327); a planted route test with vi.mock("@/lib/db")
and one `as never` failed both of those ratchets; a planted comment
"// v1.45.0 — probe stamp" failed the stamp ratchet. Raising one as-never
baseline entry by 5 failed with the write-back message, and RATCHET_WRITE=1
restored the file byte for byte. All probes were removed again.
A test that reads a source file by path and asserts an absence
(`expect(source).not.toMatch(…)`) stays green after a split: the code it
watched moved next door and the old path no longer holds it. A missing file
throws; a file that kept its name but lost its contents does not.
- `readSource(path, anchors)` in helpers/source-files.ts reads one file and
requires every anchor (a string or pattern the watched code carries) to
match at least once; it throws on a missing file, a lost anchor, or a read
without an anchor.
- `readSourceFolder(dir, { floor })` is the folder-scan form for a subject
that was split into several files.
- The twelve reads that were asserted only negatively now go through
`readSource` with an anchor: the batch route, the integration-sync
registrar, the dashboard client, the mood insights client, the sleep
overview and sleep page, the sidebar, the baseline form, the account
section (two guards), api-handler's requireAuth and the Google Health sync.
- The two schema parsers the wipe and enum guards share now refuse a parse
of the real schema that finds fewer than 50 models (and 10 enums), so a
moved file or a drifted block regex cannot leave those guards comparing
an allowlist against nothing. Synthetic schemas a test writes stay exempt.
Proof: renaming `setSaving` in the baseline form made the converted
confirm-failure guard throw "the file no longer contains setSaving"; the
helper's own test plants a missing file, a lost string anchor, a lost regex
anchor, an empty anchor list and an under-floor folder, each of which throws.
Add the in-repo rule `healthlog/import-boundaries`:
- a file under src/lib may not import src/components or src/app (alias,
relative, re-export and dynamic forms, type-only included);
- a "use client" file may not make a value import of the database client,
the crypto module, the Prisma client entry, the job queue, pg, pg-boss or
next/headers. Type-only imports and the Prisma enums entry stay legal.
The four lib files that still import upward are listed in the rule's allow
map with a reason each; a listed file that stops importing upward is
reported, so the map can only shrink.
Five of the nine upward imports are gone by moving pure modules to their
layer, without behaviour change:
- supply-runway estimate → src/lib/medications/supply-runway.ts
(read by the low-stock job and the medication list read);
- weekday BYDAY tokens → src/lib/medications/scheduling/weekday-tokens.ts
(read by the medication extraction prompt);
- settings section slugs → src/lib/settings/section-slugs.ts
(read by the record-settings classification);
- the command palette index → src/components/command-palette/ (it is a
client index built from component registries).
Proof the rule can fail: an import of @/components/ui/card appended to
src/lib/medications/supply-runway.ts reported libUpward, and a planted
"use client" file importing @/lib/db reported clientServerOnly; both were
removed again.
The value domain read the plausibility band out of the measurement
validation module, so every metric sub-page that asks whether a metric
can be negative or is integer-valued loaded the measurement schemas and
Zod with them. The band and its check live in a Zod-free module now,
re-exported by the validation module for the server writers, and the
values list checks its segment against the exhaustive type label map
instead of the Zod enum. About fifty metric routes drop roughly 88 KB
gzip from their first load. The client-route guard walks a metric page
and the values list too.
detect_changepoints no longer averages bucket means itself; it hands
them to the shared level-shift detector, so the file has no in-memory
mean left for the guard to list.
The switch was stored and shown, but nothing reads the topics flag
yet, so the page promised a change it could not make. The section,
its nav entry, its loader and its copy are gone, so its palette
entry goes with them; /settings/preview is no longer a route. The
stored flag, /api/settings/preview-features and its backup round-trip
stay, and the column's pending-consumer note says why.
The note named the activity goals among the three new readable routes.
They are the calendar read and the two source provenance reads; the
goals route stays owner-only.
Two streamed endpoints read a spreadsheet record by record with a
column mapping from the query: separator, decimal mark, date order and
zone are stated, never guessed per row. Lab rows resolve to markers the
way scanned reports do, convert units where the conversion is known and
keep what the file said; medication rows map name, dose, daily times,
as-needed and a dated window, checked against the create schema, with
reminders off unless asked for. A dry run writes nothing, a re-upload
only finds duplicates, and every refused row comes back with its line
and reason.
The first page of the workout list now carries the fitness-fatigue model
over the day-total heart-rate impulse the nightly strain run already
stores: fitness as the 42-day and fatigue as the 7-day exponentially
weighted average, form as their difference. A day without a stored impulse
counts as rest. It needs 28 days and three training days, flags the first
six weeks as warming up, and reads in the person's own units only. The
workouts page shows the three figures above the list.
/api/health gives the database two seconds and answers 503 past that.
With three retries, a pool saturated for a minute after the start period
could mark the container unhealthy and take it out of the proxy's
routing. The compose healthcheck and the image's HEALTHCHECK now allow
five misses (two and a half minutes) and end wget after four seconds,
inside Docker's five, so a stalled request is one clean miss. A test
holds both to these bounds and to each other.
The UI standards drop working shorthand, state the spacing scale once in
section 2 (section 13 had listed an older scale without 2.5 and 3.5) and
use no em dashes. The v1.3 to v1.4 migration note no longer claims that
/settings#anchor URLs redirect to their section: the fragment never
reaches the server. A test holds both documents to this.
The Topics preview describes what it will add, the workout-route detail
says off for new accounts, and German reads 'Zurück zu den Modulen',
'Unter Module ändern', 'Unter Anordnung öffnen' and 'Erfassung' for
the Tracking group. Strings that name the Insights area call it
Insights; generic uses of Auswertung stay.
The room-climate guide said both to create one automation per room and
to track only one; it now recommends the bedroom and says why a second
room cannot work (no room name, same-minute pushes collide on 409). The
blueprint offers 5, 10, 15, 20, 30 or 60 minutes, so the push lands on
the stated minutes, and it and the guide's examples use the triggers,
conditions and actions keys of Home Assistant 2024.10 and later. A test
holds all three.
The assessments list now carries, per PHQ-9, GAD-7 and WHO-5, the latest
total against the previous and the first administration, classified
against the change studies treat as meaningful: 5 points on the PHQ-9,
4 on the GAD-7, 10 on the WHO-5 percentage scale. A smaller change reads
as within the threshold rather than better or worse. The instrument
detail states each change and whether it reaches that size, in neutral
words. The read takes totals and dates only, never an answer.
A read-only view per metric and UTC day: every source with readings, its
day value, its place in the person's source priority, and which source
the charts use with the reason. A second card lines two sources up over
30, 90 or 365 days. The pick is explained from the rank the canonical
SQL returns, so the page cannot disagree with the charts. Linked from
the metric pages and from Settings > Source priority.
The standard report template now carries sleep duration, heart-rate
variability as two labelled rows (SDNN and RMSSD), daily steps, a
per-sport workout summary and the change around each medication start.
Each still rides its module, and the template is still only applied by
the button that names it; saved selections and share links are
untouched.
Two structured leaves join the catalogue: WORKOUTS (activity group,
workouts module) and MEDICATION_EFFECT (medications group). The workout
block reads narrow rows through the canonical-workout picker and sums
them per sport. The effect block reuses the efficacy loader with the
report's end as its horizon, carries a target only when that target's
own measurement leaf is admitted, and is capped at eight medications.
In the PDF the wearable series chart at half width, two to a row, in
the units the table prints; the workout and effect blocks are compact
tables with short notes. The clinician view renders both blocks from
the same payload. RMSSD joins the dense day-bucket path so long windows
stay aggregated.