Commit Graph
932 Commits
Author SHA1 Message Date
Marc-André Bombeck 1bdc5ea4ee test(backup): bind MoodEntry to a named backup select
The mood read was an inline `include`, so nothing could compare it to the
model the way `measurement-backup-completeness.test.ts` compares the
measurement select. Name it — `MOOD_ENTRY_BACKUP_SELECT` and
`MOOD_ENTRY_TAG_LINK_SELECT` — and add the sibling guard that binds both
to `prisma/schema.prisma`.

The mapper's parameter is spelled `moodEntry` rather than `e` because the
guard proves a column reaches a payload by matching `: moodEntry.`, and `e`
is also the intake-event mapper's parameter one section above: a matcher on
`: e.` would match across both and could never fail.

The guard is red as it stands, and names the defect: `note`,
`noteEncrypted`, `tz`, `syncedAt`, `syncVersion`, `createdAt` and
`updatedAt` are selected and written to no payload. The fix follows.
2026-08-07 20:57:58 +02:00
Marc-André Bombeck ce9b2814e6 feat(dashboard): reach shared records from the account switcher, not a card
The "looking after" block sat directly under the greeting and above this
account's own day, permanently, for anyone with a single shared record. The
dashboard is this account's day; other people's records are a destination,
and the account switcher in the top bar and the sidebar already is that
door — with the same access level and record kind on each entry.

Drop the card, its test, and the five strings only it used, across all six
locales.

The access-payload consumer guard named that one component as the reader of
`level` / `sections` / `recordKind`, so it went red on the deletion, which is
what it is for. It now names the surfaces that actually adopt those fields —
the presentation resolver and the switcher — and matches the property access
rather than one variable name, so a rename of the binding cannot make it
match nothing and pass. Verified by removing every reader: it fails.

Adds `scripts/i18n-key-tool.mjs` for dropping or rewriting a key across all
six bundles in one go.
2026-08-07 19:24:46 +02:00
Marc-André Bombeck 6bb0360695 docs(api): publish the sharing request bodies as named schemas
Zod 4's .meta() is immutable: it returns a new schema carrying the
metadata and registers that one. Four request bodies in the sharing
family called it as a statement and dropped the result, so the route
table went on referencing the bare schema, the bodies were emitted
inline and anonymous, and the object-level descriptions never reached
docs/api/openapi.yaml. One of them carries the clause that scopes every
rule on the invitation endpoint to an invited grant, which is the
sentence that stops a reader carrying the step-up and the refusals
across to a guardian grant.

Bind each to a const, reference the const, and correct the level
boundary in the invitation's description while it is being published:
it still carried the blanket denial that stopped being true when the
anamnesis surface opened to a manage delegate.

The wire format does not move. Four named components appear and the four
bodies point at them; the fields, the types and the requiredness are
what they were. A new guard asserts every id this route table declares
reaches the built document, because the failing form looks exactly like
the working one.
2026-08-07 08:10:48 +02:00
Marc-André Bombeck c043a77429 test: wire every release journey to the tests that carry it
The eight journeys were enumerated and one of them was wired. The other
seven existed as claims in a fixture file, which is the gap the enumeration
was written to close: a browser suite reports what it ran, never what it did
not, and a journey nobody wired reads exactly like a journey that passed.

All eight are wired now, and the list is checked in both directions — an
enumerated journey missing from the wiring fails, not just a wiring entry
naming a journey nobody declared. Before this, a subset was legal, so a
dropped journey and a not-yet-written one looked the same.

A journey names more than one file where more than one file carries it. J1
claims a grant runs through invite, accept, switch, expiry, revoke and a
direct URL; the browser proves the switch, what each level opens and the
direct-URL refusal, and the transitions either side are database facts
proved against real Postgres. Folding that into one file would mean a
browser spec driving expiry through the clock or an integration file
asserting what a page renders, and both are worse tests than the two that
exist.

