lib/sharing/grants.ts held both the grant transitions and the predicates
every reader asks (state, level, scope, the live grant for a pair). The
transitions reach into the managed-profile lifecycle and the OAuth connect
target, and both of those read a live grant, so grants, lifecycle and
connect-target formed an import cycle.
The predicates, findActiveGrant and touchGrantUsage move verbatim to
lib/sharing/grant-access.ts, which imports only the database client and
the scope vocabulary. Importers and the mocks that replaced those
functions point at the new module. Runtime import cycles fall from 7 to 6.
Renouncing a Guardian grant read the grant before taking the managed
record's lock. When the Guardian's own account deletion held the lock
first and ended the grant, the count under the lock then refused with
"add another Guardian" for a grant that was no longer active. The grant
is now re-read under the lock, so the renunciation answers that it has
already ended (or is gone) instead.
The race test accepts the already-ended answer beside 200 and 404, since
which side wins depends only on timing.
The lifecycle suite took the invitation route's answer as an account-grant row
and reached for `id` and `expiresAt`. It answers with the roster's entry now,
so the id arrives as `grantId` and the expiry is read off the stored column,
which proves it was persisted rather than echoed.
- 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