-
Notifications
You must be signed in to change notification settings - Fork 0
feat(document-records): persist immutable HR document metadata #107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
seonghobae
wants to merge
31
commits into
feat/document-record-evidence
Choose a base branch
from
feat/document-record-persistence
base: feat/document-record-evidence
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
fc422cf
test(document-records): define durable metadata persistence contract
seonghobae e3eb7d8
test(document-records): run durable persistence contract
seonghobae f29459a
test(document-records): keep foreign HR and audit ownership opaque
seonghobae fa1cbd0
feat(document-records): persist immutable governed metadata
seonghobae 6f59031
docs(document-records): record persistence ownership decision
seonghobae f42af76
docs(document-records): trace durable metadata controls
seonghobae ac55b4d
docs(document-records): record primary persistence references
seonghobae 59f2d82
test(document-records): bind persisted metadata to exact evidence bytes
seonghobae 4a5f76f
fix(document-records): bind persisted metadata to canonical evidence
seonghobae 799a126
docs(document-records): bind row to canonical evidence snapshot
seonghobae fcce901
docs(document-records): trace canonical evidence binding
seonghobae 5e521fd
fix(document-records): resolve pgcrypto digest in extension schema
seonghobae d62e72d
fix(document-records): harden persistence migration boundaries
seonghobae 78e67a0
Merge remote-tracking branch 'origin/feat/document-record-evidence' i…
seonghobae a8ca94b
merge(document-records): adopt current evidence parent
seonghobae 6592288
merge(document-records): adopt current evidence authority
seonghobae 4dfb839
test(document-records): reject duplicate canonical evidence keys
seonghobae 74933e2
fix(document-records): reject duplicate evidence keys
seonghobae b69f9a4
test(document-records): admit unique-key hardening migration
seonghobae 55ef5e7
docs(document-records): trace unique-key JSON authority
seonghobae 48eac76
docs(document-records): current persistence traceability
seonghobae dd3b969
docs(document-records): bind canonical JSON uniqueness
seonghobae 7fec046
test(document-records): reject noncanonical evidence bytes
seonghobae 2057fa3
fix(document-records): bind deterministic canonical evidence bytes
seonghobae 27c30b6
test(document-records): wire canonical-byte persistence repair
seonghobae 8221e11
test(document-records): assert duplicate-key constraint evidence
seonghobae 9374061
docs(document-records): require deterministic evidence encoding
seonghobae f564566
test(document-records): prove canonical bytes accepted
seonghobae 7ce73aa
test(document-records): preserve configured RLS database endpoint
seonghobae 78cb4a1
fix(foundation): seal document record artifacts
seonghobae 3e021ad
docs(changelog): record provenance inventory repair
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
334 changes: 334 additions & 0 deletions
334
database/migrations/0021_document_record_persistence.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
15 changes: 15 additions & 0 deletions
15
database/migrations/0022_document_record_evidence_unique_keys.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.