Three journeys ride Playwright and five ride Vitest, so a green run of this
guard means all eight are claimed and here is which have executed in the
ordinary gate — never that all eight passed. The runner is derived from the
path rather than trusted, so it cannot become a label set to whichever value
made the split read better.

The extractor now reads `it(` as well as `test(`, and the parameterised
`it.each(table)("title")` form alongside the single-call one. It saw neither
before: five of the journeys are proved by Vitest files, every per-file leg
is over a matched set, and an empty match set satisfied them all. The
synthetic spec that proves the parse grew a Vitest half, and it is what
caught the parameterised form being invisible. What the matcher still cannot
see is written into its own doc comment.
2026-08-07 06:11:36 +02:00
Marc-André Bombeck 4d86dceb99 docs: publish the last-Guardian refusal where it is actually raised
The floor was described on the one route that cannot reach it and left unsaid
on the two that answer it every time: withdrawing the last Guardian grant and
renouncing it both raise `managed_profile.guardian.required`, and both
published only 'that access had already ended'. Both 409s now name their two
codes, and the annotation on the removal route enumerates all four emitters
rather than three.

A guard closes the class rather than the instance: every route module that
raises the refusal must publish it on its own operation, with the emitters
discovered from the sources.

The freeze guard gains its containment leg — a module becomes delegable by
review, not by discovery, so every file contributing a fenced verb has to be a
member of one of the surface guard's frozen allowlists.
2026-08-07 05:43:06 +02:00
Marc-André Bombeck 0da9040f2c test: enforce sharing client step-up
Freezes the authority split the third level ships with. A token exercises a
MANAGE grant, and a Guardian's authority over a managed profile, exactly as a
browser does — proved at the resolver, where neither had a Bearer case before.
A token cannot offer MANAGE and cannot create a managed profile at all, because
both gates resolve through the session cookie; the second had no test until now,
and it runs against a real database with a wildcard token to make the width of
the permission irrelevant.

The claim set is held together by a map naming, per claim, the file and the test
title that proves it, so removing any one of them fails rather than quietly
narrowing what a native client may do.

The fence's client-facing half is frozen beside it: the account payload publishes
the record context on a cookie session and null on a token, and the published
contract says so under the same header names the server matches on.
2026-08-07 05:11:02 +02:00
Marc-André Bombeck cc316a7b85 test: preserve sharing client compatibility
Runs the legacy whole-record decoder over the entries `resolveAccountAccess`
actually produces, rather than over the fixture objects that describe them: the
existing case proves both ends and never the assembly between them, which is the
shape of test this project keeps rediscovering. Whole READ and WRITE decode,
MANAGE decodes as write and never above the level it confers, a scoped grant is
refused rather than read as the whole record, and a stored scope the server
cannot parse reads as nothing on both ends.

The canonical readers are driven over the same server-built entry, so
`level`, `sections` and `recordKind` are proved to survive the trip to the
capability resolver. The account-payload guard gains the pair for
`recordSession`: a field whose only reader is the transport type is exactly
what that guard exists to catch.
2026-08-07 05:02:18 +02:00
Marc-André Bombeck 38766cd4ca docs: define sharing refusal contracts
Every route that resolves a record through the sharing fence can answer 403
`sharing.access.denied`, and until now that fact was published on the selector
header and on none of the 120 operations that produce it. All 120 carry it now,
byte for byte, from one constant: the refusal is deliberately indistinguishable
across a missing account, a missing grant, a scope that does not reach the
surface and a read grant on a write, so 120 paraphrases would invite a client to
tell apart what the server refuses to.

Where a delegable route already answered 403 for its own reason — a module
gate, the cycle gate, AI consent — the two share one description rather than one
of them going unpublished, since OpenAPI allows a single response per status.

