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)" diff --git a/database/migrations/0021_document_record_persistence.sql b/database/migrations/0021_document_record_persistence.sql new file mode 100644 index 000000000..868ed36a8 --- /dev/null +++ b/database/migrations/0021_document_record_persistence.sql @@ -0,0 +1,334 @@ +-- 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. + +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, + 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, + canonical_evidence_json text 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_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 + 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. 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 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 + 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 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 public.enforce_document_record_system_time(); + +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; + evidence_key_count integer; + computed_evidence_digest text; + evidence_received_at timestamptz; + evidence_recorded_at timestamptz; +BEGIN + computed_evidence_digest := encode( + public.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 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 public.validate_document_record_evidence_binding(); + +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' + USING ERRCODE = '55000'; +END; +$$; + +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 public.protect_document_record_immutability(); + +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' + USING ERRCODE = '55000'; +END; +$$; + +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 public.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 = public.current_tenant_record_id() +) +WITH CHECK ( + 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 new file mode 100644 index 000000000..3660b9cec --- /dev/null +++ b/docs/adr/0107-document-record-persistence.md @@ -0,0 +1,40 @@ +# 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. + +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: + +- 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; +- 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 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. + +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. + +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. 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. diff --git a/docs/traceability/document-record-persistence.md b/docs/traceability/document-record-persistence.md new file mode 100644 index 000000000..05553803c --- /dev/null +++ b/docs/traceability/document-record-persistence.md @@ -0,0 +1,27 @@ +# 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 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`; 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 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` | + +## 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. diff --git a/manifest.json b/manifest.json index 00c4368e9..33baebaac 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":"ad6219494fe7c7ff602ba79e6cb2534d7e35c17e0f16be0f1bef5bcd8911f91b","bytes":17636,"lines":77},{"path":"CLAUDE.md","sha256":"add33884f466d324e20875388d103de41c6e062938a6e98727dc83a87ffe976f","bytes":1229,"lines":20},{"path":"LICENSE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30","bytes":11358,"lines":202},{"path":"NOTICE","sha256":"34b4618e946bdd8d33407d6ac5279f0a0388f5e7c8f79d2e7d8c3c47d0266042","bytes":305,"lines":4},{"path":"README.md","sha256":"1a9fc400d26d8137ae5911488794a6d3fa915957c95f27b36a48cef0fdf823c6","bytes":3785,"lines":81},{"path":"database/migrations/0001_foundation_schema.sql","sha256":"ce2ae52fc66b2f99597ea5285df82c66f90caa46174fef4930d68a8b6177d0dd","bytes":38747,"lines":916},{"path":"database/migrations/0002_sealed_evidence_digest.sql","sha256":"93d659ca8e0e9293a83d5422d043be7b1022c5470a5b22670aa3416fa334a04c","bytes":6649,"lines":202},{"path":"database/migrations/0003_audit_outbox_persistence.sql","sha256":"2aa7bbb8220923ec584537c0cd46f0cba2b692d69d431f097b7df6db75235bfc","bytes":15417,"lines":423},{"path":"database/migrations/0004_outbox_delivery_claim.sql","sha256":"d4504acf7d58528a2a8f4f03d1584b868c8d3ba9046a007b9c2e7cfef993b2ef","bytes":9451,"lines":234},{"path":"database/migrations/0005_outbox_delivery_finalization.sql","sha256":"b7e8790595b288f752d6ef5cc6cbfe4e1b6712248f5b7a3a25fa60016b6a4961","bytes":6125,"lines":170},{"path":"database/migrations/0006_outbox_delivery_dead_letter.sql","sha256":"c1fb91cdf98169fd6684984e86cb0a14fa19c8f1226028d2346a2a069df2b3c7","bytes":24919,"lines":628},{"path":"database/migrations/0007_outbox_retry_exhaustion.sql","sha256":"812f50d70ca5929c7eba964d34a208aedee660d11cc7ffc09d67688c4737e0d5","bytes":19081,"lines":476},{"path":"database/migrations/0008_audit_outbox_review_hardening.sql","sha256":"c3713a12db9d00fdc10005df1f86c07965e9555eefad78ca67e994537a739d9b","bytes":17562,"lines":448},{"path":"database/migrations/0009_candidate_worker_conversion_governance.sql","sha256":"4030666629a6b8deb383b8337ead4f09d6a945969313def2577a38f31f06cda9","bytes":11537,"lines":281},{"path":"database/migrations/0010_validity_study_case_integrity.sql","sha256":"3f594810ac9e1a6747a2bb4838e5ce65b921cb6e3d36fcdc3ff08b4a7579ebd1","bytes":11979,"lines":313},{"path":"database/migrations/0011_criterion_observation_scope.sql","sha256":"f9fe7c35f1ee7b167e1c2ba75a50a84febda9a6ccf8123b4f5726f51968694f9","bytes":7444,"lines":165},{"path":"database/migrations/0012_people_mutation_idempotency.sql","sha256":"52dbbb9ec7f9be5291593ba88f228d7fffd736dcb99547a08c1d6cad076afb69","bytes":3162,"lines":76},{"path":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"path":"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":"ad6219494fe7c7ff602ba79e6cb2534d7e35c17e0f16be0f1bef5bcd8911f91b","bytes":17636,"lines":77},{"path":"CLAUDE.md","sha256":"add33884f466d324e20875388d103de41c6e062938a6e98727dc83a87ffe976f","bytes":1229,"lines":20},{"path":"LICENSE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30","bytes":11358,"lines":202},{"path":"NOTICE","sha256":"34b4618e946bdd8d33407d6ac5279f0a0388f5e7c8f79d2e7d8c3c47d0266042","bytes":305,"lines":4},{"path":"README.md","sha256":"1a9fc400d26d8137ae5911488794a6d3fa915957c95f27b36a48cef0fdf823c6","bytes":3785,"lines":81},{"path":"database/migrations/0001_foundation_schema.sql","sha256":"ce2ae52fc66b2f99597ea5285df82c66f90caa46174fef4930d68a8b6177d0dd","bytes":38747,"lines":916},{"path":"database/migrations/0002_sealed_evidence_digest.sql","sha256":"93d659ca8e0e9293a83d5422d043be7b1022c5470a5b22670aa3416fa334a04c","bytes":6649,"lines":202},{"path":"database/migrations/0003_audit_outbox_persistence.sql","sha256":"2aa7bbb8220923ec584537c0cd46f0cba2b692d69d431f097b7df6db75235bfc","bytes":15417,"lines":423},{"path":"database/migrations/0004_outbox_delivery_claim.sql","sha256":"d4504acf7d58528a2a8f4f03d1584b868c8d3ba9046a007b9c2e7cfef993b2ef","bytes":9451,"lines":234},{"path":"database/migrations/0005_outbox_delivery_finalization.sql","sha256":"b7e8790595b288f752d6ef5cc6cbfe4e1b6712248f5b7a3a25fa60016b6a4961","bytes":6125,"lines":170},{"path":"database/migrations/0006_outbox_delivery_dead_letter.sql","sha256":"c1fb91cdf98169fd6684984e86cb0a14fa19c8f1226028d2346a2a069df2b3c7","bytes":24919,"lines":628},{"path":"database/migrations/0007_outbox_retry_exhaustion.sql","sha256":"812f50d70ca5929c7eba964d34a208aedee660d11cc7ffc09d67688c4737e0d5","bytes":19081,"lines":476},{"path":"database/migrations/0008_audit_outbox_review_hardening.sql","sha256":"c3713a12db9d00fdc10005df1f86c07965e9555eefad78ca67e994537a739d9b","bytes":17562,"lines":448},{"path":"database/migrations/0009_candidate_worker_conversion_governance.sql","sha256":"4030666629a6b8deb383b8337ead4f09d6a945969313def2577a38f31f06cda9","bytes":11537,"lines":281},{"path":"database/migrations/0010_validity_study_case_integrity.sql","sha256":"3f594810ac9e1a6747a2bb4838e5ce65b921cb6e3d36fcdc3ff08b4a7579ebd1","bytes":11979,"lines":313},{"path":"database/migrations/0011_criterion_observation_scope.sql","sha256":"f9fe7c35f1ee7b167e1c2ba75a50a84febda9a6ccf8123b4f5726f51968694f9","bytes":7444,"lines":165},{"path":"database/migrations/0012_people_mutation_idempotency.sql","sha256":"52dbbb9ec7f9be5291593ba88f228d7fffd736dcb99547a08c1d6cad076afb69","bytes":3162,"lines":76},{"path":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"path":"database/migrations/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 new file mode 100644 index 000000000..eccefb64e --- /dev/null +++ b/tests/test_document_record_persistence_postgres.sh @@ -0,0 +1,336 @@ +#!/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 + 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" +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_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" +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)" + 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_REFERENCE}', '${EMPLOYMENT_REFERENCE}', '${UPLOADER}', '${PERSISTED_BY}', + 'policy_acknowledgement', + 'document_artifact:00000000-0000-4000-8000-000000000042', + '${ARTIFACT_DIGEST}', '${SOURCE_DIGEST}', '${RETENTION_REFERENCE}', + '${RETENTION_DIGEST}', TIMESTAMPTZ '${RECEIVED_AT}', '{}', + '${EVIDENCE_DIGEST}', + 'audit_event:00000000-0000-4000-8000-000000000081', + 'outbox_event:00000000-0000-4000-8000-000000000082', + '${APPLICATION_DIGEST}', 'document_record_persist', 'reviewed_document_metadata', + 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_REFERENCE}', '${EMPLOYMENT_REFERENCE}', '${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}', + 'audit_event:00000000-0000-4000-8000-000000000083', + 'outbox_event:00000000-0000-4000-8000-000000000084', + '${APPLICATION_DIGEST}', 'document_record_persist', 'reviewed_document_metadata' + );" + +expect_failure \ + "document record accepted an unreviewed application reason" \ + "application_reason_code" \ + "INSERT INTO document_record (${columns}) VALUES ( + '${TENANT_ID}', '00000000-0000-7000-8000-000000000034', + 'document_record:00000000-0000-4000-8000-000000000034', + '${PERSON_REFERENCE}', '${EMPLOYMENT_REFERENCE}', '${UPLOADER}', '${PERSISTED_BY}', + 'qualification_document', + 'document_artifact:00000000-0000-4000-8000-000000000044', + '${ARTIFACT_DIGEST}', '${SOURCE_DIGEST}', '${RETENTION_REFERENCE}', + '${RETENTION_DIGEST}', TIMESTAMPTZ '${RECEIVED_AT}', '{}', + '${EVIDENCE_DIGEST}', + 'audit_event:00000000-0000-4000-8000-000000000085', + 'outbox_event:00000000-0000-4000-8000-000000000086', + '${APPLICATION_DIGEST}', 'document_record_persist', 'unrelated_change' + );" + +expect_failure \ + "document record accepted a non-opaque Person reference" \ + "person_record_reference" \ + "INSERT INTO document_record (${columns}) VALUES ( + '${TENANT_ID}', '00000000-0000-7000-8000-000000000035', + 'document_record:00000000-0000-4000-8000-000000000035', + 'person_record:employee@example.com', '${EMPLOYMENT_REFERENCE}', '${UPLOADER}', '${PERSISTED_BY}', + 'qualification_document', + 'document_artifact:00000000-0000-4000-8000-000000000045', + '${ARTIFACT_DIGEST}', '${SOURCE_DIGEST}', '${RETENTION_REFERENCE}', + '${RETENTION_DIGEST}', TIMESTAMPTZ '${RECEIVED_AT}', '{}', + '${EVIDENCE_DIGEST}', + 'audit_event:00000000-0000-4000-8000-000000000087', + 'outbox_event:00000000-0000-4000-8000-000000000088', + '${APPLICATION_DIGEST}', 'document_record_persist', 'reviewed_document_metadata' + );" + +mismatch_reference="document_record:00000000-0000-4000-8000-000000000036" +mismatch_artifact="document_artifact:00000000-0000-4000-8000-000000000046" +mismatch_evidence="$(python3 - <&1 <&2 + exit 1 +fi + +expect_failure \ + "document metadata was rewriteable" \ + "immutable" \ + "UPDATE document_record SET document_category_code = 'qualification_document' + WHERE document_record_id = '${DOCUMENT_ID}'::uuid;" +expect_failure \ + "document metadata was deletable" \ + "immutable" \ + "DELETE FROM document_record WHERE document_record_id = '${DOCUMENT_ID}'::uuid;" +expect_failure \ + "document metadata could be truncated" \ + "cannot be truncated" \ + "TRUNCATE document_record;" + +for forbidden_column in document_bytes document_title free_form_notes compensation_value rating_value credential_value; do + count="$(psql "${DATABASE_URL}" -Atqc " + SELECT count(*) FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = 'document_record' + AND column_name = '${forbidden_column}';")" + if [[ "${count}" != "0" ]]; then + echo "document persistence introduced prohibited value-bearing column: ${forbidden_column}" >&2 + exit 1 + fi +done + +foreign_app_fk_count="$(psql "${DATABASE_URL}" -Atqc " +SELECT count(*) +FROM pg_constraint AS constraint_record +JOIN pg_class AS relation ON relation.oid = constraint_record.conrelid +JOIN pg_class AS target_relation ON target_relation.oid = constraint_record.confrelid +WHERE relation.relname = 'document_record' + AND constraint_record.contype = 'f' + AND target_relation.relname IN ( + 'person_record', 'employment_record', 'audit_event_record', 'outbox_delivery_record' + );")" +if [[ "${foreign_app_fk_count}" != "0" ]]; then + echo "document persistence introduced a direct cross-service application-table dependency" >&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 + +trusted_search_path_count="$(psql "${DATABASE_URL}" -Atqc " +SELECT count(*) +FROM pg_proc AS procedure_record +JOIN pg_namespace AS namespace_record + ON namespace_record.oid = procedure_record.pronamespace +WHERE namespace_record.nspname = 'public' + AND procedure_record.proname IN ( + 'enforce_document_record_system_time', + 'validate_document_record_evidence_binding', + 'protect_document_record_immutability', + 'reject_document_record_truncate' + ) + AND procedure_record.proconfig @> 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 + 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" 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", ]