diff --git a/.github/workflows/validity-analysis-quality.yml b/.github/workflows/validity-analysis-quality.yml new file mode 100644 index 000000000..17d5e8755 --- /dev/null +++ b/.github/workflows/validity-analysis-quality.yml @@ -0,0 +1,67 @@ +name: Validity Analysis Handoff Quality + +on: + pull_request: + branches: + - bootstrap + - develop + - main + paths: + - "packages/validity-analysis/**" + - "docs/adr/**" + - "docs/doctoring/validation-analysis-handoff-references.md" + - "docs/traceability/validation-analysis-handoff.md" + - ".github/requirements/foundation-test.txt" + - ".github/workflows/validity-analysis-quality.yml" + - ".gitignore" + - ".python-version" + - "conftest.py" + - "packages/conftest.py" + - "pyproject.toml" + - "pytest.ini" + - "setup.cfg" + - "tox.ini" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: validity-analysis-quality-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + unit: + name: Validity handoff 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 Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + check-latest: false + - name: Install reviewed test toolchain + run: | + python -m pip install --require-hashes --no-deps --only-binary=:all: -r .github/requirements/foundation-test.txt + python -m pip check + - name: Compile validity-analysis handoff + run: python -m compileall -q packages/validity-analysis/src packages/validity-analysis/tests + - name: Test governed handoff with exact statement and branch coverage + env: + PYTHONPATH: packages/validity-analysis/src + COVERAGE_FILE: /tmp/orgmetra-validity-analysis.coverage + run: python -m pytest -c packages/validity-analysis/pyproject.toml packages/validity-analysis/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..d954dc355 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 `orgmetra_validity_analysis` handoff/result boundary: exact tenant and evidence references, reviewed fast-mlsirm pin, distinct requester/reviewer actors, aggregate-only scientific evidence, construction-time UTC and finite-number snapshots, canonical JSON/SHA-256 correlation, and human-review-only result 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/0027-governed-selection-validity-analysis-handoff.md b/docs/adr/0027-governed-selection-validity-analysis-handoff.md new file mode 100644 index 000000000..ac74dbec8 --- /dev/null +++ b/docs/adr/0027-governed-selection-validity-analysis-handoff.md @@ -0,0 +1,60 @@ +# ADR 0027: Govern selection-validity numerical work through an immutable handoff + +- Status: Proposed +- Maturity: Active PR only; not protected-branch truth +- Date: 2026-08-21 +- Owners: Orgmetra Workforce Validation + +## Context + +Protected Orgmetra already preserves exact validation-study cases, sealed selection evidence, candidate-to-worker lineage, and Job/cycle/staffing-scoped criterion observations. The remaining boundary is dangerous if left implicit: a statistical worker could receive an underspecified study, silently use a different dependency revision, or turn a model result into an employment decision. + +The Uniform Guidelines recognize criterion-related validity evidence as empirical evidence relating a selection procedure to important job-performance elements and require validity studies to be accurate, standardized, documented, and periodically reviewed for currency. SIOP's *Principles for the Validation and Use of Personnel Selection Procedures* likewise treats validation as an evidence-and-inference problem rather than a correlation-only shortcut. + +`ContextualWisdomLab/fast-mlsirm` owns numerical psychometric/statistical kernels. Its protected `main` was freshly resolved to commit `04d0bc21a2a20693bcf16108cd76d394fe844d23` on 2026-08-21. Orgmetra must not copy that implementation or write the foreign repository. + +## Decision + +Orgmetra adds a leaf `orgmetra_validity_analysis` package whose `ValidationAnalysisHandoff`: + +- binds the exact tenant, validation study, Job, predictor snapshot, criterion snapshot, population snapshot, decision policy, and analysis plan through opaque references plus SHA-256 evidence digests; +- binds distinct requester and reviewer actor references; +- pins fast-mlsirm to reviewed immutable commit `04d0bc21a2a20693bcf16108cd76d394fe844d23`; +- declares the numerical boundary `read_only_pinned_revision` and the initial strategy `criterion_related`; +- requires downstream result evidence for effect estimate, uncertainty interval, sample size, missingness summary, and convergence diagnostics; +- serializes no raw person-level predictor, criterion, candidate, or worker values; +- remains `not_executed`, `scientific_evidence_only`, and human-review-required; +- produces deterministic canonical JSON and a SHA-256 digest for audit/result correlation. + +Both handoff and result envelopes detach exact timezone-aware timestamps to one built-in UTC instant at construction. Result numeric evidence is converted to finite built-in floats before storage, and conversion failures including numeric overflow are normalized to the package's fail-closed `ValueError` contract, so caller-controlled timezone or numeric runtime behavior cannot rewrite canonical evidence after validation or escape normal malformed-result handling. + +The same package also validates `ValidationAnalysisResult` envelopes returned by the approved offline worker. A result must link to the handoff digest and the same pinned revision, identify a Rust CPU or GPU backend and precision, provide finite effect and interval values, match its sample size to aggregate missingness counts, reject impossible complete-versus-missing count combinations, and include explicit convergence diagnostics. The canonicalization boundary accepts only the exact governed `MissingnessSummary` and `ConvergenceDiagnostics` runtime types so subclass method overrides cannot add unreviewed or person-level fields to immutable result evidence. A nonconverged result remains typed scientific evidence requiring human review; it cannot be treated as a valid selection procedure or an employment decision. + +The package does not invoke fast-mlsirm. An approved offline worker is the later execution boundary. Before execution, the Orgmetra host must re-resolve every reference inside the tenant, verify exact study/Job membership and evidence provenance, and prove requester/reviewer identities are distinct authoritative actors. + +## Consequences + +### Positive + +- Statistical work cannot silently drift to an unreviewed fast-mlsirm revision. +- A buyer or auditor can identify exactly which governed study evidence a result was supposed to analyze without copying raw worker data into the handoff. +- Aggregate missingness evidence cannot claim all observations are complete while simultaneously reporting predictor- or criterion-missing observations. +- Result canonicalization cannot be extended by an unreviewed subclass to serialize extra decision-like or person-level fields. +- Caller-controlled timestamp and numeric runtime behavior cannot rewrite an accepted canonical digest after construction or turn malformed oversized worker output into an uncaught exception type. +- Human interpretation remains explicit and separate from numerical output. +- The dedicated-writer boundary remains intact: Orgmetra consumes only a pinned foreign revision/contract boundary and never mutates fast-mlsirm. + +### Limitations + +- This slice does not execute a statistical model, estimate validity, correct for measurement error/range restriction, evaluate adverse impact, or assert legal compliance. +- Sampling design, estimator choice, missing-data treatment, reliability evidence, multiplicity, transportability, fairness analysis, and model diagnostics must be encoded in the referenced analysis plan and reviewed before execution. +- The generic result envelope does not invent an estimator-specific minimum complete-case count. Whether a converged estimator is identified under a particular missing-data design belongs to the reviewed analysis plan and numerical-worker contract; Orgmetra fails closed on impossible aggregate counts without silently replacing that foreign scientific contract with complete-case analysis. +- The package validates the result envelope, but a future execution adapter must still re-resolve the handoff references, verify the result provenance artifact, and attach evidence only after accountable human review. + +## Verification + +The package regression suite starts RED when the public handoff contract is absent and covers canonical operational tenant UUIDs, opaque UUIDv4 references, exact evidence digests, distinct human actors, exact dependency pinning, timezone-aware event time, immutable governance constants, value minimization, deterministic canonicalization, SHA-256 correlation, impossible aggregate missingness rejection, oversized numeric conversion rejection, exact governed aggregate-evidence runtime types, and 100% owned production statement/branch coverage. The repository-wide ADR numbering regression also fails closed if integration reuses an existing decision number, and the validity quality workflow contract requires any `docs/adr/**` change to execute that regression. + +## References + +See `docs/doctoring/validation-analysis-handoff-references.md`. diff --git a/docs/doctoring/validation-analysis-handoff-references.md b/docs/doctoring/validation-analysis-handoff-references.md new file mode 100644 index 000000000..cfde9303b --- /dev/null +++ b/docs/doctoring/validation-analysis-handoff-references.md @@ -0,0 +1,23 @@ +# Validation-analysis handoff references + +Material decisions for ADR 0027 were checked against the following primary/authoritative sources on 2026-08-21. Regulatory currency was rechecked on 2026-08-29; fixed publication identifiers are retained so an auditor can reproduce the cited text even when agency web pages change. + +## APA 7 references + +Equal Employment Opportunity Commission, Civil Service Commission, Department of Justice, & Department of Labor. (1978). *Uniform Guidelines on Employee Selection Procedures (1978)*, 43 Fed. Reg. 38,290 (August 25, 1978) (codified at 29 C.F.R. pt. 1607). The EEOC continues to list 29 C.F.R. pt. 1607 among its Title VII regulations: https://www.eeoc.gov/regulations-and-guidelines + +Society for Industrial and Organizational Psychology. (2018). Principles for the validation and use of personnel selection procedures. *Industrial and Organizational Psychology, 11*(S1), 1–97. https://doi.org/10.1017/iop.2018.195 + +ContextualWisdomLab. (2026). *fast-mlsirm* (Commit 04d0bc21a2a20693bcf16108cd76d394fe844d23) [Computer software]. GitHub. https://github.com/ContextualWisdomLab/fast-mlsirm/tree/04d0bc21a2a20693bcf16108cd76d394fe844d23 + +Tabassi, E. (2023). *Artificial intelligence risk management framework (AI RMF 1.0)* (NIST AI 100-1). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.AI.100-1 + +Office of Personnel Management. (2026). *Removal of references to the Uniform Guidelines on Employee Selection Procedures in federal personnel regulations*, 91 Fed. Reg. 48,234 (July 31, 2026) (interim final rule, RIN 3206-AP20). + +## Decision notes + +- 43 Fed. Reg. 38,290 and the still-listed EEOC 29 C.F.R. pt. 1607 source support keeping criterion-related validity evidence tied to an explicit study design, job relevance, accuracy, reporting, and documentation rather than treating a bare coefficient as sufficient evidence. The fixed Federal Register identifier, not a mutable `/current/` eCFR URL, is the reproducible source for the 1978 text cited by this ADR. +- The July 31, 2026 OPM interim final rule removed UGESP references from specified federal civil-service regulations. Orgmetra therefore does not present UGESP as an undifferentiated government-wide mandate; applicability must be evaluated for the employer, jurisdiction, decision, and governing law at use time. +- The SIOP Principles are the professional validation baseline used for the handoff's evidence-and-human-review posture. The journal citation above fixes volume 11, Supplement S1, pages 1–97, and DOI 10.1017/iop.2018.195. +- The fast-mlsirm commit is recorded as a read-only dependency coordinate only. This Orgmetra slice does not modify or duplicate its numerical implementation. +- NIST AI RMF's govern, map, measure, and manage functions support preserving backend, precision, provenance, convergence, and human-review fields as inspectable result evidence rather than treating a model response as an autonomous decision. diff --git a/docs/traceability/validation-analysis-handoff.md b/docs/traceability/validation-analysis-handoff.md new file mode 100644 index 000000000..f9f8c755e --- /dev/null +++ b/docs/traceability/validation-analysis-handoff.md @@ -0,0 +1,26 @@ +# Selection-validity analysis handoff traceability + +## Buyer question + +Can an organization send one exact, reviewable validation study to its statistical engine without copying raw person-level values into a workflow envelope, silently changing the numerical dependency, or treating model output as an employment decision? + +## Active-PR contract + +| Concern | Orgmetra evidence | Verification | +|---|---|---| +| Exact study scope | tenant, validation-study, Job, predictor, criterion, population, decision-policy, and analysis-plan references plus digests | namespace/UUID/digest regressions | +| Dependency integrity | immutable fast-mlsirm commit `04d0bc21a2a20693bcf16108cd76d394fe844d23` | malformed and unreviewed revision rejection | +| Privacy minimization | no raw person-level values in canonical handoff or result; result canonicalization accepts only exact governed missingness/convergence runtime types | canonical-payload/redacted-repr regressions plus subclass-injection rejection | +| Human authority | requester/reviewer references must differ, and the host must re-resolve both within the tenant and prove they resolve to distinct authoritative actor identities before execution | direct-construction fail-closed regression plus `test_next_action_requires_resolved_actor_identity_separation` | +| Scientific evidence | effect estimate, uncertainty interval, sample size, internally possible aggregate missingness, convergence diagnostics | immutable required-result-evidence regression plus impossible-missingness rejection | +| Numerical result boundary | handoff digest, pinned fast-mlsirm revision, Rust CPU/GPU backend, precision, finite estimate/interval, aggregate missingness, explicit convergence state | `ValidationAnalysisResult` invariant/canonicalization regressions, exact-runtime-type checks, and oversized-numeric `ValueError` normalization | +| Execution boundary | `not_executed`, `scientific_evidence_only`, read-only pinned foreign dependency | immutable governance regressions | +| Reproducibility | construction-time UTC timestamp snapshots, finite numeric snapshots, canonical RFC 3339 time, canonical JSON, SHA-256 handoff digest | mutable timezone/numeric and UTC-boundary regressions plus deterministic serialization/digest tests | +| Decision-record integrity | ADR numbers remain unique repository-wide and any `docs/adr/**` change reaches the validity quality gate | ADR uniqueness regression plus workflow-trigger contract regression | +| Quality-evidence freshness | package quality reruns whenever shared repository Python/test/clean-checkout configuration can alter execution or tracked-tree cleanliness | `test_quality_workflow_retriggers_on_shared_test_configuration` plus `.github/workflows/validity-analysis-quality.yml`; this supplemental package gate does not replace central required workflows | + +## Maturity + +`implemented_on_active_pr`. + +Protected `develop` does **not** gain numerical validity computation from this slice. The handoff is execution preparation only. The active package now validates the minimum returned numerical/provenance envelope, including missingness consistency and exact aggregate-evidence runtime types, but protected Orgmetra evidence still requires host re-resolution, result-artifact verification, terminal checks, independent review, and accountable human interpretation. diff --git a/manifest.json b/manifest.json index 97f2bab14..df6ee9743 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":"264a8af7f8a324a044317d24ce8f8d7eee65ef3d54a8819e7a934d6b9859aa83","bytes":17624,"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/validity-analysis/CHANGELOG.md b/packages/validity-analysis/CHANGELOG.md new file mode 100644 index 000000000..d0ff8bd9f --- /dev/null +++ b/packages/validity-analysis/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +## 0.1.0 - Unreleased + +- Add a governed, value-minimized criterion-related validity analysis handoff. +- Pin the reviewed read-only fast-mlsirm dependency revision. +- Require separate requester/reviewer references and authoritative tenant-scoped re-resolution proving they resolve to distinct actor identities before execution. +- Require deterministic canonical evidence and 100% owned production statement/branch coverage. +- Validate a digest-linked Rust CPU/GPU result envelope with finite estimates, aggregate missingness, and explicit convergence or nonconvergence diagnostics. +- Reject impossible aggregate missingness where complete observations overlap either predictor-missing or criterion-missing counts beyond the sample total. +- Require exact governed missingness/convergence runtime types so subclass method overrides cannot inject unreviewed or person-level fields into canonical result evidence. +- Freeze exact UTC timestamps and finite numeric values at construction, and reject runtime-type forgery before canonical evidence serialization. +- Make `Validity Analysis Handoff Quality` retrigger on shared repository Python/test/clean-checkout configuration, with an executable regression preventing stale package-quality evidence after shared tooling changes. diff --git a/packages/validity-analysis/README.md b/packages/validity-analysis/README.md new file mode 100644 index 000000000..b56e9a040 --- /dev/null +++ b/packages/validity-analysis/README.md @@ -0,0 +1,37 @@ +# Orgmetra validity-analysis handoff + +This package creates an immutable **selection-validity analysis handoff** and validates the matching numerical result envelope. It is the boundary between Orgmetra's authoritative validation-study evidence and numerical work owned by `ContextualWisdomLab/fast-mlsirm`. + +## What it does + +`build_validation_analysis_handoff(...)` binds one tenant, validation study, Job, predictor snapshot, criterion snapshot, population snapshot, decision policy, analysis plan, requester, reviewer, and the reviewed fast-mlsirm revision `04d0bc21a2a20693bcf16108cd76d394fe844d23`. + +The resulting canonical JSON is digest-addressable, contains no raw person-level predictor or criterion values, and remains `not_executed`. Its timestamp is detached to one UTC instant at construction so later timezone-provider changes cannot rewrite the digest. Required result evidence is explicit: effect estimate, uncertainty interval, sample size, missingness summary, and convergence diagnostics. + +`ValidationAnalysisResult` accepts only a result linked to the handoff digest and the same reviewed fast-mlsirm revision. It records the Rust CPU/GPU backend, precision, aggregate missingness counts, finite effect and interval values, and explicit convergence or nonconvergence diagnostics. Timestamps and finite numeric values are snapshotted before canonicalization, and the result envelope accepts only the exact governed `MissingnessSummary` and `ConvergenceDiagnostics` runtime types, preventing mutable runtime values or subclass method overrides from adding unreviewed or person-level fields to canonical audit evidence. Missingness counts must be internally possible: complete observations cannot overlap either predictor-missing or criterion-missing observations beyond the declared sample total. It never promotes a result to an employment decision; human review remains mandatory. + +## What it does not do + +- It does **not** run statistics. +- It does **not** run or reproduce the fast-mlsirm numerical kernel. +- It does **not** query fast-mlsirm or any other CWL application's database. +- It does **not** claim that a selection procedure is valid. +- It does **not** interpret adverse impact. +- It does **not** authorize hiring, promotion, termination, compensation, or another employment decision. + +The fast-mlsirm repository remains a dedicated-writer dependency. This package records only the immutable revision reviewed for the handoff. + +## Host obligations + +Before an approved offline validation worker executes the handoff, the Orgmetra host must re-resolve every reference inside `tenant_record_id` and prove that the predictor, criterion, population, and policy evidence belong to the exact validation study and Job. The requester and reviewer must resolve to distinct authoritative actors. A numerical result is scientific evidence for accountable human interpretation, never an autonomous employment decision. + +## Verification + +Run: + +```bash +PYTHONPATH=packages/validity-analysis/src \ +python -m pytest -c packages/validity-analysis/pyproject.toml packages/validity-analysis/tests +``` + +The package gate requires exact 100% owned production statement and branch coverage. diff --git a/packages/validity-analysis/pyproject.toml b/packages/validity-analysis/pyproject.toml new file mode 100644 index 000000000..5547eabcb --- /dev/null +++ b/packages/validity-analysis/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["setuptools>=69"] +build-backend = "setuptools.build_meta" + +[project] +name = "orgmetra-validity-analysis" +version = "0.1.0" +description = "Governed criterion-related selection-validity analysis handoff for Orgmetra." +requires-python = ">=3.12" + +[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_validity_analysis", + "--cov-branch", + "--cov-report=term-missing", + "--cov-fail-under=100", +] diff --git a/packages/validity-analysis/src/orgmetra_validity_analysis/__init__.py b/packages/validity-analysis/src/orgmetra_validity_analysis/__init__.py new file mode 100644 index 000000000..81acbc0d1 --- /dev/null +++ b/packages/validity-analysis/src/orgmetra_validity_analysis/__init__.py @@ -0,0 +1,17 @@ +"""Public governed selection-validity analysis handoff and result contracts.""" + +from .handoff import ( + REVIEWED_FAST_MLSIRM_REVISION, + ValidationAnalysisHandoff, + build_validation_analysis_handoff, +) +from .result import ConvergenceDiagnostics, MissingnessSummary, ValidationAnalysisResult + +__all__ = [ + "REVIEWED_FAST_MLSIRM_REVISION", + "ValidationAnalysisHandoff", + "build_validation_analysis_handoff", + "ConvergenceDiagnostics", + "MissingnessSummary", + "ValidationAnalysisResult", +] diff --git a/packages/validity-analysis/src/orgmetra_validity_analysis/handoff.py b/packages/validity-analysis/src/orgmetra_validity_analysis/handoff.py new file mode 100644 index 000000000..5f3fb2f57 --- /dev/null +++ b/packages/validity-analysis/src/orgmetra_validity_analysis/handoff.py @@ -0,0 +1,305 @@ +"""Governed handoff evidence for criterion-related selection validation. + +This package does not execute statistics, read another service's database, or make an +employment decision. It binds authoritative Orgmetra evidence to one reviewed, +immutable fast-mlsirm revision so an approved offline worker can perform numerical +analysis without silently changing the study definition. +""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from hashlib import sha256 +import json +import re +from uuid import UUID + +_CODE_PATTERN = re.compile(r"^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$") +_DIGEST_PATTERN = re.compile(r"^[0-9a-f]{64}$") +_REVISION_PATTERN = re.compile(r"^[0-9a-f]{40}$") +_REFERENCE_PATTERN = re.compile( + r"^[a-z][a-z0-9_]{1,31}:[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$" +) +_PURPOSE_CODE = "selection_validity_analysis" +_REASON_CODE = "criterion_related_validation" +_VALIDATION_STRATEGY = "criterion_related" +_KERNEL_REPOSITORY = "ContextualWisdomLab/fast-mlsirm" +REVIEWED_FAST_MLSIRM_REVISION = "04d0bc21a2a20693bcf16108cd76d394fe844d23" +_KERNEL_BOUNDARY = "read_only_pinned_revision" +_EXECUTION_STATE = "not_executed" +_RESULT_AUTHORITY = "scientific_evidence_only" +_REQUIRED_RESULT_EVIDENCE = ( + "effect_estimate", + "uncertainty_interval", + "sample_size", + "missingness_summary", + "convergence_diagnostics", +) +_NEXT_ACTION = ( + "Within tenant_record_id, re-resolve the validation study, Job, predictor, criterion, " + "population, decision-policy, analysis-plan, requester, and reviewer references; prove " + "requester and reviewer resolve to distinct authoritative actor identities; prove the " + "predictor/criterion/population cases belong to the exact study and Job; then let an " + "approved offline validation worker invoke only the pinned fast-mlsirm revision. Preserve " + "the resulting model/provenance diagnostics as draft scientific evidence for an " + "accountable human reviewer; never convert the result directly into an employment decision." +) + + +def _validate_operational_uuid(value: object, field_name: str) -> None: + """Require canonical non-sentinel UUID text owned by authoritative Orgmetra.""" + 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_reference(value: object, prefix: str, field_name: str) -> None: + """Require the expected namespace plus a canonical opaque UUIDv4 suffix.""" + error_message = f"{field_name} must be an opaque {prefix}: UUIDv4 reference" + if ( + type(value) is not str + or len(value) > 160 + or not _REFERENCE_PATTERN.fullmatch(value) + or not value.startswith(f"{prefix}:") + ): + raise ValueError(error_message) + suffix = value.split(":", 1)[1] + try: + parsed = UUID(suffix) + except (ValueError, AttributeError, TypeError) as exc: + raise ValueError(error_message) from exc + if str(parsed) != suffix or parsed.version != 4 or parsed.int in (0, (1 << 128) - 1): + raise ValueError(error_message) + + +def _validate_digest(value: object, field_name: str) -> None: + """Require lowercase SHA-256 hexadecimal evidence.""" + if type(value) is not str or not _DIGEST_PATTERN.fullmatch(value): + raise ValueError(f"{field_name} must be lowercase SHA-256 hex") + + +def _validate_code(value: object, field_name: str) -> None: + """Require bounded descriptive lower snake_case governance codes.""" + if type(value) is not str or len(value) > 64 or not _CODE_PATTERN.fullmatch(value): + raise ValueError(f"{field_name} must be bounded two-or-more-word lower snake_case") + + +def _validate_kernel_revision(value: object) -> None: + """Require the exact externally reviewed immutable fast-mlsirm revision.""" + if type(value) is not str or not _REVISION_PATTERN.fullmatch(value): + raise ValueError("fast_mlsirm_revision must be lowercase 40-character Git commit hex") + if value != REVIEWED_FAST_MLSIRM_REVISION: + raise ValueError("fast_mlsirm_revision must equal the reviewed immutable revision") + + +def _freeze_timestamp(value: object, field_name: str) -> datetime: + """Detach caller-controlled timezone behavior and store one immutable UTC instant.""" + if type(value) is not datetime or value.tzinfo is None: + raise ValueError(f"{field_name} must be an exact timezone-aware datetime") + try: + offset = value.utcoffset() + except Exception as exc: # noqa: BLE001 - normalize provider behavior at trust boundary. + raise ValueError(f"{field_name} must be an exact timezone-aware datetime") from exc + if type(offset) is not timedelta: + raise ValueError(f"{field_name} must be an exact timezone-aware datetime") + try: + return (value.replace(tzinfo=None) - offset).replace(tzinfo=timezone.utc) + except OverflowError as exc: + raise ValueError(f"{field_name} must be an exact timezone-aware datetime") from exc + + +def _canonical_timestamp(value: object, field_name: str) -> str: + """Render a previously detached built-in UTC instant as RFC 3339 text.""" + if type(value) is not datetime or value.tzinfo is not timezone.utc: + raise ValueError(f"{field_name} must be timezone-aware") + return value.isoformat().replace("+00:00", "Z") + + +@dataclass(frozen=True, slots=True, repr=False) +class ValidationAnalysisHandoff: + """Immutable evidence for one not-yet-executed criterion-related validity analysis.""" + + tenant_record_id: str + handoff_reference: str + validation_study_reference: str + job_profile_reference: str + predictor_snapshot_reference: str + predictor_snapshot_digest: str + criterion_snapshot_reference: str + criterion_snapshot_digest: str + population_snapshot_reference: str + population_snapshot_digest: str + decision_policy_reference: str + decision_policy_digest: str + analysis_plan_reference: str + analysis_plan_digest: str + actor_reference: str + reviewer_reference: str + fast_mlsirm_revision: str + requested_at: datetime + purpose_code: str = _PURPOSE_CODE + reason_code: str = _REASON_CODE + evidence_version: int = 1 + validation_strategy: str = _VALIDATION_STRATEGY + kernel_repository: str = _KERNEL_REPOSITORY + kernel_boundary: str = _KERNEL_BOUNDARY + execution_state: str = _EXECUTION_STATE + contains_raw_person_level_values: bool = False + human_review_required: bool = True + result_authority: str = _RESULT_AUTHORITY + required_result_evidence: tuple[str, ...] = _REQUIRED_RESULT_EVIDENCE + next_action: str = _NEXT_ACTION + + def __post_init__(self) -> None: + """Fail closed when direct construction drifts from the governed handoff.""" + requested_at = _freeze_timestamp(self.requested_at, "requested_at") + object.__setattr__(self, "requested_at", requested_at) + _validate_operational_uuid(self.tenant_record_id, "tenant_record_id") + for value, prefix, field_name in ( + (self.handoff_reference, "validation_analysis_handoff", "handoff_reference"), + (self.validation_study_reference, "validation_study", "validation_study_reference"), + (self.job_profile_reference, "job_profile", "job_profile_reference"), + (self.predictor_snapshot_reference, "predictor_snapshot", "predictor_snapshot_reference"), + (self.criterion_snapshot_reference, "criterion_snapshot", "criterion_snapshot_reference"), + (self.population_snapshot_reference, "study_population_snapshot", "population_snapshot_reference"), + (self.decision_policy_reference, "decision_policy", "decision_policy_reference"), + (self.analysis_plan_reference, "validation_analysis_plan", "analysis_plan_reference"), + (self.actor_reference, "actor", "actor_reference"), + (self.reviewer_reference, "actor", "reviewer_reference"), + ): + _validate_reference(value, prefix, field_name) + for value, field_name in ( + (self.predictor_snapshot_digest, "predictor_snapshot_digest"), + (self.criterion_snapshot_digest, "criterion_snapshot_digest"), + (self.population_snapshot_digest, "population_snapshot_digest"), + (self.decision_policy_digest, "decision_policy_digest"), + (self.analysis_plan_digest, "analysis_plan_digest"), + ): + _validate_digest(value, field_name) + if self.actor_reference == self.reviewer_reference: + raise ValueError("reviewer_reference must identify a different accountable actor") + _validate_kernel_revision(self.fast_mlsirm_revision) + _canonical_timestamp(self.requested_at, "requested_at") + _validate_code(self.purpose_code, "purpose_code") + if self.purpose_code != _PURPOSE_CODE: + raise ValueError("purpose_code must remain selection_validity_analysis") + _validate_code(self.reason_code, "reason_code") + if self.reason_code != _REASON_CODE: + raise ValueError("reason_code must remain criterion_related_validation") + if type(self.evidence_version) is not int or not 1 <= self.evidence_version <= 2_147_483_647: + raise ValueError("evidence_version must be an integer from 1 through 2147483647") + if type(self.validation_strategy) is not str or self.validation_strategy != _VALIDATION_STRATEGY: + raise ValueError("validation_strategy must remain criterion_related") + if type(self.kernel_repository) is not str or self.kernel_repository != _KERNEL_REPOSITORY: + raise ValueError("kernel_repository must remain ContextualWisdomLab/fast-mlsirm") + if type(self.kernel_boundary) is not str or self.kernel_boundary != _KERNEL_BOUNDARY: + raise ValueError("kernel_boundary must remain read_only_pinned_revision") + if type(self.execution_state) is not str or self.execution_state != _EXECUTION_STATE: + raise ValueError("execution_state must remain not_executed") + if self.contains_raw_person_level_values is not False: + raise ValueError("handoff must not contain raw person-level values") + if self.human_review_required is not True: + raise ValueError("human review is mandatory for selection-validity interpretation") + if type(self.result_authority) is not str or self.result_authority != _RESULT_AUTHORITY: + raise ValueError("result_authority must remain scientific_evidence_only") + if ( + type(self.required_result_evidence) is not tuple + or any(type(item) is not str for item in self.required_result_evidence) + or self.required_result_evidence != _REQUIRED_RESULT_EVIDENCE + ): + raise ValueError("required_result_evidence must remain the reviewed evidence set") + if type(self.next_action) is not str or self.next_action != _NEXT_ACTION: + raise ValueError("next_action must remain the governed validation instruction") + + def __repr__(self) -> str: + """Return a fully redacted representation suitable for routine logs.""" + return "ValidationAnalysisHandoff()" + + def canonical_json(self) -> str: + """Return deterministic canonical JSON for audit and result correlation.""" + payload = { + "actor_reference": self.actor_reference, + "analysis_plan_digest": self.analysis_plan_digest, + "analysis_plan_reference": self.analysis_plan_reference, + "contains_raw_person_level_values": self.contains_raw_person_level_values, + "criterion_snapshot_digest": self.criterion_snapshot_digest, + "criterion_snapshot_reference": self.criterion_snapshot_reference, + "decision_policy_digest": self.decision_policy_digest, + "decision_policy_reference": self.decision_policy_reference, + "evidence_version": self.evidence_version, + "execution_state": self.execution_state, + "fast_mlsirm_revision": self.fast_mlsirm_revision, + "handoff_reference": self.handoff_reference, + "human_review_required": self.human_review_required, + "job_profile_reference": self.job_profile_reference, + "kernel_boundary": self.kernel_boundary, + "kernel_repository": self.kernel_repository, + "next_action": self.next_action, + "population_snapshot_digest": self.population_snapshot_digest, + "population_snapshot_reference": self.population_snapshot_reference, + "predictor_snapshot_digest": self.predictor_snapshot_digest, + "predictor_snapshot_reference": self.predictor_snapshot_reference, + "purpose_code": self.purpose_code, + "reason_code": self.reason_code, + "requested_at": _canonical_timestamp(self.requested_at, "requested_at"), + "required_result_evidence": list(self.required_result_evidence), + "result_authority": self.result_authority, + "reviewer_reference": self.reviewer_reference, + "tenant_record_id": self.tenant_record_id, + "validation_strategy": self.validation_strategy, + "validation_study_reference": self.validation_study_reference, + } + return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + + def sha256_digest(self) -> str: + """Return SHA-256 over the exact canonical UTF-8 handoff.""" + return sha256(self.canonical_json().encode("utf-8")).hexdigest() + + +def build_validation_analysis_handoff( + *, + tenant_record_id: str, + handoff_reference: str, + validation_study_reference: str, + job_profile_reference: str, + predictor_snapshot_reference: str, + predictor_snapshot_digest: str, + criterion_snapshot_reference: str, + criterion_snapshot_digest: str, + population_snapshot_reference: str, + population_snapshot_digest: str, + decision_policy_reference: str, + decision_policy_digest: str, + analysis_plan_reference: str, + analysis_plan_digest: str, + actor_reference: str, + reviewer_reference: str, + fast_mlsirm_revision: str, + requested_at: datetime, +) -> ValidationAnalysisHandoff: + """Build a governed, non-executing selection-validity analysis handoff.""" + return ValidationAnalysisHandoff( + tenant_record_id=tenant_record_id, + handoff_reference=handoff_reference, + validation_study_reference=validation_study_reference, + job_profile_reference=job_profile_reference, + predictor_snapshot_reference=predictor_snapshot_reference, + predictor_snapshot_digest=predictor_snapshot_digest, + criterion_snapshot_reference=criterion_snapshot_reference, + criterion_snapshot_digest=criterion_snapshot_digest, + population_snapshot_reference=population_snapshot_reference, + population_snapshot_digest=population_snapshot_digest, + decision_policy_reference=decision_policy_reference, + decision_policy_digest=decision_policy_digest, + analysis_plan_reference=analysis_plan_reference, + analysis_plan_digest=analysis_plan_digest, + actor_reference=actor_reference, + reviewer_reference=reviewer_reference, + fast_mlsirm_revision=fast_mlsirm_revision, + requested_at=requested_at, + ) diff --git a/packages/validity-analysis/src/orgmetra_validity_analysis/result.py b/packages/validity-analysis/src/orgmetra_validity_analysis/result.py new file mode 100644 index 000000000..3f684ab10 --- /dev/null +++ b/packages/validity-analysis/src/orgmetra_validity_analysis/result.py @@ -0,0 +1,248 @@ +"""Validate one immutable numerical result returned by the approved worker. + +Orgmetra does not fit a model in this package. It accepts only a bounded, +digest-linked result envelope from the pinned ``fast-mlsirm`` worker so that +nonconverged or malformed output cannot be presented as an employment +decision. +""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from hashlib import sha256 +import json +from math import isfinite +from numbers import Real + +from .handoff import ( + _canonical_timestamp, + _freeze_timestamp, + _validate_code, + _validate_digest, + _validate_kernel_revision, + _validate_operational_uuid, + _validate_reference, +) + +_RESULT_AUTHORITY = "scientific_evidence_only" +_EXECUTION_STATE = "completed" +_ALLOWED_BACKENDS = frozenset({"rust_cpu", "rust_gpu"}) +_ALLOWED_PRECISIONS = frozenset({"f64", "f32"}) + + +def _validate_nonnegative_integer(value: object, field_name: str) -> None: + """Require a real non-negative integer without accepting booleans.""" + if type(value) is not int or value < 0: + raise ValueError(f"{field_name} must be a non-negative integer") + + +def _validate_positive_integer(value: object, field_name: str) -> None: + """Require a real positive integer without accepting booleans.""" + if type(value) is not int or value <= 0: + raise ValueError(f"{field_name} must be a positive integer") + + +def _finite_number(value: object, field_name: str) -> float: + """Return one finite real number and normalize invalid numerics to ValueError.""" + if isinstance(value, bool) or not isinstance(value, Real): + raise ValueError(f"{field_name} must be a finite number") + try: + number = float(value) + except (OverflowError, TypeError, ValueError) as exc: + raise ValueError(f"{field_name} must be a finite number") from exc + if not isfinite(number): + raise ValueError(f"{field_name} must be a finite number") + return number + + +@dataclass(frozen=True, slots=True) +class MissingnessSummary: + """Describe missingness counts without carrying person-level observations.""" + + total_observations: int + complete_observations: int + missing_predictor_observations: int + missing_criterion_observations: int + + def __post_init__(self) -> None: + """Reject impossible counts before a result can be correlated.""" + for field_name in ( + "total_observations", + "complete_observations", + "missing_predictor_observations", + "missing_criterion_observations", + ): + _validate_nonnegative_integer(getattr(self, field_name), field_name) + if self.total_observations == 0: + raise ValueError("total_observations must be positive") + if self.complete_observations > self.total_observations: + raise ValueError("complete_observations cannot exceed total_observations") + if self.missing_predictor_observations > self.total_observations: + raise ValueError("missing_predictor_observations cannot exceed total_observations") + if self.missing_criterion_observations > self.total_observations: + raise ValueError("missing_criterion_observations cannot exceed total_observations") + if self.complete_observations + self.missing_predictor_observations > self.total_observations: + raise ValueError( + "complete_observations and missing_predictor_observations cannot overlap" + ) + if self.complete_observations + self.missing_criterion_observations > self.total_observations: + raise ValueError( + "complete_observations and missing_criterion_observations cannot overlap" + ) + + def to_dict(self) -> dict[str, int]: + """Return deterministic count fields for the canonical result JSON.""" + return { + "complete_observations": self.complete_observations, + "missing_criterion_observations": self.missing_criterion_observations, + "missing_predictor_observations": self.missing_predictor_observations, + "total_observations": self.total_observations, + } + + +@dataclass(frozen=True, slots=True) +class ConvergenceDiagnostics: + """Record convergence evidence while preserving an explicit failure state.""" + + converged: bool + iterations: int + objective_value: Real + maximum_gradient: Real + failure_code: str | None = None + + def __post_init__(self) -> None: + """Require diagnostics that distinguish convergence from a failed fit.""" + if type(self.converged) is not bool: + raise ValueError("converged must be a boolean") + _validate_positive_integer(self.iterations, "iterations") + objective = _finite_number(self.objective_value, "objective_value") + gradient = _finite_number(self.maximum_gradient, "maximum_gradient") + if gradient < 0: + raise ValueError("maximum_gradient must be non-negative") + if self.converged and self.failure_code is not None: + raise ValueError("failure_code must be absent for a converged result") + if not self.converged and ( + type(self.failure_code) is not str or not self.failure_code + ): + raise ValueError("failure_code is required for a nonconverged result") + if self.failure_code is not None: + _validate_code(self.failure_code, "failure_code") + object.__setattr__(self, "objective_value", objective) + object.__setattr__(self, "maximum_gradient", gradient) + + def to_dict(self) -> dict[str, object]: + """Return deterministic convergence fields for the canonical result JSON.""" + payload: dict[str, object] = { + "converged": self.converged, + "iterations": self.iterations, + "maximum_gradient": float(self.maximum_gradient), + "objective_value": float(self.objective_value), + } + if self.failure_code is not None: + payload["failure_code"] = self.failure_code + return payload + + +@dataclass(frozen=True, slots=True, repr=False) +class ValidationAnalysisResult: + """Immutable, digest-linked scientific evidence returned by the offline worker.""" + + tenant_record_id: str + result_reference: str + handoff_digest: str + provenance_digest: str + fast_mlsirm_revision: str + model_code: str + backend: str + precision: str + effect_estimate: Real + uncertainty_lower: Real + uncertainty_upper: Real + sample_size: int + missingness_summary: MissingnessSummary + convergence_diagnostics: ConvergenceDiagnostics + completed_at: datetime + result_authority: str = _RESULT_AUTHORITY + execution_state: str = _EXECUTION_STATE + contains_raw_person_level_values: bool = False + human_review_required: bool = True + evidence_version: int = 1 + + def __post_init__(self) -> None: + """Fail closed on malformed, unlinked, or decision-like result data.""" + _validate_operational_uuid(self.tenant_record_id, "tenant_record_id") + _validate_reference(self.result_reference, "validation_analysis_result", "result_reference") + _validate_digest(self.handoff_digest, "handoff_digest") + _validate_digest(self.provenance_digest, "provenance_digest") + _validate_kernel_revision(self.fast_mlsirm_revision) + _validate_code(self.model_code, "model_code") + if type(self.backend) is not str or self.backend not in _ALLOWED_BACKENDS: + raise ValueError("backend must be rust_cpu or rust_gpu") + if type(self.precision) is not str or self.precision not in _ALLOWED_PRECISIONS: + raise ValueError("precision must be f64 or f32") + estimate = _finite_number(self.effect_estimate, "effect_estimate") + lower = _finite_number(self.uncertainty_lower, "uncertainty_lower") + upper = _finite_number(self.uncertainty_upper, "uncertainty_upper") + if lower > upper: + raise ValueError("uncertainty_lower cannot exceed uncertainty_upper") + if not lower <= estimate <= upper: + raise ValueError("effect_estimate must be inside the uncertainty interval") + _validate_positive_integer(self.sample_size, "sample_size") + if type(self.missingness_summary) is not MissingnessSummary: + raise ValueError("missingness_summary must be a MissingnessSummary") + if type(self.convergence_diagnostics) is not ConvergenceDiagnostics: + raise ValueError("convergence_diagnostics must be ConvergenceDiagnostics") + if self.sample_size != self.missingness_summary.total_observations: + raise ValueError("sample_size must match total_observations") + completed_at = _freeze_timestamp(self.completed_at, "completed_at") + if type(self.result_authority) is not str or self.result_authority != _RESULT_AUTHORITY: + raise ValueError("result_authority must remain scientific_evidence_only") + if type(self.execution_state) is not str or self.execution_state != _EXECUTION_STATE: + raise ValueError("execution_state must remain completed") + if self.contains_raw_person_level_values is not False: + raise ValueError("result must not contain raw person-level values") + if self.human_review_required is not True: + raise ValueError("human review is mandatory for validity interpretation") + if type(self.evidence_version) is not int or self.evidence_version != 1: + raise ValueError("evidence_version must remain 1") + object.__setattr__(self, "effect_estimate", estimate) + object.__setattr__(self, "uncertainty_lower", lower) + object.__setattr__(self, "uncertainty_upper", upper) + object.__setattr__(self, "completed_at", completed_at) + + def __repr__(self) -> str: + """Return a redacted representation suitable for routine application logs.""" + return "ValidationAnalysisResult()" + + def canonical_json(self) -> str: + """Return deterministic, non-person-level JSON for audit correlation.""" + payload = { + "backend": self.backend, + "completed_at": _canonical_timestamp(self.completed_at, "completed_at"), + "contains_raw_person_level_values": self.contains_raw_person_level_values, + "convergence_diagnostics": self.convergence_diagnostics.to_dict(), + "effect_estimate": float(self.effect_estimate), + "evidence_version": self.evidence_version, + "execution_state": self.execution_state, + "fast_mlsirm_revision": self.fast_mlsirm_revision, + "handoff_digest": self.handoff_digest, + "human_review_required": self.human_review_required, + "missingness_summary": self.missingness_summary.to_dict(), + "model_code": self.model_code, + "precision": self.precision, + "provenance_digest": self.provenance_digest, + "result_authority": self.result_authority, + "result_reference": self.result_reference, + "sample_size": self.sample_size, + "tenant_record_id": self.tenant_record_id, + "uncertainty_lower": float(self.uncertainty_lower), + "uncertainty_upper": float(self.uncertainty_upper), + } + return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + + def sha256_digest(self) -> str: + """Return SHA-256 over the exact canonical result bytes.""" + return sha256(self.canonical_json().encode("utf-8")).hexdigest() + + +__all__ = ["ConvergenceDiagnostics", "MissingnessSummary", "ValidationAnalysisResult"] diff --git a/packages/validity-analysis/tests/test_adr_numbering.py b/packages/validity-analysis/tests/test_adr_numbering.py new file mode 100644 index 000000000..8f2ee0828 --- /dev/null +++ b/packages/validity-analysis/tests/test_adr_numbering.py @@ -0,0 +1,17 @@ +"""Regression tests for repository-wide ADR number ownership.""" + +from pathlib import Path + + +def test_adr_numbers_are_unique_across_the_integrated_repository() -> None: + """Every four-digit ADR number must identify exactly one decision record.""" + adr_directory = Path(__file__).resolve().parents[3] / "docs" / "adr" + owners: dict[str, str] = {} + + for adr_path in sorted(adr_directory.glob("[0-9][0-9][0-9][0-9]-*.md")): + adr_number = adr_path.name[:4] + previous_owner = owners.get(adr_number) + assert previous_owner is None, ( + f"ADR {adr_number} is reused by {previous_owner} and {adr_path.name}" + ) + owners[adr_number] = adr_path.name diff --git a/packages/validity-analysis/tests/test_handoff.py b/packages/validity-analysis/tests/test_handoff.py new file mode 100644 index 000000000..64d1ab8c6 --- /dev/null +++ b/packages/validity-analysis/tests/test_handoff.py @@ -0,0 +1,221 @@ +"""Regression tests for governed selection-validity analysis handoffs.""" + +from dataclasses import replace +from datetime import datetime, timedelta, timezone +import json + +import pytest + +from orgmetra_validity_analysis import ( + REVIEWED_FAST_MLSIRM_REVISION, + ValidationAnalysisHandoff, + build_validation_analysis_handoff, +) + +TENANT = "10000000-0000-7000-8000-000000000001" +HANDOFF = "validation_analysis_handoff:11111111-1111-4111-8111-111111111111" +STUDY = "validation_study:22222222-2222-4222-8222-222222222222" +JOB = "job_profile:33333333-3333-4333-8333-333333333333" +PREDICTOR = "predictor_snapshot:44444444-4444-4444-8444-444444444444" +CRITERION = "criterion_snapshot:55555555-5555-4555-8555-555555555555" +POPULATION = "study_population_snapshot:66666666-6666-4666-8666-666666666666" +POLICY = "decision_policy:77777777-7777-4777-8777-777777777777" +PLAN = "validation_analysis_plan:88888888-8888-4888-8888-888888888888" +ACTOR = "actor:99999999-9999-4999-8999-999999999999" +REVIEWER = "actor:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" +DIGEST_A = "a" * 64 +DIGEST_B = "b" * 64 +DIGEST_C = "c" * 64 +DIGEST_D = "d" * 64 +DIGEST_E = "e" * 64 +REQUESTED_AT = datetime(2026, 8, 21, 7, 10, 11, 123456, tzinfo=timezone(timedelta(hours=9))) + + +def valid_kwargs(): + """Return one complete governed handoff input fixture.""" + return { + "tenant_record_id": TENANT, + "handoff_reference": HANDOFF, + "validation_study_reference": STUDY, + "job_profile_reference": JOB, + "predictor_snapshot_reference": PREDICTOR, + "predictor_snapshot_digest": DIGEST_A, + "criterion_snapshot_reference": CRITERION, + "criterion_snapshot_digest": DIGEST_B, + "population_snapshot_reference": POPULATION, + "population_snapshot_digest": DIGEST_C, + "decision_policy_reference": POLICY, + "decision_policy_digest": DIGEST_D, + "analysis_plan_reference": PLAN, + "analysis_plan_digest": DIGEST_E, + "actor_reference": ACTOR, + "reviewer_reference": REVIEWER, + "fast_mlsirm_revision": REVIEWED_FAST_MLSIRM_REVISION, + "requested_at": REQUESTED_AT, + } + + +def handoff(): + """Build the canonical valid handoff fixture.""" + return build_validation_analysis_handoff(**valid_kwargs()) + + +def test_handoff_is_value_minimized_deterministic_and_human_review_only(): + """Bind exact study evidence without exposing raw person-level observations.""" + candidate = handoff() + payload = json.loads(candidate.canonical_json()) + + assert payload["tenant_record_id"] == TENANT + assert payload["validation_study_reference"] == STUDY + assert payload["job_profile_reference"] == JOB + assert payload["fast_mlsirm_revision"] == REVIEWED_FAST_MLSIRM_REVISION + assert payload["requested_at"] == "2026-08-20T22:10:11.123456Z" + assert payload["validation_strategy"] == "criterion_related" + assert payload["kernel_repository"] == "ContextualWisdomLab/fast-mlsirm" + assert payload["kernel_boundary"] == "read_only_pinned_revision" + assert payload["execution_state"] == "not_executed" + assert payload["contains_raw_person_level_values"] is False + assert payload["human_review_required"] is True + assert payload["result_authority"] == "scientific_evidence_only" + assert payload["required_result_evidence"] == [ + "effect_estimate", + "uncertainty_interval", + "sample_size", + "missingness_summary", + "convergence_diagnostics", + ] + assert "person_record" not in candidate.canonical_json() + assert "candidate" not in candidate.canonical_json() + assert repr(candidate) == "ValidationAnalysisHandoff()" + assert len(candidate.sha256_digest()) == 64 + assert candidate.canonical_json() == handoff().canonical_json() + + +@pytest.mark.parametrize( + "bad_tenant", + [ + "not-a-uuid", + "00000000-0000-0000-0000-000000000000", + "ffffffff-ffff-ffff-ffff-ffffffffffff", + "10000000-0000-7000-8000-00000000000A", + 1, + ], +) +def test_tenant_identity_must_follow_protected_operational_uuid_contract(bad_tenant): + """Reject malformed, reserved, non-canonical, and non-text tenant identities.""" + values = valid_kwargs() + values["tenant_record_id"] = bad_tenant + with pytest.raises(ValueError, match="tenant_record_id"): + build_validation_analysis_handoff(**values) + + +@pytest.mark.parametrize( + ("field", "bad", "match"), + [ + ("handoff_reference", "validation_analysis_handoff:not-a-uuid", "handoff_reference"), + ("validation_study_reference", JOB, "validation_study_reference"), + ("job_profile_reference", "job_profile:22222222-2222-7222-8222-222222222222", "job_profile_reference"), + ("predictor_snapshot_reference", 1, "predictor_snapshot_reference"), + ("criterion_snapshot_reference", "criterion_snapshot:" + "a" * 161, "criterion_snapshot_reference"), + ("population_snapshot_reference", "study_population_snapshot:not-a-uuid", "population_snapshot_reference"), + ("decision_policy_reference", "decision_policy:BBBBBBBB-BBBB-4BBB-8BBB-BBBBBBBBBBBB", "decision_policy_reference"), + ("analysis_plan_reference", "validation_analysis_plan:00000000-0000-0000-0000-000000000000", "analysis_plan_reference"), + ("actor_reference", "actor:ffffffff-ffff-ffff-ffff-ffffffffffff", "actor_reference"), + ("reviewer_reference", "actor:bbbbbbbb-bbbb-7bbb-8bbb-bbbbbbbbbbbb", "reviewer_reference"), + ], +) +def test_all_public_references_are_namespaced_opaque_uuid4(field, bad, match): + """Fail closed on wrong namespace, malformed, noncanonical, or non-v4 references.""" + values = valid_kwargs() + values[field] = bad + with pytest.raises(ValueError, match=match): + build_validation_analysis_handoff(**values) + + +@pytest.mark.parametrize( + "field", + [ + "predictor_snapshot_digest", + "criterion_snapshot_digest", + "population_snapshot_digest", + "decision_policy_digest", + "analysis_plan_digest", + ], +) +def test_evidence_digests_are_lowercase_sha256(field): + """Reject weak or noncanonical evidence digests for every source snapshot.""" + values = valid_kwargs() + values[field] = "A" * 64 + with pytest.raises(ValueError, match=field): + build_validation_analysis_handoff(**values) + + +def test_requester_and_reviewer_must_be_distinct(): + """Require accountable independent interpretation instead of self-review.""" + values = valid_kwargs() + values["reviewer_reference"] = ACTOR + with pytest.raises(ValueError, match="different accountable actor"): + build_validation_analysis_handoff(**values) + + +@pytest.mark.parametrize("bad_revision", ["not-a-sha", "A" * 40, "0" * 40]) +def test_fast_mlsirm_revision_is_exactly_the_reviewed_immutable_dependency(bad_revision): + """Reject malformed or unreviewed foreign dependency revisions.""" + values = valid_kwargs() + values["fast_mlsirm_revision"] = bad_revision + with pytest.raises(ValueError, match="fast_mlsirm_revision"): + build_validation_analysis_handoff(**values) + + +@pytest.mark.parametrize( + "bad_requested_at", + [ + datetime(2026, 8, 21, 7, 10), + "2026-08-21T07:10:00+09:00", + ], +) +def test_requested_at_requires_an_aware_datetime(bad_requested_at): + """Reject local-time ambiguity in immutable analysis correlation.""" + values = valid_kwargs() + values["requested_at"] = bad_requested_at + with pytest.raises(ValueError, match="requested_at"): + build_validation_analysis_handoff(**values) + + +@pytest.mark.parametrize( + ("field", "bad", "match"), + [ + ("purpose_code", "other_purpose", "purpose_code"), + ("reason_code", "other_reason", "reason_code"), + ("evidence_version", True, "evidence_version"), + ("evidence_version", 0, "evidence_version"), + ("validation_strategy", "content_validity", "validation_strategy"), + ("kernel_repository", "other/repository", "kernel_repository"), + ("kernel_boundary", "direct_database", "kernel_boundary"), + ("execution_state", "executed", "execution_state"), + ("contains_raw_person_level_values", True, "raw person-level"), + ("human_review_required", False, "human review"), + ("result_authority", "employment_decision", "result_authority"), + ("required_result_evidence", ("effect_estimate",), "required_result_evidence"), + ("next_action", "Auto-approve the result.", "next_action"), + ], +) +def test_direct_construction_cannot_weaken_governance(field, bad, match): + """Keep fixed scientific, privacy, dependency, and human-authority semantics immutable.""" + with pytest.raises(ValueError, match=match): + replace(handoff(), **{field: bad}) + + +def test_codes_must_remain_bounded_descriptive_snake_case_before_fixed_value_check(): + """Exercise code-shape rejection separately from the closed purpose/reason vocabulary.""" + with pytest.raises(ValueError, match="purpose_code"): + replace(handoff(), purpose_code="X") + with pytest.raises(ValueError, match="reason_code"): + replace(handoff(), reason_code="x" * 65) + + +def test_public_dataclass_type_is_constructible_only_with_all_invariants(): + """Document the public immutable type while preserving builder equivalence.""" + values = valid_kwargs() + direct = ValidationAnalysisHandoff(**values) + assert direct == handoff() diff --git a/packages/validity-analysis/tests/test_host_resolution_contract.py b/packages/validity-analysis/tests/test_host_resolution_contract.py new file mode 100644 index 000000000..c87fb1a18 --- /dev/null +++ b/packages/validity-analysis/tests/test_host_resolution_contract.py @@ -0,0 +1,34 @@ +"""Regressions for authoritative requester/reviewer identity separation.""" + +from datetime import datetime, timezone + +from orgmetra_validity_analysis import ( + REVIEWED_FAST_MLSIRM_REVISION, + build_validation_analysis_handoff, +) + + +def test_next_action_requires_resolved_actor_identity_separation() -> None: + """Do not let different opaque actor references masquerade as distinct people.""" + handoff = build_validation_analysis_handoff( + tenant_record_id="10000000-0000-7000-8000-000000000001", + handoff_reference="validation_analysis_handoff:11111111-1111-4111-8111-111111111111", + validation_study_reference="validation_study:22222222-2222-4222-8222-222222222222", + job_profile_reference="job_profile:33333333-3333-4333-8333-333333333333", + predictor_snapshot_reference="predictor_snapshot:44444444-4444-4444-8444-444444444444", + predictor_snapshot_digest="a" * 64, + criterion_snapshot_reference="criterion_snapshot:55555555-5555-4555-8555-555555555555", + criterion_snapshot_digest="b" * 64, + population_snapshot_reference="study_population_snapshot:66666666-6666-4666-8666-666666666666", + population_snapshot_digest="c" * 64, + decision_policy_reference="decision_policy:77777777-7777-4777-8777-777777777777", + decision_policy_digest="d" * 64, + analysis_plan_reference="validation_analysis_plan:88888888-8888-4888-8888-888888888888", + analysis_plan_digest="e" * 64, + actor_reference="actor:99999999-9999-4999-8999-999999999999", + reviewer_reference="actor:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + fast_mlsirm_revision=REVIEWED_FAST_MLSIRM_REVISION, + requested_at=datetime(2026, 8, 21, 1, 0, tzinfo=timezone.utc), + ) + + assert "prove requester and reviewer resolve to distinct authoritative actor identities" in handoff.next_action diff --git a/packages/validity-analysis/tests/test_numeric_overflow_contract.py b/packages/validity-analysis/tests/test_numeric_overflow_contract.py new file mode 100644 index 000000000..a4fb88115 --- /dev/null +++ b/packages/validity-analysis/tests/test_numeric_overflow_contract.py @@ -0,0 +1,18 @@ +"""Regression for malformed worker numerics that overflow float conversion.""" + +import pytest + +from orgmetra_validity_analysis import ConvergenceDiagnostics + + +def test_oversized_worker_numeric_is_rejected_as_value_error() -> None: + """Normalize float-conversion overflow to the package's ValueError contract.""" + oversized_integer = 10**10000 + + with pytest.raises(ValueError, match="finite number"): + ConvergenceDiagnostics( + converged=True, + iterations=1, + objective_value=oversized_integer, + maximum_gradient=0.1, + ) diff --git a/packages/validity-analysis/tests/test_quality_workflow_trigger.py b/packages/validity-analysis/tests/test_quality_workflow_trigger.py new file mode 100644 index 000000000..aed4f4060 --- /dev/null +++ b/packages/validity-analysis/tests/test_quality_workflow_trigger.py @@ -0,0 +1,27 @@ +"""Regression tests for the validity-analysis quality-gate trigger surface.""" + +from pathlib import Path + + +_WORKFLOW_PATH = Path(".github/workflows/validity-analysis-quality.yml") +_SHARED_TEST_CONFIGURATION = ( + ".gitignore", + ".python-version", + "conftest.py", + "packages/conftest.py", + "pyproject.toml", + "pytest.ini", + "setup.cfg", + "tox.ini", +) + + +def test_quality_workflow_retriggers_on_shared_test_configuration() -> None: + """Require shared test/runtime configuration changes to retrigger this gate.""" + workflow = _WORKFLOW_PATH.read_text(encoding="utf-8") + + for path in _SHARED_TEST_CONFIGURATION: + assert f'- "{path}"' in workflow, ( + f"{path} can change package test or clean-checkout behavior and must retrigger " + "Validity Analysis Handoff Quality" + ) diff --git a/packages/validity-analysis/tests/test_result.py b/packages/validity-analysis/tests/test_result.py new file mode 100644 index 000000000..ad00b95f2 --- /dev/null +++ b/packages/validity-analysis/tests/test_result.py @@ -0,0 +1,211 @@ +"""Regression tests for the bounded numerical result contract.""" + +from dataclasses import asdict, replace +from datetime import datetime, timezone +import json + +import pytest + +from orgmetra_validity_analysis import ( + ConvergenceDiagnostics, + MissingnessSummary, + REVIEWED_FAST_MLSIRM_REVISION, + ValidationAnalysisResult, +) + +TENANT = "10000000-0000-7000-8000-000000000001" +RESULT = "validation_analysis_result:11111111-1111-4111-8111-111111111111" +HANDOFF_DIGEST = "a" * 64 +PROVENANCE_DIGEST = "b" * 64 +COMPLETED_AT = datetime(2026, 8, 21, 7, 10, 11, 123456, tzinfo=timezone.utc) + + +def missingness() -> MissingnessSummary: + """Return one realistic aggregate-only missingness summary.""" + return MissingnessSummary( + total_observations=12, + complete_observations=10, + missing_predictor_observations=1, + missing_criterion_observations=1, + ) + + +def convergence(*, converged: bool = True) -> ConvergenceDiagnostics: + """Return one converged or explicitly nonconverged diagnostic record.""" + return ConvergenceDiagnostics( + converged=converged, + iterations=42, + objective_value=-12.5, + maximum_gradient=0.0001, + failure_code=None if converged else "maximum_iterations", + ) + + +def result(**overrides: object) -> ValidationAnalysisResult: + """Build one valid result envelope and apply targeted test overrides.""" + values: dict[str, object] = { + "tenant_record_id": TENANT, + "result_reference": RESULT, + "handoff_digest": HANDOFF_DIGEST, + "provenance_digest": PROVENANCE_DIGEST, + "fast_mlsirm_revision": REVIEWED_FAST_MLSIRM_REVISION, + "model_code": "mlsirm_criterion_related", + "backend": "rust_cpu", + "precision": "f64", + "effect_estimate": 0.42, + "uncertainty_lower": 0.10, + "uncertainty_upper": 0.70, + "sample_size": 12, + "missingness_summary": missingness(), + "convergence_diagnostics": convergence(), + "completed_at": COMPLETED_AT, + } + values.update(overrides) + return ValidationAnalysisResult(**values) + + +def test_aggregate_evidence_is_deterministic_and_redacted() -> None: + """Serialize only aggregate evidence and preserve exact replay bytes.""" + candidate = result() + payload = json.loads(candidate.canonical_json()) + + assert payload["tenant_record_id"] == TENANT + assert payload["backend"] == "rust_cpu" + assert payload["precision"] == "f64" + assert payload["execution_state"] == "completed" + assert payload["result_authority"] == "scientific_evidence_only" + assert payload["missingness_summary"]["total_observations"] == 12 + assert payload["convergence_diagnostics"]["converged"] is True + assert "person_record" not in candidate.canonical_json() + assert repr(candidate) == "ValidationAnalysisResult()" + assert len(candidate.sha256_digest()) == 64 + assert candidate.canonical_json() == result().canonical_json() + + +def test_gpu_and_nonconverged_result_are_explicitly_typed() -> None: + """Record GPU provenance and a typed nonconvergence state without promotion.""" + candidate = result( + backend="rust_gpu", + precision="f32", + convergence_diagnostics=convergence(converged=False), + ) + payload = json.loads(candidate.canonical_json()) + assert payload["backend"] == "rust_gpu" + assert payload["precision"] == "f32" + assert payload["convergence_diagnostics"]["failure_code"] == "maximum_iterations" + + +@pytest.mark.parametrize( + "bad", + [ + {"total_observations": True}, + {"total_observations": -1}, + {"total_observations": 0}, + {"complete_observations": 13}, + {"missing_predictor_observations": 13}, + {"missing_criterion_observations": 13}, + {"complete_observations": 12, "missing_predictor_observations": 1}, + { + "complete_observations": 12, + "missing_predictor_observations": 0, + "missing_criterion_observations": 1, + }, + ], +) +def test_missingness_rejects_invalid_counts(bad: dict[str, object]) -> None: + """Reject booleans, negative counts, empty samples, and impossible totals.""" + values = asdict(missingness()) + values.update(bad) + with pytest.raises(ValueError): + MissingnessSummary(**values) + + +@pytest.mark.parametrize("bad", [True, 0, -1]) +def test_positive_integer_validation_rejects_nonpositive_values(bad: object) -> None: + """Exercise strict sample and iteration bounds.""" + with pytest.raises(ValueError, match="positive integer"): + ConvergenceDiagnostics(True, bad, -1.0, 0.1) + with pytest.raises(ValueError, match="positive integer"): + result(sample_size=bad) + + +@pytest.mark.parametrize("bad", [True, "0.1", float("nan"), float("inf")]) +def test_numeric_fields_reject_boolean_text_and_nonfinite_values(bad: object) -> None: + """Do not accept values that cannot be represented as finite scientific evidence.""" + with pytest.raises(ValueError, match="finite number"): + ConvergenceDiagnostics(True, 1, bad, 0.1) + with pytest.raises(ValueError, match="finite number"): + result(effect_estimate=bad) + + +def test_negative_gradient_and_invalid_convergence_states_fail_closed() -> None: + """Require explicit and internally consistent convergence diagnostics.""" + with pytest.raises(ValueError, match="non-negative"): + ConvergenceDiagnostics(True, 1, 1.0, -0.1) + with pytest.raises(ValueError, match="boolean"): + ConvergenceDiagnostics(1, 1, 1.0, 0.1) + with pytest.raises(ValueError, match="absent"): + ConvergenceDiagnostics(True, 1, 1.0, 0.1, "failed_fit") + with pytest.raises(ValueError, match="required"): + ConvergenceDiagnostics(False, 1, 1.0, 0.1) + with pytest.raises(ValueError, match="required"): + ConvergenceDiagnostics(False, 1, 1.0, 0.1, "") + + +@pytest.mark.parametrize( + "field,bad,match", + [ + ("backend", "numpy", "backend"), + ("precision", "float16", "precision"), + ("uncertainty_lower", 0.8, "uncertainty_lower"), + ("uncertainty_upper", 0.0, "uncertainty_lower"), + ("effect_estimate", 0.8, "effect_estimate"), + ("sample_size", 11, "sample_size"), + ("result_authority", "employment_decision", "result_authority"), + ("execution_state", "not_executed", "execution_state"), + ("contains_raw_person_level_values", True, "raw person-level"), + ("human_review_required", False, "human review"), + ("evidence_version", 2, "evidence_version"), + ], +) +def test_result_invariants_cannot_be_weakened(field: str, bad: object, match: str) -> None: + """Reject malformed intervals, lineage, or governance flags.""" + with pytest.raises(ValueError, match=match): + replace(result(), **{field: bad}) + + +def test_result_requires_canonical_timestamp_and_aggregate_types() -> None: + """Reject naive times, non-contract objects, and subclass method overrides.""" + + class LeakyMissingnessSummary(MissingnessSummary): + def to_dict(self) -> dict[str, object]: + return {**super().to_dict(), "person_record": "must-not-serialize"} + + class LeakyConvergenceDiagnostics(ConvergenceDiagnostics): + def to_dict(self) -> dict[str, object]: + return {**super().to_dict(), "employment_decision": "auto_reject"} + + with pytest.raises(ValueError, match="completed_at"): + result(completed_at=datetime(2026, 8, 21, 7, 10)) + with pytest.raises(ValueError, match="missingness_summary"): + result(missingness_summary=object()) + with pytest.raises(ValueError, match="convergence_diagnostics"): + result(convergence_diagnostics=object()) + with pytest.raises(ValueError, match="missingness_summary"): + result( + missingness_summary=LeakyMissingnessSummary( + total_observations=12, + complete_observations=10, + missing_predictor_observations=1, + missing_criterion_observations=1, + ) + ) + with pytest.raises(ValueError, match="convergence_diagnostics"): + result( + convergence_diagnostics=LeakyConvergenceDiagnostics( + converged=True, + iterations=42, + objective_value=-12.5, + maximum_gradient=0.0001, + ) + ) diff --git a/packages/validity-analysis/tests/test_temporal_evidence_integrity.py b/packages/validity-analysis/tests/test_temporal_evidence_integrity.py new file mode 100644 index 000000000..0431eee55 --- /dev/null +++ b/packages/validity-analysis/tests/test_temporal_evidence_integrity.py @@ -0,0 +1,279 @@ +"""Regression coverage for selection-validity temporal evidence integrity.""" + +from __future__ import annotations + +from datetime import datetime, timedelta, timezone, tzinfo + +import pytest + +from orgmetra_validity_analysis import ( + ConvergenceDiagnostics, + MissingnessSummary, + REVIEWED_FAST_MLSIRM_REVISION, + ValidationAnalysisHandoff, + ValidationAnalysisResult, + build_validation_analysis_handoff, +) +from test_handoff import valid_kwargs +from test_result import result + + +class ForgedDateTime(datetime): + """Datetime subclass able to forge canonical validation evidence.""" + + def astimezone(self, tz=None): # type: ignore[no-untyped-def] + """Keep the hostile subclass alive across UTC normalization.""" + return self + + def isoformat(self, *args, **kwargs) -> str: # type: ignore[no-untyped-def] + """Return an instant different from the underlying evidence instant.""" + return "2099-12-31T23:59:59+00:00" + + +class ForgedReference(str): + """String subclass able to forge namespace and UUID parsing methods.""" + + def startswith(self, prefix, *args): # type: ignore[no-untyped-def] + """Pretend that an invalid namespace has the expected prefix.""" + return True + + def split(self, separator=None, maxsplit=-1): # type: ignore[no-untyped-def] + """Return a valid UUID suffix while retaining invalid source text.""" + return ["validation_analysis_handoff", "11111111-1111-4111-8111-111111111111"] + + +class ForgedFixedText(str): + """String subclass whose comparisons can forge fixed governance values.""" + + def __eq__(self, other): # type: ignore[no-untyped-def] + """Claim equality with any expected governance text.""" + return True + + def __ne__(self, other): # type: ignore[no-untyped-def] + """Claim inequality with no governance text.""" + return False + + def __hash__(self): + """Use a valid fixed-text hash for set membership forgery tests.""" + return hash("rust_cpu") + + +class MutableOffset(tzinfo): + """Timezone fixture whose offset can change after envelope construction.""" + + def __init__(self) -> None: + self.hours = 1 + + def utcoffset(self, dt: datetime | None) -> timedelta: + """Return the currently configured offset.""" + return timedelta(hours=self.hours) + + def dst(self, dt: datetime | None) -> timedelta: + """Return no daylight-saving offset.""" + return timedelta(0) + + +class UnknownOffset(tzinfo): + """Timezone fixture whose UTC offset cannot be resolved.""" + + def utcoffset(self, dt: datetime | None) -> None: + """Return no offset to exercise fail-closed validation.""" + return None + + def dst(self, dt: datetime | None) -> None: + """Return no daylight-saving offset.""" + return None + + +class ExplodingOffset(tzinfo): + """Timezone fixture whose provider raises during offset resolution.""" + + def utcoffset(self, dt: datetime | None) -> timedelta: + """Raise an untrusted provider error.""" + raise RuntimeError("offset provider failed") + + def dst(self, dt: datetime | None) -> timedelta: + """Return no daylight-saving offset when queried separately.""" + return timedelta(0) + + +class MutableReal(float): + """Numeric subclass whose float conversion changes after construction.""" + + def __new__(cls, value: float): + """Create a float-backed value with a separately mutable conversion.""" + instance = super().__new__(cls, value) + instance.current = value + return instance + + def __float__(self) -> float: + """Expose the mutable conversion used by unsafe canonicalization.""" + return self.current + + +def test_handoff_rejects_datetime_subclass_that_can_forge_requested_at() -> None: + """Handoff canonical evidence must not invoke caller-overridable datetime methods.""" + with pytest.raises(ValueError, match="requested_at"): + build_validation_analysis_handoff( + tenant_record_id="10000000-0000-7000-8000-000000000001", + handoff_reference="validation_analysis_handoff:11111111-1111-4111-8111-111111111111", + validation_study_reference="validation_study:22222222-2222-4222-8222-222222222222", + job_profile_reference="job_profile:33333333-3333-4333-8333-333333333333", + predictor_snapshot_reference="predictor_snapshot:44444444-4444-4444-8444-444444444444", + predictor_snapshot_digest="a" * 64, + criterion_snapshot_reference="criterion_snapshot:55555555-5555-4555-8555-555555555555", + criterion_snapshot_digest="b" * 64, + population_snapshot_reference="study_population_snapshot:66666666-6666-4666-8666-666666666666", + population_snapshot_digest="c" * 64, + decision_policy_reference="decision_policy:77777777-7777-4777-8777-777777777777", + decision_policy_digest="d" * 64, + analysis_plan_reference="validation_analysis_plan:88888888-8888-4888-8888-888888888888", + analysis_plan_digest="e" * 64, + actor_reference="actor:99999999-9999-4999-8999-999999999999", + reviewer_reference="actor:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + fast_mlsirm_revision=REVIEWED_FAST_MLSIRM_REVISION, + requested_at=ForgedDateTime(2026, 8, 21, 4, 45, tzinfo=timezone.utc), + ) + + +def test_result_rejects_datetime_subclass_that_can_forge_completed_at() -> None: + """Result canonical evidence must not invoke caller-overridable datetime methods.""" + with pytest.raises(ValueError, match="completed_at"): + ValidationAnalysisResult( + tenant_record_id="10000000-0000-7000-8000-000000000001", + result_reference="validation_analysis_result:11111111-1111-4111-8111-111111111111", + handoff_digest="a" * 64, + provenance_digest="b" * 64, + fast_mlsirm_revision=REVIEWED_FAST_MLSIRM_REVISION, + model_code="mlsirm_criterion_related", + backend="rust_cpu", + precision="f64", + effect_estimate=0.42, + uncertainty_lower=0.10, + uncertainty_upper=0.70, + sample_size=12, + missingness_summary=MissingnessSummary( + total_observations=12, + complete_observations=10, + missing_predictor_observations=1, + missing_criterion_observations=1, + ), + convergence_diagnostics=ConvergenceDiagnostics( + converged=True, + iterations=42, + objective_value=-12.5, + maximum_gradient=0.0001, + ), + completed_at=ForgedDateTime(2026, 8, 21, 4, 45, tzinfo=timezone.utc), + ) + + +def test_handoff_and_result_detach_mutable_timezone_before_digesting() -> None: + """Freeze one UTC instant so later timezone mutation cannot rewrite evidence.""" + handoff_zone = MutableOffset() + handoff_values = valid_kwargs() + handoff_values["requested_at"] = datetime(2026, 8, 21, 4, 45, tzinfo=handoff_zone) + handoff = build_validation_analysis_handoff(**handoff_values) + handoff_before = handoff.canonical_json(), handoff.sha256_digest() + handoff_zone.hours = 2 + assert (handoff.canonical_json(), handoff.sha256_digest()) == handoff_before + + result_zone = MutableOffset() + candidate = result(completed_at=datetime(2026, 8, 21, 4, 45, tzinfo=result_zone)) + result_before = candidate.canonical_json(), candidate.sha256_digest() + result_zone.hours = 2 + assert (candidate.canonical_json(), candidate.sha256_digest()) == result_before + + +@pytest.mark.parametrize( + "timestamp", + [ + datetime.min.replace(tzinfo=timezone(timedelta(hours=1))), + datetime.max.replace(tzinfo=timezone(-timedelta(hours=1))), + datetime(2026, 8, 21, 4, 45, tzinfo=UnknownOffset()), + datetime(2026, 8, 21, 4, 45, tzinfo=ExplodingOffset()), + ], +) +def test_handoff_rejects_unrepresentable_or_untrusted_timestamp(timestamp: datetime) -> None: + """Normalize timezone-provider failures and UTC arithmetic overflow at the boundary.""" + values = valid_kwargs() + values["requested_at"] = timestamp + with pytest.raises(ValueError, match="requested_at"): + build_validation_analysis_handoff(**values) + + +@pytest.mark.parametrize( + "timestamp", + [ + datetime.min.replace(tzinfo=timezone(timedelta(hours=1))), + datetime.max.replace(tzinfo=timezone(-timedelta(hours=1))), + datetime(2026, 8, 21, 4, 45, tzinfo=UnknownOffset()), + datetime(2026, 8, 21, 4, 45, tzinfo=ExplodingOffset()), + ], +) +def test_result_rejects_unrepresentable_or_untrusted_timestamp(timestamp: datetime) -> None: + """Apply the same fail-closed timestamp contract to completed result evidence.""" + with pytest.raises(ValueError, match="completed_at"): + result(completed_at=timestamp) + + +@pytest.mark.parametrize( + "timestamp", + [ + ForgedDateTime(2026, 8, 21, 4, 45, tzinfo=timezone.utc), + datetime(2026, 8, 21, 4, 45, tzinfo=timezone(timedelta(hours=1))), + ], +) +def test_canonicalization_rejects_low_level_timestamp_reinjection(timestamp: datetime) -> None: + """Keep canonicalization fail-closed even if an object is corrupted after construction.""" + handoff = build_validation_analysis_handoff(**valid_kwargs()) + object.__setattr__(handoff, "requested_at", timestamp) + with pytest.raises(ValueError, match="requested_at"): + handoff.canonical_json() + + candidate = result() + object.__setattr__(candidate, "completed_at", timestamp) + with pytest.raises(ValueError, match="completed_at"): + candidate.canonical_json() + + +def test_handoff_rejects_runtime_text_subclasses_before_serialization() -> None: + """Reject text subclasses that can forge reference, digest, code, or fixed-value checks.""" + for field, value in ( + ("tenant_record_id", ForgedFixedText("10000000-0000-7000-8000-000000000001")), + ("handoff_reference", ForgedReference("wrong_namespace:invalid")), + ("predictor_snapshot_digest", ForgedFixedText("a" * 64)), + ("purpose_code", ForgedFixedText("selection_validity_analysis")), + ("fast_mlsirm_revision", ForgedFixedText(REVIEWED_FAST_MLSIRM_REVISION)), + ("validation_strategy", ForgedFixedText("criterion_related")), + ("next_action", ForgedFixedText("governed")), + ): + values = valid_kwargs() + values[field] = value + with pytest.raises(ValueError, match=field): + ValidationAnalysisHandoff(**values) + + +def test_result_snapshots_numeric_values_before_canonicalization() -> None: + """Detach mutable numeric subclasses before recording scientific evidence bytes.""" + objective = MutableReal(-12.5) + gradient = MutableReal(0.0001) + diagnostics = ConvergenceDiagnostics( + converged=True, + iterations=42, + objective_value=objective, + maximum_gradient=gradient, + ) + estimate = MutableReal(0.42) + candidate = result(effect_estimate=estimate, convergence_diagnostics=diagnostics) + before = candidate.canonical_json(), candidate.sha256_digest() + objective.current = -1.0 + gradient.current = 0.5 + estimate.current = 0.69 + assert (candidate.canonical_json(), candidate.sha256_digest()) == before + + +def test_result_rejects_forged_backend_text() -> None: + """Do not allow a string subclass to forge an allowed backend membership check.""" + with pytest.raises(ValueError, match="backend"): + result(backend=ForgedFixedText("numpy")) diff --git a/packages/validity-analysis/tests/test_workflow_trigger_contract.py b/packages/validity-analysis/tests/test_workflow_trigger_contract.py new file mode 100644 index 000000000..2a0295fc9 --- /dev/null +++ b/packages/validity-analysis/tests/test_workflow_trigger_contract.py @@ -0,0 +1,13 @@ +"""Regression for the validity package's repository-wide ADR numbering trigger.""" + +from pathlib import Path + + +def test_any_adr_change_runs_the_adr_numbering_regression() -> None: + """Keep ADR uniqueness enforcement reachable when any decision record changes.""" + repository_root = Path(__file__).resolve().parents[3] + workflow = (repository_root / ".github" / "workflows" / "validity-analysis-quality.yml").read_text( + encoding="utf-8" + ) + + assert ' - "docs/adr/**"' in workflow