Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
aa58a45
feat(session): persist created sessions bound to published releases
seonghobae Aug 14, 2026
d3ab60c
test(session): serialize shared PostgreSQL schema setup
seonghobae Aug 14, 2026
303ace1
docs(schema): treat inbox consumption as protected-main after #58
seonghobae Aug 14, 2026
47fbc5b
merge(main): reconcile persist PR #61 after #64
seonghobae Aug 14, 2026
b1e82c0
fix(session): drop redundant poison-error closure
seonghobae Aug 14, 2026
193c938
test(session): cover database display and classify select failure
seonghobae Aug 14, 2026
16b1cb6
merge(main): reconcile persist PR #61 after #62
seonghobae Aug 14, 2026
ec4b27c
merge(main): reconcile persist PR #61 after #63
seonghobae Aug 14, 2026
1b65042
fix(session): classify replay select errors without isolated ?
seonghobae Aug 14, 2026
ce73e48
test(session): cover classify field conflicts and overflow
seonghobae Aug 14, 2026
da20974
Merge protected main into session-persistence slice
seonghobae Aug 14, 2026
22681f8
style(session): restore formatted module file
seonghobae Aug 14, 2026
3305f09
fix(session): simplify replay database error path
seonghobae Aug 14, 2026
d66ee3f
Merge branch 'main' into feat/session-persistence-20260814
github-actions[bot] Aug 14, 2026
7c63cfd
test(session): clean conflict classification sink
seonghobae Aug 14, 2026
e4bf25e
test(session): schema-qualify fault cleanup
seonghobae Aug 14, 2026
658f5ab
docs(session): document persistence helper contracts
seonghobae Aug 14, 2026
a151779
docs(session): complete immutable replay contract
seonghobae Aug 14, 2026
92eed0d
fix(session): remove unreachable persistence reference validation
seonghobae Aug 14, 2026
5922c4d
style(session): terminate persistence module with newline
seonghobae Aug 14, 2026
9a33e57
fix(session): restore coverage-visible replay error branch
seonghobae Aug 14, 2026
60d4be9
test(session): assert classify-select database error identity
seonghobae Aug 16, 2026
077386c
test(session): cover isolation-probe database failure
seonghobae Aug 16, 2026
9ec26ad
test(session): instantiate Database error wrap in the library
seonghobae Aug 16, 2026
c7f854f
Merge branch 'main' into feat/session-persistence-20260814
github-actions[bot] Aug 16, 2026
e95f484
Merge branch 'main' into feat/session-persistence-20260814
opencode-agent[bot] Aug 16, 2026
7f7ea0f
style(session): use let-else when wrapping a failed database connect
seonghobae Aug 16, 2026
60b090d
fix(session): record created-session persist as Active PR evidence
cursoragent Aug 16, 2026
9c21b8c
feat(session): load created sessions without rechecking publication
cursoragent Aug 16, 2026
ff82de8
docs(session): record created-session load as Active PR #109
cursoragent Aug 16, 2026
e76fe4c
test(session): prove load after suspend and retire
cursoragent Aug 16, 2026
bbe8b20
feat(session): persist command history so Activate survives restart
cursoragent Aug 16, 2026
de6089c
docs(session): record command-history persist as Active PR #125
cursoragent Aug 16, 2026
b36eee9
fix(session): reject stale shorter command history on persist
cursoragent Aug 16, 2026
a196a2d
docs(session): record stale-prefix repair as Active PR #129
cursoragent Aug 16, 2026
36fc4fb
fix(session): lock session header before command persist
cursoragent Aug 16, 2026
e66e08a
docs(session): record header-row lock as Active PR #146
cursoragent Aug 16, 2026
f7b2e35
feat(session): start created sessions from published releases
cursoragent Aug 16, 2026
6e7062d
docs(session): record published-release start as Active PR #153
cursoragent Aug 16, 2026
0385589
feat(session): start sessions from locked stored publication state
cursoragent Aug 16, 2026
cca5192
docs(session): record stored-publication start lock as Active PR #180
cursoragent Aug 16, 2026
770c18c
feat(session): replay exact start after stored publication suspend
cursoragent Aug 16, 2026
6e9e244
docs(session): record exact start replay as Active PR #198
cursoragent Aug 16, 2026
5041ee6
fix(session): seal persist first-insert against unpublished reconstit…
cursoragent Aug 16, 2026
bd9ea70
docs(session): record persist first-insert seal as Active PR #205
cursoragent Aug 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable product and architecture changes are recorded here. Releases use imm
## Unreleased

