With no provider set up, a fresh self-host's default, the Coach module
is on but /coach redirects to /insights because the Coach is not
available. The tour read the raw module map, kept its Coach stop, pushed
/coach, was sent back to /insights and pushed /coach again on the
pathname change: a loop.
The tour now reads the same capability-folded map as the navigation, so
the Coach stop is not in the list. And a stop whose page redirects is
dropped after one navigation, in the direction the person was going,
instead of being pushed again.
Every AI surface reads its capability from the ai block on
/api/auth/me, but setting up or removing a provider, signing in to or out
of Codex, saving the fallback chain, the central-Codex opt-in, the
local-OCR switch, a minted consent receipt, the admin's server key and
central Codex, and a guardian's save inside a managed record evicted only
their own reads. The surfaces stayed on the old answer until a reload.
All of these writes, and the ones that already refreshed the payload
(consent, modules, Hide Coach, the operator's switches), now evict one
bundle, aiInputDependentKeys. A structural test holds every client write
to those endpoints to it.
Settings, AI says which AI features the server's operator switched off and
shows the one setup hint while no provider is configured. The consent
withdrawal says what it deletes and what stays, and a document-reading
consent on file gets its own withdrawal. Regenerate follows the briefing
capability and the connection test names an operator refusal.
The Coach memory (facts and stored conversations) is never gated on the
Coach: Settings, Coach always shows it, and Settings, AI shows it while
the Coach page is out of reach, whenever rows exist.
Onboarding offers AI only where it can be set up here: the done-screen
panel and the getting-started row go away while the operator has AI off
or inside somebody else's record.
Navigation destinations, the bottom bar's fixed slots and the tour stops
no longer carry their own module keys; each asks the surface map by its
href. Insights has no owner there, so switching AI analysis off leaves
the Insights entry, slot and tour stop in place.
The shell's page gate now resolves Insights sub-pages by slug as well
as top-level pages, so /insights/mood, /insights/medications and the
recovery metric pages show the module notice when their module is off,
worded from the access reason, with the Settings link only for the
record's own switch. The per-page redirect guard, the achievements
redirect and the nutrients page's own notice are gone; nothing
redirects any more.
The connect step's new attention and credentials tiles link to the
integrations page with a label that names a path — "Open Settings →
Integrations". The button base is `whitespace-nowrap` with a fixed height and
`shrink-0`, so at 390 px the German and French renderings could neither shrink
nor wrap: the control ran 27 px past the viewport. The pre-existing connect
card was unaffected, its label being a short verb phrase; the path label and
the tiles that carry it are new, which is why the sweep passed before.
The CTA is bounded to its card, allowed to break between the words of the
path, and allowed to grow for the second line, with the 44 px tap target kept.
No copy moved, so the seven catalogues stay as they are.
Allowlisting `PUT /api/auth/profile` so the setup flow's baseline step could
finish also handed every visitor the shared demo account's contact email, its
display name, its full name and its insurer fields: the allowlist admits a
path and a method, and every field the handler behind it writes comes with
them. The justification beside the entry named three fields; the entry
admitted eleven.
The write is narrowed on the server, in `applyProfileUpdate`, to height, date
of birth and sex while `DEMO_MODE` is set — not in the client, which is the
party the allowlist exists to distrust. It happens ahead of the email conflict
check, which would otherwise answer "is this address registered here?" to
anybody who can open the demo. The display-name box goes read-only there, as
the anamnesis card already is, so nothing accepts input it will discard.
The entry's comment now lists what the handler writes and what is admitted.
The consent branch of the done screen's AI panel painted "insights work for
you right now" on the strength of the receipt alone, reading no health at all.
A person who had consented — including one who had just tapped the offer — was
told a dead provider was serving them. That is the claim-nobody-measured
defect closed at the offer end of the same panel, still open at the other.
The positive sentence now needs a healthy verdict as well. A consented account
on a provider that failed reads the same "not answering" note as anybody else,
and on an unmeasured one the panel claims nothing. The panel's four variants
are pinned across all three health states in every arm.
The panel paints before its status query answers, and every variant inside it
is conditional, so an assertion that a variant is absent could pass on a
question that had simply not been answered yet — which is how the journey for
the unmeasured-provider case passed on a retry against the very defect it was
written to catch.
The panel now carries the variant it settled on, absent until it has settled,
and the three journeys wait on that instead of racing it. The third journey
covers the state a freshly deployed instance is in before its first AI call:
no offer, and no claim of an outage either.
Thirty-one files carried internal build vocabulary and anchors pointing at
notes that are not in the repository: a reader of `checklist.ts` in a year
would find its rationale keyed to a research id they cannot open, inside a
comment that also names an internal build stage. Two test titles carried the
same anchors.
The version tags and the whole of the reasoning stay; only the vocabulary and
the dangling references go, with the few sentences that were built around an
anchor rewritten to stand on their own.
The demo allowlist had grown two entries it should not carry. The demo is one
published account every visitor signs into, so `PUT /api/coach/about-me` was
an anonymous write of arbitrary text into the Coach prompt, persisted for
every later visitor until the next reseed; it is gone, and the baseline step's
anamnesis card renders read-only under `DEMO_MODE` so no visitor meets the
refusal as a failed confirm. `POST /api/managed-profiles` bought nothing: the
route opens with a fresh-MFA requirement the demo account cannot satisfy, so
the guardian arm was limited to finishing without the profile either way.
The baseline profile write stays, and the block's preamble now describes what
is actually admitted and why the two refusals are refusals.
The confirm screen derived "Out of the navigation: …" from the acting
account's module map and rendered it in every arm. In the "someone I look
after" arm the completion never touches that account: it applies the
derivation to the managed record, or, when none was created, to nothing at
all. So a parent answering for a child was told Workouts, Cycle and Documents
were leaving their own navigation when nothing was going to move.
The arm now derives the list from an empty map, which makes it empty, and the
sentence disappears with it.
The done screen folded the `unknown` provider verdict into `unhealthy` and
painted "This instance's provider is not answering right now." `unknown` is
an empty `provider_health` table, which is the state of every instance until
its first AI call — so the most common path through the screen made a claim
about something nobody had measured. It now falls to the neutral panel, which
says nothing either way; the offer is withheld in both cases as before.
The projection itself drew the same unearned conclusion from staleness alone:
a quiet instance whose last success was two days old read `unhealthy` without
any failure behind it. A verdict now needs an observed one — a row whose last
outcome was a failure — and a merely old success reads `unknown`.
The done screen's assistant panel stops claiming that the instance's
provider already serves the person. A tri-state read over the operator's
own provider ledger decides between a one-tap offer that mints the
consent receipt, a plain "not answering right now" note, and today's
setup link; anything unknown, any read error, a managed profile and an
operator who has switched the assistant off all fall to no offer.
# Conflicts:
# e2e/setup/global-setup.ts
A provider with no sync history reports "unknown" instead of "connected",
so a fresh account is no longer told a device it never linked is already
delivering; the connect step resolves the sync verdict into four honest
tiles, records the flow's result only for a connection that is actually
serving, points an unconfigured provider at its credentials, and steps
over a source the account already has.
Both Settings links on the confirm screen are page routes, and the
onboarding cookie that sends every page route back into the flow is
cleared by the completion this very screen calls. On a first run they
could not go anywhere.
They now render as plain text until the flow has been completed once.
The words stay, because where a setting lives is true either way.
The screen listed what the answers switch on and said everything else
stays one click away under Settings, Modules. That is true of
reachability and false of the navigation entry: a module the answers do
not ask for is written off by the completion's derivation.
It now names those modules, read through the registry's own merge rule
and intersected with the map the navigation shows today, so nothing is
claimed about a module the account payload says nothing about.
`chooseFirstResultTask` read the answers alone, so an account whose WHOOP had
been feeding the dashboard for months was sent to go and connect WHOOP. It
takes the delivering sources now; the server still picks from the answers,
having no status in hand, and the screen re-picks once the envelope resolves.
The fall-through stops where there is nowhere to fall to: a person whose one
answer is an already-connected source keeps the step and is told what was
found, rather than ending the flow on no result at all.
The step decided "connected" from the ledger's `state` and had one piece of
copy for every outcome: "the first sync is in progress". That sentence was
false for a connection delivering for months, and worse for one the person
has to repair — the screen said everything was fine while the grant was gone.
`connectSourceView` resolves the envelope entry into one of four tiles and
says whether the flow may record its result. Only `pending_first_sync` and
`fresh` may; `stale` is acknowledged without being claimed as new; `stalled`,
`failing`, `reauth_required` and `parked` name the state and link at the page
that can change it. An unresolved envelope paints no tile at all.
A source the instance cannot connect — a BYO provider with no client pair, a
shared-app provider with no app — now points at the credentials instead of
offering a "Connect" button onto a card that has none.
Q2, Q4 and Q6 let "Weiter" write an answer with nothing ticked. On Q2
that answered "no areas", which is what switched every optional module
off for somebody whose click meant "no preference"; on Q6 it marked the
step done while recording no unit at all.
An empty selection now yields no body, so the primary button is dead
until something is chosen. Skip stays the way past, and Skip is the
conservative derivation the completion route already applies.
The card read one field and made two claims from it: that insights work,
and that no setup is needed. The first was a presence read - it knew a key
existed, never that it answered - and the second skipped the consent
receipt the egress actually needs, so the person who believed the note met
a consent.ai.required refusal on their first briefing.
Four branches now, one button. The offer appears only when the server says
the shared provider is known to work and nothing forbids it, and the tap
posts the affirmative web intent. An unhealthy or unknown verdict paints
'not answering' with no button. When the offer is refused for a reason that
is not health, the panel stays neutral rather than blaming the provider.
The note survives for the one state it was ever true of: consent on file.
The sample, the ladder, the 'fully useful without' line and the setup link
are identical in every branch. Copy in all seven locales.
The baseline form raised its pending flag and lowered it on only two of
three exits; the success path went through the confirm screen's finish(),
which caught its own error and returned normally. A failed completion
therefore disabled both buttons until the page was reloaded.
The reset now lives in a finally, finish() rethrows so the form sees the
failure, and the two managed-arm buttons report it themselves. The step
passes before the completion run off a ledger ref, so a retry after a
failure midway no longer re-sends a step the ledger already carries.
The card's visibility rule gained a "ran the setup flow" disjunct; the
component's query gate kept restating the older half of it. A wizard-run
account past five readings therefore saw a permanent checklist whose
medication, data-source, notification and insight rows were frozen at
"not done", because every supporting query was disabled.
Both places now read one exported predicate, isStillInSetup, which also
keeps the rows live for a restarted run nobody finished.
The string-headroom sweep went red on CI for French at phone width by
two pixels the Mac never showed: the runner's fallback fonts render a
touch wider. The box was the sex select on the confirm screen — its
value is whitespace-nowrap, and a grid item's automatic minimum is its
min-content, so the track was pinned at the padded option's width and
the shell scrolled sideways. Both cells of that row and the trigger
carry min-w-0 now: the track takes the free space and the value
ellipsises, which is the select primitive's own design. Reproduced and
verified locally by narrowing the viewport to 372 px, which stands in
for the wider fonts (336 px of scroll before, none after).
The sweep now names what overflowed: the boxes past the shell's edge,
with tag, slot, classes, text prefix and edges, ride the failure
message and the outside list, so the next red run says what rather
than only that.
"Finish without the profile" completed the flow with no record id, and
the route then derived the child's answers onto the guardian's own
record. The server now decides: answers given for somebody else with no
record to apply them to complete and latch the flow without deriving or
seeding anything, whatever the client sent. The checklist carries the
profile instead and reads none of those answers as the guardian's own.
A record id the caller does not guard answers 404 like every
id-addressed managed-profile route, one answer for missing, not managed
and not theirs alike; the contract documents the field, the 404 and the
422 text. Pinned by two integration cases and a second child journey.
"Someone I look after" derived the child's answers onto the guardian's
own record. The completion now takes the managed record the profile
step just created, checks the same access rule the guardian's modules
route applies, and lands the same derivation and dashboard seed on that
record through the same record-keyed write; the guardian's own flow is
stamped complete and latched without touching its map. Three
integration cases pin it, and the child journey reads the child's map
through a real switch — it asserted the guardian's map before, which
pinned the wrong record.
Also from the review: the checklist's visit row read a meta the visits
list never publishes and now counts its body; the first-result tile
read the measurements list as an array and now shows the reading; the
demo allowlist admits the disclaimer and tour writes, with a structural
test over every onboarding route file; restart clears the old first
result, and the screen trusts the ledger before painting a result as
done; the headroom sweep's raw-key check matches the flow's own key
shapes; the demo migration script stops copying the dropped column; the
questions-to-pass rule and the shell's onboarding-path rule are pure and
pinned; the baseline form composes the shared action row; the schema
diff loses the unrelated formatting churn.
At 390 px with labels a third longer the action row pushed "Confirm and
continue" past the viewport; both rows now wrap. The string-headroom
sweep lengthens every word by 30% rather than hanging the whole surplus
on the last one — that manufactured an unbreakable run no locale
produces — leaves screen-reader-only boxes alone, and accepts a select
value's ellipsis, which is the primitive's design app-wide.
Run against the standalone build, the four journeys and the axe sweep
surfaced five things the unit tier could not see. The completion route
derives only once every question is settled, and Q6 stays pending for
somebody with no unit-bearing area — the confirm screen now passes the
questions the flow never showed before it completes. The dashboard seed
matched a JSON null where the unset column is a SQL NULL, so it never
landed; the retired step route carried the same filter. The auth shell's
onboarding branch matched only the front door, so every question screen
rendered inside the full app chrome with the sidebar. Inline links
relied on colour alone, the step list sat outside any landmark, and the
baseline form brought a second h1 onto the confirm screen. The two
sweeps run serially, since each drives one account through the API.
The integration file gains the seed case that would have caught the
null filter.
The done screen moves focus to its heading like every other screen; the
confirm screen's preference list holds only dt/dd pairs and the settings
link stands beside it; Back from Q1 is not offered once the flow has
been entered, because the front door would resume past the welcome and
bounce straight back; the medication task records no target on the
ledger, as the contract says, and keeps the id on the screen; and the
two success affordances go through the outcome module rather than a
hand-rolled tick. The error-code catalogue lists the answers and restart
codes and no longer the retired step ones, and the session-surface guard
learns the one new deriving helper the onboarding pages read through.
Four journeys, one account each, from the welcome screen to the
dashboard: blood pressure and weight typed in, a daily medication added
inline, a visit within a month with labs, and a child's managed profile
created on the confirm screen. Each asserts what the walk wrote — the
module map, the tile order, the checklist, the settled ledger — and the
first proves that leaving after Q3 resumes at Q4.
Two sweeps on accounts of their own drive the state through the answers
route and visit every screen by URL: axe at both widths plus one
keyboard-only answer, and every locale with every text node lengthened
by 30% before the page, the shell, every clipping box and every control
are measured. All six run in one project because each mutates the
account it walks.
The medication wizard learns to stay where it was mounted after a
create, which the first-result screen needs and the medications list
does not.
The getting-started checklist reads the answers: a visit named within a
month adds a "prepare the visit" row that is done once one is on the
calendar, and the data-source row names the wearable that was chosen.
Its per-browser state moves to one module so the done screen, the skip
and Settings write the same keys.
Settings → Account gains the two ways back in: "Set up again" through
the restart route, which resets the ledger, keeps the answers as
prefill and touches no module; and "Show the checklist again", which
forgets what this browser hid.
A page whose module is off now answers with the shared notice and the
one action that helps, in one place for every module page: the shell
maps the route to the module through the navigation model's own gate,
so a page and its nav entry cannot disagree about which switch they
follow. And the tour's two dead stops get their anchors on the
integrations and health-record group headings.
The dashboard is one click away, already ordered, and the checklist
opens on arrival. The tour is an offer here rather than an automatic
launch: pressing it sets the referrer the shell's launcher reads, and
nothing else does. Two record-shaped exits follow the answer to Q1: an
account that said "both" is offered the profile for the person it looks
after, and one that set up a record for somebody else is offered that
record through the same switch the account menu uses, so the first
entry lands where it belongs. The step is marked done on arrival, once.
Exactly one task, by the step machine's fixed priority: connect the
wearable named in Q4, add the first medication, or log one reading for
the first area. The measurement form and the medication wizard render
inline; a connection goes through the integrations page, because the
OAuth handshake cannot run inside this screen, and the screen watches
the status envelope until it comes back connected. On completion the
screen shows what the task produced — the reading on its tile, the
medication with its next dose, the connection with its first sync — and
records the result on the ledger with the server's own instant.
The offer is not recorded on arrival: somebody who leaves mid-task
resumes here rather than on the done screen.
What the answers switch on is read off the registry's derivation and
never computed here, so the list the person confirms is the list the
completion route writes. Units, language and time zone are shown once
beside it, changeable in Settings, never asked again.
The profile half keeps the baseline form and its per-field refusals:
the fields are seeded from the account so a value it holds is confirmed
rather than typed again, and a refused field still stops the flow. For
"someone I look after" the managed-profile form takes the profile's
place, because that record is created here, after the questions; an
account without a second factor can finish without it and create it
later under Settings.
Confirming posts the completion, passes the first-result step outright
when there is no task to offer, and goes where the step machine says.
One question per screen, Q1 to Q6, each a chip set with a real radio or
checkbox inside every chip so the browser owns grouping and keyboard
movement, and a focus ring the five-step picker's hidden inputs never
had. The selection is pre-filled from the stored answers, "Next"
persists it through the answers route and navigates from the state the
server hands back, and Q1 is the one question without a skip.
The questions are data (which step, which answers, one or many, may it
be passed) with two pure translations beside them — stored answers into
a selection, a selection into the strict body — so the wire never sees
a chip this file did not offer. Q6 asks only about the units the ticked
areas need and sends nothing for a value the account already had.
The step page guards every URL with the step machine and stays under
/onboarding/, which is what the first-run redirect lets through.
The front door reads the same ledger every screen reads: a record that
never entered the flow sees the welcome, one mid-way is sent to the step
it owes, a finished one is offered the questions again. The shell prints
the counter and an ordered step list off the step machine, so it shows
exactly the screens this flow has, and every screen moves focus to its
heading on arrival.
"Skip for now" is a real exit — it stamps the account past the first-run
redirect and opens the checklist on the dashboard — and the one-time
medical disclaimer now gates it as well as "Set up", because a skip that
reached the dashboard without it would be a way round the acknowledgment.
The five-step wizard's welcome carousel, goal picker and source grid go
with it; the numbered step route follows in the next commit.
The account payload resolved the setup state for the actor while its module
map, its cycle flag and every count the checklist reads beside it answer for
the active record. A guardian looking at a child's dashboard therefore got the
child's rows ordered by their own answers, and the child's reading count
tested against their own flow state. The two halves of that component have to
agree about whose record they describe, so the field is record-scoped now,
like the module map next to it.
Under a grant narrowed to one section the answers come back empty, for the
reason the module map is masked: they name the domains the record tracks, and
a grant that opens readings does not open that. The shape of the flow still
comes through.
The three write routes stay actor-only and say why: a delegable declaration is
a reviewed entry in the sharing surface guard, and a read that is wider than
its write is the honest shape while that entry is still to come.
Additive: `onboarding` carries the nine steps with their status, the answers as
given, the flow's own completion stamp and the one task it offered. Always
present — a record that never entered the flow reads as empty answers with nine
pending steps, so a client branches on the statuses rather than on whether the
field exists, and an unknown step id is skipped rather than refused.
It is resolved for the CALLER's own record rather than for the active one,
unlike the two module fields beside it. The three write routes are actor-only,
and publishing a setup state the browser cannot write would be a read it could
act on and never complete.
Both ends land here, because a field with no reader is the failure this
repository keeps rediscovering. The getting-started checklist orders its rows
from the answers — medication first for somebody who takes one daily, the
data-source row first for somebody who named a wearable — and it stays on screen
while the flow is unfinished. That is the second half: five readings were never
evidence that the setup finished, and now a pending step, an unconfirmed set of
answers or an offered task that produced nothing keeps the list where it is.
The ordering only applies once the answers are confirmed. Re-ordering the
dashboard under somebody who is still mid-question would be movement they did
not ask for.
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.
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 German bundle's 109 spaced en-dash pauses join the em-dash norm (the
two date-range placeholders stay ranges); the three straight-quote
stragglers and three mixed-quote strings take German quotes; the eleven
Bitte-Infinitiv validation errors speak du-imperative like the rest of
the app; the two wirklich confirms become bare questions; the
notification-status description trades its semicolon telegram style for
two sentences in every locale; the Withings setup action shortens to the
sibling single word. Twelve arbitrary text-[10px]/[11px] sites move onto
the text-2xs/text-xs tokens; chart tooltips and the vendor calendar stay
as they are.
Refs #778. Two honesty gaps from the first-run report: the web onboarding
presented Apple Health like a web connector although the connection only
completes in the iPhone app, and the initial backfill gave no sign of
progress, so a working first sync looked broken.
The onboarding source step now says plainly that Apple Health is set up in
the iPhone app after onboarding, without dead-ending the flow. The
integration card gains a server-authoritative progress summary — rows
accepted, oldest reading reached, and an honest flowing/waiting line
derived from the last accepted batch. Nothing the server does not know is
shown: no percentage, no ETA, no invented throttle state; a quiet stretch
reads as waiting for the phone, not as an error.
- The daily Coach sweep now charges each ignored day against
surfaceCount and auto-dismisses at three — the cap the schema comment
promised since v1.22 but nothing enforced.
- The welcome gate compares the stored disclaimer acknowledgment
version against the current one, so revised disclaimer text
re-prompts; /api/auth/me carries the version for the client.
- slope90 and anomalyCount leave the analytics wire: slope90 had no
reader anywhere (computed on every request, in SQL twice), and the
slim path could never compute anomalyCount (it always reported 0).
anomalyCount stays as an in-process field on summarize() for the
insights feature builder, which is its only real consumer.
Sweep the spacing rests that sat off the 1/1.5/2/3/4/6/8/10 scale: the
health-score panels and their skeleton, the recommendation ask-Coach
gutter, the onboarding tour footer, the vaccination row, the danger-zone
alert margins, the recovery-codes action wrapper, and the chart-tooltip
tile. Move the last hand-written micro-sizes (coach counters, the
personal-record badge, the score-ring label, the dose-history and
cycle-calendar headers, the mood tag chips) onto the text-2xs token, and
give the documents upload buttons the mobile tap floor their peers set.
Move the last hand-rolled tile and section headers onto TileHeader and
SectionHeading so every card speaks one header language: the vorsorge
dashboard tile, the three illness cards, the metric-correlation section,
and the custom-metric list group. The mood-chart title drops to the
primitive semibold weight and its bucket label becomes a TagChip.
Align the in-card sub-headings of the integration cards to the one
sanctioned section-label tier (muted uppercase eyebrow) instead of a
second text-sm font-semibold dialect.
Keep the onboarding and mental-health wizard hero H1s at their
deliberate semibold weight, now marked inline as an intentional
exception so a future heading sweep leaves them alone.
`/` runs its reads server-side and hands them to TanStack through a
HydrationBoundary, so the first HTML already carries real tiles. React threw
that away on every cold load. The app shell hydrates first and its mount
answers `/api/auth/me`; the dashboard sits below `loading.tsx`, hydrates later,
and its first client render therefore read an account the server never had.
That disagreed with the streamed HTML, React reported #418 and re-rendered the
whole page. The prefetch paid for its queries and delivered none of its paint.
The account payload is now withheld from the hydration render wherever it is
only rendered. `useAccountOnceMounted()` answers null until the component has
mounted, which is what the server rendered, and hands the payload over on the
next render. The dashboard page, the caregiver card, the getting-started
checklist, the achievements tile and the unit labels read it that way. `useAuth`
itself still reads the cache, because the document vault redirects off
`user.modules` and a first render claiming "no account" bounces it to `/`.
Two tiles disagreed without any account involved. A query reports `isLoading`
true on the client's first render and false on the server, so both now gate on
whether the cell holds an answer.
Nothing in CI ran the shipped configuration: the suite's server turns the
prefetch off so route mocks keep governing the dashboard. A second server runs
it on, and one spec asserts that no hydration error is reported and that the
tile node the server streamed is still the node in the DOM once the page
settles.
Measured over nine cold loads with the CPU throttled 4x: seven bailouts before
and none after, median LCP 869 ms before and 145 ms after. FCP and the time to
the first tile in the DOM are unchanged, because the streamed paint was never
the problem. Keeping it was.
git, eslint and vitest each carry an exclusion for local development
worktrees, and all three name /.worktrees/ while the convention actually in
use is /.wt-*/. Every one of them was therefore walking eleven full copies of
the source tree.
The consequences differed by tool and none of them announced itself. pnpm lint
took ten minutes instead of one and reported warnings from whatever commits
those copies happened to sit on. vitest collected duplicate suites that drag in
a second React instance, which fails in ways that have nothing to do with the
working tree. git left them merely untracked, one git add -A away from
committing the repository eleven times over.
With the true warning count finally visible, teach no-unused-vars the
underscore convention this codebase already follows and drop the five
disable directives that convention made redundant. pnpm lint is clean.
A non-underscore unused argument still warns; that was checked rather than
assumed.
The baseline step submitted what you filled in and ignored the answer.
The profile write is field-independent: it stores everything that
validated and reports back what it declined. Nobody read that report
here, so a value the server refused vanished between the input and the
next screen and you moved on believing it was saved.
It now says which field did not make it, using the name that sits next
to the input you typed into, and keeps going with the rest that did
save. When nothing could be saved at all, the step stays put and names
the field that blocked it instead of a shrug. The wording and the
field-naming come from the same place the account form in Settings
uses, so the same refusal reads the same wherever you hit it.
Adds the cases that were missing everywhere except the FHIR conformance
suite: an empty string clears without taking its siblings down, OTHER
round-trips through the profile and onboarding writes, an unsupported
value is still refused and says which field it is about, the Strain
model scores an unanswered account on neither side, the dashboard bands
give OTHER the neutral fallback, the snapshot payload carries the value
instead of flattening it, and the doctor-report cover omits the row
rather than claiming a value the account never chose.
Refs #645
The profile validator has accepted MALE, FEMALE and OTHER for a long
time, but the published contract documented two, and a client written
against that contract had no way to know the third existed. The profile
PATCH is one write, so a rejected gender took every sibling field with
it - a time-format change lost to a field the person never touched.
Gender now clears on an empty string, the same contract every other
profile field documents, and an unsupported value is refused with a
message that names the field and its accepted values instead of the
validator's own prose. Onboarding validated against a narrower enum
than the profile schema and 422'd the whole submission; its form could
not record the third value at all.
The rest is the same defect in other clothes. The Strain model scored
an unanswered or OTHER account on the male Banister coefficients; it
now uses the midpoint of both published models, matching every other
sex-split formula here. The dashboard snapshot emitted null for an
account that answered OTHER, which is indistinguishable from no answer.
Three casts asserted a two-value type over a three-value column. All of
them now carry the stored value and narrow once, at the boundary of a
reference table that genuinely has no third row, through one named
helper in src/lib/profile/sex.ts.
Refs #645