From fc422cf507053aa6eb010f9f0e623caef06dbdda Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 23:17:42 -0700 Subject: [PATCH 01/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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", ] From 4dfb83901f430644f5bfdf286b06cb957e1ca73c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:36:03 +0900 Subject: [PATCH 14/28] test(document-records): reject duplicate canonical evidence keys --- ...nt_record_evidence_unique_keys_postgres.sh | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 tests/test_document_record_evidence_unique_keys_postgres.sh diff --git a/tests/test_document_record_evidence_unique_keys_postgres.sh b/tests/test_document_record_evidence_unique_keys_postgres.sh new file mode 100644 index 000000000..1c12709a8 --- /dev/null +++ b/tests/test_document_record_evidence_unique_keys_postgres.sh @@ -0,0 +1,102 @@ +#!/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/0021_document_record_persistence.sql; do + psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f "${migration}" +done + +TENANT_ID="10000000-0000-7000-8000-000000000101" +DOCUMENT_ID="00000000-0000-7000-8000-000000000131" +DOCUMENT_REFERENCE="document_record:00000000-0000-4000-8000-000000000131" +PERSON_REFERENCE="person_record:00000000-0000-4000-8000-000000000111" +EMPLOYMENT_REFERENCE="employment_record:00000000-0000-4000-8000-000000000121" +ARTIFACT_REFERENCE="document_artifact:00000000-0000-4000-8000-000000000141" +RETENTION_REFERENCE="retention_policy:00000000-0000-4000-8000-000000000151" +UPLOADER="actor:00000000-0000-4000-8000-000000000161" +PERSISTED_BY="actor:00000000-0000-4000-8000-000000000162" +AUDIT_REFERENCE="audit_event:00000000-0000-4000-8000-000000000171" +OUTBOX_REFERENCE="outbox_event:00000000-0000-4000-8000-000000000172" +ARTIFACT_DIGEST="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +SOURCE_DIGEST="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +RETENTION_DIGEST="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" +APPLICATION_DIGEST="eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +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 - <&1 <&2 + exit 1 +fi + +echo "document-record canonical evidence unique-key contract passed" From 74933e23c29b454ac2337c1d2f475c1636f8f6db Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:36:21 +0900 Subject: [PATCH 15/28] fix(document-records): reject duplicate evidence keys --- .../0022_document_record_evidence_unique_keys.sql | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 database/migrations/0022_document_record_evidence_unique_keys.sql diff --git a/database/migrations/0022_document_record_evidence_unique_keys.sql b/database/migrations/0022_document_record_evidence_unique_keys.sql new file mode 100644 index 000000000..d5a755e14 --- /dev/null +++ b/database/migrations/0022_document_record_evidence_unique_keys.sql @@ -0,0 +1,15 @@ +-- Preserve exact canonical DocumentRecordEvidence object semantics before jsonb +-- normalization can collapse duplicate object keys. + +BEGIN; + +SET LOCAL search_path = public, pg_catalog; + +ALTER TABLE document_record + ADD CONSTRAINT document_canonical_evidence_unique_keys_check + CHECK (canonical_evidence_json IS JSON OBJECT WITH UNIQUE KEYS); + +COMMENT ON CONSTRAINT document_canonical_evidence_unique_keys_check ON document_record IS + 'Rejects invalid, non-object, or duplicate-key canonical evidence before jsonb normalization; reviewed DocumentRecordEvidence bytes must represent one unique-key JSON object.'; + +COMMIT; From b69f9a4701dfc9991f0f263bf0831b0abd68ae5a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:36:46 +0900 Subject: [PATCH 16/28] test(document-records): admit unique-key hardening migration --- tests/test_document_record_evidence_unique_keys_postgres.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_document_record_evidence_unique_keys_postgres.sh b/tests/test_document_record_evidence_unique_keys_postgres.sh index 1c12709a8..3fab69e8d 100644 --- a/tests/test_document_record_evidence_unique_keys_postgres.sh +++ b/tests/test_document_record_evidence_unique_keys_postgres.sh @@ -6,7 +6,8 @@ set -euo pipefail for migration in \ database/migrations/0001_foundation_schema.sql \ database/migrations/0002_sealed_evidence_digest.sql \ - database/migrations/0021_document_record_persistence.sql; do + database/migrations/0021_document_record_persistence.sql \ + database/migrations/0022_document_record_evidence_unique_keys.sql; do psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f "${migration}" done From 55ef5e764c2aea298223651e4446dad597d6922c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:37:24 +0900 Subject: [PATCH 17/28] docs(document-records): trace unique-key JSON authority --- docs/doctoring/document-record-persistence-references.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/doctoring/document-record-persistence-references.md b/docs/doctoring/document-record-persistence-references.md index 7cc09f66a..52fdc360b 100644 --- a/docs/doctoring/document-record-persistence-references.md +++ b/docs/doctoring/document-record-persistence-references.md @@ -2,7 +2,7 @@ ## 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. +Primary-source design references reviewed for the active document-record persistence PR. These sources inform the design; Orgmetra does not claim standards certification or conformance from their citation. ## APA 7 references @@ -10,10 +10,14 @@ PostgreSQL Global Development Group. (2026). *PostgreSQL 16 documentation: CREAT PostgreSQL Global Development Group. (2026). *PostgreSQL 16 documentation: ALTER TABLE*. https://www.postgresql.org/docs/16/sql-altertable.html +PostgreSQL Global Development Group. (2026). *PostgreSQL 16 documentation: JSON functions and operators*. https://www.postgresql.org/docs/16/functions-json.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. +PostgreSQL 16 JSON predicates define `IS JSON OBJECT WITH UNIQUE KEYS` for rejecting malformed, non-object, and duplicate-key JSON before conversion to `jsonb`. The document-record boundary uses that predicate because `jsonb` normalization is not evidence of uniqueness in the original canonical JSON bytes. The byte-level SHA-256 remains authoritative for the submitted representation, while the database constraint separately proves that the representation is one unique-key JSON object. + 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 48eac76f3a4916d64add01bde98a2eadd1321624 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:37:45 +0900 Subject: [PATCH 18/28] docs(document-records): current persistence traceability --- .../traceability/document-record-persistence.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/traceability/document-record-persistence.md b/docs/traceability/document-record-persistence.md index 05553803c..02ceecbb9 100644 --- a/docs/traceability/document-record-persistence.md +++ b/docs/traceability/document-record-persistence.md @@ -2,17 +2,18 @@ ## 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. +- Protected truth: `develop@eb9757f8649aaad026a9865508d9aad50c1a7a4f` has no `document_record` persistence relation. +- Dependency-active truth: PR #98 exact `ec39bfa9bcb73b2b7730a0a6115b2e484d78acb2` is the current `DocumentRecordEvidence` authority and has adopted protected `develop` without restoring retired feature-local workflows. +- Active-PR truth: PR #107 is a Draft child of #98. Its current branch preserves only the document-record persistence domain/docs/PostgreSQL contract delta on top of #98; obsolete persistence-local workflow and stale root Foundation/manifest registrations were intentionally not restacked. +- 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. Completion-receipt and recovery-aware deletion authority remains downstream #308. ## Requirements → executable evidence | Requirement | Implementation boundary | Regression evidence | |---|---|---| -| 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 | +| Value minimization | `document_record` has metadata/references/digests plus the exact value-minimized canonical evidence JSON, never document content | `test_document_record_persistence_postgres.sh` 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` | +| Canonical JSON object integrity | migration `0022_document_record_evidence_unique_keys.sql` requires `canonical_evidence_json IS JSON OBJECT WITH UNIQUE KEYS` before `jsonb` normalization can collapse duplicate keys | `test_document_record_evidence_unique_keys_postgres.sh` recomputes SHA-256 over a syntactically valid duplicate-key payload and requires fail-closed rejection | | 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 | @@ -20,8 +21,10 @@ | Immutable metadata | UPDATE/DELETE/TRUNCATE guards | three destructive-operation failures | | 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` | +| Repository-owned acceptance | #161 keeps Foundation as the canonical repository quality owner; #258/#259 owns package-neutral Foundation evolution | no exact-head hosted GREEN is claimed for this stacked child until canonical Foundation discovers and runs owned PostgreSQL contracts without restoring a feature-local workflow | ## 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. +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 released package/API/event contract. The shared PostgreSQL cluster is not permission for direct cross-service application-table SQL. + +The #107 branch must remain Draft until current #98 authority and the canonical Foundation owner can produce fresh exact-head, non-vacuous PostgreSQL acceptance. Historical feature-local workflow results and predecessor heads are RCA only and do not transfer. From dd3b969cbb07a7cb02406eb4f6d6a2a5082dd486 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:38:08 +0900 Subject: [PATCH 19/28] docs(document-records): bind canonical JSON uniqueness --- docs/adr/0107-document-record-persistence.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/adr/0107-document-record-persistence.md b/docs/adr/0107-document-record-persistence.md index 3660b9cec..82e4af9a6 100644 --- a/docs/adr/0107-document-record-persistence.md +++ b/docs/adr/0107-document-record-persistence.md @@ -8,10 +8,12 @@ Active PR architecture. This decision does not describe protected-`develop` trut 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. +`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 released-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. +Raw JSON uniqueness is a separate trust property. PostgreSQL `jsonb` normalization can collapse duplicate object keys before key-count and typed-field checks run, so a byte-level digest plus a post-normalization 17-key check is not enough to prove that the submitted canonical bytes represented one reviewed object. Duplicate-key evidence must fail closed before `jsonb` normalization. + ## Decision Add one immutable `document_record` relation owned by the document-records boundary. It stores: @@ -25,16 +27,16 @@ Add one immutable `document_record` relation owned by the document-records bound - 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. +An insert is accepted only when the SHA-256 of the exact stored canonical JSON equals `evidence_digest_sha256`, the raw JSON is one object with unique keys, the normalized object 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. Migration `0022_document_record_evidence_unique_keys.sql` enforces the pre-normalization unique-key object predicate. 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. -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. +The migrations execute under a transaction-local `public, pg_catalog` search path, pin each trusted trigger function to `pg_catalog, public, pg_temp`, and reuse 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. +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 released 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. +Migration numbers `0021` and `0022` are reserved in this stacked branch only. After parent #98 integrates, this PR must be retargeted to fresh `develop`, migration ordering reconciled, and both PostgreSQL contracts admitted through the canonical repository Foundation path before review readiness. No feature-local workflow is restored for that purpose. From 7fec04655b92bc677322f501216b2d96f72c903b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 03:02:31 +0900 Subject: [PATCH 20/28] test(document-records): reject noncanonical evidence bytes --- ...ocument_record_canonical_bytes_postgres.sh | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 tests/test_document_record_canonical_bytes_postgres.sh diff --git a/tests/test_document_record_canonical_bytes_postgres.sh b/tests/test_document_record_canonical_bytes_postgres.sh new file mode 100644 index 000000000..4b133ebc9 --- /dev/null +++ b/tests/test_document_record_canonical_bytes_postgres.sh @@ -0,0 +1,103 @@ +#!/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/0021_document_record_persistence.sql \ + database/migrations/0022_document_record_evidence_unique_keys.sql; do + psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f "${migration}" +done + +TENANT_ID="10000000-0000-7000-8000-000000000201" +DOCUMENT_ID="00000000-0000-7000-8000-000000000231" +DOCUMENT_REFERENCE="document_record:00000000-0000-4000-8000-000000000231" +PERSON_REFERENCE="person_record:00000000-0000-4000-8000-000000000211" +EMPLOYMENT_REFERENCE="employment_record:00000000-0000-4000-8000-000000000221" +ARTIFACT_REFERENCE="document_artifact:00000000-0000-4000-8000-000000000241" +RETENTION_REFERENCE="retention_policy:00000000-0000-4000-8000-000000000251" +UPLOADER="actor:00000000-0000-4000-8000-000000000261" +PERSISTED_BY="actor:00000000-0000-4000-8000-000000000262" +AUDIT_REFERENCE="audit_event:00000000-0000-4000-8000-000000000271" +OUTBOX_REFERENCE="outbox_event:00000000-0000-4000-8000-000000000272" +ARTIFACT_DIGEST="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +SOURCE_DIGEST="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +RETENTION_DIGEST="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" +APPLICATION_DIGEST="eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +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\"'); +") + +noncanonical_evidence="$(python3 - <&1 <&2 + exit 1 +fi + +echo "document-record deterministic canonical-byte contract passed" From 2057fa3f967da019f12d97153124c2b92b63ff8c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 03:03:00 +0900 Subject: [PATCH 21/28] fix(document-records): bind deterministic canonical evidence bytes --- ...023_document_record_canonical_encoding.sql | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 database/migrations/0023_document_record_canonical_encoding.sql diff --git a/database/migrations/0023_document_record_canonical_encoding.sql b/database/migrations/0023_document_record_canonical_encoding.sql new file mode 100644 index 000000000..76a289e80 --- /dev/null +++ b/database/migrations/0023_document_record_canonical_encoding.sql @@ -0,0 +1,87 @@ +-- Bind persisted evidence to the exact deterministic JSON encoding emitted by +-- DocumentRecordEvidence, not only to an arbitrary semantically equivalent JSON +-- representation whose digest was recomputed by the caller. + +BEGIN; + +SET LOCAL search_path = public, pg_catalog; + +CREATE FUNCTION public.validate_document_record_canonical_encoding() +RETURNS trigger +LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp +AS $$ +DECLARE + evidence_payload jsonb; + evidence_recorded_at timestamptz; + canonical_received_at text; + canonical_recorded_at text; + expected_canonical_evidence text; +BEGIN + -- Migration 0022 and the earlier evidence-binding trigger already establish + -- one unique-key object, the exact v1 key set, typed-field equality, and + -- timestamp validity before this same-event trigger runs. + evidence_payload := NEW.canonical_evidence_json::jsonb; + evidence_recorded_at := (evidence_payload ->> 'recorded_at')::timestamptz; + + canonical_received_at := pg_catalog.regexp_replace( + pg_catalog.to_char( + NEW.received_at AT TIME ZONE 'UTC', + 'YYYY-MM-DD"T"HH24:MI:SS.US' + ), + '\.000000$', + '' + ) || 'Z'; + canonical_recorded_at := pg_catalog.regexp_replace( + pg_catalog.to_char( + evidence_recorded_at AT TIME ZONE 'UTC', + 'YYYY-MM-DD"T"HH24:MI:SS.US' + ), + '\.000000$', + '' + ) || 'Z'; + + -- Every v1 value is already constrained to ASCII-safe reviewed vocabularies, + -- UUID references, lowercase digests, or canonical UTC timestamps. to_json() + -- therefore supplies the same JSON string escaping contract while the fixed + -- concatenation preserves Python json.dumps(sort_keys=True, + -- separators=(",", ":"), ensure_ascii=True) key order and separators. + expected_canonical_evidence := + '{' || + '"artifact_digest":' || pg_catalog.to_json(NEW.artifact_digest_sha256)::text || ',' || + '"artifact_reference":' || pg_catalog.to_json(NEW.artifact_reference)::text || ',' || + '"classification_code":' || pg_catalog.to_json(NEW.classification_code)::text || ',' || + '"content_storage_state":' || pg_catalog.to_json(NEW.content_storage_state)::text || ',' || + '"decision_authority_state":' || pg_catalog.to_json(NEW.decision_authority_state)::text || ',' || + '"document_category_code":' || pg_catalog.to_json(NEW.document_category_code)::text || ',' || + '"document_record_reference":' || pg_catalog.to_json(NEW.document_record_reference)::text || ',' || + '"employment_record_reference":' || pg_catalog.to_json(NEW.employment_record_reference)::text || ',' || + '"person_record_reference":' || pg_catalog.to_json(NEW.person_record_reference)::text || ',' || + '"received_at":' || pg_catalog.to_json(canonical_received_at)::text || ',' || + '"recorded_at":' || pg_catalog.to_json(canonical_recorded_at)::text || ',' || + '"retention_policy_digest":' || pg_catalog.to_json(NEW.retention_policy_digest_sha256)::text || ',' || + '"retention_policy_reference":' || pg_catalog.to_json(NEW.retention_policy_reference)::text || ',' || + '"schema_version":' || pg_catalog.to_json('orgmetra.document_record_evidence.v1'::text)::text || ',' || + '"source_provenance_digest":' || pg_catalog.to_json(NEW.source_provenance_digest_sha256)::text || ',' || + '"tenant_record_id":' || pg_catalog.to_json(NEW.tenant_record_id::text)::text || ',' || + '"uploader_actor_reference":' || pg_catalog.to_json(NEW.uploader_actor_reference)::text || + '}'; + + IF NEW.canonical_evidence_json IS DISTINCT FROM expected_canonical_evidence THEN + RAISE EXCEPTION 'document-record canonical evidence bytes are not the deterministic v1 encoding' + USING ERRCODE = '23514'; + END IF; + + RETURN NULL; +END; +$$; + +COMMENT ON FUNCTION public.validate_document_record_canonical_encoding() IS + 'Rejects alternate whitespace, key-order, timestamp-text, or other semantically equivalent JSON representations; durable evidence must equal the deterministic v1 canonical bytes emitted by DocumentRecordEvidence.'; + +CREATE TRIGGER document_record_z_canonical_encoding_guard +AFTER INSERT ON document_record +FOR EACH ROW +EXECUTE FUNCTION public.validate_document_record_canonical_encoding(); + +COMMIT; From 27c30b6d02edfadc819acd59b952f2cc6b1d5e99 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 03:03:19 +0900 Subject: [PATCH 22/28] test(document-records): wire canonical-byte persistence repair --- tests/test_document_record_canonical_bytes_postgres.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_document_record_canonical_bytes_postgres.sh b/tests/test_document_record_canonical_bytes_postgres.sh index 4b133ebc9..b9b07a674 100644 --- a/tests/test_document_record_canonical_bytes_postgres.sh +++ b/tests/test_document_record_canonical_bytes_postgres.sh @@ -7,7 +7,8 @@ for migration in \ database/migrations/0001_foundation_schema.sql \ database/migrations/0002_sealed_evidence_digest.sql \ database/migrations/0021_document_record_persistence.sql \ - database/migrations/0022_document_record_evidence_unique_keys.sql; do + database/migrations/0022_document_record_evidence_unique_keys.sql \ + database/migrations/0023_document_record_canonical_encoding.sql; do psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f "${migration}" done From 8221e11ae0f0c0df0b5498eae1789662a8173035 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 03:03:38 +0900 Subject: [PATCH 23/28] test(document-records): assert duplicate-key constraint evidence --- tests/test_document_record_evidence_unique_keys_postgres.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_document_record_evidence_unique_keys_postgres.sh b/tests/test_document_record_evidence_unique_keys_postgres.sh index 3fab69e8d..ab9316022 100644 --- a/tests/test_document_record_evidence_unique_keys_postgres.sh +++ b/tests/test_document_record_evidence_unique_keys_postgres.sh @@ -95,7 +95,7 @@ SQL status=$? set -e -if [[ ${status} -eq 0 || "${output}" != *"unique keys"* ]]; then +if [[ ${status} -eq 0 || "${output}" != *"document_canonical_evidence_unique_keys_check"* ]]; then echo "document-record persistence accepted duplicate canonical evidence keys: ${output}" >&2 exit 1 fi From 937406173fd4afd303a3802e2889e8cd42fd2efe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 03:04:05 +0900 Subject: [PATCH 24/28] docs(document-records): require deterministic evidence encoding --- docs/adr/0107-document-record-persistence.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/adr/0107-document-record-persistence.md b/docs/adr/0107-document-record-persistence.md index 82e4af9a6..94c72b38f 100644 --- a/docs/adr/0107-document-record-persistence.md +++ b/docs/adr/0107-document-record-persistence.md @@ -14,6 +14,8 @@ A digest column by itself is insufficient evidence binding: a caller could other Raw JSON uniqueness is a separate trust property. PostgreSQL `jsonb` normalization can collapse duplicate object keys before key-count and typed-field checks run, so a byte-level digest plus a post-normalization 17-key check is not enough to prove that the submitted canonical bytes represented one reviewed object. Duplicate-key evidence must fail closed before `jsonb` normalization. +A second representation gap remains even after unique-key validation: syntactically valid JSON with the same keys and values can carry different whitespace, key order, or timestamp text. If a caller recomputes SHA-256 over those alternate bytes, digest verification and normalized semantic equality both succeed even though the bytes were not emitted by the reviewed `DocumentRecordEvidence.canonical_json()` contract. Persistence therefore has to prove deterministic encoding identity, not only semantic equivalence plus a matching caller-supplied digest. + ## Decision Add one immutable `document_record` relation owned by the document-records boundary. It stores: @@ -27,9 +29,11 @@ Add one immutable `document_record` relation owned by the document-records bound - 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 raw JSON is one object with unique keys, the normalized object 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. Migration `0022_document_record_evidence_unique_keys.sql` enforces the pre-normalization unique-key object predicate. The canonical JSON is value-minimized metadata evidence, not document content. +An insert is accepted only when the SHA-256 of the exact stored canonical JSON equals `evidence_digest_sha256`, the raw JSON is one object with unique keys, the normalized object 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. + +Migration `0022_document_record_evidence_unique_keys.sql` enforces the pre-normalization unique-key object predicate. Migration `0023_document_record_canonical_encoding.sql` separately reconstructs the reviewed v1 byte representation from the already validated typed row plus canonical UTC evidence timestamps and requires exact string equality with the stored evidence. That check rejects alternate separator whitespace, key ordering, and equivalent-but-noncanonical timestamp text even when the caller recomputes a matching digest. The reconstruction mirrors the package contract `json.dumps(sort_keys=True, separators=(",", ":"), ensure_ascii=True)` for the v1 ASCII-constrained value vocabulary; it is version-specific and must change only with an explicit evidence-schema/version change. -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. +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. @@ -39,4 +43,6 @@ The migrations execute under a transaction-local `public, pg_catalog` search pat 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 released 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 numbers `0021` and `0022` are reserved in this stacked branch only. After parent #98 integrates, this PR must be retargeted to fresh `develop`, migration ordering reconciled, and both PostgreSQL contracts admitted through the canonical repository Foundation path before review readiness. No feature-local workflow is restored for that purpose. +The deterministic-byte check intentionally couples persistence to evidence schema version v1. That is preferable to accepting multiple byte identities for one reviewed fact: a future serializer/vocabulary change requires a new versioned persistence contract rather than silently broadening v1 acceptance. + +Migration numbers `0021` through `0023` are reserved in this stacked branch only. After parent #98 integrates, this PR must be retargeted to fresh `develop`, migration ordering reconciled, and all PostgreSQL contracts admitted through the canonical repository Foundation path before review readiness. No feature-local workflow is restored for that purpose. From f56456636744fb11a90acfb8e1edc5a4190e722c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 03:20:44 +0900 Subject: [PATCH 25/28] test(document-records): prove canonical bytes accepted --- ...ocument_record_canonical_bytes_postgres.sh | 77 ++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/tests/test_document_record_canonical_bytes_postgres.sh b/tests/test_document_record_canonical_bytes_postgres.sh index b9b07a674..45b2c3246 100644 --- a/tests/test_document_record_canonical_bytes_postgres.sh +++ b/tests/test_document_record_canonical_bytes_postgres.sh @@ -29,8 +29,16 @@ RETENTION_DIGEST="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc APPLICATION_DIGEST="eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" 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\"'); + to_char( + (date_trunc('second', pg_catalog.transaction_timestamp() - interval '2 minutes') + + interval '123456 microseconds') AT TIME ZONE 'UTC', + 'YYYY-MM-DD\"T\"HH24:MI:SS.US\"Z\"' + ), + to_char( + (date_trunc('second', pg_catalog.transaction_timestamp() - interval '1 minute') + + interval '654321 microseconds') AT TIME ZONE 'UTC', + 'YYYY-MM-DD\"T\"HH24:MI:SS.US\"Z\"' + ); ") noncanonical_evidence="$(python3 - <&2 + exit 1 +fi + echo "document-record deterministic canonical-byte contract passed" From 7ce73aa44f47113b2ecd42d51bb5d38a22c0367d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 04:00:43 +0900 Subject: [PATCH 26/28] test(document-records): preserve configured RLS database endpoint --- ...st_document_record_persistence_postgres.sh | 56 ++++++++++++++++--- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/tests/test_document_record_persistence_postgres.sh b/tests/test_document_record_persistence_postgres.sh index eccefb64e..58dd6c4d9 100644 --- a/tests/test_document_record_persistence_postgres.sh +++ b/tests/test_document_record_persistence_postgres.sh @@ -309,7 +309,42 @@ if [[ "${trusted_search_path_count}" != "4" ]]; then exit 1 fi -psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 <<'SQL' +CURRENT_DATABASE="$(psql "${DATABASE_URL}" -Atqc 'SELECT pg_catalog.current_database();')" +if [[ -z "${CURRENT_DATABASE}" ]]; then + echo "document-record RLS fixture could not resolve current_database()" >&2 + exit 1 +fi + +READER_DATABASE_URL="$(DATABASE_URL="${DATABASE_URL}" python3 - <<'PY' +import os +from urllib.parse import parse_qsl, quote, urlencode, urlsplit, urlunsplit + +reader = "orgmetra_document_reader" +parts = urlsplit(os.environ["DATABASE_URL"]) +if parts.scheme not in {"postgres", "postgresql"}: + raise SystemExit("DATABASE_URL must use postgres or postgresql URI syntax") + +query = [ + (key, value) + for key, value in parse_qsl(parts.query, keep_blank_values=True) + if key not in {"user", "password"} +] +if parts.netloc: + endpoint = parts.netloc.rsplit("@", 1)[-1] + netloc = f"{quote(reader, safe='')}:{quote(reader, safe='')}@{endpoint}" + print(urlunsplit((parts.scheme, netloc, parts.path, urlencode(query, doseq=True), parts.fragment))) +else: + query = [("user", reader), ("password", reader), *query] + result = f"{parts.scheme}://{parts.path}" + if query: + result += "?" + urlencode(query, doseq=True) + if parts.fragment: + result += "#" + parts.fragment + print(result) +PY +)" + +psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -v current_database="${CURRENT_DATABASE}" <<'SQL' DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'orgmetra_document_reader') THEN @@ -317,17 +352,22 @@ BEGIN END IF; END $$; -GRANT CONNECT ON DATABASE orgmetra TO orgmetra_document_reader; +GRANT CONNECT ON DATABASE :"current_database" 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;')" +reader_database="$(psql "${READER_DATABASE_URL}" -Atqc 'SELECT pg_catalog.current_database();')" +if [[ "${reader_database}" != "${CURRENT_DATABASE}" ]]; then + echo "document-record reader changed the DATABASE_URL database target: owner=${CURRENT_DATABASE} reader=${reader_database}" >&2 + exit 1 +fi + +alpha_count="$(PGOPTIONS="-c orgmetra.tenant_record_id=${TENANT_ID}" \ + psql "${READER_DATABASE_URL}" -Atqc 'SELECT count(*) FROM document_record;')" +beta_count="$(PGOPTIONS="-c orgmetra.tenant_record_id=${OTHER_TENANT_ID}" \ + psql "${READER_DATABASE_URL}" -Atqc 'SELECT count(*) FROM document_record;')" +missing_count="$(psql "${READER_DATABASE_URL}" -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 From 78cb4a1412d69d530c75a7e59eace526bccd0e76 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 18:34:56 +0900 Subject: [PATCH 27/28] fix(foundation): seal document record artifacts --- manifest.json | 66 +++++++++++++++++++++++++--- scripts/foundation-contract-core.mjs | 9 ++++ tests/validate_repository.py | 9 ++++ 3 files changed, 78 insertions(+), 6 deletions(-) diff --git a/manifest.json b/manifest.json index f7b6cf55e..9a86e5412 100644 --- a/manifest.json +++ b/manifest.json @@ -135,6 +135,24 @@ "bytes": 12713, "lines": 260 }, + { + "path": "database/migrations/0021_document_record_persistence.sql", + "sha256": "e2dd9ca0c17141c2b3e06f64726f3ac0fa7cb1cd02798564cef72a12455a1286", + "bytes": 14001, + "lines": 334 + }, + { + "path": "database/migrations/0022_document_record_evidence_unique_keys.sql", + "sha256": "716960d224f1d2feabebc9302db93d7c2dd945978bbc806663eacc27f8606d73", + "bytes": 631, + "lines": 15 + }, + { + "path": "database/migrations/0023_document_record_canonical_encoding.sql", + "sha256": "9600bd071c39d904c041cf8037add92e582af577b2edc0de0f39801095dca195", + "bytes": 4364, + "lines": 87 + }, { "path": "docs/API_CONTRACT.md", "sha256": "63533dff785da62b89e585d742a158e2aeb05913644f2bf9fb6486f281c2e589", @@ -309,6 +327,12 @@ "bytes": 5365, "lines": 49 }, + { + "path": "docs/adr/0107-document-record-persistence.md", + "sha256": "04dcfa124d8cfcb2dc1527fbafaf82a217a0471f5aa27392e52ce5dfcea54810", + "bytes": 6448, + "lines": 48 + }, { "path": "docs/adr/README.md", "sha256": "f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002", @@ -321,6 +345,12 @@ "bytes": 6352, "lines": 69 }, + { + "path": "docs/doctoring/document-record-persistence-references.md", + "sha256": "e371afc5b15351682b66477e48654554704a62fa9db5fd134767d47ce8d88b36", + "bytes": 2038, + "lines": 23 + }, { "path": "docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md", "sha256": "b64f21abb19373e780db8b9e64deb8ba9a6219ccf9625a651f25407b8691fcbd", @@ -333,6 +363,12 @@ "bytes": 6237, "lines": 187 }, + { + "path": "docs/traceability/document-record-persistence.md", + "sha256": "3849ab6642f8b171aae6b379781fb19028635996428fbc37788334bd1feb167a", + "bytes": 4120, + "lines": 30 + }, { "path": "package.json", "sha256": "59ae9e3e67c3fba9320cb18439692395cdfd16ae5c24e3c4cf30d77d63ebabb5", @@ -359,9 +395,9 @@ }, { "path": "scripts/foundation-contract-core.mjs", - "sha256": "9b03efbbdffa60a05f5924e8a61b1cbc3cd75c502df428a5920085e8d0bf3603", - "bytes": 28121, - "lines": 688 + "sha256": "2305e1a6efff8b0dc47b40d517ac49b2a86f780899ae03c83d4063ed30d169e5", + "bytes": 28670, + "lines": 697 }, { "path": "scripts/foundation-contract.mjs", @@ -417,6 +453,24 @@ "bytes": 17811, "lines": 469 }, + { + "path": "tests/test_document_record_canonical_bytes_postgres.sh", + "sha256": "10b12f2f7afeb04fc04182170d2d00bca69b4f0f51c604ee565308b906d8c550", + "bytes": 8394, + "lines": 177 + }, + { + "path": "tests/test_document_record_evidence_unique_keys_postgres.sh", + "sha256": "d678b4ee7f1c69c16632f188ae8f7b5c717ecc55e49dc09d7889ebe02ed7212a", + "bytes": 5058, + "lines": 103 + }, + { + "path": "tests/test_document_record_persistence_postgres.sh", + "sha256": "f3d5fcb83a406ac202986f0272f673a8f3c6349752119d27b4d42e4c6bbe7072", + "bytes": 17015, + "lines": 376 + }, { "path": "tests/test_evidence_sealing_postgres.sh", "sha256": "57d16b632a0c60ffdcb4842ceb1cfe25d19c54cefeeefb622ff4fa6e83441ad7", @@ -467,9 +521,9 @@ }, { "path": "tests/validate_repository.py", - "sha256": "091836b2f68600a30b08f7da2cea8b3bef10201a123da720a7369bf10985eec2", - "bytes": 27237, - "lines": 637 + "sha256": "0692bfa17ebed3ba6594bc30e75bfeb8e93784ae381af3801457f4fe6cf41529", + "bytes": 27804, + "lines": 646 } ] } diff --git a/scripts/foundation-contract-core.mjs b/scripts/foundation-contract-core.mjs index 4aacefb3c..ac190292d 100644 --- a/scripts/foundation-contract-core.mjs +++ b/scripts/foundation-contract-core.mjs @@ -51,7 +51,10 @@ export const REQUIRED_FILES = Object.freeze([ 'docs/adr/0012-governed-migration-handoff.md', 'docs/adr/0013-governed-requisition-review-packet.md', 'docs/adr/0014-job-analysis-snapshot-persistence.md', + 'docs/adr/0107-document-record-persistence.md', 'docs/doctoring/REFERENCES.md', + 'docs/doctoring/document-record-persistence-references.md', + 'docs/traceability/document-record-persistence.md', 'docs/superpowers/specs/2026-08-15-orgmetra-foundation-design.md', 'docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md', 'database/migrations/0001_foundation_schema.sql', @@ -67,6 +70,9 @@ export const REQUIRED_FILES = Object.freeze([ 'database/migrations/0011_criterion_observation_scope.sql', 'database/migrations/0012_people_mutation_idempotency.sql', 'database/migrations/0013_job_analysis_snapshot.sql', + 'database/migrations/0021_document_record_persistence.sql', + 'database/migrations/0022_document_record_evidence_unique_keys.sql', + 'database/migrations/0023_document_record_canonical_encoding.sql', 'packages/hris-kernel/src/orgmetra_hris_kernel/audit.py', 'packages/hris-kernel/tests/test_audit_outbox.py', 'schemas/openapi.yaml', @@ -88,6 +94,9 @@ export const REQUIRED_FILES = Object.freeze([ 'tests/test_criterion_observation_scope_postgres.sh', 'tests/test_people_mutation_idempotency_postgres.sh', 'tests/test_job_analysis_snapshot_postgres.sh', + 'tests/test_document_record_canonical_bytes_postgres.sh', + 'tests/test_document_record_evidence_unique_keys_postgres.sh', + 'tests/test_document_record_persistence_postgres.sh', 'tests/validate_repository.py' ]); diff --git a/tests/validate_repository.py b/tests/validate_repository.py index d9d4c15a3..498974a01 100644 --- a/tests/validate_repository.py +++ b/tests/validate_repository.py @@ -54,7 +54,10 @@ "docs/adr/0012-governed-migration-handoff.md", "docs/adr/0013-governed-requisition-review-packet.md", "docs/adr/0014-job-analysis-snapshot-persistence.md", + "docs/adr/0107-document-record-persistence.md", "docs/doctoring/REFERENCES.md", + "docs/doctoring/document-record-persistence-references.md", + "docs/traceability/document-record-persistence.md", "docs/superpowers/specs/2026-08-15-orgmetra-foundation-design.md", "docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md", "database/migrations/0001_foundation_schema.sql", @@ -70,6 +73,9 @@ "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", + "database/migrations/0022_document_record_evidence_unique_keys.sql", + "database/migrations/0023_document_record_canonical_encoding.sql", "packages/hris-kernel/src/orgmetra_hris_kernel/audit.py", "packages/hris-kernel/tests/test_audit_outbox.py", "schemas/openapi.yaml", @@ -91,6 +97,9 @@ "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_canonical_bytes_postgres.sh", + "tests/test_document_record_evidence_unique_keys_postgres.sh", + "tests/test_document_record_persistence_postgres.sh", "tests/validate_repository.py", ] From 3e021ad104afe4163814ea0d2bfdaabd63ccaa7d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 18:40:42 +0900 Subject: [PATCH 28/28] docs(changelog): record provenance inventory repair --- CHANGELOG.md | 1 + manifest.json | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16454da3d..740af0752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ All notable changes to Orgmetra will be documented in this file. ### Changed +- Sealed the complete document-record persistence owner delta—migrations `0021`–`0023`, executable PostgreSQL contracts, ADR, doctoring, and traceability—in the paired Python/Node foundation inventories and deterministic manifest so stacked consumers fail closed on provenance drift. - Consolidated repository-owned PR validation from twelve workflows into one Foundation CI job, while keeping the dual-cluster recovery rehearsal separately path-scoped. Central required review and security workflows remain organization-owned. - New predictive-validity membership must use one normalized worker-level case; the three independent validity-study decision/evidence/outcome link relations are historical read surfaces only and can no longer accept new rows. A case insert also rejects a criterion observation whose recorded interval is already closed at `linked_at`. - Canonicalized service identifiers as two-or-more-word `snake_case` across architecture, deployment, ACL, metrics, and client contracts. diff --git a/manifest.json b/manifest.json index 9a86e5412..acc10b373 100644 --- a/manifest.json +++ b/manifest.json @@ -29,9 +29,9 @@ }, { "path": "CHANGELOG.md", - "sha256": "f2d2e0b488c0440533effa821808f2f17e37d92f8fb586174c2fdb594f760ca5", - "bytes": 17539, - "lines": 77 + "sha256": "321c43f388dd561b8867684daac74b0c21f3676d66e15f941feed28d5cf02459", + "bytes": 17829, + "lines": 78 }, { "path": "CLAUDE.md",