### Added
- New assessment sessions start only from a currently published release. Durable start locks the stored `instrument_release` row (`created_session_for_start` / `start_created_assessment_session` / `start_created_assessment_session_from_stored_release`) so a stale in-memory Published object cannot insert after persist Suspend or Retire. First insert through `persist_assessment_session` takes the same lock, so a reconstituted Created aggregate cannot insert after that later persist. A draft, suspended, retired, missing, or digest-mismatched stored release fails closed before a first insert. Exact replay of an already stored start or Created row still returns the original session after a later persist Suspend or Retire, so a buyer who already started can retry. Reconstituting stored identity remains load, not start.
- Assessment-session command persist locks the created-session header row until the caller transaction ends, so a concurrent Activate-only worker cannot count a shorter history and then rewind a later Pause/Resume projection.
- Created assessment sessions can be loaded from PostgreSQL without re-checking current publication eligibility, so a later suspend or retire cannot rewrite stored provenance. Missing sessions return none; later stored states without command history fail closed.
- Assessment-session Activate/Pause/Resume command history persists in PostgreSQL so an in-progress session reloads after restart. Exact command replay is idempotent; sequence reuse and evidence rebinding fail closed.
- PostgreSQL persistence for created assessment sessions bound to one published locale-specific release: exact replay is idempotent, and rebinding participant, release, version, digest, locale, state, or creation time fails closed.
- Scoring-job cancel and lease-expiry fallback classification lock the current row until the caller transaction ends, so concurrent workers cannot rewrite terminal or unleased evidence.
- PostgreSQL operational-store readiness probe classifies the supported major version and write-readiness, and fails closed when a caller-declared required relation is missing.
- PostgreSQL scoring-job cancellation: queued, leased, or retry-scheduled work becomes cancelled without transferring a fence, exact replay is idempotent, and completed or quarantined evidence cannot be rewritten.
Expand Down Expand Up @@ -59,6 +64,7 @@ All notable product and architecture changes are recorded here. Releases use imm

### Fixed

- Stale shorter assessment-session command history now fails closed instead of rewinding the current-state projection, so a later Pause/Resume still reloads after a rejected Activate-only persist.
- Scoring results now reject non-canonical engine-artifact digests and accept only `sha256:` followed by 64 lowercase hexadecimal characters as immutable provenance.
- Exact replay of an already accepted response event remains idempotent after collection pauses or closes, while conflicting replay evidence still fails closed and genuinely new responses remain restricted to active sessions.
- Documentation status drift that still described protected-main `item_delivery`, participant linking, authorization, and integration domain primitives as Target after their merge.
Expand Down
12 changes: 12 additions & 0 deletions docs/QUALITY_ATTRIBUTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ This document converts broad quality goals into **stimulus → environment → r
- **Response:** inbox deduplication prevents duplicate externally visible side effects.
- **Evidence:** duplicate/reordered delivery tests.

### QA-REL-04 — Session start retry after later suspend

- **Stimulus:** a buyer retries the exact start after the stored release is suspended or retired.
- **Response:** the original created session is returned; a new `session_ref` or rebound participant fails closed and does not insert.
- **Evidence:** `start_replays_exact_session_after_stored_release_is_suspended` against real PostgreSQL and `exact_start_identity_matches_stored_session_and_rejects_rebind`.

