diff --git a/src/__tests__/domain-write-support-guard.test.ts b/src/__tests__/domain-write-support-guard.test.ts index 76ed8c0cb..277f2107d 100644 --- a/src/__tests__/domain-write-support-guard.test.ts +++ b/src/__tests__/domain-write-support-guard.test.ts @@ -24,6 +24,16 @@ * domain outside the vocabulary other than `record` fails the run outright, * because the table is keyed on the vocabulary and would silently lack it. * + * What it deliberately does not hold, stated so nobody reads more into a green + * run than it earns: the table is EXISTENTIAL per section. It answers "does + * any route in this section accept this need", and within a section the answer + * varies by verb — `POST /api/encounters` is WRITE while `POST /api/allergies` + * beside it is MANAGE, both under `profile`. So this guard proves the table + * and the routes agree about the section, never that one control's own verb is + * covered. That is the control's job: it asks the hook for its own verb class, + * and a control that read "my section is writable, therefore my create is" + * would be wrong in three of the four sections marked writable. + * * Mutation checks, run: * - set `mind.write` to `true` in the table → "agrees with the routes" red; * - change `requireRecordAuth("manage", "mind")` in `mood-entries/route.ts` diff --git a/src/lib/sharing/account-access-view.ts b/src/lib/sharing/account-access-view.ts index ad813770e..41fb46cad 100644 --- a/src/lib/sharing/account-access-view.ts +++ b/src/lib/sharing/account-access-view.ts @@ -113,11 +113,22 @@ export interface AccountAccessEntry { */ canWrite: boolean; /** - * v1.38.12 — the sections this grant can add to, resolved server-side from - * the grant's level and scope against the delegated write routes that - * exist (`domain-write-support.ts`). In the consent screen's order. Empty - * for a READ grant, and empty for any section whose routes accept no - * delegated write at all — the vault, today. + * v1.38.12 — the sections with AT LEAST ONE delegated write route this + * grant satisfies, resolved server-side from its level and scope against + * the routes that exist (`domain-write-support.ts`). In the consent + * screen's order. Empty for a READ grant, and empty for any section whose + * routes accept no delegated write at all — the vault, today. + * + * Existential, and the word matters: within a section the answer varies by + * verb, and this list cannot say which. `profile` is writable because a + * visit, a practitioner and a dose answer at WRITE, while an allergy and a + * family-history entry are MANAGE creates; `measurements` is writable + * because a reading is, while a water entry and a checkup reminder are + * MANAGE. So a control does not read "my section is writable, therefore my + * create is": it asks the hook for its own verb class — + * `canWriteDomain` for a create the WRITE level covers, `canManageDomain` + * for one it does not. A per-verb table would be a second place the route + * declarations live, and the declarations are the thing that decides. * * A list rather than a boolean because the answer differs per section, and * the client's per-control question is "this section", never "any". diff --git a/src/lib/sharing/domain-write-support.ts b/src/lib/sharing/domain-write-support.ts index ad182b434..f5a2725e8 100644 --- a/src/lib/sharing/domain-write-support.ts +++ b/src/lib/sharing/domain-write-support.ts @@ -74,6 +74,15 @@ const LEVEL_RANK: Record = { * `sections === null` is the entire record. A MANAGE grant always carries it * (`grants.ts` refuses a scope on a MANAGE invitation), a WRITE grant may. * + * The answer is EXISTENTIAL per section, never per verb: "is there a route in + * this section at this need the grant satisfies". Three of the four sections + * that answer `write: true` also carry MANAGE-only creates — an allergy and a + * family-history entry under `profile`, a water entry and a checkup reminder + * under `measurements`, a snooze and a re-recorded dose under `medications` — + * and no list keyed on the section can distinguish them. A control asks the + * hook for its own verb class instead; the route declarations stay the one + * place the split is decided. + * * For `need === "write"` the answer is "is there a non-read route in this * section the grant satisfies": a WRITE grant reaches the section's `"write"` * routes, a MANAGE grant reaches those and its `"manage"` routes as well.