- The record-settings contract, slug-list, and section-frame tests
follow the slugs that left the registry (layout children resolve
their headings from the layout namespace, like production).
- The orphaned settings.sections.<module>.title/description keys leave
all six locales; the reverse-coverage guard flagged them the moment
their nav entries stopped existing.
- The operator-cap ledger read fails open on a read error — the
reservation layer still meters against the same ledger, and a
transient blip must not take the fallback chain down.
- The reaper queue joins the subsystem surface; its healed count and
the sweep's nag_dismissed join the job-fact vocabulary.
- The intake-event contract fixture carries the full row the resource
now declares; the workouts empty-state test asserts the
source-neutral cue; the persister poll gates on rendered content
first.
- account-sharing polls the offline persister's own IndexedDB store for
the flushed snapshot instead of sleeping past the debounce.
- The record-session-fence cases poll for the observable halves of their
setups (the switch round-trip landing, the exit switch being issued);
the one remaining fixed window is a documented absence-assertion grace
with nothing to poll on.
- The documents suite's two remaining sleeps are named for what they
are: a focus-dance grace the outer document cannot observe, and the
long-press gesture duration itself.
- The inventory-date fixtures derive their printed expiry from now
instead of a fixed 2027 date that would silently flip the fixture to
expired when the calendar catches up.
The scope question defaulted to whole record, which reads as the form
nudging the choice. Preselect neither option: an untouched fieldset now
blocks the form with a quiet hint instead of falling through to a
whole-record grant. The wire contract is unchanged — scope-absent still
means the entire record on the server, so the unset state deliberately
does not omit scope, it blocks the client until the owner picks. The
empty-selection and manage floors stay fail-closed as before.
The switcher and the banner named a shared record's owner by their greeting
name — the value they typed at onboarding for "how should HealthLog address
you". When an owner has set a full name, that is what a delegate should see:
entering somebody's record, you read whose it is by who they are.
- the account-access resolver publishes the owner's `fullName` alongside the
greeting name and the username
- `accountLabel` prefers fullName, then displayName, then username. Optional
on the argument so the grant-view and actor shapes that never carried it
keep the old two-field behaviour untouched
- the closed presentation schema carries `fullName` and its parity matcher
compares it, so the two published views of the active record cannot name
the same owner differently
- the OpenAPI contract regenerates with the additive field
This deliberately discloses the owner's full name to every delegate, read-only
included — it was patient-identity data behind a MANAGE-gated export until now.
That is the intended behaviour, documented at `accountLabel` so it is not
mistaken for a leak and tightened back. The resolver, the schema parity, the
label fallback and a read-only delegate's banner are each pinned by a test.
Three lines in a 240 px menu: the name, the access level, the kind of
record. The middle one truncated more often than it read, and it is what
the warning banner states in full once you are inside the record, where it
governs what the page lets you do. Two lines left, and the level stays on
the row as `data-access-level` for the browser journeys.
The switcher's own "Read only" / "Can add entries" strings had no other
reader and leave all six bundles.
data-slot rides {...rest} onto DateField's hidden ISO input, so the
invitation spec asserted visibility on an element that can never be
visible and typed into nothing. Use the primitive's data-testid, which
lands on the overlay a person edits, and assert the mirror took the
value.
The coach positive control navigates to the dashboard to prove the launcher
renders for a delegate in their own record. It left the browser there, and the
next step clicks Accept, which lives on the access page. The click waited out
its timeout against a page that does not carry the button.
An assertion inserted mid-journey has to restore the state it borrowed.
A server-prefetching page dehydrates a health record into the HTML and
seeds it into the client's query cache under the key the client cell
reads. That copy is only right when the account the page read is the
account the client's route would answer with, and under an account
switch it never is: `getSession()` answers who is CALLING, deliberately
and permanently, so all five prefetching pages seeded the delegate's own
rows into a document opened on somebody else's record. The dashboard was
the worst of it — snapshot, digest and widget layout ride routes that
refuse the refetch, so the delegate's numbers stayed on the owner's
dashboard, while the chart row refetched to the owner and left the page
showing two people at once under a banner naming one.
Resolve the session through `getUnswitchedSession()` instead, which
answers null while the browser is acting on another record. The prefetch
steps aside and every cell fetches through its route, which is where the
grant check lives. Resolving the owner here instead was the other
candidate and is worse: the grant check has exactly one home, half of
what these pages prefetch rides routes that declared themselves not
delegable, and re-deriving it in an RSC would be a second program
deciding whose record this is.
Close the two shell-level doors beside it. The Coach launcher, its
drawer mount and the module tour hang off `<AuthShell>` rather than off a
route, so dropping Insights and Coach from the navigation under a switch
left them painting on every page in the record and on the unavailable
panel too — one tap from a drawer whose every action refuses, and a
`POST /api/insights/coach/seen` fired by opening it at all. All three
bind `useRecordCapabilities()`, the same resolved grant the banner and
the switcher read. Absent, not disabled.
The prefetch rule is structural: the guard walks the app tree, so a sixth
prefetching page is covered the moment it lands rather than the moment
somebody remembers. The pages stay on the session-surface allowlist with
their reasons rewritten, because a wrapper one directory deeper is the
easiest way for a surface to walk out of a guard.
The invitation form gains the choice, and the option labels say what actually
ships rather than the word "write": a delegate can add readings, results,
observations, a medication and a marked dose, and can edit or delete nothing,
including what they entered a moment ago. That gap between what "write access"
sounds like and what it does is where a support case lives, so it is on the
card rather than in a reply.
The delegate reads the same thing before they accept. A pending write
invitation states its level and one sentence about what accepting means, on the
screen where they accept it, because acceptance is where the two consents meet
and it happens once.
Ending an access says what stays: the entries remain and the owner can remove
any of them, and the activity view keeps saying who entered what for as long as
the instance's audit window reaches. That number comes from the server, so an
instance running ninety days says ninety, and a browser that has not been told
says nothing rather than a made-up year.
The browser journey now invites at the wider level and reads the posted body
for it. That is the only check in the tree that can: the component convention
is server-rendered only, so no test under src/ runs the form's submit path, and
every one of them stays green with the control wired to a constant.
The destructive-control registry carried this card because a detector read
its hardcoded `expiresAt: null` as a field being blanked, and an entry
explained at length why blanking was harmless here. The explanation was
right and the cause was not: the null was there because nobody could set a
date, not because none was wanted.
With the control wired the detector no longer fires, so the entry no longer
matches anything and the registry's own staleness check asked for it back.
The switch is stamped on the session row, not held in the tab. Every
authenticated spec opens its context from the same storageState file, so
the sharing journey switching that jar into the owner's record switched
it for whatever spec happened to be mid-navigation on the other worker.
The failures land nowhere near the cause: /settings/ai painting "Not
part of shared access" instead of the AI panel, an axe scan reading a
banner its spec never asked for, a waitForResponse waiting on a request
the refused surface never sends. Which spec loses depends on scheduling,
which is why it moved between runs.
Log the delegate in a second time and hand that jar to the journey. Same
account, its own session row, no reach outside the file. The three login
captures collapse into one helper while the third one is added, and the
fixture's login-bucket reset already covers the extra attempt.
The guard matches the two things a switching spec cannot avoid naming,
the switcher entry it clicks and the endpoint that entry calls, and
checks both that the match set is non-empty and that the shared jar is
still what the ordinary specs read. Verified red by pointing the journey
back at the shared jar.
Two browser contexts, two seeded accounts, one database: the owner invites,
the delegate accepts and opens the record, the owner takes it back. Every step
through the shipped UI, because the half of this feature that can go wrong is
the half a person touches.
The stale-paint check is a negative one and deliberately not "the new record's
data arrives" — a check that waits for the new data says nothing about what
was on screen while it arrived. An init script arms a MutationObserver before
any page script on every navigation, including the reload the switch performs,
and latches the delegate's own marker weight. The observer's positive control
runs first on a page that definitely shows it, so its later silence is not the
silence of a broken observer.
Then it was broken on purpose, and the result is written into the spec rather
than assumed. Removing the switch-time cache wipe, the persister's restore
guard and the record-scoped hash — all three — leaves the spec GREEN: the
restore already refuses to clobber a query the live cache holds, and the
dashboard's query is created during render, so the snapshot never reaches that
route's screen at all. Removing the reload turns it red on the banner rather
than on the marker. So this suite is a regression net over the journey, not a
proof of any one control; the controls are proven where they can be broken and
observed, in the unit suites, and the spec's header says so.
The nav assertion turned up a real gap while it was failing: the avatar block
still linked into the account settings from inside a shared record. Those
routes refuse under a switch, so the link led only to an explanation of why it
did not work. It renders as a plain identity block now, and the mobile user
menu drops the same utilities.
Setup gains a second account, a marker weight in each record, a grant reset,
and a clear of the login bucket — this setup signs in twice now, and the
ceiling is five attempts a quarter-hour, so three local runs used to end with a
429 from the fixture rather than a failure from the product.
Three additions to the contract, all of them things a native client cannot
work out on its own.
`GET /api/auth/me` enters the spec for the first time, and enters it loose:
`accountAccess` is specified in full, the remaining preference fields are
declared open rather than half-enumerated. The endpoint has never been in this
document and its payload carries thirty-odd keys no contract has ever named;
specifying a subset and closing the object would publish something the server
does not keep.
`X-HealthLog-Account` is published as a reusable parameter with no route
referencing it, which is the honest state of the contract — the routes that
read another account's record arrive with the surface migration and will
reference it as they land. The header is live in the resolver today, and a
client needs the description before it can be built against a tagged spec: the
value comes from `accountAccess.accounts`, it is a selector and never a
permission, and it must never ride the sync engine, device registration, or an
auth call.
`GET /api/account/activity` carries `retentionDays` in the response and says
why: the window is operator-configurable, so a client that assumed 365 would
tell somebody their record was never opened when the honest answer is "further
back than this reaches".
The parameter is defined in the sharing route module rather than the shared
components block, so the boundary guard keeps holding the header's name to two
files — the resolver that reads it, and the one place that publishes it.