Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 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
51e14d8
test(session): cover created-session load failure arms
cursoragent Aug 16, 2026
97895f4
docs(session): record persist-and-load coverage as Active PR #121
cursoragent Aug 16, 2026
e9d3bb3
feat(session): start created sessions from published releases
cursoragent Aug 16, 2026
bb18b06
docs(session): record start composition as Active PR #138
cursoragent Aug 16, 2026
e307a2b
test(recovery): preserve inbox claim deadline evidence
seonghobae Aug 16, 2026
d393935
test(session): instantiate Database display without uncovered connect…
seonghobae Aug 16, 2026
e0a41ba
Merge branch 'main' into cursor/bc-0c5af809-ebff-45b2-9fcf-dfa3c8fbdb…
github-actions[bot] Aug 16, 2026
7c24084
Merge branch 'main' into cursor/bc-0c5af809-ebff-45b2-9fcf-dfa3c8fbdb…
seonghobae Aug 17, 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ All notable product and architecture changes are recorded here. Releases use imm
## Unreleased

### Added
- Session start composition calls `AssessmentSession::new` from a currently published release and then persists the created identity, so a suspended or retired release cannot begin a new session and reconstitution cannot be used as the start path.
- 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, corrupt stored identity, and load-path database failures 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
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** #138 persist, load, and start created-session identity with exact/conflicting replay; 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** #138 start composition (`created_session_for_start`) plus persist/load 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 #138 adds `src/postgres_assessment_session.rs` and `migrations/0014_assessment_session.sql`; those files are not protected-main truth.

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

**Active PR** #138 created-session persist, load, and start composition is not protected-main truth until an unchanged reviewed/check-clean head is integrated. 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. New sessions start through `created_session_for_start` / `start_created_assessment_session`, which call `AssessmentSession::new` and then persist. HTTP session transport and later-state command history remain outside this slice. #121 is the uncovered-start predecessor; #109 is the uncovered load predecessor; #106 is the persist-only predecessor.