The guard recomputes the delegable set from the route sources on every run and
classifies per verb, which matters: seventeen modules have a delegable read
beside a write that refuses a switch outright.
2026-08-07 04:56:56 +02:00
Marc-André Bombeck 26fceca219 fix: answer the managed-profile Guardian routes in one shape
- publish the roster's entry from the invitation and `{ grantId, revokedAt }`
  from the removal, instead of the account-grant row Prisma returned
- drop the invitation's self refusal: it named a code no request can elicit,
  because a Guardian grant's grantor is the profile and never the caller
- name the duplicate grant error rather than catching every grant refusal into
  a sentence that would be wrong for any other one
- keep the rate limit and the unknown-identifier 404 free of an error code,
  as their siblings elsewhere are, and say why at the call site
2026-08-07 04:39:50 +02:00
Marc-André Bombeck 29940afb57 fix: name the step-up gate on the two acts that carry one
- deleting a profile and ending a guardian's access resolve fresh MFA,
  and both sit behind a confirmation, so the request that reaches the
  server is minutes old and routinely outside the five-minute window
- an account with no second factor is told where to set one up instead
  of being asked to confirm one it does not have
- the arm is scoped to those two acts: a 401 on accept, revoke or
  renounce is a lapsed session and must not read as a factor prompt
2026-08-07 03:55:25 +02:00
Marc-André Bombeck e9084d9f17 test: cover the managed-profile journey
- drive creation, the switcher entry, the managed banner and the return
  trip in a real browser, plus the guardian roster and the floor stated
  before an invitation is accepted
- seed a guardian account with a second factor and refresh its step-up
  before the journey acts, since no fixture could reach a step-up-gated
  route before this
- tie the journey id to its test titles, so a marker that goes missing
  fails a check rather than vanishing from a run nobody counted
2026-08-07 03:22:51 +02:00
Marc-André Bombeck e4870f3b5f fix: close two gaps the last audit round left open
- give the delegated-action inventory a leg that can falsify its level
  axis: the walked module set must strictly exceed the manage-only one,
  anchored on a route that never asks for manage
- declare AUDIT_LOG_RETENTION_DAYS and SESSION_COOKIE_SECURE in the env
  manifest, so the pre-deploy check reports the two settings operators
  are told to set, and hold manifest, compose whitelist and both example
  files in step
2026-08-07 02:26:29 +02:00
Marc-André Bombeck b3548f105e feat: delete a managed profile from its guardian's panel
- confirm before deleting, naming what goes and that it cannot be undone
- state how many people lose the record, taken from the guardian roster
  rather than assumed to be one
- say that deleting is the way out when you are the last guardian, since
  this route sends no last-guardian refusal
- announce a refused deletion on the profile's own row, retryable
2026-08-07 02:07:49 +02:00
Marc-André Bombeck 6ae0cc993f fix: close four small gaps around the record surface
The privacy dashboard re-implemented the audit-retention resolver with its own
default and its own floor. Two implementations of one disclosure, quoted to the
same person on two pages and by the purge job; it uses the shared resolver now.

The record-content branch in the settings gate matched a classification rather
than a page, so a second manage-writable destination would have rendered the
allergy and family-history managers under its own heading without anybody
choosing that. It resolves through a slug map, and the contract test fails if
the map and the classification disagree in either direction — a destination
cannot join the kind without somebody deciding what it renders.

The managed-profile nav filter assumed a guardian grant is always manage. It is
today, which is why the check was easy to leave out; an entry below that level
would have listed destinations the gate refuses.

