From 98b49ae8a9bcd0f614768057f66d76675b87ebad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 12:54:09 +0900 Subject: [PATCH 1/8] build(validity): make package test setup reproducible --- .../workflows/validity-analysis-quality.yml | 1 - .gitignore | 1 + manifest.json | 2 +- packages/validity-analysis/CHANGELOG.md | 1 + packages/validity-analysis/README.md | 4 +- packages/validity-analysis/pyproject.toml | 1 + packages/validity-analysis/uv.lock | 195 ++++++++++++++++++ 7 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 packages/validity-analysis/uv.lock diff --git a/.github/workflows/validity-analysis-quality.yml b/.github/workflows/validity-analysis-quality.yml index 5b1ae41fc..68881550b 100644 --- a/.github/workflows/validity-analysis-quality.yml +++ b/.github/workflows/validity-analysis-quality.yml @@ -50,7 +50,6 @@ jobs: 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 diff --git a/.gitignore b/.gitignore index 1f0b61ba5..9f2c96bec 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ secrets/ artifacts/ reports/ *.log +/.codegraph/ diff --git a/manifest.json b/manifest.json index 97f2bab14..f02e69c8f 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":"f4963e79205b896f527dab402a0b6ef455bd44ed5f948d9d6126a6bb20c8b5a3","bytes":388,"lines":38},{"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}]} diff --git a/packages/validity-analysis/CHANGELOG.md b/packages/validity-analysis/CHANGELOG.md index 9e9e67b43..2eee72259 100644 --- a/packages/validity-analysis/CHANGELOG.md +++ b/packages/validity-analysis/CHANGELOG.md @@ -9,3 +9,4 @@ - 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. +- Make the package-local `uv` and pytest source configuration canonical so the quality workflow does not depend on a manually supplied `PYTHONPATH`. diff --git a/packages/validity-analysis/README.md b/packages/validity-analysis/README.md index 527241d5b..f9509bdde 100644 --- a/packages/validity-analysis/README.md +++ b/packages/validity-analysis/README.md @@ -30,8 +30,8 @@ Before an approved offline validation worker executes the handoff, the Orgmetra Run: ```bash -PYTHONPATH=packages/validity-analysis/src \ -python -m pytest -c packages/validity-analysis/pyproject.toml packages/validity-analysis/tests +uv run --project packages/validity-analysis --extra test \ + pytest 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 index 5547eabcb..34c3bc238 100644 --- a/packages/validity-analysis/pyproject.toml +++ b/packages/validity-analysis/pyproject.toml @@ -16,6 +16,7 @@ where = ["src"] [tool.pytest.ini_options] testpaths = ["tests"] +pythonpath = ["src"] addopts = [ "--cov=orgmetra_validity_analysis", "--cov-branch", diff --git a/packages/validity-analysis/uv.lock b/packages/validity-analysis/uv.lock new file mode 100644 index 000000000..0f6e3b423 --- /dev/null +++ b/packages/validity-analysis/uv.lock @@ -0,0 +1,195 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.15.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00", size = 936952, upload-time = "2026-08-06T13:50:24.442Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/48/bc8d4ba7b37551a767bd863f15b3f80182b271c2f55975356f5f7dbe94c2/coverage-7.15.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4fedd1f7f428f9fe83b1ead5e7cc87a43427be31aadafbac3ac0636dc7abb22", size = 222543, upload-time = "2026-08-06T13:47:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/20/dd/88d6f83f1fffc974a3691a34a97951c5b12df7512a6782c5963883cbc058/coverage-7.15.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37e2f0cdf58e2e1fed4e4d5a8f8786ae2f7eb80b478016876667dc4a01d60a97", size = 222905, upload-time = "2026-08-06T13:47:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/bd/5c/54ee0d4748585bb0acab9891cd8d92f2d3593165b4e59fc9de113bfb3140/coverage-7.15.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb55d0e70bb15f2e81477613627286581414693d74ac7963c93a790dd453ca9d", size = 254407, upload-time = "2026-08-06T13:47:40.488Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3f/f0642a372f494bd0d7dad3b497083b910194a5f1c88be2c94fef707c3b59/coverage-7.15.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:899b9da30f3c6c336566e3707495bb23e8302d39d862f01fa78c48b99b9437e2", size = 257145, upload-time = "2026-08-06T13:47:41.931Z" }, + { url = "https://files.pythonhosted.org/packages/71/17/8b46d0ed68251016002ec972c8fc0119961a765d0984cafb8bf317c43758/coverage-7.15.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d15715e8c46552827e5e4f30a35575a2dbcad14454cf3284c54483946bd16931", size = 258257, upload-time = "2026-08-06T13:47:43.527Z" }, + { url = "https://files.pythonhosted.org/packages/30/b8/8498a0e72d0adbe15477dd07463d2b3bb2c9f6a4815e8589e50939e2c3ae/coverage-7.15.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:002a438859f7b430bc99afeaf01a6d187dad1d0dc907b64cdeffc632a5db8fd8", size = 260517, upload-time = "2026-08-06T13:47:45.121Z" }, + { url = "https://files.pythonhosted.org/packages/41/e1/7dce19c3bdb1e3dd63e769508216500edad81bd5f69a26d724e32aceaf78/coverage-7.15.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4193a04b518f7968f3099755f5509ee7cccc6dc2b92a6b14841934d22e222c9", size = 254785, upload-time = "2026-08-06T13:47:46.541Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b1/e1494703c675a2561723cd9b89f45c9168782c31280c611b1f767851e57c/coverage-7.15.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e98dcc55d572b38e69d117da7e8e8efb8500f1f5eaf81ecd460a63220790b839", size = 256176, upload-time = "2026-08-06T13:47:48.155Z" }, + { url = "https://files.pythonhosted.org/packages/73/76/a5629d270fb638a43a4b10466f51e2f49d532c1aa4da2913cbbb150bbe0a/coverage-7.15.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:af6c538498ce66c10d3fd541c2a8d5b03da5850355add34e6cba564210cb9e72", size = 254321, upload-time = "2026-08-06T13:47:49.757Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/9c44447218435d5766b911534f9d798144a5560f85e9a54ebe5f3f5d19f9/coverage-7.15.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1d10025d96ea89fc2f73714dbc4cbd433fe012c1ac9e23f895d7728b238b6e52", size = 258390, upload-time = "2026-08-06T13:47:51.248Z" }, + { url = "https://files.pythonhosted.org/packages/de/36/c1e127616fb3fa18a9ff71e76c417f2fd7424332a4870015ac224ef4c039/coverage-7.15.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d802e1947603162ded419bff83ac7489820355d2b856dfb09206574e3a37ac0c", size = 253894, upload-time = "2026-08-06T13:47:52.816Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b9/fdb92c8ae7a8bb9b850cc253b7b3b9c8526f68130002048b5671cd510d09/coverage-7.15.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2de40895718f91951b86712b4c5b694acaf9a0a49be13874896f599a1eed3f4", size = 255763, upload-time = "2026-08-06T13:47:54.296Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/a7d51b2587c7bdb76e71b0896d2565bf7d60436b5122fc83e511adb1f7cd/coverage-7.15.4-cp312-cp312-win32.whl", hash = "sha256:5c3431b2161279b7db5c2a1aa58ae02e5cb8c3c42d93a5094be3f5537bd5b11b", size = 224597, upload-time = "2026-08-06T13:47:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/49/b9/5c5f80cc55f5acaaca6dee677626bfcec8c87204a7809b438b08e84f4571/coverage-7.15.4-cp312-cp312-win_amd64.whl", hash = "sha256:6befeab5fb2b51c958ca4ac6c5d141a1e8240f4f76e46350f1911963deda49cd", size = 225135, upload-time = "2026-08-06T13:47:57.52Z" }, + { url = "https://files.pythonhosted.org/packages/47/e4/2a4561f89ff6bf7c925c287d0f2cce8bdf139c3a33735c87e3203401cf94/coverage-7.15.4-cp312-cp312-win_arm64.whl", hash = "sha256:67bc345491ab55b837277d76f5775d057e8c7f1ac44d890d8c2c82adde258c6f", size = 224515, upload-time = "2026-08-06T13:47:58.977Z" }, + { url = "https://files.pythonhosted.org/packages/f1/84/651a9310859673aaa3b3203f1aa1641ca60fcf2494683e1c9474c7172780/coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921", size = 222565, upload-time = "2026-08-06T13:48:00.796Z" }, + { url = "https://files.pythonhosted.org/packages/82/f9/4dcf700137e8af550670f4d74d1b63828ce93e1e2b05e5f10710eb2ea987/coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e", size = 222936, upload-time = "2026-08-06T13:48:02.391Z" }, + { url = "https://files.pythonhosted.org/packages/07/4a/612ff1e780b3fbfd637486f542f84adc5503873d8b5d279dec1ffeef9414/coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36", size = 253926, upload-time = "2026-08-06T13:48:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/b0/04/d1cff1c2ead4708a6a79c01d3736b6a25bd38a36678398f72a8dd33dfad9/coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4", size = 256523, upload-time = "2026-08-06T13:48:05.996Z" }, + { url = "https://files.pythonhosted.org/packages/b9/80/d34e13fb4b293cbdb9665838cf5522077b8ad14ef947550631a4bced36a5/coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c", size = 257759, upload-time = "2026-08-06T13:48:08.036Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e7/2c5fe7636fdb0732fe0f09f308a5b066864078b7fc61f6678e8478554f2e/coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7", size = 259890, upload-time = "2026-08-06T13:48:09.834Z" }, + { url = "https://files.pythonhosted.org/packages/92/28/9689f0858dfff59c2ea688938ab9fa2925631235df67126a42b6c5c70ae1/coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25", size = 254121, upload-time = "2026-08-06T13:48:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/785077c230c157243eb5aa9a26c3be260ecd02001bead54a3cada3df8e03/coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b", size = 255891, upload-time = "2026-08-06T13:48:13.209Z" }, + { url = "https://files.pythonhosted.org/packages/d4/90/e20371b17b40f912f21305c2db2f30efa3de306f7320fc916804872c85a4/coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78", size = 253859, upload-time = "2026-08-06T13:48:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/05/49/25371987ee459a5f67c0427fb75c74f9358e65f2c71fe75bf41c1b6c5fcb/coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f", size = 258011, upload-time = "2026-08-06T13:48:16.464Z" }, + { url = "https://files.pythonhosted.org/packages/30/6e/32e67467f6154bf4f1c4f63b05acc5097cba4237d45bbeeea446b52e8ac1/coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d", size = 253676, upload-time = "2026-08-06T13:48:18.493Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/8b24192e89286399765155251f99ee9f070a9d637109018ac23d99b99f6f/coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff", size = 255453, upload-time = "2026-08-06T13:48:20.057Z" }, + { url = "https://files.pythonhosted.org/packages/16/6f/8b41ebdf67c87854e17c035336a90f1cfbad0c14c2a584301be6ff148718/coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c", size = 224605, upload-time = "2026-08-06T13:48:21.655Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e2/2946c7f0b42b152ecb21ff1bdad72e3d301e790c0c487e4a86e8c9f69347/coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4", size = 225148, upload-time = "2026-08-06T13:48:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/9e/83/3f4a69957f48ae7a0aba76c34743f88963d607b19e03f3f8e66f91cae0f9/coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf", size = 224536, upload-time = "2026-08-06T13:48:25.117Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ac/748cf29eeb2d6be34a3176ce26a4f49e38085ee08e8935f05f6f26ed7e0f/coverage-7.15.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:770e9325ab5ea6d56f77e59b29ecfe0ac20b57a82a601876f90494a4dda0386f", size = 222608, upload-time = "2026-08-06T13:48:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/0b/02/1abbf5c984677b0aa439cdacaccbf38d248939d8ef8fe1cc7a50d73edb77/coverage-7.15.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d12b33a3a50a1676b7784dc8d00a0c6d66a9f2add4b85a041c19b6a7e53ef23c", size = 222940, upload-time = "2026-08-06T13:48:28.432Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e1/ff8f9f53d9fcf586125b55d0b1f04ec1c14955fee41e83d5814bee141bb5/coverage-7.15.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082", size = 253985, upload-time = "2026-08-06T13:48:29.995Z" }, + { url = "https://files.pythonhosted.org/packages/a1/26/595759762e514e81be1d7d01ed03444303bcd152226a6529998d253f9201/coverage-7.15.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff97a14362eef486483ed44042ca2027ea257df6ff768e62358ee0c9776925ac", size = 256492, upload-time = "2026-08-06T13:48:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/b79aabac54d482be23b5fcdd4f4662bff24a78edc4ee29201726929936d5/coverage-7.15.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a325e815318638aed1655d9c06e6d7c2d3d46c09231ce988070428a8762d734", size = 257837, upload-time = "2026-08-06T13:48:33.186Z" }, + { url = "https://files.pythonhosted.org/packages/09/0f/bf7f297885a5bf6fd71e5782404e0ff059ca09e8711ceb3a08544abde45a/coverage-7.15.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:474223409d88eb20d2d6a0d37ea60e8647a65a90cc008dc1f0410af5f64f1e0d", size = 260152, upload-time = "2026-08-06T13:48:34.75Z" }, + { url = "https://files.pythonhosted.org/packages/fd/f1/296744e854ff8368542343457414380465e9ceefb9192342feb9d3bc461d/coverage-7.15.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f2f62ae3cd189dd2e13aece758c57b3eecbd27be070dbd4cbd10936049e5dbf", size = 253978, upload-time = "2026-08-06T13:48:36.434Z" }, + { url = "https://files.pythonhosted.org/packages/55/b0/bbdb2e9057493e66220a2e149ca2d301ba0e3a58a83bd6b90de9826d16f3/coverage-7.15.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39ece820e29e0a2ba34b3ecb3be83c27e997eed8926f2ba6fe7ce7a0bda5843b", size = 255846, upload-time = "2026-08-06T13:48:38.317Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/38015b2b6d21258713bd17e76b59d033b191efb5703589cffd037dfbca20/coverage-7.15.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f21b56dcace11dfe013014201f577dcd592b2a9b72182d930361b47cf6f73f25", size = 253808, upload-time = "2026-08-06T13:48:39.993Z" }, + { url = "https://files.pythonhosted.org/packages/0b/64/0d515c1e60ee6fbfd1a0e79c07cd87d388a233b7adc37758735677203808/coverage-7.15.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:93a3a0b662abcc10c73a47cbc72cd60f63618d6989fb2d1286e50eacd974f303", size = 258081, upload-time = "2026-08-06T13:48:41.971Z" }, + { url = "https://files.pythonhosted.org/packages/91/71/04d9e7a3642146c6351338aef4ef85ab11dbbb54744c13245caba1aad1c0/coverage-7.15.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:141fae2cabf5569b782c10afc4c850ce10f618c13f8db54765cba99cc839da1f", size = 253624, upload-time = "2026-08-06T13:48:43.731Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a7/6c28b74c81ebff66987b0e2522ba5cffa3e90b0c33cb6a2eb264d4ee8cf1/coverage-7.15.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:81294c7e6ab30c5f74c0353b11b2fd6320e72d9bee6ac73b357caa8b916323a5", size = 255280, upload-time = "2026-08-06T13:48:45.58Z" }, + { url = "https://files.pythonhosted.org/packages/52/af/bc19996a7014b98d7bbb0f0939453c67074af65784a3aa16a789a07381fa/coverage-7.15.4-cp314-cp314-win32.whl", hash = "sha256:7bbd7d6418e0dab31a206af5203bd43ae36edb8e7fba1940b055d3e9249290d7", size = 224768, upload-time = "2026-08-06T13:48:47.525Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/219484e476d6e101ba0a444852579e05f5b75c37c611a42ed1190f73ef62/coverage-7.15.4-cp314-cp314-win_amd64.whl", hash = "sha256:f0204ed122758782970526057093f448051a39db9d810d4e344bb87a3546f425", size = 225259, upload-time = "2026-08-06T13:48:49.513Z" }, + { url = "https://files.pythonhosted.org/packages/b7/66/fa77daf4e383e5f776dac62c2409b6af81910ae6fe326bd5170dba74cc63/coverage-7.15.4-cp314-cp314-win_arm64.whl", hash = "sha256:9e71e7bc71c686a123347ae47a0de33a175e797a85bb57b791492adf4eec8ed8", size = 224684, upload-time = "2026-08-06T13:48:51.235Z" }, + { url = "https://files.pythonhosted.org/packages/58/5b/f03bf0ce362bbf3f785fa5219620d00778d4ac6fc9e407734828e9c672f6/coverage-7.15.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7c922735321eef3f87c280a3d39afff6b646723a2880b862cda4ac7a093b8aa8", size = 223338, upload-time = "2026-08-06T13:48:52.896Z" }, + { url = "https://files.pythonhosted.org/packages/0f/76/e77d0ae22501831cc9f92193e8a957a5caa1dd177f90a6d1d9b106242d92/coverage-7.15.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f41c17c4668a655ce96d090d8d5ffdc24ef64b5a02f9753884d08483e8a4a41a", size = 223609, upload-time = "2026-08-06T13:48:54.688Z" }, + { url = "https://files.pythonhosted.org/packages/82/1a/b1f089da8d38ac612fa2dd6dc7f4a1a7657d12f3e261d2996edd3a838d0b/coverage-7.15.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46822e9b6ff1c6a72b518c162c44a8f45a61a1d609c51084bf5b16c023c5037b", size = 264970, upload-time = "2026-08-06T13:48:56.403Z" }, + { url = "https://files.pythonhosted.org/packages/bf/31/e66d98d6e9c7fcc88470f1e234eaf6b1950dc0dfbf797f7282c1c861da24/coverage-7.15.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d6f4955b73b5445271379a59e3792b0d978f42d4a01e0cf7a67d9c33a3bb0a5", size = 267088, upload-time = "2026-08-06T13:48:58.41Z" }, + { url = "https://files.pythonhosted.org/packages/59/a1/ae94eb2c541add426378408379f233591e069040b1e2cdb33df9498a0682/coverage-7.15.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3fc9e047706fb4a9abb54f719d3aa643e80e5bb3818182c40aee01ac0f0247ba", size = 269508, upload-time = "2026-08-06T13:49:00.42Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c7/88a10694a1c6a213569766aba9f25847b28155d4ac731b13226db216356d/coverage-7.15.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05e491d4f3165d62d4f5c8fd48dfeabf2ae8f42cbbd484319af33ea851b78982", size = 270629, upload-time = "2026-08-06T13:49:02.234Z" }, + { url = "https://files.pythonhosted.org/packages/b3/34/d8b8232e5e55169933b59aabcef2fedfa4b9d8897361bb80fcbda146505f/coverage-7.15.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:226c66e80ec0598d3b9b4874123df167ccca342aca8714f77cac6829688ee09c", size = 264043, upload-time = "2026-08-06T13:49:04.102Z" }, + { url = "https://files.pythonhosted.org/packages/7e/35/58b009dbf8c471c7224716478b9fed4a7e1af15320e1ed41660978504663/coverage-7.15.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac41cc14bebda0dbfb0628036b7f75706935c95bcc07fefe9a0f93614aa60a57", size = 266963, upload-time = "2026-08-06T13:49:05.821Z" }, + { url = "https://files.pythonhosted.org/packages/62/aa/57fbda1b42c892968273c56b6ee9dc0f1310850859230a507bc7873b1f65/coverage-7.15.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8af623e5cd92080acddd02b38f2f406a2c3a0893c38950b211890361448fbf26", size = 264569, upload-time = "2026-08-06T13:49:07.706Z" }, + { url = "https://files.pythonhosted.org/packages/98/8a/360e6e7f24d477b7e889703af0afa878d15b6d4d8d2a822b2835c169a879/coverage-7.15.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07545711d4f0f32852a18f18ad11f76f0109909d09e78b9008b4cfc67e829429", size = 268299, upload-time = "2026-08-06T13:49:09.587Z" }, + { url = "https://files.pythonhosted.org/packages/4e/89/6f701261aee21b6b5fa8f7872229406dc917e125069448292223bf213606/coverage-7.15.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a0865421cfdc53654b342d515e5a233187590882d20b95752150e53f65460017", size = 263413, upload-time = "2026-08-06T13:49:11.604Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0f/6f04036edc260ed425af83e834f627fad48941ce97b50bfe6edd8b6fa623/coverage-7.15.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:460115e32ee40566476db5048f9bec1e842c127ad8e6f8be745aad3ac9cbc839", size = 265725, upload-time = "2026-08-06T13:49:13.38Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ce/d19b5d4d5c49a7bfb925fd74310fee7d28bc99520ac3367ccbc54e662518/coverage-7.15.4-cp314-cp314t-win32.whl", hash = "sha256:cbde877ef9dd7baf272b9bfef2b8a25edd45d9170fc326951dd20eb480335e85", size = 225079, upload-time = "2026-08-06T13:49:15.265Z" }, + { url = "https://files.pythonhosted.org/packages/26/bb/7aa1b3b173faee0679037ca950bbbe1247273656697994d8d13f80f8d4b4/coverage-7.15.4-cp314-cp314t-win_amd64.whl", hash = "sha256:3da9e92d1c551fd7563833e9ade686efb0c4b7363ab7681a94283958c950bf5e", size = 225911, upload-time = "2026-08-06T13:49:17.279Z" }, + { url = "https://files.pythonhosted.org/packages/81/1c/4ea9e47426d80038d9222db3c4534cb6021a74b237d3ff97ffd33b6600dd/coverage-7.15.4-cp314-cp314t-win_arm64.whl", hash = "sha256:3a54f5a0d85050c73a38f6793090ee83974531e67fe5e57a1da9bee11398aa5e", size = 225219, upload-time = "2026-08-06T13:49:19.293Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c4/dc5d2ac8f9142e7ec7de66e7bf0591db29d78955a040bd915870d9c0e657/coverage-7.15.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2c9872e4d9dc5d3cf616bf4b382f5a00359305a5be666a3dd0b5cdb4e49597f9", size = 222604, upload-time = "2026-08-06T13:49:21.279Z" }, + { url = "https://files.pythonhosted.org/packages/70/39/33e63df81fe2ee100897451841c821467635923e58e37c6bd4b46dd8106c/coverage-7.15.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:e101dbb4b9b72f0cddd8cdc8c9c5b47f456766f5e0ac82dbfb75e5c55409b78a", size = 222944, upload-time = "2026-08-06T13:49:23.187Z" }, + { url = "https://files.pythonhosted.org/packages/99/1f/ef3ffb5557febc75a0d97aa459d0266d7d741110265121cc6d8539343d44/coverage-7.15.4-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d1abebdb047729e852b9c77a00497dfbeb11eb3a117e037d7dbc3ac8e5f5c54", size = 254050, upload-time = "2026-08-06T13:49:25.008Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f5/1f0f6f77698c3601ca0ae7431e34b24c62ca2f06fecb23b73ed1f651d2be/coverage-7.15.4-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d28a4a899354d0ea6214cc59b4fa19eefbce1b9ff1688ab579acf49e894bd3fb", size = 256967, upload-time = "2026-08-06T13:49:26.896Z" }, + { url = "https://files.pythonhosted.org/packages/03/7a/2ed9bed79925f4367c83c77f66a89e5ca7229c288d2d19ad5f36d1ca0070/coverage-7.15.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb3c2aacea411cc7e1d27712490c11108e2de1d39019ae32915493a59a8b9ed", size = 258587, upload-time = "2026-08-06T13:49:28.692Z" }, + { url = "https://files.pythonhosted.org/packages/45/8c/fa34044f71b7cc4ecb6da9c2408770959b0591fa9b5fb6fb6bca38f94298/coverage-7.15.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9447978a92f405d301123cfd39ff49895490efb769a758fe2734c7f631bf8ce", size = 260785, upload-time = "2026-08-06T13:49:30.472Z" }, + { url = "https://files.pythonhosted.org/packages/4f/54/d5727ce36b4524a7394ab9f5f1df378e1f23affcdab01037dc8655185cc7/coverage-7.15.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:050467a7983b8e2fe7dd41a78bb30c3e7f8c0b8cafda14b1c46f8b5e3cf2dd3c", size = 254545, upload-time = "2026-08-06T13:49:32.271Z" }, + { url = "https://files.pythonhosted.org/packages/dc/e6/6e3783e576719590194bdffb6dd6d85490801785b7c331e35a245d8cb8b5/coverage-7.15.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d003b7a5708ddad5c206c79607a6b92abb6fc13c57d99d8a4468cc03a2941ced", size = 256682, upload-time = "2026-08-06T13:49:34.089Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/bacdbde18b69ed2de424fcf64d9fb0a4913753d4f0eca8bae9daad69f4bd/coverage-7.15.4-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c38efe30fd74e5c19e9433f11fb1f5dc9c6522770971b7c6145bbaa413dc8800", size = 254560, upload-time = "2026-08-06T13:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a3/1fb927196e3477c1b48831169ab58ba08f451ba87ae311ff1de68b26a616/coverage-7.15.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1f4f826d70f772ab8b0c052329580d7fe8b8abd191e4ce0c8f81aec6614665d3", size = 258792, upload-time = "2026-08-06T13:49:38.01Z" }, + { url = "https://files.pythonhosted.org/packages/41/58/30d4c149c69053de0edfe325614c1d28d508f62b1783e0e4a234d2e49136/coverage-7.15.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4a4bf917c9953f57c957be31c1cd504e3bd2f34d4a352b9d391a3025336f6768", size = 253968, upload-time = "2026-08-06T13:49:39.934Z" }, + { url = "https://files.pythonhosted.org/packages/89/e4/77f639371b918aad30dda4051f95404b43578f7f2e2f87ba73e02ed1ff37/coverage-7.15.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1c9bf40ebef178a45192c75c4964760bb261b0e6ad725da5fc4c93f674f19753", size = 255893, upload-time = "2026-08-06T13:49:41.825Z" }, + { url = "https://files.pythonhosted.org/packages/5c/62/13be29b3ddab35f14c87967a4820a05106d2a3eccb4fa4ff550bf30b75e0/coverage-7.15.4-cp315-cp315-win32.whl", hash = "sha256:43619d04c3671792d2c4706ae8bf45e265dc87bbd4078189ef8b847ea1e74be2", size = 224768, upload-time = "2026-08-06T13:49:44.08Z" }, + { url = "https://files.pythonhosted.org/packages/a1/70/af0c6be0f964af6954f6b74bc109b0dbca02824696d2520fb17fe1ab06e3/coverage-7.15.4-cp315-cp315-win_amd64.whl", hash = "sha256:be619439dbcd31a2eab10b32de9fff62c26ed4bab69dc32b8363fdaaa0882809", size = 225242, upload-time = "2026-08-06T13:49:45.899Z" }, + { url = "https://files.pythonhosted.org/packages/4f/2d/f3bd3aab899fc9efc18b53133ee68f5f98574ef480649b23e12962226387/coverage-7.15.4-cp315-cp315-win_arm64.whl", hash = "sha256:def597967dafc2e8d97c9097ea453c464e0bb8ed38f193a43070f10dc623bb6d", size = 224674, upload-time = "2026-08-06T13:49:48.322Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ca/f69251cd63eabc6438321aea22148754cce758a26bde07dd490e3fe7cfc5/coverage-7.15.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c7dbc748ac8a1e3e59a2b28bea47675e6e778081dbbf081bde0d75def2fcbe1d", size = 223333, upload-time = "2026-08-06T13:49:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a7/037b53b2885b0d8447064432491a4d5a1014cd9f97a594d53acd0c04541a/coverage-7.15.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2413074a5ecbb61a01a7888fc72db0ca324d13588c5b38bc0dd8564cdcdfea26", size = 223630, upload-time = "2026-08-06T13:49:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/80/4f/152b8a4779ae90da11bb24f7467df8a59f0be48a5c52acb856325ca48289/coverage-7.15.4-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4e6f6f632b7b2f714bf7a1346e8f97b650ee71f3c298aaad42a2ab60f0f07645", size = 264489, upload-time = "2026-08-06T13:49:54.52Z" }, + { url = "https://files.pythonhosted.org/packages/10/2d/84b4b9e0e1dd6528a51920ff7031f35b789382e467a28ec6a5a578cb8812/coverage-7.15.4-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8df457da2249d3c75ca2e5e835d59c725abfe92d27fdff6cd99eed85b51d5e9a", size = 267567, upload-time = "2026-08-06T13:49:56.721Z" }, + { url = "https://files.pythonhosted.org/packages/53/fc/ba01cc25299f9f8a2c8b02d3b28c53f3543d9fbfbe4e74fa2760b48f163e/coverage-7.15.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:050f66a08805acb5b8a23c6d4a517b1ecf82c08e81ed0e4bd727df065e5c6624", size = 270123, upload-time = "2026-08-06T13:49:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d0/db2647cbf40b14f8c308f94ff7bf89c06d564e59f396906edf50086ec788/coverage-7.15.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1587fb771d1ccceef708fdde1e5af8c7ed24b486b61d13a321acb7d8145390aa", size = 271107, upload-time = "2026-08-06T13:50:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/4d2d17924552c458bb4f77dd631f0e3bc92fbbdf2d2d916cd4b33bbfd5b1/coverage-7.15.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b4f1c3a69ca580f3fbd6b2046915f536d7f586874f25c1bb23add2a3c88d50f", size = 264955, upload-time = "2026-08-06T13:50:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/ee/de/dc010c7a3691f396d93bbc26bfcafa1c2a3a351cd520470f15faf5795bd5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:ffb58d7eff5b7f6ecc6fa21d6288ab7f968a212cb67d682c269c09b9eba3b66f", size = 267949, upload-time = "2026-08-06T13:50:05.557Z" }, + { url = "https://files.pythonhosted.org/packages/78/ea/dc96a11375e83c045c2f7c61fb6918277cfe9401db7c0f7b1d111a84b2e5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d9df165544774574ee004b953023d1bebada1894a80b1052a43d798b0f676e67", size = 264421, upload-time = "2026-08-06T13:50:07.612Z" }, + { url = "https://files.pythonhosted.org/packages/c8/86/b77131a0f9503ce461cd577076147d7a9040f0c5dda772686f729e2cc9cb/coverage-7.15.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:f9de0a24a4079b53e523b5c5e2c5945ec251ab486652659955187cf255a259bc", size = 269121, upload-time = "2026-08-06T13:50:09.58Z" }, + { url = "https://files.pythonhosted.org/packages/24/24/944bc35007862955e7ebf05754e645419dcf5d7526c52735cfa2715e8ebf/coverage-7.15.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:150089274bdc9f940628552cb92844e0223c987f1902ab8efe9f45a2ec758d88", size = 264565, upload-time = "2026-08-06T13:50:11.722Z" }, + { url = "https://files.pythonhosted.org/packages/c7/cc/a3bb9f93e7e740659163e2ea584f8196ddcd2c456a5dbe15f6c50105fec1/coverage-7.15.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a58a94fed5da6997d258e8f7668c1e195fbd04a691d781b7558f1e468f9e68bc", size = 266522, upload-time = "2026-08-06T13:50:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/49/dd/e0e40f3560d878d888c580698ff5ad1179f5e1c3ac949684ef66b41a3817/coverage-7.15.4-cp315-cp315t-win32.whl", hash = "sha256:ebd5a6d8466ff30836572f3ba2cae8a5e8f85029b1c6d5e2ed338dc472a5166a", size = 225068, upload-time = "2026-08-06T13:50:15.825Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/37732ea80eebc30e976e4cdab15c190bc42d96959a42e38ddf6f8c60468f/coverage-7.15.4-cp315-cp315t-win_amd64.whl", hash = "sha256:288bde2a2d7ab6b6c2d7252fcde8b524387f2d970bdba9658fc6f8bbcaef0f9b", size = 225895, upload-time = "2026-08-06T13:50:17.928Z" }, + { url = "https://files.pythonhosted.org/packages/c6/08/1e00f7923eaaba45fb3d51dd794125fc766304b1df264f3a9c6557bfb30e/coverage-7.15.4-cp315-cp315t-win_arm64.whl", hash = "sha256:68be5e1de60ff13c9095bbec0e5a7fa45b33b101752215b91345ea1f61c4a278", size = 225213, upload-time = "2026-08-06T13:50:19.981Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84", size = 214332, upload-time = "2026-08-06T13:50:22.192Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "orgmetra-validity-analysis" +version = "0.1.0" +source = { editable = "." } + +[package.optional-dependencies] +test = [ + { name = "pytest" }, + { name = "pytest-cov" }, +] + +[package.metadata] +requires-dist = [ + { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3" }, + { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=5.0" }, +] +provides-extras = ["test"] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] From d0cb0f5998576bbdd80df811457e6527b8ddcc95 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 13:42:53 +0900 Subject: [PATCH 2/8] feat(validity): add pinned Rust recovery execution lane --- docs/TEST_STRATEGY.md | 2 +- docs/TRACEABILITY.md | 2 +- docs/TRD.md | 2 +- ...ned-selection-validity-analysis-handoff.md | 9 +- .../2026-08-21-validity-rust-execution.md | 82 ++++ manifest.json | 2 +- packages/validity-analysis/CHANGELOG.md | 2 + packages/validity-analysis/README.md | 24 +- .../run_fast_mlsirm_recovery_evidence.py | 19 + .../orgmetra_validity_analysis/__init__.py | 10 + .../orgmetra_validity_analysis/execution.py | 382 ++++++++++++++++++ .../recovery_runner.py | 262 ++++++++++++ .../validity-analysis/tests/test_execution.py | 328 +++++++++++++++ .../tests/test_execution_script_contract.py | 47 +++ .../tests/test_recovery_runner.py | 231 +++++++++++ 15 files changed, 1394 insertions(+), 10 deletions(-) create mode 100644 docs/superpowers/plans/2026-08-21-validity-rust-execution.md create mode 100644 packages/validity-analysis/scripts/run_fast_mlsirm_recovery_evidence.py create mode 100644 packages/validity-analysis/src/orgmetra_validity_analysis/execution.py create mode 100644 packages/validity-analysis/src/orgmetra_validity_analysis/recovery_runner.py create mode 100644 packages/validity-analysis/tests/test_execution.py create mode 100644 packages/validity-analysis/tests/test_execution_script_contract.py create mode 100644 packages/validity-analysis/tests/test_recovery_runner.py diff --git a/docs/TEST_STRATEGY.md b/docs/TEST_STRATEGY.md index c20813b72..a1a734e4e 100644 --- a/docs/TEST_STRATEGY.md +++ b/docs/TEST_STRATEGY.md @@ -77,7 +77,7 @@ Orgmetra does not combine fast-mlsirm and TEPP into one dependency. ### fast-mlsirm - Canonical repository: `ContextualWisdomLab/fast-mlsirm`. -- Reviewed immutable revision for this baseline: `fb67ced09d8ee00542c05d56374537a9a7239751`. +- Reviewed immutable revision for this baseline: `04d0bc21a2a20693bcf16108cd76d394fe844d23`. - Orgmetra contract identifier: `orgmetra.fast_mlsirm.v1`. - Owner: `workforce_validation`; the normal online path consumes a Psychometrics Commons immutable result snapshot rather than calling the kernel from a role workspace. - Backend: Rust production arithmetic with bounded CPU multithreading and GPU parity for material kernels. NumPy is a reference/parity path only. diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 22a4178fe..ca7ba22f2 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -33,7 +33,7 @@ | Keyverse identity and authorization | API Gateway / purpose-bound authorization | Published OIDC/API identity and scope contract plus Orgmetra-owned `orgmetra_keyverse_adapter` policy evaluation | tenant/actor/resource agreement, exact opaque target-resource reference, purpose, operation-specific scope, requested-field minimization, opaque subject, no stored credentials or protected values in authorization evidence | ADR-0002, ADR-0008 | implemented_on_protected_main | | naruon communication and calendar | Integration Hub | Published API/event adapter | idempotency, delivery audit, no direct table access | ADR-0002 | planned | | Psychometrics Commons @ `cc5850a0d1eacbbf16d03075534fce460a8286e6` | Workforce Validation | Immutable response/result snapshot contract | pinned revision, model/version/provenance snapshot, immutable result linkage, no direct application-table access | ADR-0002 | accepted_architecture | -| fast-mlsirm @ `fb67ced09d8ee00542c05d56374537a9a7239751` | Workforce Validation | Published `orgmetra.fast_mlsirm.v1` result contract; direct calls only from approved offline validation worker | pinned revision, contract identifier, backend/result provenance, CPU/GPU parity evidence where material, no duplicated kernel | ADR-0002 | accepted_architecture | +| fast-mlsirm @ `04d0bc21a2a20693bcf16108cd76d394fe844d23` | Workforce Validation | Published `orgmetra.fast_mlsirm.v1` result contract; direct calls only from approved offline validation worker | pinned revision, contract identifier, backend/result provenance, CPU/GPU parity evidence where material, no duplicated kernel | ADR-0002 | accepted_architecture | | TEPP temporal evidence | Workforce Validation | Published package/API contract | temporal provenance and version binding | ADR-0002 | planned | | MHTML ETL Gateway / mightyETL | Governed Migration | Published ETL contract | lineage, idempotency, reconciliation, rollback | ADR-0002 | planned | | Semantic Data Portal / OriginWeave / LineageWeave | Evidence and lineage adapters | Published API/event contracts | provenance, tenant ACL, retention, export controls | ADR-0002 | planned | diff --git a/docs/TRD.md b/docs/TRD.md index c6e5e5e2f..7c1233938 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -85,7 +85,7 @@ These identifiers are canonical across deployment names, ACLs, metrics, generate | `keyverse_adapter` | Keyverse OIDC/SCIM contract | `integration_hub` | | `naruon_adapter` | Naruon communication-intent contract | `integration_hub` | | `psychometrics_commons_adapter` | immutable response/result snapshot contract pinned to `cc5850a0d1eacbbf16d03075534fce460a8286e6` | `workforce_validation` | -| `fast_mlsirm_adapter` | `orgmetra.fast_mlsirm.v1`, repository `ContextualWisdomLab/fast-mlsirm` pinned to `fb67ced09d8ee00542c05d56374537a9a7239751`; online role workspaces consume it through Psychometrics Commons, while direct calls are limited to an approved offline validation worker | `workforce_validation` | +| `fast_mlsirm_adapter` | `orgmetra.fast_mlsirm.v1`, repository `ContextualWisdomLab/fast-mlsirm` pinned to `04d0bc21a2a20693bcf16108cd76d394fe844d23`; online role workspaces consume it through Psychometrics Commons, while direct calls are limited to an approved offline validation worker | `workforce_validation` | | `tepp_adapter` | `orgmetra.tepp.v1`, repository `ContextualWisdomLab/TEPP` pinned to `40adac9a26a8af85147ffa2795fb548ea243e0e5` | `workforce_validation` | | `semantic_data_portal_adapter` | versioned ontology and data-catalog contract | `job_architecture` | | `contextual_orchestrator_adapter` | schema-bound draft and verification operations; no authoritative writes | `job_architecture` and `integration_hub` | diff --git a/docs/adr/0027-governed-selection-validity-analysis-handoff.md b/docs/adr/0027-governed-selection-validity-analysis-handoff.md index 6db6c3a1c..b116658f1 100644 --- a/docs/adr/0027-governed-selection-validity-analysis-handoff.md +++ b/docs/adr/0027-governed-selection-validity-analysis-handoff.md @@ -28,7 +28,7 @@ Orgmetra adds a leaf `orgmetra_validity_analysis` package whose `ValidationAnaly 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. +The package additionally defines `RustExecutionRequest` and `RustRecoveryEvidence`. The request makes cross-sectional, nested multilevel, multiple-membership, and longitudinal input structure explicit. Only cross-sectional and nested multilevel designs are runnable in this slice; multiple-membership and longitudinal requests fail closed until a reviewed estimator exists. The optional read-only `run_fast_mlsirm_recovery_evidence.py` runner verifies the exact revision, invokes the foreign package's public Rust API in its own uv environment, and converts only aggregate simulation-recovery output into a receipt. Recovery RMSE is not mapped to `ValidationAnalysisResult.effect_estimate`. Before an approved validity execution, the Orgmetra host must still 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 @@ -39,11 +39,14 @@ The package does not invoke fast-mlsirm. An approved offline worker is the later - 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. - Human interpretation remains explicit and separate from numerical output. +- A real bounded Rust CPU smoke run now proves the pinned worker path and preserves `max_iter_reached` as an explicit nonconvergence state without promoting it to validity evidence. - 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. +- The package does not estimate criterion-related validity, correct for measurement error/range restriction, evaluate adverse impact, or assert legal compliance. The optional runner executes only a bounded synthetic recovery smoke. +- No GPU run or CPU/GPU numerical parity result is claimed until hardware-backed paired measurements are captured. +- Multiple-membership and longitudinal contracts are represented but have no numerical estimator and intentionally fail closed. - 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 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. @@ -51,6 +54,8 @@ The package does not invoke fast-mlsirm. An approved offline worker is the later 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, 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. +On 2026-08-21, the optional runner was executed against fast-mlsirm revision `04d0bc21a2a20693bcf16108cd76d394fe844d23` for 48 synthetic persons and 3 items with Rust CPU and four requested Rayon threads. Both cross-sectional and nested multilevel runs produced aggregate recovery receipts; both were explicitly `max_iter_reached` after one bounded iteration. This is smoke evidence for the pinned execution path, not estimator acceptance or protected-branch truth. + ## References See `docs/doctoring/validation-analysis-handoff-references.md`. diff --git a/docs/superpowers/plans/2026-08-21-validity-rust-execution.md b/docs/superpowers/plans/2026-08-21-validity-rust-execution.md new file mode 100644 index 000000000..860c665ce --- /dev/null +++ b/docs/superpowers/plans/2026-08-21-validity-rust-execution.md @@ -0,0 +1,82 @@ +# Rust-First Validity Execution Evidence Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add a governed, reproducible adapter boundary that records real pinned fast-mlsirm Rust CPU recovery evidence without misrepresenting recovery metrics as criterion-related validity estimates. + +**Architecture:** Orgmetra owns only immutable request and recovery-evidence contracts. A small evidence runner invokes the separately checked-out fast-mlsirm revision through its existing public API, verifies the exact Git revision, and converts only aggregate worker output into a redacted receipt. Cross-sectional and nested multilevel designs are runnable; multiple-membership and longitudinal designs remain explicit contract-only capabilities until a reviewed estimator exists. + +**Tech Stack:** Python 3.12, stdlib dataclasses/json/subprocess, uv, pytest-cov, pinned fast-mlsirm Rust backend. + +**Spec:** `docs/adr/0027-governed-selection-validity-analysis-handoff.md` and the user-provided Orgmetra execution requirements. + +## Global Constraints + +- Keep `ContextualWisdomLab/fast-mlsirm` a read-only dedicated-writer dependency at revision `04d0bc21a2a20693bcf16108cd76d394fe844d23`. +- Never map simulation recovery RMSE to `ValidationAnalysisResult.effect_estimate` or claim criterion-related validity from this lane. +- Do not carry raw person-level observations, credentials, or foreign application database access across the boundary. +- Use Rust CPU execution for the runnable evidence path; GPU evidence is recorded only when an actual GPU run succeeds, and no GPU parity claim is made without paired measurements. +- Preserve 100% owned statement and branch coverage for the package. + +--- + +### Task 1: Add fail-closed execution and design contracts + +**Files:** +- Create: `packages/validity-analysis/src/orgmetra_validity_analysis/execution.py` +- Create: `packages/validity-analysis/src/orgmetra_validity_analysis/recovery_runner.py` +- Modify: `packages/validity-analysis/src/orgmetra_validity_analysis/__init__.py` +- Test: `packages/validity-analysis/tests/test_execution.py` +- Test: `packages/validity-analysis/tests/test_recovery_runner.py` + +**Interfaces:** +- `RustExecutionRequest` binds an opaque execution reference, handoff digest, dataset digest, exact kernel revision, design code, dimensions, seed, backend, and device. +- `RustRecoveryEvidence` stores aggregate Rust fit/recovery output and remains `scientific_evidence_only`. +- `build_rust_recovery_evidence(request, worker_output, completed_at)` validates a JSON-like worker response and binds it to `request.sha256_digest()`. +- `RustExecutionRequest.runnable` is true only for `cross_sectional` and `nested_multilevel`; `multiple_membership` and `longitudinal` are contract-only and raise `UnsupportedExecutionDesign` when execution is requested. + +- [x] **Step 1: Write tests for deterministic redacted request/evidence JSON, exact revision pinning, design capability guards, malformed worker output, and request/evidence digest linkage.** +- [x] **Step 2: Run `env -u PYTHONPATH uv run --project packages/validity-analysis --extra test pytest packages/validity-analysis/tests/test_execution.py`; observe the expected RED failure before the new module exists.** +- [x] **Step 3: Implement the two frozen dataclasses and one builder with standard-library validation only; do not import fast-mlsirm into the package.** +- [x] **Step 4: Export only the reviewed public symbols and rerun the focused test until it is GREEN.** +- [x] **Step 5: Run the complete package gate and confirm 100% statement/branch coverage.** + +### Task 2: Add the real pinned fast-mlsirm recovery evidence runner + +**Files:** +- Create: `packages/validity-analysis/scripts/run_fast_mlsirm_recovery_evidence.py` +- Modify: `packages/validity-analysis/README.md` +- Modify: `packages/validity-analysis/CHANGELOG.md` +- Test: `packages/validity-analysis/tests/test_execution_script_contract.py` + +**Interfaces:** +- CLI: `uv run --project packages/validity-analysis --extra test python packages/validity-analysis/scripts/run_fast_mlsirm_recovery_evidence.py --fast-mlsirm-path /private/tmp/orgmetra-fast-mlsirm-04d0 --design-code nested_multilevel`. +- The runner verifies a clean `git -C ` checkout and exact `HEAD`, invokes `uv run --frozen --no-editable --project python -c ...` with external uv/Cargo build directories, requests `backend="rust"` and `rust_device="cpu"`, uses a deterministic seed and small bounded sample, and emits only canonical aggregate evidence JSON. +- The worker uses `cluster_id` for nested multilevel evidence and records `max_iter_reached` explicitly when the bounded smoke run does not converge. +- The runner does not invoke unsupported multiple-membership or longitudinal designs and exits with an actionable non-zero error. + +- [x] **Step 1: Write a contract test that checks the CLI exposes the exact revision/path/design arguments and rejects an unpinned checkout without running model code.** +- [x] **Step 2: Run the focused script-contract test with `--no-cov` and confirm the two contract tests pass.** +- [x] **Step 3: Implement the bounded subprocess runner with no shell interpolation of untrusted path data, exact clean-revision verification before and after execution, minimal environment forwarding, external uv/Cargo build directories, and JSON-only stdout.** +- [x] **Step 4: Run the real runner against a clean sparse worktree of revision `04d0bc21a2a20693bcf16108cd76d394fe844d23` and preserve the observed output as a local verification artifact, distinguishing bounded smoke evidence from estimator acceptance.** +- [x] **Step 5: Document the command, observed Rust CPU evidence, unsupported design boundaries, and the unrun GPU parity requirement.** + +### Task 3: Repository and handoff verification + +**Files:** +- Modify: `docs/adr/0027-governed-selection-validity-analysis-handoff.md` +- No change: `docs/product-technical-gap-baseline.md` is not present on the PR #57 stacked base; the active gap baseline remains tracked by PR #53. +- Modify: `manifest.json` + +- [x] **Step 1: Run the package gate, repository validator, root npm validation, `git diff --check`, and CodeGraph status from the isolated worktree.** +- [ ] **Step 2: Re-check the exact branch head and PR #58 state without bypassing protection or manufacturing approval.** +- [ ] **Step 3: Request independent review evidence when the hosted review path is available; keep the PR unmerged while checks/review are queued or missing.** +- [x] **Step 4: Update the active plan and handoff notes with observed, inferred, and still-open evidence separately.** + +## Self-review checklist + +- Recovery evidence is never constructively accepted as a validity result. +- The external repository is read-only and pinned by exact commit. +- Unsupported temporal and multiple-membership execution fails closed instead of flattening the design. +- No GPU parity or protected-branch truth is claimed without fresh runtime evidence. +- Every new production branch has a package test and the package remains at 100% statement/branch coverage. diff --git a/manifest.json b/manifest.json index f02e69c8f..2d935867b 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":"f4963e79205b896f527dab402a0b6ef455bd44ed5f948d9d6126a6bb20c8b5a3","bytes":388,"lines":38},{"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":"f4963e79205b896f527dab402a0b6ef455bd44ed5f948d9d6126a6bb20c8b5a3","bytes":388,"lines":38},{"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":"647f8edfb2f38f9ee6541b8c881683d09c64291fae7c04b8c78ef3d2afdfb0b8","bytes":16534,"lines":135},{"path":"docs/THREAT_MODEL.md","sha256":"f314f375c2e41252536de224c7bc7e4a10ab8f340cb86642724e7399e32f4252","bytes":6736,"lines":23},{"path":"docs/TRACEABILITY.md","sha256":"4a57e25f61baea95cd73442151515b031efe42762b9b970de884c24fecf88d0a","bytes":11462,"lines":40},{"path":"docs/TRD.md","sha256":"466a93d7b35c655fb91cfd85a7f76cf5f82637d534bf77c5cf8661438cc2a95a","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}]} \ No newline at end of file diff --git a/packages/validity-analysis/CHANGELOG.md b/packages/validity-analysis/CHANGELOG.md index 2eee72259..d63e52c0e 100644 --- a/packages/validity-analysis/CHANGELOG.md +++ b/packages/validity-analysis/CHANGELOG.md @@ -10,3 +10,5 @@ - 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. - Make the package-local `uv` and pytest source configuration canonical so the quality workflow does not depend on a manually supplied `PYTHONPATH`. +- Add fail-closed cross-sectional, nested multilevel, multiple-membership, and longitudinal execution contracts plus aggregate Rust recovery evidence. +- Add an exact-revision read-only runner for bounded fast-mlsirm Rust CPU recovery smoke evidence; recovery metrics remain separate from criterion-related validity results. diff --git a/packages/validity-analysis/README.md b/packages/validity-analysis/README.md index f9509bdde..032f577c4 100644 --- a/packages/validity-analysis/README.md +++ b/packages/validity-analysis/README.md @@ -1,6 +1,6 @@ # 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`. +This package creates an immutable **selection-validity analysis handoff**, validates the matching numerical result envelope, and records a separate pinned-kernel recovery-evidence receipt. It is the boundary between Orgmetra's authoritative validation-study evidence and numerical work owned by `ContextualWisdomLab/fast-mlsirm`. ## What it does @@ -10,16 +10,32 @@ The resulting canonical JSON is digest-addressable, contains no raw person-level `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. Missingness counts must be internally possible: complete observations cannot overlap either predictor-missing or criterion-missing observations beyond the declared sample total. The result envelope accepts only the exact governed `MissingnessSummary` and `ConvergenceDiagnostics` runtime types, preventing subclass method overrides from adding unreviewed or person-level fields to canonical audit evidence. It never promotes a result to an employment decision; human review remains mandatory. +`RustExecutionRequest` makes cross-sectional, nested multilevel, multiple-membership, and longitudinal design metadata explicit. Only cross-sectional and nested multilevel requests are currently runnable. `RustRecoveryEvidence` records aggregate simulation-recovery metrics from a bounded worker run; it is not a criterion-related validity estimate and cannot be converted into `ValidationAnalysisResult.effect_estimate`. + +The read-only evidence runner verifies the exact pinned checkout before starting the foreign worker: + +```bash +uv run --project packages/validity-analysis --extra test \ + python packages/validity-analysis/scripts/run_fast_mlsirm_recovery_evidence.py \ + --fast-mlsirm-path /private/tmp/orgmetra-fast-mlsirm-04d0 \ + --handoff-digest --design-code nested_multilevel \ + --rust-device cpu --worker-count 4 +``` + +The bounded run uses `backend="rust"`, a deterministic seed, a small synthetic matrix, and `RAYON_NUM_THREADS=4`. It emits only canonical aggregate JSON. A `max_iter_reached` result remains explicit recovery evidence and is not estimator acceptance. + ## What it does not do -- It does **not** run statistics. -- It does **not** run or reproduce the fast-mlsirm numerical kernel. +- It does **not** estimate criterion-related validity or run an approved offline validity worker. +- The core package does **not** import fast-mlsirm; the optional evidence script invokes only the pinned public API for bounded recovery evidence. - It does **not** query fast-mlsirm or any other CWL application's database. +- It does **not** estimate multiple-membership or longitudinal designs; those contracts fail closed. +- It does **not** claim GPU availability or GPU/CPU numerical parity without paired runtime measurements. - 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. +The fast-mlsirm repository remains a dedicated-writer dependency. The runner reads a separately checked-out exact revision and never writes that repository. ## Host obligations diff --git a/packages/validity-analysis/scripts/run_fast_mlsirm_recovery_evidence.py b/packages/validity-analysis/scripts/run_fast_mlsirm_recovery_evidence.py new file mode 100644 index 000000000..eb5da9162 --- /dev/null +++ b/packages/validity-analysis/scripts/run_fast_mlsirm_recovery_evidence.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +"""Run a bounded pinned fast-mlsirm Rust recovery smoke and emit one receipt.""" + +from __future__ import annotations + +from pathlib import Path +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) + +from orgmetra_validity_analysis.recovery_runner import main + + +if __name__ == "__main__": # pragma: no cover + try: + sys.exit(main()) + except (RuntimeError, ValueError) as exc: + print(f"error: {exc}", file=sys.stderr) + sys.exit(2) diff --git a/packages/validity-analysis/src/orgmetra_validity_analysis/__init__.py b/packages/validity-analysis/src/orgmetra_validity_analysis/__init__.py index 81acbc0d1..24913541b 100644 --- a/packages/validity-analysis/src/orgmetra_validity_analysis/__init__.py +++ b/packages/validity-analysis/src/orgmetra_validity_analysis/__init__.py @@ -5,12 +5,22 @@ ValidationAnalysisHandoff, build_validation_analysis_handoff, ) +from .execution import ( + RustExecutionRequest, + RustRecoveryEvidence, + UnsupportedExecutionDesign, + build_rust_recovery_evidence, +) from .result import ConvergenceDiagnostics, MissingnessSummary, ValidationAnalysisResult __all__ = [ "REVIEWED_FAST_MLSIRM_REVISION", "ValidationAnalysisHandoff", "build_validation_analysis_handoff", + "RustExecutionRequest", + "RustRecoveryEvidence", + "UnsupportedExecutionDesign", + "build_rust_recovery_evidence", "ConvergenceDiagnostics", "MissingnessSummary", "ValidationAnalysisResult", diff --git a/packages/validity-analysis/src/orgmetra_validity_analysis/execution.py b/packages/validity-analysis/src/orgmetra_validity_analysis/execution.py new file mode 100644 index 000000000..fc45f265f --- /dev/null +++ b/packages/validity-analysis/src/orgmetra_validity_analysis/execution.py @@ -0,0 +1,382 @@ +"""Governed adapter contracts for real pinned fast-mlsirm recovery evidence.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from datetime import datetime, timezone +from hashlib import sha256 +from math import isfinite +from numbers import Real +import json +import re +from uuid import UUID + +from .handoff import REVIEWED_FAST_MLSIRM_REVISION + +_DIGEST_PATTERN = re.compile(r"^[0-9a-f]{64}$") +_REFERENCE_PATTERN = re.compile(r"^[a-z][a-z0-9_]{1,63}:[0-9a-f-]{36}$") +_DESIGN_CODES = frozenset( + {"cross_sectional", "nested_multilevel", "multiple_membership", "longitudinal"} +) +_RUNNABLE_DESIGNS = frozenset({"cross_sectional", "nested_multilevel"}) +_RECOVERY_FIELDS = frozenset( + {"parameter_rmse_mean", "latent_rmse", "distance_rmse", "gamma_abs_error"} +) +_WORKER_FIELDS = frozenset( + { + "model", + "backend", + "rust_device", + "status", + "n_iter", + "objective", + "n_persons", + "n_items", + "n_clusters", + "recovery_summary", + } +) + + +class UnsupportedExecutionDesign(ValueError): + """Indicate that a valid design contract has no reviewed estimator yet.""" + + +def _validate_reference(value: object, prefix: str, field_name: str) -> None: + """Require an opaque UUID-shaped reference in the expected namespace.""" + if ( + not isinstance(value, str) + or not value.startswith(f"{prefix}:") + or _REFERENCE_PATTERN.fullmatch(value) is None + ): + raise ValueError(f"{field_name} must be an opaque {prefix} reference") + try: + parsed = UUID(value.split(":", 1)[1]) + except (AttributeError, TypeError, ValueError) as exc: + raise ValueError(f"{field_name} must be an opaque {prefix} reference") from exc + if str(parsed) != value.split(":", 1)[1] or parsed.version != 4: + raise ValueError(f"{field_name} must be an opaque {prefix} UUIDv4 reference") + + +def _validate_digest(value: object, field_name: str) -> None: + """Require a lowercase SHA-256 digest without carrying the source values.""" + if not isinstance(value, str) or _DIGEST_PATTERN.fullmatch(value) is None: + raise ValueError(f"{field_name} must be lowercase SHA-256 hex") + + +def _validate_positive_integer(value: object, field_name: str) -> None: + """Require a positive integer and reject booleans as integers.""" + if type(value) is not int or value <= 0: + raise ValueError(f"{field_name} must be a positive integer") + + +def _validate_nonnegative_integer(value: object, field_name: str) -> None: + """Require a non-negative integer and reject booleans as integers.""" + if type(value) is not int or value < 0: + raise ValueError(f"{field_name} must be a non-negative integer") + + +def _finite_number(value: object, field_name: str) -> float: + """Return one finite real number and reject booleans or non-numeric text.""" + if isinstance(value, bool) or not isinstance(value, Real): + raise ValueError(f"{field_name} must be a finite number") + number = float(value) + if not isfinite(number): + raise ValueError(f"{field_name} must be a finite number") + return number + + +def _finite_nonnegative_number(value: object, field_name: str) -> float: + """Return one finite non-negative real number.""" + number = _finite_number(value, field_name) + if number < 0: + raise ValueError(f"{field_name} must be non-negative") + return number + + +def _canonical_timestamp(value: object, field_name: str) -> str: + """Render one timezone-aware instant as precision-preserving UTC text.""" + if not isinstance(value, datetime) or value.tzinfo is None or value.utcoffset() is None: + raise ValueError(f"{field_name} must be timezone-aware") + return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + + +@dataclass(frozen=True, slots=True, repr=False) +class RustExecutionRequest: + """Bind one non-person-level execution request to a reviewed Rust kernel.""" + + execution_reference: str + handoff_digest: str + dataset_digest: str + fast_mlsirm_revision: str + design_code: str + sample_size: int + item_count: int + seed: int + cluster_count: int | None = None + occasion_count: int = 1 + maximum_memberships: int = 1 + worker_count: int = 1 + backend: str = "rust" + rust_device: str = "cpu" + + def __post_init__(self) -> None: + """Fail closed on malformed identity, design, or execution metadata.""" + _validate_reference( + self.execution_reference, "validity_execution", "execution_reference" + ) + _validate_digest(self.handoff_digest, "handoff_digest") + _validate_digest(self.dataset_digest, "dataset_digest") + if self.fast_mlsirm_revision != REVIEWED_FAST_MLSIRM_REVISION: + raise ValueError("fast_mlsirm_revision must equal the reviewed immutable revision") + if self.design_code not in _DESIGN_CODES: + raise ValueError("design_code is not a governed execution design") + if self.backend != "rust": + raise ValueError("backend must remain rust for this execution lane") + if self.rust_device not in {"cpu", "gpu"}: + raise ValueError("rust_device must be cpu or gpu") + _validate_positive_integer(self.sample_size, "sample_size") + _validate_positive_integer(self.item_count, "item_count") + _validate_nonnegative_integer(self.seed, "seed") + _validate_positive_integer(self.occasion_count, "occasion_count") + _validate_positive_integer(self.maximum_memberships, "maximum_memberships") + _validate_positive_integer(self.worker_count, "worker_count") + if self.design_code == "nested_multilevel": + if self.cluster_count is None or type(self.cluster_count) is not int or self.cluster_count < 2: + raise ValueError("nested_multilevel requires cluster_count >= 2") + if self.occasion_count != 1: + raise ValueError("nested_multilevel requires occasion_count == 1") + if self.maximum_memberships != 1: + raise ValueError("nested_multilevel requires maximum_memberships == 1") + elif self.design_code == "cross_sectional": + if self.cluster_count is not None: + raise ValueError("cross_sectional cannot carry cluster_count") + if self.occasion_count != 1: + raise ValueError("cross_sectional requires occasion_count == 1") + if self.maximum_memberships != 1: + raise ValueError("cross_sectional requires maximum_memberships == 1") + elif self.design_code == "multiple_membership": + if self.cluster_count is not None: + raise ValueError("multiple_membership cannot carry cluster_count") + if self.maximum_memberships < 2: + raise ValueError("multiple_membership requires maximum_memberships >= 2") + elif self.occasion_count < 2: + raise ValueError("longitudinal requires occasion_count >= 2") + + @property + def runnable(self) -> bool: + """Return whether this design has a reviewed numerical execution path.""" + return self.design_code in _RUNNABLE_DESIGNS + + def require_runnable(self) -> None: + """Raise when a valid contract has no reviewed estimator implementation.""" + if not self.runnable: + raise UnsupportedExecutionDesign( + f"{self.design_code} is contract-only; no reviewed estimator is available" + ) + + def __repr__(self) -> str: + """Return a redacted representation suitable for routine logs.""" + return "RustExecutionRequest()" + + def canonical_json(self) -> str: + """Return deterministic request metadata without raw observations.""" + payload = { + "backend": self.backend, + "cluster_count": self.cluster_count, + "dataset_digest": self.dataset_digest, + "design_code": self.design_code, + "execution_reference": self.execution_reference, + "fast_mlsirm_revision": self.fast_mlsirm_revision, + "handoff_digest": self.handoff_digest, + "item_count": self.item_count, + "maximum_memberships": self.maximum_memberships, + "occasion_count": self.occasion_count, + "runnable": self.runnable, + "rust_device": self.rust_device, + "sample_size": self.sample_size, + "seed": self.seed, + "worker_count": self.worker_count, + } + return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + + def sha256_digest(self) -> str: + """Return SHA-256 over the exact canonical request bytes.""" + return sha256(self.canonical_json().encode("utf-8")).hexdigest() + + +@dataclass(frozen=True, slots=True, repr=False) +class RustRecoveryEvidence: + """Store aggregate simulation-recovery evidence without promoting validity.""" + + evidence_reference: str + request_digest: str + fast_mlsirm_revision: str + design_code: str + backend: str + rust_device: str + model_code: str + sample_size: int + item_count: int + cluster_count: int | None + seed: int + convergence_status: str + iterations: int + objective_value: Real + parameter_rmse_mean: Real + latent_rmse: Real + distance_rmse: Real + gamma_abs_error: Real + completed_at: datetime + result_authority: str = "scientific_evidence_only" + execution_state: str = "completed" + contains_raw_person_level_values: bool = False + human_review_required: bool = True + + def __post_init__(self) -> None: + """Reject unlinked, non-finite, or decision-like recovery evidence.""" + _validate_reference( + self.evidence_reference, + "validity_recovery_evidence", + "evidence_reference", + ) + _validate_digest(self.request_digest, "request_digest") + if self.fast_mlsirm_revision != REVIEWED_FAST_MLSIRM_REVISION: + raise ValueError("fast_mlsirm_revision must equal the reviewed immutable revision") + if self.design_code not in _RUNNABLE_DESIGNS: + raise ValueError("recovery evidence requires a runnable design") + if self.backend != "rust": + raise ValueError("backend must remain rust") + if self.rust_device not in {"cpu", "gpu"}: + raise ValueError("rust_device must be cpu or gpu") + if self.model_code != "mlsirm_recovery": + raise ValueError("model_code must remain mlsirm_recovery") + _validate_positive_integer(self.sample_size, "sample_size") + _validate_positive_integer(self.item_count, "item_count") + _validate_nonnegative_integer(self.seed, "seed") + if self.cluster_count is not None: + _validate_positive_integer(self.cluster_count, "cluster_count") + if not isinstance(self.convergence_status, str) or self.convergence_status not in { + "converged", + "max_iter_reached", + }: + raise ValueError("convergence_status is not a governed worker state") + _validate_positive_integer(self.iterations, "iterations") + _finite_number(self.objective_value, "objective_value") + for field_name in ( + "parameter_rmse_mean", + "latent_rmse", + "distance_rmse", + "gamma_abs_error", + ): + _finite_nonnegative_number(getattr(self, field_name), field_name) + _canonical_timestamp(self.completed_at, "completed_at") + if self.result_authority != "scientific_evidence_only": + raise ValueError("result_authority must remain scientific_evidence_only") + if self.execution_state != "completed": + raise ValueError("execution_state must remain completed") + if self.contains_raw_person_level_values is not False: + raise ValueError("recovery evidence must not contain raw person-level values") + if self.human_review_required is not True: + raise ValueError("human review is mandatory for scientific evidence") + + def __repr__(self) -> str: + """Return a redacted representation suitable for routine logs.""" + return "RustRecoveryEvidence()" + + def canonical_json(self) -> str: + """Return deterministic aggregate recovery evidence for audit correlation.""" + payload = { + "backend": self.backend, + "cluster_count": self.cluster_count, + "completed_at": _canonical_timestamp(self.completed_at, "completed_at"), + "contains_raw_person_level_values": self.contains_raw_person_level_values, + "convergence_status": self.convergence_status, + "design_code": self.design_code, + "distance_rmse": float(self.distance_rmse), + "evidence_reference": self.evidence_reference, + "execution_state": self.execution_state, + "fast_mlsirm_revision": self.fast_mlsirm_revision, + "gamma_abs_error": float(self.gamma_abs_error), + "human_review_required": self.human_review_required, + "item_count": self.item_count, + "iterations": self.iterations, + "latent_rmse": float(self.latent_rmse), + "model_code": self.model_code, + "objective_value": float(self.objective_value), + "parameter_rmse_mean": float(self.parameter_rmse_mean), + "request_digest": self.request_digest, + "result_authority": self.result_authority, + "rust_device": self.rust_device, + "sample_size": self.sample_size, + "seed": self.seed, + } + return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + + def sha256_digest(self) -> str: + """Return SHA-256 over the exact canonical evidence bytes.""" + return sha256(self.canonical_json().encode("utf-8")).hexdigest() + + +def build_rust_recovery_evidence( + request: RustExecutionRequest, + worker_output: Mapping[str, object], + *, + completed_at: datetime, +) -> RustRecoveryEvidence: + """Convert one exact aggregate worker response into governed evidence.""" + request.require_runnable() + if not isinstance(worker_output, Mapping): + raise ValueError("worker_output must be a mapping") + if frozenset(worker_output) != _WORKER_FIELDS: + raise ValueError("worker_output fields do not match the governed schema") + summary = worker_output["recovery_summary"] + if not isinstance(summary, Mapping): + raise ValueError("recovery_summary must be a mapping") + if frozenset(summary) != _RECOVERY_FIELDS: + raise ValueError("recovery_summary fields do not match the governed schema") + if worker_output["model"] != "MLS2PLM": + raise ValueError("worker model must be MLS2PLM") + if worker_output["backend"] != request.backend: + raise ValueError("worker backend does not match the request") + if worker_output["rust_device"] != request.rust_device: + raise ValueError("worker rust_device does not match the request") + if worker_output["n_persons"] != request.sample_size: + raise ValueError("worker n_persons does not match the request") + if worker_output["n_items"] != request.item_count: + raise ValueError("worker n_items does not match the request") + if worker_output["n_clusters"] != request.cluster_count: + raise ValueError("worker n_clusters does not match the request") + evidence_reference = request.execution_reference.replace( + "validity_execution:", "validity_recovery_evidence:", 1 + ) + return RustRecoveryEvidence( + evidence_reference=evidence_reference, + request_digest=request.sha256_digest(), + fast_mlsirm_revision=request.fast_mlsirm_revision, + design_code=request.design_code, + backend=request.backend, + rust_device=request.rust_device, + model_code="mlsirm_recovery", + sample_size=request.sample_size, + item_count=request.item_count, + cluster_count=request.cluster_count, + seed=request.seed, + convergence_status=worker_output["status"], # type: ignore[arg-type] + iterations=worker_output["n_iter"], # type: ignore[arg-type] + objective_value=worker_output["objective"], # type: ignore[arg-type] + parameter_rmse_mean=summary["parameter_rmse_mean"], # type: ignore[arg-type] + latent_rmse=summary["latent_rmse"], # type: ignore[arg-type] + distance_rmse=summary["distance_rmse"], # type: ignore[arg-type] + gamma_abs_error=summary["gamma_abs_error"], # type: ignore[arg-type] + completed_at=completed_at, + ) + + +__all__ = [ + "RustExecutionRequest", + "RustRecoveryEvidence", + "UnsupportedExecutionDesign", + "build_rust_recovery_evidence", +] diff --git a/packages/validity-analysis/src/orgmetra_validity_analysis/recovery_runner.py b/packages/validity-analysis/src/orgmetra_validity_analysis/recovery_runner.py new file mode 100644 index 000000000..cad1d87b7 --- /dev/null +++ b/packages/validity-analysis/src/orgmetra_validity_analysis/recovery_runner.py @@ -0,0 +1,262 @@ +"""Run a bounded exact-revision fast-mlsirm recovery smoke outside Orgmetra.""" + +from __future__ import annotations + +import argparse +from datetime import datetime, timezone +import hashlib +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +from uuid import uuid4 + +from .execution import ( + REVIEWED_FAST_MLSIRM_REVISION, + RustExecutionRequest, + build_rust_recovery_evidence, +) + +_WORKER_CODE = r''' +import json +import os + +import numpy as np +from fast_mlsirm import FitConfig, MLS2PLMConfig, fit, recovery_report, simulate + +design_code = os.environ["ORGMETRA_DESIGN_CODE"] +n_persons = int(os.environ["ORGMETRA_PERSONS"]) +items_per_dim = int(os.environ["ORGMETRA_ITEMS_PER_DIM"]) +cluster_count = int(os.environ["ORGMETRA_CLUSTER_COUNT"]) +seed = int(os.environ["ORGMETRA_SEED"]) +rust_device = os.environ["ORGMETRA_RUST_DEVICE"] +config = MLS2PLMConfig( + n_persons=n_persons, + n_dims=1, + items_per_dim=items_per_dim, + latent_dim=1, + gamma=0.0, + seed=seed, +) +data = simulate(config) +cluster_id = None +if design_code == "nested_multilevel": + cluster_id = np.arange(data.Y.shape[0], dtype=np.int64) % cluster_count +result = fit( + data.Y, + data.factor_id, + config=FitConfig( + model="MLS2PLM", + estimator="mmle", + optimizer="adam", + max_iter=1, + n_restarts=1, + backend="rust", + rust_device=rust_device, + q_theta=7, + q_xi=7, + q_u=7, + ), + cluster_id=cluster_id, +) +report = recovery_report(data.truth, result.params) +print(json.dumps({ + "model": result.model, + "backend": result.backend, + "rust_device": result.rust_device, + "status": result.convergence_status, + "n_iter": result.n_iter, + "objective": result.objective, + "n_persons": int(data.Y.shape[0]), + "n_items": int(data.Y.shape[1]), + "n_clusters": None if cluster_id is None else int(np.unique(cluster_id).size), + "recovery_summary": report.summary, +}, sort_keys=True)) +''' + + +def build_parser() -> argparse.ArgumentParser: + """Build the bounded evidence runner command-line parser.""" + parser = argparse.ArgumentParser( + description="Run exact-revision fast-mlsirm Rust recovery evidence." + ) + parser.add_argument("--fast-mlsirm-path", type=Path, required=True) + parser.add_argument("--handoff-digest", required=True) + parser.add_argument( + "--design-code", + choices=("cross_sectional", "nested_multilevel", "multiple_membership", "longitudinal"), + default="nested_multilevel", + ) + parser.add_argument("--rust-device", choices=("cpu", "gpu"), default="cpu") + parser.add_argument("--persons", type=int, default=48) + parser.add_argument("--items-per-dim", type=int, default=3) + parser.add_argument("--clusters", type=int, default=4) + parser.add_argument("--seed", type=int, default=42) + parser.add_argument("--worker-count", type=int, default=4) + return parser + + +def resolve_revision(repository: Path) -> None: + """Require a clean external checkout at the exact reviewed Git revision.""" + status = subprocess.run( + [ + "git", + "-C", + str(repository), + "status", + "--porcelain=v1", + "--untracked-files=all", + "--ignored", + ], + capture_output=True, + text=True, + check=False, + ) + if status.returncode != 0: + raise RuntimeError("fast-mlsirm path is not a readable Git checkout") + if status.stdout: + raise RuntimeError("fast-mlsirm checkout must be clean, including ignored files") + completed = subprocess.run( + ["git", "-C", str(repository), "rev-parse", "HEAD"], + capture_output=True, + text=True, + check=False, + ) + if completed.returncode != 0: + raise RuntimeError("fast-mlsirm path is not a readable Git checkout") + revision = completed.stdout.strip() + if revision != REVIEWED_FAST_MLSIRM_REVISION: + raise RuntimeError( + "fast-mlsirm checkout must equal reviewed revision " + f"{REVIEWED_FAST_MLSIRM_REVISION}; got {revision or ''}" + ) + + +def dataset_digest( + *, design_code: str, persons: int, items_per_dim: int, clusters: int, seed: int +) -> str: + """Digest only the synthetic run specification, never generated responses.""" + payload = { + "clusters": clusters, + "design_code": design_code, + "items_per_dim": items_per_dim, + "persons": persons, + "seed": seed, + } + return hashlib.sha256( + json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8") + ).hexdigest() + + +def run_worker( + *, + repository: Path, + design_code: str, + rust_device: str, + persons: int, + items_per_dim: int, + clusters: int, + seed: int, + worker_count: int, +) -> dict[str, object]: + """Invoke the external public API in a temporary environment and parse JSON.""" + uv = shutil.which("uv") + if uv is None: + raise RuntimeError("uv is required to run the pinned fast-mlsirm worker") + with tempfile.TemporaryDirectory(prefix="orgmetra-fast-mlsirm-run-") as runtime_root: + runtime_path = Path(runtime_root) + environment = { + "PATH": os.environ.get("PATH", ""), + "TMPDIR": os.environ.get("TMPDIR", ""), + "ORGMETRA_DESIGN_CODE": design_code, + "ORGMETRA_PERSONS": str(persons), + "ORGMETRA_ITEMS_PER_DIM": str(items_per_dim), + "ORGMETRA_CLUSTER_COUNT": str(clusters), + "ORGMETRA_SEED": str(seed), + "ORGMETRA_RUST_DEVICE": rust_device, + "RAYON_NUM_THREADS": str(worker_count), + "PYTHONDONTWRITEBYTECODE": "1", + "UV_PROJECT_ENVIRONMENT": str(runtime_path / "venv"), + "CARGO_TARGET_DIR": str(runtime_path / "cargo-target"), + } + completed = subprocess.run( + [ + uv, + "run", + "--frozen", + "--no-editable", + "--project", + str(repository), + "python", + "-c", + _WORKER_CODE, + ], + cwd=repository, + env=environment, + capture_output=True, + text=True, + check=False, + ) + if completed.returncode != 0: + detail = completed.stderr.strip() or completed.stdout.strip() + raise RuntimeError(f"fast-mlsirm worker failed: {detail}") + try: + output = json.loads(completed.stdout) + except json.JSONDecodeError as exc: + raise RuntimeError("fast-mlsirm worker did not emit one JSON object") from exc + if not isinstance(output, dict): + raise RuntimeError("fast-mlsirm worker JSON root must be an object") + return output + + +def main(argv: list[str] | None = None) -> int: + """Verify the clean pinned checkout, run the worker, and print evidence.""" + args = build_parser().parse_args(argv) + repository = args.fast_mlsirm_path.expanduser().resolve() + resolve_revision(repository) + cluster_count = args.clusters if args.design_code == "nested_multilevel" else None + request = RustExecutionRequest( + execution_reference=f"validity_execution:{uuid4()}", + handoff_digest=args.handoff_digest, + dataset_digest=dataset_digest( + design_code=args.design_code, + persons=args.persons, + items_per_dim=args.items_per_dim, + clusters=args.clusters, + seed=args.seed, + ), + fast_mlsirm_revision=REVIEWED_FAST_MLSIRM_REVISION, + design_code=args.design_code, + sample_size=args.persons, + item_count=args.items_per_dim, + seed=args.seed, + cluster_count=cluster_count, + occasion_count=2 if args.design_code == "longitudinal" else 1, + maximum_memberships=2 if args.design_code == "multiple_membership" else 1, + worker_count=args.worker_count, + rust_device=args.rust_device, + ) + request.require_runnable() + output = run_worker( + repository=repository, + design_code=args.design_code, + rust_device=args.rust_device, + persons=args.persons, + items_per_dim=args.items_per_dim, + clusters=args.clusters, + seed=args.seed, + worker_count=args.worker_count, + ) + resolve_revision(repository) + evidence = build_rust_recovery_evidence( + request, + output, + completed_at=datetime.now(timezone.utc), + ) + print(evidence.canonical_json()) + return 0 + + +__all__ = ["build_parser", "dataset_digest", "main", "resolve_revision", "run_worker"] diff --git a/packages/validity-analysis/tests/test_execution.py b/packages/validity-analysis/tests/test_execution.py new file mode 100644 index 000000000..b030c9bdc --- /dev/null +++ b/packages/validity-analysis/tests/test_execution.py @@ -0,0 +1,328 @@ +"""Test the governed Rust execution request and recovery evidence contracts.""" + +from datetime import datetime, timezone +from dataclasses import replace +import json + +import pytest + +from orgmetra_validity_analysis import ( + REVIEWED_FAST_MLSIRM_REVISION, + RustExecutionRequest, + RustRecoveryEvidence, + UnsupportedExecutionDesign, + build_rust_recovery_evidence, +) + + +EXECUTION = "validity_execution:11111111-1111-4111-8111-111111111111" +HANDOFF_DIGEST = "a" * 64 +DATASET_DIGEST = "b" * 64 +COMPLETED_AT = datetime(2026, 8, 21, 7, 10, 11, 123456, tzinfo=timezone.utc) + + +def request(**overrides: object) -> RustExecutionRequest: + """Build one valid nested multilevel execution request.""" + values: dict[str, object] = { + "execution_reference": EXECUTION, + "handoff_digest": HANDOFF_DIGEST, + "dataset_digest": DATASET_DIGEST, + "fast_mlsirm_revision": REVIEWED_FAST_MLSIRM_REVISION, + "design_code": "nested_multilevel", + "sample_size": 48, + "item_count": 3, + "seed": 42, + "cluster_count": 4, + "occasion_count": 1, + "maximum_memberships": 1, + "worker_count": 4, + } + values.update(overrides) + return RustExecutionRequest(**values) + + +def worker_output(**overrides: object) -> dict[str, object]: + """Build one aggregate response matching the pinned worker smoke run.""" + values: dict[str, object] = { + "model": "MLS2PLM", + "backend": "rust", + "rust_device": "cpu", + "status": "max_iter_reached", + "n_iter": 1, + "objective": 61.86235830761439, + "n_persons": 48, + "n_items": 3, + "n_clusters": 4, + "recovery_summary": { + "parameter_rmse_mean": 0.9038215324094603, + "latent_rmse": 0.7660918765097591, + "distance_rmse": 1.0155729963825437, + "gamma_abs_error": 0.9304347321005841, + }, + } + values.update(overrides) + return values + + +def test_request_and_recovery_are_deterministic_and_redacted() -> None: + """Serialize only aggregate evidence and bind it to the request digest.""" + candidate = request() + evidence = build_rust_recovery_evidence( + candidate, worker_output(), completed_at=COMPLETED_AT + ) + + request_payload = json.loads(candidate.canonical_json()) + evidence_payload = json.loads(evidence.canonical_json()) + assert request_payload["design_code"] == "nested_multilevel" + assert request_payload["runnable"] is True + assert evidence_payload["request_digest"] == candidate.sha256_digest() + assert evidence_payload["result_authority"] == "scientific_evidence_only" + assert evidence_payload["execution_state"] == "completed" + assert "person_record" not in evidence.canonical_json() + assert repr(candidate) == "RustExecutionRequest()" + assert repr(evidence) == "RustRecoveryEvidence()" + assert len(evidence.sha256_digest()) == 64 + assert evidence.canonical_json() == build_rust_recovery_evidence( + candidate, worker_output(), completed_at=COMPLETED_AT + ).canonical_json() + + +def test_cross_sectional_request_is_runnable_without_clusters() -> None: + """Allow the pinned worker's plain cross-sectional design.""" + candidate = request( + design_code="cross_sectional", + cluster_count=None, + ) + assert candidate.runnable is True + candidate.require_runnable() + evidence = build_rust_recovery_evidence( + candidate, + worker_output(n_clusters=None), + completed_at=COMPLETED_AT, + ) + assert evidence.cluster_count is None + + +@pytest.mark.parametrize( + ("design_code", "overrides"), + [ + ("multiple_membership", {"maximum_memberships": 2, "cluster_count": None}), + ("longitudinal", {"occasion_count": 2, "cluster_count": None}), + ], +) +def test_contract_only_designs_fail_closed( + design_code: str, overrides: dict[str, object] +) -> None: + """Keep multiple-membership and longitudinal contracts non-executable.""" + candidate = request(design_code=design_code, **overrides) + assert candidate.runnable is False + with pytest.raises(UnsupportedExecutionDesign, match=design_code): + candidate.require_runnable() + with pytest.raises(UnsupportedExecutionDesign, match=design_code): + build_rust_recovery_evidence(candidate, worker_output(), completed_at=COMPLETED_AT) + + +def test_multiple_membership_rejects_cluster_metadata() -> None: + """Keep multiple-membership structure explicit rather than conflating clusters.""" + with pytest.raises(ValueError, match="cluster_count"): + request( + design_code="multiple_membership", + maximum_memberships=2, + cluster_count=4, + ) + + +def test_gpu_provenance_is_preserved_when_worker_reports_gpu() -> None: + """Preserve actual GPU provenance without asserting parity or availability.""" + candidate = request(rust_device="gpu") + evidence = build_rust_recovery_evidence( + candidate, + worker_output(rust_device="gpu"), + completed_at=COMPLETED_AT, + ) + assert evidence.rust_device == "gpu" + + +@pytest.mark.parametrize( + "field, value", + [ + ("execution_reference", "wrong:11111111-1111-4111-8111-111111111111"), + ("execution_reference", "validity_execution:111111111111111111111111111111111111"), + ("execution_reference", "validity_execution:11111111-1111-1111-8111-111111111111"), + ("fast_mlsirm_revision", "0" * 40), + ("handoff_digest", "bad"), + ("dataset_digest", "bad"), + ("design_code", "unsupported_design"), + ("sample_size", 0), + ("item_count", 0), + ("seed", -1), + ("worker_count", 0), + ("backend", "numpy"), + ("rust_device", "tpu"), + ], +) +def test_request_rejects_malformed_governance_fields(field: str, value: object) -> None: + """Reject malformed execution identity, dimensions, and backend fields.""" + with pytest.raises(ValueError): + request(**{field: value}) + + +@pytest.mark.parametrize( + ("overrides", "message"), + [ + ({"cluster_count": None}, "cluster_count"), + ({"cluster_count": 1}, "cluster_count"), + ({"occasion_count": 2}, "occasion_count"), + ({"maximum_memberships": 2}, "maximum_memberships"), + ], +) +def test_request_rejects_inconsistent_nested_design( + overrides: dict[str, object], message: str +) -> None: + """Reject nested designs whose structural dimensions do not agree.""" + with pytest.raises(ValueError, match=message): + request(**overrides) + + +def test_request_rejects_cross_sectional_extra_structure() -> None: + """Reject cluster, longitudinal, and membership metadata on plain designs.""" + with pytest.raises(ValueError, match="cluster_count"): + request(design_code="cross_sectional", cluster_count=4) + with pytest.raises(ValueError, match="occasion_count"): + request(design_code="cross_sectional", cluster_count=None, occasion_count=2) + with pytest.raises(ValueError, match="maximum_memberships"): + request(design_code="cross_sectional", cluster_count=None, maximum_memberships=2) + + +def test_longitudinal_and_multiple_membership_contracts_validate_their_minimums() -> None: + """Reject contract-only designs that omit the structure they claim.""" + with pytest.raises(ValueError, match="occasion_count"): + request(design_code="longitudinal", cluster_count=None) + with pytest.raises(ValueError, match="maximum_memberships"): + request(design_code="multiple_membership", cluster_count=None) + + +@pytest.mark.parametrize( + "override", + [ + {"model": "other"}, + {"backend": "numpy"}, + {"rust_device": "gpu"}, + {"status": "unknown"}, + {"status": []}, + {"n_iter": 0}, + {"objective": float("nan")}, + {"objective": "not-a-number"}, + {"n_persons": 47}, + {"n_items": 2}, + {"n_clusters": 3}, + { + "recovery_summary": { + "parameter_rmse_mean": 0.1, + "latent_rmse": 0.2, + "distance_rmse": 0.3, + } + }, + { + "recovery_summary": { + "parameter_rmse_mean": -0.1, + "latent_rmse": 0.2, + "distance_rmse": 0.3, + "gamma_abs_error": 0.4, + } + }, + ], +) +def test_builder_rejects_untrusted_worker_output(override: dict[str, object]) -> None: + """Reject worker responses that drift from the reviewed aggregate schema.""" + with pytest.raises((ValueError, KeyError)): + build_rust_recovery_evidence( + request(), worker_output(**override), completed_at=COMPLETED_AT + ) + + +def test_builder_rejects_unknown_and_non_mapping_worker_output() -> None: + """Reject output containers that could hide unreviewed evidence fields.""" + with pytest.raises(ValueError, match="mapping"): + build_rust_recovery_evidence(request(), [], completed_at=COMPLETED_AT) # type: ignore[arg-type] + with pytest.raises(ValueError, match="fields"): + build_rust_recovery_evidence( + request(), {**worker_output(), "person_record": "secret"}, completed_at=COMPLETED_AT + ) + with pytest.raises(ValueError, match="mapping"): + build_rust_recovery_evidence( + request(), worker_output(recovery_summary=[]), completed_at=COMPLETED_AT + ) + + +def test_builder_rejects_wrong_completed_at_and_result_identity() -> None: + """Reject naive completion times and mismatched worker provenance.""" + with pytest.raises(ValueError, match="timezone"): + build_rust_recovery_evidence(request(), worker_output(), completed_at=datetime.now()) + with pytest.raises(ValueError, match="backend"): + build_rust_recovery_evidence( + request(), worker_output(backend="numpy"), completed_at=COMPLETED_AT + ) + with pytest.raises(ValueError, match="rust_device"): + build_rust_recovery_evidence( + request(), worker_output(rust_device="gpu"), completed_at=COMPLETED_AT + ) + + +def test_evidence_constructor_rejects_unlinked_result() -> None: + """Require evidence to carry a real request digest and exact governed fields.""" + with pytest.raises(ValueError, match="request_digest"): + RustRecoveryEvidence( + evidence_reference="validity_recovery_evidence:11111111-1111-4111-8111-111111111111", + request_digest="bad", + fast_mlsirm_revision=REVIEWED_FAST_MLSIRM_REVISION, + design_code="nested_multilevel", + backend="rust", + rust_device="cpu", + model_code="mlsirm_recovery", + sample_size=48, + item_count=3, + cluster_count=4, + seed=42, + convergence_status="max_iter_reached", + iterations=1, + objective_value=1.0, + parameter_rmse_mean=0.1, + latent_rmse=0.1, + distance_rmse=0.1, + gamma_abs_error=0.1, + completed_at=COMPLETED_AT, + ) + + +def test_evidence_rejects_governance_drift_after_worker_build() -> None: + """Keep direct construction as strict as the worker adapter.""" + evidence = build_rust_recovery_evidence( + request(), worker_output(), completed_at=COMPLETED_AT + ) + invalid = [ + ("fast_mlsirm_revision", "0" * 40), + ("design_code", "multiple_membership"), + ("backend", "numpy"), + ("rust_device", "tpu"), + ("model_code", "other_model"), + ("cluster_count", 0), + ("convergence_status", "failed"), + ("result_authority", "employment_decision"), + ("execution_state", "not_executed"), + ("contains_raw_person_level_values", True), + ("human_review_required", False), + ] + for field, value in invalid: + with pytest.raises(ValueError): + replace(evidence, **{field: value}) + + +def test_evidence_accepts_cross_sectional_without_cluster_count() -> None: + """Keep the optional cluster field absent for a plain runnable design.""" + evidence = build_rust_recovery_evidence( + request(design_code="cross_sectional", cluster_count=None), + worker_output(n_clusters=None), + completed_at=COMPLETED_AT, + ) + assert evidence.cluster_count is None diff --git a/packages/validity-analysis/tests/test_execution_script_contract.py b/packages/validity-analysis/tests/test_execution_script_contract.py new file mode 100644 index 000000000..fa70487f9 --- /dev/null +++ b/packages/validity-analysis/tests/test_execution_script_contract.py @@ -0,0 +1,47 @@ +"""Test the safety contract of the pinned external recovery runner.""" + +from pathlib import Path +import subprocess +import sys + + +PACKAGE_ROOT = Path(__file__).resolve().parents[1] +SCRIPT = PACKAGE_ROOT / "scripts" / "run_fast_mlsirm_recovery_evidence.py" +REPOSITORY_ROOT = PACKAGE_ROOT.parents[1] + + +def test_runner_help_exposes_pinned_path_and_design_controls() -> None: + """Keep the evidence command discoverable without starting model execution.""" + completed = subprocess.run( + [sys.executable, str(SCRIPT), "--help"], + capture_output=True, + text=True, + check=False, + ) + assert completed.returncode == 0 + assert "--fast-mlsirm-path" in completed.stdout + assert "--handoff-digest" in completed.stdout + assert "multiple_membership" in completed.stdout + assert "longitudinal" in completed.stdout + + +def test_runner_rejects_an_unpinned_checkout_before_worker_start() -> None: + """Reject the Orgmetra checkout as a foreign worker before any model code runs.""" + completed = subprocess.run( + [ + sys.executable, + str(SCRIPT), + "--fast-mlsirm-path", + str(REPOSITORY_ROOT), + "--handoff-digest", + "a" * 64, + ], + capture_output=True, + text=True, + check=False, + ) + assert completed.returncode != 0 + assert ( + "must be clean" in completed.stderr + or "must equal reviewed revision" in completed.stderr + ) diff --git a/packages/validity-analysis/tests/test_recovery_runner.py b/packages/validity-analysis/tests/test_recovery_runner.py new file mode 100644 index 000000000..2c7919963 --- /dev/null +++ b/packages/validity-analysis/tests/test_recovery_runner.py @@ -0,0 +1,231 @@ +"""Test the pinned fast-mlsirm recovery runner without invoking foreign code.""" + +from datetime import datetime, timezone +import json +from pathlib import Path +from types import SimpleNamespace +from uuid import UUID + +import pytest + +from orgmetra_validity_analysis import REVIEWED_FAST_MLSIRM_REVISION, UnsupportedExecutionDesign +from orgmetra_validity_analysis import recovery_runner as runner + + +REPOSITORY = Path("/private/tmp/fast-mlsirm") + + +def output(**overrides: object) -> dict[str, object]: + """Return one worker-shaped aggregate response for runner tests.""" + values: dict[str, object] = { + "model": "MLS2PLM", + "backend": "rust", + "rust_device": "cpu", + "status": "max_iter_reached", + "n_iter": 1, + "objective": 1.0, + "n_persons": 48, + "n_items": 3, + "n_clusters": 4, + "recovery_summary": { + "parameter_rmse_mean": 0.1, + "latent_rmse": 0.2, + "distance_rmse": 0.3, + "gamma_abs_error": 0.4, + }, + } + values.update(overrides) + return values + + +def request_arguments(*, design_code: str = "nested_multilevel") -> list[str]: + """Return bounded CLI arguments for one runner invocation.""" + return [ + "--fast-mlsirm-path", + str(REPOSITORY), + "--handoff-digest", + "a" * 64, + "--design-code", + design_code, + ] + + +def test_parser_and_dataset_digest_are_stable() -> None: + """Expose the governed design controls and deterministic synthetic input digest.""" + parser = runner.build_parser() + parsed = parser.parse_args(request_arguments()) + assert parsed.design_code == "nested_multilevel" + first = runner.dataset_digest( + design_code="nested_multilevel", persons=48, items_per_dim=3, clusters=4, seed=42 + ) + assert first == runner.dataset_digest( + design_code="nested_multilevel", persons=48, items_per_dim=3, clusters=4, seed=42 + ) + assert len(first) == 64 + + +@pytest.mark.parametrize( + ("responses", "message"), + [ + ([SimpleNamespace(returncode=1, stdout="", stderr="")], "readable"), + ([SimpleNamespace(returncode=0, stdout="dirty\n", stderr="")], "clean"), + ( + [ + SimpleNamespace(returncode=0, stdout="", stderr=""), + SimpleNamespace(returncode=1, stdout="", stderr=""), + ], + "readable", + ), + ( + [ + SimpleNamespace(returncode=0, stdout="", stderr=""), + SimpleNamespace(returncode=0, stdout="deadbeef", stderr=""), + ], + "equal reviewed revision", + ), + ( + [ + SimpleNamespace(returncode=0, stdout="", stderr=""), + SimpleNamespace(returncode=0, stdout="", stderr=""), + ], + "equal reviewed revision", + ), + ], +) +def test_resolve_revision_rejects_dirty_or_unreadable_checkouts( + monkeypatch: pytest.MonkeyPatch, + responses: list[SimpleNamespace], + message: str, +) -> None: + """Reject every checkout state that cannot prove immutable reviewed source.""" + sequence = iter(responses) + monkeypatch.setattr(runner.subprocess, "run", lambda *args, **kwargs: next(sequence)) + with pytest.raises(RuntimeError, match=message): + runner.resolve_revision(REPOSITORY) + + +def test_resolve_revision_accepts_clean_exact_checkout(monkeypatch: pytest.MonkeyPatch) -> None: + """Accept only an empty status and the exact reviewed commit.""" + responses = iter( + [ + SimpleNamespace(returncode=0, stdout="", stderr=""), + SimpleNamespace(returncode=0, stdout=REVIEWED_FAST_MLSIRM_REVISION + "\n", stderr=""), + ] + ) + monkeypatch.setattr(runner.subprocess, "run", lambda *args, **kwargs: next(responses)) + runner.resolve_revision(REPOSITORY) + + +def test_run_worker_uses_external_runtime_and_parses_object( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Keep uv environments and Cargo targets outside the foreign checkout.""" + captured: dict[str, object] = {} + monkeypatch.setattr(runner.shutil, "which", lambda name: "/usr/local/bin/uv") + + def fake_run(*args: object, **kwargs: object) -> SimpleNamespace: + """Capture one subprocess call and return one valid worker object.""" + captured.update(kwargs) + return SimpleNamespace(returncode=0, stdout=json.dumps(output()), stderr="") + + monkeypatch.setattr(runner.subprocess, "run", fake_run) + result = runner.run_worker( + repository=REPOSITORY, + design_code="nested_multilevel", + rust_device="cpu", + persons=48, + items_per_dim=3, + clusters=4, + seed=42, + worker_count=4, + ) + environment = captured["env"] + assert result["model"] == "MLS2PLM" + assert isinstance(environment, dict) + assert str(environment["UV_PROJECT_ENVIRONMENT"]).endswith("/venv") + assert str(environment["CARGO_TARGET_DIR"]).endswith("/cargo-target") + + +def test_run_worker_rejects_missing_uv(monkeypatch: pytest.MonkeyPatch) -> None: + """Fail before subprocess invocation when uv is unavailable.""" + monkeypatch.setattr(runner.shutil, "which", lambda name: None) + with pytest.raises(RuntimeError, match="uv is required"): + runner.run_worker( + repository=REPOSITORY, + design_code="nested_multilevel", + rust_device="cpu", + persons=48, + items_per_dim=3, + clusters=4, + seed=42, + worker_count=4, + ) + + +@pytest.mark.parametrize( + ("completed", "message"), + [ + (SimpleNamespace(returncode=1, stdout="", stderr="worker failed"), "worker failed"), + (SimpleNamespace(returncode=1, stdout="worker output", stderr=""), "worker output"), + (SimpleNamespace(returncode=0, stdout="not json", stderr=""), "one JSON object"), + (SimpleNamespace(returncode=0, stdout="[]", stderr=""), "root must be an object"), + ], +) +def test_run_worker_rejects_process_and_json_failures( + monkeypatch: pytest.MonkeyPatch, + completed: SimpleNamespace, + message: str, +) -> None: + """Convert worker process, JSON, and root-shape failures into safe errors.""" + monkeypatch.setattr(runner.shutil, "which", lambda name: "/usr/local/bin/uv") + monkeypatch.setattr(runner.subprocess, "run", lambda *args, **kwargs: completed) + with pytest.raises(RuntimeError, match=message): + runner.run_worker( + repository=REPOSITORY, + design_code="nested_multilevel", + rust_device="cpu", + persons=48, + items_per_dim=3, + clusters=4, + seed=42, + worker_count=4, + ) + + +def test_main_emits_nested_evidence(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + """Build and print one nested multilevel receipt after the worker succeeds.""" + monkeypatch.setattr(runner, "resolve_revision", lambda repository: None) + monkeypatch.setattr(runner, "run_worker", lambda **kwargs: output()) + monkeypatch.setattr(runner, "uuid4", lambda: UUID("11111111-1111-4111-8111-111111111111")) + assert runner.main(request_arguments()) == 0 + payload = json.loads(capsys.readouterr().out) + assert payload["design_code"] == "nested_multilevel" + assert payload["cluster_count"] == 4 + + +def test_main_emits_cross_sectional_evidence( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + """Build the plain runnable design without cluster metadata.""" + monkeypatch.setattr(runner, "resolve_revision", lambda repository: None) + monkeypatch.setattr(runner, "run_worker", lambda **kwargs: output(n_clusters=None)) + monkeypatch.setattr(runner, "uuid4", lambda: UUID("22222222-2222-4222-8222-222222222222")) + assert runner.main(request_arguments(design_code="cross_sectional")) == 0 + payload = json.loads(capsys.readouterr().out) + assert payload["design_code"] == "cross_sectional" + assert payload["cluster_count"] is None + + +@pytest.mark.parametrize("design_code", ["multiple_membership", "longitudinal"]) +def test_main_fails_closed_for_contract_only_designs( + monkeypatch: pytest.MonkeyPatch, design_code: str +) -> None: + """Do not invoke a worker for unsupported multiple-membership or time designs.""" + monkeypatch.setattr(runner, "resolve_revision", lambda repository: None) + with pytest.raises(UnsupportedExecutionDesign, match=design_code): + runner.main(request_arguments(design_code=design_code)) + + +def test_runner_test_fixture_has_aware_timestamp() -> None: + """Keep this module's timestamp import contract explicit for future evidence tests.""" + assert datetime.now(timezone.utc).tzinfo is not None From 3feaf271784ca10b263cafad67a1f7bd971f2440 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 13:46:08 +0900 Subject: [PATCH 3/8] fix(validity): bound external recovery worker --- ...ned-selection-validity-analysis-handoff.md | 2 +- .../2026-08-21-validity-rust-execution.md | 4 +- packages/validity-analysis/README.md | 4 +- .../recovery_runner.py | 99 ++++++++++--------- .../tests/test_recovery_runner.py | 44 +++++++++ 5 files changed, 103 insertions(+), 50 deletions(-) diff --git a/docs/adr/0027-governed-selection-validity-analysis-handoff.md b/docs/adr/0027-governed-selection-validity-analysis-handoff.md index b116658f1..9b1245fbd 100644 --- a/docs/adr/0027-governed-selection-validity-analysis-handoff.md +++ b/docs/adr/0027-governed-selection-validity-analysis-handoff.md @@ -28,7 +28,7 @@ Orgmetra adds a leaf `orgmetra_validity_analysis` package whose `ValidationAnaly 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 additionally defines `RustExecutionRequest` and `RustRecoveryEvidence`. The request makes cross-sectional, nested multilevel, multiple-membership, and longitudinal input structure explicit. Only cross-sectional and nested multilevel designs are runnable in this slice; multiple-membership and longitudinal requests fail closed until a reviewed estimator exists. The optional read-only `run_fast_mlsirm_recovery_evidence.py` runner verifies the exact revision, invokes the foreign package's public Rust API in its own uv environment, and converts only aggregate simulation-recovery output into a receipt. Recovery RMSE is not mapped to `ValidationAnalysisResult.effect_estimate`. Before an approved validity execution, the Orgmetra host must still re-resolve every reference inside the tenant, verify exact study/Job membership and evidence provenance, and prove requester/reviewer identities are distinct authoritative actors. +The package additionally defines `RustExecutionRequest` and `RustRecoveryEvidence`. The request makes cross-sectional, nested multilevel, multiple-membership, and longitudinal input structure explicit. Only cross-sectional and nested multilevel designs are runnable in this slice; multiple-membership and longitudinal requests fail closed until a reviewed estimator exists. The optional read-only `run_fast_mlsirm_recovery_evidence.py` runner verifies a clean exact revision before and after execution, invokes the foreign package's public Rust API in its own uv environment with a bounded 180-second timeout, and converts only aggregate simulation-recovery output into a receipt. Recovery RMSE is not mapped to `ValidationAnalysisResult.effect_estimate`. Before an approved validity execution, the Orgmetra host must still 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 diff --git a/docs/superpowers/plans/2026-08-21-validity-rust-execution.md b/docs/superpowers/plans/2026-08-21-validity-rust-execution.md index 860c665ce..f2769b542 100644 --- a/docs/superpowers/plans/2026-08-21-validity-rust-execution.md +++ b/docs/superpowers/plans/2026-08-21-validity-rust-execution.md @@ -51,13 +51,13 @@ **Interfaces:** - CLI: `uv run --project packages/validity-analysis --extra test python packages/validity-analysis/scripts/run_fast_mlsirm_recovery_evidence.py --fast-mlsirm-path /private/tmp/orgmetra-fast-mlsirm-04d0 --design-code nested_multilevel`. -- The runner verifies a clean `git -C ` checkout and exact `HEAD`, invokes `uv run --frozen --no-editable --project python -c ...` with external uv/Cargo build directories, requests `backend="rust"` and `rust_device="cpu"`, uses a deterministic seed and small bounded sample, and emits only canonical aggregate evidence JSON. +- The runner verifies a clean `git -C ` checkout and exact `HEAD`, invokes `uv run --frozen --no-editable --project python -c ...` with external uv/Cargo build directories and a 180-second timeout, requests `backend="rust"` and `rust_device="cpu"`, uses a deterministic seed and small bounded sample, and emits only canonical aggregate evidence JSON. - The worker uses `cluster_id` for nested multilevel evidence and records `max_iter_reached` explicitly when the bounded smoke run does not converge. - The runner does not invoke unsupported multiple-membership or longitudinal designs and exits with an actionable non-zero error. - [x] **Step 1: Write a contract test that checks the CLI exposes the exact revision/path/design arguments and rejects an unpinned checkout without running model code.** - [x] **Step 2: Run the focused script-contract test with `--no-cov` and confirm the two contract tests pass.** -- [x] **Step 3: Implement the bounded subprocess runner with no shell interpolation of untrusted path data, exact clean-revision verification before and after execution, minimal environment forwarding, external uv/Cargo build directories, and JSON-only stdout.** +- [x] **Step 3: Implement the bounded subprocess runner with no shell interpolation of untrusted path data, exact clean-revision verification before and after execution, minimal environment forwarding, external uv/Cargo build directories, an explicit timeout, and JSON-only stdout.** - [x] **Step 4: Run the real runner against a clean sparse worktree of revision `04d0bc21a2a20693bcf16108cd76d394fe844d23` and preserve the observed output as a local verification artifact, distinguishing bounded smoke evidence from estimator acceptance.** - [x] **Step 5: Document the command, observed Rust CPU evidence, unsupported design boundaries, and the unrun GPU parity requirement.** diff --git a/packages/validity-analysis/README.md b/packages/validity-analysis/README.md index 032f577c4..ec9df7d92 100644 --- a/packages/validity-analysis/README.md +++ b/packages/validity-analysis/README.md @@ -19,10 +19,10 @@ uv run --project packages/validity-analysis --extra test \ python packages/validity-analysis/scripts/run_fast_mlsirm_recovery_evidence.py \ --fast-mlsirm-path /private/tmp/orgmetra-fast-mlsirm-04d0 \ --handoff-digest --design-code nested_multilevel \ - --rust-device cpu --worker-count 4 + --rust-device cpu --worker-count 4 --timeout-seconds 180 ``` -The bounded run uses `backend="rust"`, a deterministic seed, a small synthetic matrix, and `RAYON_NUM_THREADS=4`. It emits only canonical aggregate JSON. A `max_iter_reached` result remains explicit recovery evidence and is not estimator acceptance. +The bounded run uses `backend="rust"`, a deterministic seed, a small synthetic matrix, `RAYON_NUM_THREADS=4`, and a 180-second subprocess timeout. It emits only canonical aggregate JSON. A `max_iter_reached` result remains explicit recovery evidence and is not estimator acceptance. ## What it does not do diff --git a/packages/validity-analysis/src/orgmetra_validity_analysis/recovery_runner.py b/packages/validity-analysis/src/orgmetra_validity_analysis/recovery_runner.py index cad1d87b7..750f1116e 100644 --- a/packages/validity-analysis/src/orgmetra_validity_analysis/recovery_runner.py +++ b/packages/validity-analysis/src/orgmetra_validity_analysis/recovery_runner.py @@ -95,6 +95,7 @@ def build_parser() -> argparse.ArgumentParser: parser.add_argument("--clusters", type=int, default=4) parser.add_argument("--seed", type=int, default=42) parser.add_argument("--worker-count", type=int, default=4) + parser.add_argument("--timeout-seconds", type=int, default=180) return parser @@ -160,45 +161,50 @@ def run_worker( clusters: int, seed: int, worker_count: int, + timeout_seconds: int = 180, ) -> dict[str, object]: """Invoke the external public API in a temporary environment and parse JSON.""" uv = shutil.which("uv") if uv is None: raise RuntimeError("uv is required to run the pinned fast-mlsirm worker") - with tempfile.TemporaryDirectory(prefix="orgmetra-fast-mlsirm-run-") as runtime_root: - runtime_path = Path(runtime_root) - environment = { - "PATH": os.environ.get("PATH", ""), - "TMPDIR": os.environ.get("TMPDIR", ""), - "ORGMETRA_DESIGN_CODE": design_code, - "ORGMETRA_PERSONS": str(persons), - "ORGMETRA_ITEMS_PER_DIM": str(items_per_dim), - "ORGMETRA_CLUSTER_COUNT": str(clusters), - "ORGMETRA_SEED": str(seed), - "ORGMETRA_RUST_DEVICE": rust_device, - "RAYON_NUM_THREADS": str(worker_count), - "PYTHONDONTWRITEBYTECODE": "1", - "UV_PROJECT_ENVIRONMENT": str(runtime_path / "venv"), - "CARGO_TARGET_DIR": str(runtime_path / "cargo-target"), - } - completed = subprocess.run( - [ - uv, - "run", - "--frozen", - "--no-editable", - "--project", - str(repository), - "python", - "-c", - _WORKER_CODE, - ], - cwd=repository, - env=environment, - capture_output=True, - text=True, - check=False, - ) + try: + with tempfile.TemporaryDirectory(prefix="orgmetra-fast-mlsirm-run-") as runtime_root: + runtime_path = Path(runtime_root) + environment = { + "PATH": os.environ.get("PATH", ""), + "TMPDIR": os.environ.get("TMPDIR", ""), + "ORGMETRA_DESIGN_CODE": design_code, + "ORGMETRA_PERSONS": str(persons), + "ORGMETRA_ITEMS_PER_DIM": str(items_per_dim), + "ORGMETRA_CLUSTER_COUNT": str(clusters), + "ORGMETRA_SEED": str(seed), + "ORGMETRA_RUST_DEVICE": rust_device, + "RAYON_NUM_THREADS": str(worker_count), + "PYTHONDONTWRITEBYTECODE": "1", + "UV_PROJECT_ENVIRONMENT": str(runtime_path / "venv"), + "CARGO_TARGET_DIR": str(runtime_path / "cargo-target"), + } + completed = subprocess.run( + [ + uv, + "run", + "--frozen", + "--no-editable", + "--project", + str(repository), + "python", + "-c", + _WORKER_CODE, + ], + cwd=repository, + env=environment, + capture_output=True, + text=True, + check=False, + timeout=timeout_seconds, + ) + except subprocess.TimeoutExpired as exc: + raise RuntimeError("fast-mlsirm worker timed out") from exc if completed.returncode != 0: detail = completed.stderr.strip() or completed.stdout.strip() raise RuntimeError(f"fast-mlsirm worker failed: {detail}") @@ -239,17 +245,20 @@ def main(argv: list[str] | None = None) -> int: rust_device=args.rust_device, ) request.require_runnable() - output = run_worker( - repository=repository, - design_code=args.design_code, - rust_device=args.rust_device, - persons=args.persons, - items_per_dim=args.items_per_dim, - clusters=args.clusters, - seed=args.seed, - worker_count=args.worker_count, - ) - resolve_revision(repository) + try: + output = run_worker( + repository=repository, + design_code=args.design_code, + rust_device=args.rust_device, + persons=args.persons, + items_per_dim=args.items_per_dim, + clusters=args.clusters, + seed=args.seed, + worker_count=args.worker_count, + timeout_seconds=args.timeout_seconds, + ) + finally: + resolve_revision(repository) evidence = build_rust_recovery_evidence( request, output, diff --git a/packages/validity-analysis/tests/test_recovery_runner.py b/packages/validity-analysis/tests/test_recovery_runner.py index 2c7919963..895f1beed 100644 --- a/packages/validity-analysis/tests/test_recovery_runner.py +++ b/packages/validity-analysis/tests/test_recovery_runner.py @@ -3,6 +3,7 @@ from datetime import datetime, timezone import json from pathlib import Path +import subprocess from types import SimpleNamespace from uuid import UUID @@ -55,6 +56,7 @@ def test_parser_and_dataset_digest_are_stable() -> None: parser = runner.build_parser() parsed = parser.parse_args(request_arguments()) assert parsed.design_code == "nested_multilevel" + assert parsed.timeout_seconds == 180 first = runner.dataset_digest( design_code="nested_multilevel", persons=48, items_per_dim=3, clusters=4, seed=42 ) @@ -144,6 +146,7 @@ def fake_run(*args: object, **kwargs: object) -> SimpleNamespace: assert isinstance(environment, dict) assert str(environment["UV_PROJECT_ENVIRONMENT"]).endswith("/venv") assert str(environment["CARGO_TARGET_DIR"]).endswith("/cargo-target") + assert captured["timeout"] == 180 def test_run_worker_rejects_missing_uv(monkeypatch: pytest.MonkeyPatch) -> None: @@ -162,6 +165,28 @@ def test_run_worker_rejects_missing_uv(monkeypatch: pytest.MonkeyPatch) -> None: ) +def test_run_worker_rejects_timeout(monkeypatch: pytest.MonkeyPatch) -> None: + """Bound a hung external worker instead of waiting indefinitely.""" + monkeypatch.setattr(runner.shutil, "which", lambda name: "/usr/local/bin/uv") + + def timeout(*args: object, **kwargs: object) -> None: + """Raise the subprocess timeout sentinel for the runner boundary.""" + raise subprocess.TimeoutExpired(cmd="uv", timeout=180) + + monkeypatch.setattr(runner.subprocess, "run", timeout) + with pytest.raises(RuntimeError, match="timed out"): + runner.run_worker( + repository=REPOSITORY, + design_code="nested_multilevel", + rust_device="cpu", + persons=48, + items_per_dim=3, + clusters=4, + seed=42, + worker_count=4, + ) + + @pytest.mark.parametrize( ("completed", "message"), [ @@ -216,6 +241,25 @@ def test_main_emits_cross_sectional_evidence( assert payload["cluster_count"] is None +def test_main_rechecks_checkout_when_worker_fails(monkeypatch: pytest.MonkeyPatch) -> None: + """Always perform the post-worker integrity check, including failure paths.""" + calls: list[Path] = [] + + def resolve(repository: Path) -> None: + """Record each integrity check without touching a foreign checkout.""" + calls.append(repository) + + def fail_worker(**kwargs: object) -> dict[str, object]: + """Simulate a worker failure after the initial integrity check.""" + raise RuntimeError("worker failed") + + monkeypatch.setattr(runner, "resolve_revision", resolve) + monkeypatch.setattr(runner, "run_worker", fail_worker) + with pytest.raises(RuntimeError, match="worker failed"): + runner.main(request_arguments()) + assert calls == [REPOSITORY.resolve(), REPOSITORY.resolve()] + + @pytest.mark.parametrize("design_code", ["multiple_membership", "longitudinal"]) def test_main_fails_closed_for_contract_only_designs( monkeypatch: pytest.MonkeyPatch, design_code: str From c79a6ed49627e6a47947f171aeeed2bf02a8c152 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 13:47:28 +0900 Subject: [PATCH 4/8] docs(validity): record protected handoff evidence --- docs/superpowers/plans/2026-08-21-validity-rust-execution.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/plans/2026-08-21-validity-rust-execution.md b/docs/superpowers/plans/2026-08-21-validity-rust-execution.md index f2769b542..30c393615 100644 --- a/docs/superpowers/plans/2026-08-21-validity-rust-execution.md +++ b/docs/superpowers/plans/2026-08-21-validity-rust-execution.md @@ -69,8 +69,8 @@ - Modify: `manifest.json` - [x] **Step 1: Run the package gate, repository validator, root npm validation, `git diff --check`, and CodeGraph status from the isolated worktree.** -- [ ] **Step 2: Re-check the exact branch head and PR #58 state without bypassing protection or manufacturing approval.** -- [ ] **Step 3: Request independent review evidence when the hosted review path is available; keep the PR unmerged while checks/review are queued or missing.** +- [x] **Step 2: Re-check the exact branch head and PR #58 state without bypassing protection or manufacturing approval.** +- [x] **Step 3: Request independent review evidence; the local independent review completed, while hosted review/checks remain queued, so the PR stays unmerged.** - [x] **Step 4: Update the active plan and handoff notes with observed, inferred, and still-open evidence separately.** ## Self-review checklist From d3af656c041d113b3d9f007707bac1b8b8effe7e Mon Sep 17 00:00:00 2001 From: seonghobae Date: Wed, 26 Aug 2026 09:00:35 +0900 Subject: [PATCH 5/8] fix(validity): forward home and tool cache roots to worker env --- .../recovery_runner.py | 3 ++ .../tests/test_recovery_runner.py | 34 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/packages/validity-analysis/src/orgmetra_validity_analysis/recovery_runner.py b/packages/validity-analysis/src/orgmetra_validity_analysis/recovery_runner.py index 750f1116e..7a8662ff1 100644 --- a/packages/validity-analysis/src/orgmetra_validity_analysis/recovery_runner.py +++ b/packages/validity-analysis/src/orgmetra_validity_analysis/recovery_runner.py @@ -173,6 +173,9 @@ def run_worker( environment = { "PATH": os.environ.get("PATH", ""), "TMPDIR": os.environ.get("TMPDIR", ""), + "HOME": os.environ.get("HOME", ""), + "UV_CACHE_DIR": os.environ.get("UV_CACHE_DIR", ""), + "CARGO_HOME": os.environ.get("CARGO_HOME", ""), "ORGMETRA_DESIGN_CODE": design_code, "ORGMETRA_PERSONS": str(persons), "ORGMETRA_ITEMS_PER_DIM": str(items_per_dim), diff --git a/packages/validity-analysis/tests/test_recovery_runner.py b/packages/validity-analysis/tests/test_recovery_runner.py index 895f1beed..40cfc9f30 100644 --- a/packages/validity-analysis/tests/test_recovery_runner.py +++ b/packages/validity-analysis/tests/test_recovery_runner.py @@ -273,3 +273,37 @@ def test_main_fails_closed_for_contract_only_designs( def test_runner_test_fixture_has_aware_timestamp() -> None: """Keep this module's timestamp import contract explicit for future evidence tests.""" assert datetime.now(timezone.utc).tzinfo is not None + + +def test_run_worker_forwards_home_and_tool_cache_roots( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """uv and cargo keep working when the parent shell provides their cache roots.""" + captured: dict[str, object] = {} + monkeypatch.setenv("HOME", "/home/operator") + monkeypatch.setenv("UV_CACHE_DIR", "/cache/uv") + monkeypatch.setenv("CARGO_HOME", "/cache/cargo") + monkeypatch.setattr(runner.shutil, "which", lambda name: "/usr/local/bin/uv") + + def fake_run(*args: object, **kwargs: object) -> SimpleNamespace: + """Capture one subprocess call and return one valid worker object.""" + captured.update(kwargs) + return SimpleNamespace(returncode=0, stdout=json.dumps(output()), stderr="") + + monkeypatch.setattr(runner.subprocess, "run", fake_run) + runner.run_worker( + repository=REPOSITORY, + design_code="nested_multilevel", + rust_device="cpu", + persons=48, + items_per_dim=3, + clusters=4, + seed=42, + worker_count=4, + ) + + environment = captured["env"] + assert isinstance(environment, dict) + assert environment["HOME"] == "/home/operator" + assert environment["UV_CACHE_DIR"] == "/cache/uv" + assert environment["CARGO_HOME"] == "/cache/cargo" From b5b477545231b397e26d907b9402d78aa99649e8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 21:51:55 +0900 Subject: [PATCH 6/8] fix(validity): harden recovery evidence boundaries --- CHANGELOG.md | 1 + docs/TRACEABILITY.md | 1 + ...ned-selection-validity-analysis-handoff.md | 2 +- manifest.json | 2 +- packages/validity-analysis/CHANGELOG.md | 1 + packages/validity-analysis/README.md | 2 +- .../orgmetra_validity_analysis/execution.py | 113 ++++++---- .../src/orgmetra_validity_analysis/result.py | 1 - .../validity-analysis/tests/test_execution.py | 209 +++++++++++++++++- 9 files changed, 289 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99f4752d7..07be6ad89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,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 validity-analysis execution boundary: exact-revision read-only Rust CPU recovery smoke contracts preserve aggregate scientific evidence separately from criterion-related validity estimates, freeze completion timestamps and numeric values before canonicalization, and keep unsupported designs and high-impact employment decisions fail-closed. - 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 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. diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index ca7ba22f2..e75c90af1 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -34,6 +34,7 @@ | naruon communication and calendar | Integration Hub | Published API/event adapter | idempotency, delivery audit, no direct table access | ADR-0002 | planned | | Psychometrics Commons @ `cc5850a0d1eacbbf16d03075534fce460a8286e6` | Workforce Validation | Immutable response/result snapshot contract | pinned revision, model/version/provenance snapshot, immutable result linkage, no direct application-table access | ADR-0002 | accepted_architecture | | fast-mlsirm @ `04d0bc21a2a20693bcf16108cd76d394fe844d23` | Workforce Validation | Published `orgmetra.fast_mlsirm.v1` result contract; direct calls only from approved offline validation worker | pinned revision, contract identifier, backend/result provenance, CPU/GPU parity evidence where material, no duplicated kernel | ADR-0002 | accepted_architecture | +| Rust validity execution and recovery evidence | Workforce Validation | `RustExecutionRequest`, `RustRecoveryEvidence`, and exact-revision read-only runner | exact built-in governance text, pinned revision, detached UTC completion time, numeric snapshots, aggregate-only recovery receipt, explicit nonconvergence, and no mapping to criterion-related validity or employment decisions | ADR-0027 | implemented_on_active_pr | | TEPP temporal evidence | Workforce Validation | Published package/API contract | temporal provenance and version binding | ADR-0002 | planned | | MHTML ETL Gateway / mightyETL | Governed Migration | Published ETL contract | lineage, idempotency, reconciliation, rollback | ADR-0002 | planned | | Semantic Data Portal / OriginWeave / LineageWeave | Evidence and lineage adapters | Published API/event contracts | provenance, tenant ACL, retention, export controls | ADR-0002 | planned | diff --git a/docs/adr/0027-governed-selection-validity-analysis-handoff.md b/docs/adr/0027-governed-selection-validity-analysis-handoff.md index 9b1245fbd..c47e17c1f 100644 --- a/docs/adr/0027-governed-selection-validity-analysis-handoff.md +++ b/docs/adr/0027-governed-selection-validity-analysis-handoff.md @@ -28,7 +28,7 @@ Orgmetra adds a leaf `orgmetra_validity_analysis` package whose `ValidationAnaly 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 additionally defines `RustExecutionRequest` and `RustRecoveryEvidence`. The request makes cross-sectional, nested multilevel, multiple-membership, and longitudinal input structure explicit. Only cross-sectional and nested multilevel designs are runnable in this slice; multiple-membership and longitudinal requests fail closed until a reviewed estimator exists. The optional read-only `run_fast_mlsirm_recovery_evidence.py` runner verifies a clean exact revision before and after execution, invokes the foreign package's public Rust API in its own uv environment with a bounded 180-second timeout, and converts only aggregate simulation-recovery output into a receipt. Recovery RMSE is not mapped to `ValidationAnalysisResult.effect_estimate`. Before an approved validity execution, the Orgmetra host must still re-resolve every reference inside the tenant, verify exact study/Job membership and evidence provenance, and prove requester/reviewer identities are distinct authoritative actors. +The package additionally defines `RustExecutionRequest` and `RustRecoveryEvidence`. The request makes cross-sectional, nested multilevel, multiple-membership, and longitudinal input structure explicit, and its exact built-in governance text is validated before comparisons or canonical serialization. Only cross-sectional and nested multilevel designs are runnable in this slice; multiple-membership and longitudinal requests fail closed until a reviewed estimator exists. The optional read-only `run_fast_mlsirm_recovery_evidence.py` runner verifies a clean exact revision before and after execution, invokes the foreign package's public Rust API in its own uv environment with a bounded 180-second timeout, and converts only aggregate simulation-recovery output into a receipt. Recovery evidence freezes completion time as a UTC instant and snapshots numeric values before canonicalization; recovery RMSE is not mapped to `ValidationAnalysisResult.effect_estimate`. Before an approved validity execution, the Orgmetra host must still 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 diff --git a/manifest.json b/manifest.json index 2d935867b..2598d4380 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":"f4963e79205b896f527dab402a0b6ef455bd44ed5f948d9d6126a6bb20c8b5a3","bytes":388,"lines":38},{"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":"647f8edfb2f38f9ee6541b8c881683d09c64291fae7c04b8c78ef3d2afdfb0b8","bytes":16534,"lines":135},{"path":"docs/THREAT_MODEL.md","sha256":"f314f375c2e41252536de224c7bc7e4a10ab8f340cb86642724e7399e32f4252","bytes":6736,"lines":23},{"path":"docs/TRACEABILITY.md","sha256":"4a57e25f61baea95cd73442151515b031efe42762b9b970de884c24fecf88d0a","bytes":11462,"lines":40},{"path":"docs/TRD.md","sha256":"466a93d7b35c655fb91cfd85a7f76cf5f82637d534bf77c5cf8661438cc2a95a","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}]} \ No newline at end of file +{"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":"f4963e79205b896f527dab402a0b6ef455bd44ed5f948d9d6126a6bb20c8b5a3","bytes":388,"lines":38},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"8d41d78c7025dc0d8c5dfa38b0b636a3353418ab64b584bbf3ef951dbdcbdc9c","bytes":17649,"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":"647f8edfb2f38f9ee6541b8c881683d09c64291fae7c04b8c78ef3d2afdfb0b8","bytes":16534,"lines":135},{"path":"docs/THREAT_MODEL.md","sha256":"f314f375c2e41252536de224c7bc7e4a10ab8f340cb86642724e7399e32f4252","bytes":6736,"lines":23},{"path":"docs/TRACEABILITY.md","sha256":"e2e6f5f8623f25db06c699efecea68cdc8f2bc67a51054272ea312a3b2ab2d2d","bytes":11886,"lines":41},{"path":"docs/TRD.md","sha256":"466a93d7b35c655fb91cfd85a7f76cf5f82637d534bf77c5cf8661438cc2a95a","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 index d63e52c0e..ff6a74a9d 100644 --- a/packages/validity-analysis/CHANGELOG.md +++ b/packages/validity-analysis/CHANGELOG.md @@ -12,3 +12,4 @@ - Make the package-local `uv` and pytest source configuration canonical so the quality workflow does not depend on a manually supplied `PYTHONPATH`. - Add fail-closed cross-sectional, nested multilevel, multiple-membership, and longitudinal execution contracts plus aggregate Rust recovery evidence. - Add an exact-revision read-only runner for bounded fast-mlsirm Rust CPU recovery smoke evidence; recovery metrics remain separate from criterion-related validity results. +- Harden execution and recovery-evidence canonicalization against string/design forgery, mutable timezone state, UTC conversion overflow, and numeric runtime subclasses. diff --git a/packages/validity-analysis/README.md b/packages/validity-analysis/README.md index ec9df7d92..815022ff8 100644 --- a/packages/validity-analysis/README.md +++ b/packages/validity-analysis/README.md @@ -10,7 +10,7 @@ The resulting canonical JSON is digest-addressable, contains no raw person-level `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. Missingness counts must be internally possible: complete observations cannot overlap either predictor-missing or criterion-missing observations beyond the declared sample total. The result envelope accepts only the exact governed `MissingnessSummary` and `ConvergenceDiagnostics` runtime types, preventing subclass method overrides from adding unreviewed or person-level fields to canonical audit evidence. It never promotes a result to an employment decision; human review remains mandatory. -`RustExecutionRequest` makes cross-sectional, nested multilevel, multiple-membership, and longitudinal design metadata explicit. Only cross-sectional and nested multilevel requests are currently runnable. `RustRecoveryEvidence` records aggregate simulation-recovery metrics from a bounded worker run; it is not a criterion-related validity estimate and cannot be converted into `ValidationAnalysisResult.effect_estimate`. +`RustExecutionRequest` makes cross-sectional, nested multilevel, multiple-membership, and longitudinal design metadata explicit. Only cross-sectional and nested multilevel requests are currently runnable. `RustRecoveryEvidence` records aggregate simulation-recovery metrics from a bounded worker run, freezes completion time as an exact UTC instant, and normalizes numeric runtime values before canonicalization; it is not a criterion-related validity estimate and cannot be converted into `ValidationAnalysisResult.effect_estimate`. The read-only evidence runner verifies the exact pinned checkout before starting the foreign worker: diff --git a/packages/validity-analysis/src/orgmetra_validity_analysis/execution.py b/packages/validity-analysis/src/orgmetra_validity_analysis/execution.py index fc45f265f..eaac45b9b 100644 --- a/packages/validity-analysis/src/orgmetra_validity_analysis/execution.py +++ b/packages/validity-analysis/src/orgmetra_validity_analysis/execution.py @@ -4,7 +4,7 @@ from collections.abc import Mapping from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from hashlib import sha256 from math import isfinite from numbers import Real @@ -43,10 +43,16 @@ class UnsupportedExecutionDesign(ValueError): """Indicate that a valid design contract has no reviewed estimator yet.""" +def _validate_fixed_text(value: object, expected: str, field_name: str) -> None: + """Require exact built-in text before fixed-value comparisons or serialization.""" + if type(value) is not str or value != expected: + raise ValueError(f"{field_name} must remain {expected}") + + def _validate_reference(value: object, prefix: str, field_name: str) -> None: """Require an opaque UUID-shaped reference in the expected namespace.""" if ( - not isinstance(value, str) + type(value) is not str or not value.startswith(f"{prefix}:") or _REFERENCE_PATTERN.fullmatch(value) is None ): @@ -61,7 +67,7 @@ def _validate_reference(value: object, prefix: str, field_name: str) -> None: def _validate_digest(value: object, field_name: str) -> None: """Require a lowercase SHA-256 digest without carrying the source values.""" - if not isinstance(value, str) or _DIGEST_PATTERN.fullmatch(value) is None: + if type(value) is not str or _DIGEST_PATTERN.fullmatch(value) is None: raise ValueError(f"{field_name} must be lowercase SHA-256 hex") @@ -95,11 +101,27 @@ def _finite_nonnegative_number(value: object, field_name: str) -> float: return number +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 one timezone-aware instant as precision-preserving UTC text.""" - if not isinstance(value, datetime) or value.tzinfo is None or value.utcoffset() is None: + """Render only 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.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + return value.isoformat().replace("+00:00", "Z") @dataclass(frozen=True, slots=True, repr=False) @@ -128,13 +150,17 @@ def __post_init__(self) -> None: ) _validate_digest(self.handoff_digest, "handoff_digest") _validate_digest(self.dataset_digest, "dataset_digest") - if self.fast_mlsirm_revision != REVIEWED_FAST_MLSIRM_REVISION: - raise ValueError("fast_mlsirm_revision must equal the reviewed immutable revision") + _validate_fixed_text( + self.fast_mlsirm_revision, + REVIEWED_FAST_MLSIRM_REVISION, + "fast_mlsirm_revision", + ) + if type(self.design_code) is not str: + raise ValueError("design_code must be exact built-in text") if self.design_code not in _DESIGN_CODES: raise ValueError("design_code is not a governed execution design") - if self.backend != "rust": - raise ValueError("backend must remain rust for this execution lane") - if self.rust_device not in {"cpu", "gpu"}: + _validate_fixed_text(self.backend, "rust", "backend") + if type(self.rust_device) is not str or self.rust_device not in {"cpu", "gpu"}: raise ValueError("rust_device must be cpu or gpu") _validate_positive_integer(self.sample_size, "sample_size") _validate_positive_integer(self.item_count, "item_count") @@ -242,40 +268,53 @@ def __post_init__(self) -> None: "evidence_reference", ) _validate_digest(self.request_digest, "request_digest") - if self.fast_mlsirm_revision != REVIEWED_FAST_MLSIRM_REVISION: - raise ValueError("fast_mlsirm_revision must equal the reviewed immutable revision") + _validate_fixed_text( + self.fast_mlsirm_revision, + REVIEWED_FAST_MLSIRM_REVISION, + "fast_mlsirm_revision", + ) + if type(self.design_code) is not str: + raise ValueError("design_code must be exact built-in text") if self.design_code not in _RUNNABLE_DESIGNS: raise ValueError("recovery evidence requires a runnable design") - if self.backend != "rust": - raise ValueError("backend must remain rust") - if self.rust_device not in {"cpu", "gpu"}: + _validate_fixed_text(self.backend, "rust", "backend") + if type(self.rust_device) is not str or self.rust_device not in {"cpu", "gpu"}: raise ValueError("rust_device must be cpu or gpu") - if self.model_code != "mlsirm_recovery": - raise ValueError("model_code must remain mlsirm_recovery") + _validate_fixed_text(self.model_code, "mlsirm_recovery", "model_code") _validate_positive_integer(self.sample_size, "sample_size") _validate_positive_integer(self.item_count, "item_count") _validate_nonnegative_integer(self.seed, "seed") if self.cluster_count is not None: _validate_positive_integer(self.cluster_count, "cluster_count") - if not isinstance(self.convergence_status, str) or self.convergence_status not in { + if type(self.convergence_status) is not str or self.convergence_status not in { "converged", "max_iter_reached", }: raise ValueError("convergence_status is not a governed worker state") _validate_positive_integer(self.iterations, "iterations") - _finite_number(self.objective_value, "objective_value") + object.__setattr__( + self, + "objective_value", + _finite_number(self.objective_value, "objective_value"), + ) for field_name in ( "parameter_rmse_mean", "latent_rmse", "distance_rmse", "gamma_abs_error", ): - _finite_nonnegative_number(getattr(self, field_name), field_name) - _canonical_timestamp(self.completed_at, "completed_at") - if self.result_authority != "scientific_evidence_only": - raise ValueError("result_authority must remain scientific_evidence_only") - if self.execution_state != "completed": - raise ValueError("execution_state must remain completed") + object.__setattr__( + self, + field_name, + _finite_nonnegative_number(getattr(self, field_name), field_name), + ) + object.__setattr__(self, "completed_at", _freeze_timestamp(self.completed_at, "completed_at")) + _validate_fixed_text( + self.result_authority, + "scientific_evidence_only", + "result_authority", + ) + _validate_fixed_text(self.execution_state, "completed", "execution_state") if self.contains_raw_person_level_values is not False: raise ValueError("recovery evidence must not contain raw person-level values") if self.human_review_required is not True: @@ -336,18 +375,16 @@ def build_rust_recovery_evidence( raise ValueError("recovery_summary must be a mapping") if frozenset(summary) != _RECOVERY_FIELDS: raise ValueError("recovery_summary fields do not match the governed schema") - if worker_output["model"] != "MLS2PLM": - raise ValueError("worker model must be MLS2PLM") - if worker_output["backend"] != request.backend: - raise ValueError("worker backend does not match the request") - if worker_output["rust_device"] != request.rust_device: - raise ValueError("worker rust_device does not match the request") - if worker_output["n_persons"] != request.sample_size: - raise ValueError("worker n_persons does not match the request") - if worker_output["n_items"] != request.item_count: - raise ValueError("worker n_items does not match the request") - if worker_output["n_clusters"] != request.cluster_count: - raise ValueError("worker n_clusters does not match the request") + _validate_fixed_text(worker_output["model"], "MLS2PLM", "worker model") + _validate_fixed_text(worker_output["backend"], request.backend, "worker backend") + _validate_fixed_text(worker_output["rust_device"], request.rust_device, "worker rust_device") + for field_name, expected in ( + ("n_persons", request.sample_size), + ("n_items", request.item_count), + ("n_clusters", request.cluster_count), + ): + if type(worker_output[field_name]) is not type(expected) or worker_output[field_name] != expected: + raise ValueError(f"worker {field_name} does not match the request") evidence_reference = request.execution_reference.replace( "validity_execution:", "validity_recovery_evidence:", 1 ) diff --git a/packages/validity-analysis/src/orgmetra_validity_analysis/result.py b/packages/validity-analysis/src/orgmetra_validity_analysis/result.py index 25ce3410e..cea4c56e3 100644 --- a/packages/validity-analysis/src/orgmetra_validity_analysis/result.py +++ b/packages/validity-analysis/src/orgmetra_validity_analysis/result.py @@ -15,7 +15,6 @@ from numbers import Real from .handoff import ( - REVIEWED_FAST_MLSIRM_REVISION, _canonical_timestamp, _validate_code, _validate_digest, diff --git a/packages/validity-analysis/tests/test_execution.py b/packages/validity-analysis/tests/test_execution.py index b030c9bdc..737848811 100644 --- a/packages/validity-analysis/tests/test_execution.py +++ b/packages/validity-analysis/tests/test_execution.py @@ -1,6 +1,6 @@ """Test the governed Rust execution request and recovery evidence contracts.""" -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone, tzinfo from dataclasses import replace import json @@ -21,6 +21,111 @@ COMPLETED_AT = datetime(2026, 8, 21, 7, 10, 11, 123456, tzinfo=timezone.utc) +class ForgedReference(str): + """Expose one namespace while feeding another UUID to the parser.""" + + def startswith(self, prefix: str, *args: object) -> bool: + """Forge the expected namespace check.""" + del prefix, args + return True + + def split(self, separator: str | None = None, maxsplit: int = -1) -> list[str]: + """Feed a valid UUID suffix while retaining unsafe serialized text.""" + if separator == ":" and maxsplit == 1: + return ["validity_execution", "11111111-1111-4111-8111-111111111111"] + return super().split(separator, maxsplit) + + +class ForgedDesign(str): + """Pretend an unsupported design is the reviewed runnable design.""" + + def __eq__(self, other: object) -> bool: + """Forge membership and branch comparisons.""" + return other == "nested_multilevel" + + def __hash__(self) -> int: + """Use the reviewed design's hash bucket.""" + return hash("nested_multilevel") + + +class ForgedFixedText(str): + """Pretend unsafe worker text equals a reviewed fixed value.""" + + def __new__(cls, raw_value: str, accepted_value: str): + """Store the serialized value and the value forged during comparison.""" + instance = super().__new__(cls, raw_value) + instance.accepted_value = accepted_value + return instance + + def __eq__(self, other: object) -> bool: + """Forge the reviewed fixed-value comparison.""" + return other == self.accepted_value + + def __hash__(self) -> int: + """Use the reviewed value's hash bucket.""" + return hash(self.accepted_value) + + +class MutableReal(float): + """Expose numeric state that can change after evidence construction.""" + + def __new__(cls, value: float): + """Create one mutable numeric test value.""" + instance = super().__new__(cls, value) + instance.current_value = value + return instance + + def __float__(self) -> float: + """Return the current caller-controlled numeric value.""" + return self.current_value + + +class ExplodingOffset(tzinfo): + """Raise provider behavior so completion-time normalization is exercised.""" + + def utcoffset(self, dt: datetime | None) -> timedelta: + """Raise an arbitrary provider failure.""" + del dt + raise RuntimeError("provider details must not escape") + + def dst(self, dt: datetime | None) -> timedelta: + """Return a fixed daylight-saving offset if queried.""" + del dt + return timedelta(0) + + +class UnknownOffset(tzinfo): + """Return no offset so the completion-time boundary rejects it.""" + + def utcoffset(self, dt: datetime | None) -> None: + """Return an unresolved UTC offset.""" + del dt + return None + + def dst(self, dt: datetime | None) -> timedelta: + """Return a fixed daylight-saving offset if queried.""" + del dt + return timedelta(0) + + +class MutableOffset(tzinfo): + """Expose timezone state that can change after evidence construction.""" + + def __init__(self) -> None: + """Start with a UTC-equivalent offset.""" + self.offset = timedelta(0) + + def utcoffset(self, dt: datetime | None) -> timedelta: + """Return the current mutable offset.""" + del dt + return self.offset + + def dst(self, dt: datetime | None) -> timedelta: + """Keep daylight saving fixed.""" + del dt + return timedelta(0) + + def request(**overrides: object) -> RustExecutionRequest: """Build one valid nested multilevel execution request.""" values: dict[str, object] = { @@ -167,6 +272,20 @@ def test_request_rejects_malformed_governance_fields(field: str, value: object) request(**{field: value}) +def test_request_rejects_forged_reference_and_design_text() -> None: + """Do not let subclass methods make canonical request evidence disagree with validation.""" + with pytest.raises(ValueError): + request( + execution_reference=ForgedReference( + "shadow:11111111-1111-4111-8111-111111111111" + ) + ) + with pytest.raises(ValueError): + request(design_code=ForgedDesign("unsupported_design")) + with pytest.raises(ValueError): + request(handoff_digest=ForgedFixedText("b" * 64, "a" * 64)) + + @pytest.mark.parametrize( ("overrides", "message"), [ @@ -303,6 +422,7 @@ def test_evidence_rejects_governance_drift_after_worker_build() -> None: invalid = [ ("fast_mlsirm_revision", "0" * 40), ("design_code", "multiple_membership"), + ("design_code", ForgedDesign("unsupported_design")), ("backend", "numpy"), ("rust_device", "tpu"), ("model_code", "other_model"), @@ -326,3 +446,90 @@ def test_evidence_accepts_cross_sectional_without_cluster_count() -> None: completed_at=COMPLETED_AT, ) assert evidence.cluster_count is None + + +def test_evidence_detaches_mutable_completion_timezone() -> None: + """Keep canonical recovery evidence stable after caller timezone state mutates.""" + zone = MutableOffset() + evidence = build_rust_recovery_evidence( + request(), worker_output(), completed_at=datetime(2026, 8, 21, 7, 10, tzinfo=zone) + ) + first_json = evidence.canonical_json() + first_digest = evidence.sha256_digest() + + zone.offset = timedelta(hours=9) + + assert evidence.completed_at.tzinfo is timezone.utc + assert evidence.canonical_json() == first_json + assert evidence.sha256_digest() == first_digest + + +@pytest.mark.parametrize( + "completed_at", + [ + datetime.min.replace(tzinfo=timezone(timedelta(hours=1))), + datetime.max.replace(tzinfo=timezone(-timedelta(hours=1))), + ], +) +def test_evidence_rejects_unrepresentable_completion_utc(completed_at: datetime) -> None: + """Normalize out-of-range completion UTC conversion into a boundary error.""" + with pytest.raises(ValueError, match="completed_at"): + build_rust_recovery_evidence(request(), worker_output(), completed_at=completed_at) + + +def test_evidence_normalizes_completion_timezone_provider_failure() -> None: + """Do not leak arbitrary timezone-provider exceptions from evidence construction.""" + with pytest.raises(ValueError, match="completed_at"): + build_rust_recovery_evidence( + request(), + worker_output(), + completed_at=datetime(2026, 8, 21, 7, 10, tzinfo=ExplodingOffset()), + ) + + +def test_evidence_rejects_unknown_completion_timezone_offset() -> None: + """Reject completion times whose timezone cannot resolve an absolute instant.""" + with pytest.raises(ValueError, match="completed_at"): + build_rust_recovery_evidence( + request(), + worker_output(), + completed_at=datetime(2026, 8, 21, 7, 10, tzinfo=UnknownOffset()), + ) + + +def test_evidence_rejects_reinjected_non_utc_completion_timezone() -> None: + """Reject low-level reinjection that would escape canonical UTC evidence.""" + evidence = build_rust_recovery_evidence( + request(), worker_output(), completed_at=COMPLETED_AT + ) + object.__setattr__( + evidence, + "completed_at", + datetime(2026, 8, 21, 16, 10, tzinfo=timezone(timedelta(hours=9))), + ) + with pytest.raises(ValueError, match="completed_at"): + evidence.canonical_json() + + +def test_evidence_normalizes_numeric_runtime_values() -> None: + """Detach mutable numeric subclasses before canonical recovery evidence is stored.""" + objective = MutableReal(1.0) + evidence = build_rust_recovery_evidence( + request(), worker_output(objective=objective), completed_at=COMPLETED_AT + ) + first_json = evidence.canonical_json() + objective.current_value = 99.0 + + assert type(evidence.objective_value) is float + assert evidence.objective_value == 1.0 + assert evidence.canonical_json() == first_json + + +def test_builder_rejects_forged_worker_fixed_text() -> None: + """Do not let worker subclasses bypass model identity validation.""" + with pytest.raises(ValueError, match="worker model"): + build_rust_recovery_evidence( + request(), + worker_output(model=ForgedFixedText("shadow_model", "MLS2PLM")), + completed_at=COMPLETED_AT, + ) From 14f0f08109a134518287024d14b49bb611d7e7fd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 20:34:20 +0900 Subject: [PATCH 7/8] test(validity): reject contradictory recovery clusters --- .../tests/test_recovery_evidence_design.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 packages/validity-analysis/tests/test_recovery_evidence_design.py diff --git a/packages/validity-analysis/tests/test_recovery_evidence_design.py b/packages/validity-analysis/tests/test_recovery_evidence_design.py new file mode 100644 index 000000000..87526c0e3 --- /dev/null +++ b/packages/validity-analysis/tests/test_recovery_evidence_design.py @@ -0,0 +1,58 @@ +"""Regress direct-construction structural integrity for Rust recovery evidence.""" + +from datetime import datetime, timezone + +import pytest + +from orgmetra_validity_analysis import REVIEWED_FAST_MLSIRM_REVISION, RustRecoveryEvidence + + +COMPLETED_AT = datetime(2026, 8, 31, 11, 30, tzinfo=timezone.utc) + + +def recovery_evidence(**overrides: object) -> RustRecoveryEvidence: + """Build one valid nested-multilevel aggregate recovery receipt.""" + values: dict[str, object] = { + "evidence_reference": "validity_recovery_evidence:11111111-1111-4111-8111-111111111111", + "request_digest": "a" * 64, + "fast_mlsirm_revision": REVIEWED_FAST_MLSIRM_REVISION, + "design_code": "nested_multilevel", + "backend": "rust", + "rust_device": "cpu", + "model_code": "mlsirm_recovery", + "sample_size": 48, + "item_count": 3, + "cluster_count": 4, + "seed": 42, + "convergence_status": "max_iter_reached", + "iterations": 1, + "objective_value": 1.0, + "parameter_rmse_mean": 0.1, + "latent_rmse": 0.1, + "distance_rmse": 0.1, + "gamma_abs_error": 0.1, + "completed_at": COMPLETED_AT, + } + values.update(overrides) + return RustRecoveryEvidence(**values) # type: ignore[arg-type] + + +def test_cross_sectional_recovery_evidence_rejects_cluster_count() -> None: + """Do not let plain cross-sectional receipts carry contradictory clusters.""" + with pytest.raises(ValueError, match="cluster_count"): + recovery_evidence(design_code="cross_sectional", cluster_count=4) + + +@pytest.mark.parametrize("cluster_count", [None, 1]) +def test_nested_recovery_evidence_requires_two_or_more_clusters( + cluster_count: int | None, +) -> None: + """Require the structural cluster evidence claimed by nested designs.""" + with pytest.raises(ValueError, match="cluster_count"): + recovery_evidence(cluster_count=cluster_count) + + +def test_nested_recovery_evidence_rejects_more_clusters_than_people() -> None: + """Reject scientifically impossible receipts with more clusters than people.""" + with pytest.raises(ValueError, match="cluster_count"): + recovery_evidence(sample_size=2, cluster_count=3) From 852b088ee1fdde511ecbfdda6abcbe0dd0b7802e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 20:35:54 +0900 Subject: [PATCH 8/8] fix(validity): validate recovery cluster structure --- .../src/orgmetra_validity_analysis/execution.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/validity-analysis/src/orgmetra_validity_analysis/execution.py b/packages/validity-analysis/src/orgmetra_validity_analysis/execution.py index eaac45b9b..7dea92e2e 100644 --- a/packages/validity-analysis/src/orgmetra_validity_analysis/execution.py +++ b/packages/validity-analysis/src/orgmetra_validity_analysis/execution.py @@ -284,8 +284,13 @@ def __post_init__(self) -> None: _validate_positive_integer(self.sample_size, "sample_size") _validate_positive_integer(self.item_count, "item_count") _validate_nonnegative_integer(self.seed, "seed") - if self.cluster_count is not None: - _validate_positive_integer(self.cluster_count, "cluster_count") + if self.design_code == "nested_multilevel": + if self.cluster_count is None or type(self.cluster_count) is not int or self.cluster_count < 2: + raise ValueError("nested_multilevel recovery evidence requires cluster_count >= 2") + if self.cluster_count > self.sample_size: + raise ValueError("nested_multilevel recovery evidence cluster_count cannot exceed sample_size") + elif self.cluster_count is not None: + raise ValueError("cross_sectional recovery evidence cannot carry cluster_count") if type(self.convergence_status) is not str or self.convergence_status not in { "converged", "max_iter_reached",