From 78e092353be310f2bcefd30c1f7439b5ed1b235e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:06:16 -0700 Subject: [PATCH 01/32] test(position-reporting): define durable bitemporal persistence contract --- ...position_reporting_persistence_postgres.sh | 237 ++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 tests/test_position_reporting_persistence_postgres.sh diff --git a/tests/test_position_reporting_persistence_postgres.sh b/tests/test_position_reporting_persistence_postgres.sh new file mode 100644 index 000000000..8b24102ff --- /dev/null +++ b/tests/test_position_reporting_persistence_postgres.sh @@ -0,0 +1,237 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${DATABASE_URL:=postgresql://orgmetra:orgmetra@localhost:5432/orgmetra}" + +for migration in \ + database/migrations/0001_foundation_schema.sql \ + database/migrations/0002_sealed_evidence_digest.sql \ + database/migrations/0003_audit_outbox_persistence.sql \ + database/migrations/0020_position_reporting_relationship.sql; do + if [[ ! -f "${migration}" ]]; then + echo "required position-reporting persistence migration is missing: ${migration}" >&2 + exit 1 + fi + psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f "${migration}" +done + +TENANT_ID="10000000-0000-7000-8000-000000000001" +OTHER_TENANT_ID="20000000-0000-7000-8000-000000000002" +ORG_ID="00000000-0000-7000-8000-000000000011" +JOB_ID="00000000-0000-7000-8000-000000000021" +SUBORDINATE_POSITION_ID="00000000-0000-7000-8000-000000000031" +MANAGER_POSITION_ID="00000000-0000-7000-8000-000000000032" +OTHER_POSITION_ID="00000000-0000-7000-8000-000000000033" +RELATIONSHIP_ID="00000000-0000-7000-8000-000000000041" +RELATIONSHIP_VERSION_ID="00000000-0000-7000-8000-000000000042" +REVERSE_RELATIONSHIP_ID="00000000-0000-7000-8000-000000000043" +AUDIT_ID="00000000-0000-4000-8000-000000000051" +OUTBOX_ID="00000000-0000-4000-8000-000000000052" +REVIEWER="actor:00000000-0000-4000-8000-000000000061" +APPLIED_BY="actor:00000000-0000-4000-8000-000000000062" +REVIEW_DIGEST="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +APPLICATION_DIGEST="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + +with_tenant() { + local tenant="$1" + shift + PGOPTIONS="-c orgmetra.tenant_record_id=${tenant}" command psql "$@" +} + +with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 <&2 + exit 1 +fi + +set +e +backdated_output="$({ with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c " +INSERT INTO position_reporting_relationship_record ( + tenant_record_id, position_reporting_relationship_record_id, + subordinate_position_record_id, relationship_type_code, recorded_from +) VALUES ( + '${TENANT_ID}', '00000000-0000-7000-8000-000000000071', + '${OTHER_POSITION_ID}', 'solid_line', TIMESTAMPTZ '2000-01-01 00:00:00+00' +);" ; } 2>&1)" +backdated_status=$? +set -e +if [[ ${backdated_status} -eq 0 || "${backdated_output}" != *"transaction timestamp"* ]]; then + echo "position-reporting anchor accepted caller-backdated system time: ${backdated_output}" >&2 + exit 1 +fi + +set +e +self_output="$({ with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c " +INSERT INTO position_reporting_relationship_record ( + tenant_record_id, position_reporting_relationship_record_id, + subordinate_position_record_id, relationship_type_code +) VALUES ( + '${TENANT_ID}', '${REVERSE_RELATIONSHIP_ID}', '${MANAGER_POSITION_ID}', 'solid_line' +); +INSERT INTO position_reporting_relationship_version ( + tenant_record_id, position_reporting_relationship_version_id, + position_reporting_relationship_record_id, manager_position_record_id, + review_evidence_digest_sha256, application_evidence_digest_sha256, + reviewer_actor_reference, applied_by_actor_reference, reviewed_at, + effective_from, audit_event_record_id +) VALUES ( + '${TENANT_ID}', '00000000-0000-7000-8000-000000000072', + '${REVERSE_RELATIONSHIP_ID}', '${MANAGER_POSITION_ID}', + '${REVIEW_DIGEST}', '${APPLICATION_DIGEST}', '${REVIEWER}', '${APPLIED_BY}', + TIMESTAMPTZ '2026-08-24 01:55:00+00', DATE '2026-08-24', '${AUDIT_ID}' +);" ; } 2>&1)" +self_status=$? +set -e +if [[ ${self_status} -eq 0 || "${self_output}" != *"cannot report to itself"* ]]; then + echo "position-reporting relationship accepted self-reporting: ${self_output}" >&2 + exit 1 +fi + +set +e +cycle_output="$({ with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c " +INSERT INTO position_reporting_relationship_version ( + tenant_record_id, position_reporting_relationship_version_id, + position_reporting_relationship_record_id, manager_position_record_id, + review_evidence_digest_sha256, application_evidence_digest_sha256, + reviewer_actor_reference, applied_by_actor_reference, reviewed_at, + effective_from, audit_event_record_id +) VALUES ( + '${TENANT_ID}', '00000000-0000-7000-8000-000000000073', + '${REVERSE_RELATIONSHIP_ID}', '${SUBORDINATE_POSITION_ID}', + '${REVIEW_DIGEST}', '${APPLICATION_DIGEST}', '${REVIEWER}', '${APPLIED_BY}', + TIMESTAMPTZ '2026-08-24 01:55:00+00', DATE '2026-08-24', '${AUDIT_ID}' +);" ; } 2>&1)" +cycle_status=$? +set -e +if [[ ${cycle_status} -eq 0 || "${cycle_output}" != *"cycle"* ]]; then + echo "position-reporting relationship accepted a management cycle: ${cycle_output}" >&2 + exit 1 +fi + +set +e +rewrite_output="$({ with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c " +UPDATE position_reporting_relationship_version +SET manager_position_record_id = '${OTHER_POSITION_ID}'::uuid +WHERE position_reporting_relationship_version_id = '${RELATIONSHIP_VERSION_ID}'::uuid;" ; } 2>&1)" +rewrite_status=$? +set -e +if [[ ${rewrite_status} -eq 0 || "${rewrite_output}" != *"history"* ]]; then + echo "position-reporting history was rewriteable: ${rewrite_output}" >&2 + exit 1 +fi + +set +e +truncate_output="$({ with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c \ + "TRUNCATE position_reporting_relationship_version;" ; } 2>&1)" +truncate_status=$? +set -e +if [[ ${truncate_status} -eq 0 || "${truncate_output}" != *"cannot be truncated"* ]]; then + echo "position-reporting history could be truncated: ${truncate_output}" >&2 + exit 1 +fi + +psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 <<'SQL' +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'orgmetra_reporting_reader') THEN + CREATE ROLE orgmetra_reporting_reader LOGIN PASSWORD 'orgmetra_reporting_reader' NOSUPERUSER NOBYPASSRLS; + END IF; +END +$$; +GRANT CONNECT ON DATABASE orgmetra TO orgmetra_reporting_reader; +GRANT USAGE ON SCHEMA public TO orgmetra_reporting_reader; +GRANT SELECT ON position_reporting_relationship_record, position_reporting_relationship_version TO orgmetra_reporting_reader; +SQL + +alpha_count="$(PGPASSWORD=orgmetra_reporting_reader PGOPTIONS="-c orgmetra.tenant_record_id=${TENANT_ID}" \ + psql -h localhost -U orgmetra_reporting_reader -d orgmetra -Atqc \ + 'SELECT count(*) FROM position_reporting_relationship_version;')" +beta_count="$(PGPASSWORD=orgmetra_reporting_reader PGOPTIONS="-c orgmetra.tenant_record_id=${OTHER_TENANT_ID}" \ + psql -h localhost -U orgmetra_reporting_reader -d orgmetra -Atqc \ + 'SELECT count(*) FROM position_reporting_relationship_version;')" +missing_count="$(PGPASSWORD=orgmetra_reporting_reader \ + psql -h localhost -U orgmetra_reporting_reader -d orgmetra -Atqc \ + 'SELECT count(*) FROM position_reporting_relationship_version;')" +if [[ "${alpha_count}" != "1" || "${beta_count}" != "0" || "${missing_count}" != "0" ]]; then + echo "position-reporting RLS isolation failed: alpha=${alpha_count} beta=${beta_count} missing=${missing_count}" >&2 + exit 1 +fi + +echo "position-reporting persistence contract passed" From df8d8b4a644cac615b49356f17e09b6a174db707 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:06:28 -0700 Subject: [PATCH 02/32] ci(position-reporting): add exact-head persistence quality gate --- ...position-reporting-persistence-quality.yml | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 .github/workflows/position-reporting-persistence-quality.yml diff --git a/.github/workflows/position-reporting-persistence-quality.yml b/.github/workflows/position-reporting-persistence-quality.yml new file mode 100644 index 000000000..9e61f31d9 --- /dev/null +++ b/.github/workflows/position-reporting-persistence-quality.yml @@ -0,0 +1,88 @@ +name: Position Reporting Persistence Quality + +on: + pull_request: + branches: + - develop + - feat/position-reporting-hierarchy + paths: + - "database/migrations/0020_position_reporting_relationship.sql" + - "tests/test_position_reporting_persistence_postgres.sh" + - "docs/adr/0106-position-reporting-persistence.md" + - "docs/traceability/position-reporting-persistence.md" + - "docs/doctoring/position-reporting-persistence-references.md" + - ".github/workflows/position-reporting-persistence-quality.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: position-reporting-persistence-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + postgres_contract: + name: Governed position-reporting persistence 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 persistence provenance + run: | + python - <<'PY' + import hashlib + import json + from pathlib import Path + + required = [ + "tests/test_position_reporting_persistence_postgres.sh", + ".github/workflows/position-reporting-persistence-quality.yml", + ] + optional = [ + "database/migrations/0020_position_reporting_relationship.sql", + "docs/adr/0106-position-reporting-persistence.md", + "docs/traceability/position-reporting-persistence.md", + "docs/doctoring/position-reporting-persistence-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-reporting persistence regressions + run: bash tests/test_position_reporting_persistence_postgres.sh + - name: Require clean checkout + run: | + git diff --exit-code + test -z "$(git status --porcelain)" From 209259120d10b1cd18ab1e6184297f5af4730ffc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:07:27 -0700 Subject: [PATCH 03/32] feat(position-reporting): persist governed bitemporal relationships --- .../0020_position_reporting_relationship.sql | 352 ++++++++++++++++++ 1 file changed, 352 insertions(+) create mode 100644 database/migrations/0020_position_reporting_relationship.sql diff --git a/database/migrations/0020_position_reporting_relationship.sql b/database/migrations/0020_position_reporting_relationship.sql new file mode 100644 index 000000000..51e94afd1 --- /dev/null +++ b/database/migrations/0020_position_reporting_relationship.sql @@ -0,0 +1,352 @@ +-- Persist authoritative Position-to-Position solid-line reporting truth after +-- independent human review and authoritative application. Person, Assignment, +-- compensation, assessment, and free-form HR values remain outside this model. + +CREATE TABLE position_reporting_relationship_record ( + tenant_record_id uuid NOT NULL REFERENCES tenant_record(tenant_record_id), + position_reporting_relationship_record_id uuid PRIMARY KEY, + subordinate_position_record_id uuid NOT NULL, + relationship_type_code text NOT NULL, + recorded_from timestamptz NOT NULL DEFAULT pg_catalog.transaction_timestamp(), + recorded_to timestamptz, + CONSTRAINT position_reporting_relationship_record_id_operational_check + CHECK (public.is_operational_uuid(position_reporting_relationship_record_id)), + CONSTRAINT position_reporting_subordinate_tenant_fk + FOREIGN KEY (tenant_record_id, subordinate_position_record_id) + REFERENCES position_record(tenant_record_id, position_record_id), + CONSTRAINT position_reporting_relationship_type_check + CHECK (relationship_type_code = 'solid_line'), + CONSTRAINT position_reporting_recorded_period_check + CHECK (recorded_to IS NULL OR recorded_to > recorded_from), + CONSTRAINT position_reporting_record_tenant_identity_unique + UNIQUE (tenant_record_id, position_reporting_relationship_record_id), + CONSTRAINT position_reporting_subordinate_type_unique + UNIQUE (tenant_record_id, subordinate_position_record_id, relationship_type_code) +); + +CREATE TABLE position_reporting_relationship_version ( + tenant_record_id uuid NOT NULL REFERENCES tenant_record(tenant_record_id), + position_reporting_relationship_version_id uuid PRIMARY KEY, + position_reporting_relationship_record_id uuid NOT NULL, + manager_position_record_id uuid NOT NULL, + review_evidence_digest_sha256 text NOT NULL, + application_evidence_digest_sha256 text NOT NULL, + reviewer_actor_reference text NOT NULL, + applied_by_actor_reference text NOT NULL, + reviewed_at timestamptz NOT NULL, + effective_from date NOT NULL, + effective_to date, + recorded_from timestamptz NOT NULL DEFAULT pg_catalog.transaction_timestamp(), + recorded_to timestamptz, + audit_event_record_id uuid NOT NULL, + application_state text NOT NULL DEFAULT 'applied_after_human_review', + CONSTRAINT position_reporting_version_id_operational_check + CHECK (public.is_operational_uuid(position_reporting_relationship_version_id)), + CONSTRAINT position_reporting_version_record_tenant_fk + FOREIGN KEY (tenant_record_id, position_reporting_relationship_record_id) + REFERENCES position_reporting_relationship_record( + tenant_record_id, + position_reporting_relationship_record_id + ), + CONSTRAINT position_reporting_manager_tenant_fk + FOREIGN KEY (tenant_record_id, manager_position_record_id) + REFERENCES position_record(tenant_record_id, position_record_id), + CONSTRAINT position_reporting_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_reporting_review_digest_check + CHECK (review_evidence_digest_sha256 ~ '^[0-9a-f]{64}$'), + CONSTRAINT position_reporting_application_digest_check + CHECK (application_evidence_digest_sha256 ~ '^[0-9a-f]{64}$'), + CONSTRAINT position_reporting_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_reporting_applied_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_reporting_actor_separation_check + CHECK (reviewer_actor_reference <> applied_by_actor_reference), + CONSTRAINT position_reporting_effective_period_check + CHECK (effective_to IS NULL OR effective_to > effective_from), + CONSTRAINT position_reporting_version_recorded_period_check + CHECK (recorded_to IS NULL OR recorded_to > recorded_from), + CONSTRAINT position_reporting_review_chronology_check + CHECK (reviewed_at <= recorded_from), + CONSTRAINT position_reporting_application_state_check + CHECK (application_state = 'applied_after_human_review'), + CONSTRAINT position_reporting_version_tenant_identity_unique + UNIQUE (tenant_record_id, position_reporting_relationship_version_id), + CONSTRAINT position_reporting_audit_event_unique + UNIQUE (tenant_record_id, audit_event_record_id), + CONSTRAINT position_reporting_bitemporal_exclusion + EXCLUDE USING gist ( + tenant_record_id WITH =, + position_reporting_relationship_record_id WITH =, + daterange(effective_from, effective_to, '[)') WITH &&, + tstzrange(recorded_from, recorded_to, '[)') WITH && + ) +); + +CREATE FUNCTION enforce_position_reporting_system_time() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + IF NEW.recorded_to IS NOT NULL THEN + RAISE EXCEPTION 'position-reporting recorded_to must be NULL on insert' + USING ERRCODE = '22023'; + END IF; + IF NEW.recorded_from IS DISTINCT FROM pg_catalog.transaction_timestamp() THEN + RAISE EXCEPTION 'position-reporting recorded_from must equal the current transaction timestamp' + USING ERRCODE = '22023'; + END IF; + RETURN NEW; +END; +$$; + +COMMENT ON FUNCTION enforce_position_reporting_system_time() IS + 'Requires PostgreSQL transaction time for new position-reporting recorded intervals and requires those intervals to begin open.'; + +CREATE TRIGGER position_reporting_record_system_time_guard +BEFORE INSERT ON position_reporting_relationship_record +FOR EACH ROW +EXECUTE FUNCTION enforce_position_reporting_system_time(); + +CREATE TRIGGER position_reporting_version_system_time_guard +BEFORE INSERT ON position_reporting_relationship_version +FOR EACH ROW +EXECUTE FUNCTION enforce_position_reporting_system_time(); + +CREATE FUNCTION protect_position_reporting_history() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + IF TG_OP = 'DELETE' THEN + RAISE EXCEPTION 'position-reporting 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 'position-reporting history may only close an open recorded interval at the current transaction timestamp' + USING ERRCODE = '55000'; + END IF; + + RETURN NEW; +END; +$$; + +COMMENT ON FUNCTION protect_position_reporting_history() IS + 'Rejects deletion and in-place rewriting of position-reporting history; UPDATE may only close one open system-recorded interval at PostgreSQL transaction time.'; + +CREATE TRIGGER position_reporting_record_history_guard +BEFORE UPDATE OR DELETE ON position_reporting_relationship_record +FOR EACH ROW +EXECUTE FUNCTION protect_position_reporting_history(); + +CREATE TRIGGER position_reporting_version_history_guard +BEFORE UPDATE OR DELETE ON position_reporting_relationship_version +FOR EACH ROW +EXECUTE FUNCTION protect_position_reporting_history(); + +CREATE FUNCTION enforce_position_reporting_scope() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +DECLARE + subordinate_position_id uuid; + relationship_type text; + anchor_recorded_to timestamptz; + audit_event jsonb; + outbox_found boolean; + cycle_found boolean; +BEGIN + SELECT subordinate_position_record_id, relationship_type_code, recorded_to + INTO subordinate_position_id, relationship_type, anchor_recorded_to + FROM position_reporting_relationship_record + WHERE tenant_record_id = NEW.tenant_record_id + AND position_reporting_relationship_record_id = NEW.position_reporting_relationship_record_id + FOR SHARE; + + IF NOT FOUND OR anchor_recorded_to IS NOT NULL THEN + RAISE EXCEPTION 'position-reporting version requires an open same-tenant relationship anchor' + USING ERRCODE = '23514'; + END IF; + IF relationship_type <> 'solid_line' THEN + RAISE EXCEPTION 'position-reporting persistence supports only solid-line relationships' + USING ERRCODE = '23514'; + END IF; + IF subordinate_position_id = NEW.manager_position_record_id THEN + RAISE EXCEPTION 'a Position cannot report to itself' + USING ERRCODE = '23514'; + END IF; + + WITH RECURSIVE manager_path(position_record_id, effective_period) AS ( + SELECT + NEW.manager_position_record_id, + daterange(NEW.effective_from, NEW.effective_to, '[)') + UNION + SELECT + next_version.manager_position_record_id, + manager_path.effective_period * + daterange(next_version.effective_from, next_version.effective_to, '[)') + FROM manager_path + JOIN position_reporting_relationship_record AS next_record + ON next_record.tenant_record_id = NEW.tenant_record_id + AND next_record.subordinate_position_record_id = manager_path.position_record_id + AND next_record.relationship_type_code = 'solid_line' + AND next_record.recorded_to IS NULL + JOIN position_reporting_relationship_version AS next_version + ON next_version.tenant_record_id = next_record.tenant_record_id + AND next_version.position_reporting_relationship_record_id = + next_record.position_reporting_relationship_record_id + AND next_version.recorded_to IS NULL + WHERE manager_path.effective_period && + daterange(next_version.effective_from, next_version.effective_to, '[)') + ) + SELECT EXISTS ( + SELECT 1 + FROM manager_path + WHERE position_record_id = subordinate_position_id + ) INTO cycle_found; + + IF cycle_found THEN + RAISE EXCEPTION 'position-reporting relationship would create a management cycle' + USING ERRCODE = '23514'; + END IF; + + SELECT canonical_event_json::jsonb + INTO audit_event + FROM audit_event_record + WHERE tenant_record_id = NEW.tenant_record_id + AND audit_event_record_id = NEW.audit_event_record_id + FOR SHARE; + + IF NOT FOUND THEN + RAISE EXCEPTION 'position-reporting version requires immutable same-tenant audit evidence' + USING ERRCODE = '23514'; + END IF; + + SELECT EXISTS ( + SELECT 1 + FROM outbox_delivery_record + WHERE tenant_record_id = NEW.tenant_record_id + AND audit_event_record_id = NEW.audit_event_record_id + AND delivery_target_code = 'integration_hub' + ) INTO outbox_found; + + IF NOT outbox_found THEN + RAISE EXCEPTION 'position-reporting version requires transactional audit/outbox evidence' + USING ERRCODE = '23514'; + END IF; + + IF audit_event ->> 'orgmetrapurpose' <> 'position_reporting_change_apply' + OR audit_event ->> 'orgmetraactor' <> NEW.applied_by_actor_reference + OR audit_event ->> 'orgmetraevidence' <> NEW.application_evidence_digest_sha256 + OR audit_event ->> 'subject' + <> 'position_reporting_relationship:' || NEW.position_reporting_relationship_record_id::text + OR audit_event #>> '{data,result_code}' <> 'position_reporting_applied' + OR (audit_event #>> '{data,high_impact}')::boolean IS DISTINCT FROM false + OR (audit_event ->> 'time')::timestamptz < NEW.reviewed_at + OR (audit_event ->> 'time')::timestamptz > NEW.recorded_from THEN + RAISE EXCEPTION 'position-reporting audit evidence does not exactly match the applied relationship scope' + USING ERRCODE = '23514'; + END IF; + + RETURN NEW; +END; +$$; + +COMMENT ON FUNCTION enforce_position_reporting_scope() IS + 'Before persistence, resolves the same-tenant relationship anchor, rejects self-reporting and cycles over overlapping effective time, and requires human-review-separated immutable audit/outbox application evidence.'; + +CREATE TRIGGER position_reporting_version_scope_guard +BEFORE INSERT ON position_reporting_relationship_version +FOR EACH ROW +EXECUTE FUNCTION enforce_position_reporting_scope(); + +CREATE FUNCTION enforce_position_reporting_anchor_alignment() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + IF NEW.recorded_to IS NULL OR NEW.recorded_to IS NOT DISTINCT FROM OLD.recorded_to THEN + RETURN NULL; + END IF; + + IF EXISTS ( + SELECT 1 + FROM position_reporting_relationship_version AS version + WHERE version.tenant_record_id = NEW.tenant_record_id + AND version.position_reporting_relationship_record_id = + NEW.position_reporting_relationship_record_id + AND (version.recorded_to IS NULL OR version.recorded_to > NEW.recorded_to) + ) THEN + RAISE EXCEPTION 'cannot close position-reporting anchor while a recorded version remains open' + USING ERRCODE = '23514'; + END IF; + RETURN NULL; +END; +$$; + +COMMENT ON FUNCTION enforce_position_reporting_anchor_alignment() IS + 'Deferred anchor-closure guard: every relationship version must be recorded closed no later than its durable anchor before commit.'; + +CREATE CONSTRAINT TRIGGER position_reporting_anchor_alignment_guard +AFTER UPDATE ON position_reporting_relationship_record +DEFERRABLE INITIALLY DEFERRED +FOR EACH ROW +EXECUTE FUNCTION enforce_position_reporting_anchor_alignment(); + +CREATE FUNCTION reject_position_reporting_truncate() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + RAISE EXCEPTION 'position-reporting history cannot be truncated' + USING ERRCODE = '55000'; +END; +$$; + +COMMENT ON FUNCTION reject_position_reporting_truncate() IS + 'Rejects table-wide TRUNCATE so position-reporting history cannot bypass row-level bitemporal guards.'; + +CREATE TRIGGER position_reporting_record_truncate_guard +BEFORE TRUNCATE ON position_reporting_relationship_record +FOR EACH STATEMENT +EXECUTE FUNCTION reject_position_reporting_truncate(); + +CREATE TRIGGER position_reporting_version_truncate_guard +BEFORE TRUNCATE ON position_reporting_relationship_version +FOR EACH STATEMENT +EXECUTE FUNCTION reject_position_reporting_truncate(); + +REVOKE TRUNCATE ON position_reporting_relationship_record FROM PUBLIC; +REVOKE TRUNCATE ON position_reporting_relationship_version FROM PUBLIC; + +ALTER TABLE position_reporting_relationship_record ENABLE ROW LEVEL SECURITY; +ALTER TABLE position_reporting_relationship_record FORCE ROW LEVEL SECURITY; +CREATE POLICY position_reporting_record_scope_policy +ON position_reporting_relationship_record +USING (tenant_record_id = current_tenant_record_id()) +WITH CHECK (tenant_record_id = current_tenant_record_id()); + +ALTER TABLE position_reporting_relationship_version ENABLE ROW LEVEL SECURITY; +ALTER TABLE position_reporting_relationship_version FORCE ROW LEVEL SECURITY; +CREATE POLICY position_reporting_version_scope_policy +ON position_reporting_relationship_version +USING (tenant_record_id = current_tenant_record_id()) +WITH CHECK (tenant_record_id = current_tenant_record_id()); + +COMMENT ON TABLE position_reporting_relationship_record IS + 'Durable tenant-scoped Position-to-Position solid-line relationship anchor. The subordinate seat and relationship type are stable anchor identity; Person and Assignment are intentionally absent.'; + +COMMENT ON TABLE position_reporting_relationship_version IS + 'Authoritative bitemporal manager-Position versions applied only after separate human review and immutable audit/outbox evidence. The relation stores no worker PII, compensation, ratings, or employment-decision output.'; From af4a561e53dc208d0138daca97e54cc5727a0598 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:08:10 -0700 Subject: [PATCH 04/32] docs(position-reporting): record persistence architecture decision --- .../0106-position-reporting-persistence.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 docs/adr/0106-position-reporting-persistence.md diff --git a/docs/adr/0106-position-reporting-persistence.md b/docs/adr/0106-position-reporting-persistence.md new file mode 100644 index 000000000..0eeba92e4 --- /dev/null +++ b/docs/adr/0106-position-reporting-persistence.md @@ -0,0 +1,41 @@ +# ADR 0106 — Bitemporal Position reporting persistence + +- **Status:** Proposed in active PR #106; not protected-main truth +- **Decision date:** 2026-08-24 +- **Parent contract:** PR #94, `feat/position-reporting-hierarchy@3f67182bb3065f2fc8fd974bfdd75a390d8a8fdc` + +## Context + +Protected `develop` separates Job, Position, Assignment, and organization-unit hierarchy but does not persist Position-to-Position supervisory relationships. PR #94 adds an in-memory bitemporal solid-line reporting snapshot and explicitly leaves persistence as later work. A commercial HRIS needs durable relationship truth without deriving a manager from the worker currently occupying a Position or from the organization-unit tree. + +## Decision + +Orgmetra persists a solid-line reporting relationship as a normalized anchor/version pair: + +- `position_reporting_relationship_record` owns stable tenant, subordinate Position, and relationship type identity. +- `position_reporting_relationship_version` owns manager Position plus effective/business and recorded/system time for each reviewed application. +- one `(tenant, subordinate Position, relationship type)` anchor exists, so manager changes are versions rather than duplicate relationship identities; +- the version table carries only evidence needed to prove reviewed application: SHA-256 review/application digests, pseudonymous reviewer and applying actors, review time, immutable audit correlation, and fixed application state; +- Person, Assignment, worker identity, compensation, performance rating, assessment output, and free-form HR text are not columns in this relation. + +New recorded intervals use PostgreSQL `transaction_timestamp()` and begin open. History cannot be rewritten or deleted; the only row update is closing one open recorded interval at the transaction timestamp. `TRUNCATE` is rejected separately. Both relations use `ENABLE ROW LEVEL SECURITY` plus `FORCE ROW LEVEL SECURITY` and tenant policies based on `current_tenant_record_id()`. + +A version insert fails closed unless it has an open same-tenant anchor, a different reviewer and applying actor, immutable same-tenant audit/outbox evidence for `position_reporting_change_apply`, no self-reporting edge, and no management cycle over an overlapping effective period. Composite foreign keys bind both subordinate and manager Position IDs to the same tenant. + +## Why this shape + +The anchor/version split keeps stable relationship identity separate from changing manager/effective-time facts and therefore remains 3NF while supporting bitemporal correction. PostgreSQL exclusion/range semantics prevent overlapping business/system versions under one relationship identity; the insert guard performs cross-row cycle validation that a row-local `CHECK` constraint cannot express. RLS is defense in depth rather than authorization by itself: the application role must remain `NOSUPERUSER NOBYPASSRLS`, and high-level mutation authority remains outside this migration. + +## Integration and stack boundary + +This PR is a Draft descendant of #94 and cannot inherit #94 checks or reviews. It must not merge before #94. After #94 is integrated, retarget #106 to the fresh protected `develop`, reconcile any migration-number/document conflicts, and re-run full exact-head CI/security/recovery evidence. + +PR #95 owns the in-memory pre-mutation review packet. This persistence slice does not copy or modify that branch; it accepts review/application digests and immutable audit evidence as the handoff boundary. A later authorized host adapter may translate a verified review packet into the database command, but direct cross-service SQL is out of scope. + +Canonical `docs/DATA_MODEL.md` / `docs/ERD.md` are intentionally not edited in this stacked slice while independent active database PRs also own those high-conflict documents. Integration must reconcile the accepted relationship tables into canonical data-model documentation after dependency ordering is resolved. + +## Consequences + +The database can preserve audited supervisory hierarchy truth independently of current worker occupancy. Reads can combine persisted relationships with PR #94's staffable-Position snapshot semantics. The stricter model rejects ambiguous duplicate anchors, self-reporting, cycles, caller-backdated system time, mutation of history, and tenant-crossing references rather than silently repairing them. + +This ADR does not claim certification, branch-protection enforcement, release readiness, or authorization to make employment decisions. From c9043801e6dc485a75f310819fb80e3478fc5aa4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:08:28 -0700 Subject: [PATCH 05/32] docs(position-reporting): add persistence traceability --- .../position-reporting-persistence.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docs/traceability/position-reporting-persistence.md diff --git a/docs/traceability/position-reporting-persistence.md b/docs/traceability/position-reporting-persistence.md new file mode 100644 index 000000000..b5e5844a0 --- /dev/null +++ b/docs/traceability/position-reporting-persistence.md @@ -0,0 +1,32 @@ +# Position reporting persistence traceability + +## Truth status + +- **Protected-main truth:** `develop@9e3e4847510e1e612b48474ba42b177b8ed824df` has no persisted Position-to-Position reporting relation. +- **Parent active PR:** #94 adds bitemporal in-memory reporting reconstruction at `3f67182bb3065f2fc8fd974bfdd75a390d8a8fdc`. +- **This active PR:** #106 persists reviewed solid-line Position relationships and remains a Draft stacked descendant until #94 integrates and this branch is retargeted/revalidated. +- **Separate active owner:** #95 owns the in-memory pre-mutation review packet. #106 consumes only evidence digests/audit correlation and does not rewrite #95. + +## Requirement → implementation → executable evidence + +| Requirement | Implementation | Evidence | +| --- | --- | --- | +| Position-to-Position, never Person-to-Person | `position_reporting_relationship_record.subordinate_position_record_id` and version `manager_position_record_id`; no Person/Assignment columns | `tests/test_position_reporting_persistence_postgres.sh` valid insert and schema contract | +| One durable solid-line identity per subordinate | unique `(tenant_record_id, subordinate_position_record_id, relationship_type_code)` anchor | valid insert plus duplicate-anchor failure supplied by database unique constraint | +| Effective/business and recorded/system time remain separate | version `effective_from/effective_to`; anchor/version `recorded_from/recorded_to` | backdated-system-time regression; bitemporal exclusion | +| System-recorded time is database-owned | `enforce_position_reporting_system_time()` requires `recorded_from = transaction_timestamp()` and open `recorded_to` | caller-backdated anchor regression | +| Same-tenant subordinate and manager | composite FKs to `position_record(tenant_record_id, position_record_id)` plus FORCE RLS | schema FK and non-bypass tenant reader regression | +| No self-reporting | `enforce_position_reporting_scope()` | self-report PostgreSQL regression | +| No effective-time management cycle | recursive effective-period intersection in `enforce_position_reporting_scope()` | A→B then B→A PostgreSQL regression | +| Human review is distinct from applying actor | exact actor-format checks plus `reviewer_actor_reference <> applied_by_actor_reference` | database constraints and valid separated actors fixture | +| Applied truth requires immutable audit/outbox evidence | scope guard verifies purpose, actor, application digest, subject, result code, review/application chronology, and integration-hub outbox | valid `record_audit_outbox_event(...)` fixture; mismatched evidence fails closed | +| Historical truth cannot be rewritten/deleted | `protect_position_reporting_history()` | manager rewrite regression | +| Table-wide destruction cannot bypass row guards | explicit BEFORE TRUNCATE guards and revoked PUBLIC TRUNCATE | TRUNCATE regression | +| Tenant isolation is enforced for ordinary app/read roles | `ENABLE` + `FORCE ROW LEVEL SECURITY`; tenant policy on both relations | `NOSUPERUSER NOBYPASSRLS` reader sees alpha=1, beta=0, missing-context=0 | +| Exact candidate execution | dedicated workflow checks out `${{ github.event.pull_request.head.sha }}` | `.github/workflows/position-reporting-persistence-quality.yml` | + +## Non-goals and buyer-safe interpretation + +Persisted reporting hierarchy describes Position structure. It does not prove which worker occupies a seat, does not create/modify Assignment, does not imply performance/compensation authority, and is not an employment decision. PR #94 remains the descriptive snapshot contract for staffable endpoint interpretation at a requested bitemporal coordinate. + +The database relation is authoritative only after the application boundary supplies valid reviewed application evidence. RLS does not replace purpose-bound authorization. Production roles must remain non-superuser and `NOBYPASSRLS`; application mutation authority, API exposure, accessible organization-chart UI, migration/rollback choreography, and release integration remain separate bounded work. From d499a24e249e7cf594f68f9c66cf85b7f1c9d73a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:08:38 -0700 Subject: [PATCH 06/32] docs(position-reporting): record primary persistence references --- ...sition-reporting-persistence-references.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 docs/doctoring/position-reporting-persistence-references.md diff --git a/docs/doctoring/position-reporting-persistence-references.md b/docs/doctoring/position-reporting-persistence-references.md new file mode 100644 index 000000000..57ee11c63 --- /dev/null +++ b/docs/doctoring/position-reporting-persistence-references.md @@ -0,0 +1,22 @@ +# Position reporting persistence — primary references + +Checked against current final PostgreSQL 16 documentation on 2026-08-24. These references justify database mechanics only; they do not imply certification or a vendor-specific HR domain standard. + +## APA 7 references + +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: Range/multirange functions and operators*. https://www.postgresql.org/docs/16/functions-range.html + +PostgreSQL Global Development Group. (2026). *PostgreSQL 16 documentation: ALTER TABLE*. https://www.postgresql.org/docs/16/sql-altertable.html + +PostgreSQL Global Development Group. (2026). *PostgreSQL 16 documentation: Function security*. https://www.postgresql.org/docs/16/perm-functions.html + +## Decision notes + +- PostgreSQL row-security policies use `USING` for row visibility and `WITH CHECK` for inserted/updated rows; policies only apply when row security is enabled. Orgmetra therefore uses both `ENABLE ROW LEVEL SECURITY` and `FORCE ROW LEVEL SECURITY` on the two owned relationship relations, with a tenant-context policy. Production application roles must still be `NOSUPERUSER NOBYPASSRLS`. +- Exclusion constraints are appropriate for preventing two bitemporal versions under the same durable relationship identity from simultaneously overlapping in effective and recorded time. PostgreSQL range types provide half-open date/timestamp intervals for this purpose. +- Cross-row cycle detection cannot be expressed as an ordinary row-local `CHECK`. The migration uses a narrow PL/pgSQL insert guard over trusted Orgmetra-owned tables, explicit tenant predicates, and effective-period intersection. PostgreSQL function-security guidance is why the function does not use caller-controlled dynamic SQL or an unsafe mutable `search_path`. +- `FORCE ROW LEVEL SECURITY` is defense in depth, not an authorization substitute. Human-review and application evidence are separately bound into immutable audit/outbox correlation before a relationship version is accepted. From 2e421d8927516dd4b67e469c25ef9cf12107ca34 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:10:08 -0700 Subject: [PATCH 07/32] test(position-reporting): make self and cycle regressions independent --- ...position_reporting_persistence_postgres.sh | 159 ++++++++---------- 1 file changed, 68 insertions(+), 91 deletions(-) diff --git a/tests/test_position_reporting_persistence_postgres.sh b/tests/test_position_reporting_persistence_postgres.sh index 8b24102ff..9a2618eac 100644 --- a/tests/test_position_reporting_persistence_postgres.sh +++ b/tests/test_position_reporting_persistence_postgres.sh @@ -25,6 +25,7 @@ OTHER_POSITION_ID="00000000-0000-7000-8000-000000000033" RELATIONSHIP_ID="00000000-0000-7000-8000-000000000041" RELATIONSHIP_VERSION_ID="00000000-0000-7000-8000-000000000042" REVERSE_RELATIONSHIP_ID="00000000-0000-7000-8000-000000000043" +SELF_RELATIONSHIP_ID="00000000-0000-7000-8000-000000000044" AUDIT_ID="00000000-0000-4000-8000-000000000051" OUTBOX_ID="00000000-0000-4000-8000-000000000052" REVIEWER="actor:00000000-0000-4000-8000-000000000061" @@ -38,10 +39,24 @@ with_tenant() { PGOPTIONS="-c orgmetra.tenant_record_id=${tenant}" command psql "$@" } +expect_failure() { + local label="$1" + local needle="$2" + local sql="$3" + local output status + set +e + output="$({ with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c "${sql}"; } 2>&1)" + status=$? + set -e + if [[ ${status} -eq 0 || "${output}" != *"${needle}"* ]]; then + echo "${label}: ${output}" >&2 + exit 1 + fi +} + with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 <&1)" -backdated_status=$? -set -e -if [[ ${backdated_status} -eq 0 || "${backdated_output}" != *"transaction timestamp"* ]]; then - echo "position-reporting anchor accepted caller-backdated system time: ${backdated_output}" >&2 - exit 1 -fi - -set +e -self_output="$({ with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c " -INSERT INTO position_reporting_relationship_record ( - tenant_record_id, position_reporting_relationship_record_id, - subordinate_position_record_id, relationship_type_code -) VALUES ( - '${TENANT_ID}', '${REVERSE_RELATIONSHIP_ID}', '${MANAGER_POSITION_ID}', 'solid_line' -); -INSERT INTO position_reporting_relationship_version ( - tenant_record_id, position_reporting_relationship_version_id, - position_reporting_relationship_record_id, manager_position_record_id, - review_evidence_digest_sha256, application_evidence_digest_sha256, - reviewer_actor_reference, applied_by_actor_reference, reviewed_at, - effective_from, audit_event_record_id -) VALUES ( - '${TENANT_ID}', '00000000-0000-7000-8000-000000000072', - '${REVERSE_RELATIONSHIP_ID}', '${MANAGER_POSITION_ID}', - '${REVIEW_DIGEST}', '${APPLICATION_DIGEST}', '${REVIEWER}', '${APPLIED_BY}', - TIMESTAMPTZ '2026-08-24 01:55:00+00', DATE '2026-08-24', '${AUDIT_ID}' -);" ; } 2>&1)" -self_status=$? -set -e -if [[ ${self_status} -eq 0 || "${self_output}" != *"cannot report to itself"* ]]; then - echo "position-reporting relationship accepted self-reporting: ${self_output}" >&2 - exit 1 -fi - -set +e -cycle_output="$({ with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c " -INSERT INTO position_reporting_relationship_version ( - tenant_record_id, position_reporting_relationship_version_id, - position_reporting_relationship_record_id, manager_position_record_id, - review_evidence_digest_sha256, application_evidence_digest_sha256, - reviewer_actor_reference, applied_by_actor_reference, reviewed_at, - effective_from, audit_event_record_id -) VALUES ( - '${TENANT_ID}', '00000000-0000-7000-8000-000000000073', - '${REVERSE_RELATIONSHIP_ID}', '${SUBORDINATE_POSITION_ID}', - '${REVIEW_DIGEST}', '${APPLICATION_DIGEST}', '${REVIEWER}', '${APPLIED_BY}', - TIMESTAMPTZ '2026-08-24 01:55:00+00', DATE '2026-08-24', '${AUDIT_ID}' -);" ; } 2>&1)" -cycle_status=$? -set -e -if [[ ${cycle_status} -eq 0 || "${cycle_output}" != *"cycle"* ]]; then - echo "position-reporting relationship accepted a management cycle: ${cycle_output}" >&2 - exit 1 -fi - -set +e -rewrite_output="$({ with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c " -UPDATE position_reporting_relationship_version -SET manager_position_record_id = '${OTHER_POSITION_ID}'::uuid -WHERE position_reporting_relationship_version_id = '${RELATIONSHIP_VERSION_ID}'::uuid;" ; } 2>&1)" -rewrite_status=$? -set -e -if [[ ${rewrite_status} -eq 0 || "${rewrite_output}" != *"history"* ]]; then - echo "position-reporting history was rewriteable: ${rewrite_output}" >&2 - exit 1 -fi - -set +e -truncate_output="$({ with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c \ - "TRUNCATE position_reporting_relationship_version;" ; } 2>&1)" -truncate_status=$? -set -e -if [[ ${truncate_status} -eq 0 || "${truncate_output}" != *"cannot be truncated"* ]]; then - echo "position-reporting history could be truncated: ${truncate_output}" >&2 - exit 1 -fi +expect_failure \ + "position-reporting anchor accepted caller-backdated system time" \ + "transaction timestamp" \ + "INSERT INTO position_reporting_relationship_record ( + tenant_record_id, position_reporting_relationship_record_id, + subordinate_position_record_id, relationship_type_code, recorded_from + ) VALUES ( + '${TENANT_ID}', '00000000-0000-7000-8000-000000000071', + '${SUBORDINATE_POSITION_ID}', 'solid_line', TIMESTAMPTZ '2000-01-01 00:00:00+00' + );" + +version_columns="tenant_record_id, position_reporting_relationship_version_id, +position_reporting_relationship_record_id, manager_position_record_id, +review_evidence_digest_sha256, application_evidence_digest_sha256, +reviewer_actor_reference, applied_by_actor_reference, reviewed_at, +effective_from, audit_event_record_id" + +expect_failure \ + "position-reporting relationship accepted self-reporting" \ + "cannot report to itself" \ + "INSERT INTO position_reporting_relationship_version (${version_columns}) VALUES ( + '${TENANT_ID}', '00000000-0000-7000-8000-000000000072', + '${SELF_RELATIONSHIP_ID}', '${OTHER_POSITION_ID}', + '${REVIEW_DIGEST}', '${APPLICATION_DIGEST}', '${REVIEWER}', '${APPLIED_BY}', + TIMESTAMPTZ '2026-08-24 01:55:00+00', DATE '2026-08-24', '${AUDIT_ID}' + );" + +expect_failure \ + "position-reporting relationship accepted a management cycle" \ + "cycle" \ + "INSERT INTO position_reporting_relationship_version (${version_columns}) VALUES ( + '${TENANT_ID}', '00000000-0000-7000-8000-000000000073', + '${REVERSE_RELATIONSHIP_ID}', '${SUBORDINATE_POSITION_ID}', + '${REVIEW_DIGEST}', '${APPLICATION_DIGEST}', '${REVIEWER}', '${APPLIED_BY}', + TIMESTAMPTZ '2026-08-24 01:55:00+00', DATE '2026-08-24', '${AUDIT_ID}' + );" + +expect_failure \ + "position-reporting history was rewriteable" \ + "history" \ + "UPDATE position_reporting_relationship_version + SET manager_position_record_id = '${OTHER_POSITION_ID}'::uuid + WHERE position_reporting_relationship_version_id = '${RELATIONSHIP_VERSION_ID}'::uuid;" + +expect_failure \ + "position-reporting history could be truncated" \ + "cannot be truncated" \ + "TRUNCATE position_reporting_relationship_version;" psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 <<'SQL' DO $$ From 8657e65788290c41bb5ca6d69288074eb037dacd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:13:34 -0700 Subject: [PATCH 08/32] test(position-reporting): require immutable review-evidence binding --- ...ition_reporting_review_binding_postgres.sh | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/test_position_reporting_review_binding_postgres.sh diff --git a/tests/test_position_reporting_review_binding_postgres.sh b/tests/test_position_reporting_review_binding_postgres.sh new file mode 100644 index 000000000..6c2484a08 --- /dev/null +++ b/tests/test_position_reporting_review_binding_postgres.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${DATABASE_URL:=postgresql://orgmetra:orgmetra@localhost:5432/orgmetra}" +TENANT_ID="10000000-0000-7000-8000-000000000001" +RELATIONSHIP_VERSION_ID="00000000-0000-7000-8000-000000000042" + +binding="$(PGOPTIONS="-c orgmetra.tenant_record_id=${TENANT_ID}" \ + psql "${DATABASE_URL}" -Atqc " +SELECT + version.review_evidence_digest_sha256 || '|' || + (audit.canonical_event_json::jsonb ->> 'orgmetraevidence') +FROM position_reporting_relationship_version AS version +JOIN audit_event_record AS audit + ON audit.tenant_record_id = version.tenant_record_id + AND audit.audit_event_record_id = version.audit_event_record_id +WHERE version.tenant_record_id = '${TENANT_ID}'::uuid + AND version.position_reporting_relationship_version_id = '${RELATIONSHIP_VERSION_ID}'::uuid; +")" + +review_digest="${binding%%|*}" +audit_evidence="${binding#*|}" +if [[ -z "${review_digest}" || "${review_digest}" != "${audit_evidence}" ]]; then + echo "position-reporting review evidence is not bound into immutable application audit evidence: ${binding}" >&2 + exit 1 +fi + +echo "position-reporting immutable review-evidence binding passed" From 566b74570e479735bbc2fedae7c6b56eb35ff3d9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:13:47 -0700 Subject: [PATCH 09/32] ci(position-reporting): gate immutable review binding --- .github/workflows/position-reporting-persistence-quality.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/position-reporting-persistence-quality.yml b/.github/workflows/position-reporting-persistence-quality.yml index 9e61f31d9..67570d7a4 100644 --- a/.github/workflows/position-reporting-persistence-quality.yml +++ b/.github/workflows/position-reporting-persistence-quality.yml @@ -8,6 +8,7 @@ on: paths: - "database/migrations/0020_position_reporting_relationship.sql" - "tests/test_position_reporting_persistence_postgres.sh" + - "tests/test_position_reporting_review_binding_postgres.sh" - "docs/adr/0106-position-reporting-persistence.md" - "docs/traceability/position-reporting-persistence.md" - "docs/doctoring/position-reporting-persistence-references.md" @@ -61,6 +62,7 @@ jobs: required = [ "tests/test_position_reporting_persistence_postgres.sh", + "tests/test_position_reporting_review_binding_postgres.sh", ".github/workflows/position-reporting-persistence-quality.yml", ] optional = [ @@ -82,6 +84,8 @@ jobs: PY - name: Run position-reporting persistence regressions run: bash tests/test_position_reporting_persistence_postgres.sh + - name: Verify immutable human-review binding + run: bash tests/test_position_reporting_review_binding_postgres.sh - name: Require clean checkout run: | git diff --exit-code From fc1fb92ad208abdc895b1f94a8d637531e126d2b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:14:37 -0700 Subject: [PATCH 10/32] fix(position-reporting): bind review evidence into immutable application audit --- .../0020_position_reporting_relationship.sql | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/database/migrations/0020_position_reporting_relationship.sql b/database/migrations/0020_position_reporting_relationship.sql index 51e94afd1..faa2316a8 100644 --- a/database/migrations/0020_position_reporting_relationship.sql +++ b/database/migrations/0020_position_reporting_relationship.sql @@ -144,7 +144,7 @@ END; $$; COMMENT ON FUNCTION protect_position_reporting_history() IS - 'Rejects deletion and in-place rewriting of position-reporting history; UPDATE may only close one open system-recorded interval at PostgreSQL transaction time.'; + 'Rejects deletion and in-place rewriting of position-reporting history; UPDATE may only close an open system-recorded interval at PostgreSQL transaction time.'; CREATE TRIGGER position_reporting_record_history_guard BEFORE UPDATE OR DELETE ON position_reporting_relationship_record @@ -165,6 +165,7 @@ DECLARE relationship_type text; anchor_recorded_to timestamptz; audit_event jsonb; + audit_event_digest text; outbox_found boolean; cycle_found boolean; BEGIN @@ -222,8 +223,8 @@ BEGIN USING ERRCODE = '23514'; END IF; - SELECT canonical_event_json::jsonb - INTO audit_event + SELECT canonical_event_json::jsonb, event_envelope_digest + INTO audit_event, audit_event_digest FROM audit_event_record WHERE tenant_record_id = NEW.tenant_record_id AND audit_event_record_id = NEW.audit_event_record_id @@ -247,12 +248,18 @@ BEGIN USING ERRCODE = '23514'; END IF; - IF audit_event ->> 'orgmetrapurpose' <> 'position_reporting_change_apply' - OR audit_event ->> 'orgmetraactor' <> NEW.applied_by_actor_reference - OR audit_event ->> 'orgmetraevidence' <> NEW.application_evidence_digest_sha256 + IF audit_event ->> 'orgmetrapurpose' + IS DISTINCT FROM 'position_reporting_change_apply' + OR audit_event ->> 'orgmetraactor' + IS DISTINCT FROM NEW.applied_by_actor_reference + OR audit_event ->> 'orgmetraevidence' + IS DISTINCT FROM NEW.review_evidence_digest_sha256 + OR audit_event_digest IS DISTINCT FROM NEW.application_evidence_digest_sha256 OR audit_event ->> 'subject' - <> 'position_reporting_relationship:' || NEW.position_reporting_relationship_record_id::text - OR audit_event #>> '{data,result_code}' <> 'position_reporting_applied' + IS DISTINCT FROM + 'position_reporting_relationship:' || NEW.position_reporting_relationship_record_id::text + OR audit_event #>> '{data,result_code}' + IS DISTINCT FROM 'position_reporting_applied' OR (audit_event #>> '{data,high_impact}')::boolean IS DISTINCT FROM false OR (audit_event ->> 'time')::timestamptz < NEW.reviewed_at OR (audit_event ->> 'time')::timestamptz > NEW.recorded_from THEN @@ -265,7 +272,7 @@ END; $$; COMMENT ON FUNCTION enforce_position_reporting_scope() IS - 'Before persistence, resolves the same-tenant relationship anchor, rejects self-reporting and cycles over overlapping effective time, and requires human-review-separated immutable audit/outbox application evidence.'; + 'Before persistence, resolves the same-tenant relationship anchor, rejects self-reporting and cycles over overlapping effective time, and requires immutable audit/outbox application evidence that binds the reviewed evidence digest, applying actor, exact application event digest, subject, result, and chronology.'; CREATE TRIGGER position_reporting_version_scope_guard BEFORE INSERT ON position_reporting_relationship_version @@ -349,4 +356,4 @@ COMMENT ON TABLE position_reporting_relationship_record IS 'Durable tenant-scoped Position-to-Position solid-line relationship anchor. The subordinate seat and relationship type are stable anchor identity; Person and Assignment are intentionally absent.'; COMMENT ON TABLE position_reporting_relationship_version IS - 'Authoritative bitemporal manager-Position versions applied only after separate human review and immutable audit/outbox evidence. The relation stores no worker PII, compensation, ratings, or employment-decision output.'; + 'Authoritative bitemporal manager-Position versions applied only after separate human review and immutable audit/outbox evidence. The application audit event must bind the review digest and its exact envelope digest. The relation stores no worker PII, compensation, ratings, or employment-decision output.'; From affa260b1e94e0040fdfcc59f7bcdad5117bd9a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:15:06 -0700 Subject: [PATCH 11/32] test(position-reporting): bind application event to reviewed evidence --- tests/test_position_reporting_persistence_postgres.sh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test_position_reporting_persistence_postgres.sh b/tests/test_position_reporting_persistence_postgres.sh index 9a2618eac..55a7d8cf8 100644 --- a/tests/test_position_reporting_persistence_postgres.sh +++ b/tests/test_position_reporting_persistence_postgres.sh @@ -31,7 +31,6 @@ OUTBOX_ID="00000000-0000-4000-8000-000000000052" REVIEWER="actor:00000000-0000-4000-8000-000000000061" APPLIED_BY="actor:00000000-0000-4000-8000-000000000062" REVIEW_DIGEST="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" -APPLICATION_DIGEST="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" with_tenant() { local tenant="$1" @@ -76,7 +75,7 @@ payload = { "datacontenttype": "application/json", "id": "${AUDIT_ID}", "orgmetraactor": "${APPLIED_BY}", - "orgmetraevidence": "${APPLICATION_DIGEST}", + "orgmetraevidence": "${REVIEW_DIGEST}", "orgmetrapurpose": "position_reporting_change_apply", "orgmetrareason": "approved_reporting_line_change", "orgmetratenant": "${TENANT_ID}", @@ -117,7 +116,7 @@ INSERT INTO position_reporting_relationship_version ( effective_from, audit_event_record_id ) VALUES ( '${TENANT_ID}', '${RELATIONSHIP_VERSION_ID}', '${RELATIONSHIP_ID}', - '${MANAGER_POSITION_ID}', '${REVIEW_DIGEST}', '${APPLICATION_DIGEST}', + '${MANAGER_POSITION_ID}', '${REVIEW_DIGEST}', :'canonical_digest', '${REVIEWER}', '${APPLIED_BY}', TIMESTAMPTZ '2026-08-24 01:55:00+00', DATE '2026-08-24', '${AUDIT_ID}' ); @@ -158,7 +157,7 @@ expect_failure \ "INSERT INTO position_reporting_relationship_version (${version_columns}) VALUES ( '${TENANT_ID}', '00000000-0000-7000-8000-000000000072', '${SELF_RELATIONSHIP_ID}', '${OTHER_POSITION_ID}', - '${REVIEW_DIGEST}', '${APPLICATION_DIGEST}', '${REVIEWER}', '${APPLIED_BY}', + '${REVIEW_DIGEST}', '${canonical_digest}', '${REVIEWER}', '${APPLIED_BY}', TIMESTAMPTZ '2026-08-24 01:55:00+00', DATE '2026-08-24', '${AUDIT_ID}' );" @@ -168,7 +167,7 @@ expect_failure \ "INSERT INTO position_reporting_relationship_version (${version_columns}) VALUES ( '${TENANT_ID}', '00000000-0000-7000-8000-000000000073', '${REVERSE_RELATIONSHIP_ID}', '${SUBORDINATE_POSITION_ID}', - '${REVIEW_DIGEST}', '${APPLICATION_DIGEST}', '${REVIEWER}', '${APPLIED_BY}', + '${REVIEW_DIGEST}', '${canonical_digest}', '${REVIEWER}', '${APPLIED_BY}', TIMESTAMPTZ '2026-08-24 01:55:00+00', DATE '2026-08-24', '${AUDIT_ID}' );" From cfef3e27258104b2de448580777271e8ac7d16bf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:15:17 -0700 Subject: [PATCH 12/32] test(position-reporting): verify exact application event digest binding --- ...st_position_reporting_review_binding_postgres.sh | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/test_position_reporting_review_binding_postgres.sh b/tests/test_position_reporting_review_binding_postgres.sh index 6c2484a08..b47c5ed3b 100644 --- a/tests/test_position_reporting_review_binding_postgres.sh +++ b/tests/test_position_reporting_review_binding_postgres.sh @@ -9,7 +9,9 @@ binding="$(PGOPTIONS="-c orgmetra.tenant_record_id=${TENANT_ID}" \ psql "${DATABASE_URL}" -Atqc " SELECT version.review_evidence_digest_sha256 || '|' || - (audit.canonical_event_json::jsonb ->> 'orgmetraevidence') + (audit.canonical_event_json::jsonb ->> 'orgmetraevidence') || '|' || + version.application_evidence_digest_sha256 || '|' || + audit.event_envelope_digest FROM position_reporting_relationship_version AS version JOIN audit_event_record AS audit ON audit.tenant_record_id = version.tenant_record_id @@ -18,11 +20,14 @@ WHERE version.tenant_record_id = '${TENANT_ID}'::uuid AND version.position_reporting_relationship_version_id = '${RELATIONSHIP_VERSION_ID}'::uuid; ")" -review_digest="${binding%%|*}" -audit_evidence="${binding#*|}" +IFS='|' read -r review_digest audit_evidence application_digest audit_digest <<<"${binding}" if [[ -z "${review_digest}" || "${review_digest}" != "${audit_evidence}" ]]; then echo "position-reporting review evidence is not bound into immutable application audit evidence: ${binding}" >&2 exit 1 fi +if [[ -z "${application_digest}" || "${application_digest}" != "${audit_digest}" ]]; then + echo "position-reporting application evidence digest is not the exact immutable audit envelope digest: ${binding}" >&2 + exit 1 +fi -echo "position-reporting immutable review-evidence binding passed" +echo "position-reporting immutable review/application audit binding passed" From 0e09e3575e766eadac058c864dc9b109a41ff69f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:15:48 -0700 Subject: [PATCH 13/32] docs(position-reporting): bind review digest to application audit --- docs/adr/0106-position-reporting-persistence.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/adr/0106-position-reporting-persistence.md b/docs/adr/0106-position-reporting-persistence.md index 0eeba92e4..1abed6e15 100644 --- a/docs/adr/0106-position-reporting-persistence.md +++ b/docs/adr/0106-position-reporting-persistence.md @@ -15,27 +15,29 @@ Orgmetra persists a solid-line reporting relationship as a normalized anchor/ver - `position_reporting_relationship_record` owns stable tenant, subordinate Position, and relationship type identity. - `position_reporting_relationship_version` owns manager Position plus effective/business and recorded/system time for each reviewed application. - one `(tenant, subordinate Position, relationship type)` anchor exists, so manager changes are versions rather than duplicate relationship identities; -- the version table carries only evidence needed to prove reviewed application: SHA-256 review/application digests, pseudonymous reviewer and applying actors, review time, immutable audit correlation, and fixed application state; +- the version table carries only evidence needed to prove reviewed application: a SHA-256 review-evidence digest, the exact SHA-256 digest of the immutable application audit envelope, pseudonymous reviewer and applying actors, review time, audit correlation, and fixed application state; - Person, Assignment, worker identity, compensation, performance rating, assessment output, and free-form HR text are not columns in this relation. New recorded intervals use PostgreSQL `transaction_timestamp()` and begin open. History cannot be rewritten or deleted; the only row update is closing one open recorded interval at the transaction timestamp. `TRUNCATE` is rejected separately. Both relations use `ENABLE ROW LEVEL SECURITY` plus `FORCE ROW LEVEL SECURITY` and tenant policies based on `current_tenant_record_id()`. -A version insert fails closed unless it has an open same-tenant anchor, a different reviewer and applying actor, immutable same-tenant audit/outbox evidence for `position_reporting_change_apply`, no self-reporting edge, and no management cycle over an overlapping effective period. Composite foreign keys bind both subordinate and manager Position IDs to the same tenant. +A version insert fails closed unless it has an open same-tenant anchor, a different reviewer and applying actor, immutable same-tenant audit/outbox evidence for `position_reporting_change_apply`, no self-reporting edge, and no management cycle over an overlapping effective period. The application audit event must carry the exact reviewed-evidence digest in `orgmetraevidence`; the version's application-evidence digest must equal the persisted audit envelope digest. This prevents a syntactically valid reviewer/digest pair from being recorded without immutable evidence binding. Composite foreign keys bind both subordinate and manager Position IDs to the same tenant. ## Why this shape The anchor/version split keeps stable relationship identity separate from changing manager/effective-time facts and therefore remains 3NF while supporting bitemporal correction. PostgreSQL exclusion/range semantics prevent overlapping business/system versions under one relationship identity; the insert guard performs cross-row cycle validation that a row-local `CHECK` constraint cannot express. RLS is defense in depth rather than authorization by itself: the application role must remain `NOSUPERUSER NOBYPASSRLS`, and high-level mutation authority remains outside this migration. +The audit binding deliberately uses the already immutable `audit_event_record` envelope as the application evidence instead of trusting an unrelated caller-provided digest. The event identifies the applying actor, tenant, relationship subject, purpose, result, time, and reviewed-evidence digest; storing its exact envelope digest makes the relationship version cryptographically correlate to that immutable application fact. + ## Integration and stack boundary This PR is a Draft descendant of #94 and cannot inherit #94 checks or reviews. It must not merge before #94. After #94 is integrated, retarget #106 to the fresh protected `develop`, reconcile any migration-number/document conflicts, and re-run full exact-head CI/security/recovery evidence. -PR #95 owns the in-memory pre-mutation review packet. This persistence slice does not copy or modify that branch; it accepts review/application digests and immutable audit evidence as the handoff boundary. A later authorized host adapter may translate a verified review packet into the database command, but direct cross-service SQL is out of scope. +PR #95 owns the in-memory pre-mutation review packet. This persistence slice does not copy or modify that branch; it accepts the review digest plus immutable application audit evidence as the handoff boundary. A later authorized host adapter may translate a verified review packet into the database command, but direct cross-service SQL is out of scope. Canonical `docs/DATA_MODEL.md` / `docs/ERD.md` are intentionally not edited in this stacked slice while independent active database PRs also own those high-conflict documents. Integration must reconcile the accepted relationship tables into canonical data-model documentation after dependency ordering is resolved. ## Consequences -The database can preserve audited supervisory hierarchy truth independently of current worker occupancy. Reads can combine persisted relationships with PR #94's staffable-Position snapshot semantics. The stricter model rejects ambiguous duplicate anchors, self-reporting, cycles, caller-backdated system time, mutation of history, and tenant-crossing references rather than silently repairing them. +The database can preserve audited supervisory hierarchy truth independently of current worker occupancy. Reads can combine persisted relationships with PR #94's staffable-Position snapshot semantics. The stricter model rejects ambiguous duplicate anchors, self-reporting, cycles, caller-backdated system time, mutation of history, tenant-crossing references, and unbound review/application evidence rather than silently repairing them. This ADR does not claim certification, branch-protection enforcement, release readiness, or authorization to make employment decisions. From a5bff30499a27f7c015d2b61f894cc32fbabdba8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:16:02 -0700 Subject: [PATCH 14/32] docs(position-reporting): trace immutable review and application binding --- docs/traceability/position-reporting-persistence.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/traceability/position-reporting-persistence.md b/docs/traceability/position-reporting-persistence.md index b5e5844a0..fc67af50d 100644 --- a/docs/traceability/position-reporting-persistence.md +++ b/docs/traceability/position-reporting-persistence.md @@ -5,7 +5,7 @@ - **Protected-main truth:** `develop@9e3e4847510e1e612b48474ba42b177b8ed824df` has no persisted Position-to-Position reporting relation. - **Parent active PR:** #94 adds bitemporal in-memory reporting reconstruction at `3f67182bb3065f2fc8fd974bfdd75a390d8a8fdc`. - **This active PR:** #106 persists reviewed solid-line Position relationships and remains a Draft stacked descendant until #94 integrates and this branch is retargeted/revalidated. -- **Separate active owner:** #95 owns the in-memory pre-mutation review packet. #106 consumes only evidence digests/audit correlation and does not rewrite #95. +- **Separate active owner:** #95 owns the in-memory pre-mutation review packet. #106 consumes only the reviewed-evidence digest plus immutable application audit correlation and does not rewrite #95. ## Requirement → implementation → executable evidence @@ -19,7 +19,8 @@ | No self-reporting | `enforce_position_reporting_scope()` | self-report PostgreSQL regression | | No effective-time management cycle | recursive effective-period intersection in `enforce_position_reporting_scope()` | A→B then B→A PostgreSQL regression | | Human review is distinct from applying actor | exact actor-format checks plus `reviewer_actor_reference <> applied_by_actor_reference` | database constraints and valid separated actors fixture | -| Applied truth requires immutable audit/outbox evidence | scope guard verifies purpose, actor, application digest, subject, result code, review/application chronology, and integration-hub outbox | valid `record_audit_outbox_event(...)` fixture; mismatched evidence fails closed | +| Reviewed evidence is immutable application evidence, not an unattested column | application audit `orgmetraevidence` must equal `review_evidence_digest_sha256`; stored application digest must equal `audit_event_record.event_envelope_digest` | `tests/test_position_reporting_review_binding_postgres.sh` | +| Applied truth requires immutable audit/outbox evidence | scope guard verifies purpose, applying actor, reviewed-evidence digest, exact audit-envelope digest, subject, result code, review/application chronology, and integration-hub outbox | valid `record_audit_outbox_event(...)` fixture; mismatched evidence fails closed | | Historical truth cannot be rewritten/deleted | `protect_position_reporting_history()` | manager rewrite regression | | Table-wide destruction cannot bypass row guards | explicit BEFORE TRUNCATE guards and revoked PUBLIC TRUNCATE | TRUNCATE regression | | Tenant isolation is enforced for ordinary app/read roles | `ENABLE` + `FORCE ROW LEVEL SECURITY`; tenant policy on both relations | `NOSUPERUSER NOBYPASSRLS` reader sees alpha=1, beta=0, missing-context=0 | From f784fd7275135a415a97da00b052a1ed5789186a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:16:55 -0700 Subject: [PATCH 15/32] test(position-reporting): prove concurrent cycle prevention --- ...position_reporting_concurrency_postgres.sh | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 tests/test_position_reporting_concurrency_postgres.sh diff --git a/tests/test_position_reporting_concurrency_postgres.sh b/tests/test_position_reporting_concurrency_postgres.sh new file mode 100644 index 000000000..4b54f1a5f --- /dev/null +++ b/tests/test_position_reporting_concurrency_postgres.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${DATABASE_URL:=postgresql://orgmetra:orgmetra@localhost:5432/orgmetra}" +TENANT_ID="10000000-0000-7000-8000-000000000001" +ORG_ID="00000000-0000-7000-8000-000000000011" +JOB_ID="00000000-0000-7000-8000-000000000021" +POSITION_X="00000000-0000-7000-8000-000000000081" +POSITION_Y="00000000-0000-7000-8000-000000000082" +REL_X="00000000-0000-7000-8000-000000000083" +REL_Y="00000000-0000-7000-8000-000000000084" +VERSION_X="00000000-0000-7000-8000-000000000085" +VERSION_Y="00000000-0000-7000-8000-000000000086" +AUDIT_X="00000000-0000-4000-8000-000000000087" +AUDIT_Y="00000000-0000-4000-8000-000000000088" +OUTBOX_X="00000000-0000-4000-8000-000000000089" +OUTBOX_Y="00000000-0000-4000-8000-000000000090" +REVIEWER_X="actor:00000000-0000-4000-8000-000000000091" +REVIEWER_Y="actor:00000000-0000-4000-8000-000000000092" +APPLIER_X="actor:00000000-0000-4000-8000-000000000093" +APPLIER_Y="actor:00000000-0000-4000-8000-000000000094" +REVIEW_DIGEST_X="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" +REVIEW_DIGEST_Y="dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + +with_tenant() { + local tenant="$1" + shift + PGOPTIONS="-c orgmetra.tenant_record_id=${tenant}" command psql "$@" +} + +with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 <"${log_x}" 2>&1) & +pid_x=$! +sleep 0.5 +with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c "${sql_y}" >"${log_y}" 2>&1 +status_y=$? +wait "${pid_x}" +status_x=$? +set -e + +if [[ ${status_x} -ne 0 ]]; then + echo "first concurrent position-reporting mutation unexpectedly failed:" >&2 + cat "${log_x}" >&2 + exit 1 +fi +if [[ ${status_y} -eq 0 || "$(cat "${log_y}")" != *"cycle"* ]]; then + echo "concurrent opposite reporting mutations committed a management cycle instead of serializing fail-closed:" >&2 + cat "${log_y}" >&2 + exit 1 +fi + +committed_count="$(with_tenant "${TENANT_ID}" "${DATABASE_URL}" -Atqc " +SELECT count(*) +FROM position_reporting_relationship_version +WHERE position_reporting_relationship_version_id IN ('${VERSION_X}'::uuid, '${VERSION_Y}'::uuid); +")" +if [[ "${committed_count}" != "1" ]]; then + echo "concurrent reporting mutation did not leave exactly one non-cyclic committed edge: ${committed_count}" >&2 + exit 1 +fi + +echo "position-reporting concurrent cycle prevention passed" From 69923053bb8f39fbe9ea876c33443c38b5ef4b63 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:17:10 -0700 Subject: [PATCH 16/32] ci(position-reporting): gate concurrent graph mutation safety --- .github/workflows/position-reporting-persistence-quality.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/position-reporting-persistence-quality.yml b/.github/workflows/position-reporting-persistence-quality.yml index 67570d7a4..a931f2edf 100644 --- a/.github/workflows/position-reporting-persistence-quality.yml +++ b/.github/workflows/position-reporting-persistence-quality.yml @@ -9,6 +9,7 @@ on: - "database/migrations/0020_position_reporting_relationship.sql" - "tests/test_position_reporting_persistence_postgres.sh" - "tests/test_position_reporting_review_binding_postgres.sh" + - "tests/test_position_reporting_concurrency_postgres.sh" - "docs/adr/0106-position-reporting-persistence.md" - "docs/traceability/position-reporting-persistence.md" - "docs/doctoring/position-reporting-persistence-references.md" @@ -63,6 +64,7 @@ jobs: required = [ "tests/test_position_reporting_persistence_postgres.sh", "tests/test_position_reporting_review_binding_postgres.sh", + "tests/test_position_reporting_concurrency_postgres.sh", ".github/workflows/position-reporting-persistence-quality.yml", ] optional = [ @@ -86,6 +88,8 @@ jobs: run: bash tests/test_position_reporting_persistence_postgres.sh - name: Verify immutable human-review binding run: bash tests/test_position_reporting_review_binding_postgres.sh + - name: Verify concurrent cycle prevention + run: bash tests/test_position_reporting_concurrency_postgres.sh - name: Require clean checkout run: | git diff --exit-code From df13397ded4dfa6b605b7ded7cfb7e4468d3f0fc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:17:50 -0700 Subject: [PATCH 17/32] fix(position-reporting): serialize tenant graph mutations before cycle checks --- .../0020_position_reporting_relationship.sql | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/database/migrations/0020_position_reporting_relationship.sql b/database/migrations/0020_position_reporting_relationship.sql index faa2316a8..b4efd5abd 100644 --- a/database/migrations/0020_position_reporting_relationship.sql +++ b/database/migrations/0020_position_reporting_relationship.sql @@ -169,6 +169,19 @@ DECLARE outbox_found boolean; cycle_found boolean; BEGIN + -- Graph validity is a tenant-wide invariant. Concurrent opposite edge + -- inserts can otherwise both validate before either commits. A transaction + -- advisory lock serializes graph mutations for one tenant while still + -- allowing different tenants to progress independently. This trigger is + -- VOLATILE, so the queries after lock acquisition use current committed + -- state under PostgreSQL READ COMMITTED semantics. + PERFORM pg_catalog.pg_advisory_xact_lock( + pg_catalog.hashtextextended( + 'orgmetra_position_reporting:' || NEW.tenant_record_id::text, + 0 + ) + ); + SELECT subordinate_position_record_id, relationship_type_code, recorded_to INTO subordinate_position_id, relationship_type, anchor_recorded_to FROM position_reporting_relationship_record @@ -272,7 +285,7 @@ END; $$; COMMENT ON FUNCTION enforce_position_reporting_scope() IS - 'Before persistence, resolves the same-tenant relationship anchor, rejects self-reporting and cycles over overlapping effective time, and requires immutable audit/outbox application evidence that binds the reviewed evidence digest, applying actor, exact application event digest, subject, result, and chronology.'; + 'Before persistence, serializes one tenant reporting graph, resolves the same-tenant relationship anchor, rejects self-reporting and cycles over overlapping effective time, and requires immutable audit/outbox application evidence that binds the reviewed evidence digest, applying actor, exact application event digest, subject, result, and chronology.'; CREATE TRIGGER position_reporting_version_scope_guard BEFORE INSERT ON position_reporting_relationship_version From 2e948a0cfeb7c8ee77ae577bc55fc899b18fc01e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:18:22 -0700 Subject: [PATCH 18/32] docs(position-reporting): record concurrent graph serialization --- docs/adr/0106-position-reporting-persistence.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/adr/0106-position-reporting-persistence.md b/docs/adr/0106-position-reporting-persistence.md index 1abed6e15..7097798a3 100644 --- a/docs/adr/0106-position-reporting-persistence.md +++ b/docs/adr/0106-position-reporting-persistence.md @@ -22,12 +22,16 @@ New recorded intervals use PostgreSQL `transaction_timestamp()` and begin open. A version insert fails closed unless it has an open same-tenant anchor, a different reviewer and applying actor, immutable same-tenant audit/outbox evidence for `position_reporting_change_apply`, no self-reporting edge, and no management cycle over an overlapping effective period. The application audit event must carry the exact reviewed-evidence digest in `orgmetraevidence`; the version's application-evidence digest must equal the persisted audit envelope digest. This prevents a syntactically valid reviewer/digest pair from being recorded without immutable evidence binding. Composite foreign keys bind both subordinate and manager Position IDs to the same tenant. +Graph acyclicity is tenant-wide and cannot be protected by independent row constraints alone. Before a version trigger reads the graph, it obtains a transaction-scoped PostgreSQL advisory lock keyed from the tenant UUID. Opposite concurrent mutations for one tenant are therefore serialized, while different tenants can proceed independently. The PL/pgSQL trigger remains the default `VOLATILE`; PostgreSQL documents that a `VOLATILE` function obtains a fresh snapshot for each SQL query it executes. After a waiting transaction acquires the tenant graph lock, its recursive graph query therefore sees the relationship committed by the preceding lock holder and rejects the opposite edge as a cycle. Hash-key collisions can only over-serialize unrelated tenants; they cannot relax the invariant. + ## Why this shape The anchor/version split keeps stable relationship identity separate from changing manager/effective-time facts and therefore remains 3NF while supporting bitemporal correction. PostgreSQL exclusion/range semantics prevent overlapping business/system versions under one relationship identity; the insert guard performs cross-row cycle validation that a row-local `CHECK` constraint cannot express. RLS is defense in depth rather than authorization by itself: the application role must remain `NOSUPERUSER NOBYPASSRLS`, and high-level mutation authority remains outside this migration. The audit binding deliberately uses the already immutable `audit_event_record` envelope as the application evidence instead of trusting an unrelated caller-provided digest. The event identifies the applying actor, tenant, relationship subject, purpose, result, time, and reviewed-evidence digest; storing its exact envelope digest makes the relationship version cryptographically correlate to that immutable application fact. +The tenant advisory lock is intentionally narrow: it protects only graph mutation validation and is transaction-scoped. It does not replace transaction boundaries, RLS, authorization, or history guards. Its purpose is to make cycle validation defensible under concurrent writes rather than merely correct in single-session tests. + ## Integration and stack boundary This PR is a Draft descendant of #94 and cannot inherit #94 checks or reviews. It must not merge before #94. After #94 is integrated, retarget #106 to the fresh protected `develop`, reconcile any migration-number/document conflicts, and re-run full exact-head CI/security/recovery evidence. @@ -38,6 +42,6 @@ Canonical `docs/DATA_MODEL.md` / `docs/ERD.md` are intentionally not edited in t ## Consequences -The database can preserve audited supervisory hierarchy truth independently of current worker occupancy. Reads can combine persisted relationships with PR #94's staffable-Position snapshot semantics. The stricter model rejects ambiguous duplicate anchors, self-reporting, cycles, caller-backdated system time, mutation of history, tenant-crossing references, and unbound review/application evidence rather than silently repairing them. +The database can preserve audited supervisory hierarchy truth independently of current worker occupancy. Reads can combine persisted relationships with PR #94's staffable-Position snapshot semantics. The stricter model rejects ambiguous duplicate anchors, self-reporting, single-session and concurrent cycles, caller-backdated system time, mutation of history, tenant-crossing references, and unbound review/application evidence rather than silently repairing them. This ADR does not claim certification, branch-protection enforcement, release readiness, or authorization to make employment decisions. From 5c6cc23586fe14934d46bae856ed388fff93c37f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:18:36 -0700 Subject: [PATCH 19/32] docs(position-reporting): trace concurrent cycle prevention --- docs/traceability/position-reporting-persistence.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/traceability/position-reporting-persistence.md b/docs/traceability/position-reporting-persistence.md index fc67af50d..ffe39a623 100644 --- a/docs/traceability/position-reporting-persistence.md +++ b/docs/traceability/position-reporting-persistence.md @@ -17,7 +17,8 @@ | System-recorded time is database-owned | `enforce_position_reporting_system_time()` requires `recorded_from = transaction_timestamp()` and open `recorded_to` | caller-backdated anchor regression | | Same-tenant subordinate and manager | composite FKs to `position_record(tenant_record_id, position_record_id)` plus FORCE RLS | schema FK and non-bypass tenant reader regression | | No self-reporting | `enforce_position_reporting_scope()` | self-report PostgreSQL regression | -| No effective-time management cycle | recursive effective-period intersection in `enforce_position_reporting_scope()` | A→B then B→A PostgreSQL regression | +| No effective-time management cycle in one session | recursive effective-period intersection in `enforce_position_reporting_scope()` | A→B then B→A PostgreSQL regression | +| Concurrent opposite graph mutations cannot both commit | transaction-scoped tenant advisory lock is acquired before the VOLATILE trigger's graph queries | `tests/test_position_reporting_concurrency_postgres.sh` holds X→Y open while Y→X races; exactly one edge may commit | | Human review is distinct from applying actor | exact actor-format checks plus `reviewer_actor_reference <> applied_by_actor_reference` | database constraints and valid separated actors fixture | | Reviewed evidence is immutable application evidence, not an unattested column | application audit `orgmetraevidence` must equal `review_evidence_digest_sha256`; stored application digest must equal `audit_event_record.event_envelope_digest` | `tests/test_position_reporting_review_binding_postgres.sh` | | Applied truth requires immutable audit/outbox evidence | scope guard verifies purpose, applying actor, reviewed-evidence digest, exact audit-envelope digest, subject, result code, review/application chronology, and integration-hub outbox | valid `record_audit_outbox_event(...)` fixture; mismatched evidence fails closed | From c5dd5e5890ecd0c8961da17ff64d99d210d5703f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:18:50 -0700 Subject: [PATCH 20/32] docs(position-reporting): cite concurrency snapshot and advisory-lock semantics --- .../position-reporting-persistence-references.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/doctoring/position-reporting-persistence-references.md b/docs/doctoring/position-reporting-persistence-references.md index 57ee11c63..b668374d4 100644 --- a/docs/doctoring/position-reporting-persistence-references.md +++ b/docs/doctoring/position-reporting-persistence-references.md @@ -14,9 +14,16 @@ PostgreSQL Global Development Group. (2026). *PostgreSQL 16 documentation: ALTER PostgreSQL Global Development Group. (2026). *PostgreSQL 16 documentation: Function security*. https://www.postgresql.org/docs/16/perm-functions.html +PostgreSQL Global Development Group. (2026). *PostgreSQL 16 documentation: Function volatility categories*. https://www.postgresql.org/docs/16/xfunc-volatility.html + +PostgreSQL Global Development Group. (2026). *PostgreSQL 16 documentation: System administration functions*. https://www.postgresql.org/docs/16/functions-admin.html + +PostgreSQL Global Development Group. (2026). *PostgreSQL 16 documentation: Visibility of data changes*. https://www.postgresql.org/docs/16/spi-visibility.html + ## Decision notes - PostgreSQL row-security policies use `USING` for row visibility and `WITH CHECK` for inserted/updated rows; policies only apply when row security is enabled. Orgmetra therefore uses both `ENABLE ROW LEVEL SECURITY` and `FORCE ROW LEVEL SECURITY` on the two owned relationship relations, with a tenant-context policy. Production application roles must still be `NOSUPERUSER NOBYPASSRLS`. - Exclusion constraints are appropriate for preventing two bitemporal versions under the same durable relationship identity from simultaneously overlapping in effective and recorded time. PostgreSQL range types provide half-open date/timestamp intervals for this purpose. - Cross-row cycle detection cannot be expressed as an ordinary row-local `CHECK`. The migration uses a narrow PL/pgSQL insert guard over trusted Orgmetra-owned tables, explicit tenant predicates, and effective-period intersection. PostgreSQL function-security guidance is why the function does not use caller-controlled dynamic SQL or an unsafe mutable `search_path`. -- `FORCE ROW LEVEL SECURITY` is defense in depth, not an authorization substitute. Human-review and application evidence are separately bound into immutable audit/outbox correlation before a relationship version is accepted. +- A single-session cycle check is insufficient under concurrent opposite mutations because each transaction could validate before the other commits. The trigger therefore takes a transaction-scoped advisory lock keyed from the tenant before querying the reporting graph. PostgreSQL documents that transaction advisory locks are held to transaction end, and that standard procedural `VOLATILE` functions execute their SQL commands in read-write SPI mode with a fresh snapshot for each query. Consequently, a waiter performs its graph lookup after the preceding lock holder commits and can observe/reject the newly created cycle. The dedicated concurrency regression proves this behavior against PostgreSQL rather than relying on the documentation alone. +- `FORCE ROW LEVEL SECURITY` is defense in depth, not an authorization substitute. Human-review and application evidence are separately bound into immutable audit/outbox correlation before a relationship version is accepted. The application event's `orgmetraevidence` is the reviewed-evidence digest, and the persisted application digest must equal the immutable audit envelope digest. From 8f0a874761bcf1f84e7d0544fabf9394178773ba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 20:02:56 -0700 Subject: [PATCH 21/32] test(position-reporting): require staffable endpoint coverage --- ...position_reporting_persistence_postgres.sh | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/tests/test_position_reporting_persistence_postgres.sh b/tests/test_position_reporting_persistence_postgres.sh index 55a7d8cf8..3b1d7a055 100644 --- a/tests/test_position_reporting_persistence_postgres.sh +++ b/tests/test_position_reporting_persistence_postgres.sh @@ -22,6 +22,9 @@ JOB_ID="00000000-0000-7000-8000-000000000021" SUBORDINATE_POSITION_ID="00000000-0000-7000-8000-000000000031" MANAGER_POSITION_ID="00000000-0000-7000-8000-000000000032" OTHER_POSITION_ID="00000000-0000-7000-8000-000000000033" +SUBORDINATE_POSITION_VERSION_ID="00000000-0000-7000-8000-000000000034" +MANAGER_POSITION_VERSION_ID="00000000-0000-7000-8000-000000000035" +OTHER_POSITION_VERSION_ID="00000000-0000-7000-8000-000000000036" RELATIONSHIP_ID="00000000-0000-7000-8000-000000000041" RELATIONSHIP_VERSION_ID="00000000-0000-7000-8000-000000000042" REVERSE_RELATIONSHIP_ID="00000000-0000-7000-8000-000000000043" @@ -108,12 +111,38 @@ INSERT INTO position_reporting_relationship_record ( ('${TENANT_ID}', '${RELATIONSHIP_ID}', '${SUBORDINATE_POSITION_ID}', 'solid_line'), ('${TENANT_ID}', '${REVERSE_RELATIONSHIP_ID}', '${MANAGER_POSITION_ID}', 'solid_line'), ('${TENANT_ID}', '${SELF_RELATIONSHIP_ID}', '${OTHER_POSITION_ID}', 'solid_line'); +SQL + +version_columns="tenant_record_id, position_reporting_relationship_version_id, +position_reporting_relationship_record_id, manager_position_record_id, +review_evidence_digest_sha256, application_evidence_digest_sha256, +reviewer_actor_reference, applied_by_actor_reference, reviewed_at, +effective_from, audit_event_record_id" + +expect_failure \ + "position-reporting relationship accepted endpoints without staffable PositionVersion coverage" \ + "staffable PositionVersion coverage" \ + "INSERT INTO position_reporting_relationship_version (${version_columns}) VALUES ( + '${TENANT_ID}', '${RELATIONSHIP_VERSION_ID}', '${RELATIONSHIP_ID}', + '${MANAGER_POSITION_ID}', '${REVIEW_DIGEST}', '${canonical_digest}', + '${REVIEWER}', '${APPLIED_BY}', TIMESTAMPTZ '2026-08-24 01:55:00+00', + DATE '2026-08-24', '${AUDIT_ID}' + );" + +with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 < Date: Sun, 23 Aug 2026 20:04:17 -0700 Subject: [PATCH 22/32] fix(position-reporting): require staffable endpoint coverage --- .../0020_position_reporting_relationship.sql | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/database/migrations/0020_position_reporting_relationship.sql b/database/migrations/0020_position_reporting_relationship.sql index b4efd5abd..2dbc7ac80 100644 --- a/database/migrations/0020_position_reporting_relationship.sql +++ b/database/migrations/0020_position_reporting_relationship.sql @@ -156,6 +156,41 @@ BEFORE UPDATE OR DELETE ON position_reporting_relationship_version FOR EACH ROW EXECUTE FUNCTION protect_position_reporting_history(); +CREATE FUNCTION position_reporting_has_staffable_coverage( + checked_tenant_record_id uuid, + checked_position_record_id uuid, + checked_effective_from date, + checked_effective_to date, + checked_known_at timestamptz +) +RETURNS boolean +LANGUAGE sql +STABLE +AS $$ + SELECT COALESCE( + pg_catalog.range_agg( + daterange(version.effective_from, version.effective_to, '[)') + ) @> daterange(checked_effective_from, checked_effective_to, '[)'), + false + ) + FROM position_record AS record + JOIN position_record_version AS version + ON version.tenant_record_id = record.tenant_record_id + AND version.position_record_id = record.position_record_id + WHERE record.tenant_record_id = checked_tenant_record_id + AND record.position_record_id = checked_position_record_id + AND record.recorded_from <= checked_known_at + AND (record.recorded_to IS NULL OR checked_known_at < record.recorded_to) + AND version.recorded_from <= checked_known_at + AND (version.recorded_to IS NULL OR checked_known_at < version.recorded_to) + AND version.position_status_code IN ('active', 'open') + AND daterange(version.effective_from, version.effective_to, '[)') && + daterange(checked_effective_from, checked_effective_to, '[)'); +$$; + +COMMENT ON FUNCTION position_reporting_has_staffable_coverage(uuid, uuid, date, date, timestamptz) IS + 'Returns true only when one tenant Position is system-visible and its active/open PositionVersion union covers the entire proposed reporting effective interval at the proposed recorded-time coordinate.'; + CREATE FUNCTION enforce_position_reporting_scope() RETURNS trigger LANGUAGE plpgsql @@ -202,6 +237,23 @@ BEGIN USING ERRCODE = '23514'; END IF; + IF NOT position_reporting_has_staffable_coverage( + NEW.tenant_record_id, + subordinate_position_id, + NEW.effective_from, + NEW.effective_to, + NEW.recorded_from + ) OR NOT position_reporting_has_staffable_coverage( + NEW.tenant_record_id, + NEW.manager_position_record_id, + NEW.effective_from, + NEW.effective_to, + NEW.recorded_from + ) THEN + RAISE EXCEPTION 'position-reporting endpoints require staffable PositionVersion coverage for the entire effective interval' + USING ERRCODE = '23514'; + END IF; + WITH RECURSIVE manager_path(position_record_id, effective_period) AS ( SELECT NEW.manager_position_record_id, @@ -285,7 +337,7 @@ END; $$; COMMENT ON FUNCTION enforce_position_reporting_scope() IS - 'Before persistence, serializes one tenant reporting graph, resolves the same-tenant relationship anchor, rejects self-reporting and cycles over overlapping effective time, and requires immutable audit/outbox application evidence that binds the reviewed evidence digest, applying actor, exact application event digest, subject, result, and chronology.'; + 'Before persistence, serializes one tenant reporting graph, resolves the same-tenant relationship anchor, requires staffable subordinate/manager PositionVersion coverage for the full effective interval, rejects self-reporting and cycles over overlapping effective time, and requires immutable audit/outbox application evidence that binds the reviewed evidence digest, applying actor, exact application event digest, subject, result, and chronology.'; CREATE TRIGGER position_reporting_version_scope_guard BEFORE INSERT ON position_reporting_relationship_version @@ -369,4 +421,4 @@ COMMENT ON TABLE position_reporting_relationship_record IS 'Durable tenant-scoped Position-to-Position solid-line relationship anchor. The subordinate seat and relationship type are stable anchor identity; Person and Assignment are intentionally absent.'; COMMENT ON TABLE position_reporting_relationship_version IS - 'Authoritative bitemporal manager-Position versions applied only after separate human review and immutable audit/outbox evidence. The application audit event must bind the review digest and its exact envelope digest. The relation stores no worker PII, compensation, ratings, or employment-decision output.'; + 'Authoritative bitemporal manager-Position versions applied only after separate human review and immutable audit/outbox evidence. Both Position endpoints must remain staffable for the full effective interval at the recorded-time coordinate. The application audit event must bind the review digest and its exact envelope digest. The relation stores no worker PII, compensation, ratings, or employment-decision output.'; From 5b9833a7fc5d7882f3f41c0ef452ef209327342e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 20:05:05 -0700 Subject: [PATCH 23/32] docs(position-reporting): bind persistence to staffable endpoints --- docs/adr/0106-position-reporting-persistence.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/adr/0106-position-reporting-persistence.md b/docs/adr/0106-position-reporting-persistence.md index 7097798a3..41ba068ce 100644 --- a/docs/adr/0106-position-reporting-persistence.md +++ b/docs/adr/0106-position-reporting-persistence.md @@ -22,12 +22,16 @@ New recorded intervals use PostgreSQL `transaction_timestamp()` and begin open. A version insert fails closed unless it has an open same-tenant anchor, a different reviewer and applying actor, immutable same-tenant audit/outbox evidence for `position_reporting_change_apply`, no self-reporting edge, and no management cycle over an overlapping effective period. The application audit event must carry the exact reviewed-evidence digest in `orgmetraevidence`; the version's application-evidence digest must equal the persisted audit envelope digest. This prevents a syntactically valid reviewer/digest pair from being recorded without immutable evidence binding. Composite foreign keys bind both subordinate and manager Position IDs to the same tenant. +The database also enforces the staffable endpoint contract already defined by parent PR #94. At the proposed relationship version's system-recorded coordinate, both the subordinate and manager Position anchors must be visible and the union of same-tenant `active`/`open` `position_record_version` effective ranges must cover the relationship's entire effective interval. PostgreSQL `range_agg(daterange(...))` produces a normalized date multirange, and multirange containment proves full coverage without enumerating days or incorrectly rejecting legitimate contiguous active/open Position versions. A stable Position record without staffable bitemporal PositionVersion coverage is therefore insufficient persistence evidence. + Graph acyclicity is tenant-wide and cannot be protected by independent row constraints alone. Before a version trigger reads the graph, it obtains a transaction-scoped PostgreSQL advisory lock keyed from the tenant UUID. Opposite concurrent mutations for one tenant are therefore serialized, while different tenants can proceed independently. The PL/pgSQL trigger remains the default `VOLATILE`; PostgreSQL documents that a `VOLATILE` function obtains a fresh snapshot for each SQL query it executes. After a waiting transaction acquires the tenant graph lock, its recursive graph query therefore sees the relationship committed by the preceding lock holder and rejects the opposite edge as a cycle. Hash-key collisions can only over-serialize unrelated tenants; they cannot relax the invariant. ## Why this shape The anchor/version split keeps stable relationship identity separate from changing manager/effective-time facts and therefore remains 3NF while supporting bitemporal correction. PostgreSQL exclusion/range semantics prevent overlapping business/system versions under one relationship identity; the insert guard performs cross-row cycle validation that a row-local `CHECK` constraint cannot express. RLS is defense in depth rather than authorization by itself: the application role must remain `NOSUPERUSER NOBYPASSRLS`, and high-level mutation authority remains outside this migration. +The staffable coverage check deliberately validates the full reporting interval rather than only its start date. Otherwise a relationship could be stored as authoritative while one endpoint has no active/open PositionVersion for later days that the relationship itself claims to cover. The database check therefore matches the descriptive snapshot semantics instead of relying on downstream readers to discover and reject internally inconsistent persisted truth. + The audit binding deliberately uses the already immutable `audit_event_record` envelope as the application evidence instead of trusting an unrelated caller-provided digest. The event identifies the applying actor, tenant, relationship subject, purpose, result, time, and reviewed-evidence digest; storing its exact envelope digest makes the relationship version cryptographically correlate to that immutable application fact. The tenant advisory lock is intentionally narrow: it protects only graph mutation validation and is transaction-scoped. It does not replace transaction boundaries, RLS, authorization, or history guards. Its purpose is to make cycle validation defensible under concurrent writes rather than merely correct in single-session tests. @@ -42,6 +46,6 @@ Canonical `docs/DATA_MODEL.md` / `docs/ERD.md` are intentionally not edited in t ## Consequences -The database can preserve audited supervisory hierarchy truth independently of current worker occupancy. Reads can combine persisted relationships with PR #94's staffable-Position snapshot semantics. The stricter model rejects ambiguous duplicate anchors, self-reporting, single-session and concurrent cycles, caller-backdated system time, mutation of history, tenant-crossing references, and unbound review/application evidence rather than silently repairing them. +The database can preserve audited supervisory hierarchy truth independently of current worker occupancy. Reads can combine persisted relationships with PR #94's staffable-Position snapshot semantics, and persistence now rejects an edge whose PositionVersion evidence cannot support every day that the edge claims to cover. The stricter model rejects ambiguous duplicate anchors, self-reporting, single-session and concurrent cycles, caller-backdated system time, mutation of history, tenant-crossing references, non-staffable endpoint coverage, and unbound review/application evidence rather than silently repairing them. This ADR does not claim certification, branch-protection enforcement, release readiness, or authorization to make employment decisions. From 760b0cd0799e051d726c76232cd8926c00743a83 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 20:05:18 -0700 Subject: [PATCH 24/32] docs(position-reporting): trace endpoint coverage repair --- docs/traceability/position-reporting-persistence.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/traceability/position-reporting-persistence.md b/docs/traceability/position-reporting-persistence.md index ffe39a623..f8305eac5 100644 --- a/docs/traceability/position-reporting-persistence.md +++ b/docs/traceability/position-reporting-persistence.md @@ -16,6 +16,7 @@ | Effective/business and recorded/system time remain separate | version `effective_from/effective_to`; anchor/version `recorded_from/recorded_to` | backdated-system-time regression; bitemporal exclusion | | System-recorded time is database-owned | `enforce_position_reporting_system_time()` requires `recorded_from = transaction_timestamp()` and open `recorded_to` | caller-backdated anchor regression | | Same-tenant subordinate and manager | composite FKs to `position_record(tenant_record_id, position_record_id)` plus FORCE RLS | schema FK and non-bypass tenant reader regression | +| Both endpoints are staffable for the full reporting interval | `position_reporting_has_staffable_coverage(...)` resolves system-visible Position anchors and uses `range_agg(daterange(...))` over same-tenant `active`/`open` PositionVersion rows; the resulting multirange must contain the entire reporting effective range | regression first attempts persistence before any PositionVersion exists and requires `staffable PositionVersion coverage`; valid fixture succeeds only after active/open endpoint versions are inserted | | No self-reporting | `enforce_position_reporting_scope()` | self-report PostgreSQL regression | | No effective-time management cycle in one session | recursive effective-period intersection in `enforce_position_reporting_scope()` | A→B then B→A PostgreSQL regression | | Concurrent opposite graph mutations cannot both commit | transaction-scoped tenant advisory lock is acquired before the VOLATILE trigger's graph queries | `tests/test_position_reporting_concurrency_postgres.sh` holds X→Y open while Y→X races; exactly one edge may commit | @@ -29,6 +30,6 @@ ## Non-goals and buyer-safe interpretation -Persisted reporting hierarchy describes Position structure. It does not prove which worker occupies a seat, does not create/modify Assignment, does not imply performance/compensation authority, and is not an employment decision. PR #94 remains the descriptive snapshot contract for staffable endpoint interpretation at a requested bitemporal coordinate. +Persisted reporting hierarchy describes Position structure. It does not prove which worker occupies a seat, does not create/modify Assignment, does not imply performance/compensation authority, and is not an employment decision. PR #94 remains the descriptive snapshot contract for staffable endpoint interpretation at a requested bitemporal coordinate; #106 now rejects persistence that could not satisfy that staffable Position contract across the relationship's own effective interval. The database relation is authoritative only after the application boundary supplies valid reviewed application evidence. RLS does not replace purpose-bound authorization. Production roles must remain non-superuser and `NOBYPASSRLS`; application mutation authority, API exposure, accessible organization-chart UI, migration/rollback choreography, and release integration remain separate bounded work. From 4544d8ae5d6ed8a751dab1cab5af1f27623e32f2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 20:05:28 -0700 Subject: [PATCH 25/32] docs(position-reporting): cite multirange coverage semantics --- docs/doctoring/position-reporting-persistence-references.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/doctoring/position-reporting-persistence-references.md b/docs/doctoring/position-reporting-persistence-references.md index b668374d4..8eb23b03c 100644 --- a/docs/doctoring/position-reporting-persistence-references.md +++ b/docs/doctoring/position-reporting-persistence-references.md @@ -10,6 +10,8 @@ PostgreSQL Global Development Group. (2026). *PostgreSQL 16 documentation: Const PostgreSQL Global Development Group. (2026). *PostgreSQL 16 documentation: Range/multirange functions and operators*. https://www.postgresql.org/docs/16/functions-range.html +PostgreSQL Global Development Group. (2026). *PostgreSQL 16 documentation: Aggregate functions*. https://www.postgresql.org/docs/16/functions-aggregate.html + PostgreSQL Global Development Group. (2026). *PostgreSQL 16 documentation: ALTER TABLE*. https://www.postgresql.org/docs/16/sql-altertable.html PostgreSQL Global Development Group. (2026). *PostgreSQL 16 documentation: Function security*. https://www.postgresql.org/docs/16/perm-functions.html @@ -24,6 +26,7 @@ PostgreSQL Global Development Group. (2026). *PostgreSQL 16 documentation: Visib - PostgreSQL row-security policies use `USING` for row visibility and `WITH CHECK` for inserted/updated rows; policies only apply when row security is enabled. Orgmetra therefore uses both `ENABLE ROW LEVEL SECURITY` and `FORCE ROW LEVEL SECURITY` on the two owned relationship relations, with a tenant-context policy. Production application roles must still be `NOSUPERUSER NOBYPASSRLS`. - Exclusion constraints are appropriate for preventing two bitemporal versions under the same durable relationship identity from simultaneously overlapping in effective and recorded time. PostgreSQL range types provide half-open date/timestamp intervals for this purpose. +- PostgreSQL documents `range_agg(anyrange)` as the union of non-null input ranges returned as a multirange, and multirange containment supports asking whether that union contains one target range. Orgmetra uses this to prove that system-visible same-tenant `active`/`open` PositionVersion intervals cover the reporting relationship's entire effective range without iterating individual dates. The check also requires the stable Position anchor itself to be visible at the same recorded-time coordinate. - Cross-row cycle detection cannot be expressed as an ordinary row-local `CHECK`. The migration uses a narrow PL/pgSQL insert guard over trusted Orgmetra-owned tables, explicit tenant predicates, and effective-period intersection. PostgreSQL function-security guidance is why the function does not use caller-controlled dynamic SQL or an unsafe mutable `search_path`. - A single-session cycle check is insufficient under concurrent opposite mutations because each transaction could validate before the other commits. The trigger therefore takes a transaction-scoped advisory lock keyed from the tenant before querying the reporting graph. PostgreSQL documents that transaction advisory locks are held to transaction end, and that standard procedural `VOLATILE` functions execute their SQL commands in read-write SPI mode with a fresh snapshot for each query. Consequently, a waiter performs its graph lookup after the preceding lock holder commits and can observe/reject the newly created cycle. The dedicated concurrency regression proves this behavior against PostgreSQL rather than relying on the documentation alone. - `FORCE ROW LEVEL SECURITY` is defense in depth, not an authorization substitute. Human-review and application evidence are separately bound into immutable audit/outbox correlation before a relationship version is accepted. The application event's `orgmetraevidence` is the reviewed-evidence digest, and the persisted application digest must equal the immutable audit envelope digest. From 61611908b49a5ff74f2b7f10a2883a2572070b54 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 20:59:54 -0700 Subject: [PATCH 26/32] test(position-reporting): restore concurrency fixture coverage --- tests/test_position_reporting_concurrency_postgres.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_position_reporting_concurrency_postgres.sh b/tests/test_position_reporting_concurrency_postgres.sh index 4b54f1a5f..d6987dea7 100644 --- a/tests/test_position_reporting_concurrency_postgres.sh +++ b/tests/test_position_reporting_concurrency_postgres.sh @@ -19,6 +19,8 @@ REVIEWER_X="actor:00000000-0000-4000-8000-000000000091" REVIEWER_Y="actor:00000000-0000-4000-8000-000000000092" APPLIER_X="actor:00000000-0000-4000-8000-000000000093" APPLIER_Y="actor:00000000-0000-4000-8000-000000000094" +POSITION_X_VERSION="00000000-0000-7000-8000-000000000095" +POSITION_Y_VERSION="00000000-0000-7000-8000-000000000096" REVIEW_DIGEST_X="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" REVIEW_DIGEST_Y="dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" @@ -34,6 +36,12 @@ INSERT INTO position_record ( ) VALUES ('${TENANT_ID}', '${POSITION_X}', '${ORG_ID}', '${JOB_ID}'), ('${TENANT_ID}', '${POSITION_Y}', '${ORG_ID}', '${JOB_ID}'); +INSERT INTO position_record_version ( + tenant_record_id, position_record_version_id, position_record_id, + position_status_code, effective_from +) VALUES + ('${TENANT_ID}', '${POSITION_X_VERSION}', '${POSITION_X}', 'active', DATE '2026-01-01'), + ('${TENANT_ID}', '${POSITION_Y_VERSION}', '${POSITION_Y}', 'active', DATE '2026-01-01'); INSERT INTO position_reporting_relationship_record ( tenant_record_id, position_reporting_relationship_record_id, subordinate_position_record_id, relationship_type_code From 2af25408d95ac9bb362f318d218af507c0861f8b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 22:07:57 -0700 Subject: [PATCH 27/32] test(position-reporting): reject mismatched audit reason evidence --- ...position_reporting_persistence_postgres.sh | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/test_position_reporting_persistence_postgres.sh b/tests/test_position_reporting_persistence_postgres.sh index 3b1d7a055..95159cc79 100644 --- a/tests/test_position_reporting_persistence_postgres.sh +++ b/tests/test_position_reporting_persistence_postgres.sh @@ -31,6 +31,8 @@ REVERSE_RELATIONSHIP_ID="00000000-0000-7000-8000-000000000043" SELF_RELATIONSHIP_ID="00000000-0000-7000-8000-000000000044" AUDIT_ID="00000000-0000-4000-8000-000000000051" OUTBOX_ID="00000000-0000-4000-8000-000000000052" +WRONG_REASON_AUDIT_ID="00000000-0000-4000-8000-000000000053" +WRONG_REASON_OUTBOX_ID="00000000-0000-4000-8000-000000000054" REVIEWER="actor:00000000-0000-4000-8000-000000000061" APPLIED_BY="actor:00000000-0000-4000-8000-000000000062" REVIEW_DIGEST="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" @@ -194,6 +196,49 @@ expect_failure \ TIMESTAMPTZ '2026-08-24 01:55:00+00', DATE '2026-08-24', '${AUDIT_ID}' );" +wrong_reason_event="$(python3 - < Date: Sun, 23 Aug 2026 22:09:47 -0700 Subject: [PATCH 28/32] fix(position-reporting): bind governed audit reason --- .../migrations/0020_position_reporting_relationship.sql | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/database/migrations/0020_position_reporting_relationship.sql b/database/migrations/0020_position_reporting_relationship.sql index 2dbc7ac80..d3fd62140 100644 --- a/database/migrations/0020_position_reporting_relationship.sql +++ b/database/migrations/0020_position_reporting_relationship.sql @@ -315,6 +315,8 @@ BEGIN IF audit_event ->> 'orgmetrapurpose' IS DISTINCT FROM 'position_reporting_change_apply' + OR audit_event ->> 'orgmetrareason' + IS DISTINCT FROM 'approved_reporting_line_change' OR audit_event ->> 'orgmetraactor' IS DISTINCT FROM NEW.applied_by_actor_reference OR audit_event ->> 'orgmetraevidence' @@ -337,7 +339,7 @@ END; $$; COMMENT ON FUNCTION enforce_position_reporting_scope() IS - 'Before persistence, serializes one tenant reporting graph, resolves the same-tenant relationship anchor, requires staffable subordinate/manager PositionVersion coverage for the full effective interval, rejects self-reporting and cycles over overlapping effective time, and requires immutable audit/outbox application evidence that binds the reviewed evidence digest, applying actor, exact application event digest, subject, result, and chronology.'; + 'Before persistence, serializes one tenant reporting graph, resolves the same-tenant relationship anchor, requires staffable subordinate/manager PositionVersion coverage for the full effective interval, rejects self-reporting and cycles over overlapping effective time, and requires immutable audit/outbox application evidence that binds the governed reason, reviewed evidence digest, applying actor, exact application event digest, subject, result, and chronology.'; CREATE TRIGGER position_reporting_version_scope_guard BEFORE INSERT ON position_reporting_relationship_version @@ -421,4 +423,4 @@ COMMENT ON TABLE position_reporting_relationship_record IS 'Durable tenant-scoped Position-to-Position solid-line relationship anchor. The subordinate seat and relationship type are stable anchor identity; Person and Assignment are intentionally absent.'; COMMENT ON TABLE position_reporting_relationship_version IS - 'Authoritative bitemporal manager-Position versions applied only after separate human review and immutable audit/outbox evidence. Both Position endpoints must remain staffable for the full effective interval at the recorded-time coordinate. The application audit event must bind the review digest and its exact envelope digest. The relation stores no worker PII, compensation, ratings, or employment-decision output.'; + 'Authoritative bitemporal manager-Position versions applied only after separate human review and immutable audit/outbox evidence. Both Position endpoints must remain staffable for the full effective interval at the recorded-time coordinate. The application audit event must bind the governed reason, review digest, and its exact envelope digest. The relation stores no worker PII, compensation, ratings, or employment-decision output.'; From 8ede14cc7430d83955c38c867d1521989a557ea7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 22:10:06 -0700 Subject: [PATCH 29/32] docs(position-reporting): trace governed audit reason binding --- docs/adr/0106-position-reporting-persistence.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/adr/0106-position-reporting-persistence.md b/docs/adr/0106-position-reporting-persistence.md index 41ba068ce..53e5dbc9d 100644 --- a/docs/adr/0106-position-reporting-persistence.md +++ b/docs/adr/0106-position-reporting-persistence.md @@ -20,7 +20,7 @@ Orgmetra persists a solid-line reporting relationship as a normalized anchor/ver New recorded intervals use PostgreSQL `transaction_timestamp()` and begin open. History cannot be rewritten or deleted; the only row update is closing one open recorded interval at the transaction timestamp. `TRUNCATE` is rejected separately. Both relations use `ENABLE ROW LEVEL SECURITY` plus `FORCE ROW LEVEL SECURITY` and tenant policies based on `current_tenant_record_id()`. -A version insert fails closed unless it has an open same-tenant anchor, a different reviewer and applying actor, immutable same-tenant audit/outbox evidence for `position_reporting_change_apply`, no self-reporting edge, and no management cycle over an overlapping effective period. The application audit event must carry the exact reviewed-evidence digest in `orgmetraevidence`; the version's application-evidence digest must equal the persisted audit envelope digest. This prevents a syntactically valid reviewer/digest pair from being recorded without immutable evidence binding. Composite foreign keys bind both subordinate and manager Position IDs to the same tenant. +A version insert fails closed unless it has an open same-tenant anchor, a different reviewer and applying actor, immutable same-tenant audit/outbox evidence for purpose `position_reporting_change_apply` with governed reason `approved_reporting_line_change`, no self-reporting edge, and no management cycle over an overlapping effective period. The application audit event must carry the exact reviewed-evidence digest in `orgmetraevidence`; the version's application-evidence digest must equal the persisted audit envelope digest. This prevents a syntactically valid reviewer/digest pair—or a semantically unrelated audit reason—from being recorded as evidence for the reporting-line application. Composite foreign keys bind both subordinate and manager Position IDs to the same tenant. The database also enforces the staffable endpoint contract already defined by parent PR #94. At the proposed relationship version's system-recorded coordinate, both the subordinate and manager Position anchors must be visible and the union of same-tenant `active`/`open` `position_record_version` effective ranges must cover the relationship's entire effective interval. PostgreSQL `range_agg(daterange(...))` produces a normalized date multirange, and multirange containment proves full coverage without enumerating days or incorrectly rejecting legitimate contiguous active/open Position versions. A stable Position record without staffable bitemporal PositionVersion coverage is therefore insufficient persistence evidence. @@ -32,7 +32,7 @@ The anchor/version split keeps stable relationship identity separate from changi The staffable coverage check deliberately validates the full reporting interval rather than only its start date. Otherwise a relationship could be stored as authoritative while one endpoint has no active/open PositionVersion for later days that the relationship itself claims to cover. The database check therefore matches the descriptive snapshot semantics instead of relying on downstream readers to discover and reject internally inconsistent persisted truth. -The audit binding deliberately uses the already immutable `audit_event_record` envelope as the application evidence instead of trusting an unrelated caller-provided digest. The event identifies the applying actor, tenant, relationship subject, purpose, result, time, and reviewed-evidence digest; storing its exact envelope digest makes the relationship version cryptographically correlate to that immutable application fact. +The audit binding deliberately uses the already immutable `audit_event_record` envelope as the application evidence instead of trusting an unrelated caller-provided digest. The event identifies the applying actor, tenant, relationship subject, governed purpose and reason, result, time, and reviewed-evidence digest; storing its exact envelope digest makes the relationship version cryptographically correlate to that immutable application fact. The tenant advisory lock is intentionally narrow: it protects only graph mutation validation and is transaction-scoped. It does not replace transaction boundaries, RLS, authorization, or history guards. Its purpose is to make cycle validation defensible under concurrent writes rather than merely correct in single-session tests. @@ -46,6 +46,6 @@ Canonical `docs/DATA_MODEL.md` / `docs/ERD.md` are intentionally not edited in t ## Consequences -The database can preserve audited supervisory hierarchy truth independently of current worker occupancy. Reads can combine persisted relationships with PR #94's staffable-Position snapshot semantics, and persistence now rejects an edge whose PositionVersion evidence cannot support every day that the edge claims to cover. The stricter model rejects ambiguous duplicate anchors, self-reporting, single-session and concurrent cycles, caller-backdated system time, mutation of history, tenant-crossing references, non-staffable endpoint coverage, and unbound review/application evidence rather than silently repairing them. +The database can preserve audited supervisory hierarchy truth independently of current worker occupancy. Reads can combine persisted relationships with PR #94's staffable-Position snapshot semantics, and persistence now rejects an edge whose PositionVersion evidence cannot support every day that the edge claims to cover. The stricter model rejects ambiguous duplicate anchors, self-reporting, single-session and concurrent cycles, caller-backdated system time, mutation of history, tenant-crossing references, non-staffable endpoint coverage, and audit evidence whose governed reason does not match the reviewed reporting-line application rather than silently repairing them. This ADR does not claim certification, branch-protection enforcement, release readiness, or authorization to make employment decisions. From c35ad114edbce7a4ebafcea793748493f1346351 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 22:10:21 -0700 Subject: [PATCH 30/32] docs(position-reporting): map wrong-reason regression --- docs/traceability/position-reporting-persistence.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/traceability/position-reporting-persistence.md b/docs/traceability/position-reporting-persistence.md index f8305eac5..8bb0d98db 100644 --- a/docs/traceability/position-reporting-persistence.md +++ b/docs/traceability/position-reporting-persistence.md @@ -22,7 +22,7 @@ | Concurrent opposite graph mutations cannot both commit | transaction-scoped tenant advisory lock is acquired before the VOLATILE trigger's graph queries | `tests/test_position_reporting_concurrency_postgres.sh` holds X→Y open while Y→X races; exactly one edge may commit | | Human review is distinct from applying actor | exact actor-format checks plus `reviewer_actor_reference <> applied_by_actor_reference` | database constraints and valid separated actors fixture | | Reviewed evidence is immutable application evidence, not an unattested column | application audit `orgmetraevidence` must equal `review_evidence_digest_sha256`; stored application digest must equal `audit_event_record.event_envelope_digest` | `tests/test_position_reporting_review_binding_postgres.sh` | -| Applied truth requires immutable audit/outbox evidence | scope guard verifies purpose, applying actor, reviewed-evidence digest, exact audit-envelope digest, subject, result code, review/application chronology, and integration-hub outbox | valid `record_audit_outbox_event(...)` fixture; mismatched evidence fails closed | +| Applied truth requires immutable audit/outbox evidence with the governed reason | scope guard verifies purpose `position_reporting_change_apply`, reason `approved_reporting_line_change`, applying actor, reviewed-evidence digest, exact audit-envelope digest, subject, result code, review/application chronology, and integration-hub outbox | valid `record_audit_outbox_event(...)` fixture plus wrong-reason regression in `tests/test_position_reporting_persistence_postgres.sh`; mismatched evidence fails closed | | Historical truth cannot be rewritten/deleted | `protect_position_reporting_history()` | manager rewrite regression | | Table-wide destruction cannot bypass row guards | explicit BEFORE TRUNCATE guards and revoked PUBLIC TRUNCATE | TRUNCATE regression | | Tenant isolation is enforced for ordinary app/read roles | `ENABLE` + `FORCE ROW LEVEL SECURITY`; tenant policy on both relations | `NOSUPERUSER NOBYPASSRLS` reader sees alpha=1, beta=0, missing-context=0 | From f8b72eccb0e165db5bf40b825b6cf22fd906e9bf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 21:58:08 +0900 Subject: [PATCH 31/32] fix(position-reporting): harden persistence boundaries --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + .../0020_position_reporting_relationship.sql | 20 +++++++++++ docs/DATA_MODEL.md | 4 +++ docs/ERD.md | 6 ++++ docs/TRACEABILITY.md | 1 + .../0106-position-reporting-persistence.md | 5 +-- docs/adr/README.md | 1 + manifest.json | 2 +- scripts/foundation-contract-core.mjs | 9 ++++- tests/foundation-contract.test.mjs | 8 +++++ ...position_reporting_persistence_postgres.sh | 33 +++++++++++++++++++ tests/validate_repository.py | 4 +++ 13 files changed, 91 insertions(+), 5 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 1bdc13b5a..c29d494e7 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -75,7 +75,7 @@ The initial deployment may share one physical PostgreSQL cluster. Logical isolat | Service identifier | Owned schema and representative tables | Database role | |---|---|---| | `people_core` | `people_core`: person, name, employment, assignment, compensation, and candidate-worker linkage records | `people_core_role` | -| `organization_core` | `organization_core`: organization units and position records | `organization_core_role` | +| `organization_core` | `organization_core`: organization units, position records, and Position reporting relations | `organization_core_role` | | `job_architecture` | `job_architecture`: job profiles, publication evidence, and persisted `job_analysis_snapshot` / task / KSAO rows | `job_architecture_role` | | `talent_acquisition` | `talent_acquisition`: candidate profiles, selection decisions, and decision evidence | `talent_acquisition_role` | | `performance_management` | `performance_management`: criterion blueprints and observations | `performance_management_role` | diff --git a/CHANGELOG.md b/CHANGELOG.md index 99f4752d7..329d07335 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to Orgmetra will be documented in this file. - Accepted ADRs 0001–0003 now include buyer-facing Context, Decision, and Consequences grounded in verified ISO 30400:2022, ISO 30414:2025, Uniform Guidelines (29 C.F.R. Part 1607), SIOP (2018), OpenAPI Specification v3.2.0, OpenID Connect Core 1.0 errata set 2, CloudEvents v1.0.2, Jensen and Snodgrass (1999), Snodgrass (1999), and Allen (1983) records already listed in `docs/doctoring/REFERENCES.md`. ADRs 0004 and 0005 gained APA 7th References pointers to that same bibliography without changing their Decision bodies. - Active-PR governed Job Analysis persistence/API on the canonical `JobAnalysisSnapshot` model: migration `0013_job_analysis_snapshot.sql` stores immutable tenant-scoped snapshot, Task, KSAO, Task–KSAO, FJA and write-command evidence; `POST /v1/tenants/{tenant_record_id}/job-analysis-snapshots` and matching GET enforce purpose-bound Keyverse scope, authenticated-principal actor authority, bounded/strict JSON handling, transactional Idempotency-Key serialization, parent-scope fail-closed integrity, forced RLS, and atomic audit/outbox evidence. ADR 0014 records the persistence decision while ADR 0007 remains the domain/evidence authority; validated evidence still requires accountable human review and non-LLM provenance, and the service does not make a high-impact employment decision. +- Active-PR #106 Position-to-Position reporting persistence: normalized bitemporal solid-line relationship anchor/version tables with staffable endpoint coverage, tenant isolation, cycle prevention under concurrent writes, immutable human-review/application audit binding, history/TRUNCATE guards, trusted trigger search paths, and reserved UUID sentinel checks. It remains Draft stacked on PR #94 and is not protected-branch truth. - Active-PR `orgmetra_selection_review` packet for PII-minimized, evidence-bound human selection review: canonical operational tenant identity, UUID-backed opaque candidate/Job/sealed-evidence/reviewer references, explicit purpose/reason/evidence version, deterministic canonical JSON and SHA-256 correlation, mandatory human decision state, redacted packet repr, and provenance-paired model evidence that remains `untrusted_draft`, with exact 100% owned statement and branch coverage required by its quality gate. - Active performance-criterion scope hardening: `criterion_observation_scope_guard` rejects criterion outcomes for a Job the worker did not effectively hold at the observation date, observations before the relevant assignment, and observations outside the referenced performance cycle while preserving valid multiple-assignment cases and existing bitemporal correction semantics. The guard evaluates current-recorded facts, derives the date coordinate from `observed_at` in UTC so session `TimeZone` cannot alter the result, uses a trusted function search path, and adds no PII or automated employment decision authority. The Foundation PostgreSQL contract also rejects a closed `recorded_to` on each time-coordinate lookup and proves UTC midnight plus non-UTC session `TimeZone` boundaries. - Bitemporal tenant-scoped organization hierarchy validation that rejects visible indirect parent cycles and reuses single-valued recorded-time reconstruction before graph traversal. diff --git a/database/migrations/0020_position_reporting_relationship.sql b/database/migrations/0020_position_reporting_relationship.sql index d3fd62140..dffe80da1 100644 --- a/database/migrations/0020_position_reporting_relationship.sql +++ b/database/migrations/0020_position_reporting_relationship.sql @@ -2,6 +2,8 @@ -- independent human review and authoritative application. Person, Assignment, -- compensation, assessment, and free-form HR values remain outside this model. +SET search_path = public, pg_catalog; + CREATE TABLE position_reporting_relationship_record ( tenant_record_id uuid NOT NULL REFERENCES tenant_record(tenant_record_id), position_reporting_relationship_record_id uuid PRIMARY KEY, @@ -11,6 +13,10 @@ CREATE TABLE position_reporting_relationship_record ( recorded_to timestamptz, CONSTRAINT position_reporting_relationship_record_id_operational_check CHECK (public.is_operational_uuid(position_reporting_relationship_record_id)), + CONSTRAINT position_reporting_relationship_tenant_operational_check + CHECK (public.is_operational_uuid(tenant_record_id)), + CONSTRAINT position_reporting_subordinate_operational_check + CHECK (public.is_operational_uuid(subordinate_position_record_id)), CONSTRAINT position_reporting_subordinate_tenant_fk FOREIGN KEY (tenant_record_id, subordinate_position_record_id) REFERENCES position_record(tenant_record_id, position_record_id), @@ -42,6 +48,14 @@ CREATE TABLE position_reporting_relationship_version ( application_state text NOT NULL DEFAULT 'applied_after_human_review', CONSTRAINT position_reporting_version_id_operational_check CHECK (public.is_operational_uuid(position_reporting_relationship_version_id)), + CONSTRAINT position_reporting_version_tenant_operational_check + CHECK (public.is_operational_uuid(tenant_record_id)), + CONSTRAINT position_reporting_version_record_operational_check + CHECK (public.is_operational_uuid(position_reporting_relationship_record_id)), + CONSTRAINT position_reporting_manager_operational_check + CHECK (public.is_operational_uuid(manager_position_record_id)), + CONSTRAINT position_reporting_audit_event_operational_check + CHECK (public.is_operational_uuid(audit_event_record_id)), CONSTRAINT position_reporting_version_record_tenant_fk FOREIGN KEY (tenant_record_id, position_reporting_relationship_record_id) REFERENCES position_reporting_relationship_record( @@ -94,6 +108,7 @@ CREATE TABLE position_reporting_relationship_version ( CREATE FUNCTION enforce_position_reporting_system_time() RETURNS trigger LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp AS $$ BEGIN IF NEW.recorded_to IS NOT NULL THEN @@ -124,6 +139,7 @@ EXECUTE FUNCTION enforce_position_reporting_system_time(); CREATE FUNCTION protect_position_reporting_history() RETURNS trigger LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp AS $$ BEGIN IF TG_OP = 'DELETE' THEN @@ -166,6 +182,7 @@ CREATE FUNCTION position_reporting_has_staffable_coverage( RETURNS boolean LANGUAGE sql STABLE +SET search_path = pg_catalog, public, pg_temp AS $$ SELECT COALESCE( pg_catalog.range_agg( @@ -194,6 +211,7 @@ COMMENT ON FUNCTION position_reporting_has_staffable_coverage(uuid, uuid, date, CREATE FUNCTION enforce_position_reporting_scope() RETURNS trigger LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp AS $$ DECLARE subordinate_position_id uuid; @@ -349,6 +367,7 @@ EXECUTE FUNCTION enforce_position_reporting_scope(); CREATE FUNCTION enforce_position_reporting_anchor_alignment() RETURNS trigger LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp AS $$ BEGIN IF NEW.recorded_to IS NULL OR NEW.recorded_to IS NOT DISTINCT FROM OLD.recorded_to THEN @@ -382,6 +401,7 @@ EXECUTE FUNCTION enforce_position_reporting_anchor_alignment(); CREATE FUNCTION reject_position_reporting_truncate() RETURNS trigger LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp AS $$ BEGIN RAISE EXCEPTION 'position-reporting history cannot be truncated' diff --git a/docs/DATA_MODEL.md b/docs/DATA_MODEL.md index 7d4afd563..4ad8572fa 100644 --- a/docs/DATA_MODEL.md +++ b/docs/DATA_MODEL.md @@ -14,6 +14,8 @@ | `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_reporting_relationship_record` | Tenant-scoped durable Position-to-Position solid-line relationship anchor. | +| `position_reporting_relationship_version` | Reviewed bitemporal manager-Position application bound to immutable 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`. | @@ -52,6 +54,8 @@ Intervals are half-open and non-empty: an end value, when present, must be stric Durable anchors such as `organization_unit`, `job_profile`, `employment_record`, and `position_record` do not repeat mutable descriptive attributes. Their descriptive versions live in `organization_unit_version`, `job_profile_version`, `employment_record_version`, and `position_record_version`. Single-valued bitemporal version families reject overlapping effective/system intervals, so one `effective_from`/`effective_to` interval combined with one `recorded_from`/`recorded_to` interval cannot yield contradictory current descriptions. Corrections close the previous recorded interval and insert a replacement; in-place business mutation is rejected. +Active PR #106 adds `position_reporting_relationship_record` as the stable subordinate-Position/relationship-type anchor and `position_reporting_relationship_version` as the changing manager-Position fact. Both are tenant-qualified and bitemporal; the version requires staffable PositionVersion coverage across its full effective interval plus separate human-review and application evidence. The relation stores no Person, Assignment, compensation, rating, or employment-decision output. + 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. ## High-impact decision evidence diff --git a/docs/ERD.md b/docs/ERD.md index a547cc3a0..54d1e7543 100644 --- a/docs/ERD.md +++ b/docs/ERD.md @@ -18,6 +18,10 @@ 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_reporting_relationship_record : subordinate + position_reporting_relationship_record ||--o{ position_reporting_relationship_version : has_versions + position_record ||--o{ position_reporting_relationship_version : manager + audit_event_record ||--o| position_reporting_relationship_version : proves_application employment_record ||--o{ assignment_record : covers person_record ||--o{ assignment_record : receives position_record ||--o{ assignment_record : assigned_through @@ -49,6 +53,8 @@ erDiagram `organization_unit`, `job_profile`, `employment_record`, and `position_record` are durable anchors. Mutable names, classifications, parent relationships, titles, families, version codes, and employment or position status live in bitemporal version rows. Positions retain stable organization/job references while retroactive corrections append or supersede version facts rather than rewriting identity. An organization version may reference another durable organization as its parent; self-parenting is rejected at the database boundary. An assignment names the employment that covers it, so a person cannot be assigned through another worker's employment. Exclusive employment versions for one person cannot overlap. An assignment day must land on an `active` or `open` position version, and visible allocations for one seat cannot exceed 1.0000. +Active PR #106 adds a Position-to-Position reporting anchor and reviewed manager-Position version. It is independent of worker occupancy and Assignment state; its effective/system intervals, staffable endpoint coverage, audit binding, cycle guard, and tenant isolation are enforced by the proposed migration until the PR is integrated and retargeted. + Every owned HRIS fact carries `tenant_record_id`. Relationships that cross table boundaries use tenant-qualified foreign keys, and row-level security independently filters every tenant-scoped relation. The tenant column is therefore both a referential-integrity boundary and a runtime isolation boundary, not a caller-supplied business attribute. A candidate profile can be linked to at most one worker identity within its tenant. A person identity can have multiple candidate-worker links across reapplications or historical candidate profiles, so the person-side cardinality is one-to-many. diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 22a4178fe..76699e0c6 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -10,6 +10,7 @@ | 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 | | 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 | +| Reviewed Position-to-Position reporting truth | Organization core | `position_reporting_relationship_record`, `position_reporting_relationship_version` | PostgreSQL staffable-coverage, bitemporal, audit/outbox binding, RLS, history, single-session cycle, and concurrent-cycle contracts in active PR #106 | ADR-0106 | implemented_on_active_pr | | 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 | | Governed candidate-to-worker conversion | Talent Acquisition / People core | `candidate_worker_conversion_record` with candidate, person, employment, selection decision, audit event and outbox evidence | PostgreSQL exact hire/evidence/audit-envelope binding, correction provenance, tenant RLS, legacy-write rejection and bitemporal history contract | ADR-0001, ADR-0003, ADR-0006 | implemented_on_protected_main | diff --git a/docs/adr/0106-position-reporting-persistence.md b/docs/adr/0106-position-reporting-persistence.md index 53e5dbc9d..acb82a552 100644 --- a/docs/adr/0106-position-reporting-persistence.md +++ b/docs/adr/0106-position-reporting-persistence.md @@ -1,6 +1,7 @@ # ADR 0106 — Bitemporal Position reporting persistence -- **Status:** Proposed in active PR #106; not protected-main truth +- Status: Proposed +- Active PR: #106; not protected-main truth - **Decision date:** 2026-08-24 - **Parent contract:** PR #94, `feat/position-reporting-hierarchy@3f67182bb3065f2fc8fd974bfdd75a390d8a8fdc` @@ -42,7 +43,7 @@ This PR is a Draft descendant of #94 and cannot inherit #94 checks or reviews. I PR #95 owns the in-memory pre-mutation review packet. This persistence slice does not copy or modify that branch; it accepts the review digest plus immutable application audit evidence as the handoff boundary. A later authorized host adapter may translate a verified review packet into the database command, but direct cross-service SQL is out of scope. -Canonical `docs/DATA_MODEL.md` / `docs/ERD.md` are intentionally not edited in this stacked slice while independent active database PRs also own those high-conflict documents. Integration must reconcile the accepted relationship tables into canonical data-model documentation after dependency ordering is resolved. +Canonical `docs/DATA_MODEL.md` / `docs/ERD.md` now record the proposed active-PR relationship tables and explicitly distinguish them from protected-main truth. Integration must reconcile the accepted relationship tables and remove the active-PR qualification after dependency ordering is resolved. ## Consequences diff --git a/docs/adr/README.md b/docs/adr/README.md index 099a21139..64da6f68e 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 | +| [0106](0106-position-reporting-persistence.md) | Bitemporal Position reporting persistence | Proposed | diff --git a/manifest.json b/manifest.json index 97f2bab14..694d46d28 100644 --- a/manifest.json +++ b/manifest.json @@ -1 +1 @@ -{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"32cc4ef78d1eca557fa01731026840be01211a043eb0ada552e4e6cb9eace353","bytes":17295,"lines":76},{"path":"CLAUDE.md","sha256":"add33884f466d324e20875388d103de41c6e062938a6e98727dc83a87ffe976f","bytes":1229,"lines":20},{"path":"LICENSE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30","bytes":11358,"lines":202},{"path":"NOTICE","sha256":"34b4618e946bdd8d33407d6ac5279f0a0388f5e7c8f79d2e7d8c3c47d0266042","bytes":305,"lines":4},{"path":"README.md","sha256":"1a9fc400d26d8137ae5911488794a6d3fa915957c95f27b36a48cef0fdf823c6","bytes":3785,"lines":81},{"path":"database/migrations/0001_foundation_schema.sql","sha256":"ce2ae52fc66b2f99597ea5285df82c66f90caa46174fef4930d68a8b6177d0dd","bytes":38747,"lines":916},{"path":"database/migrations/0002_sealed_evidence_digest.sql","sha256":"93d659ca8e0e9293a83d5422d043be7b1022c5470a5b22670aa3416fa334a04c","bytes":6649,"lines":202},{"path":"database/migrations/0003_audit_outbox_persistence.sql","sha256":"2aa7bbb8220923ec584537c0cd46f0cba2b692d69d431f097b7df6db75235bfc","bytes":15417,"lines":423},{"path":"database/migrations/0004_outbox_delivery_claim.sql","sha256":"d4504acf7d58528a2a8f4f03d1584b868c8d3ba9046a007b9c2e7cfef993b2ef","bytes":9451,"lines":234},{"path":"database/migrations/0005_outbox_delivery_finalization.sql","sha256":"b7e8790595b288f752d6ef5cc6cbfe4e1b6712248f5b7a3a25fa60016b6a4961","bytes":6125,"lines":170},{"path":"database/migrations/0006_outbox_delivery_dead_letter.sql","sha256":"c1fb91cdf98169fd6684984e86cb0a14fa19c8f1226028d2346a2a069df2b3c7","bytes":24919,"lines":628},{"path":"database/migrations/0007_outbox_retry_exhaustion.sql","sha256":"812f50d70ca5929c7eba964d34a208aedee660d11cc7ffc09d67688c4737e0d5","bytes":19081,"lines":476},{"path":"database/migrations/0008_audit_outbox_review_hardening.sql","sha256":"c3713a12db9d00fdc10005df1f86c07965e9555eefad78ca67e994537a739d9b","bytes":17562,"lines":448},{"path":"database/migrations/0009_candidate_worker_conversion_governance.sql","sha256":"4030666629a6b8deb383b8337ead4f09d6a945969313def2577a38f31f06cda9","bytes":11537,"lines":281},{"path":"database/migrations/0010_validity_study_case_integrity.sql","sha256":"3f594810ac9e1a6747a2bb4838e5ce65b921cb6e3d36fcdc3ff08b4a7579ebd1","bytes":11979,"lines":313},{"path":"database/migrations/0011_criterion_observation_scope.sql","sha256":"f9fe7c35f1ee7b167e1c2ba75a50a84febda9a6ccf8123b4f5726f51968694f9","bytes":7444,"lines":165},{"path":"database/migrations/0012_people_mutation_idempotency.sql","sha256":"52dbbb9ec7f9be5291593ba88f228d7fffd736dcb99547a08c1d6cad076afb69","bytes":3162,"lines":76},{"path":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"path":"docs/API_CONTRACT.md","sha256":"63533dff785da62b89e585d742a158e2aeb05913644f2bf9fb6486f281c2e589","bytes":4555,"lines":76},{"path":"docs/DATA_MODEL.md","sha256":"6ad29731ae7ee7aa5bf3a2d0bfef88894a35a2550edb2be3244d6f143d76444a","bytes":13366,"lines":85},{"path":"docs/ERD.md","sha256":"546001aa85c4fe020e0c39d881dc860daf7f69090596666fdf9092487b0725fe","bytes":6964,"lines":70},{"path":"docs/OPERABILITY.md","sha256":"82b2d3e70cec371ef35e9e0f982ac40fef84351976bc04b863b81d27023d5a62","bytes":11189,"lines":71},{"path":"docs/PRD.md","sha256":"3ad85ae633cce0fc7a93af39b21d7a7c70bb2efa786da6b12f3c5327906e34f1","bytes":5490,"lines":111},{"path":"docs/SECURITY.md","sha256":"01918512d8882060e9cff0c4aa8206e0eccbdfb61cfd7f829331123c7a9fe6ac","bytes":11185,"lines":64},{"path":"docs/STORYBOARD.md","sha256":"6e4ffb0eb03a80343f50d363ffc43b34da9348a44232dd947a9ff416ea92a3d2","bytes":1342,"lines":28},{"path":"docs/STORYBOOK.md","sha256":"82f79029b3c2b7a45393bad5ba8fabe61014d4b6149c7d4e73f70ba447f885e9","bytes":1389,"lines":50},{"path":"docs/TEST_STRATEGY.md","sha256":"d0a0bc3b54ed0fc7973747987f1afb117d6144c390b51ed9370eb571972a33f8","bytes":16534,"lines":135},{"path":"docs/THREAT_MODEL.md","sha256":"f314f375c2e41252536de224c7bc7e4a10ab8f340cb86642724e7399e32f4252","bytes":6736,"lines":23},{"path":"docs/TRACEABILITY.md","sha256":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"lines":40},{"path":"docs/TRD.md","sha256":"23697d88a4882698e1a2782b7da3f2ccd0d3cd2d6d1bffe89b6597dc16851077","bytes":9064,"lines":101},{"path":"docs/UML.md","sha256":"fe67c37aa88e5814ceb2db7e8f7d8d85ca27a994802efbb7c75164b387adf0a9","bytes":5528,"lines":122},{"path":"docs/USER_STORIES.md","sha256":"5535b39d8c71a36c81f78e2d6dbd90a2d32e6541790f0d28f6dd4baf3ea7b45f","bytes":2670,"lines":37},{"path":"docs/WIREFRAMES.md","sha256":"b03aa6419aeaf5d42a5698c4d43a434c1633b7ac6fd0b0bd0cda979077adc56e","bytes":2005,"lines":77},{"path":"docs/adr/0001-orgmetra-authoritative-hris-record.md","sha256":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"path":"docs/adr/0008-purpose-bound-pii-authorization.md","sha256":"c5157d3bc58f3d8d29e03104dd15eb2911cc1bb66e2c92a935b26d7164648dc7","bytes":5988,"lines":55},{"path":"docs/adr/0009-performance-criterion-observation-scope.md","sha256":"1ac10bb2747b0a5b4d62f627825cfd7f978f3fa88d7575bffc23d56371240a64","bytes":7057,"lines":57},{"path":"docs/adr/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"1656ef8b57c836ef7936a8e9cb6a824681eb7563157a1ab0a29deb25849a457b","bytes":5568,"lines":53},{"path":"docs/adr/0012-governed-migration-handoff.md","sha256":"713855d670001d3964ecb36cc653830502fb1d82a58b9e39f564b6992dd2bd80","bytes":5965,"lines":59},{"path":"docs/adr/0013-governed-requisition-review-packet.md","sha256":"70bf2cbdf903a8793d6d8bc116a08331931090118341f42010236e09c6cc1802","bytes":4693,"lines":46},{"path":"docs/adr/0014-job-analysis-snapshot-persistence.md","sha256":"a7ab6fee50aaa63f7f407516a4cb39885faeb0fc6e5035ee8fc352ed73430105","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"lines":69},{"path":"docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md","sha256":"b64f21abb19373e780db8b9e64deb8ba9a6219ccf9625a651f25407b8691fcbd","bytes":8227,"lines":226},{"path":"docs/superpowers/specs/2026-08-15-orgmetra-foundation-design.md","sha256":"4a0e1a7943e40d12bd3082db3757045b4085e5a089fea7bc0d8a1565ffcbcf1d","bytes":6237,"lines":187},{"path":"package.json","sha256":"59ae9e3e67c3fba9320cb18439692395cdfd16ae5c24e3c4cf30d77d63ebabb5","bytes":388,"lines":9},{"path":"packages/hris-kernel/src/orgmetra_hris_kernel/audit.py","sha256":"3e5b7190cf857dc8c1fc7e898cef303060f34aabee6c27a9034d4d9650e33190","bytes":7707,"lines":160},{"path":"packages/hris-kernel/tests/test_audit_outbox.py","sha256":"5928dd7b97fe38d6b7472ce62966437e339058a59c3b301a93a7b5c05432b40c","bytes":7556,"lines":200},{"path":"schemas/openapi.yaml","sha256":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"path":"scripts/foundation-contract.mjs","sha256":"5242dcdbe0935775edf074462c82600e9bc4927d9fdc50c47727af915fd4b23a","bytes":218,"lines":6},{"path":"tests/dispatcher-inventory.test.mjs","sha256":"09f5e64410e6b7a26bf8d6ce61c50b737da2ea85d955f91eba63aa21f1537261","bytes":1597,"lines":34},{"path":"tests/foundation-contract.test.mjs","sha256":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"path":"tests/openapi-contract.test.mjs","sha256":"80c1610ef1c189fa325e55389501e0e51531ddf61ee335bb94d9cb3aa55a9fdc","bytes":6438,"lines":195},{"path":"tests/test_audit_outbox_hardening_postgres.sh","sha256":"518ba2f37ba6292943e5abe22c2599452b2f031a42e453b2493aedf8714421a0","bytes":13396,"lines":333},{"path":"tests/test_audit_outbox_postgres.sh","sha256":"e57a04920a0ba97fa6a06752d15ea150016ab8d44099e998c5c4f4067592b4d2","bytes":13443,"lines":357},{"path":"tests/test_bitemporal_postgres.sh","sha256":"7684b8c2ff52c044c081135515bd5aabbfd00e2daad0d471b0868701af2df6cc","bytes":8209,"lines":230},{"path":"tests/test_candidate_worker_conversion_postgres.sh","sha256":"681cb74d6cfa859ed92c6c2439881ea20c430ef8df94ec662e2807761a377f90","bytes":14673,"lines":344},{"path":"tests/test_criterion_observation_scope_postgres.sh","sha256":"0ee9539ee57f840c27d08009f7868cdc8662669df78a01dbc8be39216b8f1a3d","bytes":17811,"lines":469},{"path":"tests/test_evidence_sealing_postgres.sh","sha256":"57d16b632a0c60ffdcb4842ceb1cfe25d19c54cefeeefb622ff4fa6e83441ad7","bytes":11349,"lines":370},{"path":"tests/test_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"path":"tests/test_operational_uuid_postgres.sh","sha256":"7378f98f0d4b3000e8ea641d8701f1540dbad71410b3637d81d799969e0f6ff7","bytes":3346,"lines":101},{"path":"tests/test_outbox_claim_postgres.sh","sha256":"1027806d436ebfe34e108c25b6a4001f43b9550f1d70057c6c0d7974323b0c9b","bytes":14817,"lines":429},{"path":"tests/test_outbox_dead_letter_postgres.sh","sha256":"0d728d578e64252e6079f2d141ddaa7fa9cfbf9784e625832273596d69a6e13d","bytes":14008,"lines":377},{"path":"tests/test_people_mutation_idempotency_postgres.sh","sha256":"3f57e12f80bd1b034c9aac54b669d8530106e3e26b3795689671fb53807b3cd5","bytes":16191,"lines":381},{"path":"tests/test_tenant_isolation_postgres.sh","sha256":"dd649435ef8ab9e57f0609c101917e36656a6d40d63de9bcdbdac23d764f6c3a","bytes":15134,"lines":388},{"path":"tests/test_validity_study_case_postgres.sh","sha256":"0070ad58300323c7f9900c5645e0df3106b36ccd245ae686e982c2fd6fa4dc02","bytes":14708,"lines":301},{"path":"tests/validate_repository.py","sha256":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} +{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"7415e8a9cdc26ec3e0b083939f0f35c9573cb721fce7f3d1333dc85045d3c677","bytes":7895,"lines":107},{"path":"CHANGELOG.md","sha256":"3011688169a6305e372570e0c81c366cda06687fa975fd5b358485060855822d","bytes":17728,"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":"1a9fc400d26d8137ae5911488794a6d3fa915957c95f27b36a48cef0fdf823c6","bytes":3785,"lines":81},{"path":"database/migrations/0001_foundation_schema.sql","sha256":"ce2ae52fc66b2f99597ea5285df82c66f90caa46174fef4930d68a8b6177d0dd","bytes":38747,"lines":916},{"path":"database/migrations/0002_sealed_evidence_digest.sql","sha256":"93d659ca8e0e9293a83d5422d043be7b1022c5470a5b22670aa3416fa334a04c","bytes":6649,"lines":202},{"path":"database/migrations/0003_audit_outbox_persistence.sql","sha256":"2aa7bbb8220923ec584537c0cd46f0cba2b692d69d431f097b7df6db75235bfc","bytes":15417,"lines":423},{"path":"database/migrations/0004_outbox_delivery_claim.sql","sha256":"d4504acf7d58528a2a8f4f03d1584b868c8d3ba9046a007b9c2e7cfef993b2ef","bytes":9451,"lines":234},{"path":"database/migrations/0005_outbox_delivery_finalization.sql","sha256":"b7e8790595b288f752d6ef5cc6cbfe4e1b6712248f5b7a3a25fa60016b6a4961","bytes":6125,"lines":170},{"path":"database/migrations/0006_outbox_delivery_dead_letter.sql","sha256":"c1fb91cdf98169fd6684984e86cb0a14fa19c8f1226028d2346a2a069df2b3c7","bytes":24919,"lines":628},{"path":"database/migrations/0007_outbox_retry_exhaustion.sql","sha256":"812f50d70ca5929c7eba964d34a208aedee660d11cc7ffc09d67688c4737e0d5","bytes":19081,"lines":476},{"path":"database/migrations/0008_audit_outbox_review_hardening.sql","sha256":"c3713a12db9d00fdc10005df1f86c07965e9555eefad78ca67e994537a739d9b","bytes":17562,"lines":448},{"path":"database/migrations/0009_candidate_worker_conversion_governance.sql","sha256":"4030666629a6b8deb383b8337ead4f09d6a945969313def2577a38f31f06cda9","bytes":11537,"lines":281},{"path":"database/migrations/0010_validity_study_case_integrity.sql","sha256":"3f594810ac9e1a6747a2bb4838e5ce65b921cb6e3d36fcdc3ff08b4a7579ebd1","bytes":11979,"lines":313},{"path":"database/migrations/0011_criterion_observation_scope.sql","sha256":"f9fe7c35f1ee7b167e1c2ba75a50a84febda9a6ccf8123b4f5726f51968694f9","bytes":7444,"lines":165},{"path":"database/migrations/0012_people_mutation_idempotency.sql","sha256":"52dbbb9ec7f9be5291593ba88f228d7fffd736dcb99547a08c1d6cad076afb69","bytes":3162,"lines":76},{"path":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"path":"database/migrations/0020_position_reporting_relationship.sql","sha256":"233fedd8038154db0532478c81b28858c541077a6b91666f6ae21cf803935bb0","bytes":20613,"lines":446},{"path":"docs/API_CONTRACT.md","sha256":"63533dff785da62b89e585d742a158e2aeb05913644f2bf9fb6486f281c2e589","bytes":4555,"lines":76},{"path":"docs/DATA_MODEL.md","sha256":"b4c943857ef141ce9300987962469e0322188ac830ccc1132190ce377862abf3","bytes":14116,"lines":89},{"path":"docs/ERD.md","sha256":"2dfa3d57a28e141b4574b1afb3ba449516115ac1f3bcb2dd08f54d3d1c4e5381","bytes":7669,"lines":76},{"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":"0361cc07d38017e8e5979c4a877142de5e63700496d8528090e9a042ae2d6357","bytes":11808,"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/README.md","sha256":"23bf375a9fb8f768cf290281700e836275c2080be0e9b7c697c22187d8d4df9a","bytes":1944,"lines":19},{"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":"bd32ccc59edd82ee78aa48ad3cf4610a10ca9d327f0ab470de20eacc634528dc","bytes":28595,"lines":696},{"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":"6b7dd6d193bb5d5b773dbad2c8c07a4beaeca847c8bed67b8620171f524f53a7","bytes":15172,"lines":394},{"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_reporting_concurrency_postgres.sh","sha256":"350ed12672a678137861e347a233720a61c53ccee7d113f87562f31ea022b877","bytes":6780,"lines":169},{"path":"tests/test_position_reporting_persistence_postgres.sh","sha256":"148b270c9feb97bda2936b19436caa462f79b9e0597082ec03e274d792068120","bytes":13998,"lines":314},{"path":"tests/test_position_reporting_review_binding_postgres.sh","sha256":"f2a217d00de0e99f1d7f1f8c7b3423f182cc91311c90fc3053315d59e22aa211","bytes":1524,"lines":33},{"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":"351edc968270d7bca81059e0ef21d5216ca6ea8a405aa45eb6d08aa71b5fa422","bytes":27545,"lines":642}]} diff --git a/scripts/foundation-contract-core.mjs b/scripts/foundation-contract-core.mjs index 1e9fb267c..563ad9d7e 100644 --- a/scripts/foundation-contract-core.mjs +++ b/scripts/foundation-contract-core.mjs @@ -68,6 +68,7 @@ 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/0020_position_reporting_relationship.sql', 'packages/hris-kernel/src/orgmetra_hris_kernel/audit.py', 'packages/hris-kernel/tests/test_audit_outbox.py', 'schemas/openapi.yaml', @@ -89,6 +90,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_reporting_persistence_postgres.sh', + 'tests/test_position_reporting_review_binding_postgres.sh', + 'tests/test_position_reporting_concurrency_postgres.sh', 'tests/validate_repository.py' ]); @@ -112,6 +116,7 @@ export const DATABASE_OBJECT_NAMES = Object.freeze([ 'organization_unit', 'organization_relation', 'business_location', 'cost_center_record', 'job_family', 'job_profile', 'job_profile_version', 'position_record', 'position_record_version', 'position_relation', + 'position_reporting_relationship_record', 'position_reporting_relationship_version', 'assignment_record', 'job_analysis_snapshot', 'job_analysis_task_item', 'job_analysis_ksao_item', 'job_analysis_task_ksao_link', 'job_analysis_write_command', @@ -139,7 +144,9 @@ 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_reporting_relationship_record', + 'position_reporting_relationship_version' ]); 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..d52166f6b 100644 --- a/tests/foundation-contract.test.mjs +++ b/tests/foundation-contract.test.mjs @@ -53,6 +53,14 @@ function writeMigrationBackedTables(root) { 'CREATE TABLE job_analysis_write_command (tenant_record_id uuid NOT NULL);' ].join('\n') + '\n' ); + write( + root, + 'database/migrations/0020_position_reporting_relationship.sql', + [ + 'CREATE TABLE position_reporting_relationship_record (tenant_record_id uuid NOT NULL);', + 'CREATE TABLE position_reporting_relationship_version (tenant_record_id uuid NOT NULL);' + ].join('\n') + '\n' + ); } function makeMinimalValidFoundation(root) { diff --git a/tests/test_position_reporting_persistence_postgres.sh b/tests/test_position_reporting_persistence_postgres.sh index 95159cc79..a77d19453 100644 --- a/tests/test_position_reporting_persistence_postgres.sh +++ b/tests/test_position_reporting_persistence_postgres.sh @@ -15,6 +15,28 @@ for migration in \ psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f "${migration}" done +guarded_function_count="$(psql "${DATABASE_URL}" -Atqc " +SELECT count(*) +FROM pg_proc AS function_record +JOIN pg_namespace AS namespace_record + ON namespace_record.oid = function_record.pronamespace +WHERE namespace_record.nspname = 'public' + AND function_record.proname IN ( + 'enforce_position_reporting_system_time', + 'protect_position_reporting_history', + 'position_reporting_has_staffable_coverage', + 'enforce_position_reporting_scope', + 'enforce_position_reporting_anchor_alignment', + 'reject_position_reporting_truncate' + ) + AND function_record.proconfig @> + ARRAY['search_path=pg_catalog, public, pg_temp']::text[]; +")" +if [[ "${guarded_function_count}" != "6" ]]; then + echo "position-reporting governance functions are not all pinned to the trusted search_path: ${guarded_function_count}" >&2 + exit 1 +fi + TENANT_ID="10000000-0000-7000-8000-000000000001" OTHER_TENANT_ID="20000000-0000-7000-8000-000000000002" ORG_ID="00000000-0000-7000-8000-000000000011" @@ -73,6 +95,17 @@ INSERT INTO position_record ( ('${TENANT_ID}', '${OTHER_POSITION_ID}', '${ORG_ID}', '${JOB_ID}'); SQL +expect_failure \ + "position-reporting anchor accepted a reserved UUID sentinel" \ + "operational" \ + "INSERT INTO position_reporting_relationship_record ( + tenant_record_id, position_reporting_relationship_record_id, + subordinate_position_record_id, relationship_type_code + ) VALUES ( + '${TENANT_ID}', '00000000-0000-0000-0000-000000000000', + '${SUBORDINATE_POSITION_ID}', 'solid_line' + );" + canonical_event="$(python3 - < Date: Fri, 28 Aug 2026 22:01:27 +0900 Subject: [PATCH 32/32] test(position-reporting): remove concurrency winner assumption --- manifest.json | 2 +- .../test_position_reporting_concurrency_postgres.sh | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/manifest.json b/manifest.json index 694d46d28..5d940006e 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":"7415e8a9cdc26ec3e0b083939f0f35c9573cb721fce7f3d1333dc85045d3c677","bytes":7895,"lines":107},{"path":"CHANGELOG.md","sha256":"3011688169a6305e372570e0c81c366cda06687fa975fd5b358485060855822d","bytes":17728,"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":"1a9fc400d26d8137ae5911488794a6d3fa915957c95f27b36a48cef0fdf823c6","bytes":3785,"lines":81},{"path":"database/migrations/0001_foundation_schema.sql","sha256":"ce2ae52fc66b2f99597ea5285df82c66f90caa46174fef4930d68a8b6177d0dd","bytes":38747,"lines":916},{"path":"database/migrations/0002_sealed_evidence_digest.sql","sha256":"93d659ca8e0e9293a83d5422d043be7b1022c5470a5b22670aa3416fa334a04c","bytes":6649,"lines":202},{"path":"database/migrations/0003_audit_outbox_persistence.sql","sha256":"2aa7bbb8220923ec584537c0cd46f0cba2b692d69d431f097b7df6db75235bfc","bytes":15417,"lines":423},{"path":"database/migrations/0004_outbox_delivery_claim.sql","sha256":"d4504acf7d58528a2a8f4f03d1584b868c8d3ba9046a007b9c2e7cfef993b2ef","bytes":9451,"lines":234},{"path":"database/migrations/0005_outbox_delivery_finalization.sql","sha256":"b7e8790595b288f752d6ef5cc6cbfe4e1b6712248f5b7a3a25fa60016b6a4961","bytes":6125,"lines":170},{"path":"database/migrations/0006_outbox_delivery_dead_letter.sql","sha256":"c1fb91cdf98169fd6684984e86cb0a14fa19c8f1226028d2346a2a069df2b3c7","bytes":24919,"lines":628},{"path":"database/migrations/0007_outbox_retry_exhaustion.sql","sha256":"812f50d70ca5929c7eba964d34a208aedee660d11cc7ffc09d67688c4737e0d5","bytes":19081,"lines":476},{"path":"database/migrations/0008_audit_outbox_review_hardening.sql","sha256":"c3713a12db9d00fdc10005df1f86c07965e9555eefad78ca67e994537a739d9b","bytes":17562,"lines":448},{"path":"database/migrations/0009_candidate_worker_conversion_governance.sql","sha256":"4030666629a6b8deb383b8337ead4f09d6a945969313def2577a38f31f06cda9","bytes":11537,"lines":281},{"path":"database/migrations/0010_validity_study_case_integrity.sql","sha256":"3f594810ac9e1a6747a2bb4838e5ce65b921cb6e3d36fcdc3ff08b4a7579ebd1","bytes":11979,"lines":313},{"path":"database/migrations/0011_criterion_observation_scope.sql","sha256":"f9fe7c35f1ee7b167e1c2ba75a50a84febda9a6ccf8123b4f5726f51968694f9","bytes":7444,"lines":165},{"path":"database/migrations/0012_people_mutation_idempotency.sql","sha256":"52dbbb9ec7f9be5291593ba88f228d7fffd736dcb99547a08c1d6cad076afb69","bytes":3162,"lines":76},{"path":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"path":"database/migrations/0020_position_reporting_relationship.sql","sha256":"233fedd8038154db0532478c81b28858c541077a6b91666f6ae21cf803935bb0","bytes":20613,"lines":446},{"path":"docs/API_CONTRACT.md","sha256":"63533dff785da62b89e585d742a158e2aeb05913644f2bf9fb6486f281c2e589","bytes":4555,"lines":76},{"path":"docs/DATA_MODEL.md","sha256":"b4c943857ef141ce9300987962469e0322188ac830ccc1132190ce377862abf3","bytes":14116,"lines":89},{"path":"docs/ERD.md","sha256":"2dfa3d57a28e141b4574b1afb3ba449516115ac1f3bcb2dd08f54d3d1c4e5381","bytes":7669,"lines":76},{"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":"0361cc07d38017e8e5979c4a877142de5e63700496d8528090e9a042ae2d6357","bytes":11808,"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/README.md","sha256":"23bf375a9fb8f768cf290281700e836275c2080be0e9b7c697c22187d8d4df9a","bytes":1944,"lines":19},{"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":"bd32ccc59edd82ee78aa48ad3cf4610a10ca9d327f0ab470de20eacc634528dc","bytes":28595,"lines":696},{"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":"6b7dd6d193bb5d5b773dbad2c8c07a4beaeca847c8bed67b8620171f524f53a7","bytes":15172,"lines":394},{"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_reporting_concurrency_postgres.sh","sha256":"350ed12672a678137861e347a233720a61c53ccee7d113f87562f31ea022b877","bytes":6780,"lines":169},{"path":"tests/test_position_reporting_persistence_postgres.sh","sha256":"148b270c9feb97bda2936b19436caa462f79b9e0597082ec03e274d792068120","bytes":13998,"lines":314},{"path":"tests/test_position_reporting_review_binding_postgres.sh","sha256":"f2a217d00de0e99f1d7f1f8c7b3423f182cc91311c90fc3053315d59e22aa211","bytes":1524,"lines":33},{"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":"351edc968270d7bca81059e0ef21d5216ca6ea8a405aa45eb6d08aa71b5fa422","bytes":27545,"lines":642}]} +{"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":"7415e8a9cdc26ec3e0b083939f0f35c9573cb721fce7f3d1333dc85045d3c677","bytes":7895,"lines":107},{"path":"CHANGELOG.md","sha256":"3011688169a6305e372570e0c81c366cda06687fa975fd5b358485060855822d","bytes":17728,"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":"1a9fc400d26d8137ae5911488794a6d3fa915957c95f27b36a48cef0fdf823c6","bytes":3785,"lines":81},{"path":"database/migrations/0001_foundation_schema.sql","sha256":"ce2ae52fc66b2f99597ea5285df82c66f90caa46174fef4930d68a8b6177d0dd","bytes":38747,"lines":916},{"path":"database/migrations/0002_sealed_evidence_digest.sql","sha256":"93d659ca8e0e9293a83d5422d043be7b1022c5470a5b22670aa3416fa334a04c","bytes":6649,"lines":202},{"path":"database/migrations/0003_audit_outbox_persistence.sql","sha256":"2aa7bbb8220923ec584537c0cd46f0cba2b692d69d431f097b7df6db75235bfc","bytes":15417,"lines":423},{"path":"database/migrations/0004_outbox_delivery_claim.sql","sha256":"d4504acf7d58528a2a8f4f03d1584b868c8d3ba9046a007b9c2e7cfef993b2ef","bytes":9451,"lines":234},{"path":"database/migrations/0005_outbox_delivery_finalization.sql","sha256":"b7e8790595b288f752d6ef5cc6cbfe4e1b6712248f5b7a3a25fa60016b6a4961","bytes":6125,"lines":170},{"path":"database/migrations/0006_outbox_delivery_dead_letter.sql","sha256":"c1fb91cdf98169fd6684984e86cb0a14fa19c8f1226028d2346a2a069df2b3c7","bytes":24919,"lines":628},{"path":"database/migrations/0007_outbox_retry_exhaustion.sql","sha256":"812f50d70ca5929c7eba964d34a208aedee660d11cc7ffc09d67688c4737e0d5","bytes":19081,"lines":476},{"path":"database/migrations/0008_audit_outbox_review_hardening.sql","sha256":"c3713a12db9d00fdc10005df1f86c07965e9555eefad78ca67e994537a739d9b","bytes":17562,"lines":448},{"path":"database/migrations/0009_candidate_worker_conversion_governance.sql","sha256":"4030666629a6b8deb383b8337ead4f09d6a945969313def2577a38f31f06cda9","bytes":11537,"lines":281},{"path":"database/migrations/0010_validity_study_case_integrity.sql","sha256":"3f594810ac9e1a6747a2bb4838e5ce65b921cb6e3d36fcdc3ff08b4a7579ebd1","bytes":11979,"lines":313},{"path":"database/migrations/0011_criterion_observation_scope.sql","sha256":"f9fe7c35f1ee7b167e1c2ba75a50a84febda9a6ccf8123b4f5726f51968694f9","bytes":7444,"lines":165},{"path":"database/migrations/0012_people_mutation_idempotency.sql","sha256":"52dbbb9ec7f9be5291593ba88f228d7fffd736dcb99547a08c1d6cad076afb69","bytes":3162,"lines":76},{"path":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"path":"database/migrations/0020_position_reporting_relationship.sql","sha256":"233fedd8038154db0532478c81b28858c541077a6b91666f6ae21cf803935bb0","bytes":20613,"lines":446},{"path":"docs/API_CONTRACT.md","sha256":"63533dff785da62b89e585d742a158e2aeb05913644f2bf9fb6486f281c2e589","bytes":4555,"lines":76},{"path":"docs/DATA_MODEL.md","sha256":"b4c943857ef141ce9300987962469e0322188ac830ccc1132190ce377862abf3","bytes":14116,"lines":89},{"path":"docs/ERD.md","sha256":"2dfa3d57a28e141b4574b1afb3ba449516115ac1f3bcb2dd08f54d3d1c4e5381","bytes":7669,"lines":76},{"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":"0361cc07d38017e8e5979c4a877142de5e63700496d8528090e9a042ae2d6357","bytes":11808,"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/README.md","sha256":"23bf375a9fb8f768cf290281700e836275c2080be0e9b7c697c22187d8d4df9a","bytes":1944,"lines":19},{"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":"bd32ccc59edd82ee78aa48ad3cf4610a10ca9d327f0ab470de20eacc634528dc","bytes":28595,"lines":696},{"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":"6b7dd6d193bb5d5b773dbad2c8c07a4beaeca847c8bed67b8620171f524f53a7","bytes":15172,"lines":394},{"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_reporting_concurrency_postgres.sh","sha256":"a894e4f764c3355950106af040336636c10efbc612891951e13da88d9b1044df","bytes":6845,"lines":170},{"path":"tests/test_position_reporting_persistence_postgres.sh","sha256":"148b270c9feb97bda2936b19436caa462f79b9e0597082ec03e274d792068120","bytes":13998,"lines":314},{"path":"tests/test_position_reporting_review_binding_postgres.sh","sha256":"f2a217d00de0e99f1d7f1f8c7b3423f182cc91311c90fc3053315d59e22aa211","bytes":1524,"lines":33},{"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":"351edc968270d7bca81059e0ef21d5216ca6ea8a405aa45eb6d08aa71b5fa422","bytes":27545,"lines":642}]} diff --git a/tests/test_position_reporting_concurrency_postgres.sh b/tests/test_position_reporting_concurrency_postgres.sh index d6987dea7..4677f49ee 100644 --- a/tests/test_position_reporting_concurrency_postgres.sh +++ b/tests/test_position_reporting_concurrency_postgres.sh @@ -145,13 +145,14 @@ wait "${pid_x}" status_x=$? set -e -if [[ ${status_x} -ne 0 ]]; then - echo "first concurrent position-reporting mutation unexpectedly failed:" >&2 +if ! ( + [[ ${status_x} -eq 0 && ${status_y} -ne 0 && "$(cat "${log_y}")" == *"cycle"* ]] || + [[ ${status_y} -eq 0 && ${status_x} -ne 0 && "$(cat "${log_x}")" == *"cycle"* ]] +); then + echo "concurrent opposite reporting mutations did not leave exactly one successful edge and one cycle rejection:" >&2 + echo "X status=${status_x}" >&2 cat "${log_x}" >&2 - exit 1 -fi -if [[ ${status_y} -eq 0 || "$(cat "${log_y}")" != *"cycle"* ]]; then - echo "concurrent opposite reporting mutations committed a management cycle instead of serializing fail-closed:" >&2 + echo "Y status=${status_y}" >&2 cat "${log_y}" >&2 exit 1 fi