diff --git a/.github/workflows/enterprise-architecture-projection-quality.yml b/.github/workflows/enterprise-architecture-projection-quality.yml new file mode 100644 index 000000000..8e31844ab --- /dev/null +++ b/.github/workflows/enterprise-architecture-projection-quality.yml @@ -0,0 +1,56 @@ +name: Enterprise Architecture Projection Quality + +on: + pull_request: + branches: + - bootstrap + - develop + - main + paths: + - "packages/enterprise-architecture-projection/**" + - ".github/requirements/foundation-test.txt" + - ".github/workflows/enterprise-architecture-projection-quality.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: enterprise-architecture-projection-quality-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + unit: + name: EA projection admission and 100% coverage + runs-on: ubuntu-24.04 + 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 Enterprise Architecture projection boundary + run: python -m compileall -q packages/enterprise-architecture-projection/src packages/enterprise-architecture-projection/tests + - name: Test projection admission with exact statement and branch coverage + env: + PYTHONPATH: packages/enterprise-architecture-projection/src + COVERAGE_FILE: /tmp/orgmetra-enterprise-architecture-projection.coverage + run: python -m pytest -c packages/enterprise-architecture-projection/pyproject.toml packages/enterprise-architecture-projection/tests + - name: Require clean checkout + run: | + git diff --exit-code + test -z "$(git status --porcelain)" diff --git a/packages/enterprise-architecture-projection/README.md b/packages/enterprise-architecture-projection/README.md new file mode 100644 index 000000000..d9ac15d71 --- /dev/null +++ b/packages/enterprise-architecture-projection/README.md @@ -0,0 +1,29 @@ +# Enterprise Architecture projection admission + +This package is the Orgmetra-side fail-closed boundary for Enterprise Architecture projection candidates. It exists to keep two systems of record separate: Orgmetra owns HRIS/HCM facts, while the Enterprise Architecture Decision Plane owns accepted architecture truth. + +It does **not** publish directly to Enterprise Architecture Core, reproduce `context-graph-contracts`, or carry person, employment, job, position, assignment, performance, selection, or other authoritative HR record payloads. Cross-service database access is not part of this boundary. + +## Current operating state + +`ContractReleaseEvidence` and `ContractAdmissionEvidence` are validation shapes, not trust capabilities. They are directly constructible Python values, so a caller can never obtain an authorized handoff merely by filling them with plausible release tags, digests, receipt identities, or verification times. `evaluate_projection_readiness` validates their shape and internal relationships but remains fail-closed even when every current field is complete. + +The first gate is an immutable published `ContextualWisdomLab/context-graph-contracts` release. Without one, the next action is `install_approved_context_graph_contract_release`. Release identity alone is deliberately insufficient: the exact released commit and artifact must also be bound to executable semantic conformance, the complete published contract bundle, and artifact provenance. Missing semantic admission keeps the next action at `verify_released_context_graph_contract_admission`. + +Semantic admission still does not prove that a released contract is safe for this consumer. Exact compatibility and migration receipts must be bound to the admitted release. Missing lifecycle receipts produce the separate fail-closed next action `verify_context_graph_contract_compatibility_and_migration`. Conformance, complete-bundle, provenance, compatibility, and migration evidence are independent controls: each retained receipt must have its own SHA-256 identity, malformed or reused receipt identity is rejected, and an explicit no-migration-required result still needs a real digest-bound migration receipt. Admission verification must be at or after the release-verification instant, so pre-release lifecycle evidence cannot satisfy the validation boundary. + +Even shape-complete release and admission evidence remains blocked with `trusted_control_plane_evidence_not_available` and the next action `integrate_released_context_graph_trust_contract`. Orgmetra does not yet have a repository-owned production adapter that can establish that those external values came from an owner-authorized trust source. Documentation alone is not used as an authorization mechanism. + +The package does not guess or reproduce an unreleased Context Assertion, CloudEvent, API schema, profile, issuer, signing-key contract, signature format, admission receipt, provenance format, compatibility result, or migration result. Once the Context Graph owner publishes its immutable trust and verification contract, Orgmetra must implement a repository-owned ACL/control-plane adapter against that released contract and add consumer tests proving that caller-created or unsigned external evidence cannot produce a positive handoff. Only that trusted adapter may unlock a future `proposed` candidate for submission to the Enterprise Architecture owner. Acceptance, replacement, lifecycle decisions, and authoritative EA truth remain separate EA-owner decisions. + +## Candidate scope + +Permitted projection concepts are deployable architecture concerns: applications, services, APIs, workers, databases, runtimes, interfaces, technology components and versions, providers, lifecycle, risk, ownership, remediation, supported capabilities, initiatives, transformations, and dependencies. Projection evidence preserves Orgmetra source revision, effective time, recorded time, non-person ownership references, architecture-only dependency references, and the exact external evidence identities validated by this boundary. + +Every decision carries the exact validated Orgmetra source repository and source revision **and** a SHA-256 over the exact allowed candidate evidence snapshot used by the decision. That digest covers the projection identity and kind, source identity, effective and recorded times, architecture owner, and dependency references. A decision for one candidate therefore cannot be treated as evidence for a different candidate merely because both came from the same Orgmetra commit. + +Candidate construction is not the only integrity boundary. `evaluate_projection_readiness` revalidates the retained candidate, detaches the values used for the decision into an immutable local snapshot, validates that snapshot again, and then computes the candidate evidence digest. A caller cannot use low-level frozen-dataclass mutation to replace a validated architecture owner with a Person reference, restore a mutable dependency collection, or detach the decision evidence from the values that were actually evaluated. The resulting readiness decision is tuple-backed immutable evidence rather than a frozen dataclass, so low-level `object.__setattr__` cannot rewrite a blocked decision into a ready handoff after evaluation. + +Trust-bearing timestamps use exact built-in `datetime` values paired with Python's immutable built-in fixed-offset `timezone`. Behavior-bearing `datetime` subclasses and caller-defined mutable `tzinfo` providers are rejected both when candidate evidence is retained and when release/admission evidence is evaluated. This prevents a value that initially appears timezone-aware from changing offset or comparison behavior after validation and silently rewriting effective, recorded, or verification-time meaning. + +The package intentionally exposes no free-form payload field. If a future use case needs additional fields, add them explicitly with RED tests proving that HR record data cannot cross the boundary. diff --git a/packages/enterprise-architecture-projection/pyproject.toml b/packages/enterprise-architecture-projection/pyproject.toml new file mode 100644 index 000000000..fc5dd7659 --- /dev/null +++ b/packages/enterprise-architecture-projection/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["setuptools>=69"] +build-backend = "setuptools.build_meta" + +[project] +name = "orgmetra-enterprise-architecture-projection" +version = "0.1.0" +description = "Fail-closed admission boundary for Orgmetra enterprise-architecture projections." +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_enterprise_architecture_projection", + "--cov-branch", + "--cov-report=term-missing", + "--cov-fail-under=100", +] diff --git a/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py b/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py new file mode 100644 index 000000000..8ddc067d6 --- /dev/null +++ b/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py @@ -0,0 +1,428 @@ +"""Fail-closed admission boundary for Orgmetra Enterprise Architecture projections. + +This package validates Orgmetra-owned architecture projection candidates and the +shape and internal consistency of external release, conformance, provenance, +compatibility, and migration evidence. Caller-created evidence is never enough +to authorize an Enterprise Architecture handoff: positive admission remains +closed until Orgmetra has a repository-owned adapter for the immutable trust +contract published by the Context Graph owner. The package does not serialize +the foreign contract, write Enterprise Architecture state, or transport +authoritative HR records. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timezone +from enum import StrEnum +import hashlib +import json +import re +from typing import NamedTuple + + +_ORGMETRA_REPOSITORY = "ContextualWisdomLab/Orgmetra" +_CONTRACT_REPOSITORY = "ContextualWisdomLab/context-graph-contracts" +_SHA_RE = re.compile(r"^[0-9a-f]{40}$") +_SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +_RELEASE_TAG_RE = re.compile(r"^v(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$") +_PROJECTION_KEY_RE = re.compile(r"^orgmetra\.[a-z0-9][a-z0-9._-]*$") +_OWNER_REFERENCE_RE = re.compile(r"^(?:team|organization|application):[a-z0-9][a-z0-9._/-]*$") +_DEPENDENCY_REFERENCE_RE = re.compile( + r"^(?:application|service|interface|technology|provider|capability):[a-z0-9][a-z0-9._/-]*$" +) + + +class ProjectionKind(StrEnum): + """Architecture concepts Orgmetra may describe without becoming their authority.""" + + APPLICATION = "application" + SERVICE = "service" + API = "api" + WORKER = "worker" + DATABASE = "database" + RUNTIME = "runtime" + INTERFACE = "interface" + TECHNOLOGY_COMPONENT = "technology_component" + TECHNOLOGY_VERSION = "technology_version" + PROVIDER = "provider" + LIFECYCLE = "lifecycle" + RISK = "risk" + OWNERSHIP = "ownership" + REMEDIATION = "remediation" + CAPABILITY = "capability" + INITIATIVE = "initiative" + TRANSFORMATION = "transformation" + DEPENDENCY = "dependency" + + +class ProjectionTruthStatus(StrEnum): + """Truth status available before the Enterprise Architecture owner accepts data.""" + + PROPOSED = "proposed" + + +@dataclass(frozen=True, slots=True) +class ArchitectureProjectionCandidate: + """Minimal Orgmetra-owned evidence that may become an EA projection candidate.""" + + projection_key: str + projection_kind: ProjectionKind + source_revision: str + source_repository: str + effective_from: datetime + effective_to: datetime | None + recorded_at: datetime + owner_reference: str + dependency_references: tuple[str, ...] + + def __post_init__(self) -> None: + """Normalize immutable collections and reject HR facts or malformed evidence.""" + object.__setattr__(self, "dependency_references", tuple(self.dependency_references)) + _validate_projection_candidate(self) + + +class ContractReleaseEvidence(NamedTuple): + """Release-shape evidence that still requires repository-owned trust verification.""" + + repository: str + release_tag: str + commit_sha: str + asset_sha256: str + release_state: str + verified_at: datetime + + +class ContractAdmissionEvidence(NamedTuple): + """Admission-shape evidence that still requires repository-owned trust verification.""" + + contract_commit_sha: str + contract_asset_sha256: str + conformance_receipt_sha256: str + bundle_manifest_sha256: str + provenance_attestation_sha256: str + admission_state: str + verified_at: datetime + compatibility_receipt_sha256: str | None = None + migration_receipt_sha256: str | None = None + + +class _CandidateEvidenceSnapshot(NamedTuple): + """Immutable local snapshot of the exact candidate values evaluated for readiness.""" + + projection_key: str + projection_kind: ProjectionKind + source_revision: str + source_repository: str + effective_from: datetime + effective_to: datetime | None + recorded_at: datetime + owner_reference: str + dependency_references: tuple[str, ...] + + +class ProjectionReadiness(NamedTuple): + """Immutable decision describing whether a candidate may be handed to the EA owner.""" + + ready: bool + truth_status: ProjectionTruthStatus + source_repository: str + source_revision: str + candidate_sha256: str + reason: str + next_action: str + contract_commit_sha: str | None + contract_asset_sha256: str | None + contract_conformance_receipt_sha256: str | None + contract_bundle_manifest_sha256: str | None + contract_provenance_attestation_sha256: str | None + contract_compatibility_receipt_sha256: str | None + contract_migration_receipt_sha256: str | None + + +def _require_exact_text(value: object, field_name: str) -> None: + """Reject text subclasses whose comparison behavior can contradict stored evidence.""" + if type(value) is not str: + raise TypeError(f"{field_name} must be exact built-in text") + + +def _require_aware_time( + value: datetime, + field_name: str, + naive_requirement: str | None = None, +) -> None: + """Accept only immutable built-in temporal behavior at the cross-system trust boundary.""" + if type(value) is not datetime: + raise TypeError(f"{field_name} must use exact built-in datetime and timezone") + if value.utcoffset() is None: + requirement = naive_requirement if naive_requirement is not None else field_name + raise ValueError(f"{field_name} must be timezone-aware {requirement}") + if type(value.tzinfo) is not timezone: + raise TypeError(f"{field_name} must use exact built-in datetime and timezone") + + +def _validate_projection_candidate( + candidate: ArchitectureProjectionCandidate | _CandidateEvidenceSnapshot, +) -> None: + """Revalidate retained candidate values before any readiness decision is issued.""" + _require_exact_text(candidate.projection_key, "projection_key") + if _PROJECTION_KEY_RE.fullmatch(candidate.projection_key) is None: + raise ValueError("projection_key must be a deployable architecture key") + if not isinstance(candidate.projection_kind, ProjectionKind): + raise ValueError("projection_kind must be a supported architecture kind") + _require_exact_text(candidate.source_repository, "source_repository") + if candidate.source_repository != _ORGMETRA_REPOSITORY: + raise ValueError("source_repository must be the Orgmetra source repository") + _require_exact_text(candidate.source_revision, "source_revision") + if _SHA_RE.fullmatch(candidate.source_revision) is None: + raise ValueError("source_revision must be a 40-character lowercase source revision") + _require_aware_time(candidate.effective_from, "effective_from") + _require_aware_time(candidate.recorded_at, "recorded_at") + if candidate.effective_to is not None: + _require_aware_time(candidate.effective_to, "effective_to") + if candidate.effective_to <= candidate.effective_from: + raise ValueError("effective_to must be after effective_from") + _require_exact_text(candidate.owner_reference, "owner_reference") + if _OWNER_REFERENCE_RE.fullmatch(candidate.owner_reference) is None: + raise ValueError("owner_reference must be a non-person architecture owner reference") + if type(candidate.dependency_references) is not tuple: + raise ValueError("dependency_references must remain an immutable tuple") + for reference in candidate.dependency_references: + _require_exact_text(reference, "dependency reference") + if _DEPENDENCY_REFERENCE_RE.fullmatch(reference) is None: + raise ValueError( + "dependency_references must contain architecture-only dependency reference values" + ) + + +def _snapshot_projection_candidate( + candidate: ArchitectureProjectionCandidate, +) -> _CandidateEvidenceSnapshot: + """Detach evaluated candidate values from the caller-owned object before handoff evidence.""" + _validate_projection_candidate(candidate) + snapshot = _CandidateEvidenceSnapshot( + projection_key=candidate.projection_key, + projection_kind=candidate.projection_kind, + source_revision=candidate.source_revision, + source_repository=candidate.source_repository, + effective_from=candidate.effective_from, + effective_to=candidate.effective_to, + recorded_at=candidate.recorded_at, + owner_reference=candidate.owner_reference, + dependency_references=candidate.dependency_references, + ) + _validate_projection_candidate(snapshot) + return snapshot + + +def _candidate_sha256(candidate: _CandidateEvidenceSnapshot) -> str: + """Hash the exact allowed architecture evidence so readiness cannot move between candidates.""" + payload = { + "dependency_references": list(candidate.dependency_references), + "effective_from": candidate.effective_from.isoformat(), + "effective_to": candidate.effective_to.isoformat() if candidate.effective_to is not None else None, + "owner_reference": candidate.owner_reference, + "projection_key": candidate.projection_key, + "projection_kind": candidate.projection_kind.value, + "recorded_at": candidate.recorded_at.isoformat(), + "source_repository": candidate.source_repository, + "source_revision": candidate.source_revision, + } + canonical = json.dumps(payload, ensure_ascii=True, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(canonical.encode("utf-8")).hexdigest() + + +def _validate_contract_release(release: ContractReleaseEvidence) -> None: + """Require immutable-looking evidence for the exact foreign contract authority.""" + _require_exact_text(release.repository, "repository") + if release.repository != _CONTRACT_REPOSITORY: + raise ValueError("unexpected contract repository") + _require_exact_text(release.release_tag, "release_tag") + if _RELEASE_TAG_RE.fullmatch(release.release_tag) is None: + raise ValueError("release_tag must be a stable release tag") + _require_exact_text(release.commit_sha, "commit_sha") + if _SHA_RE.fullmatch(release.commit_sha) is None: + raise ValueError("commit_sha must be a 40-character lowercase commit SHA") + _require_exact_text(release.asset_sha256, "asset_sha256") + if _SHA256_RE.fullmatch(release.asset_sha256) is None: + raise ValueError("asset_sha256 must be a 64-character lowercase SHA-256") + _require_exact_text(release.release_state, "release_state") + if release.release_state != "published": + raise ValueError("release_state must identify a published release") + _require_aware_time(release.verified_at, "verified_at", "verification time") + + +def _validate_optional_receipt( + value: str | None, + field_name: str, + receipt_name: str, +) -> None: + """Validate an optional lifecycle receipt when a control plane supplies one.""" + if value is None: + return + _require_exact_text(value, field_name) + if _SHA256_RE.fullmatch(value) is None: + raise ValueError( + f"{field_name} must be a 64-character lowercase {receipt_name} SHA-256" + ) + + +def _validate_contract_admission( + admission: ContractAdmissionEvidence, + release: ContractReleaseEvidence, +) -> None: + """Bind semantic, bundle, provenance, and lifecycle evidence to released bytes.""" + _require_exact_text(admission.contract_commit_sha, "contract_commit_sha") + if _SHA_RE.fullmatch(admission.contract_commit_sha) is None: + raise ValueError("contract_commit_sha must be a 40-character lowercase contract commit SHA") + _require_exact_text(admission.contract_asset_sha256, "contract_asset_sha256") + if _SHA256_RE.fullmatch(admission.contract_asset_sha256) is None: + raise ValueError("contract_asset_sha256 must be a 64-character lowercase contract asset SHA-256") + _require_exact_text(admission.conformance_receipt_sha256, "conformance_receipt_sha256") + if _SHA256_RE.fullmatch(admission.conformance_receipt_sha256) is None: + raise ValueError( + "conformance_receipt_sha256 must be a 64-character lowercase conformance receipt SHA-256" + ) + _require_exact_text(admission.bundle_manifest_sha256, "bundle_manifest_sha256") + if _SHA256_RE.fullmatch(admission.bundle_manifest_sha256) is None: + raise ValueError( + "bundle_manifest_sha256 must be a 64-character lowercase bundle manifest SHA-256" + ) + _require_exact_text( + admission.provenance_attestation_sha256, + "provenance_attestation_sha256", + ) + if _SHA256_RE.fullmatch(admission.provenance_attestation_sha256) is None: + raise ValueError( + "provenance_attestation_sha256 must be a 64-character lowercase provenance attestation SHA-256" + ) + _require_exact_text(admission.admission_state, "admission_state") + if admission.admission_state != "verified": + raise ValueError("admission_state must identify verified contract admission") + _require_aware_time(admission.verified_at, "verified_at", "admission verification time") + if admission.verified_at < release.verified_at: + raise ValueError("admission verification cannot predate release verification") + if admission.contract_commit_sha != release.commit_sha: + raise ValueError("admission commit must match released commit") + if admission.contract_asset_sha256 != release.asset_sha256: + raise ValueError("admission asset must match released asset") + _validate_optional_receipt( + admission.compatibility_receipt_sha256, + "compatibility_receipt_sha256", + "compatibility receipt", + ) + _validate_optional_receipt( + admission.migration_receipt_sha256, + "migration_receipt_sha256", + "migration receipt", + ) + if ( + admission.compatibility_receipt_sha256 is not None + and admission.migration_receipt_sha256 is not None + and admission.compatibility_receipt_sha256 + == admission.migration_receipt_sha256 + ): + raise ValueError( + "compatibility and migration receipts must identify distinct lifecycle evidence" + ) + receipt_digests = [ + admission.conformance_receipt_sha256, + admission.bundle_manifest_sha256, + admission.provenance_attestation_sha256, + ] + if admission.compatibility_receipt_sha256 is not None: + receipt_digests.append(admission.compatibility_receipt_sha256) + if admission.migration_receipt_sha256 is not None: + receipt_digests.append(admission.migration_receipt_sha256) + if len(receipt_digests) != len(set(receipt_digests)): + raise ValueError("contract evidence receipt SHA-256 identities must be distinct") + + +def _blocked_readiness( + candidate: _CandidateEvidenceSnapshot, + candidate_sha256: str, + reason: str, + next_action: str, + release: ContractReleaseEvidence | None = None, + admission: ContractAdmissionEvidence | None = None, +) -> ProjectionReadiness: + """Build one immutable fail-closed decision without dropping validated evidence.""" + return ProjectionReadiness( + ready=False, + truth_status=ProjectionTruthStatus.PROPOSED, + source_repository=candidate.source_repository, + source_revision=candidate.source_revision, + candidate_sha256=candidate_sha256, + reason=reason, + next_action=next_action, + contract_commit_sha=release.commit_sha if release is not None else None, + contract_asset_sha256=release.asset_sha256 if release is not None else None, + contract_conformance_receipt_sha256=( + admission.conformance_receipt_sha256 if admission is not None else None + ), + contract_bundle_manifest_sha256=( + admission.bundle_manifest_sha256 if admission is not None else None + ), + contract_provenance_attestation_sha256=( + admission.provenance_attestation_sha256 if admission is not None else None + ), + contract_compatibility_receipt_sha256=( + admission.compatibility_receipt_sha256 if admission is not None else None + ), + contract_migration_receipt_sha256=( + admission.migration_receipt_sha256 if admission is not None else None + ), + ) + + +def evaluate_projection_readiness( + candidate: ArchitectureProjectionCandidate, + contract_release: ContractReleaseEvidence | None, + contract_admission: ContractAdmissionEvidence | None = None, +) -> ProjectionReadiness: + """Validate caller evidence while keeping handoff closed until trusted owner integration.""" + if type(candidate) is not ArchitectureProjectionCandidate: + raise TypeError("candidate must be an ArchitectureProjectionCandidate") + candidate_snapshot = _snapshot_projection_candidate(candidate) + candidate_sha256 = _candidate_sha256(candidate_snapshot) + if contract_release is None: + return _blocked_readiness( + candidate_snapshot, + candidate_sha256, + reason="context_graph_contract_release_not_admitted", + next_action="install_approved_context_graph_contract_release", + ) + if type(contract_release) is not ContractReleaseEvidence: + raise TypeError("contract_release must be ContractReleaseEvidence or None") + _validate_contract_release(contract_release) + if contract_admission is None: + return _blocked_readiness( + candidate_snapshot, + candidate_sha256, + reason="context_graph_contract_admission_not_verified", + next_action="verify_released_context_graph_contract_admission", + release=contract_release, + ) + if type(contract_admission) is not ContractAdmissionEvidence: + raise TypeError("contract_admission must be ContractAdmissionEvidence or None") + _validate_contract_admission(contract_admission, contract_release) + if ( + contract_admission.compatibility_receipt_sha256 is None + or contract_admission.migration_receipt_sha256 is None + ): + return _blocked_readiness( + candidate_snapshot, + candidate_sha256, + reason="context_graph_contract_lifecycle_evidence_not_verified", + next_action="verify_context_graph_contract_compatibility_and_migration", + release=contract_release, + admission=contract_admission, + ) + return _blocked_readiness( + candidate_snapshot, + candidate_sha256, + reason="trusted_control_plane_evidence_not_available", + next_action="integrate_released_context_graph_trust_contract", + release=contract_release, + admission=contract_admission, + ) diff --git a/packages/enterprise-architecture-projection/tests/test_docstrings.py b/packages/enterprise-architecture-projection/tests/test_docstrings.py new file mode 100644 index 000000000..8fcfb89f1 --- /dev/null +++ b/packages/enterprise-architecture-projection/tests/test_docstrings.py @@ -0,0 +1,31 @@ +"""Executable documentation-completeness contract for the EA projection package.""" + +from __future__ import annotations + +import ast +from pathlib import Path + + +PACKAGE_ROOT = Path(__file__).resolve().parents[1] +SOURCE_ROOT = PACKAGE_ROOT / "src" / "orgmetra_enterprise_architecture_projection" +TEST_ROOT = PACKAGE_ROOT / "tests" + + +def _python_files() -> tuple[Path, ...]: + """Return every owned Python source/test file in deterministic order.""" + return tuple(sorted((*SOURCE_ROOT.glob("*.py"), *TEST_ROOT.glob("*.py")))) + + +def test_owned_python_modules_and_callables_are_documented() -> None: + """Require beginner-readable docstrings on all owned modules/classes/functions.""" + missing: list[str] = [] + for path in _python_files(): + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + if ast.get_docstring(tree, clean=False) is None: + missing.append(f"{path.relative_to(PACKAGE_ROOT)}:") + for node in ast.walk(tree): + if not isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): + continue + if ast.get_docstring(node, clean=False) is None: + missing.append(f"{path.relative_to(PACKAGE_ROOT)}:{node.lineno}:{node.name}") + assert not missing, "Missing owned Python docstrings: " + ", ".join(missing) diff --git a/packages/enterprise-architecture-projection/tests/test_projection_boundary.py b/packages/enterprise-architecture-projection/tests/test_projection_boundary.py new file mode 100644 index 000000000..c94a81dd4 --- /dev/null +++ b/packages/enterprise-architecture-projection/tests/test_projection_boundary.py @@ -0,0 +1,300 @@ +"""Contract tests for Orgmetra's Enterprise Architecture projection admission boundary.""" + +from __future__ import annotations + +from datetime import UTC, datetime +from types import MappingProxyType + +import pytest + +from orgmetra_enterprise_architecture_projection import ( + ArchitectureProjectionCandidate, + ContractAdmissionEvidence, + ContractReleaseEvidence, + ProjectionKind, + ProjectionTruthStatus, + evaluate_projection_readiness, +) + + +ORGMETRA_SHA = "9e3e4847510e1e612b48474ba42b177b8ed824df" +CONTRACT_SHA = "99d230991b9d48fbf87489e0b375b7bbf09d8559" +ARTIFACT_SHA256 = "a" * 64 +CONFORMANCE_SHA256 = "b" * 64 +BUNDLE_SHA256 = "c" * 64 +PROVENANCE_SHA256 = "d" * 64 +COMPATIBILITY_SHA256 = "e" * 64 +MIGRATION_SHA256 = "f" * 64 + + +def _candidate(**overrides: object) -> ArchitectureProjectionCandidate: + """Return a minimal deployable-architecture candidate with optional overrides.""" + values: dict[str, object] = { + "projection_key": "orgmetra.people-api", + "projection_kind": ProjectionKind.APPLICATION, + "source_revision": ORGMETRA_SHA, + "source_repository": "ContextualWisdomLab/Orgmetra", + "effective_from": datetime(2026, 9, 1, tzinfo=UTC), + "effective_to": None, + "recorded_at": datetime(2026, 9, 1, 6, 0, tzinfo=UTC), + "owner_reference": "team:orgmetra", + "dependency_references": ("service:keyverse",), + } + values.update(overrides) + return ArchitectureProjectionCandidate(**values) + + +def _release(**overrides: object) -> ContractReleaseEvidence: + """Return immutable-looking context-graph release evidence with optional overrides.""" + values: dict[str, object] = { + "repository": "ContextualWisdomLab/context-graph-contracts", + "release_tag": "v1.0.0", + "commit_sha": CONTRACT_SHA, + "asset_sha256": ARTIFACT_SHA256, + "release_state": "published", + "verified_at": datetime(2026, 9, 1, 6, 1, tzinfo=UTC), + } + values.update(overrides) + return ContractReleaseEvidence(**values) + + +def _admission(**overrides: object) -> ContractAdmissionEvidence: + """Return trusted-looking conformance, provenance, and lifecycle evidence.""" + values: dict[str, object] = { + "contract_commit_sha": CONTRACT_SHA, + "contract_asset_sha256": ARTIFACT_SHA256, + "conformance_receipt_sha256": CONFORMANCE_SHA256, + "bundle_manifest_sha256": BUNDLE_SHA256, + "provenance_attestation_sha256": PROVENANCE_SHA256, + "admission_state": "verified", + "verified_at": datetime(2026, 9, 1, 6, 2, tzinfo=UTC), + "compatibility_receipt_sha256": COMPATIBILITY_SHA256, + "migration_receipt_sha256": MIGRATION_SHA256, + } + values.update(overrides) + return ContractAdmissionEvidence(**values) + + +def test_unreleased_contract_blocks_projection_with_next_action() -> None: + """Do not serialize or publish EA data before a reviewed immutable contract exists.""" + decision = evaluate_projection_readiness(_candidate(), None) + + assert decision.ready is False + assert decision.truth_status is ProjectionTruthStatus.PROPOSED + assert decision.reason == "context_graph_contract_release_not_admitted" + assert decision.next_action == "install_approved_context_graph_contract_release" + assert decision.contract_commit_sha is None + assert decision.contract_asset_sha256 is None + assert decision.contract_conformance_receipt_sha256 is None + assert decision.contract_bundle_manifest_sha256 is None + assert decision.contract_provenance_attestation_sha256 is None + assert decision.contract_compatibility_receipt_sha256 is None + assert decision.contract_migration_receipt_sha256 is None + + +def test_release_metadata_without_conformance_and_provenance_stays_blocked() -> None: + """Do not treat a tag and artifact digest as semantic admission or provenance.""" + decision = evaluate_projection_readiness(_candidate(), _release()) + + assert decision.ready is False + assert decision.truth_status is ProjectionTruthStatus.PROPOSED + assert decision.reason == "context_graph_contract_admission_not_verified" + assert decision.next_action == "verify_released_context_graph_contract_admission" + assert decision.contract_commit_sha == CONTRACT_SHA + assert decision.contract_asset_sha256 == ARTIFACT_SHA256 + assert decision.contract_conformance_receipt_sha256 is None + assert decision.contract_bundle_manifest_sha256 is None + assert decision.contract_provenance_attestation_sha256 is None + assert decision.contract_compatibility_receipt_sha256 is None + assert decision.contract_migration_receipt_sha256 is None + + +def test_caller_constructed_complete_evidence_cannot_authorize_handoff() -> None: + """Keep shape-complete caller evidence blocked until a repository-owned trust adapter exists.""" + decision = evaluate_projection_readiness(_candidate(), _release(), _admission()) + + assert decision.ready is False + assert decision.truth_status is ProjectionTruthStatus.PROPOSED + assert decision.reason == "trusted_control_plane_evidence_not_available" + assert decision.next_action == "integrate_released_context_graph_trust_contract" + assert decision.contract_commit_sha == CONTRACT_SHA + assert decision.contract_asset_sha256 == ARTIFACT_SHA256 + assert decision.contract_conformance_receipt_sha256 == CONFORMANCE_SHA256 + assert decision.contract_bundle_manifest_sha256 == BUNDLE_SHA256 + assert decision.contract_provenance_attestation_sha256 == PROVENANCE_SHA256 + assert decision.contract_compatibility_receipt_sha256 == COMPATIBILITY_SHA256 + assert decision.contract_migration_receipt_sha256 == MIGRATION_SHA256 + + +@pytest.mark.parametrize( + ("field", "value", "message"), + [ + ("repository", "ContextualWisdomLab/Orgmetra", "unexpected contract repository"), + ("release_tag", "main", "stable release tag"), + ("commit_sha", "deadbeef", "40-character lowercase commit SHA"), + ("asset_sha256", "abc", "64-character lowercase SHA-256"), + ("release_state", "draft", "published release"), + ("verified_at", datetime(2026, 9, 1), "timezone-aware verification time"), + ], +) +def test_untrusted_contract_release_evidence_is_rejected( + field: str, value: object, message: str +) -> None: + """Reject mutable, unpublished, malformed, or foreign contract-release evidence.""" + release = _release(**{field: value}) + + with pytest.raises(ValueError, match=message): + evaluate_projection_readiness(_candidate(), release) + + +@pytest.mark.parametrize( + ("field", "value", "message"), + [ + ("contract_commit_sha", "deadbeef", "40-character lowercase contract commit SHA"), + ("contract_asset_sha256", "abc", "64-character lowercase contract asset SHA-256"), + ("conformance_receipt_sha256", "abc", "64-character lowercase conformance receipt SHA-256"), + ("bundle_manifest_sha256", "abc", "64-character lowercase bundle manifest SHA-256"), + ( + "provenance_attestation_sha256", + "abc", + "64-character lowercase provenance attestation SHA-256", + ), + ("admission_state", "pending", "verified contract admission"), + ("verified_at", datetime(2026, 9, 1), "timezone-aware admission verification time"), + ], +) +def test_malformed_contract_admission_evidence_is_rejected( + field: str, value: object, message: str +) -> None: + """Reject incomplete or malformed semantic-admission and provenance evidence.""" + admission = _admission(**{field: value}) + + with pytest.raises(ValueError, match=message): + evaluate_projection_readiness(_candidate(), _release(), admission) + + +@pytest.mark.parametrize( + ("field", "value", "message"), + [ + ("contract_commit_sha", "e" * 40, "admission commit must match released commit"), + ("contract_asset_sha256", "e" * 64, "admission asset must match released asset"), + ], +) +def test_contract_admission_must_bind_the_exact_release( + field: str, value: object, message: str +) -> None: + """Reject valid-looking admission evidence for different released bytes.""" + admission = _admission(**{field: value}) + + with pytest.raises(ValueError, match=message): + evaluate_projection_readiness(_candidate(), _release(), admission) + + +@pytest.mark.parametrize( + ("field", "value", "message"), + [ + ("projection_key", "person:1234", "deployable architecture key"), + ("projection_kind", "person", "supported architecture kind"), + ( + "source_repository", + "ContextualWisdomLab/enterprise-architecture-core", + "Orgmetra source repository", + ), + ("source_revision", "not-a-sha", "40-character lowercase source revision"), + ("effective_from", datetime(2026, 9, 1), "timezone-aware effective_from"), + ( + "recorded_at", + datetime(2026, 9, 1), + "timezone-aware recorded_at", + ), + ( + "effective_to", + datetime(2026, 8, 31, tzinfo=UTC), + "effective_to must be after effective_from", + ), + ("owner_reference", "person:employee-123", "non-person architecture owner reference"), + ("owner_reference", "team:", "non-person architecture owner reference"), + ( + "dependency_references", + ("employment:abc",), + "architecture-only dependency reference", + ), + ( + "dependency_references", + ("service:",), + "architecture-only dependency reference", + ), + ], +) +def test_hr_record_or_invalid_temporal_data_cannot_cross_into_ea_projection( + field: str, value: object, message: str +) -> None: + """Keep authoritative HR facts and malformed bitemporal evidence out of EA.""" + with pytest.raises(ValueError, match=message): + _candidate(**{field: value}) + + +def test_open_ended_or_future_ended_effective_intervals_are_accepted() -> None: + """Accept an ordered effective interval while keeping recorded time separate.""" + candidate = _candidate(effective_to=datetime(2026, 9, 2, tzinfo=UTC)) + + assert candidate.effective_to == datetime(2026, 9, 2, tzinfo=UTC) + + +def test_candidate_collections_are_immutable_after_validation() -> None: + """Prevent callers from mutating dependency evidence after admission checks.""" + candidate = _candidate(dependency_references=["service:keyverse", "service:naruon"]) + + assert candidate.dependency_references == ("service:keyverse", "service:naruon") + with pytest.raises(AttributeError): + candidate.dependency_references.append("person:employee-1") # type: ignore[attr-defined] + + +def test_unvalidated_candidate_object_cannot_bypass_constructor_checks() -> None: + """Reject arbitrary objects even when contract-release evidence itself is valid.""" + with pytest.raises(TypeError, match="ArchitectureProjectionCandidate"): + evaluate_projection_readiness(object(), _release()) # type: ignore[arg-type] + + +def test_unvalidated_release_object_cannot_bypass_release_evidence_type() -> None: + """Reject duck-typed release objects before reading their caller-controlled fields.""" + with pytest.raises(TypeError, match="ContractReleaseEvidence"): + evaluate_projection_readiness(_candidate(), object()) # type: ignore[arg-type] + + +def test_unvalidated_admission_object_cannot_bypass_admission_evidence_type() -> None: + """Reject duck-typed semantic admission evidence at the trust boundary.""" + with pytest.raises(TypeError, match="ContractAdmissionEvidence"): + evaluate_projection_readiness(_candidate(), _release(), object()) # type: ignore[arg-type] + + +def test_projection_decision_exposes_no_free_form_hr_payload() -> None: + """Keep the adapter incapable of carrying person/employment/job decision payloads.""" + decision = evaluate_projection_readiness(_candidate(), _release(), _admission()) + + assert not hasattr(decision, "payload") + assert not hasattr(decision, "person") + assert not hasattr(decision, "employment") + assert not hasattr(decision, "job") + assert MappingProxyType({}) == {} + + +def test_projection_kind_covers_declared_ea_decision_plane_scope() -> None: + """Keep executable projection concepts aligned with the declared EA handoff contract.""" + required_kinds = { + "application", + "service", + "api", + "worker", + "database", + "runtime", + "provider", + "technology_version", + "lifecycle", + "risk", + "ownership", + "remediation", + "transformation", + } + + assert required_kinds <= {kind.value for kind in ProjectionKind} diff --git a/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py b/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py new file mode 100644 index 000000000..54a9d6854 --- /dev/null +++ b/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py @@ -0,0 +1,171 @@ +"""Compatibility and migration admission regressions for EA projections.""" + +from __future__ import annotations + +from datetime import UTC, datetime + +import pytest + +from orgmetra_enterprise_architecture_projection import ( + ArchitectureProjectionCandidate, + ContractAdmissionEvidence, + ContractReleaseEvidence, + ProjectionKind, + evaluate_projection_readiness, +) + + +_ORGMETRA_SHA = "9e3e4847510e1e612b48474ba42b177b8ed824df" +_CONTRACT_SHA = "99d230991b9d48fbf87489e0b375b7bbf09d8559" +_ARTIFACT_SHA256 = "a" * 64 +_COMPATIBILITY_SHA256 = "e" * 64 +_MIGRATION_SHA256 = "f" * 64 + + +def _candidate() -> ArchitectureProjectionCandidate: + """Return a valid deployable-architecture candidate.""" + return ArchitectureProjectionCandidate( + projection_key="orgmetra.people-api", + projection_kind=ProjectionKind.APPLICATION, + source_revision=_ORGMETRA_SHA, + source_repository="ContextualWisdomLab/Orgmetra", + effective_from=datetime(2026, 9, 1, tzinfo=UTC), + effective_to=None, + recorded_at=datetime(2026, 9, 1, 6, 0, tzinfo=UTC), + owner_reference="team:orgmetra", + dependency_references=("service:keyverse",), + ) + + +def _release() -> ContractReleaseEvidence: + """Return exact immutable release identity for admission tests.""" + return ContractReleaseEvidence( + repository="ContextualWisdomLab/context-graph-contracts", + release_tag="v1.0.0", + commit_sha=_CONTRACT_SHA, + asset_sha256=_ARTIFACT_SHA256, + release_state="published", + verified_at=datetime(2026, 9, 1, 6, 1, tzinfo=UTC), + ) + + +def _legacy_admission() -> ContractAdmissionEvidence: + """Return semantic admission that lacks lifecycle compatibility evidence.""" + return ContractAdmissionEvidence( + contract_commit_sha=_CONTRACT_SHA, + contract_asset_sha256=_ARTIFACT_SHA256, + conformance_receipt_sha256="b" * 64, + bundle_manifest_sha256="c" * 64, + provenance_attestation_sha256="d" * 64, + admission_state="verified", + verified_at=datetime(2026, 9, 1, 6, 2, tzinfo=UTC), + ) + + +def _admission(**overrides: object) -> ContractAdmissionEvidence: + """Return release-bound evidence including compatibility and migration receipts.""" + values: dict[str, object] = { + "contract_commit_sha": _CONTRACT_SHA, + "contract_asset_sha256": _ARTIFACT_SHA256, + "conformance_receipt_sha256": "b" * 64, + "bundle_manifest_sha256": "c" * 64, + "provenance_attestation_sha256": "d" * 64, + "admission_state": "verified", + "verified_at": datetime(2026, 9, 1, 6, 2, tzinfo=UTC), + "compatibility_receipt_sha256": _COMPATIBILITY_SHA256, + "migration_receipt_sha256": _MIGRATION_SHA256, + } + values.update(overrides) + return ContractAdmissionEvidence(**values) + + +def test_semantic_admission_without_lifecycle_receipts_remains_blocked() -> None: + """Do not mistake conformance and provenance for consumer compatibility.""" + decision = evaluate_projection_readiness( + _candidate(), + _release(), + _legacy_admission(), + ) + + assert decision.ready is False + assert decision.reason == "context_graph_contract_lifecycle_evidence_not_verified" + assert decision.next_action == "verify_context_graph_contract_compatibility_and_migration" + assert decision.contract_compatibility_receipt_sha256 is None + assert decision.contract_migration_receipt_sha256 is None + + +@pytest.mark.parametrize( + ("field", "retained_compatibility", "retained_migration"), + [ + ("compatibility_receipt_sha256", None, _MIGRATION_SHA256), + ("migration_receipt_sha256", _COMPATIBILITY_SHA256, None), + ], +) +def test_each_missing_lifecycle_receipt_blocks_projection_independently( + field: str, + retained_compatibility: str | None, + retained_migration: str | None, +) -> None: + """Require both lifecycle receipts instead of accepting whichever one exists.""" + decision = evaluate_projection_readiness( + _candidate(), + _release(), + _admission(**{field: None}), + ) + + assert decision.ready is False + assert decision.reason == "context_graph_contract_lifecycle_evidence_not_verified" + assert decision.next_action == "verify_context_graph_contract_compatibility_and_migration" + assert decision.contract_compatibility_receipt_sha256 == retained_compatibility + assert decision.contract_migration_receipt_sha256 == retained_migration + + +def test_one_lifecycle_receipt_cannot_stand_in_for_both_results() -> None: + """Reject duplicated receipt identity across compatibility and migration evidence.""" + with pytest.raises(ValueError, match="distinct lifecycle evidence"): + evaluate_projection_readiness( + _candidate(), + _release(), + _admission(migration_receipt_sha256=_COMPATIBILITY_SHA256), + ) + + +def test_admission_verification_cannot_predate_release_verification() -> None: + """Reject lifecycle evidence that was allegedly verified before release identity.""" + with pytest.raises(ValueError, match="cannot predate release verification"): + evaluate_projection_readiness( + _candidate(), + _release(), + _admission(verified_at=datetime(2026, 9, 1, 6, 0, tzinfo=UTC)), + ) + + +def test_shape_complete_projection_retains_receipts_but_stays_blocked() -> None: + """Retain lifecycle evidence without authorizing caller-created external evidence.""" + decision = evaluate_projection_readiness(_candidate(), _release(), _admission()) + + assert decision.ready is False + assert decision.reason == "trusted_control_plane_evidence_not_available" + assert decision.next_action == "integrate_released_context_graph_trust_contract" + assert decision.contract_compatibility_receipt_sha256 == _COMPATIBILITY_SHA256 + assert decision.contract_migration_receipt_sha256 == _MIGRATION_SHA256 + + +@pytest.mark.parametrize( + ("field", "message"), + [ + ("compatibility_receipt_sha256", "compatibility receipt SHA-256"), + ("migration_receipt_sha256", "migration receipt SHA-256"), + ], +) +def test_malformed_compatibility_or_migration_receipt_is_rejected( + field: str, + message: str, +) -> None: + """Reject release admission whose lifecycle evidence is not digest-bound.""" + with pytest.raises(ValueError, match=message): + evaluate_projection_readiness( + _candidate(), + _release(), + _admission(**{field: "not-a-sha256"}), + ) diff --git a/packages/enterprise-architecture-projection/tests/test_projection_evidence_identity_separation.py b/packages/enterprise-architecture-projection/tests/test_projection_evidence_identity_separation.py new file mode 100644 index 000000000..f2593a08c --- /dev/null +++ b/packages/enterprise-architecture-projection/tests/test_projection_evidence_identity_separation.py @@ -0,0 +1,97 @@ +"""Regression tests for semantic separation of EA admission evidence identities.""" + +from __future__ import annotations + +from datetime import UTC, datetime + +import pytest + +from orgmetra_enterprise_architecture_projection import ( + ArchitectureProjectionCandidate, + ContractAdmissionEvidence, + ContractReleaseEvidence, + ProjectionKind, + evaluate_projection_readiness, +) + + +ORGMETRA_SHA = "9e3e4847510e1e612b48474ba42b177b8ed824df" +CONTRACT_SHA = "99d230991b9d48fbf87489e0b375b7bbf09d8559" +ARTIFACT_SHA256 = "a" * 64 +CONFORMANCE_SHA256 = "b" * 64 +BUNDLE_SHA256 = "c" * 64 +PROVENANCE_SHA256 = "d" * 64 +COMPATIBILITY_SHA256 = "e" * 64 +MIGRATION_SHA256 = "f" * 64 + + +def _candidate() -> ArchitectureProjectionCandidate: + """Return one deployable architecture candidate with no HR record payload.""" + return ArchitectureProjectionCandidate( + projection_key="orgmetra.people-api", + projection_kind=ProjectionKind.APPLICATION, + source_revision=ORGMETRA_SHA, + source_repository="ContextualWisdomLab/Orgmetra", + effective_from=datetime(2026, 9, 1, tzinfo=UTC), + effective_to=None, + recorded_at=datetime(2026, 9, 1, 6, 0, tzinfo=UTC), + owner_reference="team:orgmetra", + dependency_references=("service:keyverse",), + ) + + +def _release() -> ContractReleaseEvidence: + """Return one immutable published Context Graph release identity.""" + return ContractReleaseEvidence( + repository="ContextualWisdomLab/context-graph-contracts", + release_tag="v1.0.0", + commit_sha=CONTRACT_SHA, + asset_sha256=ARTIFACT_SHA256, + release_state="published", + verified_at=datetime(2026, 9, 1, 6, 1, tzinfo=UTC), + ) + + +def _admission(**overrides: object) -> ContractAdmissionEvidence: + """Return distinct semantic receipt identities unless a test overrides one.""" + values: dict[str, object] = { + "contract_commit_sha": CONTRACT_SHA, + "contract_asset_sha256": ARTIFACT_SHA256, + "conformance_receipt_sha256": CONFORMANCE_SHA256, + "bundle_manifest_sha256": BUNDLE_SHA256, + "provenance_attestation_sha256": PROVENANCE_SHA256, + "admission_state": "verified", + "verified_at": datetime(2026, 9, 1, 6, 2, tzinfo=UTC), + "compatibility_receipt_sha256": COMPATIBILITY_SHA256, + "migration_receipt_sha256": MIGRATION_SHA256, + } + values.update(overrides) + return ContractAdmissionEvidence(**values) + + +@pytest.mark.parametrize( + ("field", "reused_digest"), + [ + ("bundle_manifest_sha256", CONFORMANCE_SHA256), + ("provenance_attestation_sha256", CONFORMANCE_SHA256), + ("compatibility_receipt_sha256", CONFORMANCE_SHA256), + ("migration_receipt_sha256", PROVENANCE_SHA256), + ], +) +def test_semantically_distinct_receipts_cannot_reuse_one_artifact_identity( + field: str, reused_digest: str +) -> None: + """Do not let one evidence artifact satisfy multiple independent controls.""" + admission = _admission(**{field: reused_digest}) + + with pytest.raises(ValueError, match="evidence receipt SHA-256 identities must be distinct"): + evaluate_projection_readiness(_candidate(), _release(), admission) + + +def test_distinct_receipt_identities_validate_without_authorizing_handoff() -> None: + """Keep well-formed independent receipts blocked until trusted control-plane integration.""" + decision = evaluate_projection_readiness(_candidate(), _release(), _admission()) + + assert decision.ready is False + assert decision.reason == "trusted_control_plane_evidence_not_available" + assert decision.next_action == "integrate_released_context_graph_trust_contract" diff --git a/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py b/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py new file mode 100644 index 000000000..aeb38b5ac --- /dev/null +++ b/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py @@ -0,0 +1,220 @@ +"""Runtime-integrity regressions for Enterprise Architecture projection admission.""" + +from __future__ import annotations + +from datetime import UTC, datetime, timedelta, tzinfo + +import pytest + +from orgmetra_enterprise_architecture_projection import ( + ArchitectureProjectionCandidate, + ContractAdmissionEvidence, + ContractReleaseEvidence, + ProjectionKind, + evaluate_projection_readiness, +) + + +_ORGMETRA_SHA = "9e3e4847510e1e612b48474ba42b177b8ed824df" +_CONTRACT_SHA = "99d230991b9d48fbf87489e0b375b7bbf09d8559" + + +class _AlwaysEqualText(str): + """Adversarial text whose comparison behavior lies about its stored value.""" + + def __eq__(self, other: object) -> bool: + """Claim equality with every comparison target.""" + return True + + def __ne__(self, other: object) -> bool: + """Claim inequality with no comparison target.""" + return False + + +class _MutableTimezone(tzinfo): + """Adversarial timezone whose offset can change after evidence construction.""" + + def __init__(self) -> None: + """Start by impersonating UTC.""" + self.offset = timedelta(0) + + def utcoffset(self, value: datetime | None) -> timedelta: + """Return caller-controlled offset state.""" + return self.offset + + def dst(self, value: datetime | None) -> timedelta: + """Return no daylight-saving adjustment.""" + return timedelta(0) + + +class _BehaviorBearingDateTime(datetime): + """Adversarial datetime that can override temporal comparison behavior.""" + + def utcoffset(self) -> timedelta: + """Claim a stable zero offset independent of retained timezone data.""" + return timedelta(0) + + +def _candidate() -> ArchitectureProjectionCandidate: + """Return one valid projection candidate for runtime-tampering tests.""" + return ArchitectureProjectionCandidate( + projection_key="orgmetra.people-api", + projection_kind=ProjectionKind.APPLICATION, + source_revision=_ORGMETRA_SHA, + source_repository="ContextualWisdomLab/Orgmetra", + effective_from=datetime(2026, 9, 1, tzinfo=UTC), + effective_to=None, + recorded_at=datetime(2026, 9, 1, 6, 0, tzinfo=UTC), + owner_reference="team:orgmetra", + dependency_references=("service:keyverse",), + ) + + +def _release() -> ContractReleaseEvidence: + """Return valid release-shape evidence for candidate-integrity tests.""" + return ContractReleaseEvidence( + repository="ContextualWisdomLab/context-graph-contracts", + release_tag="v1.0.0", + commit_sha=_CONTRACT_SHA, + asset_sha256="a" * 64, + release_state="published", + verified_at=datetime(2026, 9, 1, 6, 1, tzinfo=UTC), + ) + + +def _admission() -> ContractAdmissionEvidence: + """Return valid admission-shape evidence for candidate-integrity tests.""" + return ContractAdmissionEvidence( + contract_commit_sha=_CONTRACT_SHA, + contract_asset_sha256="a" * 64, + conformance_receipt_sha256="b" * 64, + bundle_manifest_sha256="c" * 64, + provenance_attestation_sha256="d" * 64, + admission_state="verified", + verified_at=datetime(2026, 9, 1, 6, 2, tzinfo=UTC), + compatibility_receipt_sha256="e" * 64, + migration_receipt_sha256="f" * 64, + ) + + +def test_mutated_projection_candidate_is_revalidated_before_readiness() -> None: + """Reject an exact candidate whose frozen fields were rewritten after construction.""" + candidate = _candidate() + object.__setattr__(candidate, "owner_reference", "person:employee-123") + + with pytest.raises(ValueError, match="non-person architecture owner reference"): + evaluate_projection_readiness(candidate, _release(), _admission()) + + +def test_mutated_candidate_collection_cannot_reintroduce_mutable_evidence() -> None: + """Reject dependency evidence rewritten to a mutable collection after construction.""" + candidate = _candidate() + object.__setattr__(candidate, "dependency_references", ["service:keyverse"]) + + with pytest.raises(ValueError, match="immutable tuple"): + evaluate_projection_readiness(candidate, _release(), _admission()) + + +def test_comparison_overriding_source_repository_cannot_claim_orgmetra_authority() -> None: + """Reject behavior-bearing text that can lie about the candidate source repository.""" + candidate = _candidate() + object.__setattr__( + candidate, + "source_repository", + _AlwaysEqualText("ContextualWisdomLab/not-orgmetra"), + ) + + with pytest.raises(TypeError, match="source_repository must be exact built-in text"): + evaluate_projection_readiness(candidate, _release(), _admission()) + + +def test_comparison_overriding_admission_commit_cannot_bind_different_release() -> None: + """Reject behavior-bearing digest text before exact release/admission equality checks.""" + admission = ContractAdmissionEvidence( + contract_commit_sha=_AlwaysEqualText("f" * 40), + contract_asset_sha256="a" * 64, + conformance_receipt_sha256="b" * 64, + bundle_manifest_sha256="c" * 64, + provenance_attestation_sha256="d" * 64, + admission_state="verified", + verified_at=datetime(2026, 9, 1, 6, 2, tzinfo=UTC), + compatibility_receipt_sha256="e" * 64, + migration_receipt_sha256="f" * 64, + ) + + with pytest.raises(TypeError, match="contract_commit_sha must be exact built-in text"): + evaluate_projection_readiness(_candidate(), _release(), admission) + + +def test_behavior_bearing_candidate_timezone_cannot_enter_projection_evidence() -> None: + """Reject mutable timezone behavior before it becomes retained bitemporal evidence.""" + mutable_timezone = _MutableTimezone() + + with pytest.raises(TypeError, match="effective_from must use exact built-in datetime and timezone"): + ArchitectureProjectionCandidate( + projection_key="orgmetra.people-api", + projection_kind=ProjectionKind.APPLICATION, + source_revision=_ORGMETRA_SHA, + source_repository="ContextualWisdomLab/Orgmetra", + effective_from=datetime(2026, 9, 1, tzinfo=mutable_timezone), + effective_to=None, + recorded_at=datetime(2026, 9, 1, 6, 0, tzinfo=UTC), + owner_reference="team:orgmetra", + dependency_references=("service:keyverse",), + ) + + +def test_behavior_bearing_release_timezone_cannot_rewrite_verification_evidence() -> None: + """Reject mutable timezone behavior before release verification evidence is trusted.""" + release = ContractReleaseEvidence( + repository="ContextualWisdomLab/context-graph-contracts", + release_tag="v1.0.0", + commit_sha=_CONTRACT_SHA, + asset_sha256="a" * 64, + release_state="published", + verified_at=datetime(2026, 9, 1, 6, 1, tzinfo=_MutableTimezone()), + ) + + with pytest.raises(TypeError, match="verified_at must use exact built-in datetime and timezone"): + evaluate_projection_readiness(_candidate(), release, _admission()) + + +def test_behavior_bearing_datetime_subclass_cannot_rewrite_recorded_time() -> None: + """Reject datetime subclasses before overridden temporal behavior enters readiness.""" + candidate = _candidate() + object.__setattr__( + candidate, + "recorded_at", + _BehaviorBearingDateTime(2026, 9, 1, 6, 0, tzinfo=UTC), + ) + + with pytest.raises(TypeError, match="recorded_at must use exact built-in datetime and timezone"): + evaluate_projection_readiness(candidate, _release(), _admission()) + + +def test_release_evidence_cannot_be_rewritten_after_construction() -> None: + """Release identity must remain immutable after it enters the admission boundary.""" + release = _release() + + with pytest.raises(AttributeError): + object.__setattr__(release, "commit_sha", "f" * 40) + assert release.commit_sha == _CONTRACT_SHA + + +def test_admission_evidence_cannot_be_rewritten_after_construction() -> None: + """Conformance and provenance receipts must remain immutable after construction.""" + admission = _admission() + + with pytest.raises(AttributeError): + object.__setattr__(admission, "conformance_receipt_sha256", "e" * 64) + assert admission.conformance_receipt_sha256 == "b" * 64 + + +def test_fail_closed_readiness_cannot_be_rewritten_after_evaluation() -> None: + """A caller must not be able to turn a blocked handoff into a ready decision in place.""" + decision = evaluate_projection_readiness(_candidate(), None) + + assert decision.ready is False + with pytest.raises(AttributeError): + object.__setattr__(decision, "ready", True) + assert decision.ready is False diff --git a/packages/enterprise-architecture-projection/tests/test_projection_source_binding.py b/packages/enterprise-architecture-projection/tests/test_projection_source_binding.py new file mode 100644 index 000000000..7c2ce495b --- /dev/null +++ b/packages/enterprise-architecture-projection/tests/test_projection_source_binding.py @@ -0,0 +1,91 @@ +"""Regression tests for source-revision and candidate provenance in EA projection handoffs.""" + +from datetime import UTC, datetime + +from orgmetra_enterprise_architecture_projection import ( + ArchitectureProjectionCandidate, + ContractAdmissionEvidence, + ContractReleaseEvidence, + ProjectionKind, + evaluate_projection_readiness, +) + + +ORGMETRA_REPOSITORY = "ContextualWisdomLab/Orgmetra" +ORGMETRA_SHA = "9e3e4847510e1e612b48474ba42b177b8ed824df" +CONTRACT_SHA = "99d230991b9d48fbf87489e0b375b7bbf09d8559" +ARTIFACT_SHA256 = "a" * 64 + + +def _candidate(**overrides: object) -> ArchitectureProjectionCandidate: + """Build one valid architecture candidate with optional evidence overrides.""" + values: dict[str, object] = { + "projection_key": "orgmetra.people-api", + "projection_kind": ProjectionKind.APPLICATION, + "source_revision": ORGMETRA_SHA, + "source_repository": ORGMETRA_REPOSITORY, + "effective_from": datetime(2026, 9, 1, tzinfo=UTC), + "effective_to": None, + "recorded_at": datetime(2026, 9, 1, 6, 0, tzinfo=UTC), + "owner_reference": "team:orgmetra", + "dependency_references": ("service:keyverse",), + } + values.update(overrides) + return ArchitectureProjectionCandidate(**values) + + +def _release() -> ContractReleaseEvidence: + """Build release evidence for one immutable context-graph artifact.""" + return ContractReleaseEvidence( + repository="ContextualWisdomLab/context-graph-contracts", + release_tag="v1.0.0", + commit_sha=CONTRACT_SHA, + asset_sha256=ARTIFACT_SHA256, + release_state="published", + verified_at=datetime(2026, 9, 1, 6, 1, tzinfo=UTC), + ) + + +def _admission() -> ContractAdmissionEvidence: + """Build admission evidence bound to exact released and lifecycle evidence.""" + return ContractAdmissionEvidence( + contract_commit_sha=CONTRACT_SHA, + contract_asset_sha256=ARTIFACT_SHA256, + conformance_receipt_sha256="b" * 64, + bundle_manifest_sha256="c" * 64, + provenance_attestation_sha256="d" * 64, + admission_state="verified", + verified_at=datetime(2026, 9, 1, 6, 2, tzinfo=UTC), + compatibility_receipt_sha256="e" * 64, + migration_receipt_sha256="f" * 64, + ) + + +def test_blocked_handoff_preserves_exact_orgmetra_source_identity() -> None: + """A blocked handoff must remain attributable to the exact Orgmetra source revision.""" + decision = evaluate_projection_readiness(_candidate(), _release(), _admission()) + + assert decision.ready is False + assert decision.reason == "trusted_control_plane_evidence_not_available" + assert decision.source_repository == ORGMETRA_REPOSITORY + assert decision.source_revision == ORGMETRA_SHA + + +def test_blocked_decision_is_bound_to_exact_candidate_evidence_not_only_source_revision() -> None: + """Two candidates from one source revision must never share transferable decision evidence.""" + first = evaluate_projection_readiness(_candidate(), _release(), _admission()) + second = evaluate_projection_readiness( + _candidate( + projection_key="orgmetra.people-api-secondary", + owner_reference="application:orgmetra-people-api", + dependency_references=("service:keyverse", "service:naruon"), + ), + _release(), + _admission(), + ) + + assert first.ready is False + assert second.ready is False + assert first.reason == second.reason == "trusted_control_plane_evidence_not_available" + assert first.source_revision == second.source_revision == ORGMETRA_SHA + assert first.candidate_sha256 != second.candidate_sha256 diff --git a/packages/enterprise-architecture-projection/tests/test_workflow_runner_contract.py b/packages/enterprise-architecture-projection/tests/test_workflow_runner_contract.py new file mode 100644 index 000000000..b153f0a70 --- /dev/null +++ b/packages/enterprise-architecture-projection/tests/test_workflow_runner_contract.py @@ -0,0 +1,19 @@ +"""Protect the EA projection workflow from floating hosted-runner aliases.""" + +from pathlib import Path + + +WORKFLOW_PATH = ( + Path(__file__).resolve().parents[3] + / ".github" + / "workflows" + / "enterprise-architecture-projection-quality.yml" +) + + +def test_projection_quality_uses_explicit_supported_runner_image() -> None: + """Keep this PR's new workflow off the floating hosted-runner alias.""" + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + + assert "runs-on: ubuntu-latest" not in workflow + assert "runs-on: ubuntu-24.04" in workflow