From 8c16f86fbe952546d5ccaf02c09131b2f772ecad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 17:11:29 -0700 Subject: [PATCH 1/4] test(job-analysis): define qualification-rule persistence contract --- ...qualification-rule-persistence-quality.yml | 90 ++++++ ...qualification_rule_persistence_postgres.sh | 264 ++++++++++++++++++ 2 files changed, 354 insertions(+) create mode 100644 .github/workflows/job-qualification-rule-persistence-quality.yml create mode 100755 tests/test_job_qualification_rule_persistence_postgres.sh diff --git a/.github/workflows/job-qualification-rule-persistence-quality.yml b/.github/workflows/job-qualification-rule-persistence-quality.yml new file mode 100644 index 000000000..e8e77ab50 --- /dev/null +++ b/.github/workflows/job-qualification-rule-persistence-quality.yml @@ -0,0 +1,90 @@ +name: Job Qualification Rule Persistence Quality + +on: + pull_request: + branches: + - develop + - feat/job-qualification-rule-review + paths: + - "database/migrations/0019_job_qualification_rule_persistence.sql" + - "tests/test_job_qualification_rule_persistence_postgres.sh" + - "docs/adr/0105-job-qualification-rule-persistence.md" + - "docs/traceability/job-qualification-rule-persistence.md" + - "docs/doctoring/job-qualification-rule-persistence-references.md" + - ".github/workflows/job-qualification-rule-persistence-quality.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: job-qualification-rule-persistence-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + postgres_contract: + name: Governed Job qualification-rule 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 qualification-rule persistence provenance + run: | + python - <<'PY' + import hashlib + import json + from pathlib import Path + + paths = [ + "tests/test_job_qualification_rule_persistence_postgres.sh", + ".github/workflows/job-qualification-rule-persistence-quality.yml", + ] + optional = [ + "database/migrations/0019_job_qualification_rule_persistence.sql", + "docs/adr/0105-job-qualification-rule-persistence.md", + "docs/traceability/job-qualification-rule-persistence.md", + "docs/doctoring/job-qualification-rule-persistence-references.md", + ] + rows = [] + for path_text in paths + [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 Job qualification-rule persistence regressions + run: bash tests/test_job_qualification_rule_persistence_postgres.sh + - name: Require clean checkout + run: | + git diff --exit-code + test -z "$(git status --porcelain)" diff --git a/tests/test_job_qualification_rule_persistence_postgres.sh b/tests/test_job_qualification_rule_persistence_postgres.sh new file mode 100755 index 000000000..e462559ea --- /dev/null +++ b/tests/test_job_qualification_rule_persistence_postgres.sh @@ -0,0 +1,264 @@ +#!/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/0013_job_analysis_snapshot.sql \ + database/migrations/0019_job_qualification_rule_persistence.sql; do + 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" +JOB_ID="00000000-0000-7000-8000-000000000021" +OTHER_JOB_ID="00000000-0000-7000-8000-000000000022" +ANALYSIS_ID="00000000-0000-7000-8000-000000000081" +OTHER_ANALYSIS_ID="00000000-0000-7000-8000-000000000082" +DRAFT_ANALYSIS_ID="00000000-0000-7000-8000-000000000083" +RULE_ID="00000000-0000-7000-8000-000000000091" +RULE_VERSION_ID="00000000-0000-7000-8000-000000000092" +OTHER_RULE_ID="00000000-0000-7000-8000-000000000093" +OTHER_RULE_VERSION_ID="00000000-0000-7000-8000-000000000094" +AUDIT_ID="00000000-0000-4000-8000-000000000095" +OUTBOX_ID="00000000-0000-4000-8000-000000000096" +OTHER_AUDIT_ID="00000000-0000-4000-8000-000000000097" +OTHER_OUTBOX_ID="00000000-0000-4000-8000-000000000098" +SNAPSHOT_DIGEST="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +OTHER_SNAPSHOT_DIGEST="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" +RULE_DIGEST="dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" +TASK_DIGEST="eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +KSAO_DIGEST="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +SOURCE_DIGEST="1111111111111111111111111111111111111111111111111111111111111111" +REVIEW_DIGEST="2222222222222222222222222222222222222222222222222222222222222222" +REVIEWER="actor:00000000-0000-4000-8000-000000000031" + +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 +mismatch_output="$({ with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c " +INSERT INTO job_qualification_rule_version ( + tenant_record_id, job_qualification_rule_version_id, job_qualification_rule_record_id, + analysis_record_id, rule_category_code, qualification_rule_artifact_digest_sha256, + job_analysis_snapshot_digest_sha256, task_linkage_digest_sha256, + ksao_linkage_digest_sha256, source_evidence_digest_sha256, + review_evidence_digest_sha256, reviewer_actor_reference, evidence_version, + reviewed_at, effective_from, audit_event_record_id +) VALUES ( + '${TENANT_ID}', '00000000-0000-7000-8000-000000000099', '${RULE_ID}', + '${OTHER_ANALYSIS_ID}', 'experience_requirement', '${RULE_DIGEST}', + '${OTHER_SNAPSHOT_DIGEST}', '${TASK_DIGEST}', '${KSAO_DIGEST}', '${SOURCE_DIGEST}', + '${REVIEW_DIGEST}', '${REVIEWER}', 2, TIMESTAMPTZ '2026-08-23 01:00:00+00', + DATE '2026-09-01', '${AUDIT_ID}' +);" ; } 2>&1)" +mismatch_status=$? +set -e +if [[ ${mismatch_status} -eq 0 || "${mismatch_output}" != *"same Job"* ]]; then + echo "qualification rule accepted a Job Analysis snapshot from another Job: ${mismatch_output}" >&2 + exit 1 +fi + +set +e +draft_output="$({ with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c " +INSERT INTO job_qualification_rule_version ( + tenant_record_id, job_qualification_rule_version_id, job_qualification_rule_record_id, + analysis_record_id, rule_category_code, qualification_rule_artifact_digest_sha256, + job_analysis_snapshot_digest_sha256, task_linkage_digest_sha256, + ksao_linkage_digest_sha256, source_evidence_digest_sha256, + review_evidence_digest_sha256, reviewer_actor_reference, evidence_version, + reviewed_at, effective_from, audit_event_record_id +) VALUES ( + '${TENANT_ID}', '00000000-0000-7000-8000-000000000100', '${RULE_ID}', + '${DRAFT_ANALYSIS_ID}', 'experience_requirement', '${RULE_DIGEST}', + '${OTHER_SNAPSHOT_DIGEST}', '${TASK_DIGEST}', '${KSAO_DIGEST}', '${SOURCE_DIGEST}', + '${REVIEW_DIGEST}', '${REVIEWER}', 2, TIMESTAMPTZ '2026-08-23 01:00:00+00', + DATE '2026-09-01', '${AUDIT_ID}' +);" ; } 2>&1)" +draft_status=$? +set -e +if [[ ${draft_status} -eq 0 || "${draft_output}" != *"validated Job Analysis"* ]]; then + echo "qualification rule accepted an unvalidated Job Analysis snapshot: ${draft_output}" >&2 + exit 1 +fi + +set +e +backdated_output="$({ with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c " +INSERT INTO job_qualification_rule_record ( + tenant_record_id, job_qualification_rule_record_id, job_profile_id, recorded_from +) VALUES ( + '${TENANT_ID}', '00000000-0000-7000-8000-000000000101', '${JOB_ID}', + 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 "qualification-rule anchor accepted caller-backdated system time: ${backdated_output}" >&2 + exit 1 +fi + +set +e +rewrite_output="$({ with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c " +UPDATE job_qualification_rule_version +SET rule_category_code = 'experience_requirement' +WHERE job_qualification_rule_version_id = '${RULE_VERSION_ID}'::uuid;" ; } 2>&1)" +rewrite_status=$? +set -e +if [[ ${rewrite_status} -eq 0 || "${rewrite_output}" != *"history"* ]]; then + echo "qualification-rule recorded evidence 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 job_qualification_rule_version;" ; } 2>&1)" +truncate_status=$? +set -e +if [[ ${truncate_status} -eq 0 || "${truncate_output}" != *"cannot be truncated"* ]]; then + echo "qualification-rule evidence could be truncated: ${truncate_output}" >&2 + exit 1 +fi + +with_tenant "${OTHER_TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 <&2 + exit 1 +fi + +echo "job qualification-rule persistence contract passed" From 3762dc422de5229dd7fc2e4dec20507e4506b016 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 17:13:49 -0700 Subject: [PATCH 2/4] feat(job-analysis): persist reviewed qualification-rule evidence --- ...019_job_qualification_rule_persistence.sql | 358 ++++++++++++++++++ ...0105-job-qualification-rule-persistence.md | 36 ++ ...alification-rule-persistence-references.md | 21 + .../job-qualification-rule-persistence.md | 26 ++ 4 files changed, 441 insertions(+) create mode 100644 database/migrations/0019_job_qualification_rule_persistence.sql create mode 100644 docs/adr/0105-job-qualification-rule-persistence.md create mode 100644 docs/doctoring/job-qualification-rule-persistence-references.md create mode 100644 docs/traceability/job-qualification-rule-persistence.md diff --git a/database/migrations/0019_job_qualification_rule_persistence.sql b/database/migrations/0019_job_qualification_rule_persistence.sql new file mode 100644 index 000000000..59e56b89b --- /dev/null +++ b/database/migrations/0019_job_qualification_rule_persistence.sql @@ -0,0 +1,358 @@ +-- Persist human-reviewed Job qualification-rule evidence without granting +-- candidate or employment-decision authority. The reviewed artifact remains +-- value-minimized: raw rule text, candidate/person PII, cut scores, assessment +-- outcomes and model output are deliberately outside these relations. + +CREATE TABLE job_qualification_rule_record ( + tenant_record_id uuid NOT NULL REFERENCES tenant_record(tenant_record_id), + job_qualification_rule_record_id uuid PRIMARY KEY, + job_profile_id uuid NOT NULL, + recorded_from timestamptz NOT NULL DEFAULT pg_catalog.transaction_timestamp(), + recorded_to timestamptz, + CONSTRAINT job_qualification_rule_record_id_operational_check + CHECK (public.is_operational_uuid(job_qualification_rule_record_id)), + CONSTRAINT job_qualification_rule_job_tenant_fk + FOREIGN KEY (tenant_record_id, job_profile_id) + REFERENCES job_profile(tenant_record_id, job_profile_id), + CONSTRAINT job_qualification_rule_recorded_period_check + CHECK (recorded_to IS NULL OR recorded_to > recorded_from), + CONSTRAINT job_qualification_rule_record_tenant_identity_unique + UNIQUE (tenant_record_id, job_qualification_rule_record_id) +); + +CREATE TABLE job_qualification_rule_version ( + tenant_record_id uuid NOT NULL REFERENCES tenant_record(tenant_record_id), + job_qualification_rule_version_id uuid PRIMARY KEY, + job_qualification_rule_record_id uuid NOT NULL, + analysis_record_id uuid NOT NULL, + rule_category_code text NOT NULL, + qualification_rule_artifact_digest_sha256 text NOT NULL, + job_analysis_snapshot_digest_sha256 text NOT NULL, + task_linkage_digest_sha256 text NOT NULL, + ksao_linkage_digest_sha256 text NOT NULL, + source_evidence_digest_sha256 text NOT NULL, + review_evidence_digest_sha256 text NOT NULL, + reviewer_actor_reference text NOT NULL, + evidence_version integer 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, + activation_state text NOT NULL DEFAULT 'requires_authoritative_activation', + decision_authority_state text NOT NULL + DEFAULT 'not_authorized_for_candidate_or_employment_decision', + CONSTRAINT job_qualification_rule_version_id_operational_check + CHECK (public.is_operational_uuid(job_qualification_rule_version_id)), + CONSTRAINT job_qualification_rule_version_record_tenant_fk + FOREIGN KEY (tenant_record_id, job_qualification_rule_record_id) + REFERENCES job_qualification_rule_record( + tenant_record_id, + job_qualification_rule_record_id + ), + CONSTRAINT job_qualification_rule_version_analysis_tenant_fk + FOREIGN KEY (tenant_record_id, analysis_record_id) + REFERENCES job_analysis_snapshot(tenant_record_id, analysis_record_id), + CONSTRAINT job_qualification_rule_version_audit_tenant_fk + FOREIGN KEY (tenant_record_id, audit_event_record_id) + REFERENCES audit_event_record(tenant_record_id, audit_event_record_id), + CONSTRAINT job_qualification_rule_category_check + CHECK (rule_category_code IN ( + 'credential_requirement', + 'education_training_requirement', + 'experience_requirement', + 'knowledge_skill_ability_requirement', + 'task_or_work_requirement' + )), + CONSTRAINT job_qualification_rule_artifact_digest_check + CHECK (qualification_rule_artifact_digest_sha256 ~ '^[0-9a-f]{64}$'), + CONSTRAINT job_qualification_rule_snapshot_digest_check + CHECK (job_analysis_snapshot_digest_sha256 ~ '^[0-9a-f]{64}$'), + CONSTRAINT job_qualification_rule_task_digest_check + CHECK (task_linkage_digest_sha256 ~ '^[0-9a-f]{64}$'), + CONSTRAINT job_qualification_rule_ksao_digest_check + CHECK (ksao_linkage_digest_sha256 ~ '^[0-9a-f]{64}$'), + CONSTRAINT job_qualification_rule_source_digest_check + CHECK (source_evidence_digest_sha256 ~ '^[0-9a-f]{64}$'), + CONSTRAINT job_qualification_rule_review_digest_check + CHECK (review_evidence_digest_sha256 ~ '^[0-9a-f]{64}$'), + CONSTRAINT job_qualification_rule_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 job_qualification_rule_evidence_version_check + CHECK (evidence_version BETWEEN 1 AND 2147483647), + CONSTRAINT job_qualification_rule_effective_period_check + CHECK (effective_to IS NULL OR effective_to > effective_from), + CONSTRAINT job_qualification_rule_recorded_period_check + CHECK (recorded_to IS NULL OR recorded_to > recorded_from), + CONSTRAINT job_qualification_rule_review_chronology_check + CHECK (reviewed_at <= recorded_from), + CONSTRAINT job_qualification_rule_activation_state_check + CHECK (activation_state = 'requires_authoritative_activation'), + CONSTRAINT job_qualification_rule_decision_authority_check + CHECK ( + decision_authority_state = + 'not_authorized_for_candidate_or_employment_decision' + ), + CONSTRAINT job_qualification_rule_version_tenant_identity_unique + UNIQUE (tenant_record_id, job_qualification_rule_version_id), + CONSTRAINT job_qualification_rule_audit_event_unique + UNIQUE (tenant_record_id, audit_event_record_id), + CONSTRAINT job_qualification_rule_bitemporal_exclusion + EXCLUDE USING gist ( + tenant_record_id WITH =, + job_qualification_rule_record_id WITH =, + daterange(effective_from, effective_to, '[)') WITH &&, + tstzrange(recorded_from, recorded_to, '[)') WITH && + ) +); + +CREATE FUNCTION enforce_job_qualification_rule_system_time() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + IF NEW.recorded_to IS NOT NULL THEN + RAISE EXCEPTION 'Job qualification-rule 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 'Job qualification-rule recorded_from must equal the current transaction timestamp' + USING ERRCODE = '22023'; + END IF; + RETURN NEW; +END; +$$; + +COMMENT ON FUNCTION enforce_job_qualification_rule_system_time() IS + 'Guards new Job qualification-rule anchors and versions: system-recorded time is PostgreSQL transaction time and new recorded intervals begin open.'; + +CREATE TRIGGER job_qualification_rule_record_system_time_guard +BEFORE INSERT ON job_qualification_rule_record +FOR EACH ROW +EXECUTE FUNCTION enforce_job_qualification_rule_system_time(); + +CREATE TRIGGER job_qualification_rule_version_system_time_guard +BEFORE INSERT ON job_qualification_rule_version +FOR EACH ROW +EXECUTE FUNCTION enforce_job_qualification_rule_system_time(); + +CREATE FUNCTION protect_job_qualification_rule_history() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + IF TG_OP = 'DELETE' THEN + RAISE EXCEPTION 'Job qualification-rule 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 'Job qualification-rule 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_job_qualification_rule_history() IS + 'Preserves bitemporal Job qualification-rule evidence: DELETE and in-place rewrites fail closed; the only UPDATE is closing an open recorded interval at PostgreSQL transaction time.'; + +CREATE TRIGGER job_qualification_rule_record_history_guard +BEFORE UPDATE OR DELETE ON job_qualification_rule_record +FOR EACH ROW +EXECUTE FUNCTION protect_job_qualification_rule_history(); + +CREATE TRIGGER job_qualification_rule_version_history_guard +BEFORE UPDATE OR DELETE ON job_qualification_rule_version +FOR EACH ROW +EXECUTE FUNCTION protect_job_qualification_rule_history(); + +CREATE FUNCTION enforce_job_qualification_rule_scope() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +DECLARE + anchor_job_profile_id uuid; + anchor_recorded_to timestamptz; + snapshot_job_profile_id uuid; + snapshot_status_code text; + snapshot_digest text; + snapshot_reviewed_by text; + snapshot_reviewed_at timestamptz; + audit_event jsonb; + outbox_found boolean; +BEGIN + SELECT job_profile_id, recorded_to + INTO anchor_job_profile_id, anchor_recorded_to + FROM job_qualification_rule_record + WHERE tenant_record_id = NEW.tenant_record_id + AND job_qualification_rule_record_id = NEW.job_qualification_rule_record_id + FOR SHARE; + + IF NOT FOUND OR anchor_recorded_to IS NOT NULL THEN + RAISE EXCEPTION 'Job qualification-rule version requires an open same-tenant rule anchor' + USING ERRCODE = '23514'; + END IF; + + SELECT job_profile_id, status_code, content_digest_sha256, + reviewed_by_reference, reviewed_at + INTO snapshot_job_profile_id, snapshot_status_code, snapshot_digest, + snapshot_reviewed_by, snapshot_reviewed_at + FROM job_analysis_snapshot + WHERE tenant_record_id = NEW.tenant_record_id + AND analysis_record_id = NEW.analysis_record_id + FOR SHARE; + + IF NOT FOUND THEN + RAISE EXCEPTION 'Job qualification-rule version requires a same-tenant Job Analysis snapshot' + USING ERRCODE = '23514'; + END IF; + IF snapshot_job_profile_id IS DISTINCT FROM anchor_job_profile_id THEN + RAISE EXCEPTION 'Job qualification-rule anchor and Job Analysis snapshot must resolve to the same Job' + USING ERRCODE = '23514'; + END IF; + IF snapshot_status_code <> 'analysis_validated' + OR snapshot_reviewed_by IS NULL + OR snapshot_reviewed_at IS NULL THEN + RAISE EXCEPTION 'Job qualification-rule version requires a human-reviewed validated Job Analysis snapshot' + USING ERRCODE = '23514'; + END IF; + IF snapshot_digest IS DISTINCT FROM NEW.job_analysis_snapshot_digest_sha256 THEN + RAISE EXCEPTION 'Job qualification-rule version snapshot digest does not match authoritative Job Analysis evidence' + 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 'Job qualification-rule 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 'Job qualification-rule version requires transactional audit/outbox evidence' + USING ERRCODE = '23514'; + END IF; + + IF audit_event ->> 'orgmetrapurpose' <> 'job_qualification_rule_review' + OR audit_event ->> 'orgmetraactor' <> NEW.reviewer_actor_reference + OR audit_event ->> 'orgmetraevidence' <> NEW.review_evidence_digest_sha256 + OR audit_event ->> 'subject' + <> 'job_qualification_rule:' || NEW.job_qualification_rule_record_id::text + OR audit_event #>> '{data,result_code}' <> 'reviewed_for_authoritative_activation' + OR (audit_event #>> '{data,high_impact}')::boolean IS DISTINCT FROM false + OR (audit_event ->> 'time')::timestamptz IS DISTINCT FROM NEW.reviewed_at THEN + RAISE EXCEPTION 'Job qualification-rule audit evidence does not exactly match the reviewed rule scope' + USING ERRCODE = '23514'; + END IF; + + RETURN NEW; +END; +$$; + +COMMENT ON FUNCTION enforce_job_qualification_rule_scope() IS + 'Before a rule version is persisted, re-resolves its open rule anchor, same Job validated Job Analysis snapshot, exact snapshot digest, human review provenance, immutable audit event and integration-hub outbox correlation. The persisted rule remains non-authorizing.'; + +CREATE TRIGGER job_qualification_rule_version_scope_guard +BEFORE INSERT ON job_qualification_rule_version +FOR EACH ROW +EXECUTE FUNCTION enforce_job_qualification_rule_scope(); + +CREATE FUNCTION enforce_job_qualification_rule_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 job_qualification_rule_version AS version + WHERE version.tenant_record_id = NEW.tenant_record_id + AND version.job_qualification_rule_record_id = NEW.job_qualification_rule_record_id + AND (version.recorded_to IS NULL OR version.recorded_to > NEW.recorded_to) + ) THEN + RAISE EXCEPTION 'cannot close Job qualification-rule anchor while a recorded version remains open' + USING ERRCODE = '23514'; + END IF; + RETURN NULL; +END; +$$; + +COMMENT ON FUNCTION enforce_job_qualification_rule_anchor_alignment() IS + 'Deferred anchor-closure guard: every version must be recorded closed no later than its durable rule anchor before commit.'; + +CREATE CONSTRAINT TRIGGER job_qualification_rule_anchor_alignment_guard +AFTER UPDATE ON job_qualification_rule_record +DEFERRABLE INITIALLY DEFERRED +FOR EACH ROW +EXECUTE FUNCTION enforce_job_qualification_rule_anchor_alignment(); + +CREATE FUNCTION reject_job_qualification_rule_truncate() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + RAISE EXCEPTION 'Job qualification-rule history cannot be truncated' + USING ERRCODE = '55000'; +END; +$$; + +COMMENT ON FUNCTION reject_job_qualification_rule_truncate() IS + 'Rejects table-wide TRUNCATE so governed Job qualification-rule evidence cannot bypass row-level bitemporal history guards.'; + +CREATE TRIGGER job_qualification_rule_record_truncate_guard +BEFORE TRUNCATE ON job_qualification_rule_record +FOR EACH STATEMENT +EXECUTE FUNCTION reject_job_qualification_rule_truncate(); + +CREATE TRIGGER job_qualification_rule_version_truncate_guard +BEFORE TRUNCATE ON job_qualification_rule_version +FOR EACH STATEMENT +EXECUTE FUNCTION reject_job_qualification_rule_truncate(); + +REVOKE TRUNCATE ON job_qualification_rule_record FROM PUBLIC; +REVOKE TRUNCATE ON job_qualification_rule_version FROM PUBLIC; + +ALTER TABLE job_qualification_rule_record ENABLE ROW LEVEL SECURITY; +ALTER TABLE job_qualification_rule_record FORCE ROW LEVEL SECURITY; +CREATE POLICY job_qualification_rule_record_scope_policy +ON job_qualification_rule_record +USING (tenant_record_id = current_tenant_record_id()) +WITH CHECK (tenant_record_id = current_tenant_record_id()); + +ALTER TABLE job_qualification_rule_version ENABLE ROW LEVEL SECURITY; +ALTER TABLE job_qualification_rule_version FORCE ROW LEVEL SECURITY; +CREATE POLICY job_qualification_rule_version_scope_policy +ON job_qualification_rule_version +USING (tenant_record_id = current_tenant_record_id()) +WITH CHECK (tenant_record_id = current_tenant_record_id()); + +COMMENT ON TABLE job_qualification_rule_record IS + 'Durable tenant-scoped Job qualification-rule anchor. It stores no raw rule text or candidate/person data; system-recorded time is PostgreSQL transaction time and closure is bitemporal history, not deletion.'; + +COMMENT ON TABLE job_qualification_rule_version IS + 'Human-reviewed, evidence-backed Job qualification-rule version. It binds one open rule anchor to the same Job validated Job Analysis snapshot, exact SHA-256 provenance, reviewer and immutable audit/outbox evidence while remaining explicitly unauthorized for candidate or employment decisions until a separate authoritative activation boundary confirms use.'; diff --git a/docs/adr/0105-job-qualification-rule-persistence.md b/docs/adr/0105-job-qualification-rule-persistence.md new file mode 100644 index 000000000..4be74c0aa --- /dev/null +++ b/docs/adr/0105-job-qualification-rule-persistence.md @@ -0,0 +1,36 @@ +# ADR 0105: Governed Job qualification-rule persistence + +Status: Active PR + +## Context + +Protected `develop` stores Job Analysis Task/FJA/KSAO evidence, and PR #104 adds a human-reviewed qualification-rule proposal that is explicitly non-authorizing. Orgmetra still needs a durable, tenant-isolated persistence boundary so a reviewed rule artifact can be versioned without collapsing Job, Job Analysis, candidate evaluation and employment-decision authority into one record. + +OPM describes job analysis as the foundation for assessment and selection decisions and says qualification standards should be supplemented by job analysis rather than used as a substitute for applicant KSA/competency analysis. This is design evidence, not a claim that Orgmetra implements U.S. federal qualification policy. + +## Decision + +Introduce two normalized relations in migration `0019_job_qualification_rule_persistence.sql`: + +- `job_qualification_rule_record` is the durable tenant-qualified anchor owned by one authoritative Job. +- `job_qualification_rule_version` is the bitemporal reviewed version. It references one Job Analysis snapshot, controlled rule category, SHA-256 artifact/Task/KSAO/source/review provenance, reviewer, evidence version, effective time, system-recorded time and one immutable audit event. + +Before INSERT, the database re-resolves that the anchor is open, the Job Analysis snapshot belongs to the same Job, the snapshot is human-reviewed and `analysis_validated`, and its content digest exactly matches the supplied snapshot digest. The audit event must be same-tenant, have the exact reviewed rule subject/purpose/actor/evidence/time/result, and have an `integration_hub` outbox record. + +The version always persists with `activation_state = requires_authoritative_activation` and `decision_authority_state = not_authorized_for_candidate_or_employment_decision`. Persisting reviewed evidence therefore does not authorize candidate screening, rejection, ranking, hiring or any other high-impact employment action. A later activation/use boundary must re-resolve current authority and emit its own immutable evidence. + +Raw qualification-rule text, candidate/person identifiers, PII, assessment outcomes, cut scores, compensation and model output are intentionally absent from these relations. The artifact is represented by digest and controlled metadata only. + +System-recorded `recorded_from` must equal PostgreSQL `transaction_timestamp()`; callers cannot backdate recorded truth. Effective time remains independent business time. Recorded history is append/correction-only, TRUNCATE is rejected, and FORCE RLS uses the existing transaction-local tenant context. A durable anchor cannot close while a child version remains recorded open. + +## Consequences + +- Job qualification evidence becomes queryable and historically reconstructable without becoming autonomous decision logic. +- Job/Job Analysis drift, foreign-tenant evidence, unvalidated analysis, digest mismatch, missing audit/outbox evidence and post hoc history rewrites fail closed. +- The database stores only governance metadata and evidence digests; customer-facing rule content remains in its separately governed artifact boundary. +- PR #104 remains the dependency root. This persistence PR stays Draft until #104 integrates, then must be retargeted and revalidated against fresh protected `develop` without inheriting predecessor evidence. +- This ADR does not claim legal compliance, validity, certification, or production activation of qualification rules. + +## Primary sources reviewed + +See `docs/doctoring/job-qualification-rule-persistence-references.md`. diff --git a/docs/doctoring/job-qualification-rule-persistence-references.md b/docs/doctoring/job-qualification-rule-persistence-references.md new file mode 100644 index 000000000..06b68ab02 --- /dev/null +++ b/docs/doctoring/job-qualification-rule-persistence-references.md @@ -0,0 +1,21 @@ +# Job qualification-rule persistence references + +Reviewed: 2026-08-24. + +The sources below are design evidence only. Orgmetra does not claim that this feature establishes legal compliance, selection validity, federal qualification-policy conformance, or certification. + +## APA 7 references + +U.S. Office of Personnel Management. (n.d.). *Job analysis*. Retrieved August 24, 2026, from https://www.opm.gov/policy-data-oversight/assessment-and-selection/job-analysis/ + +U.S. Office of Personnel Management. (2022, May). *General Schedule qualification policies*. https://www.opm.gov/policy-data-oversight/classification-qualifications/general-schedule-qualification-policies/ + +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 + +## Material design implications + +- OPM treats job analysis as the documented link between job tasks/competencies and assessment or selection practice; qualification requirements should therefore retain exact Job Analysis provenance rather than become detached candidate-screening rules. +- OPM distinguishes minimum qualification standards from ranking or identifying the best-qualified applicant. Orgmetra therefore stores a reviewed rule artifact without granting candidate-ranking or employment-decision authority. +- PostgreSQL exclusion constraints support the single-valued bitemporal version contract, while row-security policies provide the database-level tenant visibility/check boundary used by the `NOSUPERUSER NOBYPASSRLS` application role. diff --git a/docs/traceability/job-qualification-rule-persistence.md b/docs/traceability/job-qualification-rule-persistence.md new file mode 100644 index 000000000..162022f74 --- /dev/null +++ b/docs/traceability/job-qualification-rule-persistence.md @@ -0,0 +1,26 @@ +# Job qualification-rule persistence traceability + +## State + +- Protected-main truth: `develop@9e3e4847510e1e612b48474ba42b177b8ed824df` stores Job Analysis snapshot/Task/KSAO evidence but has no normalized durable qualification-rule persistence relation. +- Dependency active PR: #104 `feat/job-qualification-rule-review@d92ac4cb798b3bd32b632c0ab677c03f944070e4` adds the human-reviewed, non-authorizing proposal evidence. +- This active stacked PR: `feat/job-qualification-rule-persistence` adds durable normalized persistence only after that review boundary. +- Planned: separate authoritative activation/use boundary for recruiting or selection. This PR does not activate rules or evaluate people. + +## Requirement mapping + +| Requirement | Owned boundary | Evidence | +|---|---|---| +| PRD FR-004 stores qualification rules | `job_qualification_rule_record`, `job_qualification_rule_version` | `tests/test_job_qualification_rule_persistence_postgres.sh` | +| Preserve Job / Job Analysis provenance | version INSERT scope trigger | same-Job validated snapshot + exact snapshot digest regressions | +| Preserve Task/KSAO/source evidence | SHA-256 linkage columns | lower-case digest constraints and exact canonical review evidence | +| Human review without autonomous decision authority | reviewer/evidence version + fixed governance states | valid persisted state is `requires_authoritative_activation` and `not_authorized_for_candidate_or_employment_decision` | +| Immutable audit/outbox | `audit_event_record_id` + scope trigger | exact subject/purpose/actor/evidence/time/result and `integration_hub` outbox required | +| Effective and system-recorded time | effective range + recorded range | backdated `recorded_from` rejection, bitemporal exclusion, correction-only closure | +| Tenant isolation | FORCE RLS on both relations | `NOSUPERUSER NOBYPASSRLS` reader sees only its tenant; missing context sees no rows | +| Data minimization | relation shape | no candidate/person identifiers, PII, raw rule text, scores, compensation or model output columns | +| History integrity | history and TRUNCATE guards | in-place rewrite, DELETE and TRUNCATE fail closed; anchor cannot close before versions | + +## Stack discipline + +#104 is the dependency root and must integrate first. This descendant must remain Draft while #104 is unmerged. After #104 integrates, retarget to fresh protected `develop`, refetch exact head/base/rules/reviews/threads/checks, rerun all applicable hosted evidence on the resulting exact head, and do not transfer predecessor checks or reviews. From 168f19402b3b17762cfe60f8a0e93c649a082989 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 17:15:41 -0700 Subject: [PATCH 3/4] test(job-analysis): keep RLS fixture tenant-neutral --- .../test_job_qualification_rule_persistence_postgres.sh | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/test_job_qualification_rule_persistence_postgres.sh b/tests/test_job_qualification_rule_persistence_postgres.sh index e462559ea..ff4cb2f5f 100755 --- a/tests/test_job_qualification_rule_persistence_postgres.sh +++ b/tests/test_job_qualification_rule_persistence_postgres.sh @@ -21,12 +21,8 @@ OTHER_ANALYSIS_ID="00000000-0000-7000-8000-000000000082" DRAFT_ANALYSIS_ID="00000000-0000-7000-8000-000000000083" RULE_ID="00000000-0000-7000-8000-000000000091" RULE_VERSION_ID="00000000-0000-7000-8000-000000000092" -OTHER_RULE_ID="00000000-0000-7000-8000-000000000093" -OTHER_RULE_VERSION_ID="00000000-0000-7000-8000-000000000094" AUDIT_ID="00000000-0000-4000-8000-000000000095" OUTBOX_ID="00000000-0000-4000-8000-000000000096" -OTHER_AUDIT_ID="00000000-0000-4000-8000-000000000097" -OTHER_OUTBOX_ID="00000000-0000-4000-8000-000000000098" SNAPSHOT_DIGEST="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" OTHER_SNAPSHOT_DIGEST="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" RULE_DIGEST="dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" @@ -228,11 +224,6 @@ if [[ ${truncate_status} -eq 0 || "${truncate_output}" != *"cannot be truncated" exit 1 fi -with_tenant "${OTHER_TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 < Date: Fri, 28 Aug 2026 21:35:41 +0900 Subject: [PATCH 4/4] fix(job-analysis): harden qualification rule persistence --- CHANGELOG.md | 1 + ...019_job_qualification_rule_persistence.sql | 7 +++++ ...0105-job-qualification-rule-persistence.md | 2 +- .../job-qualification-rule-persistence.md | 1 + manifest.json | 2 +- scripts/foundation-contract-core.mjs | 2 ++ ...qualification_rule_persistence_postgres.sh | 29 +++++++++++++++++++ tests/validate_repository.py | 2 ++ 8 files changed, 44 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99f4752d7..c234fefa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to Orgmetra will be documented in this file. - 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. - Stacked governed job-analysis evidence contract via `JobAnalysisSnapshot`, `TaskEvidence`, `KSAORequirement`, `TaskKSAOLink`, `FunctionalJobAnalysisProfile`, and `EvidenceSource`: tenant/Job-scoped observable tasks, explicit Task-to-KSAO linkage, importance/difficulty/proficiency ratings, source/version/retrieval/SHA-256 provenance, deterministic canonical snapshot bytes, current O*NET evidence support, and historical DOT Data/People/Things compatibility. Validated snapshots require accountable human review and complete non-LLM evidence; LLM-origin material remains `analysis_draft`, and the snapshot is evidence input rather than a hiring, promotion, termination, compensation, or other high-impact employment decision. +- Active-PR normalized Job qualification-rule persistence on migration `0019_job_qualification_rule_persistence.sql`: tenant-scoped bitemporal rule anchors and reviewed versions bound to the same validated Job Analysis snapshot, exact provenance digests, immutable audit/outbox evidence, transaction-owned system time, append-only history, trusted trigger-function search paths, FORCE RLS, and no candidate-evaluation or employment-decision authority. The PostgreSQL contract executes the complete migration chain before exercising same-Job, human-reviewed, audit/outbox, history, truncation, and tenant-isolation guards. - Stacked governed audit/outbox slice via `AuditOutboxEvent`, `audit_event_record`, `outbox_delivery_record`, and `outbox_delivery_escalation_record`: CloudEvents 1.0-compatible PII-minimized metadata, exact canonical JSON bytes, database-verified SHA-256 digests, mandatory human confirmation for high-impact events, immutable audit evidence, tenant RLS, atomic audit/outbox insertion, guarded pending/leased/delivered/dead-lettered delivery state, tenant-safe `claim_outbox_delivery(...)` with deterministic due-work ordering, `FOR UPDATE ... SKIP LOCKED`, opaque worker identity, bounded future leases, immutable envelope return, and atomic takeover of genuinely expired leases only while retry attempts remain; owner-bound `complete_outbox_delivery(...)` and `retry_outbox_delivery(...)`; database-budget-governed `dead_letter_outbox_delivery(...)`; and a separately privileged `operator_dead_letter_expired_outbox_delivery(...)` recovery path for an exhausted final lease whose recorded worker identity is permanently unavailable. `maximum_attempt_count` is persisted on the delivery row, defaults to 5, is constrained to 1 through 100, and cannot be lowered by a dispatcher during finalization. Migration 0007 prevents retry or expired-lease takeover from creating attempt N+1; migration 0008 adds TRUNCATE guards, trusted function search paths, a concurrently built due-work partial index, session-independent immutable envelope validation, and operator recovery backed by separate NOLOGIN/NOBYPASSRLS owner/capability roles so the externally assignable operator role can invoke recovery without receiving direct transport-table read/write rights. Migration 0008 also rejects pre-existing reserved recovery-role names before project DDL, atomically contains the temporary schema-creation privilege used for function ownership handoff, and forces deferred escalation binding while the narrow SECURITY DEFINER owner is still active. Exponential/backoff policy selection, policy-specific producer configuration, and external delivery receipts remain subsequent work. - `orgmetra_hris_kernel` 0.4.0 with exclusive-versus-concurrent employment, staffable position coverage, exclusive-seat capacity, and `validate_assignment_write` at 100% statement and branch coverage. - `POST /v1/employment-records`, `POST /v1/position-records`, and `POST /v1/assignment-records` with the same Keyverse mutation context, confirmation, and versioned evidence composition as other high-impact commands. diff --git a/database/migrations/0019_job_qualification_rule_persistence.sql b/database/migrations/0019_job_qualification_rule_persistence.sql index 59e56b89b..cc4eba4db 100644 --- a/database/migrations/0019_job_qualification_rule_persistence.sql +++ b/database/migrations/0019_job_qualification_rule_persistence.sql @@ -3,6 +3,8 @@ -- value-minimized: raw rule text, candidate/person PII, cut scores, assessment -- outcomes and model output are deliberately outside these relations. +SET search_path = public, pg_catalog; + CREATE TABLE job_qualification_rule_record ( tenant_record_id uuid NOT NULL REFERENCES tenant_record(tenant_record_id), job_qualification_rule_record_id uuid PRIMARY KEY, @@ -113,6 +115,7 @@ CREATE TABLE job_qualification_rule_version ( CREATE FUNCTION enforce_job_qualification_rule_system_time() RETURNS trigger LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp AS $$ BEGIN IF NEW.recorded_to IS NOT NULL THEN @@ -143,6 +146,7 @@ EXECUTE FUNCTION enforce_job_qualification_rule_system_time(); CREATE FUNCTION protect_job_qualification_rule_history() RETURNS trigger LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp AS $$ BEGIN IF TG_OP = 'DELETE' THEN @@ -178,6 +182,7 @@ EXECUTE FUNCTION protect_job_qualification_rule_history(); CREATE FUNCTION enforce_job_qualification_rule_scope() RETURNS trigger LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp AS $$ DECLARE anchor_job_profile_id uuid; @@ -282,6 +287,7 @@ EXECUTE FUNCTION enforce_job_qualification_rule_scope(); CREATE FUNCTION enforce_job_qualification_rule_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 @@ -314,6 +320,7 @@ EXECUTE FUNCTION enforce_job_qualification_rule_anchor_alignment(); CREATE FUNCTION reject_job_qualification_rule_truncate() RETURNS trigger LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp AS $$ BEGIN RAISE EXCEPTION 'Job qualification-rule history cannot be truncated' diff --git a/docs/adr/0105-job-qualification-rule-persistence.md b/docs/adr/0105-job-qualification-rule-persistence.md index 4be74c0aa..92ee27993 100644 --- a/docs/adr/0105-job-qualification-rule-persistence.md +++ b/docs/adr/0105-job-qualification-rule-persistence.md @@ -21,7 +21,7 @@ The version always persists with `activation_state = requires_authoritative_acti Raw qualification-rule text, candidate/person identifiers, PII, assessment outcomes, cut scores, compensation and model output are intentionally absent from these relations. The artifact is represented by digest and controlled metadata only. -System-recorded `recorded_from` must equal PostgreSQL `transaction_timestamp()`; callers cannot backdate recorded truth. Effective time remains independent business time. Recorded history is append/correction-only, TRUNCATE is rejected, and FORCE RLS uses the existing transaction-local tenant context. A durable anchor cannot close while a child version remains recorded open. +System-recorded `recorded_from` must equal PostgreSQL `transaction_timestamp()`; callers cannot backdate recorded truth. Effective time remains independent business time. Recorded history is append/correction-only, TRUNCATE is rejected, and FORCE RLS uses the existing transaction-local tenant context. Every new trigger function pins `search_path` to `pg_catalog, public, pg_temp`, while migration-time object creation uses the trusted `public, pg_catalog` path. A durable anchor cannot close while a child version remains recorded open. ## Consequences diff --git a/docs/traceability/job-qualification-rule-persistence.md b/docs/traceability/job-qualification-rule-persistence.md index 162022f74..e625ff12f 100644 --- a/docs/traceability/job-qualification-rule-persistence.md +++ b/docs/traceability/job-qualification-rule-persistence.md @@ -18,6 +18,7 @@ | Immutable audit/outbox | `audit_event_record_id` + scope trigger | exact subject/purpose/actor/evidence/time/result and `integration_hub` outbox required | | Effective and system-recorded time | effective range + recorded range | backdated `recorded_from` rejection, bitemporal exclusion, correction-only closure | | Tenant isolation | FORCE RLS on both relations | `NOSUPERUSER NOBYPASSRLS` reader sees only its tenant; missing context sees no rows | +| Prevent trigger name-resolution shadowing | trusted migration path plus fixed trigger-function `search_path` | full migration-chain contract counts all five pinned trigger functions | | Data minimization | relation shape | no candidate/person identifiers, PII, raw rule text, scores, compensation or model output columns | | History integrity | history and TRUNCATE guards | in-place rewrite, DELETE and TRUNCATE fail closed; anchor cannot close before versions | diff --git a/manifest.json b/manifest.json index 97f2bab14..2e8a0a901 100644 --- a/manifest.json +++ b/manifest.json @@ -1 +1 @@ -{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"32cc4ef78d1eca557fa01731026840be01211a043eb0ada552e4e6cb9eace353","bytes":17295,"lines":76},{"path":"CLAUDE.md","sha256":"add33884f466d324e20875388d103de41c6e062938a6e98727dc83a87ffe976f","bytes":1229,"lines":20},{"path":"LICENSE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30","bytes":11358,"lines":202},{"path":"NOTICE","sha256":"34b4618e946bdd8d33407d6ac5279f0a0388f5e7c8f79d2e7d8c3c47d0266042","bytes":305,"lines":4},{"path":"README.md","sha256":"1a9fc400d26d8137ae5911488794a6d3fa915957c95f27b36a48cef0fdf823c6","bytes":3785,"lines":81},{"path":"database/migrations/0001_foundation_schema.sql","sha256":"ce2ae52fc66b2f99597ea5285df82c66f90caa46174fef4930d68a8b6177d0dd","bytes":38747,"lines":916},{"path":"database/migrations/0002_sealed_evidence_digest.sql","sha256":"93d659ca8e0e9293a83d5422d043be7b1022c5470a5b22670aa3416fa334a04c","bytes":6649,"lines":202},{"path":"database/migrations/0003_audit_outbox_persistence.sql","sha256":"2aa7bbb8220923ec584537c0cd46f0cba2b692d69d431f097b7df6db75235bfc","bytes":15417,"lines":423},{"path":"database/migrations/0004_outbox_delivery_claim.sql","sha256":"d4504acf7d58528a2a8f4f03d1584b868c8d3ba9046a007b9c2e7cfef993b2ef","bytes":9451,"lines":234},{"path":"database/migrations/0005_outbox_delivery_finalization.sql","sha256":"b7e8790595b288f752d6ef5cc6cbfe4e1b6712248f5b7a3a25fa60016b6a4961","bytes":6125,"lines":170},{"path":"database/migrations/0006_outbox_delivery_dead_letter.sql","sha256":"c1fb91cdf98169fd6684984e86cb0a14fa19c8f1226028d2346a2a069df2b3c7","bytes":24919,"lines":628},{"path":"database/migrations/0007_outbox_retry_exhaustion.sql","sha256":"812f50d70ca5929c7eba964d34a208aedee660d11cc7ffc09d67688c4737e0d5","bytes":19081,"lines":476},{"path":"database/migrations/0008_audit_outbox_review_hardening.sql","sha256":"c3713a12db9d00fdc10005df1f86c07965e9555eefad78ca67e994537a739d9b","bytes":17562,"lines":448},{"path":"database/migrations/0009_candidate_worker_conversion_governance.sql","sha256":"4030666629a6b8deb383b8337ead4f09d6a945969313def2577a38f31f06cda9","bytes":11537,"lines":281},{"path":"database/migrations/0010_validity_study_case_integrity.sql","sha256":"3f594810ac9e1a6747a2bb4838e5ce65b921cb6e3d36fcdc3ff08b4a7579ebd1","bytes":11979,"lines":313},{"path":"database/migrations/0011_criterion_observation_scope.sql","sha256":"f9fe7c35f1ee7b167e1c2ba75a50a84febda9a6ccf8123b4f5726f51968694f9","bytes":7444,"lines":165},{"path":"database/migrations/0012_people_mutation_idempotency.sql","sha256":"52dbbb9ec7f9be5291593ba88f228d7fffd736dcb99547a08c1d6cad076afb69","bytes":3162,"lines":76},{"path":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"path":"docs/API_CONTRACT.md","sha256":"63533dff785da62b89e585d742a158e2aeb05913644f2bf9fb6486f281c2e589","bytes":4555,"lines":76},{"path":"docs/DATA_MODEL.md","sha256":"6ad29731ae7ee7aa5bf3a2d0bfef88894a35a2550edb2be3244d6f143d76444a","bytes":13366,"lines":85},{"path":"docs/ERD.md","sha256":"546001aa85c4fe020e0c39d881dc860daf7f69090596666fdf9092487b0725fe","bytes":6964,"lines":70},{"path":"docs/OPERABILITY.md","sha256":"82b2d3e70cec371ef35e9e0f982ac40fef84351976bc04b863b81d27023d5a62","bytes":11189,"lines":71},{"path":"docs/PRD.md","sha256":"3ad85ae633cce0fc7a93af39b21d7a7c70bb2efa786da6b12f3c5327906e34f1","bytes":5490,"lines":111},{"path":"docs/SECURITY.md","sha256":"01918512d8882060e9cff0c4aa8206e0eccbdfb61cfd7f829331123c7a9fe6ac","bytes":11185,"lines":64},{"path":"docs/STORYBOARD.md","sha256":"6e4ffb0eb03a80343f50d363ffc43b34da9348a44232dd947a9ff416ea92a3d2","bytes":1342,"lines":28},{"path":"docs/STORYBOOK.md","sha256":"82f79029b3c2b7a45393bad5ba8fabe61014d4b6149c7d4e73f70ba447f885e9","bytes":1389,"lines":50},{"path":"docs/TEST_STRATEGY.md","sha256":"d0a0bc3b54ed0fc7973747987f1afb117d6144c390b51ed9370eb571972a33f8","bytes":16534,"lines":135},{"path":"docs/THREAT_MODEL.md","sha256":"f314f375c2e41252536de224c7bc7e4a10ab8f340cb86642724e7399e32f4252","bytes":6736,"lines":23},{"path":"docs/TRACEABILITY.md","sha256":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"lines":40},{"path":"docs/TRD.md","sha256":"23697d88a4882698e1a2782b7da3f2ccd0d3cd2d6d1bffe89b6597dc16851077","bytes":9064,"lines":101},{"path":"docs/UML.md","sha256":"fe67c37aa88e5814ceb2db7e8f7d8d85ca27a994802efbb7c75164b387adf0a9","bytes":5528,"lines":122},{"path":"docs/USER_STORIES.md","sha256":"5535b39d8c71a36c81f78e2d6dbd90a2d32e6541790f0d28f6dd4baf3ea7b45f","bytes":2670,"lines":37},{"path":"docs/WIREFRAMES.md","sha256":"b03aa6419aeaf5d42a5698c4d43a434c1633b7ac6fd0b0bd0cda979077adc56e","bytes":2005,"lines":77},{"path":"docs/adr/0001-orgmetra-authoritative-hris-record.md","sha256":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"path":"docs/adr/0008-purpose-bound-pii-authorization.md","sha256":"c5157d3bc58f3d8d29e03104dd15eb2911cc1bb66e2c92a935b26d7164648dc7","bytes":5988,"lines":55},{"path":"docs/adr/0009-performance-criterion-observation-scope.md","sha256":"1ac10bb2747b0a5b4d62f627825cfd7f978f3fa88d7575bffc23d56371240a64","bytes":7057,"lines":57},{"path":"docs/adr/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"1656ef8b57c836ef7936a8e9cb6a824681eb7563157a1ab0a29deb25849a457b","bytes":5568,"lines":53},{"path":"docs/adr/0012-governed-migration-handoff.md","sha256":"713855d670001d3964ecb36cc653830502fb1d82a58b9e39f564b6992dd2bd80","bytes":5965,"lines":59},{"path":"docs/adr/0013-governed-requisition-review-packet.md","sha256":"70bf2cbdf903a8793d6d8bc116a08331931090118341f42010236e09c6cc1802","bytes":4693,"lines":46},{"path":"docs/adr/0014-job-analysis-snapshot-persistence.md","sha256":"a7ab6fee50aaa63f7f407516a4cb39885faeb0fc6e5035ee8fc352ed73430105","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"lines":69},{"path":"docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md","sha256":"b64f21abb19373e780db8b9e64deb8ba9a6219ccf9625a651f25407b8691fcbd","bytes":8227,"lines":226},{"path":"docs/superpowers/specs/2026-08-15-orgmetra-foundation-design.md","sha256":"4a0e1a7943e40d12bd3082db3757045b4085e5a089fea7bc0d8a1565ffcbcf1d","bytes":6237,"lines":187},{"path":"package.json","sha256":"59ae9e3e67c3fba9320cb18439692395cdfd16ae5c24e3c4cf30d77d63ebabb5","bytes":388,"lines":9},{"path":"packages/hris-kernel/src/orgmetra_hris_kernel/audit.py","sha256":"3e5b7190cf857dc8c1fc7e898cef303060f34aabee6c27a9034d4d9650e33190","bytes":7707,"lines":160},{"path":"packages/hris-kernel/tests/test_audit_outbox.py","sha256":"5928dd7b97fe38d6b7472ce62966437e339058a59c3b301a93a7b5c05432b40c","bytes":7556,"lines":200},{"path":"schemas/openapi.yaml","sha256":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"path":"scripts/foundation-contract.mjs","sha256":"5242dcdbe0935775edf074462c82600e9bc4927d9fdc50c47727af915fd4b23a","bytes":218,"lines":6},{"path":"tests/dispatcher-inventory.test.mjs","sha256":"09f5e64410e6b7a26bf8d6ce61c50b737da2ea85d955f91eba63aa21f1537261","bytes":1597,"lines":34},{"path":"tests/foundation-contract.test.mjs","sha256":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"path":"tests/openapi-contract.test.mjs","sha256":"80c1610ef1c189fa325e55389501e0e51531ddf61ee335bb94d9cb3aa55a9fdc","bytes":6438,"lines":195},{"path":"tests/test_audit_outbox_hardening_postgres.sh","sha256":"518ba2f37ba6292943e5abe22c2599452b2f031a42e453b2493aedf8714421a0","bytes":13396,"lines":333},{"path":"tests/test_audit_outbox_postgres.sh","sha256":"e57a04920a0ba97fa6a06752d15ea150016ab8d44099e998c5c4f4067592b4d2","bytes":13443,"lines":357},{"path":"tests/test_bitemporal_postgres.sh","sha256":"7684b8c2ff52c044c081135515bd5aabbfd00e2daad0d471b0868701af2df6cc","bytes":8209,"lines":230},{"path":"tests/test_candidate_worker_conversion_postgres.sh","sha256":"681cb74d6cfa859ed92c6c2439881ea20c430ef8df94ec662e2807761a377f90","bytes":14673,"lines":344},{"path":"tests/test_criterion_observation_scope_postgres.sh","sha256":"0ee9539ee57f840c27d08009f7868cdc8662669df78a01dbc8be39216b8f1a3d","bytes":17811,"lines":469},{"path":"tests/test_evidence_sealing_postgres.sh","sha256":"57d16b632a0c60ffdcb4842ceb1cfe25d19c54cefeeefb622ff4fa6e83441ad7","bytes":11349,"lines":370},{"path":"tests/test_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"path":"tests/test_operational_uuid_postgres.sh","sha256":"7378f98f0d4b3000e8ea641d8701f1540dbad71410b3637d81d799969e0f6ff7","bytes":3346,"lines":101},{"path":"tests/test_outbox_claim_postgres.sh","sha256":"1027806d436ebfe34e108c25b6a4001f43b9550f1d70057c6c0d7974323b0c9b","bytes":14817,"lines":429},{"path":"tests/test_outbox_dead_letter_postgres.sh","sha256":"0d728d578e64252e6079f2d141ddaa7fa9cfbf9784e625832273596d69a6e13d","bytes":14008,"lines":377},{"path":"tests/test_people_mutation_idempotency_postgres.sh","sha256":"3f57e12f80bd1b034c9aac54b669d8530106e3e26b3795689671fb53807b3cd5","bytes":16191,"lines":381},{"path":"tests/test_tenant_isolation_postgres.sh","sha256":"dd649435ef8ab9e57f0609c101917e36656a6d40d63de9bcdbdac23d764f6c3a","bytes":15134,"lines":388},{"path":"tests/test_validity_study_case_postgres.sh","sha256":"0070ad58300323c7f9900c5645e0df3106b36ccd245ae686e982c2fd6fa4dc02","bytes":14708,"lines":301},{"path":"tests/validate_repository.py","sha256":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} +{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"321d62704dd6d5e04bfe2c75b893d6a5ba52fa104928f97edf7fe7ca4a513550","bytes":17917,"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/0019_job_qualification_rule_persistence.sql","sha256":"66c941c8dfa4229d63aa05ebe111f8654bf6344f10400840f0ca3635fe60cf85","bytes":16444,"lines":365},{"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":"13a078159a8523cd11157747b8b563b2860077028141de7ead4406f51cfa318e","bytes":28305,"lines":691},{"path":"scripts/foundation-contract.mjs","sha256":"5242dcdbe0935775edf074462c82600e9bc4927d9fdc50c47727af915fd4b23a","bytes":218,"lines":6},{"path":"tests/dispatcher-inventory.test.mjs","sha256":"09f5e64410e6b7a26bf8d6ce61c50b737da2ea85d955f91eba63aa21f1537261","bytes":1597,"lines":34},{"path":"tests/foundation-contract.test.mjs","sha256":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"path":"tests/openapi-contract.test.mjs","sha256":"80c1610ef1c189fa325e55389501e0e51531ddf61ee335bb94d9cb3aa55a9fdc","bytes":6438,"lines":195},{"path":"tests/test_audit_outbox_hardening_postgres.sh","sha256":"518ba2f37ba6292943e5abe22c2599452b2f031a42e453b2493aedf8714421a0","bytes":13396,"lines":333},{"path":"tests/test_audit_outbox_postgres.sh","sha256":"e57a04920a0ba97fa6a06752d15ea150016ab8d44099e998c5c4f4067592b4d2","bytes":13443,"lines":357},{"path":"tests/test_bitemporal_postgres.sh","sha256":"7684b8c2ff52c044c081135515bd5aabbfd00e2daad0d471b0868701af2df6cc","bytes":8209,"lines":230},{"path":"tests/test_candidate_worker_conversion_postgres.sh","sha256":"681cb74d6cfa859ed92c6c2439881ea20c430ef8df94ec662e2807761a377f90","bytes":14673,"lines":344},{"path":"tests/test_criterion_observation_scope_postgres.sh","sha256":"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_job_qualification_rule_persistence_postgres.sh","sha256":"5807c6c197dd283aaa7b2779eeb01ce62408e9b9f77f4954050973c5f1ed433a","bytes":13155,"lines":284},{"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":"e7492a10b9a204919a834148ebb90c515cfc17cadd9e343e58c058094b99a2ee","bytes":27427,"lines":640}]} diff --git a/scripts/foundation-contract-core.mjs b/scripts/foundation-contract-core.mjs index 1e9fb267c..786325419 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/0019_job_qualification_rule_persistence.sql', 'packages/hris-kernel/src/orgmetra_hris_kernel/audit.py', 'packages/hris-kernel/tests/test_audit_outbox.py', 'schemas/openapi.yaml', @@ -89,6 +90,7 @@ 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_job_qualification_rule_persistence_postgres.sh', 'tests/validate_repository.py' ]); diff --git a/tests/test_job_qualification_rule_persistence_postgres.sh b/tests/test_job_qualification_rule_persistence_postgres.sh index ff4cb2f5f..e5a90ebee 100755 --- a/tests/test_job_qualification_rule_persistence_postgres.sh +++ b/tests/test_job_qualification_rule_persistence_postgres.sh @@ -7,11 +7,40 @@ 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/0004_outbox_delivery_claim.sql \ + database/migrations/0005_outbox_delivery_finalization.sql \ + database/migrations/0006_outbox_delivery_dead_letter.sql \ + database/migrations/0007_outbox_retry_exhaustion.sql \ + database/migrations/0008_audit_outbox_review_hardening.sql \ + database/migrations/0009_candidate_worker_conversion_governance.sql \ + database/migrations/0010_validity_study_case_integrity.sql \ + database/migrations/0011_criterion_observation_scope.sql \ + database/migrations/0012_people_mutation_idempotency.sql \ database/migrations/0013_job_analysis_snapshot.sql \ database/migrations/0019_job_qualification_rule_persistence.sql; do psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f "${migration}" done +guarded_function_count="$(psql "${DATABASE_URL}" -Atqc " +SELECT count(*) +FROM pg_catalog.pg_proc AS procedure_record +JOIN pg_catalog.pg_namespace AS procedure_schema + ON procedure_schema.oid = procedure_record.pronamespace +WHERE procedure_schema.nspname = 'public' + AND procedure_record.proname IN ( + 'enforce_job_qualification_rule_system_time', + 'protect_job_qualification_rule_history', + 'enforce_job_qualification_rule_scope', + 'enforce_job_qualification_rule_anchor_alignment', + 'reject_job_qualification_rule_truncate' + ) + AND procedure_record.proconfig @> ARRAY['search_path=pg_catalog, public, pg_temp']; +")" +if [[ "${guarded_function_count}" != "5" ]]; then + echo "qualification-rule trigger functions did not pin their 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" JOB_ID="00000000-0000-7000-8000-000000000021" diff --git a/tests/validate_repository.py b/tests/validate_repository.py index fe0a329ff..635262d12 100644 --- a/tests/validate_repository.py +++ b/tests/validate_repository.py @@ -71,6 +71,7 @@ "database/migrations/0011_criterion_observation_scope.sql", "database/migrations/0012_people_mutation_idempotency.sql", "database/migrations/0013_job_analysis_snapshot.sql", + "database/migrations/0019_job_qualification_rule_persistence.sql", "packages/hris-kernel/src/orgmetra_hris_kernel/audit.py", "packages/hris-kernel/tests/test_audit_outbox.py", "schemas/openapi.yaml", @@ -92,6 +93,7 @@ "tests/test_criterion_observation_scope_postgres.sh", "tests/test_people_mutation_idempotency_postgres.sh", "tests/test_job_analysis_snapshot_postgres.sh", + "tests/test_job_qualification_rule_persistence_postgres.sh", "tests/validate_repository.py", ]