diff --git a/.github/workflows/criterion-temporal-quality.yml b/.github/workflows/criterion-temporal-quality.yml index af41ad310..a26573756 100644 --- a/.github/workflows/criterion-temporal-quality.yml +++ b/.github/workflows/criterion-temporal-quality.yml @@ -8,6 +8,8 @@ on: - main paths: - "database/migrations/0011_criterion_observation_scope.sql" + - "database/migrations/0014_criterion_observation_chronology.sql" + - "database/migrations/0015_criterion_observation_statement_default.sql" - "tests/test_criterion_observation_scope_postgres.sh" - ".github/workflows/criterion-temporal-quality.yml" workflow_dispatch: @@ -52,6 +54,38 @@ jobs: run: test "$(git rev-parse HEAD)" = "$ORGMETRA_EXPECTED_HEAD_SHA" - name: Prove recorded-time and UTC boundaries run: bash tests/test_criterion_observation_scope_postgres.sh + - name: Reject observations recorded before they occurred + env: + PGOPTIONS: -c orgmetra.tenant_record_id=10000000-0000-7000-8000-000000000101 + run: | + set +e + output="$(psql "$DATABASE_URL" -v ON_ERROR_STOP=1 <<'SQL' 2>&1 + INSERT INTO criterion_observation ( + tenant_record_id, criterion_observation_id, criterion_blueprint_id, + performance_cycle_id, person_record_id, observed_value, + observed_at, recorded_from + ) VALUES ( + '10000000-0000-7000-8000-000000000101', + '10000000-0000-7000-8000-000000000134', + '10000000-0000-7000-8000-000000000110', + '10000000-0000-7000-8000-000000000109', + '10000000-0000-7000-8000-000000000102', + 4.8, + TIMESTAMPTZ '2026-08-18 12:00:00+00', + TIMESTAMPTZ '2026-08-17 12:00:00+00' + ); + SQL + )" + status=$? + set -e + if [[ $status -eq 0 ]]; then + echo "criterion observation accepted an observed_at after its system-recorded time" >&2 + exit 1 + fi + if [[ "$output" != *"criterion observation cannot be recorded before it was observed"* ]]; then + echo "future criterion observation failed for an unexpected reason: $output" >&2 + exit 1 + fi - name: Require clean checkout run: | git diff --exit-code diff --git a/CHANGELOG.md b/CHANGELOG.md index 99f4752d7..926e06870 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ All notable changes to Orgmetra will be documented in this file. - Accepted ADRs 0001–0003 now include buyer-facing Context, Decision, and Consequences grounded in verified ISO 30400:2022, ISO 30414:2025, Uniform Guidelines (29 C.F.R. Part 1607), SIOP (2018), OpenAPI Specification v3.2.0, OpenID Connect Core 1.0 errata set 2, CloudEvents v1.0.2, Jensen and Snodgrass (1999), Snodgrass (1999), and Allen (1983) records already listed in `docs/doctoring/REFERENCES.md`. ADRs 0004 and 0005 gained APA 7th References pointers to that same bibliography without changing their Decision bodies. - Active-PR governed Job Analysis persistence/API on the canonical `JobAnalysisSnapshot` model: migration `0013_job_analysis_snapshot.sql` stores immutable tenant-scoped snapshot, Task, KSAO, Task–KSAO, FJA and write-command evidence; `POST /v1/tenants/{tenant_record_id}/job-analysis-snapshots` and matching GET enforce purpose-bound Keyverse scope, authenticated-principal actor authority, bounded/strict JSON handling, transactional Idempotency-Key serialization, parent-scope fail-closed integrity, forced RLS, and atomic audit/outbox evidence. ADR 0014 records the persistence decision while ADR 0007 remains the domain/evidence authority; validated evidence still requires accountable human review and non-LLM provenance, and the service does not make a high-impact employment decision. - Active-PR `orgmetra_selection_review` packet for PII-minimized, evidence-bound human selection review: canonical operational tenant identity, UUID-backed opaque candidate/Job/sealed-evidence/reviewer references, explicit purpose/reason/evidence version, deterministic canonical JSON and SHA-256 correlation, mandatory human decision state, redacted packet repr, and provenance-paired model evidence that remains `untrusted_draft`, with exact 100% owned statement and branch coverage required by its quality gate. -- Active performance-criterion scope hardening: `criterion_observation_scope_guard` rejects criterion outcomes for a Job the worker did not effectively hold at the observation date, observations before the relevant assignment, and observations outside the referenced performance cycle while preserving valid multiple-assignment cases and existing bitemporal correction semantics. The guard evaluates current-recorded facts, derives the date coordinate from `observed_at` in UTC so session `TimeZone` cannot alter the result, uses a trusted function search path, and adds no PII or automated employment decision authority. The Foundation PostgreSQL contract also rejects a closed `recorded_to` on each time-coordinate lookup and proves UTC midnight plus non-UTC session `TimeZone` boundaries. +- Active performance-criterion scope hardening: migration `0014_criterion_observation_chronology.sql` upgrades the already-installed `0011` trigger function without rewriting its migration or replacing the trigger binding, and migration `0015_criterion_observation_statement_default.sql` changes omitted observation `recorded_from` values from transaction time to statement time for long-transaction correctness. `criterion_observation_scope_guard` rejects criterion outcomes for a Job the worker did not effectively hold at the observation date, observations before the relevant assignment, observations recorded before `observed_at`, future observations or recording timestamps relative to database statement time, and observations outside the referenced performance cycle while preserving valid multiple-assignment cases and existing bitemporal correction semantics. The guard evaluates current-recorded facts, derives the date coordinate from `observed_at` in UTC so session `TimeZone` cannot alter the result, uses a trusted function search path, and adds no PII or automated employment decision authority. The Foundation PostgreSQL contract also rejects a closed `recorded_to` on each time-coordinate lookup, proves UTC midnight plus non-UTC session `TimeZone` boundaries, and accepts a valid observation inserted after a transaction has already begun. - Bitemporal tenant-scoped organization hierarchy validation that rejects visible indirect parent cycles and reuses single-valued recorded-time reconstruction before graph traversal. - Stacked governed job-analysis evidence contract via `JobAnalysisSnapshot`, `TaskEvidence`, `KSAORequirement`, `TaskKSAOLink`, `FunctionalJobAnalysisProfile`, and `EvidenceSource`: tenant/Job-scoped observable tasks, explicit Task-to-KSAO linkage, importance/difficulty/proficiency ratings, source/version/retrieval/SHA-256 provenance, deterministic canonical snapshot bytes, current O*NET evidence support, and historical DOT Data/People/Things compatibility. Validated snapshots require accountable human review and complete non-LLM evidence; LLM-origin material remains `analysis_draft`, and the snapshot is evidence input rather than a hiring, promotion, termination, compensation, or other high-impact employment decision. - Stacked governed audit/outbox slice via `AuditOutboxEvent`, `audit_event_record`, `outbox_delivery_record`, and `outbox_delivery_escalation_record`: CloudEvents 1.0-compatible PII-minimized metadata, exact canonical JSON bytes, database-verified SHA-256 digests, mandatory human confirmation for high-impact events, immutable audit evidence, tenant RLS, atomic audit/outbox insertion, guarded pending/leased/delivered/dead-lettered delivery state, tenant-safe `claim_outbox_delivery(...)` with deterministic due-work ordering, `FOR UPDATE ... SKIP LOCKED`, opaque worker identity, bounded future leases, immutable envelope return, and atomic takeover of genuinely expired leases only while retry attempts remain; owner-bound `complete_outbox_delivery(...)` and `retry_outbox_delivery(...)`; database-budget-governed `dead_letter_outbox_delivery(...)`; and a separately privileged `operator_dead_letter_expired_outbox_delivery(...)` recovery path for an exhausted final lease whose recorded worker identity is permanently unavailable. `maximum_attempt_count` is persisted on the delivery row, defaults to 5, is constrained to 1 through 100, and cannot be lowered by a dispatcher during finalization. Migration 0007 prevents retry or expired-lease takeover from creating attempt N+1; migration 0008 adds TRUNCATE guards, trusted function search paths, a concurrently built due-work partial index, session-independent immutable envelope validation, and operator recovery backed by separate NOLOGIN/NOBYPASSRLS owner/capability roles so the externally assignable operator role can invoke recovery without receiving direct transport-table read/write rights. Migration 0008 also rejects pre-existing reserved recovery-role names before project DDL, atomically contains the temporary schema-creation privilege used for function ownership handoff, and forces deferred escalation binding while the narrow SECURITY DEFINER owner is still active. Exponential/backoff policy selection, policy-specific producer configuration, and external delivery receipts remain subsequent work. @@ -73,4 +73,4 @@ All notable changes to Orgmetra will be documented in this file. ### Notes -- Protected `develop` at `e7ddb7a78a5e1460410005d10f43ebf18c5e12e4` includes normalized validity-study and criterion integrity, bitemporal workforce composition, governed candidate-to-worker conversion, purpose-bound PII authorization, GET-only People reads, governed People mutation/idempotency API, and the accepted ADR 0001–0003 source expansion integrated by #37. Job Analysis persistence/API and the selection-review packet remain active-PR truth until their unchanged exact heads satisfy fresh gates and merge. +- Protected `develop` at `e7ddb7a78a5e1460410005d10f43ebf18c5e12e4` includes the baseline normalized validity-study and criterion integrity, bitemporal workforce composition, governed candidate-to-worker conversion, purpose-bound PII authorization, GET-only People reads, governed People mutation/idempotency API, and the accepted ADR 0001–0003 source expansion integrated by #37. The recorded-before-observed criterion chronology guard remains active-PR truth until its unchanged exact head satisfies fresh gates and merges. Job Analysis persistence/API and the selection-review packet likewise remain active-PR truth until their unchanged exact heads satisfy fresh gates and merge. diff --git a/database/migrations/0014_criterion_observation_chronology.sql b/database/migrations/0014_criterion_observation_chronology.sql new file mode 100644 index 000000000..6768badba --- /dev/null +++ b/database/migrations/0014_criterion_observation_chronology.sql @@ -0,0 +1,173 @@ +-- Add chronology protection without rewriting the already released 0011 migration. +-- CREATE OR REPLACE preserves criterion_observation_scope_guard's binding while +-- upgrading databases that have already installed enforce_criterion_observation_scope(). + +CREATE OR REPLACE FUNCTION enforce_criterion_observation_scope() +RETURNS trigger +LANGUAGE plpgsql +SET search_path = pg_catalog, public +AS $$ +DECLARE + observation_effective_date date; + criterion_job_profile_id uuid; +BEGIN + -- An immutable performance observation cannot truthfully enter system time + -- before the event it claims to have observed. Reject impossible chronology + -- before using the claimed observation instant for any scope lookup. + IF NEW.recorded_from < NEW.observed_at THEN + RAISE EXCEPTION 'criterion observation cannot be recorded before it was observed' + USING ERRCODE = '23514'; + END IF; + + -- Effective periods in the current foundation are date-granular. Convert + -- the evidence instant through UTC explicitly so session TimeZone cannot + -- move an observation across a date boundary and bypass temporal checks. + observation_effective_date := (NEW.observed_at AT TIME ZONE 'UTC')::date; + + SELECT blueprint.job_profile_id + INTO criterion_job_profile_id + FROM criterion_blueprint AS blueprint + WHERE blueprint.tenant_record_id = NEW.tenant_record_id + AND blueprint.criterion_blueprint_id = NEW.criterion_blueprint_id + AND blueprint.effective_from <= observation_effective_date + AND ( + blueprint.effective_to IS NULL + OR observation_effective_date < blueprint.effective_to + ) + AND blueprint.recorded_from <= statement_timestamp() + AND ( + blueprint.recorded_to IS NULL + OR statement_timestamp() < blueprint.recorded_to + ); + + IF criterion_job_profile_id IS NULL THEN + RAISE EXCEPTION 'criterion observation references a criterion outside its effective or current-recorded period' + USING ERRCODE = '23514'; + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM performance_cycle AS cycle_record + WHERE cycle_record.tenant_record_id = NEW.tenant_record_id + AND cycle_record.performance_cycle_id = NEW.performance_cycle_id + AND cycle_record.effective_from <= observation_effective_date + AND ( + cycle_record.effective_to IS NULL + OR observation_effective_date < cycle_record.effective_to + ) + AND cycle_record.recorded_from <= statement_timestamp() + AND ( + cycle_record.recorded_to IS NULL + OR statement_timestamp() < cycle_record.recorded_to + ) + ) THEN + RAISE EXCEPTION 'criterion observation is outside the performance cycle effective period' + USING ERRCODE = '23514'; + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM assignment_record AS assignment + JOIN position_record AS position + ON position.tenant_record_id = assignment.tenant_record_id + AND position.position_record_id = assignment.position_record_id + WHERE assignment.tenant_record_id = NEW.tenant_record_id + AND assignment.person_record_id = NEW.person_record_id + AND position.job_profile_id = criterion_job_profile_id + AND assignment.effective_from <= observation_effective_date + AND ( + assignment.effective_to IS NULL + OR observation_effective_date < assignment.effective_to + ) + AND assignment.recorded_from <= statement_timestamp() + AND ( + assignment.recorded_to IS NULL + OR statement_timestamp() < assignment.recorded_to + ) + AND position.recorded_from <= statement_timestamp() + AND ( + position.recorded_to IS NULL + OR statement_timestamp() < position.recorded_to + ) + ) THEN + RAISE EXCEPTION 'criterion observation does not match an effective worker assignment for the criterion job' + USING ERRCODE = '23514'; + END IF; + + -- A stale assignment anchor must not make a terminated employment or a + -- closed/frozen/abolished seat look like valid performance context. Reuse + -- the same status semantics as the HRIS assignment kernel and require one + -- *single* matching assignment to have both eligible employment and a + -- staffable position at the observation coordinate. + IF NOT EXISTS ( + SELECT 1 + FROM assignment_record AS assignment + JOIN position_record AS position + ON position.tenant_record_id = assignment.tenant_record_id + AND position.position_record_id = assignment.position_record_id + JOIN employment_record_version AS employment_version + ON employment_version.tenant_record_id = assignment.tenant_record_id + AND employment_version.employment_record_id = assignment.employment_record_id + JOIN position_record_version AS position_version + ON position_version.tenant_record_id = assignment.tenant_record_id + AND position_version.position_record_id = assignment.position_record_id + WHERE assignment.tenant_record_id = NEW.tenant_record_id + AND assignment.person_record_id = NEW.person_record_id + AND position.job_profile_id = criterion_job_profile_id + AND assignment.effective_from <= observation_effective_date + AND ( + assignment.effective_to IS NULL + OR observation_effective_date < assignment.effective_to + ) + AND assignment.recorded_from <= statement_timestamp() + AND ( + assignment.recorded_to IS NULL + OR statement_timestamp() < assignment.recorded_to + ) + AND position.recorded_from <= statement_timestamp() + AND ( + position.recorded_to IS NULL + OR statement_timestamp() < position.recorded_to + ) + AND employment_version.employment_status_code IN ('active', 'leave') + AND employment_version.effective_from <= observation_effective_date + AND ( + employment_version.effective_to IS NULL + OR observation_effective_date < employment_version.effective_to + ) + AND employment_version.recorded_from <= statement_timestamp() + AND ( + employment_version.recorded_to IS NULL + OR statement_timestamp() < employment_version.recorded_to + ) + AND position_version.position_status_code IN ('active', 'open') + AND position_version.effective_from <= observation_effective_date + AND ( + position_version.effective_to IS NULL + OR observation_effective_date < position_version.effective_to + ) + AND position_version.recorded_from <= statement_timestamp() + AND ( + position_version.recorded_to IS NULL + OR statement_timestamp() < position_version.recorded_to + ) + ) THEN + RAISE EXCEPTION 'criterion observation lacks an assignment with eligible employment and staffable position coverage' + USING ERRCODE = '23514'; + END IF; + + -- A caller may order both supplied timestamps after one another while still + -- claiming an event that has not happened at the database's current time. + IF NEW.observed_at > statement_timestamp() THEN + RAISE EXCEPTION 'criterion observation cannot be observed in the future' + USING ERRCODE = '23514'; + END IF; + + IF NEW.recorded_from > statement_timestamp() THEN + RAISE EXCEPTION 'criterion observation cannot be recorded in the future' + USING ERRCODE = '23514'; + END IF; + + RETURN NEW; +END; +$$; diff --git a/database/migrations/0015_criterion_observation_statement_default.sql b/database/migrations/0015_criterion_observation_statement_default.sql new file mode 100644 index 000000000..e1fe380f1 --- /dev/null +++ b/database/migrations/0015_criterion_observation_statement_default.sql @@ -0,0 +1,5 @@ +-- Record omitted criterion-observation timestamps at statement time. +-- The transaction timestamp can precede a real observation in a long transaction. + +ALTER TABLE criterion_observation +ALTER COLUMN recorded_from SET DEFAULT statement_timestamp(); diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 22a4178fe..99cb6884f 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -16,7 +16,7 @@ | GET-only People API | People API / purpose-bound read boundary | `GET /v1/tenants/{tenant_record_id}/people/{person_record_id}`, `read_worker_people_record()`, `PostgresPeopleReadPort` | People API HTTP and PostgreSQL read contracts with exact 100% owned statement/branch coverage; current conversion lineage; no mutation writes | ADR-0002, ADR-0008 | implemented_on_protected_main | | Governed People writes and confirmed-hire materialization | People API / purpose-bound mutation boundary | `POST /v1/employment-records`, `POST /v1/position-records`, `POST /v1/assignment-records`, `POST /v1/tenants/{tenant_record_id}/candidate-worker-conversions`, `people_mutation_idempotency_record` | People command/HTTP/PostgreSQL contracts with exact owned statement/branch coverage plus PostgreSQL tenant-RLS, atomic audit/outbox/idempotency, identical-retry replay, changed-command rejection, rollback, and concurrent-key regression | ADR-0002, ADR-0006, ADR-0008 | implemented_on_protected_main | | Evidence-grounded Job analysis with governed Task/FJA/KSAO persistence | Job Analysis / Workforce Validation | `JobAnalysisSnapshot`, `TaskEvidence`, `KSAORequirement`, `FunctionalJobAnalysisProfile`, `TaskKSAOLink`, `EvidenceSource`, `job_analysis_snapshot`, `job_analysis_task_item`, `job_analysis_ksao_item`, `job_analysis_task_ksao_link`, `job_analysis_write_command`, `POST /v1/tenants/{tenant_record_id}/job-analysis-snapshots`, `GET /v1/tenants/{tenant_record_id}/job-analysis-snapshots/{analysis_record_id}` | domain tenant/Job isolation, source/version/digest provenance, task-KSAO completeness, deterministic canonicalization, accountable human-review and LLM-draft-only regressions; migration 0013 PostgreSQL parent-scope/RLS/append-only/idempotency/audit-outbox persistence; exact route/OpenAPI/error contracts and 100% owned service statement/branch coverage | ADR-0007, ADR-0014 | implemented_on_active_pr | -| Job-, cycle-, and staffing-scoped performance criterion observations | Performance / Workforce Validation | `criterion_observation`, `criterion_blueprint`, `performance_cycle`, `assignment_record`, `employment_record_version`, `position_record`, `position_record_version` | PostgreSQL wrong-Job, pre-assignment, out-of-cycle, frozen-Position, terminated-employment, closed-recorded-time, and session-TimeZone/UTC-midnight rejection plus valid worker-Job/staffing acceptance | ADR-0009 | implemented_on_protected_main | +| Job-, cycle-, and staffing-scoped performance criterion observations | Performance / Workforce Validation | `criterion_observation`, `criterion_blueprint`, `performance_cycle`, `assignment_record`, `employment_record_version`, `position_record`, `position_record_version` | PostgreSQL wrong-Job, pre-assignment, recorded-before-observed, out-of-cycle, frozen-Position, terminated-employment, closed-recorded-time, and session-TimeZone/UTC-midnight rejection plus valid worker-Job/staffing acceptance | ADR-0009 | implemented_on_active_pr | | Governed immutable audit and transactional outbox persistence | Audit Provenance / Integration Hub | `AuditOutboxEvent.canonical_json()`, `audit_event_record`, `outbox_delivery_record`, SHA-256 envelope digest | canonical-byte/digest regression plus PostgreSQL digest, allowlist/PII, high-impact confirmation, append-only, atomicity, lease-transition, terminal-state, and reserved-UUID tests | ADR-0006 | implemented_on_active_pr | | Tenant-safe atomic outbox claiming and crash recovery | Integration Hub dispatcher boundary | `outbox_delivery_record` pending/expired-lease claim indexes plus `claim_outbox_delivery(...)` | PostgreSQL already-expired-new-lease rejection, due-order claim, live-lease exclusion, pre-exhaustion takeover with `lease_expired` evidence, retry-budget claim bound, tenant-context binding, opaque-worker validation, and bounded-lease contract | ADR-0006 | implemented_on_active_pr | | Owner-bound outbox completion, retry, and terminal dead-letter escalation | Integration Hub dispatcher boundary | immutable `outbox_delivery_record.maximum_attempt_count`, `complete_outbox_delivery(...)`, `retry_outbox_delivery(...)`, `dead_letter_outbox_delivery(...)`, `outbox_delivery_escalation_record` | PostgreSQL foreign/stale-owner denial, dispatcher-budget-signature rejection, direct-terminal-DML rejection, stored-budget exhaustion, retry-attempt-N+1 denial, exhausted expired-lease non-reclaimability, recorded-owner terminalization, nonterminal-escalation rejection, terminal non-reclaimability, and append-only escalation evidence | ADR-0006 | implemented_on_active_pr | diff --git a/docs/adr/0009-performance-criterion-observation-scope.md b/docs/adr/0009-performance-criterion-observation-scope.md index 69baa87c9..9331333cc 100644 --- a/docs/adr/0009-performance-criterion-observation-scope.md +++ b/docs/adr/0009-performance-criterion-observation-scope.md @@ -23,6 +23,9 @@ The governing references are already recorded in `docs/doctoring/REFERENCES.md` 9. Existing bitemporal mutation guards remain authoritative. This ADR adds insert-time scope integrity; it does not weaken correction history or row-level tenant isolation. 10. No additional PII is copied into the scope check. It operates only on opaque record identifiers, status codes, and temporal/job relationships. 11. Any downstream high-impact employment decision that consumes a performance outcome still requires the separately governed human confirmation, actor, purpose, reason, evidence-version, and immutable audit controls. Passing this scope guard is necessary evidence hygiene, never sufficient authority for a decision. +12. An observation's system-recorded instant must not precede its `observed_at` instant; neither `observed_at` nor `recorded_from` may be later than the database statement time. The trigger rejects these impossible timestamp orderings without removing support for explicitly backdated historical facts. +13. Migration `0014_criterion_observation_chronology.sql` applies this chronology guard to databases that already installed migration `0011`; it replaces the function body without dropping or recreating `criterion_observation_scope_guard`. +14. Migration `0015_criterion_observation_statement_default.sql` changes the `criterion_observation.recorded_from` default to `statement_timestamp()`. This preserves the invariant for an insert that occurs after a long transaction began, while explicit caller-supplied recording instants remain subject to the chronology and future-time checks. ## Rejected alternatives @@ -48,9 +51,9 @@ Rejected because invalid raw observations would already be admitted to the autho ## Evidence -`tests/test_criterion_observation_scope_postgres.sh` is the executable acceptance contract. It first established the RED condition against the protected baseline: a worker assigned to Job A could receive a criterion observation for Job B. A second RED hardening cycle proved that checking only the assignment anchor still admitted outcomes while the Position was frozen or after the employment was terminated. The repaired contract must reject those cases, reject an observation dated before the worker's relevant assignment, reject an observation outside the performance cycle, and persist an in-cycle observation for the worker's actually assigned Job while both employment and Position remain eligible. It also closes each current-recorded lookup (`criterion_blueprint`, `performance_cycle`, `assignment_record`, `position_record`, `employment_record_version`, and `position_record_version`) inside an aborting transaction and requires rejection, then proves UTC calendar-date conversion under non-UTC session `TimeZone` at the UTC midnight assignment-start accept path and the last UTC instant of the prior day. +`tests/test_criterion_observation_scope_postgres.sh` is the executable acceptance contract. It first established the RED condition against the protected baseline: a worker assigned to Job A could receive a criterion observation for Job B. A second RED hardening cycle proved that checking only the assignment anchor still admitted outcomes while the Position was frozen or after the employment was terminated. The repaired contract must reject those cases, reject an observation dated before the worker's relevant assignment, reject an observation whose system-recorded instant precedes `observed_at`, reject an observation outside the performance cycle, and persist an in-cycle observation for the worker's actually assigned Job while both employment and Position remain eligible. It also closes each current-recorded lookup (`criterion_blueprint`, `performance_cycle`, `assignment_record`, `position_record`, `employment_record_version`, and `position_record_version`) inside an aborting transaction and requires rejection, then proves UTC calendar-date conversion under non-UTC session `TimeZone` at the UTC midnight assignment-start accept path and the last UTC instant of the prior day. The dedicated exact-head workflow step exercises the impossible recorded-before-observed chronology. -The hosted PostgreSQL contract is required to run from the exact PR head. Queued, stale, predecessor, status-only, or model-only evidence is non-passing. +The hosted PostgreSQL contract is required to run from the exact PR head. It also begins a transaction, waits before inserting with the default `recorded_from`, and proves the committed value is not older than `observed_at`. Queued, stale, predecessor, status-only, or model-only evidence is non-passing. ## Consequences diff --git a/manifest.json b/manifest.json index 97f2bab14..44c06a938 100644 --- a/manifest.json +++ b/manifest.json @@ -1 +1 @@ -{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"32cc4ef78d1eca557fa01731026840be01211a043eb0ada552e4e6cb9eace353","bytes":17295,"lines":76},{"path":"CLAUDE.md","sha256":"add33884f466d324e20875388d103de41c6e062938a6e98727dc83a87ffe976f","bytes":1229,"lines":20},{"path":"LICENSE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30","bytes":11358,"lines":202},{"path":"NOTICE","sha256":"34b4618e946bdd8d33407d6ac5279f0a0388f5e7c8f79d2e7d8c3c47d0266042","bytes":305,"lines":4},{"path":"README.md","sha256":"1a9fc400d26d8137ae5911488794a6d3fa915957c95f27b36a48cef0fdf823c6","bytes":3785,"lines":81},{"path":"database/migrations/0001_foundation_schema.sql","sha256":"ce2ae52fc66b2f99597ea5285df82c66f90caa46174fef4930d68a8b6177d0dd","bytes":38747,"lines":916},{"path":"database/migrations/0002_sealed_evidence_digest.sql","sha256":"93d659ca8e0e9293a83d5422d043be7b1022c5470a5b22670aa3416fa334a04c","bytes":6649,"lines":202},{"path":"database/migrations/0003_audit_outbox_persistence.sql","sha256":"2aa7bbb8220923ec584537c0cd46f0cba2b692d69d431f097b7df6db75235bfc","bytes":15417,"lines":423},{"path":"database/migrations/0004_outbox_delivery_claim.sql","sha256":"d4504acf7d58528a2a8f4f03d1584b868c8d3ba9046a007b9c2e7cfef993b2ef","bytes":9451,"lines":234},{"path":"database/migrations/0005_outbox_delivery_finalization.sql","sha256":"b7e8790595b288f752d6ef5cc6cbfe4e1b6712248f5b7a3a25fa60016b6a4961","bytes":6125,"lines":170},{"path":"database/migrations/0006_outbox_delivery_dead_letter.sql","sha256":"c1fb91cdf98169fd6684984e86cb0a14fa19c8f1226028d2346a2a069df2b3c7","bytes":24919,"lines":628},{"path":"database/migrations/0007_outbox_retry_exhaustion.sql","sha256":"812f50d70ca5929c7eba964d34a208aedee660d11cc7ffc09d67688c4737e0d5","bytes":19081,"lines":476},{"path":"database/migrations/0008_audit_outbox_review_hardening.sql","sha256":"c3713a12db9d00fdc10005df1f86c07965e9555eefad78ca67e994537a739d9b","bytes":17562,"lines":448},{"path":"database/migrations/0009_candidate_worker_conversion_governance.sql","sha256":"4030666629a6b8deb383b8337ead4f09d6a945969313def2577a38f31f06cda9","bytes":11537,"lines":281},{"path":"database/migrations/0010_validity_study_case_integrity.sql","sha256":"3f594810ac9e1a6747a2bb4838e5ce65b921cb6e3d36fcdc3ff08b4a7579ebd1","bytes":11979,"lines":313},{"path":"database/migrations/0011_criterion_observation_scope.sql","sha256":"f9fe7c35f1ee7b167e1c2ba75a50a84febda9a6ccf8123b4f5726f51968694f9","bytes":7444,"lines":165},{"path":"database/migrations/0012_people_mutation_idempotency.sql","sha256":"52dbbb9ec7f9be5291593ba88f228d7fffd736dcb99547a08c1d6cad076afb69","bytes":3162,"lines":76},{"path":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"path":"docs/API_CONTRACT.md","sha256":"63533dff785da62b89e585d742a158e2aeb05913644f2bf9fb6486f281c2e589","bytes":4555,"lines":76},{"path":"docs/DATA_MODEL.md","sha256":"6ad29731ae7ee7aa5bf3a2d0bfef88894a35a2550edb2be3244d6f143d76444a","bytes":13366,"lines":85},{"path":"docs/ERD.md","sha256":"546001aa85c4fe020e0c39d881dc860daf7f69090596666fdf9092487b0725fe","bytes":6964,"lines":70},{"path":"docs/OPERABILITY.md","sha256":"82b2d3e70cec371ef35e9e0f982ac40fef84351976bc04b863b81d27023d5a62","bytes":11189,"lines":71},{"path":"docs/PRD.md","sha256":"3ad85ae633cce0fc7a93af39b21d7a7c70bb2efa786da6b12f3c5327906e34f1","bytes":5490,"lines":111},{"path":"docs/SECURITY.md","sha256":"01918512d8882060e9cff0c4aa8206e0eccbdfb61cfd7f829331123c7a9fe6ac","bytes":11185,"lines":64},{"path":"docs/STORYBOARD.md","sha256":"6e4ffb0eb03a80343f50d363ffc43b34da9348a44232dd947a9ff416ea92a3d2","bytes":1342,"lines":28},{"path":"docs/STORYBOOK.md","sha256":"82f79029b3c2b7a45393bad5ba8fabe61014d4b6149c7d4e73f70ba447f885e9","bytes":1389,"lines":50},{"path":"docs/TEST_STRATEGY.md","sha256":"d0a0bc3b54ed0fc7973747987f1afb117d6144c390b51ed9370eb571972a33f8","bytes":16534,"lines":135},{"path":"docs/THREAT_MODEL.md","sha256":"f314f375c2e41252536de224c7bc7e4a10ab8f340cb86642724e7399e32f4252","bytes":6736,"lines":23},{"path":"docs/TRACEABILITY.md","sha256":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"lines":40},{"path":"docs/TRD.md","sha256":"23697d88a4882698e1a2782b7da3f2ccd0d3cd2d6d1bffe89b6597dc16851077","bytes":9064,"lines":101},{"path":"docs/UML.md","sha256":"fe67c37aa88e5814ceb2db7e8f7d8d85ca27a994802efbb7c75164b387adf0a9","bytes":5528,"lines":122},{"path":"docs/USER_STORIES.md","sha256":"5535b39d8c71a36c81f78e2d6dbd90a2d32e6541790f0d28f6dd4baf3ea7b45f","bytes":2670,"lines":37},{"path":"docs/WIREFRAMES.md","sha256":"b03aa6419aeaf5d42a5698c4d43a434c1633b7ac6fd0b0bd0cda979077adc56e","bytes":2005,"lines":77},{"path":"docs/adr/0001-orgmetra-authoritative-hris-record.md","sha256":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"path":"docs/adr/0008-purpose-bound-pii-authorization.md","sha256":"c5157d3bc58f3d8d29e03104dd15eb2911cc1bb66e2c92a935b26d7164648dc7","bytes":5988,"lines":55},{"path":"docs/adr/0009-performance-criterion-observation-scope.md","sha256":"1ac10bb2747b0a5b4d62f627825cfd7f978f3fa88d7575bffc23d56371240a64","bytes":7057,"lines":57},{"path":"docs/adr/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"1656ef8b57c836ef7936a8e9cb6a824681eb7563157a1ab0a29deb25849a457b","bytes":5568,"lines":53},{"path":"docs/adr/0012-governed-migration-handoff.md","sha256":"713855d670001d3964ecb36cc653830502fb1d82a58b9e39f564b6992dd2bd80","bytes":5965,"lines":59},{"path":"docs/adr/0013-governed-requisition-review-packet.md","sha256":"70bf2cbdf903a8793d6d8bc116a08331931090118341f42010236e09c6cc1802","bytes":4693,"lines":46},{"path":"docs/adr/0014-job-analysis-snapshot-persistence.md","sha256":"a7ab6fee50aaa63f7f407516a4cb39885faeb0fc6e5035ee8fc352ed73430105","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"lines":69},{"path":"docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md","sha256":"b64f21abb19373e780db8b9e64deb8ba9a6219ccf9625a651f25407b8691fcbd","bytes":8227,"lines":226},{"path":"docs/superpowers/specs/2026-08-15-orgmetra-foundation-design.md","sha256":"4a0e1a7943e40d12bd3082db3757045b4085e5a089fea7bc0d8a1565ffcbcf1d","bytes":6237,"lines":187},{"path":"package.json","sha256":"59ae9e3e67c3fba9320cb18439692395cdfd16ae5c24e3c4cf30d77d63ebabb5","bytes":388,"lines":9},{"path":"packages/hris-kernel/src/orgmetra_hris_kernel/audit.py","sha256":"3e5b7190cf857dc8c1fc7e898cef303060f34aabee6c27a9034d4d9650e33190","bytes":7707,"lines":160},{"path":"packages/hris-kernel/tests/test_audit_outbox.py","sha256":"5928dd7b97fe38d6b7472ce62966437e339058a59c3b301a93a7b5c05432b40c","bytes":7556,"lines":200},{"path":"schemas/openapi.yaml","sha256":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"path":"scripts/foundation-contract.mjs","sha256":"5242dcdbe0935775edf074462c82600e9bc4927d9fdc50c47727af915fd4b23a","bytes":218,"lines":6},{"path":"tests/dispatcher-inventory.test.mjs","sha256":"09f5e64410e6b7a26bf8d6ce61c50b737da2ea85d955f91eba63aa21f1537261","bytes":1597,"lines":34},{"path":"tests/foundation-contract.test.mjs","sha256":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"path":"tests/openapi-contract.test.mjs","sha256":"80c1610ef1c189fa325e55389501e0e51531ddf61ee335bb94d9cb3aa55a9fdc","bytes":6438,"lines":195},{"path":"tests/test_audit_outbox_hardening_postgres.sh","sha256":"518ba2f37ba6292943e5abe22c2599452b2f031a42e453b2493aedf8714421a0","bytes":13396,"lines":333},{"path":"tests/test_audit_outbox_postgres.sh","sha256":"e57a04920a0ba97fa6a06752d15ea150016ab8d44099e998c5c4f4067592b4d2","bytes":13443,"lines":357},{"path":"tests/test_bitemporal_postgres.sh","sha256":"7684b8c2ff52c044c081135515bd5aabbfd00e2daad0d471b0868701af2df6cc","bytes":8209,"lines":230},{"path":"tests/test_candidate_worker_conversion_postgres.sh","sha256":"681cb74d6cfa859ed92c6c2439881ea20c430ef8df94ec662e2807761a377f90","bytes":14673,"lines":344},{"path":"tests/test_criterion_observation_scope_postgres.sh","sha256":"0ee9539ee57f840c27d08009f7868cdc8662669df78a01dbc8be39216b8f1a3d","bytes":17811,"lines":469},{"path":"tests/test_evidence_sealing_postgres.sh","sha256":"57d16b632a0c60ffdcb4842ceb1cfe25d19c54cefeeefb622ff4fa6e83441ad7","bytes":11349,"lines":370},{"path":"tests/test_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"path":"tests/test_operational_uuid_postgres.sh","sha256":"7378f98f0d4b3000e8ea641d8701f1540dbad71410b3637d81d799969e0f6ff7","bytes":3346,"lines":101},{"path":"tests/test_outbox_claim_postgres.sh","sha256":"1027806d436ebfe34e108c25b6a4001f43b9550f1d70057c6c0d7974323b0c9b","bytes":14817,"lines":429},{"path":"tests/test_outbox_dead_letter_postgres.sh","sha256":"0d728d578e64252e6079f2d141ddaa7fa9cfbf9784e625832273596d69a6e13d","bytes":14008,"lines":377},{"path":"tests/test_people_mutation_idempotency_postgres.sh","sha256":"3f57e12f80bd1b034c9aac54b669d8530106e3e26b3795689671fb53807b3cd5","bytes":16191,"lines":381},{"path":"tests/test_tenant_isolation_postgres.sh","sha256":"dd649435ef8ab9e57f0609c101917e36656a6d40d63de9bcdbdac23d764f6c3a","bytes":15134,"lines":388},{"path":"tests/test_validity_study_case_postgres.sh","sha256":"0070ad58300323c7f9900c5645e0df3106b36ccd245ae686e982c2fd6fa4dc02","bytes":14708,"lines":301},{"path":"tests/validate_repository.py","sha256":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} +{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"057d51eaff58b586f399ed4264d4f15ef0b63b0d70a578c2f0e8e4aa28b17bd1","bytes":18029,"lines":76},{"path":"CLAUDE.md","sha256":"add33884f466d324e20875388d103de41c6e062938a6e98727dc83a87ffe976f","bytes":1229,"lines":20},{"path":"LICENSE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30","bytes":11358,"lines":202},{"path":"NOTICE","sha256":"34b4618e946bdd8d33407d6ac5279f0a0388f5e7c8f79d2e7d8c3c47d0266042","bytes":305,"lines":4},{"path":"README.md","sha256":"1a9fc400d26d8137ae5911488794a6d3fa915957c95f27b36a48cef0fdf823c6","bytes":3785,"lines":81},{"path":"database/migrations/0001_foundation_schema.sql","sha256":"ce2ae52fc66b2f99597ea5285df82c66f90caa46174fef4930d68a8b6177d0dd","bytes":38747,"lines":916},{"path":"database/migrations/0002_sealed_evidence_digest.sql","sha256":"93d659ca8e0e9293a83d5422d043be7b1022c5470a5b22670aa3416fa334a04c","bytes":6649,"lines":202},{"path":"database/migrations/0003_audit_outbox_persistence.sql","sha256":"2aa7bbb8220923ec584537c0cd46f0cba2b692d69d431f097b7df6db75235bfc","bytes":15417,"lines":423},{"path":"database/migrations/0004_outbox_delivery_claim.sql","sha256":"d4504acf7d58528a2a8f4f03d1584b868c8d3ba9046a007b9c2e7cfef993b2ef","bytes":9451,"lines":234},{"path":"database/migrations/0005_outbox_delivery_finalization.sql","sha256":"b7e8790595b288f752d6ef5cc6cbfe4e1b6712248f5b7a3a25fa60016b6a4961","bytes":6125,"lines":170},{"path":"database/migrations/0006_outbox_delivery_dead_letter.sql","sha256":"c1fb91cdf98169fd6684984e86cb0a14fa19c8f1226028d2346a2a069df2b3c7","bytes":24919,"lines":628},{"path":"database/migrations/0007_outbox_retry_exhaustion.sql","sha256":"812f50d70ca5929c7eba964d34a208aedee660d11cc7ffc09d67688c4737e0d5","bytes":19081,"lines":476},{"path":"database/migrations/0008_audit_outbox_review_hardening.sql","sha256":"c3713a12db9d00fdc10005df1f86c07965e9555eefad78ca67e994537a739d9b","bytes":17562,"lines":448},{"path":"database/migrations/0009_candidate_worker_conversion_governance.sql","sha256":"4030666629a6b8deb383b8337ead4f09d6a945969313def2577a38f31f06cda9","bytes":11537,"lines":281},{"path":"database/migrations/0010_validity_study_case_integrity.sql","sha256":"3f594810ac9e1a6747a2bb4838e5ce65b921cb6e3d36fcdc3ff08b4a7579ebd1","bytes":11979,"lines":313},{"path":"database/migrations/0011_criterion_observation_scope.sql","sha256":"f9fe7c35f1ee7b167e1c2ba75a50a84febda9a6ccf8123b4f5726f51968694f9","bytes":7444,"lines":165},{"path":"database/migrations/0012_people_mutation_idempotency.sql","sha256":"52dbbb9ec7f9be5291593ba88f228d7fffd736dcb99547a08c1d6cad076afb69","bytes":3162,"lines":76},{"path":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"path":"database/migrations/0014_criterion_observation_chronology.sql","sha256":"5f24b65cfdf49d66c1c3c98409faf8d8acdc15a2800c3c115bc25b18ce9da158","bytes":7792,"lines":173},{"path":"database/migrations/0015_criterion_observation_statement_default.sql","sha256":"0a524dbee2cd53aa7111423d7f026ad3ccc4a79501c04397ee28f36276d9e9ac","bytes":250,"lines":5},{"path":"docs/API_CONTRACT.md","sha256":"63533dff785da62b89e585d742a158e2aeb05913644f2bf9fb6486f281c2e589","bytes":4555,"lines":76},{"path":"docs/DATA_MODEL.md","sha256":"6ad29731ae7ee7aa5bf3a2d0bfef88894a35a2550edb2be3244d6f143d76444a","bytes":13366,"lines":85},{"path":"docs/ERD.md","sha256":"546001aa85c4fe020e0c39d881dc860daf7f69090596666fdf9092487b0725fe","bytes":6964,"lines":70},{"path":"docs/OPERABILITY.md","sha256":"82b2d3e70cec371ef35e9e0f982ac40fef84351976bc04b863b81d27023d5a62","bytes":11189,"lines":71},{"path":"docs/PRD.md","sha256":"3ad85ae633cce0fc7a93af39b21d7a7c70bb2efa786da6b12f3c5327906e34f1","bytes":5490,"lines":111},{"path":"docs/SECURITY.md","sha256":"01918512d8882060e9cff0c4aa8206e0eccbdfb61cfd7f829331123c7a9fe6ac","bytes":11185,"lines":64},{"path":"docs/STORYBOARD.md","sha256":"6e4ffb0eb03a80343f50d363ffc43b34da9348a44232dd947a9ff416ea92a3d2","bytes":1342,"lines":28},{"path":"docs/STORYBOOK.md","sha256":"82f79029b3c2b7a45393bad5ba8fabe61014d4b6149c7d4e73f70ba447f885e9","bytes":1389,"lines":50},{"path":"docs/TEST_STRATEGY.md","sha256":"d0a0bc3b54ed0fc7973747987f1afb117d6144c390b51ed9370eb571972a33f8","bytes":16534,"lines":135},{"path":"docs/THREAT_MODEL.md","sha256":"f314f375c2e41252536de224c7bc7e4a10ab8f340cb86642724e7399e32f4252","bytes":6736,"lines":23},{"path":"docs/TRACEABILITY.md","sha256":"857f459e3bc27dfc0806986227901992e7a5bf3575b07e6ccd4028ee3e455d0a","bytes":11483,"lines":40},{"path":"docs/TRD.md","sha256":"23697d88a4882698e1a2782b7da3f2ccd0d3cd2d6d1bffe89b6597dc16851077","bytes":9064,"lines":101},{"path":"docs/UML.md","sha256":"fe67c37aa88e5814ceb2db7e8f7d8d85ca27a994802efbb7c75164b387adf0a9","bytes":5528,"lines":122},{"path":"docs/USER_STORIES.md","sha256":"5535b39d8c71a36c81f78e2d6dbd90a2d32e6541790f0d28f6dd4baf3ea7b45f","bytes":2670,"lines":37},{"path":"docs/WIREFRAMES.md","sha256":"b03aa6419aeaf5d42a5698c4d43a434c1633b7ac6fd0b0bd0cda979077adc56e","bytes":2005,"lines":77},{"path":"docs/adr/0001-orgmetra-authoritative-hris-record.md","sha256":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"path":"docs/adr/0008-purpose-bound-pii-authorization.md","sha256":"c5157d3bc58f3d8d29e03104dd15eb2911cc1bb66e2c92a935b26d7164648dc7","bytes":5988,"lines":55},{"path":"docs/adr/0009-performance-criterion-observation-scope.md","sha256":"be0263309d4d0736380f4851db793dd0b4169311a9faf763ab3afb5017373119","bytes":8273,"lines":60},{"path":"docs/adr/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"1656ef8b57c836ef7936a8e9cb6a824681eb7563157a1ab0a29deb25849a457b","bytes":5568,"lines":53},{"path":"docs/adr/0012-governed-migration-handoff.md","sha256":"713855d670001d3964ecb36cc653830502fb1d82a58b9e39f564b6992dd2bd80","bytes":5965,"lines":59},{"path":"docs/adr/0013-governed-requisition-review-packet.md","sha256":"70bf2cbdf903a8793d6d8bc116a08331931090118341f42010236e09c6cc1802","bytes":4693,"lines":46},{"path":"docs/adr/0014-job-analysis-snapshot-persistence.md","sha256":"a7ab6fee50aaa63f7f407516a4cb39885faeb0fc6e5035ee8fc352ed73430105","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"lines":69},{"path":"docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md","sha256":"b64f21abb19373e780db8b9e64deb8ba9a6219ccf9625a651f25407b8691fcbd","bytes":8227,"lines":226},{"path":"docs/superpowers/specs/2026-08-15-orgmetra-foundation-design.md","sha256":"4a0e1a7943e40d12bd3082db3757045b4085e5a089fea7bc0d8a1565ffcbcf1d","bytes":6237,"lines":187},{"path":"package.json","sha256":"59ae9e3e67c3fba9320cb18439692395cdfd16ae5c24e3c4cf30d77d63ebabb5","bytes":388,"lines":9},{"path":"packages/hris-kernel/src/orgmetra_hris_kernel/audit.py","sha256":"3e5b7190cf857dc8c1fc7e898cef303060f34aabee6c27a9034d4d9650e33190","bytes":7707,"lines":160},{"path":"packages/hris-kernel/tests/test_audit_outbox.py","sha256":"5928dd7b97fe38d6b7472ce62966437e339058a59c3b301a93a7b5c05432b40c","bytes":7556,"lines":200},{"path":"schemas/openapi.yaml","sha256":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"68b3d2d43ca86f186925bd43b4f1d945a7bb9e148e9567884f0bcbb7d70af177","bytes":28314,"lines":691},{"path":"scripts/foundation-contract.mjs","sha256":"5242dcdbe0935775edf074462c82600e9bc4927d9fdc50c47727af915fd4b23a","bytes":218,"lines":6},{"path":"tests/dispatcher-inventory.test.mjs","sha256":"09f5e64410e6b7a26bf8d6ce61c50b737da2ea85d955f91eba63aa21f1537261","bytes":1597,"lines":34},{"path":"tests/foundation-contract.test.mjs","sha256":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"path":"tests/openapi-contract.test.mjs","sha256":"80c1610ef1c189fa325e55389501e0e51531ddf61ee335bb94d9cb3aa55a9fdc","bytes":6438,"lines":195},{"path":"tests/test_audit_outbox_hardening_postgres.sh","sha256":"518ba2f37ba6292943e5abe22c2599452b2f031a42e453b2493aedf8714421a0","bytes":13396,"lines":333},{"path":"tests/test_audit_outbox_postgres.sh","sha256":"e57a04920a0ba97fa6a06752d15ea150016ab8d44099e998c5c4f4067592b4d2","bytes":13443,"lines":357},{"path":"tests/test_bitemporal_postgres.sh","sha256":"7684b8c2ff52c044c081135515bd5aabbfd00e2daad0d471b0868701af2df6cc","bytes":8209,"lines":230},{"path":"tests/test_candidate_worker_conversion_postgres.sh","sha256":"681cb74d6cfa859ed92c6c2439881ea20c430ef8df94ec662e2807761a377f90","bytes":14673,"lines":344},{"path":"tests/test_criterion_observation_scope_postgres.sh","sha256":"dfc5030a2c5c9c7e78bf2ce10f9e893fe4aaa13bada33fcc5ad144bc643f4ea6","bytes":21920,"lines":575},{"path":"tests/test_evidence_sealing_postgres.sh","sha256":"57d16b632a0c60ffdcb4842ceb1cfe25d19c54cefeeefb622ff4fa6e83441ad7","bytes":11349,"lines":370},{"path":"tests/test_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"path":"tests/test_operational_uuid_postgres.sh","sha256":"7378f98f0d4b3000e8ea641d8701f1540dbad71410b3637d81d799969e0f6ff7","bytes":3346,"lines":101},{"path":"tests/test_outbox_claim_postgres.sh","sha256":"1027806d436ebfe34e108c25b6a4001f43b9550f1d70057c6c0d7974323b0c9b","bytes":14817,"lines":429},{"path":"tests/test_outbox_dead_letter_postgres.sh","sha256":"0d728d578e64252e6079f2d141ddaa7fa9cfbf9784e625832273596d69a6e13d","bytes":14008,"lines":377},{"path":"tests/test_people_mutation_idempotency_postgres.sh","sha256":"3f57e12f80bd1b034c9aac54b669d8530106e3e26b3795689671fb53807b3cd5","bytes":16191,"lines":381},{"path":"tests/test_tenant_isolation_postgres.sh","sha256":"dd649435ef8ab9e57f0609c101917e36656a6d40d63de9bcdbdac23d764f6c3a","bytes":15134,"lines":388},{"path":"tests/test_validity_study_case_postgres.sh","sha256":"0070ad58300323c7f9900c5645e0df3106b36ccd245ae686e982c2fd6fa4dc02","bytes":14708,"lines":301},{"path":"tests/validate_repository.py","sha256":"6cf97dd9b5a64e206370190d59bb1f75180cd3f63ef927c903987a945d6ca936","bytes":27436,"lines":640}]} diff --git a/scripts/foundation-contract-core.mjs b/scripts/foundation-contract-core.mjs index 1e9fb267c..4ebe0b4f7 100644 --- a/scripts/foundation-contract-core.mjs +++ b/scripts/foundation-contract-core.mjs @@ -68,6 +68,8 @@ export const REQUIRED_FILES = Object.freeze([ 'database/migrations/0011_criterion_observation_scope.sql', 'database/migrations/0012_people_mutation_idempotency.sql', 'database/migrations/0013_job_analysis_snapshot.sql', + 'database/migrations/0014_criterion_observation_chronology.sql', + 'database/migrations/0015_criterion_observation_statement_default.sql', 'packages/hris-kernel/src/orgmetra_hris_kernel/audit.py', 'packages/hris-kernel/tests/test_audit_outbox.py', 'schemas/openapi.yaml', diff --git a/tests/test_criterion_observation_scope_postgres.sh b/tests/test_criterion_observation_scope_postgres.sh index 397dfc112..825cb4c9c 100644 --- a/tests/test_criterion_observation_scope_postgres.sh +++ b/tests/test_criterion_observation_scope_postgres.sh @@ -3,6 +3,8 @@ set -euo pipefail : "${DATABASE_URL:=postgresql://orgmetra:orgmetra@localhost:5432/orgmetra}" +# Apply the protected-base trigger version, then sequential upgrades that +# preserve the existing trigger binding and its recorded-time default contract. for migration in \ database/migrations/0001_foundation_schema.sql \ database/migrations/0002_sealed_evidence_digest.sql \ @@ -13,10 +15,25 @@ for migration in \ database/migrations/0007_outbox_retry_exhaustion.sql \ database/migrations/0008_audit_outbox_review_hardening.sql \ database/migrations/0009_candidate_worker_conversion_governance.sql \ - database/migrations/0011_criterion_observation_scope.sql; do + database/migrations/0010_validity_study_case_integrity.sql \ + database/migrations/0011_criterion_observation_scope.sql \ + database/migrations/0012_people_mutation_idempotency.sql \ + database/migrations/0013_job_analysis_snapshot.sql \ + database/migrations/0014_criterion_observation_chronology.sql \ + database/migrations/0015_criterion_observation_statement_default.sql; do psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f "${migration}" done +trigger_definition="$(psql "${DATABASE_URL}" -Atq -c " + SELECT pg_catalog.pg_get_triggerdef(oid) + FROM pg_catalog.pg_trigger + WHERE tgname = 'criterion_observation_scope_guard' +")" +if [[ "${trigger_definition}" != *"enforce_criterion_observation_scope"* ]]; then + echo "criterion chronology upgrade replaced the trigger binding unexpectedly" >&2 + exit 1 +fi + TENANT_ID="10000000-0000-7000-8000-000000000101" tenant_psql() { PGOPTIONS="-c orgmetra.tenant_record_id=${TENANT_ID}" command psql "$@" @@ -335,6 +352,64 @@ if [[ "${terminated_employment_output}" != *"criterion observation lacks an assi exit 1 fi +set +e +future_observation_output="$({ tenant_psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 <<'SQL' +INSERT INTO criterion_observation ( + tenant_record_id, criterion_observation_id, criterion_blueprint_id, + performance_cycle_id, person_record_id, observed_value, + observed_at, recorded_from +) VALUES ( + '10000000-0000-7000-8000-000000000101', + '10000000-0000-7000-8000-000000000134', + '10000000-0000-7000-8000-000000000110', + '10000000-0000-7000-8000-000000000109', + '10000000-0000-7000-8000-000000000102', + 4.0, + statement_timestamp() + INTERVAL '1 day', + statement_timestamp() + INTERVAL '2 days' +); +SQL +} 2>&1)" +future_observation_status=$? +set -e +if [[ ${future_observation_status} -eq 0 ]]; then + echo "criterion observation accepted timestamps that were both future-dated" >&2 + exit 1 +fi +if [[ "${future_observation_output}" != *"criterion observation cannot be observed in the future"* ]]; then + echo "future-dated criterion observation failed for an unexpected reason: ${future_observation_output}" >&2 + exit 1 +fi + +set +e +future_recording_output="$({ tenant_psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 <<'SQL' +INSERT INTO criterion_observation ( + tenant_record_id, criterion_observation_id, criterion_blueprint_id, + performance_cycle_id, person_record_id, observed_value, + observed_at, recorded_from +) VALUES ( + '10000000-0000-7000-8000-000000000101', + '10000000-0000-7000-8000-000000000135', + '10000000-0000-7000-8000-000000000110', + '10000000-0000-7000-8000-000000000109', + '10000000-0000-7000-8000-000000000102', + 4.0, + TIMESTAMPTZ '2026-08-15 12:00:00+00', + statement_timestamp() + INTERVAL '2 days' +); +SQL +} 2>&1)" +future_recording_status=$? +set -e +if [[ ${future_recording_status} -eq 0 ]]; then + echo "criterion observation accepted a future-dated recorded_from" >&2 + exit 1 +fi +if [[ "${future_recording_output}" != *"criterion observation cannot be recorded in the future"* ]]; then + echo "future-dated recording failed for an unexpected reason: ${future_recording_output}" >&2 + exit 1 +fi + tenant_psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 <<'SQL' INSERT INTO criterion_observation ( tenant_record_id, criterion_observation_id, criterion_blueprint_id, @@ -363,6 +438,37 @@ if [[ "${valid_count}" != "1" ]]; then exit 1 fi +# The transaction begins before the observation is made. Omitting recorded_from +# must still use the insert statement's time, not the older transaction time. +tenant_psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 <<'SQL' +BEGIN; +SELECT pg_sleep(0.1); +INSERT INTO criterion_observation ( + tenant_record_id, criterion_observation_id, criterion_blueprint_id, + performance_cycle_id, person_record_id, observed_value, observed_at +) VALUES ( + '10000000-0000-7000-8000-000000000101', + '10000000-0000-7000-8000-000000000136', + '10000000-0000-7000-8000-000000000110', + '10000000-0000-7000-8000-000000000109', + '10000000-0000-7000-8000-000000000102', 4.9, + clock_timestamp() - INTERVAL '1 second' +); +COMMIT; +SQL + +long_transaction_count="$(tenant_psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " +SELECT count(*) +FROM criterion_observation +WHERE tenant_record_id = '${TENANT_ID}'::uuid + AND criterion_observation_id = '10000000-0000-7000-8000-000000000136'::uuid + AND recorded_from >= observed_at; +")" +if [[ "${long_transaction_count}" != "1" ]]; then + echo "long-transaction criterion observation did not use a statement-time default" >&2 + exit 1 +fi + # Fixture rows above stay current-recorded (recorded_to IS NULL). Each closure # below happens inside an aborting transaction so removing a recorded_to # predicate from the trigger would admit the observation and fail this contract. diff --git a/tests/validate_repository.py b/tests/validate_repository.py index fe0a329ff..1a68f98ca 100644 --- a/tests/validate_repository.py +++ b/tests/validate_repository.py @@ -71,6 +71,8 @@ "database/migrations/0011_criterion_observation_scope.sql", "database/migrations/0012_people_mutation_idempotency.sql", "database/migrations/0013_job_analysis_snapshot.sql", + "database/migrations/0014_criterion_observation_chronology.sql", + "database/migrations/0015_criterion_observation_statement_default.sql", "packages/hris-kernel/src/orgmetra_hris_kernel/audit.py", "packages/hris-kernel/tests/test_audit_outbox.py", "schemas/openapi.yaml",