From fc422cf507053aa6eb010f9f0e623caef06dbdda Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 23:17:42 -0700 Subject: [PATCH 01/13] test(document-records): define durable metadata persistence contract --- ...st_document_record_persistence_postgres.sh | 258 ++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 tests/test_document_record_persistence_postgres.sh diff --git a/tests/test_document_record_persistence_postgres.sh b/tests/test_document_record_persistence_postgres.sh new file mode 100644 index 000000000..a2235556e --- /dev/null +++ b/tests/test_document_record_persistence_postgres.sh @@ -0,0 +1,258 @@ +#!/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/0021_document_record_persistence.sql; do + if [[ ! -f "${migration}" ]]; then + echo "required document-record 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" +PERSON_ID="00000000-0000-7000-8000-000000000011" +EMPLOYMENT_ID="00000000-0000-7000-8000-000000000021" +DOCUMENT_ID="00000000-0000-7000-8000-000000000031" +DOCUMENT_REFERENCE="document_record:00000000-0000-4000-8000-000000000031" +ARTIFACT_REFERENCE="document_artifact:00000000-0000-4000-8000-000000000041" +RETENTION_REFERENCE="retention_policy:00000000-0000-4000-8000-000000000051" +UPLOADER="actor:00000000-0000-4000-8000-000000000061" +PERSISTED_BY="actor:00000000-0000-4000-8000-000000000062" +AUDIT_ID="00000000-0000-4000-8000-000000000071" +OUTBOX_ID="00000000-0000-4000-8000-000000000072" +WRONG_AUDIT_ID="00000000-0000-4000-8000-000000000073" +WRONG_OUTBOX_ID="00000000-0000-4000-8000-000000000074" +ARTIFACT_DIGEST="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +SOURCE_DIGEST="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +RETENTION_DIGEST="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" +EVIDENCE_DIGEST="dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + +with_tenant() { + local tenant="$1" + shift + 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 <&2 + exit 1 +fi + +expect_failure \ + "document record accepted caller-backdated system time" \ + "transaction timestamp" \ + "INSERT INTO document_record (${columns}, recorded_at) VALUES ( + '${TENANT_ID}', '00000000-0000-7000-8000-000000000032', + 'document_record:00000000-0000-4000-8000-000000000032', + '${PERSON_ID}', '${EMPLOYMENT_ID}', '${UPLOADER}', '${PERSISTED_BY}', + 'policy_acknowledgement', + 'document_artifact:00000000-0000-4000-8000-000000000042', + '${ARTIFACT_DIGEST}', '${SOURCE_DIGEST}', '${RETENTION_REFERENCE}', + '${RETENTION_DIGEST}', TIMESTAMPTZ '2026-08-24 05:55:00+00', + '${EVIDENCE_DIGEST}', '${canonical_digest}', '${AUDIT_ID}', + TIMESTAMPTZ '2000-01-01 00:00:00+00' + );" + +expect_failure \ + "document record accepted future received_at" \ + "received_at cannot be later" \ + "INSERT INTO document_record (${columns}) VALUES ( + '${TENANT_ID}', '00000000-0000-7000-8000-000000000033', + 'document_record:00000000-0000-4000-8000-000000000033', + '${PERSON_ID}', '${EMPLOYMENT_ID}', '${UPLOADER}', '${PERSISTED_BY}', + 'qualification_document', + 'document_artifact:00000000-0000-4000-8000-000000000043', + '${ARTIFACT_DIGEST}', '${SOURCE_DIGEST}', '${RETENTION_REFERENCE}', + '${RETENTION_DIGEST}', pg_catalog.transaction_timestamp() + interval '1 hour', + '${EVIDENCE_DIGEST}', '${canonical_digest}', '${AUDIT_ID}' + );" + +wrong_event="$(python3 - <&2 + exit 1 + fi +done + +psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 <<'SQL' +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'orgmetra_document_reader') THEN + CREATE ROLE orgmetra_document_reader LOGIN PASSWORD 'orgmetra_document_reader' NOSUPERUSER NOBYPASSRLS; + END IF; +END +$$; +GRANT CONNECT ON DATABASE orgmetra TO orgmetra_document_reader; +GRANT USAGE ON SCHEMA public TO orgmetra_document_reader; +GRANT SELECT ON document_record TO orgmetra_document_reader; +SQL + +alpha_count="$(PGPASSWORD=orgmetra_document_reader PGOPTIONS="-c orgmetra.tenant_record_id=${TENANT_ID}" \ + psql -h localhost -U orgmetra_document_reader -d orgmetra -Atqc 'SELECT count(*) FROM document_record;')" +beta_count="$(PGPASSWORD=orgmetra_document_reader PGOPTIONS="-c orgmetra.tenant_record_id=${OTHER_TENANT_ID}" \ + psql -h localhost -U orgmetra_document_reader -d orgmetra -Atqc 'SELECT count(*) FROM document_record;')" +missing_count="$(PGPASSWORD=orgmetra_document_reader \ + psql -h localhost -U orgmetra_document_reader -d orgmetra -Atqc 'SELECT count(*) FROM document_record;')" +if [[ "${alpha_count}" != "1" || "${beta_count}" != "0" || "${missing_count}" != "0" ]]; then + echo "document-record RLS isolation failed: alpha=${alpha_count} beta=${beta_count} missing=${missing_count}" >&2 + exit 1 +fi + +echo "document-record persistence contract passed" From e3eb7d80e04ca77a47165210291ba4a911af6b0a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 23:17:54 -0700 Subject: [PATCH 02/13] test(document-records): run durable persistence contract --- .../document-record-persistence-quality.yml | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 .github/workflows/document-record-persistence-quality.yml diff --git a/.github/workflows/document-record-persistence-quality.yml b/.github/workflows/document-record-persistence-quality.yml new file mode 100644 index 000000000..70aa28eef --- /dev/null +++ b/.github/workflows/document-record-persistence-quality.yml @@ -0,0 +1,88 @@ +name: Document Record Persistence Quality + +on: + pull_request: + branches: + - develop + - feat/document-record-evidence + paths: + - "database/migrations/0021_document_record_persistence.sql" + - "tests/test_document_record_persistence_postgres.sh" + - "docs/adr/0107-document-record-persistence.md" + - "docs/traceability/document-record-persistence.md" + - "docs/doctoring/document-record-persistence-references.md" + - ".github/workflows/document-record-persistence-quality.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: document-record-persistence-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + postgres_contract: + name: Governed document-record 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_document_record_persistence_postgres.sh", + ".github/workflows/document-record-persistence-quality.yml", + ] + optional = [ + "database/migrations/0021_document_record_persistence.sql", + "docs/adr/0107-document-record-persistence.md", + "docs/traceability/document-record-persistence.md", + "docs/doctoring/document-record-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 document-record persistence regressions + run: bash tests/test_document_record_persistence_postgres.sh + - name: Require clean checkout + run: | + git diff --exit-code + test -z "$(git status --porcelain)" From f29459aceb5d4fdeb32c01fc5489ef28b0ff4c8f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 23:19:46 -0700 Subject: [PATCH 03/13] test(document-records): keep foreign HR and audit ownership opaque --- ...st_document_record_persistence_postgres.sh | 164 ++++++++---------- 1 file changed, 70 insertions(+), 94 deletions(-) diff --git a/tests/test_document_record_persistence_postgres.sh b/tests/test_document_record_persistence_postgres.sh index a2235556e..a37beb182 100644 --- a/tests/test_document_record_persistence_postgres.sh +++ b/tests/test_document_record_persistence_postgres.sh @@ -6,7 +6,6 @@ set -euo pipefail 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/0021_document_record_persistence.sql; do if [[ ! -f "${migration}" ]]; then echo "required document-record persistence migration is missing: ${migration}" >&2 @@ -17,22 +16,21 @@ done TENANT_ID="10000000-0000-7000-8000-000000000001" OTHER_TENANT_ID="20000000-0000-7000-8000-000000000002" -PERSON_ID="00000000-0000-7000-8000-000000000011" -EMPLOYMENT_ID="00000000-0000-7000-8000-000000000021" DOCUMENT_ID="00000000-0000-7000-8000-000000000031" DOCUMENT_REFERENCE="document_record:00000000-0000-4000-8000-000000000031" +PERSON_REFERENCE="person_record:00000000-0000-4000-8000-000000000011" +EMPLOYMENT_REFERENCE="employment_record:00000000-0000-4000-8000-000000000021" ARTIFACT_REFERENCE="document_artifact:00000000-0000-4000-8000-000000000041" RETENTION_REFERENCE="retention_policy:00000000-0000-4000-8000-000000000051" UPLOADER="actor:00000000-0000-4000-8000-000000000061" PERSISTED_BY="actor:00000000-0000-4000-8000-000000000062" -AUDIT_ID="00000000-0000-4000-8000-000000000071" -OUTBOX_ID="00000000-0000-4000-8000-000000000072" -WRONG_AUDIT_ID="00000000-0000-4000-8000-000000000073" -WRONG_OUTBOX_ID="00000000-0000-4000-8000-000000000074" +AUDIT_REFERENCE="audit_event:00000000-0000-4000-8000-000000000071" +OUTBOX_REFERENCE="outbox_event:00000000-0000-4000-8000-000000000072" ARTIFACT_DIGEST="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" SOURCE_DIGEST="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" RETENTION_DIGEST="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" EVIDENCE_DIGEST="dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" +APPLICATION_DIGEST="eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" with_tenant() { local tenant="$1" @@ -58,64 +56,26 @@ expect_failure() { with_tenant "${TENANT_ID}" "${DATABASE_URL}" -v ON_ERROR_STOP=1 <&2 + exit 1 +fi + +rls_state="$(psql "${DATABASE_URL}" -Atqc " +SELECT relrowsecurity::text || '|' || relforcerowsecurity::text +FROM pg_class WHERE oid = 'document_record'::regclass;")" +if [[ "${rls_state}" != "true|true" ]]; then + echo "document-record RLS is not enabled and forced: ${rls_state}" >&2 + exit 1 +fi + psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 <<'SQL' DO $$ BEGIN From fa1cbd02299f6357b038291867ae368297583146 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 23:20:13 -0700 Subject: [PATCH 04/13] feat(document-records): persist immutable governed metadata --- .../0021_document_record_persistence.sql | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 database/migrations/0021_document_record_persistence.sql diff --git a/database/migrations/0021_document_record_persistence.sql b/database/migrations/0021_document_record_persistence.sql new file mode 100644 index 000000000..b2bfc4079 --- /dev/null +++ b/database/migrations/0021_document_record_persistence.sql @@ -0,0 +1,198 @@ +-- Persist value-minimized HR document metadata inside the document_records owner +-- boundary. Cross-service Person/Employment/audit/outbox identities remain +-- opaque published-contract references rather than direct application-table SQL. + +CREATE TABLE document_record ( + tenant_record_id uuid NOT NULL REFERENCES tenant_record(tenant_record_id), + document_record_id uuid PRIMARY KEY, + document_record_reference text NOT NULL, + person_record_reference text NOT NULL, + employment_record_reference text NOT NULL, + uploader_actor_reference text NOT NULL, + persisted_by_actor_reference text NOT NULL, + document_category_code text NOT NULL, + artifact_reference text NOT NULL, + artifact_digest_sha256 text NOT NULL, + source_provenance_digest_sha256 text NOT NULL, + retention_policy_reference text NOT NULL, + retention_policy_digest_sha256 text NOT NULL, + received_at timestamptz NOT NULL, + evidence_digest_sha256 text NOT NULL, + audit_event_reference text NOT NULL, + outbox_event_reference text NOT NULL, + application_evidence_digest_sha256 text NOT NULL, + application_purpose_code text NOT NULL DEFAULT 'document_record_persist', + application_reason_code text NOT NULL DEFAULT 'reviewed_document_metadata', + classification_code text NOT NULL DEFAULT 'restricted_hr', + content_storage_state text NOT NULL DEFAULT 'artifact_reference_only', + decision_authority_state text NOT NULL DEFAULT 'not_authorized_for_employment_decision', + recorded_at timestamptz NOT NULL DEFAULT pg_catalog.transaction_timestamp(), + + CONSTRAINT document_record_id_operational_check + CHECK (public.is_operational_uuid(document_record_id)), + CONSTRAINT document_record_reference_check + CHECK ( + document_record_reference ~ + '^document_record:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + ), + CONSTRAINT document_person_record_reference_check + CHECK ( + person_record_reference ~ + '^person_record:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + ), + CONSTRAINT document_employment_record_reference_check + CHECK ( + employment_record_reference ~ + '^employment_record:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + ), + CONSTRAINT document_uploader_actor_reference_check + CHECK ( + uploader_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 document_persisted_actor_reference_check + CHECK ( + persisted_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 document_category_code_check + CHECK ( + document_category_code IN ( + 'employment_contract', + 'policy_acknowledgement', + 'qualification_document' + ) + ), + CONSTRAINT document_artifact_reference_check + CHECK ( + artifact_reference ~ + '^document_artifact:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + ), + CONSTRAINT document_artifact_digest_check + CHECK (artifact_digest_sha256 ~ '^[0-9a-f]{64}$'), + CONSTRAINT document_source_provenance_digest_check + CHECK (source_provenance_digest_sha256 ~ '^[0-9a-f]{64}$'), + CONSTRAINT document_retention_policy_reference_check + CHECK ( + retention_policy_reference ~ + '^retention_policy:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + ), + CONSTRAINT document_retention_policy_digest_check + CHECK (retention_policy_digest_sha256 ~ '^[0-9a-f]{64}$'), + CONSTRAINT document_evidence_digest_check + CHECK (evidence_digest_sha256 ~ '^[0-9a-f]{64}$'), + CONSTRAINT document_audit_event_reference_check + CHECK ( + audit_event_reference ~ + '^audit_event:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + ), + CONSTRAINT document_outbox_event_reference_check + CHECK ( + outbox_event_reference ~ + '^outbox_event:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + ), + CONSTRAINT document_application_evidence_digest_check + CHECK (application_evidence_digest_sha256 ~ '^[0-9a-f]{64}$'), + CONSTRAINT document_application_purpose_code_check + CHECK (application_purpose_code = 'document_record_persist'), + CONSTRAINT document_application_reason_code_check + CHECK (application_reason_code = 'reviewed_document_metadata'), + CONSTRAINT document_classification_code_check + CHECK (classification_code = 'restricted_hr'), + CONSTRAINT document_content_storage_state_check + CHECK (content_storage_state = 'artifact_reference_only'), + CONSTRAINT document_decision_authority_state_check + CHECK (decision_authority_state = 'not_authorized_for_employment_decision'), + CONSTRAINT document_record_tenant_reference_unique + UNIQUE (tenant_record_id, document_record_reference), + CONSTRAINT document_record_tenant_artifact_unique + UNIQUE (tenant_record_id, artifact_reference), + CONSTRAINT document_record_tenant_audit_reference_unique + UNIQUE (tenant_record_id, audit_event_reference), + CONSTRAINT document_record_tenant_outbox_reference_unique + UNIQUE (tenant_record_id, outbox_event_reference) +); + +COMMENT ON TABLE document_record IS + 'Immutable, value-minimized HR document metadata owned by document_records. Person, Employment, audit, and outbox identities are opaque contract references; document bytes and employment-decision authority are not stored here.'; + +CREATE FUNCTION enforce_document_record_system_time() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + IF NEW.recorded_at IS DISTINCT FROM pg_catalog.transaction_timestamp() THEN + RAISE EXCEPTION 'document-record recorded_at must equal the current transaction timestamp' + USING ERRCODE = '22023'; + END IF; + IF NEW.received_at > NEW.recorded_at THEN + RAISE EXCEPTION 'document-record received_at cannot be later than recorded_at' + USING ERRCODE = '22023'; + END IF; + RETURN NEW; +END; +$$; + +COMMENT ON FUNCTION enforce_document_record_system_time() IS + 'Requires PostgreSQL-owned system-recorded time and rejects document receipt time later than the durable recording instant.'; + +CREATE TRIGGER document_record_system_time_guard +BEFORE INSERT ON document_record +FOR EACH ROW +EXECUTE FUNCTION enforce_document_record_system_time(); + +CREATE FUNCTION protect_document_record_immutability() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + RAISE EXCEPTION 'document metadata is immutable; lifecycle changes require a separate governed relation' + USING ERRCODE = '55000'; +END; +$$; + +COMMENT ON FUNCTION protect_document_record_immutability() IS + 'Rejects UPDATE and DELETE so the artifact/provenance metadata snapshot cannot be rewritten after issuance.'; + +CREATE TRIGGER document_record_immutability_guard +BEFORE UPDATE OR DELETE ON document_record +FOR EACH ROW +EXECUTE FUNCTION protect_document_record_immutability(); + +CREATE FUNCTION reject_document_record_truncate() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + RAISE EXCEPTION 'document-record history cannot be truncated' + USING ERRCODE = '55000'; +END; +$$; + +COMMENT ON FUNCTION reject_document_record_truncate() IS + 'Rejects table-wide TRUNCATE so immutable document metadata cannot bypass row-level controls.'; + +CREATE TRIGGER document_record_truncate_guard +BEFORE TRUNCATE ON document_record +FOR EACH STATEMENT +EXECUTE FUNCTION reject_document_record_truncate(); + +REVOKE TRUNCATE ON document_record FROM PUBLIC; + +ALTER TABLE document_record ENABLE ROW LEVEL SECURITY; +ALTER TABLE document_record FORCE ROW LEVEL SECURITY; + +CREATE POLICY document_record_tenant_isolation_policy +ON document_record +USING ( + tenant_record_id = NULLIF( + pg_catalog.current_setting('orgmetra.tenant_record_id', true), + '' + )::uuid +) +WITH CHECK ( + tenant_record_id = NULLIF( + pg_catalog.current_setting('orgmetra.tenant_record_id', true), + '' + )::uuid +); From 6f5903156fda108adc54bf0bfcc3cb3a8bfff3af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 23:20:42 -0700 Subject: [PATCH 05/13] docs(document-records): record persistence ownership decision --- docs/adr/0107-document-record-persistence.md | 33 ++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 docs/adr/0107-document-record-persistence.md diff --git a/docs/adr/0107-document-record-persistence.md b/docs/adr/0107-document-record-persistence.md new file mode 100644 index 000000000..73f07ea14 --- /dev/null +++ b/docs/adr/0107-document-record-persistence.md @@ -0,0 +1,33 @@ +# ADR 0107: Immutable document-record metadata persistence + +## Status + +Active PR architecture. This decision does not describe protected-`develop` truth until the stacked change is integrated. + +## Context + +Orgmetra architecture assigns `document_records` ownership of HR document metadata and immutable artifact references. PR #98 adds a value-minimized `DocumentRecordEvidence` value boundary but intentionally leaves durable persistence out of scope. The persistence layer must keep document content and unrelated HR values out of the database relation, retain tenant isolation, distinguish business receipt time from system-recorded time, and remain extractable as its own service. + +`people_core`, `audit_provenance`, and `integration_hub` are separate bounded contexts. Therefore the document-record relation must not query or foreign-key their application tables merely because the initial modular deployment can share one PostgreSQL cluster. Cross-context identities remain opaque published-contract references. + +## Decision + +Add one immutable `document_record` relation owned by the document-records boundary. It stores: + +- one opaque tenant-local document correlation; +- opaque Person and Employment references rather than cross-service table identifiers; +- reviewed document category, uploader/persisting actor correlations, and immutable artifact reference; +- SHA-256 artifact, source-provenance, retention-policy, evidence, and application-evidence digests; +- opaque audit/outbox handoff references from owner contracts; +- business `received_at` and PostgreSQL-owned `recorded_at`; +- fixed `restricted_hr`, `artifact_reference_only`, and `not_authorized_for_employment_decision` states. + +The relation stores no document bytes/title, free-form HR text, compensation, rating, credentials, or employment-decision output. UPDATE, DELETE, and TRUNCATE are rejected. Lifecycle disposition belongs to a separate governed relation rather than rewriting the immutable metadata snapshot. + +Tenant isolation uses enabled and forced PostgreSQL row-level security. A missing tenant context yields no visible rows. The design deliberately keeps Person/Employment/audit/outbox as opaque references so later service extraction does not require changing the persistence contract. + +## Consequences + +This is an evidence/metadata system of record, not object storage and not authorization to read, export, delete, or use the document in an employment decision. The host must resolve current authorization and foreign references through published owner contracts before persistence or retrieval. Audit/outbox references are correlations to owner-controlled immutable evidence; this relation does not directly query those foreign application tables. + +Migration number `0021` is reserved in this stacked branch only. After parent #98 integrates, this PR must be retargeted to fresh `develop` and migration ordering reconciled before review readiness. From f42af766313231f71d0f2e60ad78cbe062357c32 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 23:20:57 -0700 Subject: [PATCH 06/13] docs(document-records): trace durable metadata controls --- .../document-record-persistence.md | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/traceability/document-record-persistence.md diff --git a/docs/traceability/document-record-persistence.md b/docs/traceability/document-record-persistence.md new file mode 100644 index 000000000..7d3d4bb4c --- /dev/null +++ b/docs/traceability/document-record-persistence.md @@ -0,0 +1,26 @@ +# Document-record persistence traceability + +## Truth status + +- Protected-main truth: `develop@9e3e4847510e1e612b48474ba42b177b8ed824df` has no `document_record` persistence relation. +- Dependency-active truth: PR #98 defines `DocumentRecordEvidence` and remains a separate dependency root. +- Active-PR truth: this stacked PR adds durable document metadata persistence only after #98's value boundary. +- Out of scope: document bytes/object storage, content viewing, export authorization, legal retention/disposition execution, employment decisions, and direct reads of People/audit/outbox application tables. + +## Requirements → executable evidence + +| Requirement | Implementation boundary | Regression evidence | +|---|---|---| +| Value minimization | `document_record` has metadata/references/digests only | test rejects prohibited value-bearing columns | +| Person/Employment service extraction | opaque `person_record:` / `employment_record:` references | test rejects non-opaque Person reference and asserts no foreign FK to People tables | +| Audit/outbox service extraction | opaque `audit_event:` / `outbox_event:` references + application digest | test asserts no FK to audit/outbox application tables | +| Reviewed vocabulary | closed document category and fixed persistence purpose/reason | happy path + wrong-reason failure | +| Business/system time | caller `received_at`; PostgreSQL `transaction_timestamp()` `recorded_at` | future receipt and backdated system-time failures | +| Immutable metadata | UPDATE/DELETE/TRUNCATE guards | three destructive-operation failures | +| Tenant isolation | ENABLE + FORCE RLS with transaction tenant context | NOSUPERUSER/NOBYPASSRLS reader sees only its tenant; no context sees zero rows | +| Non-decision posture | fixed classification/storage/decision-authority states | persisted-state assertion | +| Exact candidate provenance | pinned PostgreSQL workflow and exact-head checkout | `Document Record Persistence Quality` | + +## Integration rule + +This relation never authorizes foreign-resource use. Before persistence or retrieval, the host resolves tenant/purpose authorization and foreign reference truth through the owning service's published package/API/event contract. The initial shared PostgreSQL cluster is not permission for direct cross-service application-table SQL. From ac55b4d2eca1c758e530b840e0e1cfe443ded8d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 23:21:06 -0700 Subject: [PATCH 07/13] docs(document-records): record primary persistence references --- .../document-record-persistence-references.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 docs/doctoring/document-record-persistence-references.md diff --git a/docs/doctoring/document-record-persistence-references.md b/docs/doctoring/document-record-persistence-references.md new file mode 100644 index 000000000..7cc09f66a --- /dev/null +++ b/docs/doctoring/document-record-persistence-references.md @@ -0,0 +1,19 @@ +# Document-record persistence references + +## Status + +Primary-source design references reviewed for the active document-record persistence PR on 2026-08-24. These sources inform the design; Orgmetra does not claim standards certification or conformance from their citation. + +## 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: ALTER TABLE*. https://www.postgresql.org/docs/16/sql-altertable.html + +World Wide Web Consortium. (2013, April 30). *PROV-O: The PROV ontology* (W3C Recommendation). https://www.w3.org/TR/prov-o/ + +## Design use + +PostgreSQL `CREATE POLICY` documents that row visibility and new-row checks are controlled by `USING` and `WITH CHECK` once row-level security is enabled; false or null policy results do not expose rows. `ALTER TABLE ... FORCE ROW LEVEL SECURITY` additionally applies row policies to the table owner, so this PR uses both ENABLE and FORCE and still tests a NOSUPERUSER/NOBYPASSRLS reader. + +PROV-O is used only as a provenance design reference: the persisted relation carries source-provenance and evidence correlations without copying source document content. The repository's own service-ownership contract remains authoritative for direct-database-access boundaries. From 59f2d8247c631da0f35fb8bf0a4b806c5ee8925b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 23:23:47 -0700 Subject: [PATCH 08/13] test(document-records): bind persisted metadata to exact evidence bytes --- ...st_document_record_persistence_postgres.sh | 99 +++++++++++++++++-- 1 file changed, 90 insertions(+), 9 deletions(-) diff --git a/tests/test_document_record_persistence_postgres.sh b/tests/test_document_record_persistence_postgres.sh index a37beb182..8377282ed 100644 --- a/tests/test_document_record_persistence_postgres.sh +++ b/tests/test_document_record_persistence_postgres.sh @@ -29,8 +29,40 @@ OUTBOX_REFERENCE="outbox_event:00000000-0000-4000-8000-000000000072" ARTIFACT_DIGEST="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" SOURCE_DIGEST="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" RETENTION_DIGEST="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" -EVIDENCE_DIGEST="dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" APPLICATION_DIGEST="eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +RECEIVED_AT="2026-08-24T05:55:00Z" +EVIDENCE_RECORDED_AT="2026-08-24T05:58:00Z" + +canonical_evidence="$(python3 - <&1 <&2 + exit 1 +fi + expect_failure \ "document metadata was rewriteable" \ "immutable" \ From 4a5f76f067fc505420f729a35f2b4c8e68fcc116 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 23:25:33 -0700 Subject: [PATCH 09/13] fix(document-records): bind persisted metadata to canonical evidence --- .../0021_document_record_persistence.sql | 134 +++++++++++++++++- 1 file changed, 133 insertions(+), 1 deletion(-) diff --git a/database/migrations/0021_document_record_persistence.sql b/database/migrations/0021_document_record_persistence.sql index b2bfc4079..e32266080 100644 --- a/database/migrations/0021_document_record_persistence.sql +++ b/database/migrations/0021_document_record_persistence.sql @@ -17,6 +17,7 @@ CREATE TABLE document_record ( retention_policy_reference text NOT NULL, retention_policy_digest_sha256 text NOT NULL, received_at timestamptz NOT NULL, + canonical_evidence_json text NOT NULL, evidence_digest_sha256 text NOT NULL, audit_event_reference text NOT NULL, outbox_event_reference text NOT NULL, @@ -79,6 +80,11 @@ CREATE TABLE document_record ( ), CONSTRAINT document_retention_policy_digest_check CHECK (retention_policy_digest_sha256 ~ '^[0-9a-f]{64}$'), + CONSTRAINT document_canonical_evidence_size_check + CHECK ( + octet_length(canonical_evidence_json) > 0 + AND octet_length(canonical_evidence_json) <= 4096 + ), CONSTRAINT document_evidence_digest_check CHECK (evidence_digest_sha256 ~ '^[0-9a-f]{64}$'), CONSTRAINT document_audit_event_reference_check @@ -114,7 +120,7 @@ CREATE TABLE document_record ( ); COMMENT ON TABLE document_record IS - 'Immutable, value-minimized HR document metadata owned by document_records. Person, Employment, audit, and outbox identities are opaque contract references; document bytes and employment-decision authority are not stored here.'; + 'Immutable, value-minimized HR document metadata owned by document_records. The exact canonical evidence snapshot is digest-bound to the typed row; Person, Employment, audit, and outbox identities are opaque contract references. Document bytes and employment-decision authority are not stored here.'; CREATE FUNCTION enforce_document_record_system_time() RETURNS trigger @@ -141,6 +147,132 @@ BEFORE INSERT ON document_record FOR EACH ROW EXECUTE FUNCTION enforce_document_record_system_time(); +CREATE FUNCTION validate_document_record_evidence_binding() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +DECLARE + evidence_payload jsonb; + evidence_key_count integer; + computed_evidence_digest text; + evidence_received_at timestamptz; + evidence_recorded_at timestamptz; +BEGIN + computed_evidence_digest := encode( + pg_catalog.digest( + pg_catalog.convert_to(NEW.canonical_evidence_json, 'UTF8'), + 'sha256' + ), + 'hex' + ); + IF computed_evidence_digest IS DISTINCT FROM NEW.evidence_digest_sha256 THEN + RAISE EXCEPTION 'document-record canonical evidence digest does not match the stored evidence bytes' + USING ERRCODE = '23514'; + END IF; + + BEGIN + evidence_payload := NEW.canonical_evidence_json::jsonb; + EXCEPTION WHEN OTHERS THEN + RAISE EXCEPTION 'document-record canonical evidence must be valid JSON' + USING ERRCODE = '22023'; + END; + + IF pg_catalog.jsonb_typeof(evidence_payload) IS DISTINCT FROM 'object' THEN + RAISE EXCEPTION 'document-record canonical evidence must be one JSON object' + USING ERRCODE = '23514'; + END IF; + + SELECT count(*) + INTO evidence_key_count + FROM pg_catalog.jsonb_object_keys(evidence_payload); + + IF evidence_key_count <> 17 + OR NOT ( + evidence_payload ?& ARRAY[ + 'artifact_digest', + 'artifact_reference', + 'classification_code', + 'content_storage_state', + 'decision_authority_state', + 'document_category_code', + 'document_record_reference', + 'employment_record_reference', + 'person_record_reference', + 'received_at', + 'recorded_at', + 'retention_policy_digest', + 'retention_policy_reference', + 'schema_version', + 'source_provenance_digest', + 'tenant_record_id', + 'uploader_actor_reference' + ] + ) THEN + RAISE EXCEPTION 'document-record canonical evidence has an unexpected key set' + USING ERRCODE = '23514'; + END IF; + + IF evidence_payload ->> 'artifact_digest' + IS DISTINCT FROM NEW.artifact_digest_sha256 + OR evidence_payload ->> 'artifact_reference' + IS DISTINCT FROM NEW.artifact_reference + OR evidence_payload ->> 'classification_code' + IS DISTINCT FROM NEW.classification_code + OR evidence_payload ->> 'content_storage_state' + IS DISTINCT FROM NEW.content_storage_state + OR evidence_payload ->> 'decision_authority_state' + IS DISTINCT FROM NEW.decision_authority_state + OR evidence_payload ->> 'document_category_code' + IS DISTINCT FROM NEW.document_category_code + OR evidence_payload ->> 'document_record_reference' + IS DISTINCT FROM NEW.document_record_reference + OR evidence_payload ->> 'employment_record_reference' + IS DISTINCT FROM NEW.employment_record_reference + OR evidence_payload ->> 'person_record_reference' + IS DISTINCT FROM NEW.person_record_reference + OR evidence_payload ->> 'retention_policy_digest' + IS DISTINCT FROM NEW.retention_policy_digest_sha256 + OR evidence_payload ->> 'retention_policy_reference' + IS DISTINCT FROM NEW.retention_policy_reference + OR evidence_payload ->> 'schema_version' + IS DISTINCT FROM 'orgmetra.document_record_evidence.v1' + OR evidence_payload ->> 'source_provenance_digest' + IS DISTINCT FROM NEW.source_provenance_digest_sha256 + OR evidence_payload ->> 'tenant_record_id' + IS DISTINCT FROM NEW.tenant_record_id::text + OR evidence_payload ->> 'uploader_actor_reference' + IS DISTINCT FROM NEW.uploader_actor_reference THEN + RAISE EXCEPTION 'document-record canonical evidence does not exactly match the typed metadata row' + USING ERRCODE = '23514'; + END IF; + + BEGIN + evidence_received_at := (evidence_payload ->> 'received_at')::timestamptz; + evidence_recorded_at := (evidence_payload ->> 'recorded_at')::timestamptz; + EXCEPTION WHEN OTHERS THEN + RAISE EXCEPTION 'document-record canonical evidence timestamps are invalid' + USING ERRCODE = '22023'; + END; + + IF evidence_received_at IS DISTINCT FROM NEW.received_at + OR evidence_recorded_at < evidence_received_at + OR evidence_recorded_at > NEW.recorded_at THEN + RAISE EXCEPTION 'document-record canonical evidence chronology does not match persistence time' + USING ERRCODE = '23514'; + END IF; + + RETURN NULL; +END; +$$; + +COMMENT ON FUNCTION validate_document_record_evidence_binding() IS + 'After insert constraints and system-time checks, validates exact canonical DocumentRecordEvidence bytes, SHA-256 digest, schema/key shape, typed metadata equality, and evidence chronology without reading foreign application tables.'; + +CREATE TRIGGER document_record_evidence_binding_guard +AFTER INSERT ON document_record +FOR EACH ROW +EXECUTE FUNCTION validate_document_record_evidence_binding(); + CREATE FUNCTION protect_document_record_immutability() RETURNS trigger LANGUAGE plpgsql From 799a126df7f86f2c936def7208226dddcd38fd8c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 23:26:11 -0700 Subject: [PATCH 10/13] docs(document-records): bind row to canonical evidence snapshot --- docs/adr/0107-document-record-persistence.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/adr/0107-document-record-persistence.md b/docs/adr/0107-document-record-persistence.md index 73f07ea14..ab3751ad4 100644 --- a/docs/adr/0107-document-record-persistence.md +++ b/docs/adr/0107-document-record-persistence.md @@ -10,6 +10,8 @@ Orgmetra architecture assigns `document_records` ownership of HR document metada `people_core`, `audit_provenance`, and `integration_hub` are separate bounded contexts. Therefore the document-record relation must not query or foreign-key their application tables merely because the initial modular deployment can share one PostgreSQL cluster. Cross-context identities remain opaque published-contract references. +A digest column by itself is insufficient evidence binding: a caller could otherwise persist typed metadata from one document together with a syntactically valid SHA-256 from a different `DocumentRecordEvidence` packet. Durable persistence therefore has to retain the exact value-minimized canonical evidence bytes and verify that their digest and semantic fields describe the same row. + ## Decision Add one immutable `document_record` relation owned by the document-records boundary. It stores: @@ -18,10 +20,13 @@ Add one immutable `document_record` relation owned by the document-records bound - opaque Person and Employment references rather than cross-service table identifiers; - reviewed document category, uploader/persisting actor correlations, and immutable artifact reference; - SHA-256 artifact, source-provenance, retention-policy, evidence, and application-evidence digests; +- the exact bounded canonical JSON emitted by the reviewed `DocumentRecordEvidence` schema; - opaque audit/outbox handoff references from owner contracts; -- business `received_at` and PostgreSQL-owned `recorded_at`; +- business `received_at` and PostgreSQL-owned persistence `recorded_at`; - fixed `restricted_hr`, `artifact_reference_only`, and `not_authorized_for_employment_decision` states. +An insert is accepted only when the SHA-256 of the exact stored canonical JSON equals `evidence_digest_sha256`, the JSON has exactly the reviewed v1 key set, every trust-bearing evidence field equals the typed persistence column or fixed state, the schema version is `orgmetra.document_record_evidence.v1`, the evidence receipt timestamp equals the row receipt timestamp, and the evidence issuance timestamp falls between receipt and durable persistence. The canonical JSON is value-minimized metadata evidence, not document content. + The relation stores no document bytes/title, free-form HR text, compensation, rating, credentials, or employment-decision output. UPDATE, DELETE, and TRUNCATE are rejected. Lifecycle disposition belongs to a separate governed relation rather than rewriting the immutable metadata snapshot. Tenant isolation uses enabled and forced PostgreSQL row-level security. A missing tenant context yields no visible rows. The design deliberately keeps Person/Employment/audit/outbox as opaque references so later service extraction does not require changing the persistence contract. From fcce901ee2f63241666788dfb635cfb35cb87418 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 23:26:22 -0700 Subject: [PATCH 11/13] docs(document-records): trace canonical evidence binding --- docs/traceability/document-record-persistence.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/traceability/document-record-persistence.md b/docs/traceability/document-record-persistence.md index 7d3d4bb4c..611ea88bf 100644 --- a/docs/traceability/document-record-persistence.md +++ b/docs/traceability/document-record-persistence.md @@ -11,14 +11,15 @@ | Requirement | Implementation boundary | Regression evidence | |---|---|---| -| Value minimization | `document_record` has metadata/references/digests only | test rejects prohibited value-bearing columns | +| Value minimization | `document_record` has metadata/references/digests plus the exact value-minimized canonical evidence JSON, never document content | test rejects prohibited value-bearing columns | +| Evidence-to-row binding | SHA-256 over exact stored canonical JSON; exact v1 key set/schema; typed-field equality; evidence receipt/issuance chronology | mismatch packet with a different valid evidence payload but predecessor digest must fail with `canonical evidence digest` | | Person/Employment service extraction | opaque `person_record:` / `employment_record:` references | test rejects non-opaque Person reference and asserts no foreign FK to People tables | | Audit/outbox service extraction | opaque `audit_event:` / `outbox_event:` references + application digest | test asserts no FK to audit/outbox application tables | | Reviewed vocabulary | closed document category and fixed persistence purpose/reason | happy path + wrong-reason failure | -| Business/system time | caller `received_at`; PostgreSQL `transaction_timestamp()` `recorded_at` | future receipt and backdated system-time failures | +| Business/system time | caller `received_at`; evidence-issued `recorded_at` in canonical payload; PostgreSQL `transaction_timestamp()` durable `recorded_at` | future receipt, evidence chronology, and backdated persistence-time controls | | Immutable metadata | UPDATE/DELETE/TRUNCATE guards | three destructive-operation failures | | Tenant isolation | ENABLE + FORCE RLS with transaction tenant context | NOSUPERUSER/NOBYPASSRLS reader sees only its tenant; no context sees zero rows | -| Non-decision posture | fixed classification/storage/decision-authority states | persisted-state assertion | +| Non-decision posture | fixed classification/storage/decision-authority states | persisted-state assertion and canonical payload equality | | Exact candidate provenance | pinned PostgreSQL workflow and exact-head checkout | `Document Record Persistence Quality` | ## Integration rule From 5e521fd829de313a037f45ac28227c2ae5362d37 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 23:26:53 -0700 Subject: [PATCH 12/13] fix(document-records): resolve pgcrypto digest in extension schema --- database/migrations/0021_document_record_persistence.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/0021_document_record_persistence.sql b/database/migrations/0021_document_record_persistence.sql index e32266080..5a5537801 100644 --- a/database/migrations/0021_document_record_persistence.sql +++ b/database/migrations/0021_document_record_persistence.sql @@ -159,7 +159,7 @@ DECLARE evidence_recorded_at timestamptz; BEGIN computed_evidence_digest := encode( - pg_catalog.digest( + public.digest( pg_catalog.convert_to(NEW.canonical_evidence_json, 'UTF8'), 'sha256' ), From d62e72d3fff8df7f1eb424fea5ee24389605c3e7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 23:15:53 +0900 Subject: [PATCH 13/13] fix(document-records): harden persistence migration boundaries --- .../0021_document_record_persistence.sql | 44 ++++++++++--------- docs/adr/0107-document-record-persistence.md | 2 + .../document-record-persistence.md | 2 +- manifest.json | 2 +- scripts/foundation-contract-core.mjs | 2 + ...st_document_record_persistence_postgres.sh | 25 ++++++++++- tests/validate_repository.py | 2 + 7 files changed, 55 insertions(+), 24 deletions(-) diff --git a/database/migrations/0021_document_record_persistence.sql b/database/migrations/0021_document_record_persistence.sql index 5a5537801..868ed36a8 100644 --- a/database/migrations/0021_document_record_persistence.sql +++ b/database/migrations/0021_document_record_persistence.sql @@ -2,6 +2,10 @@ -- boundary. Cross-service Person/Employment/audit/outbox identities remain -- opaque published-contract references rather than direct application-table SQL. +BEGIN; + +SET LOCAL search_path = public, pg_catalog; + CREATE TABLE document_record ( tenant_record_id uuid NOT NULL REFERENCES tenant_record(tenant_record_id), document_record_id uuid PRIMARY KEY, @@ -122,9 +126,10 @@ CREATE TABLE document_record ( COMMENT ON TABLE document_record IS 'Immutable, value-minimized HR document metadata owned by document_records. The exact canonical evidence snapshot is digest-bound to the typed row; Person, Employment, audit, and outbox identities are opaque contract references. Document bytes and employment-decision authority are not stored here.'; -CREATE FUNCTION enforce_document_record_system_time() +CREATE FUNCTION public.enforce_document_record_system_time() RETURNS trigger LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp AS $$ BEGIN IF NEW.recorded_at IS DISTINCT FROM pg_catalog.transaction_timestamp() THEN @@ -139,17 +144,18 @@ BEGIN END; $$; -COMMENT ON FUNCTION enforce_document_record_system_time() IS +COMMENT ON FUNCTION public.enforce_document_record_system_time() IS 'Requires PostgreSQL-owned system-recorded time and rejects document receipt time later than the durable recording instant.'; CREATE TRIGGER document_record_system_time_guard BEFORE INSERT ON document_record FOR EACH ROW -EXECUTE FUNCTION enforce_document_record_system_time(); +EXECUTE FUNCTION public.enforce_document_record_system_time(); -CREATE FUNCTION validate_document_record_evidence_binding() +CREATE FUNCTION public.validate_document_record_evidence_binding() RETURNS trigger LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp AS $$ DECLARE evidence_payload jsonb; @@ -265,17 +271,18 @@ BEGIN END; $$; -COMMENT ON FUNCTION validate_document_record_evidence_binding() IS +COMMENT ON FUNCTION public.validate_document_record_evidence_binding() IS 'After insert constraints and system-time checks, validates exact canonical DocumentRecordEvidence bytes, SHA-256 digest, schema/key shape, typed metadata equality, and evidence chronology without reading foreign application tables.'; CREATE TRIGGER document_record_evidence_binding_guard AFTER INSERT ON document_record FOR EACH ROW -EXECUTE FUNCTION validate_document_record_evidence_binding(); +EXECUTE FUNCTION public.validate_document_record_evidence_binding(); -CREATE FUNCTION protect_document_record_immutability() +CREATE FUNCTION public.protect_document_record_immutability() RETURNS trigger LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp AS $$ BEGIN RAISE EXCEPTION 'document metadata is immutable; lifecycle changes require a separate governed relation' @@ -283,17 +290,18 @@ BEGIN END; $$; -COMMENT ON FUNCTION protect_document_record_immutability() IS +COMMENT ON FUNCTION public.protect_document_record_immutability() IS 'Rejects UPDATE and DELETE so the artifact/provenance metadata snapshot cannot be rewritten after issuance.'; CREATE TRIGGER document_record_immutability_guard BEFORE UPDATE OR DELETE ON document_record FOR EACH ROW -EXECUTE FUNCTION protect_document_record_immutability(); +EXECUTE FUNCTION public.protect_document_record_immutability(); -CREATE FUNCTION reject_document_record_truncate() +CREATE FUNCTION public.reject_document_record_truncate() RETURNS trigger LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp AS $$ BEGIN RAISE EXCEPTION 'document-record history cannot be truncated' @@ -301,13 +309,13 @@ BEGIN END; $$; -COMMENT ON FUNCTION reject_document_record_truncate() IS +COMMENT ON FUNCTION public.reject_document_record_truncate() IS 'Rejects table-wide TRUNCATE so immutable document metadata cannot bypass row-level controls.'; CREATE TRIGGER document_record_truncate_guard BEFORE TRUNCATE ON document_record FOR EACH STATEMENT -EXECUTE FUNCTION reject_document_record_truncate(); +EXECUTE FUNCTION public.reject_document_record_truncate(); REVOKE TRUNCATE ON document_record FROM PUBLIC; @@ -317,14 +325,10 @@ ALTER TABLE document_record FORCE ROW LEVEL SECURITY; CREATE POLICY document_record_tenant_isolation_policy ON document_record USING ( - tenant_record_id = NULLIF( - pg_catalog.current_setting('orgmetra.tenant_record_id', true), - '' - )::uuid + tenant_record_id = public.current_tenant_record_id() ) WITH CHECK ( - tenant_record_id = NULLIF( - pg_catalog.current_setting('orgmetra.tenant_record_id', true), - '' - )::uuid + tenant_record_id = public.current_tenant_record_id() ); + +COMMIT; diff --git a/docs/adr/0107-document-record-persistence.md b/docs/adr/0107-document-record-persistence.md index ab3751ad4..3660b9cec 100644 --- a/docs/adr/0107-document-record-persistence.md +++ b/docs/adr/0107-document-record-persistence.md @@ -31,6 +31,8 @@ The relation stores no document bytes/title, free-form HR text, compensation, ra Tenant isolation uses enabled and forced PostgreSQL row-level security. A missing tenant context yields no visible rows. The design deliberately keeps Person/Employment/audit/outbox as opaque references so later service extraction does not require changing the persistence contract. +The migration creates the relation under a transaction-local `public, pg_catalog` search path, pins each trusted trigger function to `pg_catalog, public, pg_temp`, and reuses the shared `public.current_tenant_record_id()` policy helper. + ## Consequences This is an evidence/metadata system of record, not object storage and not authorization to read, export, delete, or use the document in an employment decision. The host must resolve current authorization and foreign references through published owner contracts before persistence or retrieval. Audit/outbox references are correlations to owner-controlled immutable evidence; this relation does not directly query those foreign application tables. diff --git a/docs/traceability/document-record-persistence.md b/docs/traceability/document-record-persistence.md index 611ea88bf..05553803c 100644 --- a/docs/traceability/document-record-persistence.md +++ b/docs/traceability/document-record-persistence.md @@ -18,7 +18,7 @@ | Reviewed vocabulary | closed document category and fixed persistence purpose/reason | happy path + wrong-reason failure | | Business/system time | caller `received_at`; evidence-issued `recorded_at` in canonical payload; PostgreSQL `transaction_timestamp()` durable `recorded_at` | future receipt, evidence chronology, and backdated persistence-time controls | | Immutable metadata | UPDATE/DELETE/TRUNCATE guards | three destructive-operation failures | -| Tenant isolation | ENABLE + FORCE RLS with transaction tenant context | NOSUPERUSER/NOBYPASSRLS reader sees only its tenant; no context sees zero rows | +| Tenant isolation | ENABLE + FORCE RLS with transaction tenant context and the shared tenant helper | NOSUPERUSER/NOBYPASSRLS reader sees only its tenant; no context sees zero rows; trigger functions pin the trusted search path | | Non-decision posture | fixed classification/storage/decision-authority states | persisted-state assertion and canonical payload equality | | Exact candidate provenance | pinned PostgreSQL workflow and exact-head checkout | `Document Record Persistence Quality` | diff --git a/manifest.json b/manifest.json index 97f2bab14..5778a894c 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":"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":"database/migrations/0021_document_record_persistence.sql","sha256":"e2dd9ca0c17141c2b3e06f64726f3ac0fa7cb1cd02798564cef72a12455a1286","bytes":14001,"lines":334},{"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":"eed5650ddd57a1c7efbbfc8703a0e86ce34e16e0718337f968af47c4fde1f2f2","bytes":28291,"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_document_record_persistence_postgres.sh","sha256":"904af2c232e1d0739e148a5b1c6560416a202430f979fce01a93bef0563f10a7","bytes":15610,"lines":336},{"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":"70bf5bcc98cafa55275ab2de59064ab40d9c9cc76e03463ab270e055e8f80e85","bytes":27413,"lines":640}]} diff --git a/scripts/foundation-contract-core.mjs b/scripts/foundation-contract-core.mjs index 1e9fb267c..704f7d53f 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/0021_document_record_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_document_record_persistence_postgres.sh', 'tests/validate_repository.py' ]); diff --git a/tests/test_document_record_persistence_postgres.sh b/tests/test_document_record_persistence_postgres.sh index 8377282ed..eccefb64e 100644 --- a/tests/test_document_record_persistence_postgres.sh +++ b/tests/test_document_record_persistence_postgres.sh @@ -30,8 +30,11 @@ ARTIFACT_DIGEST="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa SOURCE_DIGEST="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" RETENTION_DIGEST="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" APPLICATION_DIGEST="eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" -RECEIVED_AT="2026-08-24T05:55:00Z" -EVIDENCE_RECORDED_AT="2026-08-24T05:58:00Z" +IFS='|' read -r RECEIVED_AT EVIDENCE_RECORDED_AT < <(psql "${DATABASE_URL}" -Atqc " +SELECT + to_char((pg_catalog.transaction_timestamp() - interval '2 minutes') AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"'), + to_char((pg_catalog.transaction_timestamp() - interval '1 minute') AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"'); +") canonical_evidence="$(python3 - < ARRAY['search_path=pg_catalog, public, pg_temp']::text[];")" +if [[ "${trusted_search_path_count}" != "4" ]]; then + echo "document-record trigger functions do not pin the trusted search_path: ${trusted_search_path_count}/4" >&2 + exit 1 +fi + psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 <<'SQL' DO $$ BEGIN diff --git a/tests/validate_repository.py b/tests/validate_repository.py index fe0a329ff..2a666f170 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/0021_document_record_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_document_record_persistence_postgres.sh", "tests/validate_repository.py", ]