v1.38.14 — the reported bugs at their class, and a gate that stops crying wolf - #945
Merged
Conversation
…etres The Apple export writes DistanceWalkingRunning in the account's own length unit; the record mapping assumes metres and never reads the attribute, so a 2.484 km day lands as 2.484 m. Refs #944
Apple stamps every quantity `<Record>` of an export archive with the account's own display unit, not the unit the mapping table assumes: a metric archive reports walking distance in km, an imperial one in mi, body mass in lb, energy in kJ, temperature in degF. The mapper accepted `input.unit` and never read it, so a 2.484 km day was folded into the day's total as 2.484 m and a decade of imported history read as a couple of metres walked. `hk-units.ts` is now the one place a HealthKit unit is converted, shared by the record path and the workout path (which carried its own km/mi branch — the duplicate is what let the record path stay wrong). It converts only inside a family and only for units Apple can emit, and returns the reading untouched for anything it cannot place, so an archive that already speaks the table's unit is unchanged. Every mapping-table entry whose unit cannot be converted — a dimensionless count, a pinned event, a logarithmic dB level, a compound rate, the percent units whose value rides as a 0..1 fraction — now carries a written reason, and a structural guard fails on an entry that offers neither a convertible unit nor a reason. Refs #944
The three score engines' unit tests hand `persist*Score` a fake Prisma, but `upsertScoreRow` routes the written row through `afterMeasurementMutation`, which reads the `@/lib/db` singleton instead of the injected client. Both legs of that tail therefore opened a socket to a Postgres the unit job does not run: on CI the attempt failed with ECONNREFUSED a few hundred milliseconds later and logged on the way out, after the test that started it had ended. A console write that lands while the worker is closing its RPC channel fails the whole run with `EnvironmentTeardownError: Closing rpc while "onUserConsoleLog" was pending` — four of nine unit-suite failures in thirty days, every one of them with all tests green, blamed on whichever score file the worker happened to be tearing down. Run 31961548185 carries the full stack: recomputeBucketsForMeasurement <- afterMeasurementMutation <- upsertScoreRow <- persistStressScore <- stress-score.test.ts:105. It stayed invisible on a maintainer's machine because a local Postgres answers on 5432: the same call succeeded quietly, and the suite was writing rollup rows for its fixture users into a real database. The four files that drive `upsertScoreRow` stub the tail. That removes the doomed I/O rather than the log line, and the call they were making by accident is now asserted in score-row.test.ts instead: the v1.37.19 wiring is pinned by an expectation rather than by a stack trace. Verified by mutation — dropping the `afterMeasurementMutation` call from score-row.ts fails the new assertion. The settle guard's sweep is widened while here. Its eight-line handler window and five-line registration preamble were both measured against the sites that existed when it was written and both cut through real code: the console write in mood-rollups.ts sits nine lines into its handler, so the one genuinely unregistered detached promise in the tree was never seen, and the registration in fire-and-forget.ts sits below its handler, so a correct site read as a violation. Registration is now looked for wherever it wraps the statement, by paren depth rather than by line count, and mood-rollups.ts registers its WEEK/MONTH/YEAR enqueue. Verified by mutation: unwrapping that call fails the guard. Gate: the four files, twenty consecutive runs, 20/20 green (53 tests each). Test time across the three engine files falls from 708 ms to 20 ms — the round-trips that are no longer made.
`pnpm audit --prod` answers a question about the published advisory database, not about the diff. The same lockfile passes on Monday and fails on Tuesday because someone else published, and the branch that turns red changed nothing: seventeen of the forty-one Security & Quality failures in the last thirty days were advisory publications on branches that touched no dependency (GHSA-jqff-g426-hqxp, GHSA-fph4-wmhf-6fwf, GHSA-f65p-4m7j-42xc and GHSA-5jgf-p345-68v8 in one run alone). A gate that fails for a reason the author cannot act on is a gate people re-run instead of read. It keeps running on every push to `main` and once a day on a new schedule trigger, which is the cadence the question actually has. The other three jobs sit out the scheduled run. Nothing about what ships is loosened: the container scan still builds and scans the image on every push to `main`, and the daily Trivy workflow still scans the lockfile with dev dependencies included and the published image. Gate — the trigger set, read back from the parsed workflow: triggers: push, pull_request, schedule quality | if: github.event_name != 'schedule' dependency-audit | if: github.event_name != 'pull_request' secret-scan | if: github.event_name != 'schedule' docker-security | if: github.event_name == 'push' && github.ref == 'refs/heads/main'
The unit fix only helps the next import. An account that imported an archive before it carries a decade of days understated 1000x, and the authoritative repair — re-importing the archive on the fixed build — needs an archive the account may no longer have. `scripts/repair-apple-health-distance.ts` is dry-run by default and selects rows only by the stamp the archive fold itself writes: the EXPORT_XML_SOURCE_MAX provenance plus the `stats:` external id minted for that fold. Never by "the value looks small", so a row a native sync has since overwritten is left alone. `--apply` multiplies one account per transaction and writes the audit row in the same transaction, which is what makes a second run a no-op. The archive's own unit is named by the operator (`--unit=mi`) because the discarded attribute is the defect itself, and a row whose repaired value would leave the plausible range is reported rather than written. Refs #944
The field carried no description while the server ignored it. It no longer ignores it, so the contract has to say so. Refs #944
`totalEnergyBurned` rides the same account-display-unit contract as the distance beside it: a Health app set to kilojoules writes `kJ`, and the column is kilocalories. Convert it through the shared table. Refs #944
The hero-geometry check stopped skipping under CI in v1.38 and started failing
instead, which was the point: it had caught its own launch failure and passed
for months, measuring nothing. The install step went into the quality job of
`security.yml` and nowhere else, and a second job runs the same suite — the
auto-merge job for dependency bumps. Every Dependabot pull request has failed
since on `browserType.launch: Executable doesn't exist`, four of them on
2026-09-06 alone (run 34043840314), on an error that says nothing about the
bump. One workflow was fixed; the class was not.
That job installs the same headless shell now, off the same cache key. The
error the check throws names both jobs rather than sending a reader to the
workflow that was already correct, and a guard sweeps the workflow directory so
a third job cannot run the suite with no browser anywhere in it. Its limit is
written down: the sweep is file-scoped, so a workflow with two jobs where only
one installs still satisfies it.
Gates:
CI=1, browser path hidden exit=1, "This check is the gate in CI and it
has no browser to measure with"
CI=1, browser present exit=0, 4 tests passed (it measures)
install step deleted from the guard fails: ["dependabot-auto-merge.yml"]
auto-merge workflow
Three of the four watched routes sat within a kilobyte of their ceiling — 444 of 445, 459 of 460, 459 of 460 — because the numbers had been re-stated for four releases and never re-based. A budget a route is already touching does not measure the route; it measures whichever dependency bump landed that week. e2e run 34043772954 failed on `/insights/page: 445 KB gz exceeds the 445 KB budget` with nothing in the diff that touched the bundle, and it was one of at least four e2e failures that were the budget gate rather than a test. The file states the measured value per route now, plus one drift allowance over it, and a route fails on the delta. Measured on a local build at d48bbff: / 447, /insights 434, /measurements 433, /insights/mood 447, allowance 20 KB, so the effective ceilings are 467 / 454 / 453 / 467 — a raise for all four. The allowance covers the runner delta and ordinary week-to-week drift; the step change the gate exists for, a statically imported catalog or a duplicated chart library, is several times larger and still fails. Every run prints the delta, so a route that has crept most of the way through its allowance is legible in the log long before it is red. The reason and the re-base instructions are written into the file's running note, as with every raise before it. A budget file with no baselines used to gate nothing quietly, since the loop had nothing to walk; it now refuses. Gates, against the current build: real baselines passed; /page +0.3, /insights +0.3, /measurements +0.2, /insights/mood +0.3 KB /page baseline 425 (cap 445), FAILED: "/page: 447 KB gz is 21.6 KB over the /insights 413 (cap 433) 425 KB baseline, past the 20 KB drift allowance (cap 445 KB)" and the same for /insights/page, exit 1 routeBaselineKbGz removed exit 2, "states no routeBaselineKbGz entries, so no route is gated"
The window-reset test burned its budget in a 50 ms window: four sequential
round-trips to a containerised Postgres had to finish inside a twentieth of a
second or the window closed under them, the counter reset, and the fourth call
came back allowed. Three failures in thirty days, two of them on `main` — run
33614804032 shows `expected true to be false` at rate-limit.test.ts:56 in a file
that took 1278 ms to run.
The short window bought nothing: the expiry this test is about is forced by hand
a few lines further down, by writing `reset_at` into the past. The burn now runs
in a 60 s window, so no round-trip can outlast it, and the test asserts the row's
count is 4 after the denial — a window that closed mid-burn would also have
produced three allowed calls, and the old assertion could not tell the two
apart.
Gates:
pnpm exec vitest run --config vitest.integration.config.mts \
tests/integration/rate-limit.test.ts
three consecutive runs, 2 passed / 2 passed / 2 passed.
Mutation: with the burn window back at 1 ms the run fails exactly as CI did —
"AssertionError: expected true to be false" on the denial assertion.
The two metric sub-page cases waited for `.recharts-wrapper`, a class the charting library writes and every chart on the page carries, so the gate was satisfied by whichever chart rendered first rather than by the one the route exists to show. On `/insights/mood` that is exactly what happened: the mock carried a single mood entry on a fixed date, the chart withholds its line below three distinct days, and the sparse placeholder painted while some other chart answered the gate. The mood line was never waited for and may never have been scanned. Both charts now carry `data-slot="chart-plot"` on the wrapper their data branch renders — an empty window still paints the empty state and the scan still waits — and the mood fixture is anchored on now with ten daily scores, the same way the measurement rows above it are.
The maximize case clicked the drawer's maximize control as soon as the drawer was on screen. Those are not the same event: the vault records `?doc=` as the way back only once the detail sheet has actually closed with the drawer owning the same document, and maximizing before that sends the close down the other branch, which calls `history.back()` and races the push to `/coach`. The URL then stays on `/documents` and nothing is raised. The sheet's own close control leaves the tree with the sheet, so its absence is that hand-off, observed. The delete case is addressed to the toast's attributes rather than its copy. The literal there has already been wrong once — a trailing full stop moved and the assertion spent its whole timeout hunting a sentence that no longer existed, on every attempt, on both projects.
The scan's fixture was written with a Playwright API context. Every API context in a runner shares one process-wide keep-alive agent, so a request can be handed a socket the server has already closed on its five-second idle timeout, and a POST — unlike a GET — is never replayed on a fresh one. That is what ended this spec twice, on this line, on all three attempts each time: `socket hang up` and `read ECONNRESET`. The dose is written from the page instead. The browser opens its own connection, the service worker is blocked in this project, and the row still goes through the real route, so nothing about the fixture changes except which socket carries it.
FENCE-AC-07 drove its external switch through the browser context's API request context, which shares one process-wide keep-alive agent with every other API context in the runner. A POST there can be handed a connection the server has already closed on its five-second idle timeout, and a POST is never replayed on a fresh one — twice this line ended the spec with `read ECONNRESET`, on all three attempts. The switch is sent with the page's own `fetch`. It stays external in the sense the case needs: the switch UI is what runs the cache wipe and a bare `fetch` is not it, and the service worker returns without touching any non-GET, so the request reaches the network unaltered. The positive control below still proves the cached entry survived.
`glucose_context` is nullable, but every surface that fanned out over the four enum members treated the named list as exhaustive. A meter synced through Apple Health writes no HealthKit meal-time metadata at all, so an account can hold nothing but rows the list cannot match. Add one shared notion of the untagged bucket — the name the Coach's glucose block already used — with a resolver that files NULL, blank and unrecognised values under it, and a grouper that walks the named contexts first and the untagged bucket last. The untagged bucket borrows the RANDOM threshold: an untagged reading is a spot reading, and a fifth threshold would give the user a second editor row judging the same readings by a different number. Labels in all seven locales. Refs #943
Tile eligibility filtered the four named contexts against the per-context summaries and asked whether any of them held readings. An account whose source records no meal time matched none of them, so the tile was omitted with the module on, the layout toggle on, and a reading from today. Nothing warned; the strip was simply short one card. Move the gate out of the page into `resolveGlucoseTiles`, which walks the shared bucket list — untagged included — and hand the caller the label key with the summary. Both summary producers group through the shared resolver, so `UNSPECIFIED` reaches the wire with its own count, latest value and trend, and a tagged account still gets its per-context breakdown. Refs #943
`buildGlucoseTargets` had the same gap as the dashboard tile: it iterated the four named contexts, so an account whose readings carry no meal time got no glucose target at all. Route the loop through the shared grouper. The untagged card is judged against the RANDOM band and is not editable — one threshold, one editor row — so the cog renders disabled the way it does for a derived card. The insights reference panel lists the new target type, otherwise such an account sees no band on the blood-glucose sub-page. Refs #943
Both report paths dropped them: the raw path iterated the named contexts, and the dense path skipped any row without a context outright, so a report built for a meter that records no meal time carried an empty glucose panel while the readings sat in the database. The FHIR export dropped them for the third time, at the LOINC lookup — its own comment already said an unspecified reading codes as 2339-0, but the table had no entry to hit. The insights context breakdown names the bucket, and the Coach's glucose block now resolves through the shared helper rather than its own copy. Refs #943
The defect was invisible from the outside because every behavioural test in the suite seeded a tagged reading. Two checks close that: A structural guard that reads the dashboard surfaces and fails when a tile derives presence from a list of enum buckets with no null / unknown arm and no written reason, following one hop through a named constant. It carries its own positive and negative controls, so a refactor that silently stops matching anything fails here rather than going quiet. An integration test that seeds twenty readings with a genuinely NULL context against a real Postgres and reads the tile back through the same resolver the page renders with: eligible, latest value intact, and a tagged reading still filed under its own context. Refs #943
The step's profile fieldset moves to `baseline-fields.tsx` unchanged. The markup can now be rendered against a server answer on its own, which is where the next commit puts a test.
The onboarding baseline step flashed a toast naming one refused field and walked on to the done screen, stamping the account as set up over a value the server never stored. The value the person typed was gone with the step. A refused field now writes its reason into that field's own error slot, marks the control invalid, and holds the step until the person corrects it. Every refused field is named, not only the first, and the reason comes from the validator code rather than the validator's own prose, in all seven shipped languages.
It shares the route and had the same gap: the banner named the first refused field and nothing marked the input that held the value. Each of the seven submitted fields now carries the reason in its own slot, through the same component the onboarding step uses.
A partial save is a whole-class trap: any 200 read as a clean save throws the person's correction away. The guard enumerates every surface writing to either profile route and requires each to call the per-field describer, or to prove its body carries a single field, for which the partial arm is unreachable.
The onboarding step keeps the preference and the adapter where the save happens and renders the control from its inputs file, so the three conditions belong to the surface, not to one file. The control check is word-bounded now: a wrapper named after it passed a substring check while rendering something else.
…ng them The audit had moved off the pull_request trigger entirely, which left a window in which a tag could be cut from a merge the audit had not seen. It runs on pull requests again as an advisory job (continue-on-error), stays blocking on the push run for main and on the daily schedule, and a tag is only ever cut from a green push run.
The record-unit conversion is now opt-in per caller and only the `export.xml` archive path opts in. There the attribute is authoritative: Apple stamps every quantity `<Record>` with the account's own display unit, so `km` means kilometres and the reading is meaningless without it. `POST /api/measurements/batch` keeps its documented contract — `unit` is captured for audit and never read. Nothing in the tree pins the native client's unit strings for the seventeen convertible identifiers, and a client that reads a sample in metres while stamping the person's display unit into the field would have had every walking distance multiplied by a thousand on ingest. Reading the field there is a wire change and needs the client's strings pinned first. The mapping-table audit and the shared conversion module stay. The batch path's identity behaviour is written down where it is decided and pinned twice: every convertible identifier round-trips a sibling unit unchanged by default, and a walking-distance entry sent as `km` is stored as it arrived. The OpenAPI description says what the server actually does. Review finding H2.
The provenance criterion proves where a row came from, not which build imported it. An account re-imported on the fixed build carries the same `EXPORT_XML_SOURCE_MAX` stamp and the same `stats:` external id with the right numbers and no audit row, so it is selected exactly like a broken one — and its rest days, under 200 m because the phone stayed at home, survive the plausibility check after another thousandfold multiply. So one candidate row that would leave the range now refuses the whole account: nothing written, no audit row, the offending rows and a printed reason instead. The idempotency guard only ever held against the script's own prior run; this holds against the case it could not see. The runbook says the same in words — a re-imported account must not be repaired, the re-import already did it — and says what the dry run is for: it is where the decision is made, not a formality in front of one already taken. Integration coverage for the re-imported account: correct rows, an `--apply` run, nothing changes and no audit row is left behind. Review finding H1.
An in-place multiplier with no reverse operation was documented with only a dry run in front of it. Point at the backup runbook where the write happens. Review finding M1.
The route was exempt from the published contract as a browser handoff. It is not one: Nightscout has no OAuth step and no authorise page, and the route is an authenticated JSON POST that Zod-parses a URL and an optional token, probes the instance and encrypts the pair onto the row. It was the only POST among eight `connect` exemptions, and its three siblings — status, test, disconnect — have been published all along, so a client could read every part of the Nightscout flow except the one that starts it. Register it from `nightscoutConnectSchema` so the request shape stays single-source, document the 400 / 413 / 422 / 429 arms the handler actually answers with, and drop the exemption. Removing the registration now fails the coverage guard by name.
The rollups and the cached status insights are recomputed after the transaction, so a throw there left the rows repaired and every chart on the pre-repair numbers — and no later run would notice, because the audit row makes the account skip from then on. The tail is wrapped now. The audit row is written with `rollupsRefreshed: false` and only flipped once the recompute returned, so a re-run can tell "repaired, rollups pending" from "done" and says so. A run that could not finish the tail names the account, prints the backfill to re-run — `scripts/backfill-rollups.ts --user <userId>` — and exits non-zero. Review finding M2.
…ates The native client replays its offline outbox with the same Idempotency-Key on every attempt, and `POST /api/medications`, the side-effect log and the inventory register ran the handler again on the replay: after a lost success response the account was left with a duplicate medication, a duplicate symptom entry or a second pen in the supply count. The dedupe on `(externalSource, externalId)` only ever covered a MIRRORED create; a manually entered medication carries neither field and had nothing to collapse a retry onto. All three wrap in `withIdempotency`, the composition the batch ingest already ships, and the three operations publish the header so a client generated against the contract can tell that a retry is safe. Zod parsing and `requireRecordAuth` are untouched. An integration test drives each route twice against real Postgres and asserts what a status check cannot: one row in the table, and a replayed 201 carrying `X-Idempotent-Replay: true`.
The client's notification service extension replaces the medication name on the lock screen, and iOS routes a payload through an extension only when the alert carries `mutable-content`. The dispatcher has set the flag on every alert it builds since the category work for the v0.5.4 client, but the only assertion on it sat inside a category test, where dropping it would have failed nothing and the name would have quietly reappeared on the lock screen of a device that asked for it to be hidden. Two cases now say it out loud: the medication reminder carries the flag, and so does another event type, because that is the shipped behaviour and narrowing it would take a capability away from the client rather than tidy something up.
S1. The journey signs in with a real account and POSTs that account's password to whatever BASE_URL names, and the workflow took BASE_URL as free text. Anyone with repo write, or a leaked token, could dispatch the run at a collector of their own and keep a working credential; secret masking guards the log, never the egress. The host now has to be on a short const allowlist in the script, checked before anything is sent, with exit 2 and a named refusal otherwise. The workflow input is a choice between the two hosted instances, so the form cannot offer a third; the script's own check still stands behind it for a dispatch that goes round the form. --self-test covers the refusal, the prefix lookalike a startsWith check would have waved through included.
S2. The legs ran in one loop that returned on the first red one, so a failed read back never reached the delete and the probe account kept the row. Worse, the id was assigned only after the write's await returned: a POST that timed out once the server had already committed left a row the script could not even name. Legs 3 to 5 now sit in a try/finally. The finally sweeps whatever the delete leg did not, and when there is no id it asks the instance for the row by a marker the write puts in `notes`, unique per run and known before the request goes out. The sweep never throws — the red leg is the verdict, and it says out loud what it could not remove. --self-test asserts the mock holds no rows after every case that got as far as the write, so a red read that skipped the delete is caught.
Low, review 2. The delete leg asserted HTTP 200 and printed "account left clean". An instance that answers 200 and keeps the row would have printed the same line, which is the failure mode the journey exists to catch. The leg now reads the id back and requires a 404. The self-test carries a mock that answers 200 without deleting; with the read-back removed that case goes green, which is the proof the check can fail.
Low, review 2. The self-test walked every leg but never once asked whether the redactor actually holds. The excerpt path is the only thing between a token in a failure body and the run log, so it gets its own case: a body carrying this project's own key prefix must come back with the token replaced.
U1. The new `unit` description on the batch entry said `value` is read in the unit HealthLog stores the identifier in. Sixteen mappings disagree: `convertToDbUnit` runs on every entry, and a waist circumference arrives in metres to be stored in centimetres, an oxygen saturation and a body fat arrive as a fraction to be stored as a percent. A client generated against that sentence sends 98 and stores 9800. The description now says what happens: `value` is read in Apple's own HKUnit for the identifier, HealthLog applies its storage scaling afterwards, and `unit` is recorded for audit rather than validated against. Two worked examples, because the percent-shaped ones are the pair that bites. YAML regenerated.
Low, review 2. An account with one out-of-range candidate is refused whole, rows and audit row included, but the dry run counted its rows into "N row(s) would be repaired" — so the number an operator read before --apply was larger than the number --apply could ever produce. The refused accounts are now counted and reported separately, and the per-account line says its rows are not in the total. The refusal itself told the operator to check the Health app "before forcing anything". There is no --force and never was. It now names the only route that exists: re-run with the matching --unit if the archive really was in another one.
E1. Two halves of one correction. `.env.example` still said backup objects older than BACKUP_RETENTION_DAYS are pruned, which no code does, so a self-hoster reading that file alone still expected the worker to keep the bucket tidy. It now carries the same sentence as the production example. And that sentence overreached in the other direction: "the worker never deletes an object" is not true of `_healthcheck/<ts>.bin`, the 1-byte key the Test-connection button writes and then deletes. The delete swallows its own failure, so an operator who granted exactly the three documented actions leaves one orphan per press and is never told. Both files now say "a BACKUP object", and name the one key and what happens without DeleteObject on that prefix.
E2. `loadOffhostConfig` parsed BACKUP_RETENTION_DAYS, clamped it to a minimum of one day and put it on the config object, where no caller read it. Neither annotation fits: it is not deliberately internal, because there is no internal use, and no consumer is pending. The design forbids one outright — the worker's grant excludes DeleteObject on purpose, so nothing here can act on a retention window without giving a compromised worker the ability to wipe the history. A clamp that guards nothing only read as an enforcer the worker is not. So the field and the clamp go, and the reason sits where the parse was. The variable keeps its place in the two example files and on the compose whitelist: it is the number the operator sets the bucket's own lifecycle rule to. The docs table said "defaults to 30", which implied the app had an opinion; it now says the app does not read it.
The catch-all angle-bracket pattern read as an HTML sanitiser to static analysis. The annotation Apple writes is a number, so the pattern now matches exactly that, and a unit with anything else between the brackets stays unknown instead of being silently trimmed into a lookup.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two bugs reported by @mbreitkreuz this week, each fixed at its class, and a round of work on the things that made the release gate cry wolf. Refs #943, #944.
#944, walking distance from an
export.zipimport stored a thousand times too small. The import never read the record's ownunit. HealthKit units now convert through one shared table (src/lib/measurements/hk-units.ts), applied on the export path; the batch route keeps its documented "captured, not validated" contract. Every mapping entry with a fixed unit carries a written reason, enforced by a structural test.scripts/repair-apple-health-distance.tsrepairs stored rows: dry-run by default, only rows the export path provably wrote, refuses any account with a row that would leave the accepted range, once per account, with a runbook.#943, blood glucose without a meal-time tag never lit the dashboard tile. Untagged readings are an
UNSPECIFIEDbucket across tile eligibility, value and trend, targets, doctor report and FHIR; a structural test refuses a tile gated on an enum without a null arm.Gate hygiene. Four e2e specs de-flaked at their causes (a shared library class, a drawer race, Playwright's keep-alive socket reuse against the server's idle timeout); the score tests' teardown race was a fake database bypassed by a tail call to the real client; bundle budgets with a measured baseline and 20 KB headroom; rate-limit test timing; geometry checks fail instead of skipping without a browser; the dependency audit advisory on pull requests, blocking on
mainand daily.Also in this release. The first-run profile step shows the server's rejected fields instead of dropping them;
withIdempotencyon the three medication create routes the native outbox replays; a synthetic post-deploy journey script and workflow (host allowlist, cleanup infinally); two more end-to-end journeys (glucose in both units, Apple Health export import);POST /api/nightscout/connectdocumented; off-host backup variables documented and the runbook corrected (the worker never deletes a backup object).Two adversarial reviews ran on this branch (
.planning/2026-09-10-v13814-review.md,-2.md); every High and Medium finding is fixed here. Gates on the final tree: typecheck, lint (three baseline warnings), format, openapi:check, unit suite, build, and the full integration suite run serially; this PR's CI runs both e2e shards and the image builds.