fix(identity): recover participant from unterminated history - #133
fix(identity): recover participant from unterminated history#133cursor[bot] wants to merge 11 commits into
Conversation
A buyer who links an anonymous assessment to a Keyverse account must still see that link after process restart. Persist assessment_participant plus append-only link and link-end evidence, reload through the domain lifecycle, and fail closed on conflicting replay or a subject already bound to another participant. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Keep TRACEABILITY, ADR-0020, ERD, and as-built schema pointing at the opened persist/reload vehicle instead of an unnamed Active PR. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Apply each identity link and then its matching ends in one transaction so a complete in-memory unlink+relink aggregate survives restart. Cover one-shot persist, exact replay, and subject reuse after unlink. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Add a tenant-scoped current-subject lookup so a returning Keyverse login can find the same product-owned participant after the anonymous session token is gone. Ended or replaced subjects stay unfindable until they are current again. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Remove the accidentally committed build tree and ignore /target so later local verification cannot leak compiler outputs into the identity-link successor. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
A missing current_participant_identity_link row no longer hides a returning Keyverse login or lets another participant bind the same issuer-scoped subject. Lookup and uniqueness now read append-only link rows that have no matching end. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Name the Active PR vehicle as the successor of #124 so TRACEABILITY, ADR-0020, and the as-built schema do not treat projection-only lookup as the landing contract. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
There was a problem hiding this comment.
Stale comment
Review
#133 is the persist/reload landing for
assessment_participantand append-only identity-link history. Prefer this head over #114 and #124.
load_participant_by_current_identity_subject/current_subject_participantread unterminatedparticipant_identity_linkrows (NOT EXISTSend event) and fail closed asCorruptHistorywhen two unterminated links exist. That matches the projection-loss contract. Persist still locks the participant row withFOR UPDATEand uses the current-subject unique constraint as the concurrent first-link backstop.Do not merge until exact-head checks and independent last-push approval land. Never self-approve.
Command-authorization honesty is now #144, not #135. Session command-history landing remains #129; that head still counts stored commands after insert without
FOR UPDATEonassessment_session, so a READ COMMITTED Activate-only worker can race a later Pause/Resume persist. Do not start HTTP until #144, #133, and a locked #129 successor are integrated.Sent by Cursor Automation: Fix Issues
There was a problem hiding this comment.
Stale comment
History-as-truth lookup and uniqueness are the right #124 repair. Prefer this head over #114 and #124.
Do not merge yet. Two landing defects remain on this exact head:
- Exact replay does not restore or clear
current_participant_identity_link. After operator repair deletes the derived row, or leaves a stale row after unlink, the unique enforcer that backs concurrent first-inserts is gone even though history still has the unterminated subject. Successorcursor/bc-3286fd2e-32dd-4741-a21f-ada045db3ab9-1d81reconciles that projection after persist.- TRACEABILITY still names superseded #124 as the Active PR for this adapter. Operators can merge the wrong vehicle.
Residual, already marked planned in ADR-0020: concurrency arbitration beyond the participant row lock. History
FOR SHAREplus the derived unique constraint is the current defense; a later slice should add subject-scoped locking or an equivalent database constraint on unterminated history.Do not merge #114 or #124. Do not self-approve. Wait for exact-head checks and independent last-push approval. Prefer the successor once it is the named landing head.
Sent by Cursor Automation: Fix Issues
There was a problem hiding this comment.
Do not merge #133, #124, or #114. History-sourced lookup is the right contract, but two buyer-facing holes remain after projection loss.
- Exact replay of an unterminated link returns
Duplicateand does not restorecurrent_participant_identity_link. After restore or operator repair, the derived uniqueness safety net stays down. - Unterminated
(tenant, issuer, subject)uniqueness lives only on that derived projection. A second history row can be inserted, and the returning login then getsCorruptHistoryinstead of the originalparticipant_ref. TRACEABILITY still names #124 as the landing vehicle.
Prefer the successor that restores the current projection on exact replay and rejects a second unterminated subject in the database. Do not self-approve. Do not merge until exact-head checks and independent last-push approval are satisfied.
Sent by Cursor Automation: Fix Issues
| && stored_authenticated == authenticated_proof_ref | ||
| && stored_linked == linked_at_unix_ms | ||
| { | ||
| Ok(false) |
There was a problem hiding this comment.
Exact replay of an unterminated link stops at Ok(false) and never rebuilds current_participant_identity_link. After projection loss, a returning persist leaves uniqueness unenforced. Restore the derived current row when this stored link has no matching end.
| REFERENCES participant_identity_link (participant_ref, identity_link_ref), | ||
| CONSTRAINT current_participant_identity_link_participant_fk FOREIGN KEY (participant_ref) | ||
| REFERENCES assessment_participant (participant_ref), | ||
| CONSTRAINT current_participant_identity_link_subject_unique UNIQUE ( |
There was a problem hiding this comment.
This unique constraint protects only the derived projection. Unterminated history is the claimed source of truth, but a second open (tenant_ref, identity_issuer, identity_subject_ref) row can still be inserted. That turns a returning-account lookup into CorruptHistory. Enforce one unterminated subject on participant_identity_link itself.
| | Publication event replay is idempotent/conflicting reuse fails closed | TRD §7 | `src/instrument.rs` | durable DB uniqueness/concurrency test | | ||
| | Published instrument requires exact-version scientific evidence | Measurement Governance; ADR-0019 | `src/instrument.rs` binds approved evidence status, provenance/scope, mandatory evidence references, validity window, and immutable release identity before publication/reactivation | persistence/API publication integration and real instrument-specific evidence artifacts | | ||
| | Optional account linking does not rewrite historical participant/result identity | ADR-0003, ADR-0020 | `src/participant.rs` issuer-scoped first-link primitive preserves stable participant ID | append-only identity-link persistence + unlink/relink/recovery audit tests | | ||
| | Optional account linking does not rewrite historical participant/result identity | ADR-0003, ADR-0020 | `src/participant.rs` issuer-scoped first-link primitive preserves stable participant ID; **Active PR** #124 `src/postgres_participant_identity_link.rs` persists and reloads that history without rewriting `participant_ref` | HTTP unlink/relink transport, live Keyverse verification, and backup/restore evidence | |
* test(auth): bind anonymous proof to exact session resource * feat(auth): bind anonymous proof to exact session resource * feat(auth): expose anonymous session authorization * style(auth): apply rustfmt to anonymous authorization tests * test(auth): pin anonymous denial precedence * docs(auth): explain anonymous session authorization * feat(auth): authorize anonymous commands from loaded session Derive the assessment-session resource from the stored participant tenant and the loaded session so a transport cannot invent a matching scope and then command a different session. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * feat(auth): apply session commands only after anonymous authorization Keep the loaded session unchanged when the proof is expired or names a different session, and still fail closed on illegal lifecycle transitions. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * fix(auth): classify anonymous commands from loaded records Compare the verified actor to the loaded participant tenant and session instead of rebuilding a ResourceScope. Tenant mismatch is reported before ownership so a foreign-tenant inconsistent pair cannot hide as OwnerMismatch. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * feat(participant): persist anonymous assessment identity Store tenant, participant reference, anonymous status, and creation time so command authorization can load the participant instead of rebuilding it from the proof. Exact replay is idempotent; tenant or time rebinding fails closed; linked participants stay out of this slice. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * docs(traceability): name Active PR #118 for participant persist Record the opened successor so architecture views do not leave the assessment-participant slice unlabeled. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * revert(participant): leave persist/reload on Active PR #114 history on migration 0021. This successor keeps the #104 command-auth contract fix and does not open a colliding anonymous-only persist slice. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * fix(auth): tell the truth about supplied command records Command authorization compares the verified actor to supplied participant and session values. It does not accept a ResourceScope and does not claim those aggregates were store-loaded. Align rustdoc, ADR-0003, SECURITY_AND_DATA, UML Activate, TRACEABILITY, and CHANGELOG. Use a session created after publication and before exclusive proof expiry. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * fix(auth): satisfy clippy doc-markdown on command timeline Backtick the publication and exclusive-expiry instants in the command authorization fixtures, and name honesty successor #135 in TRACEABILITY. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * fix(auth): stop claiming command records were store-loaded and named superseded #114 as the persist landing. The gate compares supplied records only; persist/reload remains Active PR #133. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * docs(traceability): name honesty successor Active PR #144 Point command-authorization honesty at the opened successor so reviewers do not treat #135 as the landing head. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * fix(auth): name persist landing #147 and drop leftover load claims The command gate still compares supplied records. Docs and the architecture contract now name Active PR #147 for persist/reload, forbid the leftover #133 pointer, and stop saying the gate authorized from loaded records. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * docs(traceability): name honesty successor Active PR #159 Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * fix(auth): name persist landing #158 instead of superseded #147 Identity persist/reload landing moved to #158. Keep the command gate honest about supplied records and stop pinning the leftover #147 pointer. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * fix(auth): drop closed #158 persist landing and loaded names Name persist/reload as Target, forbid the closed pointer, and call command-test records supplied. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * docs(traceability): name honesty successor Active PR #225 Record the opened successor so architecture views do not leave the command-auth honesty slice labeled as closed #159. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * fix(auth): keep anonymous authorization after rebase onto main Preserve account-link and anonymous-credential modules beside the session-command authorization entry point so this honesty head stays mergeable without claiming participant persist on this branch. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> * docs(traceability): drop merged Active PR leftovers after rebase After rebasing onto 0c695b9, exclusive outbox leases, observation clocks/membership, and claim-next scoring-job poll are protected-main truth. Keep #225 as the Active PR for supplied-record anonymous command authorization. Persist/reload of assessment_participant remains Target. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com> --------- Co-authored-by: Seongho Bae <me@seonghobae.me> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>


Superseded
This unterminated-history recovery slice is fully contained in the verified successor chain #147 → #160. Do not merge this predecessor.
Fresh ancestry evidence:
1dd1d8b32a7f32313755f499fecc11947268827ffb0ca5b3da71ffc93768db44484de9ede9f6d6b9is ahead of fix(identity): restore current projection on exact replay #147 by 4 commits, behind by 0.#160 retains append-only history as the lookup/uniqueness truth and adds the hosted dual-proof persist/recover command boundary. #160 remains Draft and must still satisfy exact-head CI/security/review gates. Closing this PR does not promote any successor behavior to protected-main truth.