### QA-REL-05 — Persist first-insert after later suspend

- **Stimulus:** a caller persists a reconstituted or stale in-memory Created aggregate after the stored release is suspended or retired, and no session row exists yet.
- **Response:** persist fails closed and inserts nothing; exact replay of an already stored Created row still returns duplicate.
- **Evidence:** `persist_rejects_reconstituted_first_insert_after_stored_suspend`, `persist_replays_exact_created_row_after_stored_suspend`, and `persist_rejects_first_insert_when_stored_release_is_missing` against real PostgreSQL plus `persist_maps_unpublished_stored_release_to_first_insert_seal`.

## 3. Availability and graceful degradation

### QA-AVL-01 — AI unavailable
Expand Down
8 changes: 5 additions & 3 deletions docs/TRACEABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ An active PR, architecture document, conversation decision, or scheduler plan is

| Invariant | Source | Enforcement/evidence on evaluated main | Missing evidence before GA |
|---|---|---|---|
| Server-authoritative session state | TRD §5 | `src/session.rs` + session contract tests, including published-release/locale binding at creation | persistence/API concurrency test |
| Server-authoritative session state | TRD §5 | `src/session.rs` + session contract tests, including published-release/locale binding at creation | **Active PR** #205 persist/load created-session identity and later command history with exact/conflicting replay, fail-closed stale shorter history, header-row `FOR UPDATE` before command insert/count, stored-publication start lock, persist first-insert seal, and exact start replay after later persist Suspend or Retire; HTTP/API concurrency remains missing |
| Only Active accepts responses | TRD §5–6 | `SessionState::accepts_responses` + response tests | transport-level rejection test |
| Item delivery sequence is positive and evidence-safe | TRD §5–7 | `src/item_delivery.rs` + item-delivery domain tests | durable uniqueness/order/API integration |
| Conflicting idempotency replay fails closed | TRD §6 | `src/response.rs` | DB uniqueness/concurrency test |
Expand All @@ -63,7 +63,7 @@ An active PR, architecture document, conversation decision, or scheduler plan is
| Historical result does not mutate | TRD §9 | `src/result.rs` snapshot semantics | persistence and API supersession tests |
| Narrative cannot mutate score / deterministic fallback exists | AI Governance; ADR-0018 | architecture policy | mapping implementation + canonical style-assignment key + fallback/no-score-mutation tests |
| Instrument release bytes/version/item order are immutable | TRD §7 | `src/instrument.rs` + publication contract tests; `src/postgres_instrument_release.rs` persists immutable manifest columns | API publication integration |
| Only Published release accepts new sessions | TRD §7 | `PublicationState::accepts_new_sessions` in `src/instrument.rs`; `AssessmentSession` creation copies exact published release/version/locale provenance and fails closed on unpublished eligibility or locale mismatch | session-creation persistence/API integration test |
| Only Published release accepts new sessions | TRD §7 | `PublicationState::accepts_new_sessions` in `src/instrument.rs`; `AssessmentSession` creation copies exact published release/version/locale provenance and fails closed on unpublished eligibility or locale mismatch | **Active PR** #205 stored-publication start lock plus persist first-insert seal (`load_published_instrument_release` + `start_created_assessment_session_from_stored_release` + `persist_assessment_session`) and exact start replay after later persist Suspend or Retire; load still restores created identity without re-checking current eligibility; HTTP session-creation remains missing |
| 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 |
Expand Down Expand Up @@ -128,10 +128,12 @@ migrations/
└── 0012_integration_consumption.sql
```

Still-Target logical modules/adapters include remaining product aggregate persistence/repositories, public/admin HTTP and event transports, live fast-mlsirm/Keyverse/Gyeot/TEPP/semantic-data-portal adapters, research-release staging, deterministic narrative mapping, longitudinal normalized ingestion, participant identity-link history persistence, runtime health transports/metrics, and Measurement Workbench orchestration.
Still-Target logical modules/adapters include remaining product aggregate persistence/repositories, public/admin HTTP and event transports, live fast-mlsirm/Keyverse/Gyeot/TEPP/semantic-data-portal adapters, research-release staging, deterministic narrative mapping, longitudinal normalized ingestion, participant identity-link history persistence, runtime health transports/metrics, and Measurement Workbench orchestration. Active PR #205 adds `src/postgres_assessment_session.rs`, `migrations/0014_assessment_session.sql`, and `migrations/0016_assessment_session_command.sql`; those files are not protected-main truth.

### Active implementation work that is not protected-main truth

**Active PR** #205 created-session persist, load, command-history replay, stored-publication start, exact start replay after later persist Suspend or Retire, and persist first-insert seal is not protected-main truth until an unchanged reviewed/check-clean head is integrated. New sessions start only through `created_session_for_start` / `start_created_assessment_session` / `start_created_assessment_session_from_stored_release`. Durable start locks `instrument_release` with `SELECT … FOR UPDATE` so a stale in-memory Published object cannot insert after persist Suspend or Retire. First insert through `persist_assessment_session` takes the same lock, so a reconstituted Created aggregate cannot insert after that later persist. A draft, suspended, retired, missing, or digest-mismatched stored release fails closed before a first insert. Exact replay of an already stored start or Created row still returns the original session after that later persist, so a buyer who already started can retry. A new `session_ref` or rebound participant after that later persist fails closed. Created sessions persist participant, published-release, version, digest, locale, state, and creation-time identity under `READ COMMITTED`; exact replay is idempotent and rebinding fails closed. Load restores that created identity without re-checking current publication eligibility. Later Activate/Pause/Resume commands persist in `assessment_session_command` and replay on load so an in-progress session survives restart. A stale persist that carries fewer commands than already stored fails closed and does not rewind the current-state projection. Command persist locks the `assessment_session` header row with `SELECT … FOR UPDATE` before inserting or counting commands, so a concurrent Activate-only worker cannot count a prefix and then overwrite a later Pause/Resume projection. HTTP session transport remains outside this slice. #198 is the exact start-replay predecessor; #180 is the stored-publication lock predecessor; #188 is the in-memory replay predecessor that still lacks the store lock; #153 is the in-memory-start predecessor; #164 is the unlocked stored-load predecessor on the #138 lineage; #146 is the header-lock predecessor; #129 is the sequential stale-prefix predecessor that still races under `READ COMMITTED`; #125 is the command-history predecessor that still rewinds on a stale shorter persist; #109 is the persist-and-load predecessor; #106 is persist-only.

**Active PR** #76 data-rights processing-start persistence is not protected-main truth until an unchanged reviewed/check-clean head is integrated. Identity-verified requests persist an immutable operation identity and processing-start time under `FOR UPDATE` so later lifecycle composition cannot race the classified row. Dependent-system execution remains outside this slice.

## 5. ADR traceability by concern
Expand Down
20 changes: 19 additions & 1 deletion docs/adr/0005-hosted-assessment-runtime-state-machine.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Psychometrics Commons implements an explicit hosted assessment runtime with appe
draft -> review -> published -> suspended -> retired
```