**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
10 changes: 9 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` from a currently published release (`created_session_for_start` / `start_created_assessment_session`). Persist of an already-created aggregate is not the start boundary, and load is not authorization.

### Assessment session

Expand Down Expand Up @@ -91,3 +91,11 @@ 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

American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. https://www.testingstandards.net/

International Organization for Standardization. (2022). *ISO/IEC 27001:2022 Information security, cybersecurity and privacy protection—Information security management systems—Requirements* (3rd ed.). https://www.iso.org/standard/27001

Temoshok, D., Proud-Madruga, D., Choong, Y.-Y., Galluzzo, R., Gupta, S., LaSalle, C., Lefkovitz, N., & Regenscheid, A. (2025). *Digital identity guidelines* (NIST Special Publication 800-63-4). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-63-4
5 changes: 5 additions & 0 deletions docs/architecture/AS_BUILT_SCHEMA.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@ Protected main contains executable PostgreSQL 18 persistence subsets for integra
| `scoring_job_state` | scoring | Implemented subset |
| `instrument_release` | instrument publication | Implemented subset |
| `integration_consumption` | integration | **Active PR** #58 (not protected-main truth) |
| `assessment_session` | session | **Active PR** #138 (not protected-main truth) |

The protected-main integration identity is source- and tenant-scoped. A physical implementation must continue to preserve the stronger logical tenant/resource, replay, and crash-safety invariants in ADR-0014 and ADR-0015.

## Active PR assessment-session physical schema

PR #138 `migrations/0014_assessment_session.sql` and `src/postgres_assessment_session.rs` persist and load one created assessment-session identity bound to a published locale-specific release. The slice is **Active PR**, not protected-main truth. It stores participant, release, version, digest, locale, `created` state, and creation time. Exact replay is idempotent. Rebinding any stored field fails closed. Load restores that identity without asking whether the release still accepts new sessions. New sessions start through `created_session_for_start` / `start_created_assessment_session` (`AssessmentSession::new` then persist). Isolation is the global opaque `session_ref` primary key; this first slice does not add `tenant_ref` because the domain `AssessmentSession` aggregate does not carry tenant. The slice does not persist command history, later lifecycle states, or HTTP session transport.

## Active PR inbox-consumption physical schema

PR #58 (`feat/inbox-consumption-persistence-20260814`) `migrations/0012_integration_consumption.sql` and `src/postgres_inbox_consumption.rs` adapter persist one consumption work item for an existing `integration_inbox` receipt. The slice is **Active PR**, not protected-main truth. It stores pending/processing/completed/quarantined evidence, a monotonically increasing fencing token, a time-bounded processing claim, a durable `side_effect_ref`, and optional completion or quarantine evidence. Receipt-only inbox rows remain uncompleted. A processing claim cannot be stolen by another worker. Expire-and-reclaim returns an expired claim to pending without transferring the crashed worker's fence.
Expand Down
1 change: 1 addition & 0 deletions docs/architecture/ERD.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ The target ERD deliberately includes several logical entities that are not yet p

- `instrument_release` is the locale-specific publication identity already owned by `src/instrument.rs`. Physical `migrations/0006_instrument_release.sql` persists that one-row aggregate (immutable manifest columns plus `publication_state`); HTTP publication transport remains Target.
- `data_rights_request` and `data_rights_propagation_state` are the first durable export/deletion slice. Physical `migrations/0003_data_rights_propagation.sql` stores requested-state identity plus one local outbox event per dependent system; verification, processing, completion, and dependent-system execution remain Target.
- Physical `assessment_session` exists only on Active PR #138 (`migrations/0014_assessment_session.sql`): Created-only identity (participant, release, version, digest, locale, `created`, creation time) with no command history. Load reconstitutes that identity without re-checking current publication eligibility. New sessions start through `start_created_assessment_session`. Protected main still has the `src/session.rs` aggregate only.
- `item_delivery_event` reflects the already-merged `src/item_delivery.rs` domain primitive; durable persistence/API orchestration is still Target.
- `consent_ledger` and `consent_event` persist the already-merged `src/consent.rs` append-only ledger. Physical persistence is carried by Active PR #49 (`migrations/0005_consent_lifecycle.sql`); HTTP consent transport and derived snapshot tables remain Target.
- `participant_identity_link` is the persistence target accepted by ADR-0020. The current `src/participant.rs` `keyverse_subject_ref` field is an application-domain first-link projection, not the future mutable persistence source of truth.
Expand Down
3 changes: 2 additions & 1 deletion docs/architecture/UML.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,8 @@ sequenceDiagram

P->>C: choose published instrument + locale
C->>A: POST session (idempotency key)
A->>DB: persist anonymous participant/session
A->>A: AssessmentSession::new from currently published release
A->>DB: persist created session identity
DB-->>A: session_ref + pinned instrument version
A-->>C: session resource + item-delivery contract

Expand Down
62 changes: 62 additions & 0 deletions migrations/0014_assessment_session.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
CREATE TABLE IF NOT EXISTS assessment_session (
session_ref TEXT NOT NULL
CHECK (
session_ref = btrim(session_ref)
AND session_ref <> ''
AND NOT (
session_ref ~ '[[:digit:]]'
AND session_ref ~ '^[[:digit:]+,.eE-]+$'
)
),
participant_ref TEXT NOT NULL
CHECK (
participant_ref = btrim(participant_ref)
AND participant_ref <> ''
AND NOT (
participant_ref ~ '[[:digit:]]'
AND participant_ref ~ '^[[:digit:]+,.eE-]+$'
)
),
instrument_release_ref TEXT NOT NULL
CHECK (
instrument_release_ref = btrim(instrument_release_ref)
AND instrument_release_ref <> ''
AND NOT (
instrument_release_ref ~ '[[:digit:]]'
AND instrument_release_ref ~ '^[[:digit:]+,.eE-]+$'
)
),
instrument_version_ref TEXT NOT NULL
CHECK (
instrument_version_ref = btrim(instrument_version_ref)
AND instrument_version_ref <> ''
AND NOT (
instrument_version_ref ~ '[[:digit:]]'
AND instrument_version_ref ~ '^[[:digit:]+,.eE-]+$'
)
),
instrument_release_content_digest TEXT NOT NULL
CHECK (instrument_release_content_digest ~ '^sha256:[0-9a-f]{64}$'),
locale TEXT NOT NULL
CHECK (
locale = btrim(locale)
AND locale ~ '^[A-Za-z]{2,8}(-[A-Za-z0-9]{1,8})*$'
),
session_state TEXT NOT NULL
CHECK (
session_state IN (
'created',
'active',
'paused',
'completed',
'scoring',
'scored',
'released',
'expired',
'cancelled',
'invalidated'
)
),
created_at_unix_ms BIGINT NOT NULL CHECK (created_at_unix_ms > 0),
PRIMARY KEY (session_ref)
);
4 changes: 2 additions & 2 deletions src/instrument.rs
Original file line number Diff line number Diff line change
Expand Up @@ -916,7 +916,7 @@ fn required_reference(reference: &str) -> Result<&str, InstrumentReleaseError> {
normalized_reference(reference).ok_or(InstrumentReleaseError::InvalidReference)
}

fn valid_sha256_digest(digest: &str) -> bool {
pub(crate) fn valid_sha256_digest(digest: &str) -> bool {
let Some(hex) = digest.strip_prefix("sha256:") else {
return false;
};
Expand All @@ -926,7 +926,7 @@ fn valid_sha256_digest(digest: &str) -> bool {
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}

fn valid_locale(locale: &str) -> bool {
pub(crate) fn valid_locale(locale: &str) -> bool {
let mut subtags = locale.split('-');
let primary = subtags.next().unwrap_or_default();
if !(2..=8).contains(&primary.len()) || !primary.bytes().all(|byte| byte.is_ascii_alphabetic())
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub mod integration;
pub mod item_delivery;
pub mod narrative;
pub mod participant;
pub mod postgres_assessment_session;
pub mod postgres_consent;
pub mod postgres_data_rights;
pub mod postgres_data_rights_processing;
Expand Down
Loading
Loading