diff --git a/.github/workflows/tepp-adapter-quality.yml b/.github/workflows/tepp-adapter-quality.yml new file mode 100644 index 000000000..e4bad22f0 --- /dev/null +++ b/.github/workflows/tepp-adapter-quality.yml @@ -0,0 +1,67 @@ +name: TEPP Adapter Quality + +on: + pull_request: + branches: + - bootstrap + - develop + - main + paths: + - "packages/tepp-adapter/**" + - "docs/adr/0023-governed-tepp-analysis-request-boundary.md" + - "docs/doctoring/tepp-analysis-adapter-references.md" + - "docs/traceability/tepp-analysis-adapter.md" + - ".github/requirements/foundation-test.txt" + - ".github/workflows/tepp-adapter-quality.yml" + - ".gitignore" + - ".python-version" + - "conftest.py" + - "packages/conftest.py" + - "pyproject.toml" + - "pytest.ini" + - "setup.cfg" + - "tox.ini" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: tepp-adapter-quality-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + unit: + name: TEPP request boundary and 100% coverage + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout exact candidate + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + - name: Prove exact candidate checkout + env: + ORGMETRA_EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: test "$(git rev-parse HEAD)" = "$ORGMETRA_EXPECTED_HEAD_SHA" + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + check-latest: false + - name: Install reviewed test toolchain + run: | + python -m pip install --require-hashes --no-deps --only-binary=:all: -r .github/requirements/foundation-test.txt + python -m pip check + - name: Compile TEPP adapter boundary + run: python -m compileall -q packages/tepp-adapter/src packages/tepp-adapter/tests + - name: Test TEPP request contract with exact statement and branch coverage + env: + PYTHONPATH: packages/tepp-adapter/src + COVERAGE_FILE: /tmp/orgmetra-tepp-adapter.coverage + run: python -m pytest -c packages/tepp-adapter/pyproject.toml packages/tepp-adapter/tests + - name: Require clean checkout + run: | + git diff --exit-code + test -z "$(git status --porcelain)" diff --git a/CHANGELOG.md b/CHANGELOG.md index 99f4752d7..4e70902fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to Orgmetra will be documented in this file. - 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. +- Active-PR TEPP analysis-request evidence now detaches knowledge and generation times to exact UTC instants and normalizes failing or overflowing timezone providers before request or governance digests are emitted. - Stacked governed job-analysis evidence contract via `JobAnalysisSnapshot`, `TaskEvidence`, `KSAORequirement`, `TaskKSAOLink`, `FunctionalJobAnalysisProfile`, and `EvidenceSource`: tenant/Job-scoped observable tasks, explicit Task-to-KSAO linkage, importance/difficulty/proficiency ratings, source/version/retrieval/SHA-256 provenance, deterministic canonical snapshot bytes, current O*NET evidence support, and historical DOT Data/People/Things compatibility. Validated snapshots require accountable human review and complete non-LLM evidence; LLM-origin material remains `analysis_draft`, and the snapshot is evidence input rather than a hiring, promotion, termination, compensation, or other high-impact employment decision. - Stacked governed audit/outbox slice via `AuditOutboxEvent`, `audit_event_record`, `outbox_delivery_record`, and `outbox_delivery_escalation_record`: CloudEvents 1.0-compatible PII-minimized metadata, exact canonical JSON bytes, database-verified SHA-256 digests, mandatory human confirmation for high-impact events, immutable audit evidence, tenant RLS, atomic audit/outbox insertion, guarded pending/leased/delivered/dead-lettered delivery state, tenant-safe `claim_outbox_delivery(...)` with deterministic due-work ordering, `FOR UPDATE ... SKIP LOCKED`, opaque worker identity, bounded future leases, immutable envelope return, and atomic takeover of genuinely expired leases only while retry attempts remain; owner-bound `complete_outbox_delivery(...)` and `retry_outbox_delivery(...)`; database-budget-governed `dead_letter_outbox_delivery(...)`; and a separately privileged `operator_dead_letter_expired_outbox_delivery(...)` recovery path for an exhausted final lease whose recorded worker identity is permanently unavailable. `maximum_attempt_count` is persisted on the delivery row, defaults to 5, is constrained to 1 through 100, and cannot be lowered by a dispatcher during finalization. Migration 0007 prevents retry or expired-lease takeover from creating attempt N+1; migration 0008 adds TRUNCATE guards, trusted function search paths, a concurrently built due-work partial index, session-independent immutable envelope validation, and operator recovery backed by separate NOLOGIN/NOBYPASSRLS owner/capability roles so the externally assignable operator role can invoke recovery without receiving direct transport-table read/write rights. Migration 0008 also rejects pre-existing reserved recovery-role names before project DDL, atomically contains the temporary schema-creation privilege used for function ownership handoff, and forces deferred escalation binding while the narrow SECURITY DEFINER owner is still active. Exponential/backoff policy selection, policy-specific producer configuration, and external delivery receipts remain subsequent work. - `orgmetra_hris_kernel` 0.4.0 with exclusive-versus-concurrent employment, staffable position coverage, exclusive-seat capacity, and `validate_assignment_write` at 100% statement and branch coverage. diff --git a/docs/adr/0023-governed-tepp-analysis-request-boundary.md b/docs/adr/0023-governed-tepp-analysis-request-boundary.md new file mode 100644 index 000000000..687eb911c --- /dev/null +++ b/docs/adr/0023-governed-tepp-analysis-request-boundary.md @@ -0,0 +1,32 @@ +# ADR-0023: Governed TEPP analysis-request boundary + +**Status:** Proposed on active PR +**Decision owner:** Orgmetra +**Foreign owner:** TEPP (read-only dependency) + +## Context + +Orgmetra needs temporal/event analytical evidence for workforce validation without duplicating TEPP kernels or violating dedicated-writer ownership. TEPP protected revision `7c29e7c971d7940e1fb3def1ed3aae2d1bc8ad4a` exposes `AnalysisRunRequest` contract v1 in `tepp_api`, with immutable snapshot identity, knowledge cutoff, model contract, output profile, and idempotency key. The same TEPP revision documents that protected main is not yet a production HTTP service. + +A direct HTTP client in Orgmetra would therefore overstate foreign runtime maturity. Conversely, copying TEPP's analytical implementation would create split scientific authority and an acquisition-grade integration liability. + +## Decision + +Orgmetra owns a small pre-transport adapter that: + +1. emits exactly the seven fields accepted by TEPP `AnalysisRunRequest` v1; +2. pins the reviewed TEPP protected revision and contract version as evidence, not as permission to mutate TEPP; +3. binds the request to an Orgmetra tenant, validation study, accountable actor, immutable snapshot digest, evidence version, and deterministic request digest; +4. detaches the knowledge cutoff and generation instant to exact UTC datetimes, canonicalizes them to RFC 3339 UTC, and rejects naive or unusable instants; +5. distinguishes exact same-key retries from same-key semantic conflicts; +6. treats opaque correlations as linkable personal/governance data while refusing direct identity values, source text, and credentials at this boundary; +7. performs no network transport until the host proves a compatible executable TEPP service contract is published and authorized; and +8. treats returned TEPP/LLM output as untrusted analytical evidence requiring accountable human scientific review before any high-impact employment use. + +Orgmetra does not read TEPP application tables, mutate TEPP source/configuration, or reimplement TEPP's temporal/event/statistical kernels. + +## Consequences + +The integration can be unit-tested and audited now without manufacturing runtime evidence. Future transport can be added behind the same adapter only after TEPP publishes an executable service contract; that future change must add consumer/provider compatibility, timeout/deadline, authentication, retry, observability, privacy, and failure-mode tests against the then-current foreign contract. + +The adapter deliberately requires host re-resolution of tenant/workspace/snapshot/model/output authority. Syntax validation alone never proves tenant membership, artifact existence, policy applicability, or scientific suitability. diff --git a/docs/doctoring/tepp-analysis-adapter-references.md b/docs/doctoring/tepp-analysis-adapter-references.md new file mode 100644 index 000000000..3572df807 --- /dev/null +++ b/docs/doctoring/tepp-analysis-adapter-references.md @@ -0,0 +1,9 @@ +# TEPP analysis adapter references + +Primary technical evidence reviewed for ADR-0023 and the active adapter slice: + +- ContextualWisdomLab. (2026). *TEPP API and modular integration contract* (protected revision `7c29e7c971d7940e1fb3def1ed3aae2d1bc8ad4a`, `docs/API_CONTRACT.md`). GitHub repository source. Reviewed August 20, 2026. +- ContextualWisdomLab. (2026). *TEPP analysis-run request and accepted-run response contracts* (protected revision `7c29e7c971d7940e1fb3def1ed3aae2d1bc8ad4a`, `crates/tepp_api/src/analysis_run.rs`). GitHub repository source. Reviewed August 20, 2026. +- Klyne, G., & Newman, C. (2002). *Date and time on the Internet: Timestamps* (RFC 3339). Internet Engineering Task Force. https://doi.org/10.17487/RFC3339 + +The TEPP revision is evidence of the reviewed foreign contract, not a claim that Orgmetra owns or may modify TEPP. Before executable transport, re-resolve TEPP's then-current protected contract and service maturity rather than treating this snapshot as perpetual compatibility evidence. diff --git a/docs/traceability/tepp-analysis-adapter.md b/docs/traceability/tepp-analysis-adapter.md new file mode 100644 index 000000000..c0e29c3f5 --- /dev/null +++ b/docs/traceability/tepp-analysis-adapter.md @@ -0,0 +1,19 @@ +# TEPP analysis adapter traceability + +| Requirement | Orgmetra evidence | Verification | Maturity | +|---|---|---|---| +| No foreign table access or duplicated TEPP kernels | `orgmetra_tepp_adapter.analysis` contains only request/governance binding logic | package source review; no TEPP runtime dependency | implemented_on_active_pr | +| Exact TEPP analysis-run v1 request shape | `TeppAnalysisRequestPacket.tepp_request()` | exact seven-field regression against reviewed TEPP DTO | implemented_on_active_pr | +| Temporal cutoff provenance | timezone-aware `knowledge_cutoff` and `generated_at` → detached exact UTC instants and canonical RFC 3339 UTC | mutable-timezone stability, provider-failure, malformed-offset, arithmetic-overflow, naive/null-offset, and datetime-subclass rejection tests | implemented_on_active_pr | +| Tenant/study/actor governance | authoritative canonical non-sentinel operational tenant UUID plus namespaced UUIDv4 Orgmetra references | protected-core UUIDv7 interoperability plus nil/max/noncanonical tenant rejection; wrong-namespace/reference-version regressions | implemented_on_active_pr | +| Immutable analytical snapshot evidence | opaque TEPP snapshot ID plus independent SHA-256 `snapshot_digest` | digest format and governance-evidence regressions | implemented_on_active_pr | +| Durable idempotency handoff | `idempotency_key` + deterministic `request_digest()` + `governance_scope_digest()` | exact-retry and same-key request/scope conflict regressions | implemented_on_active_pr | +| Privacy minimization without anonymity claims | `contains_personal_data=true`; direct identity/source text/credentials false; redacted repr | direct-construction/replace/privacy regressions | implemented_on_active_pr | +| High-impact human review | fixed `human_scientific_review_only`, `untrusted_draft_evidence`, governed `next_action` | authority-expansion regressions | implemented_on_active_pr | +| Foreign runtime maturity is not overstated | `transport_state=requires_published_tepp_service_contract` | fixed-state regression and package/ADR documentation | implemented_on_active_pr | +| Exact owned production coverage | package pytest-cov gate | 100% statement + branch coverage | implemented_on_active_pr | +| Keep adapter-quality evidence current when shared repository test/runtime configuration changes | package quality workflow explicitly retriggers on shared Python/test/clean-checkout configuration inputs | `tests/test_quality_workflow_trigger.py`; supplemental to central required workflows | implemented_on_active_pr | + +## Foreign evidence snapshot + +Reviewed TEPP protected `main`: `7c29e7c971d7940e1fb3def1ed3aae2d1bc8ad4a`. The reviewed `crates/tepp_api/src/analysis_run.rs` defines contract version `1` and the seven request fields above; `docs/API_CONTRACT.md` states that protected main exposes library/domain contracts rather than a production HTTP service. Orgmetra must re-resolve this evidence before enabling transport. diff --git a/manifest.json b/manifest.json index 97f2bab14..aba0bdd8c 100644 --- a/manifest.json +++ b/manifest.json @@ -1 +1 @@ -{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"32cc4ef78d1eca557fa01731026840be01211a043eb0ada552e4e6cb9eace353","bytes":17295,"lines":76},{"path":"CLAUDE.md","sha256":"add33884f466d324e20875388d103de41c6e062938a6e98727dc83a87ffe976f","bytes":1229,"lines":20},{"path":"LICENSE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30","bytes":11358,"lines":202},{"path":"NOTICE","sha256":"34b4618e946bdd8d33407d6ac5279f0a0388f5e7c8f79d2e7d8c3c47d0266042","bytes":305,"lines":4},{"path":"README.md","sha256":"1a9fc400d26d8137ae5911488794a6d3fa915957c95f27b36a48cef0fdf823c6","bytes":3785,"lines":81},{"path":"database/migrations/0001_foundation_schema.sql","sha256":"ce2ae52fc66b2f99597ea5285df82c66f90caa46174fef4930d68a8b6177d0dd","bytes":38747,"lines":916},{"path":"database/migrations/0002_sealed_evidence_digest.sql","sha256":"93d659ca8e0e9293a83d5422d043be7b1022c5470a5b22670aa3416fa334a04c","bytes":6649,"lines":202},{"path":"database/migrations/0003_audit_outbox_persistence.sql","sha256":"2aa7bbb8220923ec584537c0cd46f0cba2b692d69d431f097b7df6db75235bfc","bytes":15417,"lines":423},{"path":"database/migrations/0004_outbox_delivery_claim.sql","sha256":"d4504acf7d58528a2a8f4f03d1584b868c8d3ba9046a007b9c2e7cfef993b2ef","bytes":9451,"lines":234},{"path":"database/migrations/0005_outbox_delivery_finalization.sql","sha256":"b7e8790595b288f752d6ef5cc6cbfe4e1b6712248f5b7a3a25fa60016b6a4961","bytes":6125,"lines":170},{"path":"database/migrations/0006_outbox_delivery_dead_letter.sql","sha256":"c1fb91cdf98169fd6684984e86cb0a14fa19c8f1226028d2346a2a069df2b3c7","bytes":24919,"lines":628},{"path":"database/migrations/0007_outbox_retry_exhaustion.sql","sha256":"812f50d70ca5929c7eba964d34a208aedee660d11cc7ffc09d67688c4737e0d5","bytes":19081,"lines":476},{"path":"database/migrations/0008_audit_outbox_review_hardening.sql","sha256":"c3713a12db9d00fdc10005df1f86c07965e9555eefad78ca67e994537a739d9b","bytes":17562,"lines":448},{"path":"database/migrations/0009_candidate_worker_conversion_governance.sql","sha256":"4030666629a6b8deb383b8337ead4f09d6a945969313def2577a38f31f06cda9","bytes":11537,"lines":281},{"path":"database/migrations/0010_validity_study_case_integrity.sql","sha256":"3f594810ac9e1a6747a2bb4838e5ce65b921cb6e3d36fcdc3ff08b4a7579ebd1","bytes":11979,"lines":313},{"path":"database/migrations/0011_criterion_observation_scope.sql","sha256":"f9fe7c35f1ee7b167e1c2ba75a50a84febda9a6ccf8123b4f5726f51968694f9","bytes":7444,"lines":165},{"path":"database/migrations/0012_people_mutation_idempotency.sql","sha256":"52dbbb9ec7f9be5291593ba88f228d7fffd736dcb99547a08c1d6cad076afb69","bytes":3162,"lines":76},{"path":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"path":"docs/API_CONTRACT.md","sha256":"63533dff785da62b89e585d742a158e2aeb05913644f2bf9fb6486f281c2e589","bytes":4555,"lines":76},{"path":"docs/DATA_MODEL.md","sha256":"6ad29731ae7ee7aa5bf3a2d0bfef88894a35a2550edb2be3244d6f143d76444a","bytes":13366,"lines":85},{"path":"docs/ERD.md","sha256":"546001aa85c4fe020e0c39d881dc860daf7f69090596666fdf9092487b0725fe","bytes":6964,"lines":70},{"path":"docs/OPERABILITY.md","sha256":"82b2d3e70cec371ef35e9e0f982ac40fef84351976bc04b863b81d27023d5a62","bytes":11189,"lines":71},{"path":"docs/PRD.md","sha256":"3ad85ae633cce0fc7a93af39b21d7a7c70bb2efa786da6b12f3c5327906e34f1","bytes":5490,"lines":111},{"path":"docs/SECURITY.md","sha256":"01918512d8882060e9cff0c4aa8206e0eccbdfb61cfd7f829331123c7a9fe6ac","bytes":11185,"lines":64},{"path":"docs/STORYBOARD.md","sha256":"6e4ffb0eb03a80343f50d363ffc43b34da9348a44232dd947a9ff416ea92a3d2","bytes":1342,"lines":28},{"path":"docs/STORYBOOK.md","sha256":"82f79029b3c2b7a45393bad5ba8fabe61014d4b6149c7d4e73f70ba447f885e9","bytes":1389,"lines":50},{"path":"docs/TEST_STRATEGY.md","sha256":"d0a0bc3b54ed0fc7973747987f1afb117d6144c390b51ed9370eb571972a33f8","bytes":16534,"lines":135},{"path":"docs/THREAT_MODEL.md","sha256":"f314f375c2e41252536de224c7bc7e4a10ab8f340cb86642724e7399e32f4252","bytes":6736,"lines":23},{"path":"docs/TRACEABILITY.md","sha256":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"lines":40},{"path":"docs/TRD.md","sha256":"23697d88a4882698e1a2782b7da3f2ccd0d3cd2d6d1bffe89b6597dc16851077","bytes":9064,"lines":101},{"path":"docs/UML.md","sha256":"fe67c37aa88e5814ceb2db7e8f7d8d85ca27a994802efbb7c75164b387adf0a9","bytes":5528,"lines":122},{"path":"docs/USER_STORIES.md","sha256":"5535b39d8c71a36c81f78e2d6dbd90a2d32e6541790f0d28f6dd4baf3ea7b45f","bytes":2670,"lines":37},{"path":"docs/WIREFRAMES.md","sha256":"b03aa6419aeaf5d42a5698c4d43a434c1633b7ac6fd0b0bd0cda979077adc56e","bytes":2005,"lines":77},{"path":"docs/adr/0001-orgmetra-authoritative-hris-record.md","sha256":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"path":"docs/adr/0008-purpose-bound-pii-authorization.md","sha256":"c5157d3bc58f3d8d29e03104dd15eb2911cc1bb66e2c92a935b26d7164648dc7","bytes":5988,"lines":55},{"path":"docs/adr/0009-performance-criterion-observation-scope.md","sha256":"1ac10bb2747b0a5b4d62f627825cfd7f978f3fa88d7575bffc23d56371240a64","bytes":7057,"lines":57},{"path":"docs/adr/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"1656ef8b57c836ef7936a8e9cb6a824681eb7563157a1ab0a29deb25849a457b","bytes":5568,"lines":53},{"path":"docs/adr/0012-governed-migration-handoff.md","sha256":"713855d670001d3964ecb36cc653830502fb1d82a58b9e39f564b6992dd2bd80","bytes":5965,"lines":59},{"path":"docs/adr/0013-governed-requisition-review-packet.md","sha256":"70bf2cbdf903a8793d6d8bc116a08331931090118341f42010236e09c6cc1802","bytes":4693,"lines":46},{"path":"docs/adr/0014-job-analysis-snapshot-persistence.md","sha256":"a7ab6fee50aaa63f7f407516a4cb39885faeb0fc6e5035ee8fc352ed73430105","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"lines":69},{"path":"docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md","sha256":"b64f21abb19373e780db8b9e64deb8ba9a6219ccf9625a651f25407b8691fcbd","bytes":8227,"lines":226},{"path":"docs/superpowers/specs/2026-08-15-orgmetra-foundation-design.md","sha256":"4a0e1a7943e40d12bd3082db3757045b4085e5a089fea7bc0d8a1565ffcbcf1d","bytes":6237,"lines":187},{"path":"package.json","sha256":"59ae9e3e67c3fba9320cb18439692395cdfd16ae5c24e3c4cf30d77d63ebabb5","bytes":388,"lines":9},{"path":"packages/hris-kernel/src/orgmetra_hris_kernel/audit.py","sha256":"3e5b7190cf857dc8c1fc7e898cef303060f34aabee6c27a9034d4d9650e33190","bytes":7707,"lines":160},{"path":"packages/hris-kernel/tests/test_audit_outbox.py","sha256":"5928dd7b97fe38d6b7472ce62966437e339058a59c3b301a93a7b5c05432b40c","bytes":7556,"lines":200},{"path":"schemas/openapi.yaml","sha256":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"path":"scripts/foundation-contract.mjs","sha256":"5242dcdbe0935775edf074462c82600e9bc4927d9fdc50c47727af915fd4b23a","bytes":218,"lines":6},{"path":"tests/dispatcher-inventory.test.mjs","sha256":"09f5e64410e6b7a26bf8d6ce61c50b737da2ea85d955f91eba63aa21f1537261","bytes":1597,"lines":34},{"path":"tests/foundation-contract.test.mjs","sha256":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"path":"tests/openapi-contract.test.mjs","sha256":"80c1610ef1c189fa325e55389501e0e51531ddf61ee335bb94d9cb3aa55a9fdc","bytes":6438,"lines":195},{"path":"tests/test_audit_outbox_hardening_postgres.sh","sha256":"518ba2f37ba6292943e5abe22c2599452b2f031a42e453b2493aedf8714421a0","bytes":13396,"lines":333},{"path":"tests/test_audit_outbox_postgres.sh","sha256":"e57a04920a0ba97fa6a06752d15ea150016ab8d44099e998c5c4f4067592b4d2","bytes":13443,"lines":357},{"path":"tests/test_bitemporal_postgres.sh","sha256":"7684b8c2ff52c044c081135515bd5aabbfd00e2daad0d471b0868701af2df6cc","bytes":8209,"lines":230},{"path":"tests/test_candidate_worker_conversion_postgres.sh","sha256":"681cb74d6cfa859ed92c6c2439881ea20c430ef8df94ec662e2807761a377f90","bytes":14673,"lines":344},{"path":"tests/test_criterion_observation_scope_postgres.sh","sha256":"0ee9539ee57f840c27d08009f7868cdc8662669df78a01dbc8be39216b8f1a3d","bytes":17811,"lines":469},{"path":"tests/test_evidence_sealing_postgres.sh","sha256":"57d16b632a0c60ffdcb4842ceb1cfe25d19c54cefeeefb622ff4fa6e83441ad7","bytes":11349,"lines":370},{"path":"tests/test_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"path":"tests/test_operational_uuid_postgres.sh","sha256":"7378f98f0d4b3000e8ea641d8701f1540dbad71410b3637d81d799969e0f6ff7","bytes":3346,"lines":101},{"path":"tests/test_outbox_claim_postgres.sh","sha256":"1027806d436ebfe34e108c25b6a4001f43b9550f1d70057c6c0d7974323b0c9b","bytes":14817,"lines":429},{"path":"tests/test_outbox_dead_letter_postgres.sh","sha256":"0d728d578e64252e6079f2d141ddaa7fa9cfbf9784e625832273596d69a6e13d","bytes":14008,"lines":377},{"path":"tests/test_people_mutation_idempotency_postgres.sh","sha256":"3f57e12f80bd1b034c9aac54b669d8530106e3e26b3795689671fb53807b3cd5","bytes":16191,"lines":381},{"path":"tests/test_tenant_isolation_postgres.sh","sha256":"dd649435ef8ab9e57f0609c101917e36656a6d40d63de9bcdbdac23d764f6c3a","bytes":15134,"lines":388},{"path":"tests/test_validity_study_case_postgres.sh","sha256":"0070ad58300323c7f9900c5645e0df3106b36ccd245ae686e982c2fd6fa4dc02","bytes":14708,"lines":301},{"path":"tests/validate_repository.py","sha256":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} +{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"8f722e70756d20ce251cebd49b4066c1a3a9298d0333986143263dc27d58b35d","bytes":17511,"lines":77},{"path":"CLAUDE.md","sha256":"add33884f466d324e20875388d103de41c6e062938a6e98727dc83a87ffe976f","bytes":1229,"lines":20},{"path":"LICENSE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30","bytes":11358,"lines":202},{"path":"NOTICE","sha256":"34b4618e946bdd8d33407d6ac5279f0a0388f5e7c8f79d2e7d8c3c47d0266042","bytes":305,"lines":4},{"path":"README.md","sha256":"1a9fc400d26d8137ae5911488794a6d3fa915957c95f27b36a48cef0fdf823c6","bytes":3785,"lines":81},{"path":"database/migrations/0001_foundation_schema.sql","sha256":"ce2ae52fc66b2f99597ea5285df82c66f90caa46174fef4930d68a8b6177d0dd","bytes":38747,"lines":916},{"path":"database/migrations/0002_sealed_evidence_digest.sql","sha256":"93d659ca8e0e9293a83d5422d043be7b1022c5470a5b22670aa3416fa334a04c","bytes":6649,"lines":202},{"path":"database/migrations/0003_audit_outbox_persistence.sql","sha256":"2aa7bbb8220923ec584537c0cd46f0cba2b692d69d431f097b7df6db75235bfc","bytes":15417,"lines":423},{"path":"database/migrations/0004_outbox_delivery_claim.sql","sha256":"d4504acf7d58528a2a8f4f03d1584b868c8d3ba9046a007b9c2e7cfef993b2ef","bytes":9451,"lines":234},{"path":"database/migrations/0005_outbox_delivery_finalization.sql","sha256":"b7e8790595b288f752d6ef5cc6cbfe4e1b6712248f5b7a3a25fa60016b6a4961","bytes":6125,"lines":170},{"path":"database/migrations/0006_outbox_delivery_dead_letter.sql","sha256":"c1fb91cdf98169fd6684984e86cb0a14fa19c8f1226028d2346a2a069df2b3c7","bytes":24919,"lines":628},{"path":"database/migrations/0007_outbox_retry_exhaustion.sql","sha256":"812f50d70ca5929c7eba964d34a208aedee660d11cc7ffc09d67688c4737e0d5","bytes":19081,"lines":476},{"path":"database/migrations/0008_audit_outbox_review_hardening.sql","sha256":"c3713a12db9d00fdc10005df1f86c07965e9555eefad78ca67e994537a739d9b","bytes":17562,"lines":448},{"path":"database/migrations/0009_candidate_worker_conversion_governance.sql","sha256":"4030666629a6b8deb383b8337ead4f09d6a945969313def2577a38f31f06cda9","bytes":11537,"lines":281},{"path":"database/migrations/0010_validity_study_case_integrity.sql","sha256":"3f594810ac9e1a6747a2bb4838e5ce65b921cb6e3d36fcdc3ff08b4a7579ebd1","bytes":11979,"lines":313},{"path":"database/migrations/0011_criterion_observation_scope.sql","sha256":"f9fe7c35f1ee7b167e1c2ba75a50a84febda9a6ccf8123b4f5726f51968694f9","bytes":7444,"lines":165},{"path":"database/migrations/0012_people_mutation_idempotency.sql","sha256":"52dbbb9ec7f9be5291593ba88f228d7fffd736dcb99547a08c1d6cad076afb69","bytes":3162,"lines":76},{"path":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"path":"docs/API_CONTRACT.md","sha256":"63533dff785da62b89e585d742a158e2aeb05913644f2bf9fb6486f281c2e589","bytes":4555,"lines":76},{"path":"docs/DATA_MODEL.md","sha256":"6ad29731ae7ee7aa5bf3a2d0bfef88894a35a2550edb2be3244d6f143d76444a","bytes":13366,"lines":85},{"path":"docs/ERD.md","sha256":"546001aa85c4fe020e0c39d881dc860daf7f69090596666fdf9092487b0725fe","bytes":6964,"lines":70},{"path":"docs/OPERABILITY.md","sha256":"82b2d3e70cec371ef35e9e0f982ac40fef84351976bc04b863b81d27023d5a62","bytes":11189,"lines":71},{"path":"docs/PRD.md","sha256":"3ad85ae633cce0fc7a93af39b21d7a7c70bb2efa786da6b12f3c5327906e34f1","bytes":5490,"lines":111},{"path":"docs/SECURITY.md","sha256":"01918512d8882060e9cff0c4aa8206e0eccbdfb61cfd7f829331123c7a9fe6ac","bytes":11185,"lines":64},{"path":"docs/STORYBOARD.md","sha256":"6e4ffb0eb03a80343f50d363ffc43b34da9348a44232dd947a9ff416ea92a3d2","bytes":1342,"lines":28},{"path":"docs/STORYBOOK.md","sha256":"82f79029b3c2b7a45393bad5ba8fabe61014d4b6149c7d4e73f70ba447f885e9","bytes":1389,"lines":50},{"path":"docs/TEST_STRATEGY.md","sha256":"d0a0bc3b54ed0fc7973747987f1afb117d6144c390b51ed9370eb571972a33f8","bytes":16534,"lines":135},{"path":"docs/THREAT_MODEL.md","sha256":"f314f375c2e41252536de224c7bc7e4a10ab8f340cb86642724e7399e32f4252","bytes":6736,"lines":23},{"path":"docs/TRACEABILITY.md","sha256":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"lines":40},{"path":"docs/TRD.md","sha256":"23697d88a4882698e1a2782b7da3f2ccd0d3cd2d6d1bffe89b6597dc16851077","bytes":9064,"lines":101},{"path":"docs/UML.md","sha256":"fe67c37aa88e5814ceb2db7e8f7d8d85ca27a994802efbb7c75164b387adf0a9","bytes":5528,"lines":122},{"path":"docs/USER_STORIES.md","sha256":"5535b39d8c71a36c81f78e2d6dbd90a2d32e6541790f0d28f6dd4baf3ea7b45f","bytes":2670,"lines":37},{"path":"docs/WIREFRAMES.md","sha256":"b03aa6419aeaf5d42a5698c4d43a434c1633b7ac6fd0b0bd0cda979077adc56e","bytes":2005,"lines":77},{"path":"docs/adr/0001-orgmetra-authoritative-hris-record.md","sha256":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"path":"docs/adr/0008-purpose-bound-pii-authorization.md","sha256":"c5157d3bc58f3d8d29e03104dd15eb2911cc1bb66e2c92a935b26d7164648dc7","bytes":5988,"lines":55},{"path":"docs/adr/0009-performance-criterion-observation-scope.md","sha256":"1ac10bb2747b0a5b4d62f627825cfd7f978f3fa88d7575bffc23d56371240a64","bytes":7057,"lines":57},{"path":"docs/adr/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"1656ef8b57c836ef7936a8e9cb6a824681eb7563157a1ab0a29deb25849a457b","bytes":5568,"lines":53},{"path":"docs/adr/0012-governed-migration-handoff.md","sha256":"713855d670001d3964ecb36cc653830502fb1d82a58b9e39f564b6992dd2bd80","bytes":5965,"lines":59},{"path":"docs/adr/0013-governed-requisition-review-packet.md","sha256":"70bf2cbdf903a8793d6d8bc116a08331931090118341f42010236e09c6cc1802","bytes":4693,"lines":46},{"path":"docs/adr/0014-job-analysis-snapshot-persistence.md","sha256":"a7ab6fee50aaa63f7f407516a4cb39885faeb0fc6e5035ee8fc352ed73430105","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"lines":69},{"path":"docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md","sha256":"b64f21abb19373e780db8b9e64deb8ba9a6219ccf9625a651f25407b8691fcbd","bytes":8227,"lines":226},{"path":"docs/superpowers/specs/2026-08-15-orgmetra-foundation-design.md","sha256":"4a0e1a7943e40d12bd3082db3757045b4085e5a089fea7bc0d8a1565ffcbcf1d","bytes":6237,"lines":187},{"path":"package.json","sha256":"59ae9e3e67c3fba9320cb18439692395cdfd16ae5c24e3c4cf30d77d63ebabb5","bytes":388,"lines":9},{"path":"packages/hris-kernel/src/orgmetra_hris_kernel/audit.py","sha256":"3e5b7190cf857dc8c1fc7e898cef303060f34aabee6c27a9034d4d9650e33190","bytes":7707,"lines":160},{"path":"packages/hris-kernel/tests/test_audit_outbox.py","sha256":"5928dd7b97fe38d6b7472ce62966437e339058a59c3b301a93a7b5c05432b40c","bytes":7556,"lines":200},{"path":"schemas/openapi.yaml","sha256":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"path":"scripts/foundation-contract.mjs","sha256":"5242dcdbe0935775edf074462c82600e9bc4927d9fdc50c47727af915fd4b23a","bytes":218,"lines":6},{"path":"tests/dispatcher-inventory.test.mjs","sha256":"09f5e64410e6b7a26bf8d6ce61c50b737da2ea85d955f91eba63aa21f1537261","bytes":1597,"lines":34},{"path":"tests/foundation-contract.test.mjs","sha256":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"path":"tests/openapi-contract.test.mjs","sha256":"80c1610ef1c189fa325e55389501e0e51531ddf61ee335bb94d9cb3aa55a9fdc","bytes":6438,"lines":195},{"path":"tests/test_audit_outbox_hardening_postgres.sh","sha256":"518ba2f37ba6292943e5abe22c2599452b2f031a42e453b2493aedf8714421a0","bytes":13396,"lines":333},{"path":"tests/test_audit_outbox_postgres.sh","sha256":"e57a04920a0ba97fa6a06752d15ea150016ab8d44099e998c5c4f4067592b4d2","bytes":13443,"lines":357},{"path":"tests/test_bitemporal_postgres.sh","sha256":"7684b8c2ff52c044c081135515bd5aabbfd00e2daad0d471b0868701af2df6cc","bytes":8209,"lines":230},{"path":"tests/test_candidate_worker_conversion_postgres.sh","sha256":"681cb74d6cfa859ed92c6c2439881ea20c430ef8df94ec662e2807761a377f90","bytes":14673,"lines":344},{"path":"tests/test_criterion_observation_scope_postgres.sh","sha256":"0ee9539ee57f840c27d08009f7868cdc8662669df78a01dbc8be39216b8f1a3d","bytes":17811,"lines":469},{"path":"tests/test_evidence_sealing_postgres.sh","sha256":"57d16b632a0c60ffdcb4842ceb1cfe25d19c54cefeeefb622ff4fa6e83441ad7","bytes":11349,"lines":370},{"path":"tests/test_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"path":"tests/test_operational_uuid_postgres.sh","sha256":"7378f98f0d4b3000e8ea641d8701f1540dbad71410b3637d81d799969e0f6ff7","bytes":3346,"lines":101},{"path":"tests/test_outbox_claim_postgres.sh","sha256":"1027806d436ebfe34e108c25b6a4001f43b9550f1d70057c6c0d7974323b0c9b","bytes":14817,"lines":429},{"path":"tests/test_outbox_dead_letter_postgres.sh","sha256":"0d728d578e64252e6079f2d141ddaa7fa9cfbf9784e625832273596d69a6e13d","bytes":14008,"lines":377},{"path":"tests/test_people_mutation_idempotency_postgres.sh","sha256":"3f57e12f80bd1b034c9aac54b669d8530106e3e26b3795689671fb53807b3cd5","bytes":16191,"lines":381},{"path":"tests/test_tenant_isolation_postgres.sh","sha256":"dd649435ef8ab9e57f0609c101917e36656a6d40d63de9bcdbdac23d764f6c3a","bytes":15134,"lines":388},{"path":"tests/test_validity_study_case_postgres.sh","sha256":"0070ad58300323c7f9900c5645e0df3106b36ccd245ae686e982c2fd6fa4dc02","bytes":14708,"lines":301},{"path":"tests/validate_repository.py","sha256":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} diff --git a/packages/tepp-adapter/CHANGELOG.md b/packages/tepp-adapter/CHANGELOG.md new file mode 100644 index 000000000..da1d31642 --- /dev/null +++ b/packages/tepp-adapter/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +## Unreleased + +- Add a non-executing, fail-closed Orgmetra binding for TEPP `AnalysisRunRequest` contract v1, pinned to reviewed TEPP protected revision `7c29e7c971d7940e1fb3def1ed3aae2d1bc8ad4a`. +- Bind workforce-validation study/actor provenance, immutable snapshot digest, RFC 3339 cutoff, evidence version, exact request digest, and same-key retry/conflict semantics without copying source text, credentials, or direct identity values into the TEPP request. +- Reject common credential-shaped values in both TEPP foreign opaque identifiers and the idempotency key so the packet cannot persist or forward an obvious secret while asserting that credentials are absent. +- Align `tenant_record_id` with protected Orgmetra's canonical non-sentinel operational UUID contract, including UUIDv7 interoperability, while keeping packet-owned validation-study and actor references UUIDv4-constrained. +- Detach timezone-aware `knowledge_cutoff` and `generated_at` values to exact UTC datetimes before canonical request and governance evidence, rejecting failing, malformed, or overflowing timezone providers. +- Keep transport disabled until a compatible executable TEPP service contract is published and re-resolved by the host; returned analytical/LLM evidence remains human-scientific-review-only. +- Make `TEPP Adapter Quality` retrigger on shared repository Python/test/clean-checkout configuration, with an executable regression preventing stale adapter-quality evidence after shared tooling changes. diff --git a/packages/tepp-adapter/README.md b/packages/tepp-adapter/README.md new file mode 100644 index 000000000..4e4e51bff --- /dev/null +++ b/packages/tepp-adapter/README.md @@ -0,0 +1,32 @@ +# Orgmetra TEPP adapter + +`orgmetra-tepp-adapter` is Orgmetra's fail-closed pre-transport boundary for TEPP analysis-run request contract v1. It lets an authorized workforce-validation host bind an Orgmetra validation study, accountable actor, immutable analytical snapshot evidence, temporal cutoff, and idempotency evidence to the exact field shape published by TEPP without reading TEPP tables or copying TEPP implementation code. + +## Current maturity + +This package is **active-PR Orgmetra code**. It does not make TEPP HTTP service availability a shipped fact. The reviewed TEPP protected revision is `7c29e7c971d7940e1fb3def1ed3aae2d1bc8ad4a`; at that revision TEPP publishes `AnalysisRunRequest` v1 as a Rust wire DTO, while its API documentation explicitly says protected main is not yet a production HTTP service. Therefore this adapter creates a governed request packet and deterministic request digest but performs no network transport. + +## Contract + +`TeppAnalysisRequestPacket.tepp_request()` emits exactly seven TEPP v1 fields: `contract_version`, `idempotency_key`, `tenant_workspace_id`, `snapshot_id`, RFC 3339 `knowledge_cutoff`, `model_contract_version`, and `output_profile`. Unknown Orgmetra governance fields never enter the foreign body because TEPP's DTO is fail-closed on unknown fields. + +The packet additionally binds, on the Orgmetra side, the authoritative canonical non-sentinel operational tenant UUID, opaque UUIDv4 validation-study and actor references, an independent SHA-256 snapshot digest, evidence version, generation instant, reviewed TEPP revision, and a digest of the exact TEPP request body. Timezone-aware knowledge and generation values are detached to exact UTC datetimes at construction, so caller-owned timezone providers cannot rewrite request or governance evidence later. Same-key exact retries are distinguishable from same-key semantic conflicts before transport. The tenant identifier follows the protected Orgmetra core identity contract rather than imposing a duplicate UUID-version policy; packet-owned trust references remain UUIDv4-constrained. + +## Privacy and authority + +Opaque actor, study, workspace, and snapshot correlations remain linkable personal/governance data, so the packet declares `contains_personal_data=true`; it does **not** claim anonymity. Direct identity values, source text, credentials, free-form case narrative, or model output are not accepted by this boundary. The packet representation is fully redacted. + +Model-contract and output-profile strings are syntax-constrained machine codes only; syntax is not authority. Immediately before any transport the host must re-resolve the exact tenant, validation study, accountable actor, TEPP workspace/snapshot binding, snapshot digest, model contract, output profile, purpose authorization, and contract compatibility. Any TEPP/LLM result remains untrusted analytical evidence and cannot autonomously make a high-impact employment decision. + +## Idempotency + +Persist the packet's `idempotency_key`, `request_digest()`, and `governance_scope_digest()` together in Orgmetra's authoritative audit/outbox transaction before a future executable transport is enabled. An exact retry has the same key, request digest, and governance-scope digest. Reusing the key with a different request or governance digest is a conflict and must fail closed. This package exposes comparison helpers but does not invent a second persistence store. + +## Test + +```bash +PYTHONPATH=packages/tepp-adapter/src \ +python -m pytest -c packages/tepp-adapter/pyproject.toml packages/tepp-adapter/tests +``` + +The package quality gate requires exact 100% owned production statement and branch coverage. diff --git a/packages/tepp-adapter/pyproject.toml b/packages/tepp-adapter/pyproject.toml new file mode 100644 index 000000000..275847131 --- /dev/null +++ b/packages/tepp-adapter/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["setuptools>=69"] +build-backend = "setuptools.build_meta" + +[project] +name = "orgmetra-tepp-adapter" +version = "0.1.0" +description = "Fail-closed TEPP analysis-run request adapter for governed Orgmetra workforce validation." +requires-python = ">=3.12" + +[project.optional-dependencies] +test = ["pytest>=8.3", "pytest-cov>=5.0"] + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = [ + "--cov=orgmetra_tepp_adapter", + "--cov-branch", + "--cov-report=term-missing", + "--cov-fail-under=100", +] diff --git a/packages/tepp-adapter/src/orgmetra_tepp_adapter/__init__.py b/packages/tepp-adapter/src/orgmetra_tepp_adapter/__init__.py new file mode 100644 index 000000000..66b16cdbb --- /dev/null +++ b/packages/tepp-adapter/src/orgmetra_tepp_adapter/__init__.py @@ -0,0 +1,15 @@ +"""Public Orgmetra → TEPP analysis-run integration boundary.""" + +from .analysis import ( + TEPP_ANALYSIS_RUN_CONTRACT_VERSION, + TEPP_PROTECTED_REVISION, + TeppAnalysisRequestPacket, + build_tepp_analysis_request_packet, +) + +__all__ = [ + "TEPP_ANALYSIS_RUN_CONTRACT_VERSION", + "TEPP_PROTECTED_REVISION", + "TeppAnalysisRequestPacket", + "build_tepp_analysis_request_packet", +] diff --git a/packages/tepp-adapter/src/orgmetra_tepp_adapter/analysis.py b/packages/tepp-adapter/src/orgmetra_tepp_adapter/analysis.py new file mode 100644 index 000000000..bb1bb8288 --- /dev/null +++ b/packages/tepp-adapter/src/orgmetra_tepp_adapter/analysis.py @@ -0,0 +1,326 @@ +"""Fail-closed Orgmetra binding for TEPP analysis-run request contract v1.""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +import hashlib +import json +import re +from typing import Final +from uuid import UUID + +TEPP_PROTECTED_REVISION: Final = "7c29e7c971d7940e1fb3def1ed3aae2d1bc8ad4a" +TEPP_ANALYSIS_RUN_CONTRACT_VERSION: Final = 1 +_PURPOSE_CODE: Final = "workforce_validation_analysis" +_TRANSPORT_STATE: Final = "requires_published_tepp_service_contract" +_DECISION_AUTHORITY: Final = "human_scientific_review_only" +_LLM_OUTPUT_AUTHORITY: Final = "untrusted_draft_evidence" +_MAX_UUID_INT: Final = (1 << 128) - 1 +_SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$") +_GOVERNED_CODE_PATTERN = re.compile(r"^[a-z0-9][a-z0-9._:-]{0,127}$") +_CREDENTIAL_PREFIXES: Final = ("sk-", "ghp_", "github_pat_", "nvapi-", "bearer") +_NEXT_ACTION: Final = ( + "Within tenant_record_id, re-resolve validation_study_reference, requested_by_actor_reference, " + "tepp_workspace_id, and tepp_snapshot_id; prove the workspace and immutable snapshot belong to " + "the authorized workforce-validation scope; verify snapshot_digest and evidence_version; verify " + f"TEPP protected revision {TEPP_PROTECTED_REVISION} remains compatible with analysis-run contract " + "version 1 and that an executable TEPP service contract is actually published before transport; " + "then record accountable human scientific review of any returned analytical evidence before it can " + "influence a high-impact employment decision." +) + + +def _validate_operational_uuid(value: str, field_name: str) -> None: + """Require canonical non-sentinel UUID text for an authoritative Orgmetra identity.""" + if type(value) is not str: + raise ValueError(f"{field_name} must be canonical UUID text") + try: + parsed = UUID(value) + except (ValueError, AttributeError, TypeError) as error: + raise ValueError(f"{field_name} must be canonical UUID text") from error + if str(parsed) != value or parsed.int in {0, _MAX_UUID_INT}: + raise ValueError(f"{field_name} must be a canonical operational UUID") + + +def _validate_uuid4(value: str, field_name: str) -> None: + """Require one exact canonical, non-sentinel UUIDv4 string.""" + if type(value) is not str: + raise ValueError(f"{field_name} must be a canonical UUIDv4 string") + try: + parsed = UUID(value) + except (ValueError, AttributeError, TypeError) as error: + raise ValueError(f"{field_name} must be a canonical UUIDv4 string") from error + if str(parsed) != value or parsed.version != 4 or parsed.int in {0, _MAX_UUID_INT}: + raise ValueError(f"{field_name} must be a canonical non-sentinel UUIDv4 string") + + +def _validate_reference(value: str, namespace: str, field_name: str) -> None: + """Require one namespaced opaque UUIDv4 reference owned by Orgmetra.""" + if type(value) is not str or not value.startswith(f"{namespace}:"): + raise ValueError(f"{field_name} must use the {namespace}: namespace") + _validate_uuid4(value.removeprefix(f"{namespace}:"), field_name) + + +def _validate_digest(value: str, field_name: str) -> None: + """Require one lowercase SHA-256 evidence digest.""" + if type(value) is not str or _SHA256_PATTERN.fullmatch(value) is None: + raise ValueError(f"{field_name} must be a lowercase SHA-256 digest") + + +def _validate_evidence_version(value: int) -> None: + """Require one bounded positive evidence version.""" + if type(value) is not int or not 1 <= value <= 2_147_483_647: + raise ValueError("evidence_version must be an integer from 1 through 2147483647") + + +def _validate_aware_datetime(value: datetime, field_name: str) -> datetime: + """Validate and detach an exact built-in datetime to a UTC instant.""" + if type(value) is not datetime or value.tzinfo is None: + raise ValueError(f"{field_name} must be a 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 a timezone-aware datetime") from exc + if type(offset) is not timedelta: + raise ValueError(f"{field_name} must be a 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 a timezone-aware datetime") from exc + + +def _canonical_rfc3339(value: datetime, field_name: str = "knowledge_cutoff") -> str: + """Render an aware instant as deterministic RFC 3339 UTC text.""" + canonical = _validate_aware_datetime(value, field_name) + return canonical.isoformat(timespec="microseconds").replace("+00:00", "Z") + + +def _validate_opaque_identifier(value: str, field_name: str, maximum_length: int = 256) -> None: + """Require exact bounded printable text with no whitespace, controls, or credential shape.""" + if type(value) is not str or not 1 <= len(value) <= maximum_length: + raise ValueError(f"{field_name} must be a bounded opaque identifier") + if any(ord(character) < 0x21 or ord(character) > 0x7E for character in value): + raise ValueError(f"{field_name} must contain only visible ASCII without whitespace") + if value.lower().startswith(_CREDENTIAL_PREFIXES): + raise ValueError(f"{field_name} must not contain a credential-shaped value") + + +def _validate_idempotency_key(value: str) -> None: + """Require an exact durable-correlation-safe idempotency key.""" + if type(value) is not str or not 16 <= len(value) <= 128: + raise ValueError("idempotency_key must contain 16 through 128 visible ASCII characters") + if any(ord(character) < 0x21 or ord(character) > 0x7E for character in value): + raise ValueError("idempotency_key must contain 16 through 128 visible ASCII characters") + if value.lower().startswith(_CREDENTIAL_PREFIXES): + raise ValueError("idempotency_key must not contain a credential-shaped value") + + +def _validate_governed_code(value: str, field_name: str) -> None: + """Require exact bounded machine-code text rather than free-form narrative.""" + if type(value) is not str or _GOVERNED_CODE_PATTERN.fullmatch(value) is None: + raise ValueError(f"{field_name} must be a bounded governed machine code") + + +@dataclass(frozen=True, slots=True, repr=False) +class TeppAnalysisRequestPacket: + """Immutable pre-transport evidence for one TEPP analysis-run request. + + The packet intentionally does not perform HTTP. It binds Orgmetra governance + evidence to TEPP's published Rust wire DTO while keeping foreign service + availability and authorization authoritative at the host boundary. + """ + + tenant_record_id: str + validation_study_reference: str + requested_by_actor_reference: str + tepp_workspace_id: str + tepp_snapshot_id: str + snapshot_digest: str + idempotency_key: str + knowledge_cutoff: datetime + model_contract_version: str + output_profile: str + generated_at: datetime + evidence_version: int = 1 + purpose_code: str = _PURPOSE_CODE + tepp_contract_version: int = TEPP_ANALYSIS_RUN_CONTRACT_VERSION + tepp_protected_revision: str = TEPP_PROTECTED_REVISION + transport_state: str = _TRANSPORT_STATE + decision_authority: str = _DECISION_AUTHORITY + llm_output_authority: str = _LLM_OUTPUT_AUTHORITY + contains_personal_data: bool = True + contains_direct_identity_values: bool = False + contains_source_text: bool = False + contains_credentials: bool = False + human_confirmation_required: bool = True + next_action: str = _NEXT_ACTION + + def __repr__(self) -> str: + """Return a representation that never emits tenant or evidence correlation values.""" + return "TeppAnalysisRequestPacket()" + + def __post_init__(self) -> None: + """Fail closed when direct construction drifts from the governed boundary.""" + object.__setattr__( + self, + "knowledge_cutoff", + _validate_aware_datetime(self.knowledge_cutoff, "knowledge_cutoff"), + ) + object.__setattr__( + self, + "generated_at", + _validate_aware_datetime(self.generated_at, "generated_at"), + ) + _validate_operational_uuid(self.tenant_record_id, "tenant_record_id") + _validate_reference(self.validation_study_reference, "validation_study", "validation_study_reference") + _validate_reference(self.requested_by_actor_reference, "actor", "requested_by_actor_reference") + _validate_opaque_identifier(self.tepp_workspace_id, "tepp_workspace_id") + _validate_opaque_identifier(self.tepp_snapshot_id, "tepp_snapshot_id") + _validate_digest(self.snapshot_digest, "snapshot_digest") + _validate_idempotency_key(self.idempotency_key) + _validate_governed_code(self.model_contract_version, "model_contract_version") + _validate_governed_code(self.output_profile, "output_profile") + if self.generated_at < self.knowledge_cutoff: + raise ValueError("generated_at must not precede knowledge_cutoff") + if self.tepp_workspace_id == self.tepp_snapshot_id: + raise ValueError("tepp_workspace_id and tepp_snapshot_id must be distinct opaque identifiers") + _validate_evidence_version(self.evidence_version) + if self.purpose_code != _PURPOSE_CODE: + raise ValueError(f"purpose_code must remain {_PURPOSE_CODE}") + if self.tepp_contract_version != TEPP_ANALYSIS_RUN_CONTRACT_VERSION: + raise ValueError("tepp_contract_version must remain 1") + if self.tepp_protected_revision != TEPP_PROTECTED_REVISION: + raise ValueError("tepp_protected_revision must remain pinned to the reviewed protected revision") + if self.transport_state != _TRANSPORT_STATE: + raise ValueError(f"transport_state must remain {_TRANSPORT_STATE}") + if self.decision_authority != _DECISION_AUTHORITY: + raise ValueError(f"decision_authority must remain {_DECISION_AUTHORITY}") + if self.llm_output_authority != _LLM_OUTPUT_AUTHORITY: + raise ValueError(f"llm_output_authority must remain {_LLM_OUTPUT_AUTHORITY}") + if self.contains_personal_data is not True: + raise ValueError("contains_personal_data must remain true because governed correlations are linkable") + for field_name in ("contains_direct_identity_values", "contains_source_text", "contains_credentials"): + if getattr(self, field_name) is not False: + raise ValueError(f"{field_name} must remain false") + if self.human_confirmation_required is not True: + raise ValueError("human_confirmation_required must remain true") + if self.next_action != _NEXT_ACTION: + raise ValueError("next_action must remain the governed TEPP handoff instruction") + + def tepp_request(self) -> dict[str, object]: + """Return exactly the fields accepted by TEPP `AnalysisRunRequest` v1.""" + return { + "contract_version": self.tepp_contract_version, + "idempotency_key": self.idempotency_key, + "tenant_workspace_id": self.tepp_workspace_id, + "snapshot_id": self.tepp_snapshot_id, + "knowledge_cutoff": _canonical_rfc3339(self.knowledge_cutoff), + "model_contract_version": self.model_contract_version, + "output_profile": self.output_profile, + } + + def canonical_tepp_json(self) -> str: + """Return deterministic JSON suitable for request-digest and retry comparison.""" + return json.dumps(self.tepp_request(), sort_keys=True, separators=(",", ":"), ensure_ascii=True) + + def request_digest(self) -> str: + """Return SHA-256 over the exact canonical TEPP request body.""" + return hashlib.sha256(self.canonical_tepp_json().encode("utf-8")).hexdigest() + + def governance_scope_digest(self) -> str: + """Return SHA-256 over local governance fields that must remain stable on retry. + + Every tenant-, study-, actor-, workspace-, snapshot-, and idempotency-scoped + correlation participates, so swapping any single reference between packets + from different scopes changes the digest and fails the retry comparison. + """ + payload = { + "evidence_version": self.evidence_version, + "idempotency_key": self.idempotency_key, + "requested_by_actor_reference": self.requested_by_actor_reference, + "snapshot_digest": self.snapshot_digest, + "tepp_snapshot_id": self.tepp_snapshot_id, + "tepp_workspace_id": self.tepp_workspace_id, + "tenant_record_id": self.tenant_record_id, + "validation_study_reference": self.validation_study_reference, + } + canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + return hashlib.sha256(canonical.encode("utf-8")).hexdigest() + + def is_idempotent_retry_of(self, other: object) -> bool: + """Return whether another packet replays the same key, TEPP body, and local scope.""" + return ( + type(other) is TeppAnalysisRequestPacket + and self.idempotency_key == other.idempotency_key + and self.request_digest() == other.request_digest() + and self.governance_scope_digest() == other.governance_scope_digest() + ) + + def idempotency_conflicts_with(self, other: object) -> bool: + """Return whether one key has been rebound to different TEPP or governance semantics.""" + return ( + type(other) is TeppAnalysisRequestPacket + and self.idempotency_key == other.idempotency_key + and ( + self.request_digest() != other.request_digest() + or self.governance_scope_digest() != other.governance_scope_digest() + ) + ) + + def governance_evidence(self) -> dict[str, object]: + """Return purpose-minimized Orgmetra evidence for durable audit/outbox correlation.""" + return { + "tenant_record_id": self.tenant_record_id, + "validation_study_reference": self.validation_study_reference, + "requested_by_actor_reference": self.requested_by_actor_reference, + "tepp_workspace_id": self.tepp_workspace_id, + "tepp_snapshot_id": self.tepp_snapshot_id, + "snapshot_digest": self.snapshot_digest, + "evidence_version": self.evidence_version, + "generated_at": _canonical_rfc3339(self.generated_at, "generated_at"), + "purpose_code": self.purpose_code, + "tepp_contract_version": self.tepp_contract_version, + "tepp_protected_revision": self.tepp_protected_revision, + "idempotency_key": self.idempotency_key, + "tepp_request_digest": self.request_digest(), + "governance_scope_digest": self.governance_scope_digest(), + "transport_state": self.transport_state, + "decision_authority": self.decision_authority, + "llm_output_authority": self.llm_output_authority, + "contains_personal_data": self.contains_personal_data, + "contains_direct_identity_values": self.contains_direct_identity_values, + "human_confirmation_required": self.human_confirmation_required, + } + + +def build_tepp_analysis_request_packet( + *, + tenant_record_id: str, + validation_study_reference: str, + requested_by_actor_reference: str, + tepp_workspace_id: str, + tepp_snapshot_id: str, + snapshot_digest: str, + idempotency_key: str, + knowledge_cutoff: datetime, + model_contract_version: str, + output_profile: str, + generated_at: datetime, + evidence_version: int = 1, +) -> TeppAnalysisRequestPacket: + """Build one packet without exposing fixed authority or privacy controls as inputs.""" + return TeppAnalysisRequestPacket( + tenant_record_id=tenant_record_id, + validation_study_reference=validation_study_reference, + requested_by_actor_reference=requested_by_actor_reference, + tepp_workspace_id=tepp_workspace_id, + tepp_snapshot_id=tepp_snapshot_id, + snapshot_digest=snapshot_digest, + idempotency_key=idempotency_key, + knowledge_cutoff=knowledge_cutoff, + model_contract_version=model_contract_version, + output_profile=output_profile, + generated_at=generated_at, + evidence_version=evidence_version, + ) diff --git a/packages/tepp-adapter/tests/test_analysis.py b/packages/tepp-adapter/tests/test_analysis.py new file mode 100644 index 000000000..15d74b9d1 --- /dev/null +++ b/packages/tepp-adapter/tests/test_analysis.py @@ -0,0 +1,240 @@ +"""Contract, privacy, and evidence regressions for the Orgmetra → TEPP adapter.""" + +from dataclasses import FrozenInstanceError, replace +from datetime import datetime, timedelta, timezone, tzinfo +import json +from uuid import uuid1 + +import pytest + +from orgmetra_tepp_adapter import ( + TEPP_ANALYSIS_RUN_CONTRACT_VERSION, + TEPP_PROTECTED_REVISION, + TeppAnalysisRequestPacket, + build_tepp_analysis_request_packet, +) + + +def values() -> dict[str, object]: + """Return one valid, non-PII workforce-validation request.""" + return { + "tenant_record_id": "11111111-1111-4111-8111-111111111111", + "validation_study_reference": "validation_study:22222222-2222-4222-8222-222222222222", + "requested_by_actor_reference": "actor:33333333-3333-4333-8333-333333333333", + "tepp_workspace_id": "workspace-opaque-8e9f1d", + "tepp_snapshot_id": "snapshot-opaque-2f6c91", + "snapshot_digest": "a" * 64, + "idempotency_key": "orgmetra-tepp-20260820-0001", + "knowledge_cutoff": datetime(2026, 8, 20, 16, 45, 12, 345678, tzinfo=timezone(timedelta(hours=9))), + "model_contract_version": "temporal-event-v1", + "output_profile": "validation-report", + "generated_at": datetime(2026, 8, 20, 7, 50, 1, 123456, tzinfo=timezone.utc), + } + + +def build_valid() -> TeppAnalysisRequestPacket: + """Build a valid packet through the public builder.""" + return build_tepp_analysis_request_packet(**values()) + + +def test_tenant_identity_accepts_protected_core_operational_uuid_contract() -> None: + """Keep this leaf interoperable with authoritative Orgmetra tenant UUIDs.""" + tenant_uuid7 = "10000000-0000-7000-8000-000000000001" + kwargs = values() + kwargs["tenant_record_id"] = tenant_uuid7 + + packet = build_tepp_analysis_request_packet(**kwargs) + + assert packet.tenant_record_id == tenant_uuid7 + assert replace(build_valid(), tenant_record_id=tenant_uuid7).tenant_record_id == tenant_uuid7 + + +def test_request_matches_exact_tepp_v1_wire_shape_and_is_deterministic() -> None: + packet = build_valid() + request = packet.tepp_request() + + assert request == { + "contract_version": 1, + "idempotency_key": "orgmetra-tepp-20260820-0001", + "tenant_workspace_id": "workspace-opaque-8e9f1d", + "snapshot_id": "snapshot-opaque-2f6c91", + "knowledge_cutoff": "2026-08-20T07:45:12.345678Z", + "model_contract_version": "temporal-event-v1", + "output_profile": "validation-report", + } + assert TEPP_ANALYSIS_RUN_CONTRACT_VERSION == 1 + assert TEPP_PROTECTED_REVISION == "7c29e7c971d7940e1fb3def1ed3aae2d1bc8ad4a" + assert json.loads(packet.canonical_tepp_json()) == request + assert len(packet.request_digest()) == 64 + assert packet.request_digest() == build_valid().request_digest() + + +def test_retry_comparison_detects_exact_replays_and_same_key_conflicts() -> None: + packet = build_valid() + same = build_valid() + changed_values = values() + changed_values["tepp_snapshot_id"] = "snapshot-opaque-different" + conflict = TeppAnalysisRequestPacket(**changed_values) + different_key_values = values() + different_key_values["idempotency_key"] = "orgmetra-tepp-20260820-0002" + different_key = TeppAnalysisRequestPacket(**different_key_values) + + assert packet.is_idempotent_retry_of(same) + assert not packet.idempotency_conflicts_with(same) + assert not packet.is_idempotent_retry_of(conflict) + assert packet.idempotency_conflicts_with(conflict) + assert not packet.is_idempotent_retry_of(different_key) + assert not packet.idempotency_conflicts_with(different_key) + assert not packet.is_idempotent_retry_of(object()) + assert not packet.idempotency_conflicts_with(object()) + + +def test_governance_evidence_is_value_minimized_and_actionable() -> None: + packet = build_valid() + evidence = packet.governance_evidence() + + assert evidence["tenant_record_id"] == packet.tenant_record_id + assert evidence["validation_study_reference"] == packet.validation_study_reference + assert evidence["requested_by_actor_reference"] == packet.requested_by_actor_reference + assert evidence["tepp_workspace_id"] == packet.tepp_workspace_id + assert evidence["tepp_snapshot_id"] == packet.tepp_snapshot_id + assert evidence["snapshot_digest"] == "a" * 64 + assert evidence["generated_at"] == "2026-08-20T07:50:01.123456Z" + assert evidence["tepp_contract_version"] == 1 + assert evidence["tepp_protected_revision"] == TEPP_PROTECTED_REVISION + assert evidence["tepp_request_digest"] == packet.request_digest() + assert evidence["transport_state"] == "requires_published_tepp_service_contract" + assert evidence["decision_authority"] == "human_scientific_review_only" + assert evidence["llm_output_authority"] == "untrusted_draft_evidence" + assert evidence["contains_personal_data"] is True + assert evidence["contains_direct_identity_values"] is False + assert evidence["human_confirmation_required"] is True + assert "source_text" not in evidence + assert "credentials" not in evidence + assert "salary" not in json.dumps(evidence) + assert "executable TEPP service contract is actually published before transport" in packet.next_action + assert "human scientific review" in packet.next_action + + +def test_repr_and_immutability_protect_correlation_values() -> None: + packet = build_valid() + rendered = repr(packet) + + assert rendered == "TeppAnalysisRequestPacket()" + assert packet.tenant_record_id not in rendered + assert packet.validation_study_reference not in rendered + assert packet.snapshot_digest not in rendered + with pytest.raises(FrozenInstanceError): + packet.tepp_workspace_id = "changed" # type: ignore[misc] + + +@pytest.mark.parametrize( + ("field_name", "value", "message"), + [ + ("tenant_record_id", "00000000-0000-0000-0000-000000000000", "UUID"), + ("tenant_record_id", "ffffffff-ffff-ffff-ffff-ffffffffffff", "UUID"), + ("tenant_record_id", "11111111-1111-4111-8111-11111111111A", "UUID"), + ("tenant_record_id", "not-a-uuid", "UUID"), + ("tenant_record_id", 1, "UUID"), + ("validation_study_reference", "wrong:22222222-2222-4222-8222-222222222222", "validation_study"), + ("validation_study_reference", f"validation_study:{uuid1()}", "UUIDv4"), + ("requested_by_actor_reference", "actor:not-a-uuid", "UUIDv4"), + ("snapshot_digest", "A" * 64, "lowercase SHA-256"), + ("snapshot_digest", 1, "lowercase SHA-256"), + ("evidence_version", 0, "1 through"), + ("evidence_version", True, "1 through"), + ("evidence_version", 2_147_483_648, "1 through"), + ], +) +def test_local_authority_and_evidence_identifiers_fail_closed(field_name: str, value: object, message: str) -> None: + kwargs = values() + kwargs[field_name] = value + with pytest.raises(ValueError, match=message): + TeppAnalysisRequestPacket(**kwargs) + + +@pytest.mark.parametrize( + ("field_name", "value", "message"), + [ + ("tepp_workspace_id", "", "bounded opaque"), + ("tepp_workspace_id", "a" * 257, "bounded opaque"), + ("tepp_workspace_id", "workspace with space", "visible ASCII"), + ("tepp_workspace_id", "sk-secret-looking-token", "credential-shaped"), + ("tepp_snapshot_id", "github_pat_secretlooking", "credential-shaped"), + ("tepp_workspace_id", 7, "bounded opaque"), + ("tepp_snapshot_id", "snapshot\nvalue", "visible ASCII"), + ("idempotency_key", "short", "16 through 128"), + ("idempotency_key", "a" * 129, "16 through 128"), + ("idempotency_key", "valid-length-but space", "16 through 128"), + ("idempotency_key", "sk-secret-looking-idempotency", "credential-shaped"), + ("idempotency_key", 7, "16 through 128"), + ("model_contract_version", "Temporal Event", "governed machine code"), + ("model_contract_version", "", "governed machine code"), + ("model_contract_version", 1, "governed machine code"), + ("output_profile", "x" * 129, "governed machine code"), + ], +) +def test_foreign_contract_tokens_are_bounded_without_inventing_foreign_authority( + field_name: str, value: object, message: str +) -> None: + kwargs = values() + kwargs[field_name] = value + with pytest.raises(ValueError, match=message): + TeppAnalysisRequestPacket(**kwargs) + + +class NullOffsetTz(tzinfo): + """Timezone object whose offset is intentionally unusable.""" + + def utcoffset(self, dt: datetime | None) -> None: + return None + + def dst(self, dt: datetime | None) -> None: + return None + + def tzname(self, dt: datetime | None) -> str: + return "NULL" + + +@pytest.mark.parametrize( + ("field_name", "value"), + [ + ("knowledge_cutoff", datetime(2026, 8, 20, 7, 45)), + ("knowledge_cutoff", "2026-08-20T07:45:00Z"), + ("generated_at", datetime(2026, 8, 20, 7, 50).replace(tzinfo=NullOffsetTz())), + ], +) +def test_temporal_evidence_requires_real_timezone_aware_instants(field_name: str, value: object) -> None: + kwargs = values() + kwargs[field_name] = value + with pytest.raises(ValueError, match="timezone-aware"): + TeppAnalysisRequestPacket(**kwargs) + + +@pytest.mark.parametrize( + ("field_name", "value", "message"), + [ + ("purpose_code", "selection_decision", "workforce_validation_analysis"), + ("tepp_contract_version", 2, "remain 1"), + ("tepp_protected_revision", "0" * 40, "reviewed protected revision"), + ("transport_state", "ready_to_send", "requires_published_tepp_service_contract"), + ("decision_authority", "automated", "human_scientific_review_only"), + ("llm_output_authority", "authoritative", "untrusted_draft_evidence"), + ("contains_personal_data", False, "must remain true"), + ("contains_direct_identity_values", True, "must remain false"), + ("contains_source_text", True, "must remain false"), + ("contains_credentials", True, "must remain false"), + ("human_confirmation_required", False, "must remain true"), + ("next_action", "Send immediately.", "governed TEPP handoff"), + ], +) +def test_direct_construction_and_replace_cannot_expand_authority( + field_name: str, value: object, message: str +) -> None: + kwargs = values() + kwargs[field_name] = value + with pytest.raises(ValueError, match=message): + TeppAnalysisRequestPacket(**kwargs) + + with pytest.raises(ValueError, match=message): + replace(build_valid(), **{field_name: value}) diff --git a/packages/tepp-adapter/tests/test_idempotency_governance_scope.py b/packages/tepp-adapter/tests/test_idempotency_governance_scope.py new file mode 100644 index 000000000..2f4a3c563 --- /dev/null +++ b/packages/tepp-adapter/tests/test_idempotency_governance_scope.py @@ -0,0 +1,65 @@ +"""Regression coverage for Orgmetra-scoped TEPP idempotency semantics.""" + +from __future__ import annotations + +from dataclasses import replace + +import pytest + +from test_analysis import build_valid + + +@pytest.mark.parametrize( + ("field_name", "replacement"), + ( + ("tenant_record_id", "44444444-4444-4444-8444-444444444444"), + ( + "validation_study_reference", + "validation_study:55555555-5555-4555-8555-555555555555", + ), + ("requested_by_actor_reference", "actor:66666666-6666-4666-8666-666666666666"), + ("snapshot_digest", "b" * 64), + ("evidence_version", 2), + ), +) +def test_same_tepp_request_key_cannot_replay_across_governance_scope( + field_name: str, + replacement: object, +) -> None: + """Treat same-key reuse across local tenant/study/actor/evidence scope as conflict.""" + packet = build_valid() + rebound = replace(packet, **{field_name: replacement}) + + assert packet.request_digest() == rebound.request_digest() + assert not packet.is_idempotent_retry_of(rebound) + assert packet.idempotency_conflicts_with(rebound) + + +def test_governance_evidence_carries_the_key_required_for_durable_retry_control() -> None: + """Keep the durable audit/outbox evidence self-sufficient for retry/conflict checks.""" + packet = build_valid() + + evidence = packet.governance_evidence() + + assert evidence["idempotency_key"] == packet.idempotency_key + assert evidence["tepp_request_digest"] == packet.request_digest() + assert evidence["governance_scope_digest"] == packet.governance_scope_digest() + + +@pytest.mark.parametrize( + ("field_name", "replacement"), + ( + ("idempotency_key", "orgmetra-tepp-20260820-9999"), + ("tepp_snapshot_id", "snapshot-opaque-tampered"), + ("tepp_workspace_id", "workspace-opaque-tampered"), + ), +) +def test_scope_digest_binds_every_retry_stable_correlation( + field_name: str, + replacement: object, +) -> None: + """Detect reference manipulation of any correlation inside the retry-stable scope.""" + packet = build_valid() + rebound = replace(packet, **{field_name: replacement}) + + assert packet.governance_scope_digest() != rebound.governance_scope_digest() diff --git a/packages/tepp-adapter/tests/test_quality_workflow_trigger.py b/packages/tepp-adapter/tests/test_quality_workflow_trigger.py new file mode 100644 index 000000000..93c39cd2d --- /dev/null +++ b/packages/tepp-adapter/tests/test_quality_workflow_trigger.py @@ -0,0 +1,27 @@ +"""Regression tests for the TEPP-adapter quality-gate trigger surface.""" + +from pathlib import Path + + +_WORKFLOW_PATH = Path(".github/workflows/tepp-adapter-quality.yml") +_SHARED_TEST_CONFIGURATION = ( + ".gitignore", + ".python-version", + "conftest.py", + "packages/conftest.py", + "pyproject.toml", + "pytest.ini", + "setup.cfg", + "tox.ini", +) + + +def test_quality_workflow_retriggers_on_shared_test_configuration() -> None: + """Require every shared test/runtime configuration input to retrigger this gate.""" + workflow = _WORKFLOW_PATH.read_text(encoding="utf-8") + + for path in _SHARED_TEST_CONFIGURATION: + assert f'- "{path}"' in workflow, ( + f"{path} can change package test or clean-checkout behavior and must retrigger " + "TEPP Adapter Quality" + ) diff --git a/packages/tepp-adapter/tests/test_retry_type_annotations.py b/packages/tepp-adapter/tests/test_retry_type_annotations.py new file mode 100644 index 000000000..c2cadca33 --- /dev/null +++ b/packages/tepp-adapter/tests/test_retry_type_annotations.py @@ -0,0 +1,14 @@ +"""Static contract regressions for TEPP retry-comparison helper annotations.""" + +from typing import get_type_hints + +from orgmetra_tepp_adapter import TeppAnalysisRequestPacket + + +def test_retry_comparison_accepts_arbitrary_runtime_objects_in_its_type_contract() -> None: + """Keep annotations aligned with the exact-type fail-closed runtime boundary.""" + retry_hints = get_type_hints(TeppAnalysisRequestPacket.is_idempotent_retry_of) + conflict_hints = get_type_hints(TeppAnalysisRequestPacket.idempotency_conflicts_with) + + assert retry_hints["other"] is object + assert conflict_hints["other"] is object diff --git a/packages/tepp-adapter/tests/test_string_runtime_evidence_integrity.py b/packages/tepp-adapter/tests/test_string_runtime_evidence_integrity.py new file mode 100644 index 000000000..bdfb845b8 --- /dev/null +++ b/packages/tepp-adapter/tests/test_string_runtime_evidence_integrity.py @@ -0,0 +1,96 @@ +"""Regression coverage for hostile string subclasses at the TEPP evidence boundary.""" + +from __future__ import annotations + +from datetime import datetime, timedelta, timezone + +import pytest + +from orgmetra_tepp_adapter import build_tepp_analysis_request_packet +from orgmetra_tepp_adapter.analysis import _validate_uuid4 + + +class ForgedReference(str): + """Forge namespace removal while retaining attacker-controlled underlying text.""" + + def startswith(self, prefix, *args): # type: ignore[no-untyped-def] + return True + + def removeprefix(self, prefix): # type: ignore[no-untyped-def] + return "22222222-2222-4222-8222-222222222222" + + +class ForgedTenantUUIDText(str): + """Forge UUID parsing/canonical equality while retaining malformed tenant text.""" + + def replace(self, old, new, *args): # type: ignore[no-untyped-def] + canonical = "11111111-1111-4111-8111-111111111111" + return canonical.replace(old, new, *args) + + def __eq__(self, other): # type: ignore[no-untyped-def] + return other is not None + + def __ne__(self, other): # type: ignore[no-untyped-def] + return other is None + + +class ForgedOpaqueIdentifier(str): + """Hide a credential-shaped underlying value from the credential detector.""" + + def lower(self): # type: ignore[no-untyped-def] + return "workspace-safe" + + +def values() -> dict[str, object]: + """Return one valid governed TEPP request packet input.""" + return { + "tenant_record_id": "11111111-1111-4111-8111-111111111111", + "validation_study_reference": "validation_study:22222222-2222-4222-8222-222222222222", + "requested_by_actor_reference": "actor:33333333-3333-4333-8333-333333333333", + "tepp_workspace_id": "workspace-opaque-8e9f1d", + "tepp_snapshot_id": "snapshot-opaque-2f6c91", + "snapshot_digest": "a" * 64, + "idempotency_key": "orgmetra-tepp-20260820-0001", + "knowledge_cutoff": datetime(2026, 8, 20, 16, 45, 12, 345678, tzinfo=timezone(timedelta(hours=9))), + "model_contract_version": "temporal-event-v1", + "output_profile": "validation-report", + "generated_at": datetime(2026, 8, 20, 7, 50, 1, 123456, tzinfo=timezone.utc), + } + + +def test_rejects_reference_string_subclass_that_can_forge_namespace_validation() -> None: + """Reject subclassed references before namespace helpers can be overridden.""" + kwargs = values() + kwargs["validation_study_reference"] = ForgedReference("evil-reference") + with pytest.raises(ValueError, match="validation_study_reference"): + build_tepp_analysis_request_packet(**kwargs) + + +def test_rejects_tenant_string_subclass_that_can_forge_uuid_validation() -> None: + """Reject subclassed authoritative tenant text before UUID canonicalization.""" + kwargs = values() + kwargs["tenant_record_id"] = ForgedTenantUUIDText("not-a-tenant-uuid") + with pytest.raises(ValueError, match="tenant_record_id"): + build_tepp_analysis_request_packet(**kwargs) + + +def test_uuid4_validator_rejects_string_subclass_before_uuid_parsing() -> None: + """Keep the shared UUIDv4 validator independently fail-closed for future callers.""" + with pytest.raises(ValueError, match="probe_reference"): + _validate_uuid4(ForgedTenantUUIDText("22222222-2222-4222-8222-222222222222"), "probe_reference") + + +def test_rejects_opaque_string_subclass_that_can_hide_credential_shape() -> None: + """Reject subclassed opaque identifiers before credential-shape inspection.""" + kwargs = values() + kwargs["tepp_workspace_id"] = ForgedOpaqueIdentifier("sk-secret-workspace") + with pytest.raises(ValueError, match="tepp_workspace_id"): + build_tepp_analysis_request_packet(**kwargs) + + +def test_rejects_idempotency_string_subclass_that_can_hide_credential_shape() -> None: + """Reject subclassed idempotency keys before credential-shape inspection.""" + kwargs = values() + kwargs["idempotency_key"] = ForgedOpaqueIdentifier("sk-secret-idempotency-key") + with pytest.raises(ValueError, match="idempotency_key"): + build_tepp_analysis_request_packet(**kwargs) diff --git a/packages/tepp-adapter/tests/test_temporal_evidence_integrity.py b/packages/tepp-adapter/tests/test_temporal_evidence_integrity.py new file mode 100644 index 000000000..59fa598a5 --- /dev/null +++ b/packages/tepp-adapter/tests/test_temporal_evidence_integrity.py @@ -0,0 +1,160 @@ +"""Regression coverage for TEPP temporal-evidence canonicalization integrity.""" + +from __future__ import annotations + +from datetime import datetime, timedelta, timezone, tzinfo + +import pytest + +from orgmetra_tepp_adapter import build_tepp_analysis_request_packet + + +class ForgedDateTime(datetime): + """Datetime subclass able to forge canonical TEPP/audit evidence.""" + + def astimezone(self, tz=None): # type: ignore[no-untyped-def] + """Keep the hostile subclass alive across UTC normalization.""" + return self + + def isoformat(self, *args, **kwargs) -> str: # type: ignore[no-untyped-def] + """Return an instant different from the underlying evidence instant.""" + return "2099-12-31T23:59:59+00:00" + + +class MutableOffsetTimezone(tzinfo): + """Timezone provider whose offset can change after packet construction.""" + + def __init__(self, offset: timedelta) -> None: + self.offset = offset + + def utcoffset(self, value: datetime | None) -> timedelta: + """Return the currently configured offset.""" + return self.offset + + +class ExplodingOffsetTimezone(tzinfo): + """Timezone provider that raises while its offset is requested.""" + + def utcoffset(self, value: datetime | None) -> timedelta: + """Raise to verify provider failures become validation errors.""" + raise RuntimeError("offset provider unavailable") + + +class WrongOffsetTimezone(tzinfo): + """Timezone provider that returns a non-timedelta offset.""" + + def utcoffset(self, value: datetime | None) -> str: # type: ignore[override] + """Return an invalid offset type at the trust boundary.""" + return "not-a-timedelta" + + +def valid_kwargs() -> dict[str, object]: + """Return one otherwise valid Orgmetra-to-TEPP request packet input.""" + return { + "tenant_record_id": "11111111-1111-4111-8111-111111111111", + "validation_study_reference": "validation_study:22222222-2222-4222-8222-222222222222", + "requested_by_actor_reference": "actor:33333333-3333-4333-8333-333333333333", + "tepp_workspace_id": "workspace-opaque-8e9f1d", + "tepp_snapshot_id": "snapshot-opaque-2f6c91", + "snapshot_digest": "a" * 64, + "idempotency_key": "orgmetra-tepp-20260821-0001", + "knowledge_cutoff": datetime(2026, 8, 21, 3, 30, tzinfo=timezone.utc), + "model_contract_version": "temporal-event-v1", + "output_profile": "validation-report", + "generated_at": datetime(2026, 8, 21, 4, 30, tzinfo=timezone.utc), + } + + +@pytest.mark.parametrize("field_name", ["knowledge_cutoff", "generated_at"]) +def test_rejects_datetime_subclasses_that_can_forge_temporal_evidence(field_name: str) -> None: + """TEPP request and audit evidence must not invoke caller-overridable datetime methods.""" + kwargs = valid_kwargs() + kwargs[field_name] = ForgedDateTime(2026, 8, 21, 4, 30, tzinfo=timezone.utc) + + with pytest.raises(ValueError, match="timezone-aware datetime"): + build_tepp_analysis_request_packet(**kwargs) + + +@pytest.mark.parametrize("field_name", ["knowledge_cutoff", "generated_at"]) +def test_freezes_mutable_timezone_before_request_and_governance_evidence(field_name: str) -> None: + """Changing a caller-owned timezone cannot rewrite stored TEPP evidence.""" + provider = MutableOffsetTimezone(timedelta(hours=9)) + kwargs = valid_kwargs() + kwargs[field_name] = datetime( + 2026, + 8, + 21, + 12 if field_name == "knowledge_cutoff" else 13, + 30, + 1 if field_name == "generated_at" else 0, + 123456 if field_name == "generated_at" else 0, + tzinfo=provider, + ) + packet = build_tepp_analysis_request_packet(**kwargs) + request = packet.canonical_tepp_json() + evidence = packet.governance_evidence() + + provider.offset = timedelta(hours=10) + + assert packet.knowledge_cutoff == datetime(2026, 8, 21, 3, 30, tzinfo=timezone.utc) + assert packet.generated_at == datetime( + 2026, + 8, + 21, + 4, + 30, + 1 if field_name == "generated_at" else 0, + 123456 if field_name == "generated_at" else 0, + tzinfo=timezone.utc, + ) + assert packet.canonical_tepp_json() == request + assert packet.governance_evidence() == evidence + + +@pytest.mark.parametrize( + ("field_name", "value"), + [ + ("knowledge_cutoff", datetime.min.replace(tzinfo=timezone(timedelta(hours=1)))), + ("generated_at", datetime.max.replace(tzinfo=timezone(-timedelta(hours=1)))), + ("knowledge_cutoff", datetime(2026, 8, 21, 3, 30, tzinfo=ExplodingOffsetTimezone())), + ("generated_at", datetime(2026, 8, 21, 4, 30, tzinfo=ExplodingOffsetTimezone())), + ("knowledge_cutoff", datetime(2026, 8, 21, 3, 30, tzinfo=WrongOffsetTimezone())), + ("generated_at", datetime(2026, 8, 21, 4, 30, tzinfo=WrongOffsetTimezone())), + ], +) +def test_rejects_unusable_temporal_providers(field_name: str, value: object) -> None: + """Provider failures, malformed offsets, and UTC arithmetic overflow fail closed.""" + kwargs = valid_kwargs() + kwargs[field_name] = value + + with pytest.raises(ValueError, match="timezone-aware datetime"): + build_tepp_analysis_request_packet(**kwargs) + + +def test_canonical_evidence_rejects_low_level_datetime_reinjection() -> None: + """Unsafe post-construction mutation cannot invoke forged datetime renderers.""" + packet = build_tepp_analysis_request_packet(**valid_kwargs()) + object.__setattr__(packet, "knowledge_cutoff", ForgedDateTime(2026, 8, 21, 4, 30, tzinfo=timezone.utc)) + + with pytest.raises(ValueError, match="timezone-aware datetime"): + packet.canonical_tepp_json() + + +def test_generated_at_must_not_precede_knowledge_cutoff() -> None: + """Reject packets whose generation instant precedes their own knowledge cutoff.""" + kwargs = valid_kwargs() + kwargs["knowledge_cutoff"] = datetime(2026, 8, 21, 5, 0, tzinfo=timezone.utc) + kwargs["generated_at"] = datetime(2026, 8, 21, 4, 59, 59, 999999, tzinfo=timezone.utc) + + with pytest.raises(ValueError, match="generated_at must not precede knowledge_cutoff"): + build_tepp_analysis_request_packet(**kwargs) + + +def test_equal_workspace_and_snapshot_identifiers_are_rejected() -> None: + """Reference manipulation cannot reuse one opaque identifier for two entity types.""" + kwargs = valid_kwargs() + kwargs["tepp_workspace_id"] = "opaque-duplicated-1f2e3d" + kwargs["tepp_snapshot_id"] = "opaque-duplicated-1f2e3d" + + with pytest.raises(ValueError, match="must be distinct opaque identifiers"): + build_tepp_analysis_request_packet(**kwargs)