Published releases are immutable. Editing content creates a new `instrument_version_ref`. Suspension blocks new sessions but does not invalidate existing result provenance. Retirement blocks new sessions permanently unless a new release is published.
Published releases are immutable. Editing content creates a new `instrument_version_ref`. Suspension blocks new sessions but does not invalidate existing result provenance. Retirement blocks new sessions permanently unless a new release is published. A created session loaded from durable storage must restore the copied release/version/digest/locale identity without re-checking whether the release currently accepts new sessions. Starting a *new* session must call `AssessmentSession::new` or `AssessmentSession::from_currently_published_manifest` from a currently published release (`created_session_for_start` / `start_created_assessment_session` / `start_created_assessment_session_from_stored_release`). Durable start locks the stored `instrument_release` row (`SELECT … FOR UPDATE`) in the same transaction and fails closed unless stored state is `published` and locale/digest match, or an exact stored start already exists. First insert through `persist_assessment_session` takes the same lock, so a reconstituted Created aggregate cannot insert after later persist Suspend or Retire. Exact replay of an already stored Created row after that later persist stays legal. Load is not authorization. Later lifecycle commands persist as append-only history and replay on load so Activate/Pause/Resume survive process restart.

### Assessment session

Expand Down Expand Up @@ -60,6 +60,11 @@ A released result references exactly one response snapshot and one scoring resul
4. Scoring cannot begin before the response snapshot is durable.
5. Repeated completion/scoring commands are idempotent.
6. No client-provided timestamp determines authoritative ordering.
7. Persisting session command history is append-only. A shorter in-memory history than already stored is conflicting replay and must not rewind the current-state projection.
8. Command-history persist locks the `assessment_session` header row (`SELECT … FOR UPDATE`) before inserting or counting commands, so a concurrent shorter-history writer cannot count a prefix under `READ COMMITTED` and then overwrite a later projection.
9. A new session starts only from a currently published release through `created_session_for_start` / `start_created_assessment_session` / `start_created_assessment_session_from_stored_release`. Durable start locks stored `instrument_release.publication_state` in the same transaction. Reconstituting stored identity is load, not start.
10. Exact replay of an already stored start after a later persist Suspend or Retire returns the original session. A new `session_ref` or rebound participant/release identity after that later persist fails closed.
11. First insert through `persist_assessment_session` locks stored `instrument_release.publication_state` in the same transaction. A reconstituted Created aggregate cannot insert after later persist Suspend or Retire. Exact replay of an already stored Created row after that later persist stays legal.

