From 8354e5a2afaa7e0bbfa2d37e66b0e02ca3e22232 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 07:40:36 -0700 Subject: [PATCH 01/23] test(position): define lifecycle application persistence contract --- ...position_lifecycle_application_postgres.sh | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 tests/test_position_lifecycle_application_postgres.sh diff --git a/tests/test_position_lifecycle_application_postgres.sh b/tests/test_position_lifecycle_application_postgres.sh new file mode 100644 index 000000000..f27ec8f59 --- /dev/null +++ b/tests/test_position_lifecycle_application_postgres.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${DATABASE_URL:?DATABASE_URL is required}" + +migration="database/migrations/0023_position_lifecycle_application.sql" +test -f "$migration" + +psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f database/migrations/0001_foundation_schema.sql >/dev/null +psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f database/migrations/0002_sealed_evidence_digest.sql >/dev/null +psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f database/migrations/0003_audit_outbox_persistence.sql >/dev/null +psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f "$migration" >/dev/null + +TENANT="0198a412-8000-7000-8000-000000000101" +ORG="0198a412-8000-7000-8000-000000000102" +JOB="0198a412-8000-7000-8000-000000000103" +POSITION="0198a412-8000-7000-8000-000000000104" +CURRENT_VERSION="0198a412-8000-7000-8000-000000000105" +SUCCESSOR="0198a412-8000-7000-8000-000000000106" +APPLICATION="0198a412-8000-7000-8000-000000000107" +AUDIT="0198a412-8000-7000-8000-000000000108" +OUTBOX="0198a412-8000-7000-8000-000000000109" +REVIEW_REF="a0e89c71-41c1-4c63-935c-8569d83f7901" +REQUESTER="actor:9d5a177e-79e9-4022-8f35-b2408ec5a503" +REVIEWER="actor:6aacb560-ec5d-41d7-94a5-27cf95438b1b" +APPLIER="actor:faefac04-52e8-43d2-aa54-d9046238733f" +POSITION_DIGEST="$(printf 'position-snapshot' | sha256sum | awk '{print $1}')" +ASSIGNMENT_DIGEST="$(printf 'assignment-snapshot' | sha256sum | awk '{print $1}')" + +psql "$DATABASE_URL" -v ON_ERROR_STOP=1 \ + -v tenant="$TENANT" -v org="$ORG" -v job="$JOB" -v position="$POSITION" -v current_version="$CURRENT_VERSION" <<'SQL' >/dev/null +INSERT INTO tenant_record (tenant_record_id, tenant_name) VALUES (:'tenant', 'Lifecycle Test'); +INSERT INTO organization_unit ( + tenant_record_id, organization_unit_id, organization_unit_name, effective_from +) VALUES (:'tenant', :'org', 'Lifecycle Org', DATE '2026-01-01'); +INSERT INTO job_profile (tenant_record_id, job_profile_id) VALUES (:'tenant', :'job'); +INSERT INTO position_record ( + tenant_record_id, position_record_id, organization_unit_id, job_profile_id +) VALUES (:'tenant', :'position', :'org', :'job'); +INSERT INTO position_record_version ( + tenant_record_id, position_record_version_id, position_record_id, + position_status_code, effective_from +) VALUES (:'tenant', :'current_version', :'position', 'open', DATE '2026-01-01'); +SQL + +make_review() { + local outcome="$1" + local current="$2" + local proposed="$3" + local reason="$4" + psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 \ + -v tenant="$TENANT" -v position="$POSITION" -v review_ref="$REVIEW_REF" \ + -v requester="$REQUESTER" -v reviewer="$REVIEWER" \ + -v position_digest="$POSITION_DIGEST" -v assignment_digest="$ASSIGNMENT_DIGEST" \ + -v outcome="$outcome" -v current="$current" -v proposed="$proposed" -v reason="$reason" <<'SQL' +SELECT jsonb_build_object( + 'assignment_snapshot_digest_sha256', :'assignment_digest', + 'current_status_code', :'current', + 'decision_authority', 'human_review_only', + 'effective_on', '2026-09-01', + 'evidence_version', 1, + 'mutation_state', 'not_authorized_to_apply', + 'next_action', CASE WHEN :'outcome' = 'rejected' + THEN 'Do not apply the proposed Position lifecycle change.' + ELSE 'Re-resolve tenant-qualified Position and Assignment truth at the requested business/system coordinate; require authoritative actor separation, reviewed evidence, staffing safety, and immutable audit/outbox before any lifecycle mutation.' END, + 'position_lifecycle_change_reference', :'review_ref', + 'position_record_id', :'position', + 'position_snapshot_digest_sha256', :'position_digest', + 'proposed_status_code', :'proposed', + 'reason_code', :'reason', + 'recorded_at', '2026-08-24T14:00:00Z', + 'requester_actor_reference', :'requester', + 'review_outcome_code', :'outcome', + 'review_state', 'human_reviewed', + 'reviewed_at', '2026-08-24T13:55:00Z', + 'reviewer_actor_reference', :'reviewer', + 'scope_verification_state', 'requires_authoritative_resolution', + 'tenant_record_id', :'tenant' +)::text; +SQL +} + +REVIEW_JSON="$(make_review approved_for_authoritative_resolution open frozen temporary_freeze)" +REVIEW_DIGEST="$(printf '%s' "$REVIEW_JSON" | sha256sum | awk '{print $1}')" + +psql "$DATABASE_URL" -v ON_ERROR_STOP=1 \ + -v tenant="$TENANT" -v position="$POSITION" -v current_version="$CURRENT_VERSION" \ + -v successor="$SUCCESSOR" -v application="$APPLICATION" \ + -v review_json="$REVIEW_JSON" -v review_digest="$REVIEW_DIGEST" \ + -v applier="$APPLIER" -v audit="$AUDIT" -v outbox="$OUTBOX" <<'SQL' >/dev/null +SELECT public.apply_position_lifecycle_change( + :'tenant', :'position', :'current_version', :'successor', :'application', + :'review_json', :'review_digest', :'applier', :'audit', :'outbox' +); +SQL + +status_rows="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 -v tenant="$TENANT" -v position="$POSITION" <<'SQL' +SELECT string_agg(position_status_code || ':' || effective_from::text || ':' || coalesce(effective_to::text, 'infinity'), ',' ORDER BY effective_from) +FROM position_record_version +WHERE tenant_record_id = :'tenant' + AND position_record_id = :'position' + AND recorded_to IS NULL; +SQL +)" +test "$status_rows" = "open:2026-01-01:2026-09-01,frozen:2026-09-01:infinity" + +application_count="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 -v tenant="$TENANT" -v application="$APPLICATION" <<'SQL' +SELECT count(*) FROM position_lifecycle_application_record +WHERE tenant_record_id = :'tenant' + AND position_lifecycle_application_record_id = :'application' + AND application_state = 'applied_after_human_review'; +SQL +)" +test "$application_count" = "1" + +# Rejected review evidence must not mutate Position truth. +REJECTED_REVIEW="$(make_review rejected frozen closed position_closure)" +REJECTED_DIGEST="$(printf '%s' "$REJECTED_REVIEW" | sha256sum | awk '{print $1}')" +if psql "$DATABASE_URL" -v ON_ERROR_STOP=1 \ + -v tenant="$TENANT" -v position="$POSITION" -v current_version="$SUCCESSOR" \ + -v successor="0198a412-8000-7000-8000-00000000010a" \ + -v application="0198a412-8000-7000-8000-00000000010b" \ + -v review_json="$REJECTED_REVIEW" -v review_digest="$REJECTED_DIGEST" \ + -v applier="$APPLIER" -v audit="0198a412-8000-7000-8000-00000000010c" \ + -v outbox="0198a412-8000-7000-8000-00000000010d" <<'SQL' >/dev/null 2>&1 +SELECT public.apply_position_lifecycle_change( + :'tenant', :'position', :'current_version', :'successor', :'application', + :'review_json', :'review_digest', :'applier', :'audit', :'outbox' +); +SQL +then + echo "rejected lifecycle review was applied" >&2 + exit 1 +fi + +# Closing/abolishing a seat with a live assignment crossing the effective date must fail. +EMPLOYMENT="0198a412-8000-7000-8000-00000000010e" +PERSON="0198a412-8000-7000-8000-00000000010f" +ASSIGNMENT="0198a412-8000-7000-8000-000000000110" +psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -v tenant="$TENANT" -v person="$PERSON" -v employment="$EMPLOYMENT" -v assignment="$ASSIGNMENT" -v position="$POSITION" <<'SQL' >/dev/null +INSERT INTO person_record (tenant_record_id, person_record_id) VALUES (:'tenant', :'person'); +INSERT INTO employment_record (tenant_record_id, employment_record_id, person_record_id) VALUES (:'tenant', :'employment', :'person'); +INSERT INTO assignment_record ( + tenant_record_id, assignment_record_id, employment_record_id, person_record_id, + position_record_id, allocation_ratio, effective_from +) VALUES (:'tenant', :'assignment', :'employment', :'person', :'position', 1.0000, DATE '2026-08-01'); +SQL + +CLOSE_REVIEW="$(make_review approved_for_authoritative_resolution frozen closed position_closure)" +CLOSE_DIGEST="$(printf '%s' "$CLOSE_REVIEW" | sha256sum | awk '{print $1}')" +if psql "$DATABASE_URL" -v ON_ERROR_STOP=1 \ + -v tenant="$TENANT" -v position="$POSITION" -v current_version="$SUCCESSOR" \ + -v successor="0198a412-8000-7000-8000-000000000111" \ + -v application="0198a412-8000-7000-8000-000000000112" \ + -v review_json="$CLOSE_REVIEW" -v review_digest="$CLOSE_DIGEST" \ + -v applier="$APPLIER" -v audit="0198a412-8000-7000-8000-000000000113" \ + -v outbox="0198a412-8000-7000-8000-000000000114" <<'SQL' >/dev/null 2>&1 +SELECT public.apply_position_lifecycle_change( + :'tenant', :'position', :'current_version', :'successor', :'application', + :'review_json', :'review_digest', :'applier', :'audit', :'outbox' +); +SQL +then + echo "position close was applied across a live Assignment" >&2 + exit 1 +fi + +# Direct history rewrite/delete must remain blocked. +if psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -v tenant="$TENANT" -v position="$POSITION" <<'SQL' >/dev/null 2>&1 +UPDATE position_record_version +SET position_status_code = 'closed' +WHERE tenant_record_id = :'tenant' AND position_record_id = :'position' AND recorded_to IS NULL; +SQL +then + echo "position version history was rewritten directly" >&2 + exit 1 +fi + +if psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -v tenant="$TENANT" -v application="$APPLICATION" <<'SQL' >/dev/null 2>&1 +DELETE FROM position_lifecycle_application_record +WHERE tenant_record_id = :'tenant' AND position_lifecycle_application_record_id = :'application'; +SQL +then + echo "position lifecycle application evidence was deleted" >&2 + exit 1 +fi + +# Both owned relations must be forced-RLS. +force_rls="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 <<'SQL' +SELECT string_agg(relname || ':' || relforcerowsecurity::text, ',' ORDER BY relname) +FROM pg_class +WHERE relname IN ('position_record_version', 'position_lifecycle_application_record'); +SQL +)" +test "$force_rls" = "position_lifecycle_application_record:true,position_record_version:true" + +echo "position lifecycle application persistence: PASS" From abe1c478b2b9e66bcbe8bc92f05b7ce38ca64d22 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 07:40:51 -0700 Subject: [PATCH 02/23] test(position): add lifecycle application quality gate --- ...position-lifecycle-application-quality.yml | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 .github/workflows/position-lifecycle-application-quality.yml diff --git a/.github/workflows/position-lifecycle-application-quality.yml b/.github/workflows/position-lifecycle-application-quality.yml new file mode 100644 index 000000000..d87bbb492 --- /dev/null +++ b/.github/workflows/position-lifecycle-application-quality.yml @@ -0,0 +1,88 @@ +name: Position Lifecycle Application Quality + +on: + pull_request: + branches: + - develop + - feat/position-lifecycle-review + paths: + - "database/migrations/0023_position_lifecycle_application.sql" + - "tests/test_position_lifecycle_application_postgres.sh" + - "docs/adr/0112-position-lifecycle-application.md" + - "docs/traceability/position-lifecycle-application.md" + - "docs/doctoring/position-lifecycle-application-references.md" + - ".github/workflows/position-lifecycle-application-quality.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: position-lifecycle-application-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + postgres_contract: + name: Governed Position lifecycle application contract + runs-on: ubuntu-latest + timeout-minutes: 10 + services: + postgres: + image: postgres:16.14@sha256:33f923b05f64ca54ac4401c01126a6b92afe839a0aa0a52bc5aeb5cc958e5f20 + env: + POSTGRES_USER: orgmetra + POSTGRES_PASSWORD: orgmetra + POSTGRES_DB: orgmetra + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U orgmetra -d orgmetra" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + env: + DATABASE_URL: postgresql://orgmetra:orgmetra@localhost:5432/orgmetra + steps: + - name: Checkout exact candidate + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + - name: Prove exact candidate checkout + env: + ORGMETRA_EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: test "$(git rev-parse HEAD)" = "$ORGMETRA_EXPECTED_HEAD_SHA" + - name: Print deterministic lifecycle application provenance + run: | + python - <<'PY' + import hashlib + import json + from pathlib import Path + + required = [ + "tests/test_position_lifecycle_application_postgres.sh", + ".github/workflows/position-lifecycle-application-quality.yml", + ] + optional = [ + "database/migrations/0023_position_lifecycle_application.sql", + "docs/adr/0112-position-lifecycle-application.md", + "docs/traceability/position-lifecycle-application.md", + "docs/doctoring/position-lifecycle-application-references.md", + ] + rows = [] + for path_text in required + [path for path in optional if Path(path).is_file()]: + data = Path(path_text).read_bytes() + rows.append({ + "path": path_text, + "sha256": hashlib.sha256(data).hexdigest(), + "bytes": len(data), + "lines": len(data.decode("utf-8").splitlines()), + }) + print(json.dumps(rows, separators=(",", ":"))) + PY + - name: Run Position lifecycle application regressions + run: bash tests/test_position_lifecycle_application_postgres.sh + - name: Require clean checkout + run: | + git diff --exit-code + test -z "$(git status --porcelain)" From e3e91e2855ad9853ea1fd5fc95026807d0fda787 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 07:43:43 -0700 Subject: [PATCH 03/23] feat(position): persist reviewed lifecycle changes --- .../0023_position_lifecycle_application.sql | 600 ++++++++++++++++++ 1 file changed, 600 insertions(+) create mode 100644 database/migrations/0023_position_lifecycle_application.sql diff --git a/database/migrations/0023_position_lifecycle_application.sql b/database/migrations/0023_position_lifecycle_application.sql new file mode 100644 index 000000000..c8ad15669 --- /dev/null +++ b/database/migrations/0023_position_lifecycle_application.sql @@ -0,0 +1,600 @@ +-- Apply one independently reviewed Position lifecycle transition to authoritative +-- bitemporal Position truth. Review evidence remains value-minimized; Person, +-- candidate, compensation, assessment, rating, and free-form HR values are not +-- copied into this relation. + +CREATE TABLE position_lifecycle_application_record ( + tenant_record_id uuid NOT NULL REFERENCES tenant_record(tenant_record_id), + position_lifecycle_application_record_id uuid PRIMARY KEY, + position_record_id uuid NOT NULL, + predecessor_position_record_version_id uuid NOT NULL + REFERENCES position_record_version(position_record_version_id), + successor_position_record_version_id uuid NOT NULL, + position_lifecycle_change_reference uuid NOT NULL, + canonical_review_json text NOT NULL, + review_evidence_digest_sha256 text NOT NULL, + requester_actor_reference text NOT NULL, + reviewer_actor_reference text NOT NULL, + applied_by_actor_reference text NOT NULL, + current_status_code text NOT NULL, + proposed_status_code text NOT NULL, + reason_code text NOT NULL, + effective_on date NOT NULL, + reviewed_at timestamptz NOT NULL, + review_packet_recorded_at timestamptz NOT NULL, + audit_event_record_id uuid NOT NULL, + outbox_delivery_record_id uuid NOT NULL, + application_state text NOT NULL DEFAULT 'applied_after_human_review', + decision_authority_state text NOT NULL DEFAULT 'human_review_then_authoritative_application', + recorded_at timestamptz NOT NULL DEFAULT pg_catalog.transaction_timestamp(), + CONSTRAINT position_lifecycle_application_id_operational_check + CHECK (public.is_operational_uuid(position_lifecycle_application_record_id)), + CONSTRAINT position_lifecycle_successor_id_operational_check + CHECK (public.is_operational_uuid(successor_position_record_version_id)), + CONSTRAINT position_lifecycle_review_reference_v4_check + CHECK ( + position_lifecycle_change_reference::text ~ + '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + ), + CONSTRAINT position_lifecycle_position_tenant_fk + FOREIGN KEY (tenant_record_id, position_record_id) + REFERENCES position_record(tenant_record_id, position_record_id), + CONSTRAINT position_lifecycle_audit_tenant_fk + FOREIGN KEY (tenant_record_id, audit_event_record_id) + REFERENCES audit_event_record(tenant_record_id, audit_event_record_id), + CONSTRAINT position_lifecycle_outbox_tenant_fk + FOREIGN KEY (tenant_record_id, outbox_delivery_record_id) + REFERENCES outbox_delivery_record(tenant_record_id, outbox_delivery_record_id), + CONSTRAINT position_lifecycle_review_digest_check + CHECK (review_evidence_digest_sha256 ~ '^[0-9a-f]{64}$'), + CONSTRAINT position_lifecycle_requester_actor_check + CHECK ( + requester_actor_reference ~ + '^actor:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + ), + CONSTRAINT position_lifecycle_reviewer_actor_check + CHECK ( + reviewer_actor_reference ~ + '^actor:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + ), + CONSTRAINT position_lifecycle_applier_actor_check + CHECK ( + applied_by_actor_reference ~ + '^actor:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + ), + CONSTRAINT position_lifecycle_actor_separation_check + CHECK ( + requester_actor_reference <> reviewer_actor_reference + AND reviewer_actor_reference <> applied_by_actor_reference + ), + CONSTRAINT position_lifecycle_current_status_check + CHECK (current_status_code IN ('active', 'open', 'closed', 'frozen', 'abolished')), + CONSTRAINT position_lifecycle_proposed_status_check + CHECK (proposed_status_code IN ('active', 'open', 'closed', 'frozen', 'abolished')), + CONSTRAINT position_lifecycle_reason_check + CHECK (reason_code IN ('temporary_freeze', 'position_reactivation', 'position_closure', 'position_abolition')), + CONSTRAINT position_lifecycle_review_chronology_check + CHECK (reviewed_at <= review_packet_recorded_at AND review_packet_recorded_at <= recorded_at), + CONSTRAINT position_lifecycle_application_state_check + CHECK (application_state = 'applied_after_human_review'), + CONSTRAINT position_lifecycle_decision_authority_check + CHECK (decision_authority_state = 'human_review_then_authoritative_application'), + CONSTRAINT position_lifecycle_application_tenant_identity_unique + UNIQUE (tenant_record_id, position_lifecycle_application_record_id), + CONSTRAINT position_lifecycle_review_reference_unique + UNIQUE (tenant_record_id, position_lifecycle_change_reference), + CONSTRAINT position_lifecycle_successor_unique + UNIQUE (tenant_record_id, successor_position_record_version_id), + CONSTRAINT position_lifecycle_audit_unique + UNIQUE (tenant_record_id, audit_event_record_id), + CONSTRAINT position_lifecycle_outbox_unique + UNIQUE (tenant_record_id, outbox_delivery_record_id) +); + +COMMENT ON TABLE position_lifecycle_application_record IS + 'Immutable application evidence linking one reviewed Position lifecycle proposal to one authoritative successor PositionVersion and atomic audit/outbox evidence.'; + +ALTER TABLE position_record_version + ADD COLUMN position_lifecycle_application_record_id uuid; + +ALTER TABLE position_record_version + ADD CONSTRAINT position_version_lifecycle_application_tenant_fk + FOREIGN KEY (tenant_record_id, position_lifecycle_application_record_id) + REFERENCES position_lifecycle_application_record( + tenant_record_id, + position_lifecycle_application_record_id + ); + +CREATE FUNCTION validate_position_lifecycle_review_evidence( + p_canonical_review_json text, + p_review_digest text, + p_tenant_record_id uuid, + p_position_record_id uuid, + p_expected_current_status text, + p_expected_proposed_status text, + p_effective_on date +) +RETURNS boolean +LANGUAGE plpgsql +STABLE +STRICT +AS $$ +DECLARE + review_json json; + review_payload jsonb; + review_keys text[]; + key_count integer; + reviewed_at timestamptz; + review_recorded_at timestamptz; + expected_keys constant text[] := ARRAY[ + 'assignment_snapshot_digest_sha256', + 'current_status_code', + 'decision_authority', + 'effective_on', + 'evidence_version', + 'mutation_state', + 'next_action', + 'position_lifecycle_change_reference', + 'position_record_id', + 'position_snapshot_digest_sha256', + 'proposed_status_code', + 'reason_code', + 'recorded_at', + 'requester_actor_reference', + 'review_outcome_code', + 'review_state', + 'reviewed_at', + 'reviewer_actor_reference', + 'scope_verification_state', + 'tenant_record_id' + ]; +BEGIN + IF p_review_digest !~ '^[0-9a-f]{64}$' + OR encode( + public.digest(pg_catalog.convert_to(p_canonical_review_json, 'UTF8'), 'sha256'), + 'hex' + ) <> p_review_digest THEN + RETURN false; + END IF; + + BEGIN + review_json := p_canonical_review_json::json; + review_payload := p_canonical_review_json::jsonb; + EXCEPTION WHEN others THEN + RETURN false; + END; + + IF pg_catalog.jsonb_typeof(review_payload) <> 'object' THEN + RETURN false; + END IF; + + SELECT count(*), array_agg(key ORDER BY key) + INTO key_count, review_keys + FROM pg_catalog.json_object_keys(review_json) AS key_set(key); + IF key_count <> 20 OR review_keys <> expected_keys THEN + RETURN false; + END IF; + + IF review_payload ->> 'tenant_record_id' <> p_tenant_record_id::text + OR review_payload ->> 'position_record_id' <> p_position_record_id::text + OR review_payload ->> 'current_status_code' <> p_expected_current_status + OR review_payload ->> 'proposed_status_code' <> p_expected_proposed_status + OR review_payload ->> 'effective_on' <> p_effective_on::text + OR review_payload ->> 'decision_authority' <> 'human_review_only' + OR review_payload ->> 'mutation_state' <> 'not_authorized_to_apply' + OR review_payload ->> 'review_outcome_code' <> 'approved_for_authoritative_resolution' + OR review_payload ->> 'review_state' <> 'human_reviewed' + OR review_payload ->> 'scope_verification_state' <> 'requires_authoritative_resolution' + OR review_payload ->> 'evidence_version' <> '1' + OR review_payload ->> 'position_snapshot_digest_sha256' !~ '^[0-9a-f]{64}$' + OR review_payload ->> 'assignment_snapshot_digest_sha256' !~ '^[0-9a-f]{64}$' + OR review_payload ->> 'position_lifecycle_change_reference' + !~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + OR review_payload ->> 'requester_actor_reference' + !~ '^actor:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + OR review_payload ->> 'reviewer_actor_reference' + !~ '^actor:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + OR review_payload ->> 'requester_actor_reference' = review_payload ->> 'reviewer_actor_reference' THEN + RETURN false; + END IF; + + IF (review_payload ->> 'proposed_status_code') IN ('active', 'open') + AND review_payload ->> 'reason_code' <> 'position_reactivation' THEN + RETURN false; + ELSIF review_payload ->> 'proposed_status_code' = 'frozen' + AND review_payload ->> 'reason_code' <> 'temporary_freeze' THEN + RETURN false; + ELSIF review_payload ->> 'proposed_status_code' = 'closed' + AND review_payload ->> 'reason_code' <> 'position_closure' THEN + RETURN false; + ELSIF review_payload ->> 'proposed_status_code' = 'abolished' + AND review_payload ->> 'reason_code' <> 'position_abolition' THEN + RETURN false; + END IF; + + BEGIN + reviewed_at := (review_payload ->> 'reviewed_at')::timestamptz; + review_recorded_at := (review_payload ->> 'recorded_at')::timestamptz; + EXCEPTION WHEN others THEN + RETURN false; + END; + IF reviewed_at IS NULL + OR review_recorded_at IS NULL + OR reviewed_at > review_recorded_at + OR review_recorded_at > pg_catalog.transaction_timestamp() THEN + RETURN false; + END IF; + + RETURN true; +END; +$$; + +COMMENT ON FUNCTION validate_position_lifecycle_review_evidence(text, text, uuid, uuid, text, text, date) IS + 'Validates exact v1 Position lifecycle review shape, digest, tenant/Position/status/effective scope, human approval state, and chronology without granting mutation authority.'; + +CREATE FUNCTION protect_position_lifecycle_application_history() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + RAISE EXCEPTION 'position lifecycle application evidence is append-only' + USING ERRCODE = '55000'; +END; +$$; + +CREATE TRIGGER position_lifecycle_application_append_only_guard +BEFORE UPDATE OR DELETE ON position_lifecycle_application_record +FOR EACH ROW +EXECUTE FUNCTION protect_position_lifecycle_application_history(); + +CREATE FUNCTION protect_position_version_history_after_lifecycle_support() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + IF TG_OP = 'DELETE' THEN + RAISE EXCEPTION 'PositionVersion history cannot be deleted' + USING ERRCODE = '55000'; + END IF; + IF OLD.recorded_to IS NOT NULL + OR NEW.recorded_to IS NULL + OR NEW.recorded_to IS DISTINCT FROM pg_catalog.transaction_timestamp() + OR to_jsonb(NEW) - 'recorded_to' <> to_jsonb(OLD) - 'recorded_to' THEN + RAISE EXCEPTION 'PositionVersion history may only close an open recorded interval at transaction time' + USING ERRCODE = '55000'; + END IF; + RETURN NEW; +END; +$$; + +COMMENT ON FUNCTION protect_position_version_history_after_lifecycle_support() IS + 'Prevents delete/in-place rewrite of PositionVersion facts; an open system-recorded interval may only be closed at PostgreSQL transaction time.'; + +CREATE TRIGGER position_version_lifecycle_history_guard +BEFORE UPDATE OR DELETE ON position_record_version +FOR EACH ROW +EXECUTE FUNCTION protect_position_version_history_after_lifecycle_support(); + +CREATE FUNCTION validate_position_lifecycle_application_audit() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +DECLARE + audit_payload jsonb; + outbox_audit_id uuid; +BEGIN + IF NEW.recorded_at IS DISTINCT FROM pg_catalog.transaction_timestamp() THEN + RAISE EXCEPTION 'position lifecycle application recorded_at must equal transaction timestamp' + USING ERRCODE = '22023'; + END IF; + IF NOT validate_position_lifecycle_review_evidence( + NEW.canonical_review_json, + NEW.review_evidence_digest_sha256, + NEW.tenant_record_id, + NEW.position_record_id, + NEW.current_status_code, + NEW.proposed_status_code, + NEW.effective_on + ) THEN + RAISE EXCEPTION 'position lifecycle review evidence is invalid or out of scope' + USING ERRCODE = '23514'; + END IF; + + audit_payload := ( + SELECT canonical_event_json::jsonb + FROM audit_event_record + WHERE tenant_record_id = NEW.tenant_record_id + AND audit_event_record_id = NEW.audit_event_record_id + ); + IF audit_payload IS NULL + OR audit_payload ->> 'subject' <> 'position_record:' || NEW.position_record_id::text + OR audit_payload ->> 'orgmetraactor' <> NEW.applied_by_actor_reference + OR audit_payload ->> 'orgmetrapurpose' <> 'position_lifecycle_change' + OR audit_payload ->> 'orgmetrareason' <> NEW.reason_code + OR audit_payload ->> 'orgmetraevidence' <> NEW.review_evidence_digest_sha256 + OR audit_payload #>> '{data,result_code}' <> 'position_lifecycle_changed' + OR audit_payload #>> '{data,high_impact}' <> 'true' + OR audit_payload ->> 'orgmetraconfirmation' + <> 'human_confirmation:' || NEW.position_lifecycle_change_reference::text THEN + RAISE EXCEPTION 'position lifecycle audit event does not match the applied review' + USING ERRCODE = '23514'; + END IF; + + SELECT audit_event_record_id + INTO outbox_audit_id + FROM outbox_delivery_record + WHERE tenant_record_id = NEW.tenant_record_id + AND outbox_delivery_record_id = NEW.outbox_delivery_record_id; + IF outbox_audit_id IS DISTINCT FROM NEW.audit_event_record_id THEN + RAISE EXCEPTION 'position lifecycle outbox does not reference the application audit event' + USING ERRCODE = '23514'; + END IF; + + RETURN NEW; +END; +$$; + +CREATE TRIGGER position_lifecycle_application_integrity_guard +BEFORE INSERT ON position_lifecycle_application_record +FOR EACH ROW +EXECUTE FUNCTION validate_position_lifecycle_application_audit(); + +CREATE FUNCTION apply_position_lifecycle_change( + p_tenant_record_id uuid, + p_position_record_id uuid, + p_expected_predecessor_version_id uuid, + p_successor_version_id uuid, + p_application_record_id uuid, + p_canonical_review_json text, + p_review_digest text, + p_applied_by_actor_reference text, + p_audit_event_record_id uuid, + p_outbox_delivery_record_id uuid +) +RETURNS void +LANGUAGE plpgsql +AS $$ +DECLARE + review_payload jsonb; + predecessor position_record_version%ROWTYPE; + preserved_version_id uuid; + event_json text; + event_digest text; + current_status text; + proposed_status text; + reason text; + effective_on date; + requester text; + reviewer text; + reviewed_at timestamptz; + review_recorded_at timestamptz; + review_reference uuid; +BEGIN + IF is_operational_uuid(p_tenant_record_id) IS NOT TRUE + OR is_operational_uuid(p_position_record_id) IS NOT TRUE + OR is_operational_uuid(p_expected_predecessor_version_id) IS NOT TRUE + OR is_operational_uuid(p_successor_version_id) IS NOT TRUE + OR is_operational_uuid(p_application_record_id) IS NOT TRUE + OR is_operational_uuid(p_audit_event_record_id) IS NOT TRUE + OR is_operational_uuid(p_outbox_delivery_record_id) IS NOT TRUE THEN + RAISE EXCEPTION 'position lifecycle application requires operational UUID identities' + USING ERRCODE = '23514'; + END IF; + IF p_applied_by_actor_reference + !~ '^actor:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN + RAISE EXCEPTION 'position lifecycle applier must be a pseudonymous actor UUIDv4 correlation' + USING ERRCODE = '23514'; + END IF; + + review_payload := p_canonical_review_json::jsonb; + current_status := review_payload ->> 'current_status_code'; + proposed_status := review_payload ->> 'proposed_status_code'; + reason := review_payload ->> 'reason_code'; + effective_on := (review_payload ->> 'effective_on')::date; + requester := review_payload ->> 'requester_actor_reference'; + reviewer := review_payload ->> 'reviewer_actor_reference'; + reviewed_at := (review_payload ->> 'reviewed_at')::timestamptz; + review_recorded_at := (review_payload ->> 'recorded_at')::timestamptz; + review_reference := (review_payload ->> 'position_lifecycle_change_reference')::uuid; + + IF reviewer = p_applied_by_actor_reference THEN + RAISE EXCEPTION 'position lifecycle reviewer and applier must be distinct actors' + USING ERRCODE = '23514'; + END IF; + + PERFORM 1 + FROM position_record + WHERE tenant_record_id = p_tenant_record_id + AND position_record_id = p_position_record_id + AND recorded_from <= pg_catalog.transaction_timestamp() + AND (recorded_to IS NULL OR pg_catalog.transaction_timestamp() < recorded_to) + FOR UPDATE; + IF NOT FOUND THEN + RAISE EXCEPTION 'Position is not current in the tenant' + USING ERRCODE = '23503'; + END IF; + + SELECT version.* + INTO predecessor + FROM position_record_version AS version + WHERE version.tenant_record_id = p_tenant_record_id + AND version.position_record_id = p_position_record_id + AND version.recorded_from <= pg_catalog.transaction_timestamp() + AND (version.recorded_to IS NULL OR pg_catalog.transaction_timestamp() < version.recorded_to) + AND version.effective_from <= effective_on + AND (version.effective_to IS NULL OR effective_on < version.effective_to) + FOR UPDATE; + IF NOT FOUND THEN + RAISE EXCEPTION 'no current PositionVersion covers the requested effective date' + USING ERRCODE = '23503'; + END IF; + IF predecessor.position_record_version_id <> p_expected_predecessor_version_id + OR predecessor.position_status_code <> current_status THEN + RAISE EXCEPTION 'reviewed Position state is stale at authoritative application time' + USING ERRCODE = '40001'; + END IF; + + IF NOT validate_position_lifecycle_review_evidence( + p_canonical_review_json, + p_review_digest, + p_tenant_record_id, + p_position_record_id, + current_status, + proposed_status, + effective_on + ) THEN + RAISE EXCEPTION 'position lifecycle review evidence is invalid or out of scope' + USING ERRCODE = '23514'; + END IF; + + IF proposed_status IN ('closed', 'abolished') + AND EXISTS ( + SELECT 1 + FROM assignment_record AS assignment + WHERE assignment.tenant_record_id = p_tenant_record_id + AND assignment.position_record_id = p_position_record_id + AND assignment.recorded_from <= pg_catalog.transaction_timestamp() + AND (assignment.recorded_to IS NULL OR pg_catalog.transaction_timestamp() < assignment.recorded_to) + AND daterange(assignment.effective_from, assignment.effective_to, '[)') + && daterange(effective_on, NULL, '[)') + ) THEN + RAISE EXCEPTION 'Position cannot close or be abolished while a current Assignment crosses the effective date' + USING ERRCODE = '23514'; + END IF; + + event_json := pg_catalog.jsonb_build_object( + 'data', pg_catalog.jsonb_build_object( + 'high_impact', true, + 'result_code', 'position_lifecycle_changed' + ), + 'datacontenttype', 'application/json', + 'id', p_audit_event_record_id::text, + 'orgmetraactor', p_applied_by_actor_reference, + 'orgmetraconfirmation', 'human_confirmation:' || review_reference::text, + 'orgmetraevidence', p_review_digest, + 'orgmetrapurpose', 'position_lifecycle_change', + 'orgmetrareason', reason, + 'orgmetratenant', p_tenant_record_id::text, + 'source', 'urn:orgmetra:people_api', + 'specversion', '1.0', + 'subject', 'position_record:' || p_position_record_id::text, + 'time', to_char( + pg_catalog.transaction_timestamp() AT TIME ZONE 'UTC', + 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"' + ), + 'type', 'orgmetra.people.position_lifecycle_changed' + )::text; + event_digest := encode( + public.digest(pg_catalog.convert_to(event_json, 'UTF8'), 'sha256'), + 'hex' + ); + PERFORM public.record_audit_outbox_event( + p_tenant_record_id, + p_audit_event_record_id, + p_outbox_delivery_record_id, + event_json, + event_digest, + 'orgmetra_domain_events' + ); + + INSERT INTO position_lifecycle_application_record ( + tenant_record_id, + position_lifecycle_application_record_id, + position_record_id, + predecessor_position_record_version_id, + successor_position_record_version_id, + position_lifecycle_change_reference, + canonical_review_json, + review_evidence_digest_sha256, + requester_actor_reference, + reviewer_actor_reference, + applied_by_actor_reference, + current_status_code, + proposed_status_code, + reason_code, + effective_on, + reviewed_at, + review_packet_recorded_at, + audit_event_record_id, + outbox_delivery_record_id + ) VALUES ( + p_tenant_record_id, + p_application_record_id, + p_position_record_id, + predecessor.position_record_version_id, + p_successor_version_id, + review_reference, + p_canonical_review_json, + p_review_digest, + requester, + reviewer, + p_applied_by_actor_reference, + current_status, + proposed_status, + reason, + effective_on, + reviewed_at, + review_recorded_at, + p_audit_event_record_id, + p_outbox_delivery_record_id + ); + + UPDATE position_record_version + SET recorded_to = pg_catalog.transaction_timestamp() + WHERE tenant_record_id = p_tenant_record_id + AND position_record_version_id = predecessor.position_record_version_id; + + IF predecessor.effective_from < effective_on THEN + preserved_version_id := pg_catalog.gen_random_uuid(); + INSERT INTO position_record_version ( + tenant_record_id, + position_record_version_id, + position_record_id, + position_status_code, + effective_from, + effective_to, + recorded_from, + position_lifecycle_application_record_id + ) VALUES ( + p_tenant_record_id, + preserved_version_id, + p_position_record_id, + predecessor.position_status_code, + predecessor.effective_from, + effective_on, + pg_catalog.transaction_timestamp(), + p_application_record_id + ); + END IF; + + INSERT INTO position_record_version ( + tenant_record_id, + position_record_version_id, + position_record_id, + position_status_code, + effective_from, + effective_to, + recorded_from, + position_lifecycle_application_record_id + ) VALUES ( + p_tenant_record_id, + p_successor_version_id, + p_position_record_id, + proposed_status, + effective_on, + predecessor.effective_to, + pg_catalog.transaction_timestamp(), + p_application_record_id + ); +END; +$$; + +COMMENT ON FUNCTION apply_position_lifecycle_change(uuid, uuid, uuid, uuid, uuid, text, text, text, uuid, uuid) IS + 'Atomically validates one approved v1 lifecycle review against locked bitemporal Position/Assignment truth, records immutable audit/outbox evidence, closes the predecessor system-time interval, and inserts preserved/successor PositionVersion truth.'; + +ALTER TABLE position_lifecycle_application_record ENABLE ROW LEVEL SECURITY; +ALTER TABLE position_lifecycle_application_record FORCE ROW LEVEL SECURITY; +CREATE POLICY position_lifecycle_application_scope_policy +ON position_lifecycle_application_record +USING (tenant_record_id = current_tenant_record_id()) +WITH CHECK (tenant_record_id = current_tenant_record_id()); From c10f723b47c6cf0ae98ccc79af928340307f2426 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 07:44:24 -0700 Subject: [PATCH 04/23] docs(position): record lifecycle application architecture --- .../0112-position-lifecycle-application.md | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 docs/adr/0112-position-lifecycle-application.md diff --git a/docs/adr/0112-position-lifecycle-application.md b/docs/adr/0112-position-lifecycle-application.md new file mode 100644 index 000000000..587565d87 --- /dev/null +++ b/docs/adr/0112-position-lifecycle-application.md @@ -0,0 +1,33 @@ +# ADR 0112: Apply reviewed Position lifecycle changes as bitemporal truth + +Status: Proposed + +## Context + +PR #111 adds a human-reviewed, deliberately non-authorizing `PositionLifecycleChangeReviewPacket`. Protected `develop` already separates stable `position_record` identity from bitemporal `position_record_version`, and Assignment truth separately represents worker occupancy. A commercial HRIS still needs an authoritative boundary that can turn an approved lifecycle proposal into Position truth without treating stale UI state or the review artifact itself as authority. + +Closing or abolishing an occupied Position is high-impact because it can make staffing truth internally inconsistent. Rewriting a PositionVersion in place would also destroy what the system previously knew. + +## Decision + +Add an Orgmetra-owned Position lifecycle application boundary. It consumes exact v1 canonical review evidence, revalidates its SHA-256 and governed shape, locks the tenant-qualified Position, resolves the exact PositionVersion covering the requested business-effective date at current system time, rejects stale reviewed status, and checks current Assignment occupancy before `closed` or `abolished` transitions. + +Application closes only the predecessor system-recorded interval at PostgreSQL transaction time. When the reviewed effective date splits an existing business-effective interval, it inserts a preserved predecessor segment plus the reviewed successor segment at the new system-recorded time. The stable Position identity is unchanged. + +One immutable `position_lifecycle_application_record` binds predecessor/successor identities, exact review bytes/digest, requester/reviewer/applier separation, lifecycle reason, human-review chronology, and the audit/outbox identities. The audit/outbox event is created in the same transaction and must match Position subject, purpose, reason, review digest, applier, high-impact result, and human-confirmation reference. Application evidence and PositionVersion history reject UPDATE/DELETE rewrite; tenant-scoped application evidence uses forced row-level security. + +## Consequences + +- Business-effective time and system-recorded time remain independent and reconstructable. +- A review packet never authorizes mutation by itself; current Position and Assignment truth is re-resolved at application time. +- `closed` and `abolished` fail closed while any current Assignment overlaps the requested effective date or later. +- Existing Assignment, reporting-line, compensation, assessment, and Person facts remain separate; no cross-service table SQL is introduced. +- The branch is a dependency-first descendant of #111 and remains Draft until the parent integrates and fresh post-restack gates pass. +- Direct production database privileges remain a deployment concern; this slice does not claim that PostgreSQL row-level security replaces application authorization. + +## Alternatives rejected + +1. **Overwrite `position_record_version.position_status_code`.** Rejected because it destroys system-time history. +2. **Create a new Position identity for each lifecycle change.** Rejected because Job/Position/Assignment semantics require a stable Position anchor with versioned state. +3. **Apply the review without refreshing Assignment truth.** Rejected because a reviewed closure can become stale before application and strand active staffing evidence. +4. **Move lifecycle state into reporting or vacancy tables.** Rejected because those relations own different facts and would violate the Job/Position/Assignment model. \ No newline at end of file From 1349a2c58bf904c750fc8888b214b7ff2dc36b76 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 07:44:43 -0700 Subject: [PATCH 05/23] docs(position): trace lifecycle application evidence --- .../position-lifecycle-application.md | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 docs/traceability/position-lifecycle-application.md diff --git a/docs/traceability/position-lifecycle-application.md b/docs/traceability/position-lifecycle-application.md new file mode 100644 index 000000000..7e288a7fe --- /dev/null +++ b/docs/traceability/position-lifecycle-application.md @@ -0,0 +1,30 @@ +# Position lifecycle application traceability + +## Truth state + +- **Protected-main truth:** `develop@9e3e4847510e1e612b48474ba42b177b8ed824df` has stable Position anchors, bitemporal PositionVersion facts, Assignment occupancy, tenant RLS, and immutable audit/outbox foundations, but no authoritative existing-Position lifecycle mutation. +- **Dependency-active truth:** PR #111 adds only reviewed lifecycle evidence and remains a separate review boundary. +- **Active PR truth:** this stacked branch applies an approved v1 review to locked current Position/Assignment truth and records the resulting bitemporal PositionVersion plus immutable application/audit/outbox correlation. +- **Planned after parent integration:** retarget to fresh `develop`, reconcile migration/repository inventories, and rerun all applicable Foundation, Security, SAST, Recovery, People and lifecycle-application gates on one exact head. +- **Out of scope:** Person/candidate data, compensation, assessment/rating data, reporting-line changes, autonomous employment decisions, foreign repository writes, and direct foreign application-table SQL. + +## Requirement mapping + +| Requirement | Evidence | +|---|---| +| Stable Position identity with versioned lifecycle state | existing `position_record`; `position_record_version`; ADR 0112 | +| Separate business and system time | reviewed `effective_on`; predecessor split; PostgreSQL `transaction_timestamp()` for new system-time truth | +| Stale-review defense | locked current Position and exact predecessor/status comparison before application | +| Staffing safety | current Assignment overlap check blocks `closed`/`abolished` transitions | +| Human review remains non-authorizing | exact v1 review state must be `approved_for_authoritative_resolution` while application independently re-resolves live truth | +| Actor separation | requester/reviewer from review plus distinct application actor | +| Immutable review correlation | exact canonical review bytes + SHA-256 + review UUIDv4 stored in `position_lifecycle_application_record` | +| Immutable audit/outbox | same-transaction `record_audit_outbox_event`; application trigger binds event subject/actor/purpose/reason/evidence/result/confirmation and outbox identity | +| Correction, not rewrite | PositionVersion history trigger permits only closing an open recorded interval at transaction time; application evidence is append-only | +| Tenant isolation | tenant-qualified FKs and forced-RLS application relation; existing PositionVersion forced-RLS remains intact | +| Value minimization | no Person/candidate identity, pay, rating, assessment, allocation value, prompt/model output, or free-form HR text in application evidence | +| Exact-head quality | dedicated PostgreSQL regression and pinned exact-checkout workflow | + +## Buyer behavior + +An HR operator can review a Position freeze, closure, abolition, or reactivation in #111. This branch does not trust that review as current truth: at application time it reopens the exact tenant/Position state, confirms the reviewed predecessor still covers the business date, prevents closure/abolition across live staffing, then writes correction-preserving PositionVersion truth with immutable audit/outbox. If any scope, status, evidence, chronology, actor-separation, or staffing condition changed, the application fails closed and the buyer must review the fresh state rather than silently applying stale intent. \ No newline at end of file From f0a0568e872977eb43b01b4fbac1528a4505aeaa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 07:44:55 -0700 Subject: [PATCH 06/23] docs(position): add lifecycle application primary references --- ...sition-lifecycle-application-references.md | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 docs/doctoring/position-lifecycle-application-references.md diff --git a/docs/doctoring/position-lifecycle-application-references.md b/docs/doctoring/position-lifecycle-application-references.md new file mode 100644 index 000000000..2b0f0945c --- /dev/null +++ b/docs/doctoring/position-lifecycle-application-references.md @@ -0,0 +1,20 @@ +# Position lifecycle application — primary references + +Reviewed 2026-08-24. These sources support database integrity, tenant isolation, and identifier semantics; they do not establish an employment-law rule or certification claim. + +## APA 7 references + +Davis, K., Peabody, B., & Leach, P. (2024). *Universally Unique IDentifiers (UUIDs)* (RFC 9562). RFC Editor. https://doi.org/10.17487/RFC9562 + +PostgreSQL Global Development Group. (2026). *PostgreSQL 16 documentation: CREATE POLICY*. https://www.postgresql.org/docs/16/sql-createpolicy.html + +PostgreSQL Global Development Group. (2026). *PostgreSQL 16 documentation: Constraints*. https://www.postgresql.org/docs/16/ddl-constraints.html + +PostgreSQL Global Development Group. (2026). *PostgreSQL 16 documentation: Function security*. https://www.postgresql.org/docs/16/perm-functions.html + +## Design use + +- RFC 9562 is the primary identifier source. Orgmetra continues to distinguish operational HRIS UUIDs from packet-owned UUIDv4 correlation identifiers. +- PostgreSQL row-security policy semantics support tenant-scoped `USING`/`WITH CHECK`; `FORCE ROW LEVEL SECURITY` is used for the new application evidence relation rather than claiming RLS is application authorization. +- PostgreSQL constraints and existing GiST bitemporal exclusions preserve non-overlapping effective/system-time truth. The lifecycle operation therefore closes the prior system-time interval and inserts a new effective segmentation rather than rewriting status in place. +- PostgreSQL warns that database functions can become privilege boundaries. This migration remains invoker-rights and schema-qualified for sensitive built-ins/relations where practical; it does not use a broad `SECURITY DEFINER` shortcut. \ No newline at end of file From f78cab16bb63ed4e369c8c432694a5e42059cc69 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 07:46:43 -0700 Subject: [PATCH 07/23] test(position): align lifecycle fixture with foundation anchors --- tests/test_position_lifecycle_application_postgres.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_position_lifecycle_application_postgres.sh b/tests/test_position_lifecycle_application_postgres.sh index f27ec8f59..37398b4d8 100644 --- a/tests/test_position_lifecycle_application_postgres.sh +++ b/tests/test_position_lifecycle_application_postgres.sh @@ -29,11 +29,12 @@ ASSIGNMENT_DIGEST="$(printf 'assignment-snapshot' | sha256sum | awk '{print $1}' psql "$DATABASE_URL" -v ON_ERROR_STOP=1 \ -v tenant="$TENANT" -v org="$ORG" -v job="$JOB" -v position="$POSITION" -v current_version="$CURRENT_VERSION" <<'SQL' >/dev/null -INSERT INTO tenant_record (tenant_record_id, tenant_name) VALUES (:'tenant', 'Lifecycle Test'); -INSERT INTO organization_unit ( - tenant_record_id, organization_unit_id, organization_unit_name, effective_from -) VALUES (:'tenant', :'org', 'Lifecycle Org', DATE '2026-01-01'); -INSERT INTO job_profile (tenant_record_id, job_profile_id) VALUES (:'tenant', :'job'); +INSERT INTO tenant_record (tenant_record_id, tenant_reference) +VALUES (:'tenant', 'tenant:lifecycle_test'); +INSERT INTO organization_unit (tenant_record_id, organization_unit_id) +VALUES (:'tenant', :'org'); +INSERT INTO job_profile (tenant_record_id, job_profile_id) +VALUES (:'tenant', :'job'); INSERT INTO position_record ( tenant_record_id, position_record_id, organization_unit_id, job_profile_id ) VALUES (:'tenant', :'position', :'org', :'job'); From 8e55d29f3df95ddb3d344aefbd9d3f0a9564fe74 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 07:47:58 -0700 Subject: [PATCH 08/23] test(position): reject forged lifecycle transitions --- ...ifecycle_transition_validation_postgres.sh | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 tests/test_position_lifecycle_transition_validation_postgres.sh diff --git a/tests/test_position_lifecycle_transition_validation_postgres.sh b/tests/test_position_lifecycle_transition_validation_postgres.sh new file mode 100644 index 000000000..237ca45fc --- /dev/null +++ b/tests/test_position_lifecycle_transition_validation_postgres.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${DATABASE_URL:?DATABASE_URL is required}" + +TENANT="0198a412-8000-7000-8000-000000000101" +POSITION="0198a412-8000-7000-8000-000000000104" +REVIEW_REF="a0e89c71-41c1-4c63-935c-8569d83f7901" +REQUESTER="actor:9d5a177e-79e9-4022-8f35-b2408ec5a503" +REVIEWER="actor:6aacb560-ec5d-41d7-94a5-27cf95438b1b" +POSITION_DIGEST="$(printf 'position-snapshot' | sha256sum | awk '{print $1}')" +ASSIGNMENT_DIGEST="$(printf 'assignment-snapshot' | sha256sum | awk '{print $1}')" + +review_json="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 \ + -v tenant="$TENANT" -v position="$POSITION" -v review_ref="$REVIEW_REF" \ + -v requester="$REQUESTER" -v reviewer="$REVIEWER" \ + -v position_digest="$POSITION_DIGEST" -v assignment_digest="$ASSIGNMENT_DIGEST" <<'SQL' +SELECT jsonb_build_object( + 'assignment_snapshot_digest_sha256', :'assignment_digest', + 'current_status_code', 'frozen', + 'decision_authority', 'human_review_only', + 'effective_on', '2026-09-01', + 'evidence_version', 1, + 'mutation_state', 'not_authorized_to_apply', + 'next_action', 'Re-resolve tenant-qualified Position and Assignment truth at the requested business/system coordinate; require authoritative actor separation, reviewed evidence, staffing safety, and immutable audit/outbox before any lifecycle mutation.', + 'position_lifecycle_change_reference', :'review_ref', + 'position_record_id', :'position', + 'position_snapshot_digest_sha256', :'position_digest', + 'proposed_status_code', 'frozen', + 'reason_code', 'temporary_freeze', + 'recorded_at', '2026-08-24T14:00:00Z', + 'requester_actor_reference', :'requester', + 'review_outcome_code', 'approved_for_authoritative_resolution', + 'review_state', 'human_reviewed', + 'reviewed_at', '2026-08-24T13:55:00Z', + 'reviewer_actor_reference', :'reviewer', + 'scope_verification_state', 'requires_authoritative_resolution', + 'tenant_record_id', :'tenant' +)::text; +SQL +)" +review_digest="$(printf '%s' "$review_json" | sha256sum | awk '{print $1}')" + +accepted="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 \ + -v review_json="$review_json" -v review_digest="$review_digest" \ + -v tenant="$TENANT" -v position="$POSITION" <<'SQL' +SELECT public.validate_position_lifecycle_review_evidence( + :'review_json', :'review_digest', :'tenant', :'position', + 'frozen', 'frozen', DATE '2026-09-01' +); +SQL +)" + +test "$accepted" = "f" +echo "position lifecycle transition validation: PASS" From 33effec9fc7a78187db216ab5f0a073aa8eb1f8d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 07:48:26 -0700 Subject: [PATCH 09/23] test(position): exercise lifecycle transition vocabulary --- .github/workflows/position-lifecycle-application-quality.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/position-lifecycle-application-quality.yml b/.github/workflows/position-lifecycle-application-quality.yml index d87bbb492..f61b69151 100644 --- a/.github/workflows/position-lifecycle-application-quality.yml +++ b/.github/workflows/position-lifecycle-application-quality.yml @@ -8,6 +8,7 @@ on: paths: - "database/migrations/0023_position_lifecycle_application.sql" - "tests/test_position_lifecycle_application_postgres.sh" + - "tests/test_position_lifecycle_transition_validation_postgres.sh" - "docs/adr/0112-position-lifecycle-application.md" - "docs/traceability/position-lifecycle-application.md" - "docs/doctoring/position-lifecycle-application-references.md" @@ -61,6 +62,7 @@ jobs: required = [ "tests/test_position_lifecycle_application_postgres.sh", + "tests/test_position_lifecycle_transition_validation_postgres.sh", ".github/workflows/position-lifecycle-application-quality.yml", ] optional = [ @@ -82,6 +84,8 @@ jobs: PY - name: Run Position lifecycle application regressions run: bash tests/test_position_lifecycle_application_postgres.sh + - name: Reject forged lifecycle transition evidence + run: bash tests/test_position_lifecycle_transition_validation_postgres.sh - name: Require clean checkout run: | git diff --exit-code From 37f6a80d65a490ddd1824fa2d7f8aa3cad97994d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 07:49:06 -0700 Subject: [PATCH 10/23] fix(position): enforce reviewed lifecycle transitions --- ...osition_lifecycle_transition_hardening.sql | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 database/migrations/0024_position_lifecycle_transition_hardening.sql diff --git a/database/migrations/0024_position_lifecycle_transition_hardening.sql b/database/migrations/0024_position_lifecycle_transition_hardening.sql new file mode 100644 index 000000000..9627d158b --- /dev/null +++ b/database/migrations/0024_position_lifecycle_transition_hardening.sql @@ -0,0 +1,140 @@ +-- Strengthen the Position lifecycle application boundary so a caller cannot +-- manufacture otherwise well-shaped review JSON for a no-op or forbidden state +-- transition and recompute its SHA-256. The authoritative persistence boundary +-- independently enforces the same transition graph as the reviewed evidence type. + +CREATE OR REPLACE FUNCTION validate_position_lifecycle_review_evidence( + p_canonical_review_json text, + p_review_digest text, + p_tenant_record_id uuid, + p_position_record_id uuid, + p_expected_current_status text, + p_expected_proposed_status text, + p_effective_on date +) +RETURNS boolean +LANGUAGE plpgsql +STABLE +STRICT +AS $$ +DECLARE + review_json json; + review_payload jsonb; + review_keys text[]; + key_count integer; + reviewed_at timestamptz; + review_recorded_at timestamptz; + expected_keys constant text[] := ARRAY[ + 'assignment_snapshot_digest_sha256', + 'current_status_code', + 'decision_authority', + 'effective_on', + 'evidence_version', + 'mutation_state', + 'next_action', + 'position_lifecycle_change_reference', + 'position_record_id', + 'position_snapshot_digest_sha256', + 'proposed_status_code', + 'reason_code', + 'recorded_at', + 'requester_actor_reference', + 'review_outcome_code', + 'review_state', + 'reviewed_at', + 'reviewer_actor_reference', + 'scope_verification_state', + 'tenant_record_id' + ]; +BEGIN + IF p_review_digest !~ '^[0-9a-f]{64}$' + OR encode( + public.digest(pg_catalog.convert_to(p_canonical_review_json, 'UTF8'), 'sha256'), + 'hex' + ) <> p_review_digest THEN + RETURN false; + END IF; + + BEGIN + review_json := p_canonical_review_json::json; + review_payload := p_canonical_review_json::jsonb; + EXCEPTION WHEN others THEN + RETURN false; + END; + + IF pg_catalog.jsonb_typeof(review_payload) <> 'object' THEN + RETURN false; + END IF; + + SELECT count(*), array_agg(key ORDER BY key) + INTO key_count, review_keys + FROM pg_catalog.json_object_keys(review_json) AS key_set(key); + IF key_count <> 20 OR review_keys <> expected_keys THEN + RETURN false; + END IF; + + IF review_payload ->> 'tenant_record_id' <> p_tenant_record_id::text + OR review_payload ->> 'position_record_id' <> p_position_record_id::text + OR review_payload ->> 'current_status_code' <> p_expected_current_status + OR review_payload ->> 'proposed_status_code' <> p_expected_proposed_status + OR review_payload ->> 'effective_on' <> p_effective_on::text + OR review_payload ->> 'decision_authority' <> 'human_review_only' + OR review_payload ->> 'mutation_state' <> 'not_authorized_to_apply' + OR review_payload ->> 'review_outcome_code' <> 'approved_for_authoritative_resolution' + OR review_payload ->> 'review_state' <> 'human_reviewed' + OR review_payload ->> 'scope_verification_state' <> 'requires_authoritative_resolution' + OR review_payload ->> 'evidence_version' <> '1' + OR review_payload ->> 'position_snapshot_digest_sha256' !~ '^[0-9a-f]{64}$' + OR review_payload ->> 'assignment_snapshot_digest_sha256' !~ '^[0-9a-f]{64}$' + OR review_payload ->> 'position_lifecycle_change_reference' + !~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + OR review_payload ->> 'requester_actor_reference' + !~ '^actor:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + OR review_payload ->> 'reviewer_actor_reference' + !~ '^actor:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + OR review_payload ->> 'requester_actor_reference' = review_payload ->> 'reviewer_actor_reference' THEN + RETURN false; + END IF; + + IF NOT ( + (p_expected_current_status = 'open' AND p_expected_proposed_status IN ('active', 'frozen', 'closed', 'abolished')) + OR (p_expected_current_status = 'active' AND p_expected_proposed_status IN ('frozen', 'closed', 'abolished')) + OR (p_expected_current_status = 'frozen' AND p_expected_proposed_status IN ('open', 'active', 'closed', 'abolished')) + OR (p_expected_current_status = 'closed' AND p_expected_proposed_status IN ('open', 'abolished')) + ) THEN + RETURN false; + END IF; + + IF p_expected_proposed_status IN ('active', 'open') + AND review_payload ->> 'reason_code' <> 'position_reactivation' THEN + RETURN false; + ELSIF p_expected_proposed_status = 'frozen' + AND review_payload ->> 'reason_code' <> 'temporary_freeze' THEN + RETURN false; + ELSIF p_expected_proposed_status = 'closed' + AND review_payload ->> 'reason_code' <> 'position_closure' THEN + RETURN false; + ELSIF p_expected_proposed_status = 'abolished' + AND review_payload ->> 'reason_code' <> 'position_abolition' THEN + RETURN false; + END IF; + + BEGIN + reviewed_at := (review_payload ->> 'reviewed_at')::timestamptz; + review_recorded_at := (review_payload ->> 'recorded_at')::timestamptz; + EXCEPTION WHEN others THEN + RETURN false; + END; + IF reviewed_at IS NULL + OR review_recorded_at IS NULL + OR reviewed_at > review_recorded_at + OR review_recorded_at > pg_catalog.transaction_timestamp() THEN + RETURN false; + END IF; + + RETURN true; +END; +$$; + +COMMENT ON FUNCTION validate_position_lifecycle_review_evidence(text, text, uuid, uuid, text, text, date) IS + 'Validates exact v1 Position lifecycle review shape, digest, tenant/Position/status/effective scope, allowed non-no-op transition graph, human approval state, and chronology without granting mutation authority.'; From a5b6ce25e6e5f080d78887cd42ff28a55d22a5f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 07:49:33 -0700 Subject: [PATCH 11/23] test(position): exercise transition hardening migration --- tests/test_position_lifecycle_transition_validation_postgres.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_position_lifecycle_transition_validation_postgres.sh b/tests/test_position_lifecycle_transition_validation_postgres.sh index 237ca45fc..08dd83d54 100644 --- a/tests/test_position_lifecycle_transition_validation_postgres.sh +++ b/tests/test_position_lifecycle_transition_validation_postgres.sh @@ -3,6 +3,8 @@ set -euo pipefail : "${DATABASE_URL:?DATABASE_URL is required}" +psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f database/migrations/0024_position_lifecycle_transition_hardening.sql >/dev/null + TENANT="0198a412-8000-7000-8000-000000000101" POSITION="0198a412-8000-7000-8000-000000000104" REVIEW_REF="a0e89c71-41c1-4c63-935c-8569d83f7901" From 1a76b4697363cdf6770bf881b34d5ee9148b2657 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 07:49:50 -0700 Subject: [PATCH 12/23] test(position): bind transition hardening provenance --- .github/workflows/position-lifecycle-application-quality.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/position-lifecycle-application-quality.yml b/.github/workflows/position-lifecycle-application-quality.yml index f61b69151..dc0ef9ac4 100644 --- a/.github/workflows/position-lifecycle-application-quality.yml +++ b/.github/workflows/position-lifecycle-application-quality.yml @@ -7,6 +7,7 @@ on: - feat/position-lifecycle-review paths: - "database/migrations/0023_position_lifecycle_application.sql" + - "database/migrations/0024_position_lifecycle_transition_hardening.sql" - "tests/test_position_lifecycle_application_postgres.sh" - "tests/test_position_lifecycle_transition_validation_postgres.sh" - "docs/adr/0112-position-lifecycle-application.md" @@ -67,6 +68,7 @@ jobs: ] optional = [ "database/migrations/0023_position_lifecycle_application.sql", + "database/migrations/0024_position_lifecycle_transition_hardening.sql", "docs/adr/0112-position-lifecycle-application.md", "docs/traceability/position-lifecycle-application.md", "docs/doctoring/position-lifecycle-application-references.md", From 3ab4ae1ae0c0d6e46ff263b6d76add6867ebbc47 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 07:55:42 -0700 Subject: [PATCH 13/23] test(position): require fresh lifecycle snapshot evidence --- ...n_lifecycle_snapshot_integrity_postgres.sh | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 tests/test_position_lifecycle_snapshot_integrity_postgres.sh diff --git a/tests/test_position_lifecycle_snapshot_integrity_postgres.sh b/tests/test_position_lifecycle_snapshot_integrity_postgres.sh new file mode 100644 index 000000000..973377783 --- /dev/null +++ b/tests/test_position_lifecycle_snapshot_integrity_postgres.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${DATABASE_URL:?DATABASE_URL is required}" + +TENANT="0198a412-8000-7000-8000-000000000101" +POSITION="0198a412-8000-7000-8000-000000000104" +CURRENT_VERSION="0198a412-8000-7000-8000-000000000106" +SUCCESSOR="0198a412-8000-7000-8000-000000000121" +APPLICATION="0198a412-8000-7000-8000-000000000122" +AUDIT="0198a412-8000-7000-8000-000000000123" +OUTBOX="0198a412-8000-7000-8000-000000000124" +REVIEW_REF="f0ec646a-4f21-4e6c-87e1-7a51c4545534" +REQUESTER="actor:9d5a177e-79e9-4022-8f35-b2408ec5a503" +REVIEWER="actor:6aacb560-ec5d-41d7-94a5-27cf95438b1b" +APPLIER="actor:faefac04-52e8-43d2-aa54-d9046238733f" + +# The authoritative database snapshot digests must be available to the review +# producer and to the application guard. Their absence is itself fail-closed. +POSITION_DIGEST="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 -v tenant="$TENANT" -v position="$POSITION" <<'SQL' +SELECT public.position_lifecycle_position_snapshot_digest( + :'tenant', :'position', DATE '2026-09-15' +); +SQL +)" +ASSIGNMENT_DIGEST="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 -v tenant="$TENANT" -v position="$POSITION" <<'SQL' +SELECT public.position_lifecycle_assignment_snapshot_digest( + :'tenant', :'position', DATE '2026-09-15' +); +SQL +)" +test "$POSITION_DIGEST" != "" +test "$ASSIGNMENT_DIGEST" != "" + +# Exact parent-package canonical JSON has sorted keys and compact separators. +REVIEW_JSON="$(python - </dev/null 2>&1 +SELECT public.apply_position_lifecycle_change( + :'tenant', :'position', :'current_version', :'successor', :'application', + :'review_json', :'review_digest', :'applier', :'audit', :'outbox' +); +SQL +then + echo "forged lifecycle snapshot evidence was applied" >&2 + exit 1 +fi + +# The high-impact mutation function must never be executable through PostgreSQL's +# default PUBLIC function privilege; deployment must grant it deliberately. +public_execute="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 <<'SQL' +SELECT EXISTS ( + SELECT 1 + FROM pg_proc AS procedure + CROSS JOIN LATERAL aclexplode( + coalesce(procedure.proacl, acldefault('f', procedure.proowner)) + ) AS privilege + WHERE procedure.oid = 'public.apply_position_lifecycle_change(uuid,uuid,uuid,uuid,uuid,text,text,text,uuid,uuid)'::regprocedure + AND privilege.grantee = 0 + AND privilege.privilege_type = 'EXECUTE' +); +SQL +)" +test "$public_execute" = "f" + +echo "position lifecycle snapshot integrity: PASS" From 3738929c1946277dc6fc32fdaa411d3935407b97 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 07:56:03 -0700 Subject: [PATCH 14/23] test(position): add fresh snapshot integrity regression --- .../workflows/position-lifecycle-application-quality.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/position-lifecycle-application-quality.yml b/.github/workflows/position-lifecycle-application-quality.yml index dc0ef9ac4..e2c338d0d 100644 --- a/.github/workflows/position-lifecycle-application-quality.yml +++ b/.github/workflows/position-lifecycle-application-quality.yml @@ -8,8 +8,10 @@ on: paths: - "database/migrations/0023_position_lifecycle_application.sql" - "database/migrations/0024_position_lifecycle_transition_hardening.sql" + - "database/migrations/0025_position_lifecycle_snapshot_hardening.sql" - "tests/test_position_lifecycle_application_postgres.sh" - "tests/test_position_lifecycle_transition_validation_postgres.sh" + - "tests/test_position_lifecycle_snapshot_integrity_postgres.sh" - "docs/adr/0112-position-lifecycle-application.md" - "docs/traceability/position-lifecycle-application.md" - "docs/doctoring/position-lifecycle-application-references.md" @@ -64,11 +66,13 @@ jobs: required = [ "tests/test_position_lifecycle_application_postgres.sh", "tests/test_position_lifecycle_transition_validation_postgres.sh", + "tests/test_position_lifecycle_snapshot_integrity_postgres.sh", ".github/workflows/position-lifecycle-application-quality.yml", ] optional = [ "database/migrations/0023_position_lifecycle_application.sql", "database/migrations/0024_position_lifecycle_transition_hardening.sql", + "database/migrations/0025_position_lifecycle_snapshot_hardening.sql", "docs/adr/0112-position-lifecycle-application.md", "docs/traceability/position-lifecycle-application.md", "docs/doctoring/position-lifecycle-application-references.md", @@ -88,6 +92,8 @@ jobs: run: bash tests/test_position_lifecycle_application_postgres.sh - name: Reject forged lifecycle transition evidence run: bash tests/test_position_lifecycle_transition_validation_postgres.sh + - name: Verify fresh Position and Assignment snapshot evidence + run: bash tests/test_position_lifecycle_snapshot_integrity_postgres.sh - name: Require clean checkout run: | git diff --exit-code From dcb7446e75e1ec3f4a3ce43f32663e88b9558f5b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 08:08:43 -0700 Subject: [PATCH 15/23] fix(position): bind lifecycle application to fresh snapshots --- ..._position_lifecycle_snapshot_hardening.sql | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 database/migrations/0025_position_lifecycle_snapshot_hardening.sql diff --git a/database/migrations/0025_position_lifecycle_snapshot_hardening.sql b/database/migrations/0025_position_lifecycle_snapshot_hardening.sql new file mode 100644 index 000000000..386c51707 --- /dev/null +++ b/database/migrations/0025_position_lifecycle_snapshot_hardening.sql @@ -0,0 +1,217 @@ +-- Harden authoritative Position lifecycle application against stale or forged +-- review snapshots. Review evidence is accepted only when its exact canonical +-- JSON bytes are preserved and its value-minimized Position/Assignment digests +-- still match authoritative bitemporal truth at application time. + +CREATE FUNCTION position_lifecycle_review_canonical_json( + p_review_json text +) +RETURNS text +LANGUAGE plpgsql +IMMUTABLE +STRICT +AS $$ +DECLARE + payload jsonb; + canonical_text text; +BEGIN + BEGIN + payload := p_review_json::jsonb; + EXCEPTION WHEN others THEN + RETURN NULL; + END; + + IF pg_catalog.jsonb_typeof(payload) <> 'object' THEN + RETURN NULL; + END IF; + + SELECT '{' || pg_catalog.string_agg( + pg_catalog.to_json(key_name)::text || ':' || key_value::text, + ',' ORDER BY key_name + ) || '}' + INTO canonical_text + FROM pg_catalog.jsonb_each(payload) AS entry(key_name, key_value); + + RETURN canonical_text; +END; +$$; + +COMMENT ON FUNCTION position_lifecycle_review_canonical_json(text) IS + 'Returns the compact key-sorted JSON object representation required by Position lifecycle review evidence; malformed or non-object input returns NULL.'; + +CREATE FUNCTION position_lifecycle_position_snapshot_digest( + p_tenant_record_id uuid, + p_position_record_id uuid, + p_effective_on date +) +RETURNS text +LANGUAGE sql +STABLE +STRICT +AS $$ + WITH current_snapshot AS ( + SELECT pg_catalog.jsonb_build_object( + 'effective_on', p_effective_on, + 'effective_from', version.effective_from, + 'effective_to', version.effective_to, + 'job_profile_id', position.job_profile_id::text, + 'organization_unit_id', position.organization_unit_id::text, + 'position_record_id', position.position_record_id::text, + 'position_record_version_id', version.position_record_version_id::text, + 'position_status_code', version.position_status_code + ) AS payload + FROM public.position_record AS position + JOIN public.position_record_version AS version + ON version.tenant_record_id = position.tenant_record_id + AND version.position_record_id = position.position_record_id + WHERE position.tenant_record_id = p_tenant_record_id + AND position.position_record_id = p_position_record_id + AND position.recorded_from <= pg_catalog.transaction_timestamp() + AND (position.recorded_to IS NULL OR pg_catalog.transaction_timestamp() < position.recorded_to) + AND version.recorded_from <= pg_catalog.transaction_timestamp() + AND (version.recorded_to IS NULL OR pg_catalog.transaction_timestamp() < version.recorded_to) + AND version.effective_from <= p_effective_on + AND (version.effective_to IS NULL OR p_effective_on < version.effective_to) + ) + SELECT pg_catalog.encode( + public.digest(pg_catalog.convert_to(payload::text, 'UTF8'), 'sha256'), + 'hex' + ) + FROM current_snapshot; +$$; + +COMMENT ON FUNCTION position_lifecycle_position_snapshot_digest(uuid, uuid, date) IS + 'Returns a SHA-256 digest of the exact system-visible Position anchor/version truth at one business-effective date, or NULL when no authoritative Position snapshot exists.'; + +CREATE FUNCTION position_lifecycle_assignment_snapshot_digest( + p_tenant_record_id uuid, + p_position_record_id uuid, + p_effective_on date +) +RETURNS text +LANGUAGE sql +STABLE +STRICT +AS $$ + WITH visible_assignments AS ( + SELECT assignment.assignment_record_id, + assignment.allocation_ratio, + assignment.effective_from, + assignment.effective_to + FROM public.assignment_record AS assignment + WHERE assignment.tenant_record_id = p_tenant_record_id + AND assignment.position_record_id = p_position_record_id + AND assignment.recorded_from <= pg_catalog.transaction_timestamp() + AND (assignment.recorded_to IS NULL OR pg_catalog.transaction_timestamp() < assignment.recorded_to) + AND assignment.effective_from <= p_effective_on + AND (assignment.effective_to IS NULL OR p_effective_on < assignment.effective_to) + ), snapshot AS ( + SELECT pg_catalog.jsonb_build_object( + 'assignments', COALESCE( + pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'allocation_ratio', assignment.allocation_ratio::text, + 'assignment_record_id', assignment.assignment_record_id::text, + 'effective_from', assignment.effective_from, + 'effective_to', assignment.effective_to + ) ORDER BY assignment.assignment_record_id + ), + '[]'::jsonb + ), + 'effective_on', p_effective_on, + 'position_record_id', p_position_record_id::text + ) AS payload + FROM visible_assignments AS assignment + ) + SELECT pg_catalog.encode( + public.digest(pg_catalog.convert_to(payload::text, 'UTF8'), 'sha256'), + 'hex' + ) + FROM snapshot; +$$; + +COMMENT ON FUNCTION position_lifecycle_assignment_snapshot_digest(uuid, uuid, date) IS + 'Returns a SHA-256 digest of value-minimized system-visible Assignment occupancy truth for one Position at one business-effective date; an empty assignment set has a deterministic digest.'; + +-- Preserve the already-reviewed transition/shape validator as an internal stage, +-- then put canonical-byte and fresh-snapshot checks in front of every caller of +-- the public validation contract. +ALTER FUNCTION public.validate_position_lifecycle_review_evidence( + text, text, uuid, uuid, text, text, date +) RENAME TO validate_position_lifecycle_review_evidence_v1_shape; + +CREATE FUNCTION validate_position_lifecycle_review_evidence( + p_canonical_review_json text, + p_review_digest text, + p_tenant_record_id uuid, + p_position_record_id uuid, + p_expected_current_status text, + p_expected_proposed_status text, + p_effective_on date +) +RETURNS boolean +LANGUAGE plpgsql +STABLE +STRICT +AS $$ +DECLARE + review_payload jsonb; + canonical_review text; + expected_position_digest text; + expected_assignment_digest text; +BEGIN + canonical_review := public.position_lifecycle_review_canonical_json( + p_canonical_review_json + ); + IF canonical_review IS NULL + OR canonical_review <> p_canonical_review_json THEN + RETURN false; + END IF; + + IF public.validate_position_lifecycle_review_evidence_v1_shape( + p_canonical_review_json, + p_review_digest, + p_tenant_record_id, + p_position_record_id, + p_expected_current_status, + p_expected_proposed_status, + p_effective_on + ) IS NOT TRUE THEN + RETURN false; + END IF; + + review_payload := p_canonical_review_json::jsonb; + expected_position_digest := public.position_lifecycle_position_snapshot_digest( + p_tenant_record_id, + p_position_record_id, + p_effective_on + ); + expected_assignment_digest := public.position_lifecycle_assignment_snapshot_digest( + p_tenant_record_id, + p_position_record_id, + p_effective_on + ); + + IF expected_position_digest IS NULL + OR review_payload ->> 'position_snapshot_digest_sha256' + IS DISTINCT FROM expected_position_digest + OR review_payload ->> 'assignment_snapshot_digest_sha256' + IS DISTINCT FROM expected_assignment_digest THEN + RETURN false; + END IF; + + RETURN true; +END; +$$; + +COMMENT ON FUNCTION validate_position_lifecycle_review_evidence(text, text, uuid, uuid, text, text, date) IS + 'Validates exact canonical v1 review bytes, digest, tenant/Position/status/effective scope, allowed transition graph, human-review chronology, and fresh authoritative Position/Assignment snapshot digests without granting mutation authority.'; + +COMMENT ON FUNCTION validate_position_lifecycle_review_evidence_v1_shape(text, text, uuid, uuid, text, text, date) IS + 'Internal v1 review shape/transition validator retained beneath the canonical-byte and fresh-snapshot validation boundary.'; + +-- High-impact lifecycle mutation must never inherit PostgreSQL default PUBLIC +-- function execution. Deployment grants are an explicit authority decision. +REVOKE ALL ON FUNCTION public.apply_position_lifecycle_change( + uuid, uuid, uuid, uuid, uuid, text, text, text, uuid, uuid +) FROM PUBLIC; From abce10e82ea2ed253110dd4de7af2b56176e34d6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 08:09:28 -0700 Subject: [PATCH 16/23] test(position): exercise snapshot hardening migration --- ...n_lifecycle_snapshot_integrity_postgres.sh | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/tests/test_position_lifecycle_snapshot_integrity_postgres.sh b/tests/test_position_lifecycle_snapshot_integrity_postgres.sh index 973377783..17f3cd76f 100644 --- a/tests/test_position_lifecycle_snapshot_integrity_postgres.sh +++ b/tests/test_position_lifecycle_snapshot_integrity_postgres.sh @@ -3,6 +3,10 @@ set -euo pipefail : "${DATABASE_URL:?DATABASE_URL is required}" +migration="database/migrations/0025_position_lifecycle_snapshot_hardening.sql" +test -f "$migration" +psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f "$migration" >/dev/null + TENANT="0198a412-8000-7000-8000-000000000101" POSITION="0198a412-8000-7000-8000-000000000104" CURRENT_VERSION="0198a412-8000-7000-8000-000000000106" @@ -16,7 +20,7 @@ REVIEWER="actor:6aacb560-ec5d-41d7-94a5-27cf95438b1b" APPLIER="actor:faefac04-52e8-43d2-aa54-d9046238733f" # The authoritative database snapshot digests must be available to the review -# producer and to the application guard. Their absence is itself fail-closed. +# producer and to the application guard. Their absence is itself fail-closed. POSITION_DIGEST="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 -v tenant="$TENANT" -v position="$POSITION" <<'SQL' SELECT public.position_lifecycle_position_snapshot_digest( :'tenant', :'position', DATE '2026-09-15' @@ -62,11 +66,22 @@ PY REVIEW_DIGEST="$(printf '%s' "$REVIEW_JSON" | sha256sum | awk '{print $1}')" canonicalized="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 -v review_json="$REVIEW_JSON" <<'SQL' -SELECT public.position_lifecycle_review_canonical_json(:'review_json'::jsonb); +SELECT public.position_lifecycle_review_canonical_json(:'review_json'); SQL )" test "$canonicalized" = "$REVIEW_JSON" +valid_accepted="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 \ + -v review_json="$REVIEW_JSON" -v review_digest="$REVIEW_DIGEST" \ + -v tenant="$TENANT" -v position="$POSITION" <<'SQL' +SELECT public.validate_position_lifecycle_review_evidence( + :'review_json', :'review_digest', :'tenant', :'position', + 'frozen', 'open', DATE '2026-09-15' +); +SQL +)" +test "$valid_accepted" = "t" + # A semantically equivalent but noncanonical representation must not become # durable review evidence merely because a caller recomputed its SHA-256. NONCANONICAL_JSON="$(printf '%s' "$REVIEW_JSON" | python -c 'import json,sys; print(json.dumps(json.load(sys.stdin), sort_keys=False, indent=1, ensure_ascii=False))')" @@ -100,6 +115,30 @@ then exit 1 fi +# The exact fresh snapshot evidence must still be usable through the authoritative +# application boundary after the forged attempt rolls back atomically. +psql "$DATABASE_URL" -v ON_ERROR_STOP=1 \ + -v tenant="$TENANT" -v position="$POSITION" -v current_version="$CURRENT_VERSION" \ + -v successor="$SUCCESSOR" -v application="$APPLICATION" \ + -v review_json="$REVIEW_JSON" -v review_digest="$REVIEW_DIGEST" \ + -v applier="$APPLIER" -v audit="$AUDIT" -v outbox="$OUTBOX" <<'SQL' >/dev/null +SELECT public.apply_position_lifecycle_change( + :'tenant', :'position', :'current_version', :'successor', :'application', + :'review_json', :'review_digest', :'applier', :'audit', :'outbox' +); +SQL + +applied_status="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 \ + -v tenant="$TENANT" -v successor="$SUCCESSOR" <<'SQL' +SELECT position_status_code +FROM position_record_version +WHERE tenant_record_id = :'tenant' + AND position_record_version_id = :'successor' + AND recorded_to IS NULL; +SQL +)" +test "$applied_status" = "open" + # The high-impact mutation function must never be executable through PostgreSQL's # default PUBLIC function privilege; deployment must grant it deliberately. public_execute="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 <<'SQL' From db4905b83e167967588c0bd63fc873338e6b4bf4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 08:10:36 -0700 Subject: [PATCH 17/23] docs(position): record snapshot integrity boundary --- docs/adr/0112-position-lifecycle-application.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/adr/0112-position-lifecycle-application.md b/docs/adr/0112-position-lifecycle-application.md index 587565d87..c1953e9d7 100644 --- a/docs/adr/0112-position-lifecycle-application.md +++ b/docs/adr/0112-position-lifecycle-application.md @@ -6,28 +6,35 @@ Status: Proposed PR #111 adds a human-reviewed, deliberately non-authorizing `PositionLifecycleChangeReviewPacket`. Protected `develop` already separates stable `position_record` identity from bitemporal `position_record_version`, and Assignment truth separately represents worker occupancy. A commercial HRIS still needs an authoritative boundary that can turn an approved lifecycle proposal into Position truth without treating stale UI state or the review artifact itself as authority. -Closing or abolishing an occupied Position is high-impact because it can make staffing truth internally inconsistent. Rewriting a PositionVersion in place would also destroy what the system previously knew. +Closing or abolishing an occupied Position is high-impact because it can make staffing truth internally inconsistent. Rewriting a PositionVersion in place would also destroy what the system previously knew. The review packet carries value-minimized Position and Assignment snapshot digests, so the application boundary must prove those digests still describe authoritative truth rather than merely checking that they look like SHA-256 values. ## Decision Add an Orgmetra-owned Position lifecycle application boundary. It consumes exact v1 canonical review evidence, revalidates its SHA-256 and governed shape, locks the tenant-qualified Position, resolves the exact PositionVersion covering the requested business-effective date at current system time, rejects stale reviewed status, and checks current Assignment occupancy before `closed` or `abolished` transitions. +The authoritative boundary independently canonicalizes the review JSON as compact key-sorted object bytes and rejects semantically equivalent but differently encoded input. It recomputes a value-minimized Position snapshot digest from the current Position anchor plus the system-visible PositionVersion at the reviewed effective date, and a deterministic Assignment-occupancy digest from the system-visible assignments at that date. Both must exactly match the reviewed digests before mutation. Missing authoritative Position truth fails closed; an empty Assignment set has a deterministic digest. This prevents a caller from substituting arbitrary syntactically valid hashes or replaying a stale review after relevant Position/Assignment truth changes. + Application closes only the predecessor system-recorded interval at PostgreSQL transaction time. When the reviewed effective date splits an existing business-effective interval, it inserts a preserved predecessor segment plus the reviewed successor segment at the new system-recorded time. The stable Position identity is unchanged. One immutable `position_lifecycle_application_record` binds predecessor/successor identities, exact review bytes/digest, requester/reviewer/applier separation, lifecycle reason, human-review chronology, and the audit/outbox identities. The audit/outbox event is created in the same transaction and must match Position subject, purpose, reason, review digest, applier, high-impact result, and human-confirmation reference. Application evidence and PositionVersion history reject UPDATE/DELETE rewrite; tenant-scoped application evidence uses forced row-level security. +The high-impact `apply_position_lifecycle_change(...)` function does not retain PostgreSQL's default `PUBLIC EXECUTE` privilege. Production execution must therefore be granted deliberately to the intended application role; this database privilege is defense in depth and does not replace purpose-bound application authorization. + ## Consequences - Business-effective time and system-recorded time remain independent and reconstructable. -- A review packet never authorizes mutation by itself; current Position and Assignment truth is re-resolved at application time. +- A review packet never authorizes mutation by itself; current Position and Assignment truth is re-resolved and cryptographically compared with the reviewed snapshots at application time. +- Canonical review bytes are part of the governed contract; recomputing a digest over a differently encoded but semantically equivalent document does not create acceptable evidence. - `closed` and `abolished` fail closed while any current Assignment overlaps the requested effective date or later. - Existing Assignment, reporting-line, compensation, assessment, and Person facts remain separate; no cross-service table SQL is introduced. - The branch is a dependency-first descendant of #111 and remains Draft until the parent integrates and fresh post-restack gates pass. -- Direct production database privileges remain a deployment concern; this slice does not claim that PostgreSQL row-level security replaces application authorization. +- Direct production database privileges remain a deployment concern; forced row-level security and revoked default function execution do not replace application authorization. ## Alternatives rejected 1. **Overwrite `position_record_version.position_status_code`.** Rejected because it destroys system-time history. 2. **Create a new Position identity for each lifecycle change.** Rejected because Job/Position/Assignment semantics require a stable Position anchor with versioned state. 3. **Apply the review without refreshing Assignment truth.** Rejected because a reviewed closure can become stale before application and strand active staffing evidence. -4. **Move lifecycle state into reporting or vacancy tables.** Rejected because those relations own different facts and would violate the Job/Position/Assignment model. \ No newline at end of file +4. **Trust caller-supplied snapshot hashes after syntax validation.** Rejected because a caller could forge or replay otherwise well-shaped evidence without proving it still matches authoritative bitemporal truth. +5. **Leave `apply_position_lifecycle_change(...)` executable by PostgreSQL `PUBLIC`.** Rejected because a high-impact employment-data mutation boundary should require an explicit deployment grant. +6. **Move lifecycle state into reporting or vacancy tables.** Rejected because those relations own different facts and would violate the Job/Position/Assignment model. From 0f1aa47bab64805826f73c849319a3be4e85a8d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 08:10:55 -0700 Subject: [PATCH 18/23] docs(position): trace fresh snapshot enforcement --- docs/traceability/position-lifecycle-application.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/traceability/position-lifecycle-application.md b/docs/traceability/position-lifecycle-application.md index 7e288a7fe..66dd78b6c 100644 --- a/docs/traceability/position-lifecycle-application.md +++ b/docs/traceability/position-lifecycle-application.md @@ -4,7 +4,7 @@ - **Protected-main truth:** `develop@9e3e4847510e1e612b48474ba42b177b8ed824df` has stable Position anchors, bitemporal PositionVersion facts, Assignment occupancy, tenant RLS, and immutable audit/outbox foundations, but no authoritative existing-Position lifecycle mutation. - **Dependency-active truth:** PR #111 adds only reviewed lifecycle evidence and remains a separate review boundary. -- **Active PR truth:** this stacked branch applies an approved v1 review to locked current Position/Assignment truth and records the resulting bitemporal PositionVersion plus immutable application/audit/outbox correlation. +- **Active PR truth:** this stacked branch applies an approved v1 review to locked current Position/Assignment truth, requires the exact reviewed snapshot digests to match freshly recomputed authoritative truth, and records the resulting bitemporal PositionVersion plus immutable application/audit/outbox correlation. - **Planned after parent integration:** retarget to fresh `develop`, reconcile migration/repository inventories, and rerun all applicable Foundation, Security, SAST, Recovery, People and lifecycle-application gates on one exact head. - **Out of scope:** Person/candidate data, compensation, assessment/rating data, reporting-line changes, autonomous employment decisions, foreign repository writes, and direct foreign application-table SQL. @@ -15,6 +15,9 @@ | Stable Position identity with versioned lifecycle state | existing `position_record`; `position_record_version`; ADR 0112 | | Separate business and system time | reviewed `effective_on`; predecessor split; PostgreSQL `transaction_timestamp()` for new system-time truth | | Stale-review defense | locked current Position and exact predecessor/status comparison before application | +| Fresh Position snapshot binding | `position_lifecycle_position_snapshot_digest(...)` hashes the current anchor + system-visible PositionVersion at the reviewed business date; reviewed digest must match exactly | +| Fresh Assignment snapshot binding | `position_lifecycle_assignment_snapshot_digest(...)` deterministically hashes value-minimized current occupancy at the reviewed business date; reviewed digest must match exactly | +| Canonical review bytes | `position_lifecycle_review_canonical_json(...)` produces compact key-sorted object bytes; the validator rejects semantically equivalent noncanonical encodings even with a recomputed SHA-256 | | Staffing safety | current Assignment overlap check blocks `closed`/`abolished` transitions | | Human review remains non-authorizing | exact v1 review state must be `approved_for_authoritative_resolution` while application independently re-resolves live truth | | Actor separation | requester/reviewer from review plus distinct application actor | @@ -22,9 +25,10 @@ | Immutable audit/outbox | same-transaction `record_audit_outbox_event`; application trigger binds event subject/actor/purpose/reason/evidence/result/confirmation and outbox identity | | Correction, not rewrite | PositionVersion history trigger permits only closing an open recorded interval at transaction time; application evidence is append-only | | Tenant isolation | tenant-qualified FKs and forced-RLS application relation; existing PositionVersion forced-RLS remains intact | -| Value minimization | no Person/candidate identity, pay, rating, assessment, allocation value, prompt/model output, or free-form HR text in application evidence | -| Exact-head quality | dedicated PostgreSQL regression and pinned exact-checkout workflow | +| Explicit mutation privilege | migration 0025 revokes PostgreSQL default `PUBLIC EXECUTE` from `apply_position_lifecycle_change(...)`; intended application-role execution must be granted deliberately | +| Value minimization | no Person/candidate identity, pay, rating, assessment, prompt/model output, or free-form HR text in application evidence; Assignment snapshot evidence is stored only as SHA-256 | +| Exact-head quality | dedicated PostgreSQL regression applies migration 0025, proves positive fresh-snapshot application, rejects forged/noncanonical review evidence, checks default PUBLIC execution is absent, and runs under a pinned exact-checkout workflow | ## Buyer behavior -An HR operator can review a Position freeze, closure, abolition, or reactivation in #111. This branch does not trust that review as current truth: at application time it reopens the exact tenant/Position state, confirms the reviewed predecessor still covers the business date, prevents closure/abolition across live staffing, then writes correction-preserving PositionVersion truth with immutable audit/outbox. If any scope, status, evidence, chronology, actor-separation, or staffing condition changed, the application fails closed and the buyer must review the fresh state rather than silently applying stale intent. \ No newline at end of file +An HR operator can review a Position freeze, closure, abolition, or reactivation in #111. This branch does not trust that review as current truth: at application time it reopens the exact tenant/Position state, confirms the reviewed predecessor still covers the business date, recomputes the reviewed Position and Assignment snapshot digests, prevents closure/abolition across live staffing, then writes correction-preserving PositionVersion truth with immutable audit/outbox. If any scope, status, canonical evidence, reviewed snapshot, chronology, actor-separation, or staffing condition changed, the application fails closed and the buyer must review the fresh state rather than silently applying stale intent. From e546bcf4e20b0442196c412d7976a45a4caa7efe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 09:13:34 -0700 Subject: [PATCH 19/23] fix(position): make lifecycle review key validation locale-stable --- .../0024_position_lifecycle_transition_hardening.sql | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/database/migrations/0024_position_lifecycle_transition_hardening.sql b/database/migrations/0024_position_lifecycle_transition_hardening.sql index 9627d158b..e914ad763 100644 --- a/database/migrations/0024_position_lifecycle_transition_hardening.sql +++ b/database/migrations/0024_position_lifecycle_transition_hardening.sql @@ -66,7 +66,11 @@ BEGIN RETURN false; END IF; - SELECT count(*), array_agg(key ORDER BY key) + -- Key-shape validation must not depend on the database's locale collation. + -- The v1 contract is ASCII field names in deterministic C order; otherwise + -- en_US collation can reorder underscore-bearing keys and reject valid review + -- evidence even though the key set is exact. + SELECT count(*), array_agg(key ORDER BY key COLLATE "C") INTO key_count, review_keys FROM pg_catalog.json_object_keys(review_json) AS key_set(key); IF key_count <> 20 OR review_keys <> expected_keys THEN From f478cf489b56f53aaeed7314067493f5879e4429 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 09:14:12 -0700 Subject: [PATCH 20/23] test(position): exercise transition-hardened lifecycle validator --- tests/test_position_lifecycle_application_postgres.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_position_lifecycle_application_postgres.sh b/tests/test_position_lifecycle_application_postgres.sh index 37398b4d8..888578df2 100644 --- a/tests/test_position_lifecycle_application_postgres.sh +++ b/tests/test_position_lifecycle_application_postgres.sh @@ -10,6 +10,9 @@ psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f database/migrations/0001_foundation_s psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f database/migrations/0002_sealed_evidence_digest.sql >/dev/null psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f database/migrations/0003_audit_outbox_persistence.sql >/dev/null psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f "$migration" >/dev/null +# Exercise the integrated transition-hardened v1 validator. Migration 0024 +# replaces the predecessor validator from 0023 before any application call. +psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f database/migrations/0024_position_lifecycle_transition_hardening.sql >/dev/null TENANT="0198a412-8000-7000-8000-000000000101" ORG="0198a412-8000-7000-8000-000000000102" From 4f2a003769bf8f773559ac8122702f1451f0e8c0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 10:01:58 -0700 Subject: [PATCH 21/23] fix(position): make lifecycle canonical JSON locale-stable --- .../migrations/0025_position_lifecycle_snapshot_hardening.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/database/migrations/0025_position_lifecycle_snapshot_hardening.sql b/database/migrations/0025_position_lifecycle_snapshot_hardening.sql index 386c51707..5ce0c4096 100644 --- a/database/migrations/0025_position_lifecycle_snapshot_hardening.sql +++ b/database/migrations/0025_position_lifecycle_snapshot_hardening.sql @@ -27,7 +27,7 @@ BEGIN SELECT '{' || pg_catalog.string_agg( pg_catalog.to_json(key_name)::text || ':' || key_value::text, - ',' ORDER BY key_name + ',' ORDER BY key_name COLLATE "C" ) || '}' INTO canonical_text FROM pg_catalog.jsonb_each(payload) AS entry(key_name, key_value); @@ -37,7 +37,7 @@ END; $$; COMMENT ON FUNCTION position_lifecycle_review_canonical_json(text) IS - 'Returns the compact key-sorted JSON object representation required by Position lifecycle review evidence; malformed or non-object input returns NULL.'; + 'Returns the compact C-collation key-sorted JSON object representation required by Position lifecycle review evidence; malformed or non-object input returns NULL.'; CREATE FUNCTION position_lifecycle_position_snapshot_digest( p_tenant_record_id uuid, From 260f95243e2679515bde8d3e3c0bac944030a2c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 00:10:44 +0900 Subject: [PATCH 22/23] fix(position): harden lifecycle application boundary --- CHANGELOG.md | 1 + README.md | 2 + .../0023_position_lifecycle_application.sql | 202 ++++++++++++++---- ...osition_lifecycle_transition_hardening.sql | 11 +- ..._position_lifecycle_snapshot_hardening.sql | 28 ++- docs/DATA_MODEL.md | 3 + docs/ERD.md | 4 + docs/OPERABILITY.md | 6 + docs/SECURITY.md | 2 + docs/TEST_STRATEGY.md | 1 + docs/THREAT_MODEL.md | 1 + docs/TRACEABILITY.md | 1 + docs/adr/README.md | 1 + manifest.json | 2 +- scripts/foundation-contract-core.mjs | 16 +- tests/foundation-contract.test.mjs | 5 + ...position_lifecycle_application_postgres.sh | 27 +++ ...n_lifecycle_snapshot_integrity_postgres.sh | 21 ++ tests/validate_repository.py | 10 + 19 files changed, 294 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99f4752d7..31d069e24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to Orgmetra will be documented in this file. ### Added +- Active-PR governed Position lifecycle application: migrations `0023_position_lifecycle_application.sql`, `0024_position_lifecycle_transition_hardening.sql`, and `0025_position_lifecycle_snapshot_hardening.sql` apply only independently reviewed, canonical v1 evidence whose fresh bitemporal Position and Assignment snapshots still match; occupied closures/abolitions fail closed, PositionVersion history and application evidence remain append-only, and one atomic audit/outbox event records the high-impact mutation. PostgreSQL function search paths, tenant-qualified version bindings, forced RLS, truncation guards, explicit mutation privileges, and exact PostgreSQL regressions are part of the boundary; the review remains non-authorizing and the branch remains active-PR truth until protected gates pass. - 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. diff --git a/README.md b/README.md index 47bb087a3..2fd6c456a 100644 --- a/README.md +++ b/README.md @@ -79,3 +79,5 @@ Job evidence ## Status Protected `develop` includes the employment-truth kernel, governed candidate-to-worker conversion, purpose-bound PII authorization, normalized worker-bound validity studies, criterion-observation scope, bitemporal workforce-composition evidence, the governed Naruon intent adapter, and requisition review packets. This active PR adds durable purpose-bound People mutation and confirmed-hire materialization paths for Employment, Position, and Assignment with atomic audit/outbox evidence and tenant-scoped idempotency; treat those write paths as active-PR truth until this exact head passes all fresh protected-base gates and merges. + +The stacked Position lifecycle application branch adds a separate high-impact database boundary: only canonical human-reviewed lifecycle evidence whose fresh tenant-scoped Position and Assignment snapshots still match may produce an immutable bitemporal PositionVersion correction and atomic audit/outbox evidence. It remains active-PR truth until its parent review boundary is integrated and fresh protected gates pass. diff --git a/database/migrations/0023_position_lifecycle_application.sql b/database/migrations/0023_position_lifecycle_application.sql index c8ad15669..c254d93a3 100644 --- a/database/migrations/0023_position_lifecycle_application.sql +++ b/database/migrations/0023_position_lifecycle_application.sql @@ -3,12 +3,19 @@ -- candidate, compensation, assessment, rating, and free-form HR values are not -- copied into this relation. +BEGIN; + +SET LOCAL search_path = public, pg_catalog; + +ALTER TABLE public.position_record_version + ADD CONSTRAINT position_version_lifecycle_scope_unique + UNIQUE (tenant_record_id, position_record_id, position_record_version_id); + CREATE TABLE position_lifecycle_application_record ( - tenant_record_id uuid NOT NULL REFERENCES tenant_record(tenant_record_id), + tenant_record_id uuid NOT NULL REFERENCES public.tenant_record(tenant_record_id), position_lifecycle_application_record_id uuid PRIMARY KEY, position_record_id uuid NOT NULL, - predecessor_position_record_version_id uuid NOT NULL - REFERENCES position_record_version(position_record_version_id), + predecessor_position_record_version_id uuid NOT NULL, successor_position_record_version_id uuid NOT NULL, position_lifecycle_change_reference uuid NOT NULL, canonical_review_json text NOT NULL, @@ -38,13 +45,27 @@ CREATE TABLE position_lifecycle_application_record ( ), CONSTRAINT position_lifecycle_position_tenant_fk FOREIGN KEY (tenant_record_id, position_record_id) - REFERENCES position_record(tenant_record_id, position_record_id), + REFERENCES public.position_record(tenant_record_id, position_record_id), + CONSTRAINT position_lifecycle_predecessor_scope_fk + FOREIGN KEY (tenant_record_id, position_record_id, predecessor_position_record_version_id) + REFERENCES public.position_record_version( + tenant_record_id, + position_record_id, + position_record_version_id + ), + CONSTRAINT position_lifecycle_successor_scope_fk + FOREIGN KEY (tenant_record_id, position_record_id, successor_position_record_version_id) + REFERENCES public.position_record_version( + tenant_record_id, + position_record_id, + position_record_version_id + ) DEFERRABLE INITIALLY DEFERRED, CONSTRAINT position_lifecycle_audit_tenant_fk FOREIGN KEY (tenant_record_id, audit_event_record_id) - REFERENCES audit_event_record(tenant_record_id, audit_event_record_id), + REFERENCES public.audit_event_record(tenant_record_id, audit_event_record_id), CONSTRAINT position_lifecycle_outbox_tenant_fk FOREIGN KEY (tenant_record_id, outbox_delivery_record_id) - REFERENCES outbox_delivery_record(tenant_record_id, outbox_delivery_record_id), + REFERENCES public.outbox_delivery_record(tenant_record_id, outbox_delivery_record_id), CONSTRAINT position_lifecycle_review_digest_check CHECK (review_evidence_digest_sha256 ~ '^[0-9a-f]{64}$'), CONSTRAINT position_lifecycle_requester_actor_check @@ -94,18 +115,18 @@ CREATE TABLE position_lifecycle_application_record ( COMMENT ON TABLE position_lifecycle_application_record IS 'Immutable application evidence linking one reviewed Position lifecycle proposal to one authoritative successor PositionVersion and atomic audit/outbox evidence.'; -ALTER TABLE position_record_version +ALTER TABLE public.position_record_version ADD COLUMN position_lifecycle_application_record_id uuid; -ALTER TABLE position_record_version +ALTER TABLE public.position_record_version ADD CONSTRAINT position_version_lifecycle_application_tenant_fk FOREIGN KEY (tenant_record_id, position_lifecycle_application_record_id) - REFERENCES position_lifecycle_application_record( + REFERENCES public.position_lifecycle_application_record( tenant_record_id, position_lifecycle_application_record_id ); -CREATE FUNCTION validate_position_lifecycle_review_evidence( +CREATE FUNCTION public.validate_position_lifecycle_review_evidence( p_canonical_review_json text, p_review_digest text, p_tenant_record_id uuid, @@ -118,6 +139,7 @@ RETURNS boolean LANGUAGE plpgsql STABLE STRICT +SET search_path = pg_catalog, public, pg_temp AS $$ DECLARE review_json json; @@ -229,12 +251,13 @@ BEGIN END; $$; -COMMENT ON FUNCTION validate_position_lifecycle_review_evidence(text, text, uuid, uuid, text, text, date) IS +COMMENT ON FUNCTION public.validate_position_lifecycle_review_evidence(text, text, uuid, uuid, text, text, date) IS 'Validates exact v1 Position lifecycle review shape, digest, tenant/Position/status/effective scope, human approval state, and chronology without granting mutation authority.'; -CREATE FUNCTION protect_position_lifecycle_application_history() +CREATE FUNCTION public.protect_position_lifecycle_application_history() RETURNS trigger LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp AS $$ BEGIN RAISE EXCEPTION 'position lifecycle application evidence is append-only' @@ -243,13 +266,14 @@ END; $$; CREATE TRIGGER position_lifecycle_application_append_only_guard -BEFORE UPDATE OR DELETE ON position_lifecycle_application_record +BEFORE UPDATE OR DELETE ON public.position_lifecycle_application_record FOR EACH ROW -EXECUTE FUNCTION protect_position_lifecycle_application_history(); +EXECUTE FUNCTION public.protect_position_lifecycle_application_history(); -CREATE FUNCTION protect_position_version_history_after_lifecycle_support() +CREATE FUNCTION public.protect_position_version_history_after_lifecycle_support() RETURNS trigger LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp AS $$ BEGIN IF TG_OP = 'DELETE' THEN @@ -267,20 +291,22 @@ BEGIN END; $$; -COMMENT ON FUNCTION protect_position_version_history_after_lifecycle_support() IS +COMMENT ON FUNCTION public.protect_position_version_history_after_lifecycle_support() IS 'Prevents delete/in-place rewrite of PositionVersion facts; an open system-recorded interval may only be closed at PostgreSQL transaction time.'; CREATE TRIGGER position_version_lifecycle_history_guard -BEFORE UPDATE OR DELETE ON position_record_version +BEFORE UPDATE OR DELETE ON public.position_record_version FOR EACH ROW -EXECUTE FUNCTION protect_position_version_history_after_lifecycle_support(); +EXECUTE FUNCTION public.protect_position_version_history_after_lifecycle_support(); -CREATE FUNCTION validate_position_lifecycle_application_audit() +CREATE FUNCTION public.validate_position_lifecycle_application_audit() RETURNS trigger LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp AS $$ DECLARE audit_payload jsonb; + review_payload jsonb; outbox_audit_id uuid; BEGIN IF NEW.recorded_at IS DISTINCT FROM pg_catalog.transaction_timestamp() THEN @@ -300,9 +326,49 @@ BEGIN USING ERRCODE = '23514'; END IF; + review_payload := NEW.canonical_review_json::jsonb; + IF NEW.position_lifecycle_change_reference IS DISTINCT FROM + (review_payload ->> 'position_lifecycle_change_reference')::uuid + OR NEW.requester_actor_reference IS DISTINCT FROM + review_payload ->> 'requester_actor_reference' + OR NEW.reviewer_actor_reference IS DISTINCT FROM + review_payload ->> 'reviewer_actor_reference' + OR NEW.current_status_code IS DISTINCT FROM + review_payload ->> 'current_status_code' + OR NEW.proposed_status_code IS DISTINCT FROM + review_payload ->> 'proposed_status_code' + OR NEW.reason_code IS DISTINCT FROM review_payload ->> 'reason_code' + OR NEW.effective_on IS DISTINCT FROM + (review_payload ->> 'effective_on')::date + OR NEW.reviewed_at IS DISTINCT FROM + (review_payload ->> 'reviewed_at')::timestamptz + OR NEW.review_packet_recorded_at IS DISTINCT FROM + (review_payload ->> 'recorded_at')::timestamptz THEN + RAISE EXCEPTION 'position lifecycle application row does not match reviewed evidence' + USING ERRCODE = '23514'; + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM public.position_record_version AS predecessor + WHERE predecessor.tenant_record_id = NEW.tenant_record_id + AND predecessor.position_record_id = NEW.position_record_id + AND predecessor.position_record_version_id = NEW.predecessor_position_record_version_id + AND predecessor.position_status_code = NEW.current_status_code + AND predecessor.recorded_from <= pg_catalog.transaction_timestamp() + AND (predecessor.recorded_to IS NULL + OR pg_catalog.transaction_timestamp() < predecessor.recorded_to) + AND predecessor.effective_from <= NEW.effective_on + AND (predecessor.effective_to IS NULL + OR NEW.effective_on < predecessor.effective_to) + ) THEN + RAISE EXCEPTION 'position lifecycle predecessor is not current at the reviewed effective date' + USING ERRCODE = '23514'; + END IF; + audit_payload := ( SELECT canonical_event_json::jsonb - FROM audit_event_record + FROM public.audit_event_record WHERE tenant_record_id = NEW.tenant_record_id AND audit_event_record_id = NEW.audit_event_record_id ); @@ -322,7 +388,7 @@ BEGIN SELECT audit_event_record_id INTO outbox_audit_id - FROM outbox_delivery_record + FROM public.outbox_delivery_record WHERE tenant_record_id = NEW.tenant_record_id AND outbox_delivery_record_id = NEW.outbox_delivery_record_id; IF outbox_audit_id IS DISTINCT FROM NEW.audit_event_record_id THEN @@ -335,11 +401,68 @@ END; $$; CREATE TRIGGER position_lifecycle_application_integrity_guard -BEFORE INSERT ON position_lifecycle_application_record +BEFORE INSERT ON public.position_lifecycle_application_record FOR EACH ROW -EXECUTE FUNCTION validate_position_lifecycle_application_audit(); +EXECUTE FUNCTION public.validate_position_lifecycle_application_audit(); -CREATE FUNCTION apply_position_lifecycle_change( +CREATE FUNCTION public.validate_position_lifecycle_application_successor() +RETURNS trigger +LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp +AS $$ +DECLARE + successor public.position_record_version%ROWTYPE; +BEGIN + SELECT version.* + INTO successor + FROM public.position_record_version AS version + WHERE version.tenant_record_id = NEW.tenant_record_id + AND version.position_record_id = NEW.position_record_id + AND version.position_record_version_id = NEW.successor_position_record_version_id; + IF NOT FOUND + OR successor.position_status_code <> NEW.proposed_status_code + OR successor.effective_from <> NEW.effective_on + OR successor.recorded_from IS DISTINCT FROM pg_catalog.transaction_timestamp() + OR successor.recorded_to IS NOT NULL + OR successor.position_lifecycle_application_record_id + IS DISTINCT FROM NEW.position_lifecycle_application_record_id THEN + RAISE EXCEPTION 'position lifecycle successor does not match the application evidence' + USING ERRCODE = '23514'; + END IF; + RETURN NEW; +END; +$$; + +CREATE CONSTRAINT TRIGGER position_lifecycle_application_successor_guard +AFTER INSERT ON public.position_lifecycle_application_record +DEFERRABLE INITIALLY DEFERRED +FOR EACH ROW +EXECUTE FUNCTION public.validate_position_lifecycle_application_successor(); + +CREATE FUNCTION public.reject_position_lifecycle_history_truncate() +RETURNS trigger +LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp +AS $$ +BEGIN + RAISE EXCEPTION 'Position lifecycle history cannot be truncated' + USING ERRCODE = '55000'; +END; +$$; + +CREATE TRIGGER position_lifecycle_application_truncate_guard +BEFORE TRUNCATE ON public.position_lifecycle_application_record +FOR EACH STATEMENT +EXECUTE FUNCTION public.reject_position_lifecycle_history_truncate(); + +CREATE TRIGGER position_record_version_lifecycle_truncate_guard +BEFORE TRUNCATE ON public.position_record_version +FOR EACH STATEMENT +EXECUTE FUNCTION public.reject_position_lifecycle_history_truncate(); + +REVOKE TRUNCATE ON public.position_lifecycle_application_record, public.position_record_version FROM PUBLIC; + +CREATE FUNCTION public.apply_position_lifecycle_change( p_tenant_record_id uuid, p_position_record_id uuid, p_expected_predecessor_version_id uuid, @@ -353,10 +476,11 @@ CREATE FUNCTION apply_position_lifecycle_change( ) RETURNS void LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp AS $$ DECLARE review_payload jsonb; - predecessor position_record_version%ROWTYPE; + predecessor public.position_record_version%ROWTYPE; preserved_version_id uuid; event_json text; event_digest text; @@ -403,7 +527,7 @@ BEGIN END IF; PERFORM 1 - FROM position_record + FROM public.position_record WHERE tenant_record_id = p_tenant_record_id AND position_record_id = p_position_record_id AND recorded_from <= pg_catalog.transaction_timestamp() @@ -416,7 +540,7 @@ BEGIN SELECT version.* INTO predecessor - FROM position_record_version AS version + FROM public.position_record_version AS version WHERE version.tenant_record_id = p_tenant_record_id AND version.position_record_id = p_position_record_id AND version.recorded_from <= pg_catalog.transaction_timestamp() @@ -450,7 +574,7 @@ BEGIN IF proposed_status IN ('closed', 'abolished') AND EXISTS ( SELECT 1 - FROM assignment_record AS assignment + FROM public.assignment_record AS assignment WHERE assignment.tenant_record_id = p_tenant_record_id AND assignment.position_record_id = p_position_record_id AND assignment.recorded_from <= pg_catalog.transaction_timestamp() @@ -497,7 +621,7 @@ BEGIN 'orgmetra_domain_events' ); - INSERT INTO position_lifecycle_application_record ( + INSERT INTO public.position_lifecycle_application_record ( tenant_record_id, position_lifecycle_application_record_id, position_record_id, @@ -539,14 +663,14 @@ BEGIN p_outbox_delivery_record_id ); - UPDATE position_record_version + UPDATE public.position_record_version SET recorded_to = pg_catalog.transaction_timestamp() WHERE tenant_record_id = p_tenant_record_id AND position_record_version_id = predecessor.position_record_version_id; IF predecessor.effective_from < effective_on THEN preserved_version_id := pg_catalog.gen_random_uuid(); - INSERT INTO position_record_version ( + INSERT INTO public.position_record_version ( tenant_record_id, position_record_version_id, position_record_id, @@ -567,7 +691,7 @@ BEGIN ); END IF; - INSERT INTO position_record_version ( + INSERT INTO public.position_record_version ( tenant_record_id, position_record_version_id, position_record_id, @@ -589,12 +713,18 @@ BEGIN END; $$; -COMMENT ON FUNCTION apply_position_lifecycle_change(uuid, uuid, uuid, uuid, uuid, text, text, text, uuid, uuid) IS +COMMENT ON FUNCTION public.apply_position_lifecycle_change(uuid, uuid, uuid, uuid, uuid, text, text, text, uuid, uuid) IS 'Atomically validates one approved v1 lifecycle review against locked bitemporal Position/Assignment truth, records immutable audit/outbox evidence, closes the predecessor system-time interval, and inserts preserved/successor PositionVersion truth.'; ALTER TABLE position_lifecycle_application_record ENABLE ROW LEVEL SECURITY; ALTER TABLE position_lifecycle_application_record FORCE ROW LEVEL SECURITY; CREATE POLICY position_lifecycle_application_scope_policy -ON position_lifecycle_application_record -USING (tenant_record_id = current_tenant_record_id()) -WITH CHECK (tenant_record_id = current_tenant_record_id()); +ON public.position_lifecycle_application_record +USING (tenant_record_id = public.current_tenant_record_id()) +WITH CHECK (tenant_record_id = public.current_tenant_record_id()); + +REVOKE ALL ON FUNCTION public.apply_position_lifecycle_change( + uuid, uuid, uuid, uuid, uuid, text, text, text, uuid, uuid +) FROM PUBLIC; + +COMMIT; diff --git a/database/migrations/0024_position_lifecycle_transition_hardening.sql b/database/migrations/0024_position_lifecycle_transition_hardening.sql index e914ad763..0422a221d 100644 --- a/database/migrations/0024_position_lifecycle_transition_hardening.sql +++ b/database/migrations/0024_position_lifecycle_transition_hardening.sql @@ -3,7 +3,11 @@ -- transition and recompute its SHA-256. The authoritative persistence boundary -- independently enforces the same transition graph as the reviewed evidence type. -CREATE OR REPLACE FUNCTION validate_position_lifecycle_review_evidence( +BEGIN; + +SET LOCAL search_path = public, pg_catalog; + +CREATE OR REPLACE FUNCTION public.validate_position_lifecycle_review_evidence( p_canonical_review_json text, p_review_digest text, p_tenant_record_id uuid, @@ -16,6 +20,7 @@ RETURNS boolean LANGUAGE plpgsql STABLE STRICT +SET search_path = pg_catalog, public, pg_temp AS $$ DECLARE review_json json; @@ -140,5 +145,7 @@ BEGIN END; $$; -COMMENT ON FUNCTION validate_position_lifecycle_review_evidence(text, text, uuid, uuid, text, text, date) IS +COMMENT ON FUNCTION public.validate_position_lifecycle_review_evidence(text, text, uuid, uuid, text, text, date) IS 'Validates exact v1 Position lifecycle review shape, digest, tenant/Position/status/effective scope, allowed non-no-op transition graph, human approval state, and chronology without granting mutation authority.'; + +COMMIT; diff --git a/database/migrations/0025_position_lifecycle_snapshot_hardening.sql b/database/migrations/0025_position_lifecycle_snapshot_hardening.sql index 5ce0c4096..bcb57e9d2 100644 --- a/database/migrations/0025_position_lifecycle_snapshot_hardening.sql +++ b/database/migrations/0025_position_lifecycle_snapshot_hardening.sql @@ -3,13 +3,18 @@ -- JSON bytes are preserved and its value-minimized Position/Assignment digests -- still match authoritative bitemporal truth at application time. -CREATE FUNCTION position_lifecycle_review_canonical_json( +BEGIN; + +SET LOCAL search_path = public, pg_catalog; + +CREATE FUNCTION public.position_lifecycle_review_canonical_json( p_review_json text ) RETURNS text LANGUAGE plpgsql IMMUTABLE STRICT +SET search_path = pg_catalog, public, pg_temp AS $$ DECLARE payload jsonb; @@ -36,10 +41,10 @@ BEGIN END; $$; -COMMENT ON FUNCTION position_lifecycle_review_canonical_json(text) IS +COMMENT ON FUNCTION public.position_lifecycle_review_canonical_json(text) IS 'Returns the compact C-collation key-sorted JSON object representation required by Position lifecycle review evidence; malformed or non-object input returns NULL.'; -CREATE FUNCTION position_lifecycle_position_snapshot_digest( +CREATE FUNCTION public.position_lifecycle_position_snapshot_digest( p_tenant_record_id uuid, p_position_record_id uuid, p_effective_on date @@ -48,6 +53,7 @@ RETURNS text LANGUAGE sql STABLE STRICT +SET search_path = pg_catalog, public, pg_temp AS $$ WITH current_snapshot AS ( SELECT pg_catalog.jsonb_build_object( @@ -80,10 +86,10 @@ AS $$ FROM current_snapshot; $$; -COMMENT ON FUNCTION position_lifecycle_position_snapshot_digest(uuid, uuid, date) IS +COMMENT ON FUNCTION public.position_lifecycle_position_snapshot_digest(uuid, uuid, date) IS 'Returns a SHA-256 digest of the exact system-visible Position anchor/version truth at one business-effective date, or NULL when no authoritative Position snapshot exists.'; -CREATE FUNCTION position_lifecycle_assignment_snapshot_digest( +CREATE FUNCTION public.position_lifecycle_assignment_snapshot_digest( p_tenant_record_id uuid, p_position_record_id uuid, p_effective_on date @@ -92,6 +98,7 @@ RETURNS text LANGUAGE sql STABLE STRICT +SET search_path = pg_catalog, public, pg_temp AS $$ WITH visible_assignments AS ( SELECT assignment.assignment_record_id, @@ -130,7 +137,7 @@ AS $$ FROM snapshot; $$; -COMMENT ON FUNCTION position_lifecycle_assignment_snapshot_digest(uuid, uuid, date) IS +COMMENT ON FUNCTION public.position_lifecycle_assignment_snapshot_digest(uuid, uuid, date) IS 'Returns a SHA-256 digest of value-minimized system-visible Assignment occupancy truth for one Position at one business-effective date; an empty assignment set has a deterministic digest.'; -- Preserve the already-reviewed transition/shape validator as an internal stage, @@ -140,7 +147,7 @@ ALTER FUNCTION public.validate_position_lifecycle_review_evidence( text, text, uuid, uuid, text, text, date ) RENAME TO validate_position_lifecycle_review_evidence_v1_shape; -CREATE FUNCTION validate_position_lifecycle_review_evidence( +CREATE FUNCTION public.validate_position_lifecycle_review_evidence( p_canonical_review_json text, p_review_digest text, p_tenant_record_id uuid, @@ -153,6 +160,7 @@ RETURNS boolean LANGUAGE plpgsql STABLE STRICT +SET search_path = pg_catalog, public, pg_temp AS $$ DECLARE review_payload jsonb; @@ -204,10 +212,10 @@ BEGIN END; $$; -COMMENT ON FUNCTION validate_position_lifecycle_review_evidence(text, text, uuid, uuid, text, text, date) IS +COMMENT ON FUNCTION public.validate_position_lifecycle_review_evidence(text, text, uuid, uuid, text, text, date) IS 'Validates exact canonical v1 review bytes, digest, tenant/Position/status/effective scope, allowed transition graph, human-review chronology, and fresh authoritative Position/Assignment snapshot digests without granting mutation authority.'; -COMMENT ON FUNCTION validate_position_lifecycle_review_evidence_v1_shape(text, text, uuid, uuid, text, text, date) IS +COMMENT ON FUNCTION public.validate_position_lifecycle_review_evidence_v1_shape(text, text, uuid, uuid, text, text, date) IS 'Internal v1 review shape/transition validator retained beneath the canonical-byte and fresh-snapshot validation boundary.'; -- High-impact lifecycle mutation must never inherit PostgreSQL default PUBLIC @@ -215,3 +223,5 @@ COMMENT ON FUNCTION validate_position_lifecycle_review_evidence_v1_shape(text, t REVOKE ALL ON FUNCTION public.apply_position_lifecycle_change( uuid, uuid, uuid, uuid, uuid, text, text, text, uuid, uuid ) FROM PUBLIC; + +COMMIT; diff --git a/docs/DATA_MODEL.md b/docs/DATA_MODEL.md index 7d4afd563..eef86e05f 100644 --- a/docs/DATA_MODEL.md +++ b/docs/DATA_MODEL.md @@ -14,6 +14,7 @@ | `job_profile_version` | Bitemporal title, family, and version definition for a job profile. | | `position_record` | Durable seat identity that keeps stable organization and job references. | | `position_record_version` | Bitemporal position status and effective period. | +| `position_lifecycle_application_record` | Immutable tenant-scoped correlation of one human-reviewed lifecycle change with its predecessor/successor PositionVersion and audit/outbox evidence. | | `assignment_record` | A person's allocation to a position through one employment. | | `candidate_profile` | Applicant/candidate record before hire. | | `candidate_worker_link` | Legacy append-only candidate-to-worker linkage retained for historical reads; new writes use `candidate_worker_conversion_record`. | @@ -54,6 +55,8 @@ Durable anchors such as `organization_unit`, `job_profile`, `employment_record`, Assignments remain a legitimately multiple-membership fact. Each assignment must name the covering employment and the same person as that employment. Exclusive employments for one person cannot overlap; a second job must be marked `concurrent`. Allocation totals for one employment, and visible allocations for one position, are enforced by `orgmetra_hris_kernel` rather than a single-valued exclusion. An assignment day must also land on an `active` or `open` position version. +The reviewed Position lifecycle boundary keeps `position_record` stable and applies only canonical, human-reviewed evidence whose Position and Assignment snapshot digests still match current authoritative truth. It closes the predecessor's system-recorded interval and inserts preserved/successor `position_record_version` segments at one transaction timestamp; it never rewrites status in place. `position_lifecycle_application_record` stores no Person, candidate, compensation, rating, assessment, or free-form HR payload. Its tenant-qualified predecessor/successor bindings, forced RLS, append-only/TRUNCATE guards, and same-transaction audit/outbox references fail closed when review evidence, staffing occupancy, or actor separation is inconsistent. + ## High-impact decision evidence Evidence membership is constructed in `selection_decision_evidence` while its `decision_evidence_set` is open. An open set has no caller-supplied content digest. Finalizing `selection_decision` requires at least one versioned evidence member, canonicalizes the members by `(evidence_reference, evidence_version_code)`, computes SHA-256 inside PostgreSQL, and atomically stores that digest while binding `sealed_selection_decision_id`. Database triggers reject later evidence inserts, second-decision reuse, arbitrary post-seal mutation, and a sealed-set pointer that does not resolve back to the decision that consumed that exact set. This makes the stored digest evidence about database-observed membership at finalization rather than an unverified client assertion. diff --git a/docs/ERD.md b/docs/ERD.md index a547cc3a0..36fda5b81 100644 --- a/docs/ERD.md +++ b/docs/ERD.md @@ -18,6 +18,8 @@ erDiagram job_profile ||--o{ job_profile_version : has_versions job_profile ||--o{ position_record : defines position_record ||--o{ position_record_version : has_versions + position_record ||--o{ position_lifecycle_application_record : changes_through + position_record_version ||--o{ position_lifecycle_application_record : predecessor_or_successor employment_record ||--o{ assignment_record : covers person_record ||--o{ assignment_record : receives position_record ||--o{ assignment_record : assigned_through @@ -63,6 +65,8 @@ One immutable `audit_event_record` may have multiple `outbox_delivery_record` ro A `people_mutation_idempotency_record` belongs to one tenant and names one created employment, position, or assignment identity for one route and `Idempotency-Key`. The unique `(tenant_record_id, command_route, idempotency_key)` key prevents a retry from creating a second authoritative fact. Tenants do not share keys. +One `position_lifecycle_application_record` binds a human-reviewed lifecycle proposal to the exact tenant-qualified predecessor and successor `position_record_version` rows, immutable review bytes/digest, and one audit/outbox pair. The application record is append-only; the lifecycle function closes the predecessor system-time interval and inserts the successor rather than rewriting Position history. + A delivery can have at most one `outbox_delivery_escalation_record`, enforced by the unique `(tenant_record_id, outbox_delivery_record_id)` key. The escalation row exists only for a terminal `dead_lettered` delivery and records the failure classification, terminal attempt count, recorded time, and an opaque operator/customer escalation reference without copying the event payload. The row is append-only; terminal queue history is not reopened or rewritten. ## Naming contract diff --git a/docs/OPERABILITY.md b/docs/OPERABILITY.md index 31f3ff23e..501e5c3e9 100644 --- a/docs/OPERABILITY.md +++ b/docs/OPERABILITY.md @@ -41,6 +41,12 @@ - Audit/outbox SQL boundaries pin `search_path` to `pg_catalog, public, pg_temp`, the migration revokes `CREATE` on `public` from `PUBLIC`, and project objects remain in the trusted application schema until schema extraction work explicitly moves them. Normal dispatcher/persistence functions remain security-invoker boundaries; the lost-final-worker recovery function is the sole `SECURITY DEFINER` exception and is owned by the hardened NOLOGIN recovery role rather than a login or superuser role. - Exponential/backoff policy selection, policy-specific producer configuration, and external delivery receipts remain release blockers before reliable asynchronous delivery is called production-ready; terminal dead-letter/escalation evidence and lost-final-worker recovery are implemented but do not by themselves prove downstream receipt. +### Position lifecycle application + +- Position lifecycle application is a high-impact, transaction-bound operation. It must fail closed on missing or stale Position/Assignment snapshot evidence, forbidden transitions, occupied closure/abolition, tenant/version/audit/outbox mismatch, or unavailable authoritative truth. +- Deployments apply migrations 0023 through 0025 in order and run the dedicated PostgreSQL contracts against a fresh PostgreSQL 16 database. The application role receives the explicit grant for `apply_position_lifecycle_change(...)` only after purpose-bound authorization is configured; PostgreSQL `PUBLIC` retains no execute privilege. +- Restore and migration rehearsals must verify the successor/predecessor bitemporal chain, immutable application evidence, audit/outbox pairing, forced RLS, trusted function `search_path`, and append-only/TRUNCATE guards before the Position lifecycle capability is re-enabled. + ### Other dependencies - Psychometrics Commons unavailable: assessment-result fetches show an unavailable state, not invented scores. diff --git a/docs/SECURITY.md b/docs/SECURITY.md index fd6dd3ea6..d94c0aa44 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -45,6 +45,8 @@ Every mutating HTTP operation and its server-side command handler requires one v All mutation families additionally require resource-scoped authorization and a versioned audit/provenance correlation reference. High-risk commands require an explicit human-confirmation boundary and immutable versioned evidence. Employment, position, and assignment commands carry confirmation/evidence on the command. Confirmed-hire materialization resolves the exact previously sealed `selection_decision` in the same tenant-bound transaction and rejects the mutation unless that decision records explicit human confirmation and sealed evidence provenance. +The Position lifecycle application boundary is a separate high-impact capability. It accepts only canonical, human-reviewed lifecycle evidence, re-resolves the locked tenant-qualified PositionVersion and current Assignment occupancy, rejects stale or forged snapshot digests and occupied closure/abolition, and records one immutable review/application/audit/outbox correlation. Its PostgreSQL function pins `search_path`, loses default `PUBLIC EXECUTE`, and uses forced RLS plus append-only and TRUNCATE guards; a review packet never authorizes the mutation by itself. + `people_mutation_idempotency_record` stores the tenant, route, idempotency key, semantic-command digest, committed resource identity, and transaction time in the same transaction as the authoritative HRIS fact and governed audit/outbox pair. A transaction-scoped advisory lock serializes concurrent requests for the exact tenant/route/key. A same-key same-command replay returns the first committed identity without repeating Person, Employment, candidate-worker conversion, audit, or outbox writes; a changed command under the same key fails closed. A rolled-back command leaves no successful replay marker. The idempotency relation is tenant-RLS isolated and append-only, including TRUNCATE protection. A caller-controlled purpose value cannot substitute for a missing token scope. The OpenAPI contract is executable input to generated gateway and server validation; an implementation that accepts a request outside its published contract fails CI. diff --git a/docs/TEST_STRATEGY.md b/docs/TEST_STRATEGY.md index c20813b72..88dc21704 100644 --- a/docs/TEST_STRATEGY.md +++ b/docs/TEST_STRATEGY.md @@ -31,6 +31,7 @@ The command runs Python repository-integrity validation, the dependency-free Nod | Predictive-validity study case worker/decision/evidence/criterion and recorded-time integrity | `bash tests/test_validity_study_case_postgres.sh` against PostgreSQL 16 in Foundation CI | | Performance criterion observation Job, cycle, staffing, current-recorded-time, and UTC date-boundary integrity | `bash tests/test_criterion_observation_scope_postgres.sh` against PostgreSQL 16 in Foundation CI | | Governed People mutation idempotency: tenant/route/key uniqueness, identical-command replay, changed-command rejection, rollback safety, append-only/TRUNCATE protection, forced RLS and concurrent exact-key serialization | `bash tests/test_people_mutation_idempotency_postgres.sh` against PostgreSQL 16 in Foundation CI | +| Reviewed Position lifecycle application: canonical review bytes, fresh Position/Assignment snapshot binding, transition/stale-state/staffing guards, tenant-qualified predecessor/successor identity, atomic audit/outbox evidence, append-only/TRUNCATE protection, revoked default function execution, and fixed trusted function paths | `bash tests/test_position_lifecycle_application_postgres.sh`, `bash tests/test_position_lifecycle_transition_validation_postgres.sh`, and `bash tests/test_position_lifecycle_snapshot_integrity_postgres.sh` against PostgreSQL 16 in the dedicated lifecycle workflow | | Tenant/actor/purpose authorization matrix and negative high-impact commands | service-specific unit and integration test commands recorded in each service package | | AsyncAPI/CloudEvents envelope compatibility | provider and consumer contract test commands recorded beside the versioned event schema | | External adapter timeout, malformed response, tenant mismatch, and unavailable-state handling | fake-server tests in each adapter package | diff --git a/docs/THREAT_MODEL.md b/docs/THREAT_MODEL.md index 29d11a478..d31bc8506 100644 --- a/docs/THREAT_MODEL.md +++ b/docs/THREAT_MODEL.md @@ -6,6 +6,7 @@ |---|---|---|---| | Spoofing | External identity treated as HR person identity | Separate `person_record` from the Keyverse subject; verify issuer, audience, tenant, actor binding, and token lifetime. | Authentication-denial audit; tests reject subject/person substitution and stale authorization. | | Tampering | Selection evidence, audit envelope, terminal escalation evidence, or delivery history changed after decision | Append-only decision/evidence records; database-sealed evidence digest; append-only `audit_event_record` and `outbox_delivery_escalation_record`; immutable database-owned delivery retry budget; database recomputation of SHA-256 over exact canonical audit bytes; guarded delivery state stored separately. | Integrity alert; tests reject update/delete, digest mismatch, added audit fields, illegal outbox state changes, retry-budget mutation/dispatcher override, attempt N+1, terminal-row reopening, escalation mutation/fabrication, and version mismatch. | +| Tampering or stale high-impact application | A reviewed Position lifecycle change is replayed after Position/Assignment truth changes, or direct DML binds mismatched review, version, audit, or outbox identities | Canonical review bytes, fresh bitemporal Position/Assignment snapshot digests, locked predecessor resolution, transition and staffing guards, tenant-qualified predecessor/successor bindings, append-only/TRUNCATE protection, and atomic audit/outbox correlation. | Position lifecycle PostgreSQL contracts reject noncanonical/forged/stale evidence, occupied closure/abolition, cross-tenant bindings, direct history rewrite/truncation, untrusted function resolution, and mismatched application metadata. | | Repudiation | Hiring manager or integration operator denies a decision or terminal delivery failure | Human confirmation reference, actor, tenant, purpose, reason, evidence versions, immutable audit event, recorded dispatcher identity, and immutable terminal escalation reference with durable attempt count. | Correlated decision/audit/escalation lookup; tests prove actor traceability, prohibit high-impact audit persistence without confirmation, and preserve one append-only escalation record for dead-lettered work. | | Information disclosure | PII broadcast, over-broad field response, or retained data through an event bus or failure queue | Opaque references, exact durable audit-field allowlist, purpose-bound tenant/resource/operation/scope/field authorization before protected values leave the HR boundary, tenant-scoped encryption, and no copied mutable HR payload in audit or escalation evidence. | Payload scanner plus authorization/database contracts; tests reject extra employee-name/PII event fields, foreign-resource access, missing operation scope, disallowed fields, and malformed or wildcard-like authorization attributes while constraining escalation metadata to governance codes/references. | | Cross-tenant access | Tenant A reads, reconstructs, changes, or emits evidence for Tenant B HRIS facts by altering a path, header, reference, cache key, event, delivery, escalation, or by supplying a foreign fact with a colliding durable identifier to an in-memory decision. | Authenticated tenant context, explicit agreement among request/actor/resource/policy tenants, explicit tenant scope in historical reconstruction and HRIS decision functions, audit event/tenant identity binding, forced RLS on audit/outbox/escalation relations, service-owned database roles, tenant-aware cache keys, and consumer-side event validation. | `cross_tenant_access_denied` audit event with no sensitive values; authorization/integration/kernel/database tests attempt request/actor/resource tenant mismatches, direct reads/writes, object-reference swaps, colliding identifiers, event tenant mismatch, foreign delivery finalization/escalation, cache poisoning, and replay across tenants and require denial or exclusion with unchanged target data. | diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 22a4178fe..150fd7306 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -9,6 +9,7 @@ | Tenant-qualified HRIS integrity and fail-closed isolation | Core bounded contexts / Security architecture | `tenant_record`, tenant-qualified foreign keys, forced row-level security policies, tenant-scoped kernel query parameters | PostgreSQL cross-tenant FK/application-role RLS contracts plus kernel cross-tenant reconstruction, employment coverage, position coverage, seat-capacity, portfolio, exclusivity, and organization-hierarchy regressions | ADR-0001, ADR-0003 | implemented_on_active_pr | | Reserved UUID sentinel exclusion | Persistence integrity boundary | every foundation UUID `*_id` column plus audit/outbox identifiers | PostgreSQL inventory proof plus Nil/Max foundation and audit/outbox persistence regressions | ADR-0001, RFC 9562 | implemented_on_active_pr | | Normalized bitemporal organization/job/employment/position history | Core bounded contexts | `organization_unit_version`, `job_profile_version`, `employment_record_version`, `position_record_version` | PostgreSQL non-overlap, concurrent conflict, correction, rewrite-rejection, assignment-employment binding, and single-valued historical reconstruction | ADR-0001, ADR-0003, ADR-0004 | implemented_on_active_pr | +| Reviewed Position lifecycle application | Job Architecture / People persistence boundary | `position_record`, `position_record_version`, `position_lifecycle_application_record`, audit/outbox identities | PostgreSQL exact canonical-review, fresh Position/Assignment snapshot, stale-state, staffing-safety, tenant-binding, append-only/TRUNCATE, and trusted-function-boundary regressions | ADR-0112 | implemented_on_active_pr | | Acyclic organization hierarchy at historical coordinates | Organization core | `organization_unit_version.parent_organization_unit_id` | indirect A→B→C→A rejection plus future-recorded and foreign-tenant isolation in `orgmetra_hris_kernel` | ADR-0001, ADR-0003 | implemented_on_protected_main | | Effective/system time | Bitemporal HRIS | `effective_from`, `recorded_from` | strict half-open interval and historical-coordinate tests | ADR-0003 | implemented_on_active_pr | | Evidence-backed human selection decisions | Talent Acquisition | `decision_evidence_set`, `selection_decision_evidence`, `selection_decision` | database-owned SHA-256 sealing, non-empty evidence, drift/reuse rejection, OpenAPI human-confirmation tests | ADR-0001 | implemented_on_active_pr | diff --git a/docs/adr/README.md b/docs/adr/README.md index 099a21139..8dba403d5 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -16,3 +16,4 @@ | [0012](0012-governed-migration-handoff.md) | Governed migration handoff | Accepted on active implementation branch | | [0013](0013-governed-requisition-review-packet.md) | Governed requisition review packet | Accepted on active implementation branch | | [0014](0014-job-analysis-snapshot-persistence.md) | Persist governed job-analysis snapshots | Accepted on active implementation branch | +| [0112](0112-position-lifecycle-application.md) | Apply reviewed Position lifecycle changes as bitemporal truth | Proposed | diff --git a/manifest.json b/manifest.json index 97f2bab14..b1c6bae0c 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":".github/workflows/position-lifecycle-application-quality.yml","sha256":"bdafe04878566da41c7079da8012d5e8a1d5ed770ec8a70ad2b805f9cf31d845","bytes":4244,"lines":100},{"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":"0ca9f7dc6c3759b4a4ad1fee77aa28b40986de4bdb6df95df6fe8e9616f6107c","bytes":18104,"lines":77},{"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":"bb7a90f13cd26ba84a03930e6771f507130ea728fb59b94c2a97d41aa4410086","bytes":4207,"lines":83},{"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/0023_position_lifecycle_application.sql","sha256":"f2dde23cc006058d91df57e3d0e6a9f574f538eaee54f2f1b632ce1c41cd0ec5","bytes":30624,"lines":730},{"path":"database/migrations/0024_position_lifecycle_transition_hardening.sql","sha256":"afa0667bab087866e921dda35042457b3b1646f07e86d55bbeb4854c1c6f2a6a","bytes":6222,"lines":151},{"path":"database/migrations/0025_position_lifecycle_snapshot_hardening.sql","sha256":"2434563a3f299e658abb315e94ad6fbbeef5442f2585dd73a95fb33214397163","bytes":8861,"lines":227},{"path":"docs/API_CONTRACT.md","sha256":"63533dff785da62b89e585d742a158e2aeb05913644f2bf9fb6486f281c2e589","bytes":4555,"lines":76},{"path":"docs/DATA_MODEL.md","sha256":"2650eb6cc697eb391c96ac76c8a2b1087c8e0382d8faa6a594598e70f9b90423","bytes":14317,"lines":88},{"path":"docs/ERD.md","sha256":"d74daf4fdff037afa1a7938d828e8eeb6c155806430934d981fd56d1ad3f5fc7","bytes":7552,"lines":74},{"path":"docs/OPERABILITY.md","sha256":"046da783778ec931cd118da3c8472d81c7c0d86e74e7601e8b1735aaa702a60d","bytes":12124,"lines":77},{"path":"docs/PRD.md","sha256":"3ad85ae633cce0fc7a93af39b21d7a7c70bb2efa786da6b12f3c5327906e34f1","bytes":5490,"lines":111},{"path":"docs/SECURITY.md","sha256":"8d401d0f28465a7e6609b49bf81a8b273d07e41296fb09327768491a2fea028c","bytes":11755,"lines":66},{"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":"e977a0700d4a933e462d2b70d0d1dd63641b565a8e15b53cc9fd687bdb5cbf1a","bytes":17134,"lines":136},{"path":"docs/THREAT_MODEL.md","sha256":"7089af53e58880c8f0df7d9da4ed4413bb737eeb2d338694a28ef52ef4e6d184","bytes":7453,"lines":24},{"path":"docs/TRACEABILITY.md","sha256":"cde45939a05d20f97986cc16facb6ea8f0ac2cf31dfbf7c345e0410e67649114","bytes":11889,"lines":41},{"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/0112-position-lifecycle-application.md","sha256":"0aa5c252e2c9c4e01d50482cd06d1d00b85bfc80fdcd07126175a8053fd2507f","bytes":5545,"lines":40},{"path":"docs/adr/README.md","sha256":"4e325155f114e0bc7ff76442172a7e6d23c03513f79543121597d8e50936001a","bytes":1964,"lines":19},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"lines":69},{"path":"docs/doctoring/position-lifecycle-application-references.md","sha256":"2e5f52b2a628f000c7ae5dd4350c50a2dd25ecf86f3a3dadd33797845b56702b","bytes":1739,"lines":20},{"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":"docs/traceability/position-lifecycle-application.md","sha256":"884e597c38351bb1acb06fa2dc3832b94c07749196455651a40020fd7e2afe78","bytes":4682,"lines":34},{"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":"8a340146145783821cd914c93bafd8798426b78a5595d6bb9ea377e577aea36e","bytes":28905,"lines":701},{"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":"c72f4d3c346dc753a64d09ec9b3fd83717a30264618b2119a4200de068921d0c","bytes":15044,"lines":391},{"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_position_lifecycle_application_postgres.sh","sha256":"d35b7f91f1acece1e84f2b2e1b63720076320c9c7cb6a36632e8093c6a40f915","bytes":11103,"lines":228},{"path":"tests/test_position_lifecycle_snapshot_integrity_postgres.sh","sha256":"76fdd78de493422c57b9e533c9a63bab66ef92ee1b6d039e5b8b2a835399528f","bytes":8379,"lines":180},{"path":"tests/test_position_lifecycle_transition_validation_postgres.sh","sha256":"4098f4813626c38b74e6da1c813491fc75801deb9d533bc1f2f12e3b25bdbe1a","bytes":2595,"lines":57},{"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":"83fc918739618506237d0b98023976518435ca78e3c46dc5c1df7569f3fc4b7e","bytes":27957,"lines":648}]} diff --git a/scripts/foundation-contract-core.mjs b/scripts/foundation-contract-core.mjs index 1e9fb267c..ddf9fd08b 100644 --- a/scripts/foundation-contract-core.mjs +++ b/scripts/foundation-contract-core.mjs @@ -22,6 +22,7 @@ export const REQUIRED_FILES = Object.freeze([ 'package.json', '.github/workflows/foundation-ci.yml', '.github/workflows/job-analysis-api-quality.yml', + '.github/workflows/position-lifecycle-application-quality.yml', 'docs/PRD.md', 'docs/TRD.md', 'docs/USER_STORIES.md', @@ -52,7 +53,10 @@ export const REQUIRED_FILES = Object.freeze([ 'docs/adr/0012-governed-migration-handoff.md', 'docs/adr/0013-governed-requisition-review-packet.md', 'docs/adr/0014-job-analysis-snapshot-persistence.md', + 'docs/adr/0112-position-lifecycle-application.md', 'docs/doctoring/REFERENCES.md', + 'docs/doctoring/position-lifecycle-application-references.md', + 'docs/traceability/position-lifecycle-application.md', 'docs/superpowers/specs/2026-08-15-orgmetra-foundation-design.md', 'docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md', 'database/migrations/0001_foundation_schema.sql', @@ -68,6 +72,9 @@ 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/0023_position_lifecycle_application.sql', + 'database/migrations/0024_position_lifecycle_transition_hardening.sql', + 'database/migrations/0025_position_lifecycle_snapshot_hardening.sql', 'packages/hris-kernel/src/orgmetra_hris_kernel/audit.py', 'packages/hris-kernel/tests/test_audit_outbox.py', 'schemas/openapi.yaml', @@ -89,6 +96,9 @@ export const REQUIRED_FILES = Object.freeze([ 'tests/test_criterion_observation_scope_postgres.sh', 'tests/test_people_mutation_idempotency_postgres.sh', 'tests/test_job_analysis_snapshot_postgres.sh', + 'tests/test_position_lifecycle_application_postgres.sh', + 'tests/test_position_lifecycle_transition_validation_postgres.sh', + 'tests/test_position_lifecycle_snapshot_integrity_postgres.sh', 'tests/validate_repository.py' ]); @@ -129,7 +139,8 @@ export const DATABASE_OBJECT_NAMES = Object.freeze([ 'evidence_record', 'evidence_source_segment', 'authorization_policy', 'authorization_decision', 'audit_event', 'audit_event_record', 'data_rights_request', 'outbox_event', 'outbox_delivery_record', 'outbox_delivery_escalation_record', - 'people_mutation_idempotency_record', 'inbox_event', 'integration_delivery' + 'people_mutation_idempotency_record', 'position_lifecycle_application_record', + 'inbox_event', 'integration_delivery' ]); /** Migration-backed logical objects whose persisted table identity must not drift. */ @@ -139,7 +150,8 @@ export const MIGRATION_BACKED_DATABASE_OBJECT_NAMES = Object.freeze([ 'job_analysis_task_item', 'job_analysis_ksao_item', 'job_analysis_task_ksao_link', - 'job_analysis_write_command' + 'job_analysis_write_command', + 'position_lifecycle_application_record' ]); const UNFINISHED_MARKER_LINE_PATTERN = /^\s*(?:#{1,6}\s+|[-*+]\s+)?(?:\[(?:TODO|TBD|FIXME)\]|\{\{(?:TODO|TBD|FIXME)\}\}|<(?:TODO|TBD|FIXME)>|(?:TODO|TBD|FIXME)(?:\s*:\s*.*)?\s*)$/i; diff --git a/tests/foundation-contract.test.mjs b/tests/foundation-contract.test.mjs index 72b18466f..58ffc5a8d 100644 --- a/tests/foundation-contract.test.mjs +++ b/tests/foundation-contract.test.mjs @@ -53,6 +53,11 @@ function writeMigrationBackedTables(root) { 'CREATE TABLE job_analysis_write_command (tenant_record_id uuid NOT NULL);' ].join('\n') + '\n' ); + write( + root, + 'database/migrations/0023_position_lifecycle_application.sql', + 'CREATE TABLE position_lifecycle_application_record (tenant_record_id uuid NOT NULL);\n' + ); } function makeMinimalValidFoundation(root) { diff --git a/tests/test_position_lifecycle_application_postgres.sh b/tests/test_position_lifecycle_application_postgres.sh index 888578df2..917297d74 100644 --- a/tests/test_position_lifecycle_application_postgres.sh +++ b/tests/test_position_lifecycle_application_postgres.sh @@ -198,4 +198,31 @@ SQL )" test "$force_rls" = "position_lifecycle_application_record:true,position_record_version:true" +search_path_contract="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 <<'SQL' +SELECT count(*) +FROM pg_catalog.pg_proc AS procedure_record +WHERE procedure_record.oid IN ( + 'public.validate_position_lifecycle_review_evidence(text,text,uuid,uuid,text,text,date)'::regprocedure, + 'public.protect_position_lifecycle_application_history()'::regprocedure, + 'public.protect_position_version_history_after_lifecycle_support()'::regprocedure, + 'public.validate_position_lifecycle_application_audit()'::regprocedure, + 'public.validate_position_lifecycle_application_successor()'::regprocedure, + 'public.reject_position_lifecycle_history_truncate()'::regprocedure, + 'public.apply_position_lifecycle_change(uuid,uuid,uuid,uuid,uuid,text,text,text,uuid,uuid)'::regprocedure +) +AND procedure_record.proconfig @> ARRAY['search_path=pg_catalog, public, pg_temp']::text[]; +SQL +)" +test "$search_path_contract" = "7" + +if psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -c 'TRUNCATE public.position_lifecycle_application_record' >/dev/null 2>&1; then + echo "position lifecycle application evidence was truncated" >&2 + exit 1 +fi + +if psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -c 'TRUNCATE public.position_record_version' >/dev/null 2>&1; then + echo "position version history was truncated" >&2 + exit 1 +fi + echo "position lifecycle application persistence: PASS" diff --git a/tests/test_position_lifecycle_snapshot_integrity_postgres.sh b/tests/test_position_lifecycle_snapshot_integrity_postgres.sh index 17f3cd76f..1e72e4753 100644 --- a/tests/test_position_lifecycle_snapshot_integrity_postgres.sh +++ b/tests/test_position_lifecycle_snapshot_integrity_postgres.sh @@ -156,4 +156,25 @@ SQL )" test "$public_execute" = "f" +search_path_contract="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 <<'SQL' +SELECT count(*) +FROM pg_catalog.pg_proc AS procedure_record +WHERE procedure_record.oid IN ( + 'public.validate_position_lifecycle_review_evidence_v1_shape(text,text,uuid,uuid,text,text,date)'::regprocedure, + 'public.validate_position_lifecycle_review_evidence(text,text,uuid,uuid,text,text,date)'::regprocedure, + 'public.position_lifecycle_review_canonical_json(text)'::regprocedure, + 'public.position_lifecycle_position_snapshot_digest(uuid,uuid,date)'::regprocedure, + 'public.position_lifecycle_assignment_snapshot_digest(uuid,uuid,date)'::regprocedure, + 'public.protect_position_lifecycle_application_history()'::regprocedure, + 'public.protect_position_version_history_after_lifecycle_support()'::regprocedure, + 'public.validate_position_lifecycle_application_audit()'::regprocedure, + 'public.validate_position_lifecycle_application_successor()'::regprocedure, + 'public.reject_position_lifecycle_history_truncate()'::regprocedure, + 'public.apply_position_lifecycle_change(uuid,uuid,uuid,uuid,uuid,text,text,text,uuid,uuid)'::regprocedure +) +AND procedure_record.proconfig @> ARRAY['search_path=pg_catalog, public, pg_temp']::text[]; +SQL +)" +test "$search_path_contract" = "11" + echo "position lifecycle snapshot integrity: PASS" diff --git a/tests/validate_repository.py b/tests/validate_repository.py index fe0a329ff..a5fbc6dfa 100644 --- a/tests/validate_repository.py +++ b/tests/validate_repository.py @@ -25,6 +25,7 @@ "package.json", ".github/workflows/foundation-ci.yml", ".github/workflows/job-analysis-api-quality.yml", + ".github/workflows/position-lifecycle-application-quality.yml", "docs/PRD.md", "docs/TRD.md", "docs/USER_STORIES.md", @@ -55,7 +56,10 @@ "docs/adr/0012-governed-migration-handoff.md", "docs/adr/0013-governed-requisition-review-packet.md", "docs/adr/0014-job-analysis-snapshot-persistence.md", + "docs/adr/0112-position-lifecycle-application.md", "docs/doctoring/REFERENCES.md", + "docs/doctoring/position-lifecycle-application-references.md", + "docs/traceability/position-lifecycle-application.md", "docs/superpowers/specs/2026-08-15-orgmetra-foundation-design.md", "docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md", "database/migrations/0001_foundation_schema.sql", @@ -71,6 +75,9 @@ "database/migrations/0011_criterion_observation_scope.sql", "database/migrations/0012_people_mutation_idempotency.sql", "database/migrations/0013_job_analysis_snapshot.sql", + "database/migrations/0023_position_lifecycle_application.sql", + "database/migrations/0024_position_lifecycle_transition_hardening.sql", + "database/migrations/0025_position_lifecycle_snapshot_hardening.sql", "packages/hris-kernel/src/orgmetra_hris_kernel/audit.py", "packages/hris-kernel/tests/test_audit_outbox.py", "schemas/openapi.yaml", @@ -92,6 +99,9 @@ "tests/test_criterion_observation_scope_postgres.sh", "tests/test_people_mutation_idempotency_postgres.sh", "tests/test_job_analysis_snapshot_postgres.sh", + "tests/test_position_lifecycle_application_postgres.sh", + "tests/test_position_lifecycle_transition_validation_postgres.sh", + "tests/test_position_lifecycle_snapshot_integrity_postgres.sh", "tests/validate_repository.py", ] From 1889851f9f8c7e0528e047ec53f33f947be6dd88 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 00:15:05 +0900 Subject: [PATCH 23/23] fix(position): close lifecycle review gaps --- .../workflows/position-lifecycle-application-quality.yml | 6 ++++++ database/migrations/0023_position_lifecycle_application.sql | 4 ++++ .../0024_position_lifecycle_transition_hardening.sql | 3 +++ docs/adr/0112-position-lifecycle-application.md | 1 + manifest.json | 2 +- ...est_position_lifecycle_transition_validation_postgres.sh | 4 ++++ 6 files changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/position-lifecycle-application-quality.yml b/.github/workflows/position-lifecycle-application-quality.yml index e2c338d0d..bdd6c141c 100644 --- a/.github/workflows/position-lifecycle-application-quality.yml +++ b/.github/workflows/position-lifecycle-application-quality.yml @@ -90,7 +90,13 @@ jobs: PY - name: Run Position lifecycle application regressions run: bash tests/test_position_lifecycle_application_postgres.sh + - name: Create isolated transition-validation database + env: + ADMIN_DATABASE_URL: postgresql://orgmetra:orgmetra@localhost:5432/postgres + run: psql "$ADMIN_DATABASE_URL" -v ON_ERROR_STOP=1 -c 'CREATE DATABASE orgmetra_position_lifecycle_transition' - name: Reject forged lifecycle transition evidence + env: + DATABASE_URL: postgresql://orgmetra:orgmetra@localhost:5432/orgmetra_position_lifecycle_transition run: bash tests/test_position_lifecycle_transition_validation_postgres.sh - name: Verify fresh Position and Assignment snapshot evidence run: bash tests/test_position_lifecycle_snapshot_integrity_postgres.sh diff --git a/database/migrations/0023_position_lifecycle_application.sql b/database/migrations/0023_position_lifecycle_application.sql index c254d93a3..77992a6c5 100644 --- a/database/migrations/0023_position_lifecycle_application.sql +++ b/database/migrations/0023_position_lifecycle_application.sql @@ -87,6 +87,7 @@ CREATE TABLE position_lifecycle_application_record ( CHECK ( requester_actor_reference <> reviewer_actor_reference AND reviewer_actor_reference <> applied_by_actor_reference + AND requester_actor_reference <> applied_by_actor_reference ), CONSTRAINT position_lifecycle_current_status_check CHECK (current_status_code IN ('active', 'open', 'closed', 'frozen', 'abolished')), @@ -207,7 +208,10 @@ BEGIN OR review_payload ->> 'review_outcome_code' <> 'approved_for_authoritative_resolution' OR review_payload ->> 'review_state' <> 'human_reviewed' OR review_payload ->> 'scope_verification_state' <> 'requires_authoritative_resolution' + OR pg_catalog.jsonb_typeof(review_payload -> 'evidence_version') <> 'number' OR review_payload ->> 'evidence_version' <> '1' + OR review_payload ->> 'next_action' + <> 'Re-resolve tenant-qualified Position and Assignment truth at the requested business/system coordinate; require authoritative actor separation, reviewed evidence, staffing safety, and immutable audit/outbox before any lifecycle mutation.' OR review_payload ->> 'position_snapshot_digest_sha256' !~ '^[0-9a-f]{64}$' OR review_payload ->> 'assignment_snapshot_digest_sha256' !~ '^[0-9a-f]{64}$' OR review_payload ->> 'position_lifecycle_change_reference' diff --git a/database/migrations/0024_position_lifecycle_transition_hardening.sql b/database/migrations/0024_position_lifecycle_transition_hardening.sql index 0422a221d..1c6a894ef 100644 --- a/database/migrations/0024_position_lifecycle_transition_hardening.sql +++ b/database/migrations/0024_position_lifecycle_transition_hardening.sql @@ -92,7 +92,10 @@ BEGIN OR review_payload ->> 'review_outcome_code' <> 'approved_for_authoritative_resolution' OR review_payload ->> 'review_state' <> 'human_reviewed' OR review_payload ->> 'scope_verification_state' <> 'requires_authoritative_resolution' + OR pg_catalog.jsonb_typeof(review_payload -> 'evidence_version') <> 'number' OR review_payload ->> 'evidence_version' <> '1' + OR review_payload ->> 'next_action' + <> 'Re-resolve tenant-qualified Position and Assignment truth at the requested business/system coordinate; require authoritative actor separation, reviewed evidence, staffing safety, and immutable audit/outbox before any lifecycle mutation.' OR review_payload ->> 'position_snapshot_digest_sha256' !~ '^[0-9a-f]{64}$' OR review_payload ->> 'assignment_snapshot_digest_sha256' !~ '^[0-9a-f]{64}$' OR review_payload ->> 'position_lifecycle_change_reference' diff --git a/docs/adr/0112-position-lifecycle-application.md b/docs/adr/0112-position-lifecycle-application.md index c1953e9d7..43a3b2d1e 100644 --- a/docs/adr/0112-position-lifecycle-application.md +++ b/docs/adr/0112-position-lifecycle-application.md @@ -23,6 +23,7 @@ The high-impact `apply_position_lifecycle_change(...)` function does not retain ## Consequences - Business-effective time and system-recorded time remain independent and reconstructable. +- A later-effective PositionVersion that was already recorded remains a separate scheduled fact; applying a review to an earlier interval does not silently overwrite that future lifecycle state. Any change to that later interval requires its own reviewed application. - A review packet never authorizes mutation by itself; current Position and Assignment truth is re-resolved and cryptographically compared with the reviewed snapshots at application time. - Canonical review bytes are part of the governed contract; recomputing a digest over a differently encoded but semantically equivalent document does not create acceptable evidence. - `closed` and `abolished` fail closed while any current Assignment overlaps the requested effective date or later. diff --git a/manifest.json b/manifest.json index b1c6bae0c..9e10cb0e3 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":".github/workflows/position-lifecycle-application-quality.yml","sha256":"bdafe04878566da41c7079da8012d5e8a1d5ed770ec8a70ad2b805f9cf31d845","bytes":4244,"lines":100},{"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":"0ca9f7dc6c3759b4a4ad1fee77aa28b40986de4bdb6df95df6fe8e9616f6107c","bytes":18104,"lines":77},{"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":"bb7a90f13cd26ba84a03930e6771f507130ea728fb59b94c2a97d41aa4410086","bytes":4207,"lines":83},{"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/0023_position_lifecycle_application.sql","sha256":"f2dde23cc006058d91df57e3d0e6a9f574f538eaee54f2f1b632ce1c41cd0ec5","bytes":30624,"lines":730},{"path":"database/migrations/0024_position_lifecycle_transition_hardening.sql","sha256":"afa0667bab087866e921dda35042457b3b1646f07e86d55bbeb4854c1c6f2a6a","bytes":6222,"lines":151},{"path":"database/migrations/0025_position_lifecycle_snapshot_hardening.sql","sha256":"2434563a3f299e658abb315e94ad6fbbeef5442f2585dd73a95fb33214397163","bytes":8861,"lines":227},{"path":"docs/API_CONTRACT.md","sha256":"63533dff785da62b89e585d742a158e2aeb05913644f2bf9fb6486f281c2e589","bytes":4555,"lines":76},{"path":"docs/DATA_MODEL.md","sha256":"2650eb6cc697eb391c96ac76c8a2b1087c8e0382d8faa6a594598e70f9b90423","bytes":14317,"lines":88},{"path":"docs/ERD.md","sha256":"d74daf4fdff037afa1a7938d828e8eeb6c155806430934d981fd56d1ad3f5fc7","bytes":7552,"lines":74},{"path":"docs/OPERABILITY.md","sha256":"046da783778ec931cd118da3c8472d81c7c0d86e74e7601e8b1735aaa702a60d","bytes":12124,"lines":77},{"path":"docs/PRD.md","sha256":"3ad85ae633cce0fc7a93af39b21d7a7c70bb2efa786da6b12f3c5327906e34f1","bytes":5490,"lines":111},{"path":"docs/SECURITY.md","sha256":"8d401d0f28465a7e6609b49bf81a8b273d07e41296fb09327768491a2fea028c","bytes":11755,"lines":66},{"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":"e977a0700d4a933e462d2b70d0d1dd63641b565a8e15b53cc9fd687bdb5cbf1a","bytes":17134,"lines":136},{"path":"docs/THREAT_MODEL.md","sha256":"7089af53e58880c8f0df7d9da4ed4413bb737eeb2d338694a28ef52ef4e6d184","bytes":7453,"lines":24},{"path":"docs/TRACEABILITY.md","sha256":"cde45939a05d20f97986cc16facb6ea8f0ac2cf31dfbf7c345e0410e67649114","bytes":11889,"lines":41},{"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/0112-position-lifecycle-application.md","sha256":"0aa5c252e2c9c4e01d50482cd06d1d00b85bfc80fdcd07126175a8053fd2507f","bytes":5545,"lines":40},{"path":"docs/adr/README.md","sha256":"4e325155f114e0bc7ff76442172a7e6d23c03513f79543121597d8e50936001a","bytes":1964,"lines":19},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"lines":69},{"path":"docs/doctoring/position-lifecycle-application-references.md","sha256":"2e5f52b2a628f000c7ae5dd4350c50a2dd25ecf86f3a3dadd33797845b56702b","bytes":1739,"lines":20},{"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":"docs/traceability/position-lifecycle-application.md","sha256":"884e597c38351bb1acb06fa2dc3832b94c07749196455651a40020fd7e2afe78","bytes":4682,"lines":34},{"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":"8a340146145783821cd914c93bafd8798426b78a5595d6bb9ea377e577aea36e","bytes":28905,"lines":701},{"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":"c72f4d3c346dc753a64d09ec9b3fd83717a30264618b2119a4200de068921d0c","bytes":15044,"lines":391},{"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_position_lifecycle_application_postgres.sh","sha256":"d35b7f91f1acece1e84f2b2e1b63720076320c9c7cb6a36632e8093c6a40f915","bytes":11103,"lines":228},{"path":"tests/test_position_lifecycle_snapshot_integrity_postgres.sh","sha256":"76fdd78de493422c57b9e533c9a63bab66ef92ee1b6d039e5b8b2a835399528f","bytes":8379,"lines":180},{"path":"tests/test_position_lifecycle_transition_validation_postgres.sh","sha256":"4098f4813626c38b74e6da1c813491fc75801deb9d533bc1f2f12e3b25bdbe1a","bytes":2595,"lines":57},{"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":"83fc918739618506237d0b98023976518435ca78e3c46dc5c1df7569f3fc4b7e","bytes":27957,"lines":648}]} +{"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":".github/workflows/position-lifecycle-application-quality.yml","sha256":"264f1dbf0d59b4b1540cb944725113fe6a239bc737a9abe665ca6055c355ada1","bytes":4644,"lines":106},{"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":"0ca9f7dc6c3759b4a4ad1fee77aa28b40986de4bdb6df95df6fe8e9616f6107c","bytes":18104,"lines":77},{"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":"bb7a90f13cd26ba84a03930e6771f507130ea728fb59b94c2a97d41aa4410086","bytes":4207,"lines":83},{"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/0023_position_lifecycle_application.sql","sha256":"2b6d2d384ea82c3a266d7de2de7cadad5991445fb65fc39fb65f67fd0a339ed3","bytes":31075,"lines":734},{"path":"database/migrations/0024_position_lifecycle_transition_hardening.sql","sha256":"e502634e4ba61f19a352c39213487ab412eacd201056ba88ad57c1c07d8fb6b2","bytes":6601,"lines":154},{"path":"database/migrations/0025_position_lifecycle_snapshot_hardening.sql","sha256":"2434563a3f299e658abb315e94ad6fbbeef5442f2585dd73a95fb33214397163","bytes":8861,"lines":227},{"path":"docs/API_CONTRACT.md","sha256":"63533dff785da62b89e585d742a158e2aeb05913644f2bf9fb6486f281c2e589","bytes":4555,"lines":76},{"path":"docs/DATA_MODEL.md","sha256":"2650eb6cc697eb391c96ac76c8a2b1087c8e0382d8faa6a594598e70f9b90423","bytes":14317,"lines":88},{"path":"docs/ERD.md","sha256":"d74daf4fdff037afa1a7938d828e8eeb6c155806430934d981fd56d1ad3f5fc7","bytes":7552,"lines":74},{"path":"docs/OPERABILITY.md","sha256":"046da783778ec931cd118da3c8472d81c7c0d86e74e7601e8b1735aaa702a60d","bytes":12124,"lines":77},{"path":"docs/PRD.md","sha256":"3ad85ae633cce0fc7a93af39b21d7a7c70bb2efa786da6b12f3c5327906e34f1","bytes":5490,"lines":111},{"path":"docs/SECURITY.md","sha256":"8d401d0f28465a7e6609b49bf81a8b273d07e41296fb09327768491a2fea028c","bytes":11755,"lines":66},{"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":"e977a0700d4a933e462d2b70d0d1dd63641b565a8e15b53cc9fd687bdb5cbf1a","bytes":17134,"lines":136},{"path":"docs/THREAT_MODEL.md","sha256":"7089af53e58880c8f0df7d9da4ed4413bb737eeb2d338694a28ef52ef4e6d184","bytes":7453,"lines":24},{"path":"docs/TRACEABILITY.md","sha256":"cde45939a05d20f97986cc16facb6ea8f0ac2cf31dfbf7c345e0410e67649114","bytes":11889,"lines":41},{"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/0112-position-lifecycle-application.md","sha256":"8abbf1be05f212ca3aa6e5cf47d8c3927a6ebbd96f2fed1b1ef7ea905db69244","bytes":5813,"lines":41},{"path":"docs/adr/README.md","sha256":"4e325155f114e0bc7ff76442172a7e6d23c03513f79543121597d8e50936001a","bytes":1964,"lines":19},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"lines":69},{"path":"docs/doctoring/position-lifecycle-application-references.md","sha256":"2e5f52b2a628f000c7ae5dd4350c50a2dd25ecf86f3a3dadd33797845b56702b","bytes":1739,"lines":20},{"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":"docs/traceability/position-lifecycle-application.md","sha256":"884e597c38351bb1acb06fa2dc3832b94c07749196455651a40020fd7e2afe78","bytes":4682,"lines":34},{"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":"8a340146145783821cd914c93bafd8798426b78a5595d6bb9ea377e577aea36e","bytes":28905,"lines":701},{"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":"c72f4d3c346dc753a64d09ec9b3fd83717a30264618b2119a4200de068921d0c","bytes":15044,"lines":391},{"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_position_lifecycle_application_postgres.sh","sha256":"d35b7f91f1acece1e84f2b2e1b63720076320c9c7cb6a36632e8093c6a40f915","bytes":11103,"lines":228},{"path":"tests/test_position_lifecycle_snapshot_integrity_postgres.sh","sha256":"76fdd78de493422c57b9e533c9a63bab66ef92ee1b6d039e5b8b2a835399528f","bytes":8379,"lines":180},{"path":"tests/test_position_lifecycle_transition_validation_postgres.sh","sha256":"81b9f80bc490086911298318ba820df68a68202ae2e02eeaadd6b78a88f42bf4","bytes":3024,"lines":61},{"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":"83fc918739618506237d0b98023976518435ca78e3c46dc5c1df7569f3fc4b7e","bytes":27957,"lines":648}]} diff --git a/tests/test_position_lifecycle_transition_validation_postgres.sh b/tests/test_position_lifecycle_transition_validation_postgres.sh index 08dd83d54..a54c93b95 100644 --- a/tests/test_position_lifecycle_transition_validation_postgres.sh +++ b/tests/test_position_lifecycle_transition_validation_postgres.sh @@ -3,6 +3,10 @@ set -euo pipefail : "${DATABASE_URL:?DATABASE_URL is required}" +psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f database/migrations/0001_foundation_schema.sql >/dev/null +psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f database/migrations/0002_sealed_evidence_digest.sql >/dev/null +psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f database/migrations/0003_audit_outbox_persistence.sql >/dev/null +psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f database/migrations/0023_position_lifecycle_application.sql >/dev/null psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f database/migrations/0024_position_lifecycle_transition_hardening.sql >/dev/null TENANT="0198a412-8000-7000-8000-000000000101"