diff --git a/.github/workflows/document-record-evidence-quality.yml b/.github/workflows/document-record-evidence-quality.yml new file mode 100644 index 000000000..5ecfe621f --- /dev/null +++ b/.github/workflows/document-record-evidence-quality.yml @@ -0,0 +1,74 @@ +name: Document Record Evidence Quality + +on: + pull_request: + branches: + - develop + paths: + - "packages/document-record-evidence/**" + - ".github/requirements/foundation-test.txt" + - ".github/workflows/document-record-evidence-quality.yml" + - "docs/adr/0098-governed-document-record-evidence.md" + - "docs/doctoring/document-record-evidence-references.md" + - "docs/traceability/document-record-evidence.md" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: document-record-evidence-quality-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + unit: + name: Document record evidence contract and 100% coverage + runs-on: ubuntu-latest + timeout-minutes: 10 + 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: Set up exact Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14.7" + check-latest: false + - name: Install reviewed test and build toolchain + run: | + python -m pip install --require-hashes --no-deps --only-binary=:all: -r .github/requirements/foundation-test.txt + printf '%s\n' 'setuptools==84.0.0 --hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670' > /tmp/orgmetra-document-record-build.txt + python -m pip install --require-hashes --no-deps --only-binary=:all: -r /tmp/orgmetra-document-record-build.txt + python -m pip check + - name: Compile package and tests + run: python -m compileall -q packages/document-record-evidence/src packages/document-record-evidence/tests + - name: Build and install exact package artifact + run: | + rm -rf /tmp/orgmetra-document-record-build-src /tmp/orgmetra-document-record-dist /tmp/orgmetra-document-record-venv + cp -a packages/document-record-evidence /tmp/orgmetra-document-record-build-src + mkdir -p /tmp/orgmetra-document-record-dist + python -m pip wheel --no-deps --no-build-isolation --wheel-dir /tmp/orgmetra-document-record-dist /tmp/orgmetra-document-record-build-src + test "$(find /tmp/orgmetra-document-record-dist -maxdepth 1 -type f -name '*.whl' | wc -l)" -eq 1 + python -m venv /tmp/orgmetra-document-record-venv + /tmp/orgmetra-document-record-venv/bin/python -m pip install --require-hashes --no-deps --only-binary=:all: -r "$GITHUB_WORKSPACE/.github/requirements/foundation-test.txt" + wheel_path="$(find /tmp/orgmetra-document-record-dist -maxdepth 1 -type f -name '*.whl' -print -quit)" + wheel_sha="$(sha256sum "$wheel_path" | awk '{print $1}')" + printf 'orgmetra-document-record-evidence[test] @ file://%s --hash=sha256:%s\n' "$wheel_path" "$wheel_sha" > /tmp/orgmetra-document-record-install.txt + /tmp/orgmetra-document-record-venv/bin/python -m pip install --require-hashes --no-deps -r /tmp/orgmetra-document-record-install.txt + /tmp/orgmetra-document-record-venv/bin/python -m pip check + - name: Test installed artifact with exact statement and branch coverage + env: + COVERAGE_FILE: /tmp/orgmetra-document-record.coverage + run: | + cd /tmp + /tmp/orgmetra-document-record-venv/bin/python -m pytest -c "$GITHUB_WORKSPACE/packages/document-record-evidence/pyproject.toml" "$GITHUB_WORKSPACE/packages/document-record-evidence/tests" + - name: Require clean checkout + run: | + git diff --exit-code + test -z "$(git status --porcelain)" diff --git a/CHANGELOG.md b/CHANGELOG.md index 99f4752d7..10bae6906 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to Orgmetra will be documented in this file. - Accepted ADRs 0001–0003 now include buyer-facing Context, Decision, and Consequences grounded in verified ISO 30400:2022, ISO 30414:2025, Uniform Guidelines (29 C.F.R. Part 1607), SIOP (2018), OpenAPI Specification v3.2.0, OpenID Connect Core 1.0 errata set 2, CloudEvents v1.0.2, Jensen and Snodgrass (1999), Snodgrass (1999), and Allen (1983) records already listed in `docs/doctoring/REFERENCES.md`. ADRs 0004 and 0005 gained APA 7th References pointers to that same bibliography without changing their Decision bodies. - Active-PR governed Job Analysis persistence/API on the canonical `JobAnalysisSnapshot` model: migration `0013_job_analysis_snapshot.sql` stores immutable tenant-scoped snapshot, Task, KSAO, Task–KSAO, FJA and write-command evidence; `POST /v1/tenants/{tenant_record_id}/job-analysis-snapshots` and matching GET enforce purpose-bound Keyverse scope, authenticated-principal actor authority, bounded/strict JSON handling, transactional Idempotency-Key serialization, parent-scope fail-closed integrity, forced RLS, and atomic audit/outbox evidence. ADR 0014 records the persistence decision while ADR 0007 remains the domain/evidence authority; validated evidence still requires accountable human review and non-LLM provenance, and the service does not make a high-impact employment decision. - Active-PR `orgmetra_selection_review` packet for PII-minimized, evidence-bound human selection review: canonical operational tenant identity, UUID-backed opaque candidate/Job/sealed-evidence/reviewer references, explicit purpose/reason/evidence version, deterministic canonical JSON and SHA-256 correlation, mandatory human decision state, redacted packet repr, and provenance-paired model evidence that remains `untrusted_draft`, with exact 100% owned statement and branch coverage required by its quality gate. +- Active-PR `DocumentRecordEvidence` for value-minimized HR document metadata: tenant/Person/Employment scope, reviewed category, opaque artifact and provenance/retention digests, separate business receipt and Orgmetra-generated system-recorded time, process-local tamper detection, and no document content or employment-decision authority. - Active performance-criterion scope hardening: `criterion_observation_scope_guard` rejects criterion outcomes for a Job the worker did not effectively hold at the observation date, observations before the relevant assignment, and observations outside the referenced performance cycle while preserving valid multiple-assignment cases and existing bitemporal correction semantics. The guard evaluates current-recorded facts, derives the date coordinate from `observed_at` in UTC so session `TimeZone` cannot alter the result, uses a trusted function search path, and adds no PII or automated employment decision authority. The Foundation PostgreSQL contract also rejects a closed `recorded_to` on each time-coordinate lookup and proves UTC midnight plus non-UTC session `TimeZone` boundaries. - Bitemporal tenant-scoped organization hierarchy validation that rejects visible indirect parent cycles and reuses single-valued recorded-time reconstruction before graph traversal. - Stacked governed job-analysis evidence contract via `JobAnalysisSnapshot`, `TaskEvidence`, `KSAORequirement`, `TaskKSAOLink`, `FunctionalJobAnalysisProfile`, and `EvidenceSource`: tenant/Job-scoped observable tasks, explicit Task-to-KSAO linkage, importance/difficulty/proficiency ratings, source/version/retrieval/SHA-256 provenance, deterministic canonical snapshot bytes, current O*NET evidence support, and historical DOT Data/People/Things compatibility. Validated snapshots require accountable human review and complete non-LLM evidence; LLM-origin material remains `analysis_draft`, and the snapshot is evidence input rather than a hiring, promotion, termination, compensation, or other high-impact employment decision. diff --git a/docs/adr/0098-governed-document-record-evidence.md b/docs/adr/0098-governed-document-record-evidence.md new file mode 100644 index 000000000..f5f7324a6 --- /dev/null +++ b/docs/adr/0098-governed-document-record-evidence.md @@ -0,0 +1,29 @@ +# ADR 0098: Governed HR document-record evidence + +**Status: Proposed** + +## Context + +Orgmetra's accepted architecture assigns document metadata, source evidence and immutable artifact references to `document_records`. Protected `develop` has no executable value object for that boundary, creating a commercial diligence gap: a buyer cannot yet point to code that binds an HR document artifact to HR scope, provenance, retention-policy evidence and system-recorded time without copying content into a second evidence store. + +## Decision + +Add a transport-neutral `DocumentRecordEvidence` value that binds tenant, Person, Employment, pseudonymous uploader correlation, one reviewed document category, an opaque artifact reference plus SHA-256 digest, source-provenance SHA-256, retention-policy reference/digest, caller-owned business receipt time, and Orgmetra-generated document-record identity/system-recorded time. + +Canonical evidence is classified `restricted_hr`, records only `artifact_reference_only`, and is explicitly `not_authorized_for_employment_decision`. The initial category vocabulary is closed. Document bytes, titles, free-form notes, credentials, compensation, ratings and unrelated HR values are excluded. + +`received_at` and `recorded_at` remain distinct: receipt time is business/event time supplied by the source boundary; recorded time is generated by Orgmetra at evidence issuance and cannot precede receipt time. + +A process-local weak issuance registry stores the creation-time canonical evidence digest outside packet-writable slots. Canonical export validates the live fields, snapshots the payload once, compares that exact snapshot against the creation digest, and returns only the verified snapshot. This rejects valid-value `object.__setattr__` rewriting after issuance without pretending that an in-process Python object is durable cryptographic evidence. + +## Consequences + +- Document metadata can be correlated to immutable artifacts and provenance without duplicating content. +- The packet does not authorize storage access, export, retention/disposition or an employment decision. +- Process-local tamper detection is defense in depth only; durable immutability, uniqueness, access control and audit ordering remain responsibilities of authoritative `document_records`/`audit_provenance` persistence. +- Clearfolio and NewsDOM remain read-only external dependencies through published contracts; this ADR creates no cross-service application-table access. +- W3C PROV-O and NIST Privacy Framework 1.0 are design inputs only; no conformance or certification claim is made. + +## Follow-up + +A later bounded slice may add authoritative 3NF document metadata persistence, object-store authorization, lifecycle reconciliation, malware/content validation and governed preview/OCR adapters. Those changes must preserve tenant isolation, purpose-bound access, retention/legal-hold controls and immutable audit/outbox evidence. diff --git a/docs/doctoring/document-record-evidence-references.md b/docs/doctoring/document-record-evidence-references.md new file mode 100644 index 000000000..d24f326de --- /dev/null +++ b/docs/doctoring/document-record-evidence-references.md @@ -0,0 +1,17 @@ +# Document-record evidence references + +Reviewed 2026-08-23. + +## Primary standards inputs + +World Wide Web Consortium. (2013, April 30). *PROV-O: The PROV Ontology* (W3C Recommendation). https://www.w3.org/TR/prov-o/ + +PROV-O is used as a provenance-model design input: Orgmetra preserves explicit artifact and source-provenance correlation so evidence can later be mapped into a broader provenance graph. This slice does **not** claim PROV-O serialization conformance. + +National Institute of Standards and Technology. (2020, January). *NIST Privacy Framework: A tool for improving privacy through enterprise risk management, Version 1.0*. https://www.nist.gov/privacy-framework/privacy-framework + +NIST Privacy Framework 1.0 is used as a privacy-risk design input for data minimization and governed processing. At the review date, NIST's site separately presents Privacy Framework 1.1 as an Initial Public Draft; this document therefore does not mislabel 1.1 as a final standard and does not claim NIST certification or conformity. + +## Orgmetra interpretation + +The evidence contract stores document metadata, opaque correlations and SHA-256 provenance while excluding document bytes, titles, free-form notes, credentials and unrelated HR values. Retention-policy evidence is bound but no universal statutory retention period is encoded. Authoritative access, export, retention/disposition and employment decisions remain separate human-accountable Orgmetra boundaries. diff --git a/docs/traceability/document-record-evidence.md b/docs/traceability/document-record-evidence.md new file mode 100644 index 000000000..09bf9b015 --- /dev/null +++ b/docs/traceability/document-record-evidence.md @@ -0,0 +1,26 @@ +# Document-record evidence traceability + +## State + +- **Protected-main truth:** `develop@9e3e4847510e1e612b48474ba42b177b8ed824df` defines `document_records` as the owner of document metadata, source evidence and immutable artifact references, but has no executable document-record evidence package. +- **Active PR truth:** PR #98 adds a transport-neutral, value-minimized document metadata evidence contract. It is not protected-main/shipped truth until merged. +- **Out of scope:** document bytes/object-store implementation, Clearfolio/NewsDOM mutations, OCR, preview generation, legal retention decisions, content export, and employment-decision authority. + +## Requirement → executable evidence + +| Requirement | Evidence | +|---|---| +| Tenant, Person and Employment scope is explicit | `test_builds_value_minimized_document_evidence` plus malformed-reference regressions | +| Document content and HR values are not copied into canonical governance evidence | `test_builds_value_minimized_document_evidence` | +| Record identity and system-recorded time are Orgmetra-generated | `test_generates_packet_owned_reference_and_system_time` | +| Artifact, provenance and retention-policy integrity are SHA-256-bound | malformed digest regressions plus canonical digest assertion | +| Caller business receipt time remains distinct from system-recorded time | future/non-UTC receipt-time regressions | +| A valid-value rewrite after issuance cannot emit a second canonical truth | `test_rejects_valid_value_rewrite_after_issuance`; process-local creation digest is stored outside packet-writable slots and the verified payload snapshot is reused for export | +| Owned production statement and branch coverage are exact 100% | `Document Record Evidence Quality` workflow | +| Installed package, not source-tree import, is tested | exact-head hash-bound wheel build/install in `Document Record Evidence Quality` | + +The process-local issuance digest is defense in depth only. It is not a durable signature, MAC key-management system, or substitute for database uniqueness/immutability. Durable evidence systems persist the already-emitted canonical bytes and digest through authoritative `document_records` and immutable `audit_provenance`/outbox persistence. + +## Next authoritative boundary + +Before any content read, export, disposition or high-impact HR action, the owning Orgmetra service must freshly re-resolve tenant, actor, purpose, resource, artifact digest/provenance, retention/legal-hold state and human authority, then write immutable audit/outbox evidence atomically with any consequential operation. diff --git a/manifest.json b/manifest.json index 97f2bab14..00c4368e9 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":"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}]} diff --git a/packages/document-record-evidence/CHANGELOG.md b/packages/document-record-evidence/CHANGELOG.md new file mode 100644 index 000000000..8208c22dc --- /dev/null +++ b/packages/document-record-evidence/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## Unreleased + +- Add value-minimized HR document-record metadata evidence with Orgmetra-generated record identity and system-recorded time. +- Bind artifact integrity, source provenance and retention-policy evidence without copying document content or HR field values. +- Reject valid-value post-issuance rewrites by comparing each export against a process-local creation-time canonical evidence digest; durable immutability remains owned by authoritative audit/outbox persistence. +- Add an exact-head, installed-wheel quality lane with 100% owned statement/branch coverage requirement. diff --git a/packages/document-record-evidence/README.md b/packages/document-record-evidence/README.md new file mode 100644 index 000000000..7f137e070 --- /dev/null +++ b/packages/document-record-evidence/README.md @@ -0,0 +1,15 @@ +# Orgmetra document-record evidence + +This package creates **value-minimized metadata evidence** for an HR document artifact. It is intended for the `document_records` bounded context described by Orgmetra's accepted architecture. + +A `DocumentRecordEvidence` binds one tenant, Person, Employment, uploader correlation, reviewed document category, immutable artifact reference and SHA-256 digest, source-provenance digest, retention-policy reference/digest, the business receipt time, and a system-generated recorded time. The evidence intentionally contains **no document bytes, document title, free-form notes, credentials, compensation, rating, or other HR field values**. + +The packet is not a storage credential, legal retention determination, or employment-decision authorization. Before content access, export, retention/disposition, or a high-impact HR action, the authoritative Orgmetra host must re-resolve tenant/actor/purpose/resource scope, retention/legal-hold state, artifact integrity and human authority, then persist immutable audit/outbox evidence through the owning service. + +`document_record_reference` and `recorded_at` are generated inside the Orgmetra issuance boundary so callers cannot claim a chosen system-recorded identity or timestamp. Caller-owned `received_at` remains separate business-event time and must be exact built-in UTC and no later than issuance. + +The Python value also keeps its creation-time canonical evidence digest in a process-local weak issuance registry outside packet-writable slots. Every export validates the live fields, snapshots them once, and compares that exact snapshot with the issuance digest before returning document or JSON evidence. This is **defense in depth against accidental or same-process post-construction rewriting**, not durable cryptographic attestation: authoritative persistence must store the already-emitted canonical evidence and digest through Orgmetra's immutable audit/outbox boundary. + +The closed initial document-category vocabulary is `employment_contract`, `policy_acknowledgement`, and `qualification_document`. New categories require a reviewed contract change rather than free-form metadata. + +This package does not fetch or mutate Clearfolio, NewsDOM, or any other dedicated-writer CWL service and introduces no cross-service application-table SQL. diff --git a/packages/document-record-evidence/pyproject.toml b/packages/document-record-evidence/pyproject.toml new file mode 100644 index 000000000..53aa9b75a --- /dev/null +++ b/packages/document-record-evidence/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["setuptools>=69"] +build-backend = "setuptools.build_meta" + +[project] +name = "orgmetra-document-record-evidence" +version = "0.1.0" +description = "Value-minimized HR document-record evidence for Orgmetra." +requires-python = ">=3.14.7,<3.15" + +[project.optional-dependencies] +test = ["pytest>=8.3", "pytest-cov>=5.0"] + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = [ + "--cov=orgmetra_document_record_evidence", + "--cov-branch", + "--cov-report=term-missing", + "--cov-fail-under=100", +] diff --git a/packages/document-record-evidence/src/orgmetra_document_record_evidence/__init__.py b/packages/document-record-evidence/src/orgmetra_document_record_evidence/__init__.py new file mode 100644 index 000000000..19f80c5bd --- /dev/null +++ b/packages/document-record-evidence/src/orgmetra_document_record_evidence/__init__.py @@ -0,0 +1,8 @@ +"""Public Orgmetra HR document-record evidence contract.""" + +from orgmetra_document_record_evidence.evidence import ( + DocumentRecordEvidence, + build_document_record_evidence, +) + +__all__ = ["DocumentRecordEvidence", "build_document_record_evidence"] diff --git a/packages/document-record-evidence/src/orgmetra_document_record_evidence/evidence.py b/packages/document-record-evidence/src/orgmetra_document_record_evidence/evidence.py new file mode 100644 index 000000000..65497990f --- /dev/null +++ b/packages/document-record-evidence/src/orgmetra_document_record_evidence/evidence.py @@ -0,0 +1,242 @@ +"""Value-minimized evidence for one HR document artifact. + +This module records document metadata and provenance only. It deliberately does +not carry document bytes, titles, free-form notes, credentials, HR field values, +or employment-decision authority. Durable immutability and authorization belong +to Orgmetra's authoritative document-records and audit/outbox persistence. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import datetime, timezone +from hashlib import sha256 +import hmac +import json +import re +from threading import RLock +from typing import Any +from uuid import UUID, uuid4 +from weakref import WeakKeyDictionary + +_DIGEST_PATTERN = re.compile(r"^[0-9a-f]{64}$") +_ALLOWED_DOCUMENT_CATEGORIES = frozenset( + {"employment_contract", "policy_acknowledgement", "qualification_document"} +) +_CLASSIFICATION_CODE = "restricted_hr" +_CONTENT_STORAGE_STATE = "artifact_reference_only" +_DECISION_AUTHORITY_STATE = "not_authorized_for_employment_decision" +_SCHEMA_VERSION = "orgmetra.document_record_evidence.v1" +_ISSUANCE_LOCK = RLock() +_ISSUANCE_DIGESTS: WeakKeyDictionary[DocumentRecordEvidence, str] + + +def _validate_operational_uuid(value: str, field_name: str) -> None: + """Require exact canonical non-sentinel UUID text without fixing its version.""" + if type(value) is not str: + raise ValueError(f"{field_name} must be canonical UUID text") + try: + parsed = UUID(value) + except (ValueError, AttributeError, TypeError) as exc: + raise ValueError(f"{field_name} must be canonical UUID text") from exc + if str(parsed) != value or parsed.int in (0, (1 << 128) - 1): + raise ValueError(f"{field_name} must be a canonical operational UUID") + + +def _validate_uuid4_reference(value: str, prefix: str, field_name: str) -> None: + """Require one bounded namespaced UUIDv4 correlation reference.""" + if type(value) is not str: + raise ValueError(f"{field_name} must be an opaque {prefix}: UUIDv4 reference") + if len(value) > 160 or not value.startswith(f"{prefix}:"): + raise ValueError(f"{field_name} must be an opaque {prefix}: UUIDv4 reference") + suffix = value.split(":", 1)[1] + try: + parsed = UUID(suffix) + except (ValueError, AttributeError, TypeError) as exc: + raise ValueError(f"{field_name} must be an opaque {prefix}: UUIDv4 reference") from exc + if str(parsed) != suffix or parsed.version != 4: + raise ValueError(f"{field_name} must be an opaque {prefix}: UUIDv4 reference") + + +def _validate_digest(value: str, field_name: str) -> None: + """Require exact lowercase SHA-256 evidence text.""" + if type(value) is not str or _DIGEST_PATTERN.fullmatch(value) is None: + raise ValueError(f"{field_name} must be lowercase SHA-256 hex") + + +def _validate_received_at(value: datetime, recorded_at: datetime) -> None: + """Require detached UTC event time no later than system-recorded issuance.""" + if type(recorded_at) is not datetime or recorded_at.tzinfo is not timezone.utc: + raise ValueError("recorded_at must be an exact built-in UTC datetime") + if type(value) is not datetime or value.tzinfo is not timezone.utc: + raise ValueError("received_at must be an exact built-in UTC datetime") + if value > recorded_at: + raise ValueError("received_at cannot be in the future relative to recorded_at") + + +def _new_reference() -> str: + """Return a packet-owned opaque document-record correlation reference.""" + return f"document_record:{uuid4()}" + + +def _now_utc() -> datetime: + """Return one built-in UTC system-recorded issuance instant.""" + return datetime.now(timezone.utc) + + +def _canonical_json(payload: dict[str, Any]) -> str: + """Serialize one already-snapshotted evidence payload deterministically.""" + return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + + +def _payload_digest(payload: dict[str, Any]) -> str: + """Return SHA-256 over one exact canonical payload snapshot.""" + return sha256(_canonical_json(payload).encode("utf-8")).hexdigest() + + +@dataclass(frozen=True, slots=True, weakref_slot=True, repr=False, eq=False) +class DocumentRecordEvidence: + """Governed metadata evidence for one HR document artifact. + + The object is a transport-neutral evidence value. It is not document content, + a storage credential, a legal retention decision, or authority for any + employment action. + """ + + tenant_record_id: str + person_record_reference: str + employment_record_reference: str + uploader_actor_reference: str + document_category_code: str + artifact_reference: str + artifact_digest: str + source_provenance_digest: str + retention_policy_reference: str + retention_policy_digest: str + received_at: datetime + document_record_reference: str = field(default_factory=_new_reference, init=False) + recorded_at: datetime = field(default_factory=_now_utc, init=False) + classification_code: str = field(default=_CLASSIFICATION_CODE, init=False) + content_storage_state: str = field(default=_CONTENT_STORAGE_STATE, init=False) + decision_authority_state: str = field(default=_DECISION_AUTHORITY_STATE, init=False) + schema_version: str = field(default=_SCHEMA_VERSION, init=False) + + def __post_init__(self) -> None: + """Validate caller evidence and seal the exact issuance payload outside writable slots.""" + self._validate() + payload = self._payload() + with _ISSUANCE_LOCK: + _ISSUANCE_DIGESTS[self] = _payload_digest(payload) + + def __repr__(self) -> str: + """Avoid exposing HR document correlations in routine logs.""" + return "DocumentRecordEvidence()" + + def _validate(self) -> None: + """Fail closed when caller-controlled fields violate the reviewed evidence contract.""" + _validate_operational_uuid(self.tenant_record_id, "tenant_record_id") + _validate_uuid4_reference( + self.person_record_reference, "person_record", "person_record_reference" + ) + _validate_uuid4_reference( + self.employment_record_reference, + "employment_record", + "employment_record_reference", + ) + _validate_uuid4_reference( + self.uploader_actor_reference, "actor", "uploader_actor_reference" + ) + if type(self.document_category_code) is not str: + raise ValueError("document_category_code must use the reviewed vocabulary") + if self.document_category_code not in _ALLOWED_DOCUMENT_CATEGORIES: + raise ValueError("document_category_code must use the reviewed vocabulary") + _validate_uuid4_reference( + self.artifact_reference, "document_artifact", "artifact_reference" + ) + _validate_digest(self.artifact_digest, "artifact_digest") + _validate_digest(self.source_provenance_digest, "source_provenance_digest") + _validate_uuid4_reference( + self.retention_policy_reference, + "retention_policy", + "retention_policy_reference", + ) + _validate_digest(self.retention_policy_digest, "retention_policy_digest") + _validate_received_at(self.received_at, self.recorded_at) + + def _payload(self) -> dict[str, Any]: + """Snapshot the exact value-minimized evidence fields once.""" + return { + "artifact_digest": self.artifact_digest, + "artifact_reference": self.artifact_reference, + "classification_code": self.classification_code, + "content_storage_state": self.content_storage_state, + "decision_authority_state": self.decision_authority_state, + "document_category_code": self.document_category_code, + "document_record_reference": self.document_record_reference, + "employment_record_reference": self.employment_record_reference, + "person_record_reference": self.person_record_reference, + "received_at": self.received_at.isoformat().replace("+00:00", "Z"), + "recorded_at": self.recorded_at.isoformat().replace("+00:00", "Z"), + "retention_policy_digest": self.retention_policy_digest, + "retention_policy_reference": self.retention_policy_reference, + "schema_version": self.schema_version, + "source_provenance_digest": self.source_provenance_digest, + "tenant_record_id": self.tenant_record_id, + "uploader_actor_reference": self.uploader_actor_reference, + } + + def _verified_payload(self) -> dict[str, Any]: + """Validate and return the same payload snapshot whose issuance seal was verified.""" + self._validate() + payload = self._payload() + actual_digest = _payload_digest(payload) + with _ISSUANCE_LOCK: + expected_digest = _ISSUANCE_DIGESTS.get(self, f"missing:{actual_digest}") + if not hmac.compare_digest(expected_digest, actual_digest): + raise ValueError("document record evidence changed after construction") + return payload + + def canonical_document(self) -> dict[str, Any]: + """Return deterministic, value-minimized document metadata evidence.""" + return self._verified_payload() + + def canonical_json(self) -> str: + """Return deterministic JSON over the exact verified payload snapshot.""" + return _canonical_json(self._verified_payload()) + + def sha256_digest(self) -> str: + """Return SHA-256 over the exact verified canonical JSON bytes.""" + return sha256(self.canonical_json().encode("utf-8")).hexdigest() + + +_ISSUANCE_DIGESTS = WeakKeyDictionary() + + +def build_document_record_evidence( + *, + tenant_record_id: str, + person_record_reference: str, + employment_record_reference: str, + uploader_actor_reference: str, + document_category_code: str, + artifact_reference: str, + artifact_digest: str, + source_provenance_digest: str, + retention_policy_reference: str, + retention_policy_digest: str, + received_at: datetime, +) -> DocumentRecordEvidence: + """Build one non-authorizing HR document metadata evidence value.""" + return DocumentRecordEvidence( + tenant_record_id=tenant_record_id, + person_record_reference=person_record_reference, + employment_record_reference=employment_record_reference, + uploader_actor_reference=uploader_actor_reference, + document_category_code=document_category_code, + artifact_reference=artifact_reference, + artifact_digest=artifact_digest, + source_provenance_digest=source_provenance_digest, + retention_policy_reference=retention_policy_reference, + retention_policy_digest=retention_policy_digest, + received_at=received_at, + ) diff --git a/packages/document-record-evidence/tests/test_evidence.py b/packages/document-record-evidence/tests/test_evidence.py new file mode 100644 index 000000000..0d6b3cc18 --- /dev/null +++ b/packages/document-record-evidence/tests/test_evidence.py @@ -0,0 +1,136 @@ +"""Executable contract for value-minimized HR document-record evidence.""" + +from datetime import datetime, timedelta, timezone +from pathlib import Path +from uuid import UUID, uuid1, uuid4 + +import pytest + +from orgmetra_document_record_evidence import build_document_record_evidence + + +def values() -> dict[str, object]: + """Return one valid caller-owned document-record input set.""" + now = datetime.now(timezone.utc) + return { + "tenant_record_id": str(uuid4()), + "person_record_reference": f"person_record:{uuid4()}", + "employment_record_reference": f"employment_record:{uuid4()}", + "uploader_actor_reference": f"actor:{uuid4()}", + "document_category_code": "employment_contract", + "artifact_reference": f"document_artifact:{uuid4()}", + "artifact_digest": "a" * 64, + "source_provenance_digest": "b" * 64, + "retention_policy_reference": f"retention_policy:{uuid4()}", + "retention_policy_digest": "c" * 64, + "received_at": now - timedelta(seconds=1), + } + + +def test_builds_value_minimized_document_evidence() -> None: + """Build one canonical record without copying document content or HR values.""" + evidence = build_document_record_evidence(**values()) + document = evidence.canonical_document() + assert document["schema_version"] == "orgmetra.document_record_evidence.v1" + assert document["classification_code"] == "restricted_hr" + assert document["content_storage_state"] == "artifact_reference_only" + assert document["decision_authority_state"] == "not_authorized_for_employment_decision" + assert document["document_category_code"] == "employment_contract" + assert document["artifact_digest"] == "a" * 64 + assert document["source_provenance_digest"] == "b" * 64 + assert "document_content" not in document + assert "document_title" not in document + assert len(evidence.sha256_digest()) == 64 + assert evidence.canonical_json() == evidence.canonical_json() + assert repr(evidence) == "DocumentRecordEvidence()" + + +def test_generates_packet_owned_reference_and_system_time() -> None: + """Generate packet identity and recorded time inside the Orgmetra issuance boundary.""" + before = datetime.now(timezone.utc) + evidence = build_document_record_evidence(**values()) + after = datetime.now(timezone.utc) + reference = evidence.document_record_reference + assert reference.startswith("document_record:") + assert UUID(reference.split(":", 1)[1]).version == 4 + assert before <= evidence.recorded_at <= after + assert evidence.recorded_at.tzinfo is timezone.utc + + +@pytest.mark.parametrize( + ("field_name", "bad_value"), + [ + ("tenant_record_id", 7), + ("tenant_record_id", "not-a-uuid"), + ("tenant_record_id", "00000000-0000-0000-0000-000000000000"), + ("person_record_reference", 7), + ("person_record_reference", f"wrong_namespace:{uuid4()}"), + ("person_record_reference", f"person_record:{uuid1()}"), + ("person_record_reference", "person_record:"), + ("employment_record_reference", "employment_record:"), + ("uploader_actor_reference", "actor:Jane-Doe"), + ("document_category_code", 7), + ("document_category_code", "free_form_category"), + ("artifact_reference", "document_artifact:"), + ("artifact_digest", "A" * 64), + ("source_provenance_digest", "b" * 63), + ("retention_policy_reference", "retention_policy:"), + ("retention_policy_digest", "not-a-digest"), + ], +) +def test_rejects_malformed_trust_evidence(field_name: str, bad_value: object) -> None: + """Fail closed before malformed trust evidence can enter canonical audit correlation.""" + payload = values() + payload[field_name] = bad_value + with pytest.raises(ValueError): + build_document_record_evidence(**payload) + + +def test_rejects_future_received_time() -> None: + """Do not record a document as received after its system issuance time.""" + payload = values() + payload["received_at"] = datetime.now(timezone.utc) + timedelta(days=1) + with pytest.raises(ValueError, match="received_at cannot be in the future"): + build_document_record_evidence(**payload) + + +def test_rejects_non_utc_received_time() -> None: + """Require detached built-in UTC business-event time at the evidence boundary.""" + payload = values() + payload["received_at"] = datetime.now().replace(tzinfo=None) + with pytest.raises(ValueError, match="received_at must be an exact built-in UTC datetime"): + build_document_record_evidence(**payload) + + +def test_rejects_valid_value_rewrite_after_issuance() -> None: + """Do not emit a second evidence truth after a frozen packet is forcibly rewritten.""" + evidence = build_document_record_evidence(**values()) + object.__setattr__(evidence, "document_category_code", "policy_acknowledgement") + with pytest.raises(ValueError, match="document record evidence changed after construction"): + evidence.canonical_json() + + +def test_rejects_forged_recorded_time_before_seal_verification() -> None: + """Require the system-recorded time type before canonical output can verify its seal.""" + evidence = build_document_record_evidence(**values()) + original = evidence.recorded_at + + class ForgedDateTime(datetime): + """Pretend to serialize a different datetime as the original system time.""" + + def isoformat(self, sep: str = "T", timespec: str = "auto") -> str: + """Return the original text while retaining a different underlying datetime.""" + return original.isoformat(sep=sep, timespec=timespec) + + object.__setattr__(evidence, "recorded_at", ForgedDateTime(2099, 1, 1, tzinfo=timezone.utc)) + with pytest.raises(ValueError, match="recorded_at must be an exact built-in UTC datetime"): + evidence.canonical_json() + + +def test_quality_workflow_watches_governance_adr() -> None: + """Ensure an ADR-only contract edit cannot bypass the dedicated package gate.""" + repository_root = Path(__file__).resolve().parents[3] + workflow = ( + repository_root / ".github" / "workflows" / "document-record-evidence-quality.yml" + ).read_text(encoding="utf-8") + assert '"docs/adr/0098-governed-document-record-evidence.md"' in workflow