## Failure modes

Expand All @@ -79,6 +84,9 @@ Runtime tables are private to Psychometrics Commons. Downstream consumers receiv
- concurrent duplicate and out-of-order response tests;
- crash testing between transaction and event publication;
- pause/resume and offline replay tests;
- stale shorter command-history persist fail-closed tests (`stale_shorter_command_history_cannot_rewind_paused_projection`);
- concurrent header-row lock tests (`command_persist_locks_session_header_until_caller_commits`);
- published-release start-boundary tests (`start_uses_published_release_and_never_reconstitution`, `start_rejects_unpublished_release_and_locale_mismatch`, `start_persists_published_release_and_rejects_unpublished_before_insert`, `start_from_stored_release_uses_database_publication_state`, `start_from_published_snapshot_matches_new_and_rejects_locale_mismatch`, `start_replays_exact_session_after_stored_release_is_suspended`, `exact_start_identity_matches_stored_session_and_rejects_rebind`, `persist_rejects_reconstituted_first_insert_after_stored_suspend`, `persist_replays_exact_created_row_after_stored_suspend`, `persist_rejects_first_insert_when_stored_release_is_missing`, `persist_maps_unpublished_stored_release_to_first_insert_seal`);
- immutable snapshot and supersession tests;
- end-to-end scoring dispatch contract tests.

Expand All @@ -91,3 +99,13 @@ Runtime tables are private to Psychometrics Commons. Downstream consumers receiv
## Reversal conditions

Revisit the storage implementation if event volume demands a different backend, but retain state semantics, idempotency, immutable snapshots, and outbox guarantees.

## References

Fowler, M. (2005, December 12). *Event sourcing*. https://martinfowler.com/eaaDev/EventSourcing.html

Hohpe, G., & Woolf, B. (2003). *Enterprise integration patterns: Designing, building, and deploying messaging solutions*. Addison-Wesley.

PostgreSQL Global Development Group. (2026). *PostgreSQL 18 documentation*. https://www.postgresql.org/docs/18/index.html

PostgreSQL Global Development Group. (2026). *Explicit locking*. In *PostgreSQL 18 documentation*. https://www.postgresql.org/docs/18/explicit-locking.html
Loading
Loading