Interpolation used a replacement string, and String.replace reads $&, $`, $',
$1 and $$ out of one. A display name containing $' rewrote the sentence around
it — and display names land in exactly the sentences that say who did what in
somebody's health record. A replacer function has no $ grammar at all, which
removes the class rather than escaping the members of it somebody thought of.
2026-08-07 01:00:02 +02:00
Marc-André Bombeck 5de488c35c docs: state the sharing boundary that is actually enforced
Two sentences promised that a manage grant reaches no settings at all. Both
were true when they were written and stopped being true when the anamnesis
destination opened, and both are in the worst places for a stale promise: one
is the access description generated into the public contract and read by the
native client, the other is the paragraph a delegate meets on a page sharing
does not cover.

Both now name the boundary the code draws — record content versus account
configuration — and say which part of the record a manage grant reaches. What
stays with the account owner is spelled out rather than implied: login, the
second factor, provider connections, tokens, notification routing, module and
threshold configuration, and who else has access.

A guard checks the copy against the classification rather than against itself,
so opening a second destination or closing this one moves the sentence with it.
The contract is regenerated.
2026-08-07 00:52:25 +02:00
Marc-André Bombeck b1667ef692 fix: let the audit window an operator sets reach the container
AUDIT_LOG_RETENTION_DAYS is not only enforced, it is published: the
shared-access panel prints the window under the activity list, and the dialog
that ends somebody else's access states how long "who entered what" stays
answerable. Both read the value the server resolved. It was missing from the
compose environment: whitelist and from both example files, so an operator who
set 90 got 365 told to every user, in a sentence about their own data.

That block is a whitelist and Compose substitutes from .env regardless, which
is what makes the variable look configured while never arriving — the same
shape as the session-cookie flag in v1.5.2. A guard now covers the three places
an operator meets such a variable, and its control leg found that the cookie
flag itself is still absent from the production example. Documented there too.
2026-08-07 00:43:39 +02:00
Marc-André Bombeck f91f701e13 fix: say what a refused request actually was
Every delegable route files a breadcrumb when a request does not parse, and it
files it through auditLog on purpose: that helper is what stamps the actor, and
without the stamp a delegate's malformed request would read as the owner's own.
The rows are meant to be attributable. None of them had a sentence, so all of
them rendered in the owner's activity view as "made a change in your record" —
including one a view-only delegate can trigger by sending a query the route
cannot parse. Nothing had changed and nothing could have.

Thirty-three actions now have honest lines. A refusal says it was an attempt
and says it was refused; it never borrows the language of something that
happened. Two of them are filed by library modules the routes import — a
background import's completion row and a medication lifecycle reconciliation —
and a walk over route files alone could not see either.

The inventory guard was asking a different question from the feed. It kept only
awaited audit calls while the feed shows fire-and-forget rows identically, and
it read route files while the feed shows whatever the request reached. Both
criteria now match the feed's, the three rows the feed structurally cannot show
are excluded by name with the reason written down, and the exclusion list is
pinned at three. A leg asserts the shape of the sentence for every refusal, so
a future arm cannot map an attempt onto an ordinary verb and pass on coverage.

The whole round trip is covered against real Postgres: a view-only delegate
sends a malformed query, the row lands attributed to them under the owner, and
the sentence the owner reads is an attempt.
2026-08-07 00:40:40 +02:00
Marc-André Bombeck ef242f33bb feat: open the anamnesis record surface to managers
POST /api/allergies and POST /api/family-history have resolved
requireRecordAuth("manage", "profile") since the level shipped, while the only
form in the product that posts to either sat behind a Settings destination
classified unavailable. The write was admitted, audited and owner-scoped, and
nobody could reach it — the one-ended change this repository keeps
rediscovering, with the permission shipped and the surface left as a follow-up.

The destination now carries a classification that means what the routes mean:
reachable by everybody they already admit, which is a guardian inside the
profile they administer and an adult delegate holding manage. The four places
that decided visibility on the guardian-only literal were each updated
deliberately, and the shell that refused every settings path in an adult share
now admits exactly the destinations classified as record content. Record
configuration — modules, thresholds, notification routing — stays with the
owner at every level.

The page inside a record is its own component holding the two managers whose
routes answer for a delegate. Mounting the personal section would have put
three more forms on screen that refuse on submit. The family-history card says
whose statement the row is, which the route comment could not.

Both routes carried a long paragraph arguing the write was deliberately
unreachable. It is gone rather than left to send the next reader back to close
what this opens.

The integration coverage moves from the undeclared-mode refusal to a manage
contract: a manage delegate and a guardian each write and the row lands under
the owner with the delegate named as actor, while read and write delegates get
the insufficient-access refusal. Nothing exercised either route at manage
before.
2026-08-07 00:01:27 +02:00
Marc-André Bombeck 0cdfb97a45 test: cover sharing accessibility states
The shared-record accessibility suite had four tests for five states: success
and empty shared one, and neither of them ever entered an empty state. A
browser run reports what it ran and never what it did not, so a state nobody
wrote a test for produced exactly the same output as a state that passed.

The five states are enumerated in a fixture, each test names its state, and a
fast guard reads the spec back against the list — a state that loses its test,
its anchor, or one of its proofs now fails in the ordinary gate rather than at
the point somebody notices the spinner is silent.

Every state runs four proofs: an axe scan, the document reading order, either
a keyboard reach with a visible focus indicator or a live-region announcement
for the one state with nothing to press, and, where the copy is something a
person has to act on, that it renders as content rather than as muted meta and
clears the contrast floor. The retry control on the read-failure card gained a
stable slot so the keyboard proof targets the affordance rather than its
wording.

The eight release journeys are enumerated in the same fixture.
2026-08-06 23:46:43 +02:00
Marc-André Bombeck 71ad8d66b6 fix: preserve sharing failure states
Accepting, revoking and handing back a grant had no failure rendering at all.
The request came back refused, or never left the machine, and the panel did
nothing: the confirm dialog had already closed, the list did not change, and
the only signal was the absence of one. The two refusals that most need a
reader were the two that arrived most quietly — an invitation that had lapsed
before it was opened, and the last guardian being told they may not hand back
a profile that would then have nobody looking after it.

Each refusal now renders on the row it belongs to, announced, in content
contrast, beside a control that runs the same act again. The sentences are
split by what the person does next rather than by status code, so lapsed and
withdrawn read differently and the guardian refusal names the way out. A
request that never reached the server says so instead of reporting a refusal
nobody issued.

The failed state is read off the mutation rather than copied into local state
from onError: the copy is what goes stale, and the mutation already knows
which row it was for. The invitation form keeps every field it had, and only
a successful send clears them.
2026-08-06 23:39:39 +02:00
Marc-André Bombeck c92a80ee4c feat: describe managed sharing activity
Every action a manage-level route files now renders as its own sentence in
the owner's activity view, in all six locales. A write delegate can only add,
so the generic "made a change" was at least the right shape of fact; a manager
changes and removes entries the owner made themselves, and the generic line
withholds the only part that matters — whether a reading was corrected or
deleted, whether a dose history was purged.

The inventory reads the routes and calls the map rather than matching `case`
in its source, so an action that falls through to the fallback fails, and it
follows each resolved key into all six bundles. Every sentence interpolates
the actor's name and nothing else: the audit table is not encrypted and a
value in a verb line would be a health figure in a second store.

The revoke dialog's retention disclosure no longer depends on which of two
requests answers first. `retentionDays` rides the grant list, so a client
holding a row to revoke holds the window it must state, and the branch that
omitted the sentence is gone.
2026-08-06 23:30:07 +02:00
Marc-André Bombeck 9a5830753c test(02-12.1): name the transport guard's blind spots, match one more
- document srcSet, poster, object data, form action, window.open,
  sendBeacon, CSS url() and spread props as shapes this matcher cannot
  see, alongside the two trees it walks
- match the imperative `.src =` form, anchored to a real occurrence so
  the new leg cannot pass by matching nothing
2026-08-06 23:04:29 +02:00
Marc-André Bombeck 120323d995 test(02-12.1): prove Vary on the serving path, not only the contents
Removing the Vary block left the disk-layer case green: it asserted what
is IN the cache, and the claim is what the cache will SERVE. On the
external-switch path nothing wipes those entries — the app never
initiated the switch — so Vary is the only thing between a cached
delegated response and the next request under another context.

- assert cache.match() hits under the stored context and misses under
  the new one; it fails when the Vary is removed
- correct the acceptance map and the client docblock to claim exactly
  what executes: no zero-entry count, no offline leg
2026-08-06 23:04:29 +02:00
Marc-André Bombeck dc343f434c docs(02-12.1): narrow two overstated claims
- the idempotency arm refuses only a wrongly-asserted context; an admin
  route behind unfenced requireAdmin still serves a headerless request
- the epoch trigger does not fire on a statement that names record_epoch
  without moving the selector; the source guard is what covers that
2026-08-06 20:48:50 +02:00
Marc-André Bombeck ffe38c9bb6 fix(02-12.1): reach fenced document routes through the app transport
A browser-issued <img src>, <iframe src> or <a href download> cannot
carry the record-session assertion, so the fence refused all three on
any session that had been inside a shared record — permanently, since
none of them will ever send a header.

- add a structural guard: no client file may name a record-fenced route
  in a src=, href= or window.location sink
- fetch the thumbnail, the inline preview and the download through the
  app transport and hand the DOM an object URL, revoked on unmount
- make the two API probes in the managed-profiles browser spec assert
  the same context the app beside them asserts
2026-08-06 20:36:45 +02:00
Marc-André Bombeck 8fcbdcc0df test(02-12.1): prove the record-session fence end to end
- give every acceptance case a stable id and assert the mapping from a
  runnable artifact, recording which cases execute in the ordinary gate
  and which await the browser stage
- add the browser spec for the cases no cooperative layer can reach:
  peers reconciling to an exact context, an aborted switch response, a
  closed initiator, a delayed response, and the offline disk layers
2026-08-06 19:33:15 +02:00
Marc-André Bombeck 31b4857d8b feat(02-12.1): carry and verify the record context from the browser
- attach the asserted context to every same-origin request inside the
  three fetch entries, so no call site classifies itself
- discard a response whose echoed context contradicts the adopted one;
  serve one that carries no echo, which is what keeps the public poll
  and every actor surface working
- adopt from /api/auth/me and the switch response, and from nothing else
- send the epoch on every switch and retry once after reconciling
- hold and reconcile on the 409 instead of leaving the record
2026-08-06 19:10:02 +02:00
Marc-André Bombeck 3a99de312e fix(02-12.1): check record context before an idempotent replay
- evaluate the fence in the wrapper above findCached, so a stale request
  neither replays a body, learns a cell exists, nor inserts a claim
- return the refusal rather than throwing: one route composes the
  wrapper outside apiHandler, where a throw escapes the envelope
- read the claim and the session's own context on one session lookup
2026-08-06 18:54:20 +02:00
Marc-André Bombeck 45fec0fc1b feat(02-12.1): refuse a record request whose context has moved
- both record resolvers check the fence after authentication and before
  the carrier, off the one session row that also decides the scope
- POST /api/account/switch takes an optional expectedEpoch and writes
  conditionally, so concurrent switchers resolve to one outcome
- GET /api/auth/me publishes the context a browser bootstraps from
- every response that resolved a record scope echoes it back
- regenerate the API contract
2026-08-06 18:21:40 +02:00
Marc-André Bombeck 59359738c8 feat(02-12.1): decide record-session context in one place
- add an import-free contract module both bundles can name, holding the
  header names, the parse and the verdict
- add the server fence: header read, wide-event stamp, and the refusal
- add RecordSessionChangedError as a third sibling on the existing
  sharing-error arm, so the 409 needs no new serialisation branch
2026-08-06 17:59:07 +02:00
Marc-André Bombeck a08e82d0be Merge commit '38214e472' into work/v137-record-session-fence 2026-08-06 17:10:56 +02:00
Marc-André Bombeck c56d1ca50b fix(02-11): protect guardian egress data lifecycle
- Exclude and erase egress claims with recipient-side lookup support\n- Serialize data wipes with managed-profile Guardian authorization\n- Cover last-Guardian and queued-claim wipe races
2026-08-06 14:05:59 +02:00
Marc-André Bombeck 4b79f83cf7 fix(02-11): release guardian lock before provider egress
- Commit a final authorization claim under the lifecycle lock\n- Keep provider calls and retry outcomes outside database transactions\n- Retain authorization claims with notification delivery diagnostics
2026-08-06 13:46:42 +02:00
Marc-André Bombeck 1818bd874b fix(02-11): authorize guardian egress under lock
- Hold the managed-profile lock through every provider send\n- Add the notification event retention index and race coverage
2026-08-06 09:42:47 +02:00
Marc-André Bombeck 60b4845fea feat(02-11): secure guardian notification delivery
- Remove record-mutating affordances from Guardian channel payloads\n- Reject managed Telegram callbacks and verify channel attribution
2026-08-06 05:44:41 +02:00
Marc-André Bombeck accb98e223 test(02-11): cover guardian notification channel safety
- Cover non-interactive Guardian payloads across delivery channels\n- Reject Telegram callbacks tied to managed records
2026-08-06 05:37:05 +02:00
Marc-André Bombeck be4ee9ac6d test: cover guardian notification preferences 2026-08-06 05:30:49 +02:00
Marc-André Bombeck a78189b7f6 feat: add guardian notification fanout 2026-08-06 05:23:38 +02:00
Marc-André Bombeck 1c0a5d7777 test: cover guardian notification eligibility 2026-08-06 05:17:56 +02:00
Marc-André Bombeck b231a7892c test(02-12): retire sharing inventory anchors
- remove obsolete inventory-only journey declarations\n- select persistent shared-record journeys by Playwright project config
2026-08-06 05:16:32 +02:00
Marc-André Bombeck 5e8b90bcc1 Merge notification delivery identity 2026-08-06 05:12:42 +02:00
Marc-André Bombeck a33cde3034 test(02-12): cover defensive record presentation
- reject inconsistent access levels before chrome can elevate them\n- fall back to the caller record for malformed payloads
2026-08-06 05:06:36 +02:00
Marc-André Bombeck e037c1b8c5 test(02-12): admit scoped cycle startup reads
- freeze calendar and insights as cycle read handlers\n- retain the exact record resolver scope matrix
2026-08-06 05:05:13 +02:00
Marc-André Bombeck 34073cf8dc test(02-12): cover scoped navigation presentation
- prove profile uses a read-only shared route\n- cover scoped navigation independent of actor module flags\n- require action, access, and kind in record button names
2026-08-06 05:00:33 +02:00
Marc-André Bombeck 9eab70f032 feat(02-12): add scoped profile summary
- expose bounded read-only profile facts for shared records\n- keep profile navigation separate from Settings mutations
2026-08-06 04:59:25 +02:00
Marc-André Bombeck 1ba73a4d51 fix: enforce notification delivery authority
- Require active Guardian authority before cross-record delivery\n- Serialize managed delivery checks with lifecycle changes
2026-08-06 04:29:18 +02:00
Marc-André Bombeck a42d2a6d2f test: cover managed record navigation
- Cover filtered managed settings navigation\n- Keep the exit action visible for an active record
2026-08-06 04:22:32 +02:00
Marc-André Bombeck 908897cc6b test: cover shared record modes 2026-08-06 04:17:58 +02:00
Marc-André Bombeck cb15100341 test: cover scoped record navigation 2026-08-06 04:13:54 +02:00
Marc-André Bombeck 201a24e404 Merge managed record settings 2026-08-06 04:05:16 +02:00