fix(longitudinal): gate collection on current ledger and tenant - #226
fix(longitudinal): gate collection on current ledger and tenant#226cursor[bot] wants to merge 6 commits into
Conversation
A Seoul clinic participant can join a 14-day mood diary only after granting longitudinal observation consent. Work and home membership stay distinct, research refusal does not block personal enrollment, and pause/resume/withdraw keep the enrollment evidence. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Collection now fails closed after a later longitudinal revoke, even if the enrollment is still Enrolled or was resumed. Enroll takes the tenant-owned participant record so a second clinic cannot borrow the Seoul participant under another tenant. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Enroll-time snapshots stay Granted after a later revoke. Collection now takes the current consent ledger and the tenant-owned participant record so a Seoul clinic revoke, an empty ledger, or another clinic fails closed even when EnrollmentState is still Enrolled. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
There was a problem hiding this comment.
Review (8c54fcc)
Collection authorization is no longer enroll-time snapshot or Enrolled membership. authorize_collection(&ParticipantRecord, &ConsentLedger) re-binds the tenant-owned record and the latest LongitudinalObservation event on the supplied ledger. Seoul revoke-after-enroll, resume-after-revoke, empty same-participant ledger, other-participant ledger, and other-clinic record all fail closed. consent_snapshot_ref stays enroll evidence only. TRACEABILITY §2 remains Target on evaluated main.
This is not approval. Independent last-push review by seonghobae is still required. Do not merge #199 or #184. Do not self-approve.
Residual (correctly out of scope here)
The ledger and participant record are still caller-supplied. Persistence/HTTP must load the current participant ledger and the store row under the authenticated caller tenant. Do not “fix” that by routing collection back through consent_snapshot_ref.
enroll can still mint Enrolled from a stale granted snapshot after a later revoke. Collection still fails when it receives the current ledger. Load the current grant at persist/enroll time in the follow-up slice.
Operator next action
Review the enroll-time-snapshot-after-revoke and collection-time cross-tenant contracts on this exact head. Keep #226 as the enrollment landing vehicle. After it lands, persist longitudinal_enrollment + enrollment_membership_context with server-side current-ledger load and caller-tenant bind.
Sent by Cursor Automation: Fix Issues
| fn current_longitudinal_grant(ledger: &ConsentLedger) -> Option<u64> { | ||
| ledger | ||
| .events() | ||
| .iter() | ||
| .rev() | ||
| .find(|event| event.purpose() == ConsentPurpose::LongitudinalObservation) | ||
| .and_then(|event| { | ||
| (event.decision() == ConsentDecision::Granted).then_some(event.occurred_at_unix_ms()) | ||
| }) | ||
| } |
There was a problem hiding this comment.
Residual, not a change request on this in-memory slice: current_longitudinal_grant trusts the caller-supplied event list. A prefix that ends on Granted (later revoke omitted) still authorizes while state == Enrolled.
Persistence/HTTP must load the current participant ledger server-side and must not accept a client-built event list. Do not route collection back through consent_snapshot_ref.
| Self::ParticipantMismatch => { | ||
| "use the consent snapshot that belongs to this participant" | ||
| } |
There was a problem hiding this comment.
Wording only: collection now fails on a mismatched ledger or participant record, not only a snapshot. Follow-up copy can say to use the consent evidence that belongs to this participant. Not blocking for this head.


Why
#199 enrolls a Seoul clinic participant after a longitudinal grant and re-checks a caller-supplied ConsentSnapshot. That snapshot is enroll-time evidence. After revoke it stays Granted, so authorize_collection on the enroll-time snapshot still succeeds. Enrolled still documented collection authority. Collection also never re-bound tenant.
TDD
RED tests/longitudinal_enrollment_contract.rs required authorize_collection(&ParticipantRecord, &ConsentLedger) so an enroll-time snapshot that still looks Granted cannot authorize after revoke, and a second clinic's participant record is CrossTenantDenied while consent remains granted.
GREEN replaces the snapshot-only gate with the current ledger head plus the tenant-owned record.
Local GREEN:
Scope
Out of scope
Operator next action
Review the enroll-time-snapshot-after-revoke and collection-time cross-tenant contracts. Do not merge #184 or #199. Do not self-approve. Independent last-push approval and exact-head checks are required.