From 5af5eb4dee8953cf5dd8d031ad21905944e0f26d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 15:09:08 +0900 Subject: [PATCH 01/58] test(ea-projection): define fail-closed contract admission boundary --- ...rprise-architecture-projection-quality.yml | 56 +++++++ .../pyproject.toml | 24 +++ .../tests/test_docstrings.py | 31 ++++ .../tests/test_projection_boundary.py | 150 ++++++++++++++++++ 4 files changed, 261 insertions(+) create mode 100644 .github/workflows/enterprise-architecture-projection-quality.yml create mode 100644 packages/enterprise-architecture-projection/pyproject.toml create mode 100644 packages/enterprise-architecture-projection/tests/test_docstrings.py create mode 100644 packages/enterprise-architecture-projection/tests/test_projection_boundary.py diff --git a/.github/workflows/enterprise-architecture-projection-quality.yml b/.github/workflows/enterprise-architecture-projection-quality.yml new file mode 100644 index 000000000..46559182b --- /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-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 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/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/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..6472adffe --- /dev/null +++ b/packages/enterprise-architecture-projection/tests/test_projection_boundary.py @@ -0,0 +1,150 @@ +"""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, + ContractReleaseEvidence, + ProjectionKind, + ProjectionTruthStatus, + evaluate_projection_readiness, +) + + +ORGMETRA_SHA = "9e3e4847510e1e612b48474ba42b177b8ed824df" +CONTRACT_SHA = "99d230991b9d48fbf87489e0b375b7bbf09d8559" +ARTIFACT_SHA256 = "a" * 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 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 + + +def test_published_contract_evidence_admits_only_a_candidate_not_ea_truth() -> None: + """Admit a candidate after release pinning without granting EA authoritative status.""" + decision = evaluate_projection_readiness(_candidate(), _release()) + + assert decision.ready is True + assert decision.truth_status is ProjectionTruthStatus.PROPOSED + assert decision.reason == "projection_candidate_ready" + assert decision.next_action == "submit_candidate_to_enterprise_architecture_owner" + assert decision.contract_commit_sha == CONTRACT_SHA + assert decision.contract_asset_sha256 == ARTIFACT_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"), + [ + ("projection_key", "person:1234", "deployable architecture key"), + ("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"), + ( + "dependency_references", + ("employment:abc",), + "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_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_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()) + + assert not hasattr(decision, "payload") + assert not hasattr(decision, "person") + assert not hasattr(decision, "employment") + assert not hasattr(decision, "job") + assert MappingProxyType({}) == {} From 37bbaa16b7c7dcdc1a5301601bb032c3ecfc9366 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 15:11:11 +0900 Subject: [PATCH 02/58] test(ea-projection): cover kind and temporal interval branches --- .../tests/test_projection_boundary.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/enterprise-architecture-projection/tests/test_projection_boundary.py b/packages/enterprise-architecture-projection/tests/test_projection_boundary.py index 6472adffe..bf753aac1 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_boundary.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_boundary.py @@ -101,6 +101,7 @@ def test_untrusted_contract_release_evidence_is_rejected( ("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"), @@ -130,6 +131,13 @@ def test_hr_record_or_invalid_temporal_data_cannot_cross_into_ea_projection( _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"]) From ae09329e68437308ab3c6184ed7e2ec178603a55 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 15:13:46 +0900 Subject: [PATCH 03/58] feat(ea-projection): enforce released-contract admission boundary --- .../README.md | 19 ++ .../__init__.py | 164 ++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 packages/enterprise-architecture-projection/README.md create mode 100644 packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py diff --git a/packages/enterprise-architecture-projection/README.md b/packages/enterprise-architecture-projection/README.md new file mode 100644 index 000000000..c69cef082 --- /dev/null +++ b/packages/enterprise-architecture-projection/README.md @@ -0,0 +1,19 @@ +# Enterprise Architecture projection admission + +This package is the Orgmetra-side admission 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 + +A candidate is blocked until a trusted control-plane lookup has observed an immutable published `ContextualWisdomLab/context-graph-contracts` release and supplied its stable release tag, exact commit SHA, artifact SHA-256, publication state, and timezone-aware observation time. Shape validation in this package is not a substitute for that trusted lookup and must not be populated from untrusted request data. + +When no approved contract release is available, `evaluate_projection_readiness` returns a fail-closed decision whose next action is `install_approved_context_graph_contract_release`. + +When admissible release evidence is present, the result is still only a `proposed` candidate. Its next action is `submit_candidate_to_enterprise_architecture_owner`. Acceptance, replacement, lifecycle decisions, and authoritative EA truth remain with the Enterprise Architecture owner. + +## Candidate scope + +Permitted projection concepts are deployable architecture concerns such as applications, interfaces, technology components and versions, providers, lifecycle, supported capabilities, initiatives, transformations, and dependencies. Projection evidence preserves Orgmetra source revision, effective time, recorded time, non-person ownership references, and architecture-only dependency references. + +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/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..3c0afe0d6 --- /dev/null +++ b/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py @@ -0,0 +1,164 @@ +"""Fail-closed admission boundary for Orgmetra Enterprise Architecture projections. + +This package validates Orgmetra-owned architecture projection candidates and the +release evidence required before they can be handed to the Enterprise +Architecture owner. It does not serialize the foreign context-graph contract, +write Enterprise Architecture state, or transport authoritative HR records. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from enum import StrEnum +import re + + +_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_PREFIXES = ("team:", "organization:", "application:") +_DEPENDENCY_PREFIXES = ( + "application:", + "service:", + "interface:", + "technology:", + "provider:", + "capability:", +) + + +class ProjectionKind(StrEnum): + """Architecture concepts Orgmetra may describe without becoming their authority.""" + + APPLICATION = "application" + INTERFACE = "interface" + TECHNOLOGY_COMPONENT = "technology_component" + TECHNOLOGY_VERSION = "technology_version" + PROVIDER = "provider" + LIFECYCLE = "lifecycle" + 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.""" + if _PROJECTION_KEY_RE.fullmatch(self.projection_key) is None: + raise ValueError("projection_key must be a deployable architecture key") + if not isinstance(self.projection_kind, ProjectionKind): + raise ValueError("projection_kind must be a supported architecture kind") + if self.source_repository != _ORGMETRA_REPOSITORY: + raise ValueError("source_repository must be the Orgmetra source repository") + if _SHA_RE.fullmatch(self.source_revision) is None: + raise ValueError("source_revision must be a 40-character lowercase source revision") + _require_aware_time(self.effective_from, "effective_from") + _require_aware_time(self.recorded_at, "recorded_at") + if self.effective_to is not None: + _require_aware_time(self.effective_to, "effective_to") + if self.effective_to <= self.effective_from: + raise ValueError("effective_to must be after effective_from") + if not self.owner_reference.startswith(_OWNER_PREFIXES): + raise ValueError("owner_reference must be a non-person architecture owner reference") + dependencies = tuple(self.dependency_references) + if any(not reference.startswith(_DEPENDENCY_PREFIXES) for reference in dependencies): + raise ValueError( + "dependency_references must contain architecture-only dependency reference values" + ) + object.__setattr__(self, "dependency_references", dependencies) + + +@dataclass(frozen=True, slots=True) +class ContractReleaseEvidence: + """Trusted-control-plane observation of a published context-graph release.""" + + repository: str + release_tag: str + commit_sha: str + asset_sha256: str + release_state: str + verified_at: datetime + + +@dataclass(frozen=True, slots=True) +class ProjectionReadiness: + """Decision describing whether a candidate may be handed to the EA owner.""" + + ready: bool + truth_status: ProjectionTruthStatus + reason: str + next_action: str + contract_commit_sha: str | None + contract_asset_sha256: str | None + + +def _require_aware_time(value: datetime, field_name: str) -> None: + """Reject naive timestamps because projection evidence is compared across systems.""" + if value.utcoffset() is None: + raise ValueError(f"{field_name} must be timezone-aware {field_name}") + + +def _validate_contract_release(release: ContractReleaseEvidence) -> None: + """Require immutable-looking evidence for the exact foreign contract authority.""" + if release.repository != _CONTRACT_REPOSITORY: + raise ValueError("unexpected contract repository") + if _RELEASE_TAG_RE.fullmatch(release.release_tag) is None: + raise ValueError("release_tag must be a stable release tag") + if _SHA_RE.fullmatch(release.commit_sha) is None: + raise ValueError("commit_sha must be a 40-character lowercase commit SHA") + if _SHA256_RE.fullmatch(release.asset_sha256) is None: + raise ValueError("asset_sha256 must be a 64-character lowercase SHA-256") + if release.release_state != "published": + raise ValueError("release_state must identify a published release") + if release.verified_at.utcoffset() is None: + raise ValueError("verified_at must be a timezone-aware verification time") + + +def evaluate_projection_readiness( + candidate: ArchitectureProjectionCandidate, + contract_release: ContractReleaseEvidence | None, +) -> ProjectionReadiness: + """Return fail-closed readiness without conferring Enterprise Architecture truth.""" + del candidate + if contract_release is None: + return ProjectionReadiness( + ready=False, + truth_status=ProjectionTruthStatus.PROPOSED, + reason="context_graph_contract_release_not_admitted", + next_action="install_approved_context_graph_contract_release", + contract_commit_sha=None, + contract_asset_sha256=None, + ) + _validate_contract_release(contract_release) + return ProjectionReadiness( + ready=True, + truth_status=ProjectionTruthStatus.PROPOSED, + reason="projection_candidate_ready", + next_action="submit_candidate_to_enterprise_architecture_owner", + contract_commit_sha=contract_release.commit_sha, + contract_asset_sha256=contract_release.asset_sha256, + ) From ad4a6382b5175ecda8b5aaaf48702c49ede613e2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 15:15:00 +0900 Subject: [PATCH 04/58] test(ea-projection): reject type and empty-reference bypasses --- .../tests/test_projection_boundary.py | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/enterprise-architecture-projection/tests/test_projection_boundary.py b/packages/enterprise-architecture-projection/tests/test_projection_boundary.py index bf753aac1..10cc84555 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_boundary.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_boundary.py @@ -102,7 +102,11 @@ def test_untrusted_contract_release_evidence_is_rejected( [ ("projection_key", "person:1234", "deployable architecture key"), ("projection_kind", "person", "supported architecture kind"), - ("source_repository", "ContextualWisdomLab/enterprise-architecture-core", "Orgmetra source repository"), + ( + "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"), ( @@ -116,11 +120,17 @@ def test_untrusted_contract_release_evidence_is_rejected( "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( @@ -147,6 +157,18 @@ def test_candidate_collections_are_immutable_after_validation() -> None: 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_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()) From c3e4a51324a9930da10941821138d7f63652882b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 15:16:03 +0900 Subject: [PATCH 05/58] fix(ea-projection): reject unvalidated boundary objects --- .../__init__.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) 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 index 3c0afe0d6..8f1be8604 100644 --- a/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py +++ b/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py @@ -20,14 +20,9 @@ _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_PREFIXES = ("team:", "organization:", "application:") -_DEPENDENCY_PREFIXES = ( - "application:", - "service:", - "interface:", - "technology:", - "provider:", - "capability:", +_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._/-]*$" ) @@ -82,10 +77,13 @@ def __post_init__(self) -> None: _require_aware_time(self.effective_to, "effective_to") if self.effective_to <= self.effective_from: raise ValueError("effective_to must be after effective_from") - if not self.owner_reference.startswith(_OWNER_PREFIXES): + if _OWNER_REFERENCE_RE.fullmatch(self.owner_reference) is None: raise ValueError("owner_reference must be a non-person architecture owner reference") dependencies = tuple(self.dependency_references) - if any(not reference.startswith(_DEPENDENCY_PREFIXES) for reference in dependencies): + if any( + _DEPENDENCY_REFERENCE_RE.fullmatch(reference) is None + for reference in dependencies + ): raise ValueError( "dependency_references must contain architecture-only dependency reference values" ) @@ -143,7 +141,8 @@ def evaluate_projection_readiness( contract_release: ContractReleaseEvidence | None, ) -> ProjectionReadiness: """Return fail-closed readiness without conferring Enterprise Architecture truth.""" - del candidate + if type(candidate) is not ArchitectureProjectionCandidate: + raise TypeError("candidate must be an ArchitectureProjectionCandidate") if contract_release is None: return ProjectionReadiness( ready=False, @@ -153,6 +152,8 @@ def evaluate_projection_readiness( contract_commit_sha=None, contract_asset_sha256=None, ) + if type(contract_release) is not ContractReleaseEvidence: + raise TypeError("contract_release must be ContractReleaseEvidence or None") _validate_contract_release(contract_release) return ProjectionReadiness( ready=True, From 4910251906ccb2239655e831655b979423a0f0ec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 16:07:17 +0900 Subject: [PATCH 06/58] test: require context contract admission provenance --- .../tests/test_projection_boundary.py | 95 ++++++++++++++++++- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/packages/enterprise-architecture-projection/tests/test_projection_boundary.py b/packages/enterprise-architecture-projection/tests/test_projection_boundary.py index 10cc84555..9572bc98f 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_boundary.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_boundary.py @@ -9,6 +9,7 @@ from orgmetra_enterprise_architecture_projection import ( ArchitectureProjectionCandidate, + ContractAdmissionEvidence, ContractReleaseEvidence, ProjectionKind, ProjectionTruthStatus, @@ -19,6 +20,9 @@ ORGMETRA_SHA = "9e3e4847510e1e612b48474ba42b177b8ed824df" CONTRACT_SHA = "99d230991b9d48fbf87489e0b375b7bbf09d8559" ARTIFACT_SHA256 = "a" * 64 +CONFORMANCE_SHA256 = "b" * 64 +BUNDLE_SHA256 = "c" * 64 +PROVENANCE_SHA256 = "d" * 64 def _candidate(**overrides: object) -> ArchitectureProjectionCandidate: @@ -52,6 +56,21 @@ def _release(**overrides: object) -> ContractReleaseEvidence: return ContractReleaseEvidence(**values) +def _admission(**overrides: object) -> ContractAdmissionEvidence: + """Return trusted conformance and provenance evidence with optional overrides.""" + 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), + } + 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) @@ -62,18 +81,39 @@ def test_unreleased_contract_blocks_projection_with_next_action() -> None: 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 -def test_published_contract_evidence_admits_only_a_candidate_not_ea_truth() -> None: - """Admit a candidate after release pinning without granting EA authoritative status.""" +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 + + +def test_verified_contract_admission_allows_only_a_candidate_not_ea_truth() -> None: + """Admit only evidence bound to conformance, bundle identity, and provenance.""" + decision = evaluate_projection_readiness(_candidate(), _release(), _admission()) + assert decision.ready is True assert decision.truth_status is ProjectionTruthStatus.PROPOSED assert decision.reason == "projection_candidate_ready" assert decision.next_action == "submit_candidate_to_enterprise_architecture_owner" 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 @pytest.mark.parametrize( @@ -97,6 +137,49 @@ def test_untrusted_contract_release_evidence_is_rejected( 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"), [ @@ -169,9 +252,15 @@ def test_unvalidated_release_object_cannot_bypass_release_evidence_type() -> Non 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()) + decision = evaluate_projection_readiness(_candidate(), _release(), _admission()) assert not hasattr(decision, "payload") assert not hasattr(decision, "person") From e5594c0045927217c6281819320a172f3d599f15 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 16:07:54 +0900 Subject: [PATCH 07/58] fix: fail closed without context contract admission --- .../__init__.py | 76 ++++++++++++++++++- 1 file changed, 73 insertions(+), 3 deletions(-) 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 index 8f1be8604..fa5f2902f 100644 --- a/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py +++ b/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py @@ -1,9 +1,10 @@ """Fail-closed admission boundary for Orgmetra Enterprise Architecture projections. This package validates Orgmetra-owned architecture projection candidates and the -release evidence required before they can be handed to the Enterprise -Architecture owner. It does not serialize the foreign context-graph contract, -write Enterprise Architecture state, or transport authoritative HR records. +release, semantic-conformance, bundle-identity, and provenance evidence required +before they can be handed to the Enterprise Architecture owner. It does not +serialize the foreign context-graph contract, write Enterprise Architecture +state, or transport authoritative HR records. """ from __future__ import annotations @@ -102,6 +103,19 @@ class ContractReleaseEvidence: verified_at: datetime +@dataclass(frozen=True, slots=True) +class ContractAdmissionEvidence: + """Trusted evidence that exact released contract bytes passed semantic admission.""" + + 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 + + @dataclass(frozen=True, slots=True) class ProjectionReadiness: """Decision describing whether a candidate may be handed to the EA owner.""" @@ -112,6 +126,9 @@ class ProjectionReadiness: 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 def _require_aware_time(value: datetime, field_name: str) -> None: @@ -136,9 +153,41 @@ def _validate_contract_release(release: ContractReleaseEvidence) -> None: raise ValueError("verified_at must be a timezone-aware verification time") +def _validate_contract_admission( + admission: ContractAdmissionEvidence, + release: ContractReleaseEvidence, +) -> None: + """Bind semantic, bundle, and provenance evidence to the exact released bytes.""" + if _SHA_RE.fullmatch(admission.contract_commit_sha) is None: + raise ValueError("contract_commit_sha must be a 40-character lowercase contract commit SHA") + 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") + 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" + ) + 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" + ) + 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" + ) + if admission.admission_state != "verified": + raise ValueError("admission_state must identify verified contract admission") + if admission.verified_at.utcoffset() is None: + raise ValueError("verified_at must be a timezone-aware admission verification time") + 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") + + def evaluate_projection_readiness( candidate: ArchitectureProjectionCandidate, contract_release: ContractReleaseEvidence | None, + contract_admission: ContractAdmissionEvidence | None = None, ) -> ProjectionReadiness: """Return fail-closed readiness without conferring Enterprise Architecture truth.""" if type(candidate) is not ArchitectureProjectionCandidate: @@ -151,10 +200,28 @@ def evaluate_projection_readiness( next_action="install_approved_context_graph_contract_release", contract_commit_sha=None, contract_asset_sha256=None, + contract_conformance_receipt_sha256=None, + contract_bundle_manifest_sha256=None, + contract_provenance_attestation_sha256=None, ) 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 ProjectionReadiness( + ready=False, + truth_status=ProjectionTruthStatus.PROPOSED, + reason="context_graph_contract_admission_not_verified", + next_action="verify_released_context_graph_contract_admission", + contract_commit_sha=contract_release.commit_sha, + contract_asset_sha256=contract_release.asset_sha256, + contract_conformance_receipt_sha256=None, + contract_bundle_manifest_sha256=None, + contract_provenance_attestation_sha256=None, + ) + if type(contract_admission) is not ContractAdmissionEvidence: + raise TypeError("contract_admission must be ContractAdmissionEvidence or None") + _validate_contract_admission(contract_admission, contract_release) return ProjectionReadiness( ready=True, truth_status=ProjectionTruthStatus.PROPOSED, @@ -162,4 +229,7 @@ def evaluate_projection_readiness( next_action="submit_candidate_to_enterprise_architecture_owner", contract_commit_sha=contract_release.commit_sha, contract_asset_sha256=contract_release.asset_sha256, + contract_conformance_receipt_sha256=contract_admission.conformance_receipt_sha256, + contract_bundle_manifest_sha256=contract_admission.bundle_manifest_sha256, + contract_provenance_attestation_sha256=contract_admission.provenance_attestation_sha256, ) From d7afb7b54842f91d8778e8c80d5d00a9dbbd158f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 16:08:08 +0900 Subject: [PATCH 08/58] docs: distinguish release identity from contract admission --- packages/enterprise-architecture-projection/README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/enterprise-architecture-projection/README.md b/packages/enterprise-architecture-projection/README.md index c69cef082..444e1d47a 100644 --- a/packages/enterprise-architecture-projection/README.md +++ b/packages/enterprise-architecture-projection/README.md @@ -6,14 +6,16 @@ It does **not** publish directly to Enterprise Architecture Core, reproduce `con ## Current operating state -A candidate is blocked until a trusted control-plane lookup has observed an immutable published `ContextualWisdomLab/context-graph-contracts` release and supplied its stable release tag, exact commit SHA, artifact SHA-256, publication state, and timezone-aware observation time. Shape validation in this package is not a substitute for that trusted lookup and must not be populated from untrusted request data. +A candidate is blocked until a trusted control-plane lookup has observed an immutable published `ContextualWisdomLab/context-graph-contracts` release and supplied its stable release tag, exact commit SHA, artifact SHA-256, publication state, and timezone-aware verification time. Shape validation in this package is not proof that the external release exists and must not be populated from untrusted request data. -When no approved contract release is available, `evaluate_projection_readiness` returns a fail-closed decision whose 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 trusted evidence for executable semantic conformance, the complete published contract bundle, and artifact provenance. Until those admission and provenance receipts are independently verified, `evaluate_projection_readiness` remains fail-closed with the next action `verify_released_context_graph_contract_admission`. -When admissible release evidence is present, the result is still only a `proposed` candidate. Its next action is `submit_candidate_to_enterprise_architecture_owner`. Acceptance, replacement, lifecycle decisions, and authoritative EA truth remain with the Enterprise Architecture owner. +The package does not guess or reproduce an unreleased Context Assertion, CloudEvent, API schema, profile, admission receipt, or provenance format. The trusted control plane must obtain those semantics from the immutable released `context-graph-contracts` package and retain their exact evidence identities. When `context-graph-contracts` has no immutable release, the earlier gate remains `install_approved_context_graph_contract_release`. + +Only after release identity and the release-bound conformance, complete-bundle, and provenance evidence are all admitted can Orgmetra produce a `proposed` candidate whose next action is `submit_candidate_to_enterprise_architecture_owner`. Acceptance, replacement, lifecycle decisions, and authoritative EA truth remain with the Enterprise Architecture owner. ## Candidate scope -Permitted projection concepts are deployable architecture concerns such as applications, interfaces, technology components and versions, providers, lifecycle, supported capabilities, initiatives, transformations, and dependencies. Projection evidence preserves Orgmetra source revision, effective time, recorded time, non-person ownership references, and architecture-only dependency references. +Permitted projection concepts are deployable architecture concerns such as applications, interfaces, technology components and versions, providers, lifecycle, 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 contract-admission evidence identities used for the handoff. 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. From 0841367ea3efddcd1c637414a3c00a1b055067a3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 17:02:34 +0900 Subject: [PATCH 09/58] test: reject mutated EA projection candidates --- .../test_projection_runtime_integrity.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py 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..3d6e0e1b1 --- /dev/null +++ b/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py @@ -0,0 +1,56 @@ +"""Runtime-integrity regressions for Enterprise Architecture projection admission.""" + +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" + + +def test_mutated_projection_candidate_is_revalidated_before_readiness() -> None: + """Reject an exact candidate whose frozen fields were rewritten after construction.""" + candidate = 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",), + ) + object.__setattr__(candidate, "owner_reference", "person:employee-123") + + 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=UTC), + ) + admission = 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), + ) + + with pytest.raises(ValueError, match="non-person architecture owner reference"): + evaluate_projection_readiness(candidate, release, admission) From 96f8ab82fb6f7d6022fa4b3f0777a62f20c9e884 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 17:03:08 +0900 Subject: [PATCH 10/58] test: require immutable candidate collections at readiness --- .../test_projection_runtime_integrity.py | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py b/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py index 3d6e0e1b1..45672318c 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py @@ -19,9 +19,9 @@ _CONTRACT_SHA = "99d230991b9d48fbf87489e0b375b7bbf09d8559" -def test_mutated_projection_candidate_is_revalidated_before_readiness() -> None: - """Reject an exact candidate whose frozen fields were rewritten after construction.""" - candidate = ArchitectureProjectionCandidate( +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, @@ -32,9 +32,11 @@ def test_mutated_projection_candidate_is_revalidated_before_readiness() -> None: owner_reference="team:orgmetra", dependency_references=("service:keyverse",), ) - object.__setattr__(candidate, "owner_reference", "person:employee-123") - release = ContractReleaseEvidence( + +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, @@ -42,7 +44,11 @@ def test_mutated_projection_candidate_is_revalidated_before_readiness() -> None: release_state="published", verified_at=datetime(2026, 9, 1, 6, 1, tzinfo=UTC), ) - admission = ContractAdmissionEvidence( + + +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, @@ -52,5 +58,20 @@ def test_mutated_projection_candidate_is_revalidated_before_readiness() -> None: verified_at=datetime(2026, 9, 1, 6, 2, tzinfo=UTC), ) + +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) + 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()) From 9963c6a75cfca68d9388ea573b689a3958e02a76 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 17:03:40 +0900 Subject: [PATCH 11/58] fix: revalidate EA projection candidates at admission --- .../__init__.py | 57 +++++++++++-------- 1 file changed, 32 insertions(+), 25 deletions(-) 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 index fa5f2902f..fcaba2cff 100644 --- a/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py +++ b/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py @@ -64,31 +64,8 @@ class ArchitectureProjectionCandidate: def __post_init__(self) -> None: """Normalize immutable collections and reject HR facts or malformed evidence.""" - if _PROJECTION_KEY_RE.fullmatch(self.projection_key) is None: - raise ValueError("projection_key must be a deployable architecture key") - if not isinstance(self.projection_kind, ProjectionKind): - raise ValueError("projection_kind must be a supported architecture kind") - if self.source_repository != _ORGMETRA_REPOSITORY: - raise ValueError("source_repository must be the Orgmetra source repository") - if _SHA_RE.fullmatch(self.source_revision) is None: - raise ValueError("source_revision must be a 40-character lowercase source revision") - _require_aware_time(self.effective_from, "effective_from") - _require_aware_time(self.recorded_at, "recorded_at") - if self.effective_to is not None: - _require_aware_time(self.effective_to, "effective_to") - if self.effective_to <= self.effective_from: - raise ValueError("effective_to must be after effective_from") - if _OWNER_REFERENCE_RE.fullmatch(self.owner_reference) is None: - raise ValueError("owner_reference must be a non-person architecture owner reference") - dependencies = tuple(self.dependency_references) - if any( - _DEPENDENCY_REFERENCE_RE.fullmatch(reference) is None - for reference in dependencies - ): - raise ValueError( - "dependency_references must contain architecture-only dependency reference values" - ) - object.__setattr__(self, "dependency_references", dependencies) + object.__setattr__(self, "dependency_references", tuple(self.dependency_references)) + _validate_projection_candidate(self) @dataclass(frozen=True, slots=True) @@ -137,6 +114,35 @@ def _require_aware_time(value: datetime, field_name: str) -> None: raise ValueError(f"{field_name} must be timezone-aware {field_name}") +def _validate_projection_candidate(candidate: ArchitectureProjectionCandidate) -> None: + """Revalidate the exact retained candidate before any readiness decision is issued.""" + 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") + if candidate.source_repository != _ORGMETRA_REPOSITORY: + raise ValueError("source_repository must be the Orgmetra source repository") + 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") + 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") + if any( + _DEPENDENCY_REFERENCE_RE.fullmatch(reference) is None + for reference in candidate.dependency_references + ): + raise ValueError( + "dependency_references must contain architecture-only dependency reference values" + ) + + def _validate_contract_release(release: ContractReleaseEvidence) -> None: """Require immutable-looking evidence for the exact foreign contract authority.""" if release.repository != _CONTRACT_REPOSITORY: @@ -192,6 +198,7 @@ def evaluate_projection_readiness( """Return fail-closed readiness without conferring Enterprise Architecture truth.""" if type(candidate) is not ArchitectureProjectionCandidate: raise TypeError("candidate must be an ArchitectureProjectionCandidate") + _validate_projection_candidate(candidate) if contract_release is None: return ProjectionReadiness( ready=False, From 0b5b0e0e2cd3c38cf4290007fae506ba52937ee5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 17:04:36 +0900 Subject: [PATCH 12/58] docs: record EA candidate runtime-integrity gate --- packages/enterprise-architecture-projection/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/enterprise-architecture-projection/README.md b/packages/enterprise-architecture-projection/README.md index 444e1d47a..e5256b2ae 100644 --- a/packages/enterprise-architecture-projection/README.md +++ b/packages/enterprise-architecture-projection/README.md @@ -18,4 +18,6 @@ Only after release identity and the release-bound conformance, complete-bundle, Permitted projection concepts are deployable architecture concerns such as applications, interfaces, technology components and versions, providers, lifecycle, 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 contract-admission evidence identities used for the handoff. +Candidate construction is not the only integrity boundary. `evaluate_projection_readiness` revalidates the exact retained candidate immediately before issuing a readiness decision so 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 otherwise bypass constructor checks after validation. + 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. From 7ff7007d6d4373d2da38e5e2e6f650d3d9196a1b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 18:01:08 +0900 Subject: [PATCH 13/58] test(ea-projection): reject comparison-overriding trust text --- .../test_projection_runtime_integrity.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py b/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py index 45672318c..dd9ef4ebd 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py @@ -19,6 +19,18 @@ _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 + + def _candidate() -> ArchitectureProjectionCandidate: """Return one valid projection candidate for runtime-tampering tests.""" return ArchitectureProjectionCandidate( @@ -75,3 +87,25 @@ def test_mutated_candidate_collection_cannot_reintroduce_mutable_evidence() -> N 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 = _admission() + object.__setattr__(admission, "contract_commit_sha", _AlwaysEqualText("f" * 40)) + + with pytest.raises(TypeError, match="contract_commit_sha must be exact built-in text"): + evaluate_projection_readiness(_candidate(), _release(), admission) From 636f9e88c3bdaae932f14e46d232d00379f931df Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 18:01:46 +0900 Subject: [PATCH 14/58] fix(ea-projection): require exact trust-bearing text --- .../__init__.py | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) 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 index fcaba2cff..604e2c3c0 100644 --- a/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py +++ b/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py @@ -108,6 +108,12 @@ class ProjectionReadiness: contract_provenance_attestation_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) -> None: """Reject naive timestamps because projection evidence is compared across systems.""" if value.utcoffset() is None: @@ -116,12 +122,15 @@ def _require_aware_time(value: datetime, field_name: str) -> None: def _validate_projection_candidate(candidate: ArchitectureProjectionCandidate) -> None: """Revalidate the exact retained candidate 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") @@ -130,29 +139,34 @@ def _validate_projection_candidate(candidate: ArchitectureProjectionCandidate) - _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") - if any( - _DEPENDENCY_REFERENCE_RE.fullmatch(reference) is None - for reference in candidate.dependency_references - ): - raise ValueError( - "dependency_references must contain architecture-only dependency reference values" - ) + 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 _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") if release.verified_at.utcoffset() is None: @@ -164,22 +178,31 @@ def _validate_contract_admission( release: ContractReleaseEvidence, ) -> None: """Bind semantic, bundle, and provenance evidence to the exact 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") if admission.verified_at.utcoffset() is None: From 3f46384f2ae4a1975249af8bbd4c77ed74b81d89 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 19:05:13 +0900 Subject: [PATCH 15/58] test(ea-projection): reject behavior-bearing time providers --- .../test_projection_runtime_integrity.py | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py b/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py index dd9ef4ebd..753379000 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import UTC, datetime +from datetime import UTC, datetime, timedelta, tzinfo import pytest @@ -31,6 +31,22 @@ def __ne__(self, other: object) -> bool: 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) + + def _candidate() -> ArchitectureProjectionCandidate: """Return one valid projection candidate for runtime-tampering tests.""" return ArchitectureProjectionCandidate( @@ -109,3 +125,34 @@ def test_comparison_overriding_admission_commit_cannot_bind_different_release() 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.""" + 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=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 = _release() + object.__setattr__( + release, + "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()) From 26141a3b48cdbf07d5836c21355596752d6845d0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 19:06:14 +0900 Subject: [PATCH 16/58] test(ea-projection): reject behavior-bearing datetime subclasses --- .../test_projection_runtime_integrity.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py b/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py index 753379000..e66772e96 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py @@ -47,6 +47,14 @@ def dst(self, value: datetime | None) -> timedelta: 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( @@ -156,3 +164,16 @@ def test_behavior_bearing_release_timezone_cannot_rewrite_verification_evidence( 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()) From 08b9ba0715a9baf0c9dbc3fb53f5513f2d462e75 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 19:06:48 +0900 Subject: [PATCH 17/58] fix(ea-projection): freeze temporal runtime types at trust boundary --- .../__init__.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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 index 604e2c3c0..138d6e959 100644 --- a/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py +++ b/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py @@ -10,7 +10,7 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, timezone from enum import StrEnum import re @@ -115,9 +115,13 @@ def _require_exact_text(value: object, field_name: str) -> None: def _require_aware_time(value: datetime, field_name: str) -> None: - """Reject naive timestamps because projection evidence is compared across systems.""" + """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: raise ValueError(f"{field_name} must be timezone-aware {field_name}") + 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) -> None: @@ -169,8 +173,7 @@ def _validate_contract_release(release: ContractReleaseEvidence) -> None: _require_exact_text(release.release_state, "release_state") if release.release_state != "published": raise ValueError("release_state must identify a published release") - if release.verified_at.utcoffset() is None: - raise ValueError("verified_at must be a timezone-aware verification time") + _require_aware_time(release.verified_at, "verified_at") def _validate_contract_admission( @@ -205,8 +208,7 @@ def _validate_contract_admission( _require_exact_text(admission.admission_state, "admission_state") if admission.admission_state != "verified": raise ValueError("admission_state must identify verified contract admission") - if admission.verified_at.utcoffset() is None: - raise ValueError("verified_at must be a timezone-aware admission verification time") + _require_aware_time(admission.verified_at, "verified_at") if admission.contract_commit_sha != release.commit_sha: raise ValueError("admission commit must match released commit") if admission.contract_asset_sha256 != release.asset_sha256: From 233d7e102994557d3937e7ffa43841192cad10b0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 19:08:00 +0900 Subject: [PATCH 18/58] docs(ea-projection): document immutable temporal runtime boundary --- packages/enterprise-architecture-projection/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/enterprise-architecture-projection/README.md b/packages/enterprise-architecture-projection/README.md index e5256b2ae..47c9ff9ee 100644 --- a/packages/enterprise-architecture-projection/README.md +++ b/packages/enterprise-architecture-projection/README.md @@ -20,4 +20,6 @@ Permitted projection concepts are deployable architecture concerns such as appli Candidate construction is not the only integrity boundary. `evaluate_projection_readiness` revalidates the exact retained candidate immediately before issuing a readiness decision so 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 otherwise bypass constructor checks after validation. +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. From 2f2b128cf5af6d1c4dab7ab34ef301776b6612a3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 19:10:22 +0900 Subject: [PATCH 19/58] fix(ea-projection): preserve temporal validation contract messages --- .../__init__.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) 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 index 138d6e959..d1b116320 100644 --- a/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py +++ b/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py @@ -114,12 +114,17 @@ def _require_exact_text(value: object, field_name: str) -> None: raise TypeError(f"{field_name} must be exact built-in text") -def _require_aware_time(value: datetime, field_name: str) -> None: +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: - raise ValueError(f"{field_name} must be timezone-aware {field_name}") + 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") @@ -173,7 +178,7 @@ def _validate_contract_release(release: ContractReleaseEvidence) -> None: _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") + _require_aware_time(release.verified_at, "verified_at", "verification time") def _validate_contract_admission( @@ -208,7 +213,7 @@ def _validate_contract_admission( _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") + _require_aware_time(admission.verified_at, "verified_at", "admission verification time") if admission.contract_commit_sha != release.commit_sha: raise ValueError("admission commit must match released commit") if admission.contract_asset_sha256 != release.asset_sha256: From 8a3cc07c570f9bf8e3f50c50d44fc533d6218f73 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 20:06:46 +0900 Subject: [PATCH 20/58] test(ea-projection): bind readiness to Orgmetra source revision --- .../tests/test_projection_source_binding.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 packages/enterprise-architecture-projection/tests/test_projection_source_binding.py 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..b0536b7e2 --- /dev/null +++ b/packages/enterprise-architecture-projection/tests/test_projection_source_binding.py @@ -0,0 +1,55 @@ +"""Regression tests for source-revision 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 test_ready_handoff_preserves_exact_orgmetra_source_identity() -> None: + """A ready handoff must remain attributable to the exact Orgmetra source revision.""" + candidate = ArchitectureProjectionCandidate( + 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",), + ) + release = 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), + ) + admission = 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), + ) + + decision = evaluate_projection_readiness(candidate, release, admission) + + assert decision.ready is True + assert decision.source_repository == ORGMETRA_REPOSITORY + assert decision.source_revision == ORGMETRA_SHA From 23142a70b6171b9f0f36ce282217556eb4b659a6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 20:07:35 +0900 Subject: [PATCH 21/58] fix(ea-projection): preserve source identity in readiness evidence --- .../__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) 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 index d1b116320..822b5adc6 100644 --- a/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py +++ b/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py @@ -99,6 +99,8 @@ class ProjectionReadiness: ready: bool truth_status: ProjectionTruthStatus + source_repository: str + source_revision: str reason: str next_action: str contract_commit_sha: str | None @@ -233,6 +235,8 @@ def evaluate_projection_readiness( return ProjectionReadiness( ready=False, truth_status=ProjectionTruthStatus.PROPOSED, + source_repository=candidate.source_repository, + source_revision=candidate.source_revision, reason="context_graph_contract_release_not_admitted", next_action="install_approved_context_graph_contract_release", contract_commit_sha=None, @@ -248,6 +252,8 @@ def evaluate_projection_readiness( return ProjectionReadiness( ready=False, truth_status=ProjectionTruthStatus.PROPOSED, + source_repository=candidate.source_repository, + source_revision=candidate.source_revision, reason="context_graph_contract_admission_not_verified", next_action="verify_released_context_graph_contract_admission", contract_commit_sha=contract_release.commit_sha, @@ -262,6 +268,8 @@ def evaluate_projection_readiness( return ProjectionReadiness( ready=True, truth_status=ProjectionTruthStatus.PROPOSED, + source_repository=candidate.source_repository, + source_revision=candidate.source_revision, reason="projection_candidate_ready", next_action="submit_candidate_to_enterprise_architecture_owner", contract_commit_sha=contract_release.commit_sha, From 699acb8fce40dd9b7b32026bfa22861ca1666810 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 20:07:58 +0900 Subject: [PATCH 22/58] docs(ea-projection): bind readiness to source revision --- packages/enterprise-architecture-projection/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/enterprise-architecture-projection/README.md b/packages/enterprise-architecture-projection/README.md index 47c9ff9ee..f1796d5a0 100644 --- a/packages/enterprise-architecture-projection/README.md +++ b/packages/enterprise-architecture-projection/README.md @@ -16,7 +16,7 @@ Only after release identity and the release-bound conformance, complete-bundle, ## Candidate scope -Permitted projection concepts are deployable architecture concerns such as applications, interfaces, technology components and versions, providers, lifecycle, 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 contract-admission evidence identities used for the handoff. +Permitted projection concepts are deployable architecture concerns such as applications, interfaces, technology components and versions, providers, lifecycle, 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 contract-admission evidence identities used for the handoff. Every readiness result also carries the exact validated Orgmetra source repository and source revision, including fail-closed results, so downstream evidence cannot be detached from the candidate version that was evaluated. Candidate construction is not the only integrity boundary. `evaluate_projection_readiness` revalidates the exact retained candidate immediately before issuing a readiness decision so 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 otherwise bypass constructor checks after validation. From 29a0985296eb3bdffe870b4931ef15aa6cbdc9d0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 20:11:20 +0900 Subject: [PATCH 23/58] test(ea-projection): prevent readiness mutation after admission --- .../tests/test_projection_runtime_integrity.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py b/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py index e66772e96..5f8f0f182 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py @@ -177,3 +177,13 @@ def test_behavior_bearing_datetime_subclass_cannot_rewrite_recorded_time() -> No with pytest.raises(TypeError, match="recorded_at must use exact built-in datetime and timezone"): evaluate_projection_readiness(candidate, _release(), _admission()) + + +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 From 3521de53f8efb9bbd567abc7f315e3639f2c7c31 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 20:13:02 +0900 Subject: [PATCH 24/58] fix(ea-projection): make readiness decision immutable --- .../orgmetra_enterprise_architecture_projection/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index 822b5adc6..a4465d927 100644 --- a/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py +++ b/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py @@ -13,6 +13,7 @@ from datetime import datetime, timezone from enum import StrEnum import re +from typing import NamedTuple _ORGMETRA_REPOSITORY = "ContextualWisdomLab/Orgmetra" @@ -93,9 +94,8 @@ class ContractAdmissionEvidence: verified_at: datetime -@dataclass(frozen=True, slots=True) -class ProjectionReadiness: - """Decision describing whether a candidate may be handed to the EA owner.""" +class ProjectionReadiness(NamedTuple): + """Immutable decision describing whether a candidate may be handed to the EA owner.""" ready: bool truth_status: ProjectionTruthStatus From 0c5b017e71975052ee4f078707344f0859ab7a69 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 20:13:22 +0900 Subject: [PATCH 25/58] docs(ea-projection): record immutable readiness boundary --- packages/enterprise-architecture-projection/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/enterprise-architecture-projection/README.md b/packages/enterprise-architecture-projection/README.md index f1796d5a0..9bfb4b0cd 100644 --- a/packages/enterprise-architecture-projection/README.md +++ b/packages/enterprise-architecture-projection/README.md @@ -18,7 +18,7 @@ Only after release identity and the release-bound conformance, complete-bundle, Permitted projection concepts are deployable architecture concerns such as applications, interfaces, technology components and versions, providers, lifecycle, 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 contract-admission evidence identities used for the handoff. Every readiness result also carries the exact validated Orgmetra source repository and source revision, including fail-closed results, so downstream evidence cannot be detached from the candidate version that was evaluated. -Candidate construction is not the only integrity boundary. `evaluate_projection_readiness` revalidates the exact retained candidate immediately before issuing a readiness decision so 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 otherwise bypass constructor checks after validation. +Candidate construction is not the only integrity boundary. `evaluate_projection_readiness` revalidates the exact retained candidate immediately before issuing a readiness decision so 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 otherwise bypass constructor checks after validation. 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. From 52130fac552aa6fe156b52950711bae0b2888af5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 21:08:13 +0900 Subject: [PATCH 26/58] test(ea): bind readiness to exact candidate evidence --- .../tests/test_projection_source_binding.py | 66 ++++++++++++++----- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/packages/enterprise-architecture-projection/tests/test_projection_source_binding.py b/packages/enterprise-architecture-projection/tests/test_projection_source_binding.py index b0536b7e2..5568315bc 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_source_binding.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_source_binding.py @@ -1,4 +1,4 @@ -"""Regression tests for source-revision provenance in EA projection handoffs.""" +"""Regression tests for source-revision and candidate provenance in EA projection handoffs.""" from datetime import UTC, datetime @@ -17,20 +17,26 @@ ARTIFACT_SHA256 = "a" * 64 -def test_ready_handoff_preserves_exact_orgmetra_source_identity() -> None: - """A ready handoff must remain attributable to the exact Orgmetra source revision.""" - candidate = ArchitectureProjectionCandidate( - 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",), - ) - release = ContractReleaseEvidence( +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, @@ -38,7 +44,11 @@ def test_ready_handoff_preserves_exact_orgmetra_source_identity() -> None: release_state="published", verified_at=datetime(2026, 9, 1, 6, 1, tzinfo=UTC), ) - admission = ContractAdmissionEvidence( + + +def _admission() -> ContractAdmissionEvidence: + """Build admission evidence bound to the exact released contract bytes.""" + return ContractAdmissionEvidence( contract_commit_sha=CONTRACT_SHA, contract_asset_sha256=ARTIFACT_SHA256, conformance_receipt_sha256="b" * 64, @@ -48,8 +58,30 @@ def test_ready_handoff_preserves_exact_orgmetra_source_identity() -> None: verified_at=datetime(2026, 9, 1, 6, 2, tzinfo=UTC), ) - decision = evaluate_projection_readiness(candidate, release, admission) + +def test_ready_handoff_preserves_exact_orgmetra_source_identity() -> None: + """A ready handoff must remain attributable to the exact Orgmetra source revision.""" + decision = evaluate_projection_readiness(_candidate(), _release(), _admission()) assert decision.ready is True assert decision.source_repository == ORGMETRA_REPOSITORY assert decision.source_revision == ORGMETRA_SHA + + +def test_readiness_is_bound_to_exact_candidate_evidence_not_only_source_revision() -> None: + """Two candidates from one source revision must never share transferable readiness 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 True + assert second.ready is True + assert first.source_revision == second.source_revision == ORGMETRA_SHA + assert first.candidate_sha256 != second.candidate_sha256 From a1cb03d74919dcdbb31c2ec12332fc81ee3e62ec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 21:10:12 +0900 Subject: [PATCH 27/58] fix(ea): bind readiness to exact candidate evidence --- .../__init__.py | 78 ++++++++++++++++--- 1 file changed, 69 insertions(+), 9 deletions(-) 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 index a4465d927..e0ed243d2 100644 --- a/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py +++ b/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py @@ -12,6 +12,8 @@ from dataclasses import dataclass from datetime import datetime, timezone from enum import StrEnum +import hashlib +import json import re from typing import NamedTuple @@ -94,6 +96,20 @@ class ContractAdmissionEvidence: verified_at: datetime +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.""" @@ -101,6 +117,7 @@ class ProjectionReadiness(NamedTuple): truth_status: ProjectionTruthStatus source_repository: str source_revision: str + candidate_sha256: str reason: str next_action: str contract_commit_sha: str | None @@ -131,8 +148,10 @@ def _require_aware_time( raise TypeError(f"{field_name} must use exact built-in datetime and timezone") -def _validate_projection_candidate(candidate: ArchitectureProjectionCandidate) -> None: - """Revalidate the exact retained candidate before any readiness decision is issued.""" +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") @@ -163,6 +182,43 @@ def _validate_projection_candidate(candidate: ArchitectureProjectionCandidate) - ) +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") @@ -230,13 +286,15 @@ def evaluate_projection_readiness( """Return fail-closed readiness without conferring Enterprise Architecture truth.""" if type(candidate) is not ArchitectureProjectionCandidate: raise TypeError("candidate must be an ArchitectureProjectionCandidate") - _validate_projection_candidate(candidate) + candidate_snapshot = _snapshot_projection_candidate(candidate) + candidate_sha256 = _candidate_sha256(candidate_snapshot) if contract_release is None: return ProjectionReadiness( ready=False, truth_status=ProjectionTruthStatus.PROPOSED, - source_repository=candidate.source_repository, - source_revision=candidate.source_revision, + source_repository=candidate_snapshot.source_repository, + source_revision=candidate_snapshot.source_revision, + candidate_sha256=candidate_sha256, reason="context_graph_contract_release_not_admitted", next_action="install_approved_context_graph_contract_release", contract_commit_sha=None, @@ -252,8 +310,9 @@ def evaluate_projection_readiness( return ProjectionReadiness( ready=False, truth_status=ProjectionTruthStatus.PROPOSED, - source_repository=candidate.source_repository, - source_revision=candidate.source_revision, + source_repository=candidate_snapshot.source_repository, + source_revision=candidate_snapshot.source_revision, + candidate_sha256=candidate_sha256, reason="context_graph_contract_admission_not_verified", next_action="verify_released_context_graph_contract_admission", contract_commit_sha=contract_release.commit_sha, @@ -268,8 +327,9 @@ def evaluate_projection_readiness( return ProjectionReadiness( ready=True, truth_status=ProjectionTruthStatus.PROPOSED, - source_repository=candidate.source_repository, - source_revision=candidate.source_revision, + source_repository=candidate_snapshot.source_repository, + source_revision=candidate_snapshot.source_revision, + candidate_sha256=candidate_sha256, reason="projection_candidate_ready", next_action="submit_candidate_to_enterprise_architecture_owner", contract_commit_sha=contract_release.commit_sha, From c5d9a86f1987956dc90b959f79ff917c60f455d9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 21:10:50 +0900 Subject: [PATCH 28/58] docs(ea): document candidate-bound readiness evidence --- packages/enterprise-architecture-projection/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/enterprise-architecture-projection/README.md b/packages/enterprise-architecture-projection/README.md index 9bfb4b0cd..d7d59da3d 100644 --- a/packages/enterprise-architecture-projection/README.md +++ b/packages/enterprise-architecture-projection/README.md @@ -16,9 +16,11 @@ Only after release identity and the release-bound conformance, complete-bundle, ## Candidate scope -Permitted projection concepts are deployable architecture concerns such as applications, interfaces, technology components and versions, providers, lifecycle, 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 contract-admission evidence identities used for the handoff. Every readiness result also carries the exact validated Orgmetra source repository and source revision, including fail-closed results, so downstream evidence cannot be detached from the candidate version that was evaluated. +Permitted projection concepts are deployable architecture concerns such as applications, interfaces, technology components and versions, providers, lifecycle, 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 contract-admission evidence identities used for the handoff. -Candidate construction is not the only integrity boundary. `evaluate_projection_readiness` revalidates the exact retained candidate immediately before issuing a readiness decision so 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 otherwise bypass constructor checks after validation. 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. +Every readiness result 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 readiness decision for one candidate therefore cannot be treated as evidence for a different candidate merely because both came from the same Orgmetra commit. Fail-closed and ready outcomes use the same candidate-binding rule. + +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 readiness 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. From 71de6799bc54a1e5e32253c01e374aa2ce703bc5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 00:05:52 +0900 Subject: [PATCH 29/58] test(ea): pin runner admission regression --- .../tests/test_workflow_contract.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 packages/enterprise-architecture-projection/tests/test_workflow_contract.py diff --git a/packages/enterprise-architecture-projection/tests/test_workflow_contract.py b/packages/enterprise-architecture-projection/tests/test_workflow_contract.py new file mode 100644 index 000000000..7483371e1 --- /dev/null +++ b/packages/enterprise-architecture-projection/tests/test_workflow_contract.py @@ -0,0 +1,17 @@ +"""Regression contracts for Enterprise Architecture projection CI admission.""" + +from pathlib import Path + + +def _workflow() -> str: + """Read the repository-local EA projection workflow as UTF-8 text.""" + return Path(".github/workflows/enterprise-architecture-projection-quality.yml").read_text( + encoding="utf-8" + ) + + +def test_ea_projection_quality_uses_explicit_available_runner_image() -> None: + """Avoid the ubuntu-latest selector that is currently failing runner admission.""" + workflow = _workflow() + assert " runs-on: ubuntu-24.04\n" in workflow + assert " runs-on: ubuntu-latest\n" not in workflow From 901c3c17f3dfdf36ab395d7ea855a81f22bf3b2d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 00:06:50 +0900 Subject: [PATCH 30/58] fix(ci): use explicit available Ubuntu runner --- .../workflows/enterprise-architecture-projection-quality.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/enterprise-architecture-projection-quality.yml b/.github/workflows/enterprise-architecture-projection-quality.yml index 46559182b..8e31844ab 100644 --- a/.github/workflows/enterprise-architecture-projection-quality.yml +++ b/.github/workflows/enterprise-architecture-projection-quality.yml @@ -22,7 +22,7 @@ concurrency: jobs: unit: name: EA projection admission and 100% coverage - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 10 steps: - name: Checkout exact candidate From adf8a91a64c3ea3f6de3c36046d231ecbe383dd3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 00:12:18 +0900 Subject: [PATCH 31/58] chore(ea): defer runner selector to canonical CI repair --- .../workflows/enterprise-architecture-projection-quality.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/enterprise-architecture-projection-quality.yml b/.github/workflows/enterprise-architecture-projection-quality.yml index 8e31844ab..46559182b 100644 --- a/.github/workflows/enterprise-architecture-projection-quality.yml +++ b/.github/workflows/enterprise-architecture-projection-quality.yml @@ -22,7 +22,7 @@ concurrency: jobs: unit: name: EA projection admission and 100% coverage - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout exact candidate From dc6ed990040d6767b9ce656befeabd1d28b221b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 00:12:53 +0900 Subject: [PATCH 32/58] chore(ea): remove duplicate runner contract --- .../tests/test_workflow_contract.py | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 packages/enterprise-architecture-projection/tests/test_workflow_contract.py diff --git a/packages/enterprise-architecture-projection/tests/test_workflow_contract.py b/packages/enterprise-architecture-projection/tests/test_workflow_contract.py deleted file mode 100644 index 7483371e1..000000000 --- a/packages/enterprise-architecture-projection/tests/test_workflow_contract.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Regression contracts for Enterprise Architecture projection CI admission.""" - -from pathlib import Path - - -def _workflow() -> str: - """Read the repository-local EA projection workflow as UTF-8 text.""" - return Path(".github/workflows/enterprise-architecture-projection-quality.yml").read_text( - encoding="utf-8" - ) - - -def test_ea_projection_quality_uses_explicit_available_runner_image() -> None: - """Avoid the ubuntu-latest selector that is currently failing runner admission.""" - workflow = _workflow() - assert " runs-on: ubuntu-24.04\n" in workflow - assert " runs-on: ubuntu-latest\n" not in workflow From 6e55443b9cff8cf280bb937d4b3ec472b6cf3ed7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 04:08:28 +0900 Subject: [PATCH 33/58] test(ci): reject floating runner in EA projection workflow --- .../tests/test_workflow_runner_contract.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 packages/enterprise-architecture-projection/tests/test_workflow_runner_contract.py 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..0cc1e6d9f --- /dev/null +++ b/packages/enterprise-architecture-projection/tests/test_workflow_runner_contract.py @@ -0,0 +1,17 @@ +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 From 07b58ca76a0d50fa8f34b8ccd49f1f3a1133f53f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 04:08:45 +0900 Subject: [PATCH 34/58] fix(ci): pin EA projection runner image --- .../workflows/enterprise-architecture-projection-quality.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/enterprise-architecture-projection-quality.yml b/.github/workflows/enterprise-architecture-projection-quality.yml index 46559182b..8e31844ab 100644 --- a/.github/workflows/enterprise-architecture-projection-quality.yml +++ b/.github/workflows/enterprise-architecture-projection-quality.yml @@ -22,7 +22,7 @@ concurrency: jobs: unit: name: EA projection admission and 100% coverage - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 10 steps: - name: Checkout exact candidate From 1dcf1316ed4389dc26cff0d75d2dcedc01362da3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 05:05:55 +0900 Subject: [PATCH 35/58] test(ea): document runner contract module --- .../tests/test_workflow_runner_contract.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/enterprise-architecture-projection/tests/test_workflow_runner_contract.py b/packages/enterprise-architecture-projection/tests/test_workflow_runner_contract.py index 0cc1e6d9f..b153f0a70 100644 --- a/packages/enterprise-architecture-projection/tests/test_workflow_runner_contract.py +++ b/packages/enterprise-architecture-projection/tests/test_workflow_runner_contract.py @@ -1,3 +1,5 @@ +"""Protect the EA projection workflow from floating hosted-runner aliases.""" + from pathlib import Path From 635eeef538e73289a8aadfbcd8b9ebbaf3b39b24 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 05:35:19 +0900 Subject: [PATCH 36/58] test(ea): require immutable release admission evidence --- .../tests/test_projection_runtime_integrity.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py b/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py index 5f8f0f182..c97f2a200 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py @@ -179,6 +179,24 @@ def test_behavior_bearing_datetime_subclass_cannot_rewrite_recorded_time() -> No 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) From c52b9b9864c278d3ae3829204903252bf344fc50 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 05:40:25 +0900 Subject: [PATCH 37/58] fix(ea): make contract trust evidence immutable --- .../__init__.py | 10 ++++---- .../test_projection_runtime_integrity.py | 23 +++++++++++++------ 2 files changed, 20 insertions(+), 13 deletions(-) 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 index e0ed243d2..b3ecce0f3 100644 --- a/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py +++ b/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py @@ -71,9 +71,8 @@ def __post_init__(self) -> None: _validate_projection_candidate(self) -@dataclass(frozen=True, slots=True) -class ContractReleaseEvidence: - """Trusted-control-plane observation of a published context-graph release.""" +class ContractReleaseEvidence(NamedTuple): + """Immutable trusted-control-plane observation of a published context-graph release.""" repository: str release_tag: str @@ -83,9 +82,8 @@ class ContractReleaseEvidence: verified_at: datetime -@dataclass(frozen=True, slots=True) -class ContractAdmissionEvidence: - """Trusted evidence that exact released contract bytes passed semantic admission.""" +class ContractAdmissionEvidence(NamedTuple): + """Immutable evidence that exact released contract bytes passed semantic admission.""" contract_commit_sha: str contract_asset_sha256: str diff --git a/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py b/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py index c97f2a200..cc55417bb 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py @@ -128,8 +128,15 @@ def test_comparison_overriding_source_repository_cannot_claim_orgmetra_authority def test_comparison_overriding_admission_commit_cannot_bind_different_release() -> None: """Reject behavior-bearing digest text before exact release/admission equality checks.""" - admission = _admission() - object.__setattr__(admission, "contract_commit_sha", _AlwaysEqualText("f" * 40)) + 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), + ) with pytest.raises(TypeError, match="contract_commit_sha must be exact built-in text"): evaluate_projection_readiness(_candidate(), _release(), admission) @@ -155,11 +162,13 @@ def test_behavior_bearing_candidate_timezone_cannot_enter_projection_evidence() def test_behavior_bearing_release_timezone_cannot_rewrite_verification_evidence() -> None: """Reject mutable timezone behavior before release verification evidence is trusted.""" - release = _release() - object.__setattr__( - release, - "verified_at", - datetime(2026, 9, 1, 6, 1, tzinfo=_MutableTimezone()), + 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"): From 99da43d65a5071ac2bfb986a687ca814d8578c19 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 08:32:16 +0900 Subject: [PATCH 38/58] test(ea): require compatibility and migration receipts --- .../test_projection_compatibility_evidence.py | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py 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..293dcc617 --- /dev/null +++ b/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py @@ -0,0 +1,95 @@ +"""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 _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, + "compatibility_receipt_sha256": _COMPATIBILITY_SHA256, + "migration_receipt_sha256": _MIGRATION_SHA256, + "admission_state": "verified", + "verified_at": datetime(2026, 9, 1, 6, 2, tzinfo=UTC), + } + values.update(overrides) + return ContractAdmissionEvidence(**values) + + +def test_ready_projection_retains_compatibility_and_migration_receipts() -> None: + """Do not authorize handoff without exact compatibility and migration evidence.""" + decision = evaluate_projection_readiness(_candidate(), _release(), _admission()) + + assert decision.ready is True + 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"}), + ) From 4a1e1c1f9695bf5bd5f2b2bafcff15d810b3794a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 08:36:13 +0900 Subject: [PATCH 39/58] test(ea): block admission without lifecycle receipts --- .../test_projection_compatibility_evidence.py | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py b/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py index 293dcc617..2f0769907 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py @@ -49,6 +49,19 @@ def _release() -> ContractReleaseEvidence: ) +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] = { @@ -57,15 +70,30 @@ def _admission(**overrides: object) -> ContractAdmissionEvidence: "conformance_receipt_sha256": "b" * 64, "bundle_manifest_sha256": "c" * 64, "provenance_attestation_sha256": "d" * 64, - "compatibility_receipt_sha256": _COMPATIBILITY_SHA256, - "migration_receipt_sha256": _MIGRATION_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_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 + + def test_ready_projection_retains_compatibility_and_migration_receipts() -> None: """Do not authorize handoff without exact compatibility and migration evidence.""" decision = evaluate_projection_readiness(_candidate(), _release(), _admission()) From b9d5532b14e37f8414293e5c6de7ea6abd0f7f30 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 08:40:34 +0900 Subject: [PATCH 40/58] fix(ea): require compatibility and migration evidence --- .../README.md | 6 +- .../__init__.py | 127 ++++++++++++++---- .../tests/test_projection_boundary.py | 14 +- .../test_projection_runtime_integrity.py | 8 +- .../tests/test_projection_source_binding.py | 4 +- 5 files changed, 123 insertions(+), 36 deletions(-) diff --git a/packages/enterprise-architecture-projection/README.md b/packages/enterprise-architecture-projection/README.md index d7d59da3d..aa8086838 100644 --- a/packages/enterprise-architecture-projection/README.md +++ b/packages/enterprise-architecture-projection/README.md @@ -10,9 +10,11 @@ A candidate is blocked until a trusted control-plane lookup has observed an immu Release identity alone is deliberately insufficient. The exact released commit and artifact must also be bound to trusted evidence for executable semantic conformance, the complete published contract bundle, and artifact provenance. Until those admission and provenance receipts are independently verified, `evaluate_projection_readiness` remains fail-closed with the next action `verify_released_context_graph_contract_admission`. -The package does not guess or reproduce an unreleased Context Assertion, CloudEvent, API schema, profile, admission receipt, or provenance format. The trusted control plane must obtain those semantics from the immutable released `context-graph-contracts` package and retain their exact evidence identities. When `context-graph-contracts` has no immutable release, the earlier gate remains `install_approved_context_graph_contract_release`. +Semantic admission still does not prove that a released contract is safe for this consumer. Positive handoff also requires exact compatibility and migration receipts bound to the admitted release. Missing lifecycle receipts produce the separate fail-closed next action `verify_context_graph_contract_compatibility_and_migration`; one receipt cannot stand in for the other, and malformed receipt identity is rejected. -Only after release identity and the release-bound conformance, complete-bundle, and provenance evidence are all admitted can Orgmetra produce a `proposed` candidate whose next action is `submit_candidate_to_enterprise_architecture_owner`. Acceptance, replacement, lifecycle decisions, and authoritative EA truth remain with the Enterprise Architecture owner. +The package does not guess or reproduce an unreleased Context Assertion, CloudEvent, API schema, profile, admission receipt, provenance format, compatibility result, or migration result. The trusted control plane must obtain those semantics from the immutable released `context-graph-contracts` package and retain their exact evidence identities. When `context-graph-contracts` has no immutable release, the earlier gate remains `install_approved_context_graph_contract_release`. + +Only after release identity and the release-bound conformance, complete-bundle, provenance, compatibility, and migration evidence are all admitted can Orgmetra produce a `proposed` candidate whose next action is `submit_candidate_to_enterprise_architecture_owner`. Acceptance, replacement, lifecycle decisions, and authoritative EA truth remain with the Enterprise Architecture owner. ## Candidate scope 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 index b3ecce0f3..0f918fb77 100644 --- a/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py +++ b/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py @@ -1,10 +1,10 @@ """Fail-closed admission boundary for Orgmetra Enterprise Architecture projections. This package validates Orgmetra-owned architecture projection candidates and the -release, semantic-conformance, bundle-identity, and provenance evidence required -before they can be handed to the Enterprise Architecture owner. It does not -serialize the foreign context-graph contract, write Enterprise Architecture -state, or transport authoritative HR records. +release, semantic-conformance, bundle-identity, provenance, compatibility, and +migration evidence required before they can be handed to the Enterprise +Architecture owner. It does not serialize the foreign context-graph contract, +write Enterprise Architecture state, or transport authoritative HR records. """ from __future__ import annotations @@ -83,7 +83,7 @@ class ContractReleaseEvidence(NamedTuple): class ContractAdmissionEvidence(NamedTuple): - """Immutable evidence that exact released contract bytes passed semantic admission.""" + """Immutable evidence that exact released bytes passed semantic and lifecycle admission.""" contract_commit_sha: str contract_asset_sha256: str @@ -92,6 +92,8 @@ class ContractAdmissionEvidence(NamedTuple): 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): @@ -123,6 +125,8 @@ class ProjectionReadiness(NamedTuple): 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: @@ -237,11 +241,26 @@ def _validate_contract_release(release: ContractReleaseEvidence) -> None: _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, and provenance evidence to the exact released bytes.""" + """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") @@ -274,6 +293,53 @@ def _validate_contract_admission( 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", + ) + + +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 verified 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( @@ -287,41 +353,38 @@ def evaluate_projection_readiness( candidate_snapshot = _snapshot_projection_candidate(candidate) candidate_sha256 = _candidate_sha256(candidate_snapshot) if contract_release is None: - return ProjectionReadiness( - ready=False, - truth_status=ProjectionTruthStatus.PROPOSED, - source_repository=candidate_snapshot.source_repository, - source_revision=candidate_snapshot.source_revision, - candidate_sha256=candidate_sha256, + return _blocked_readiness( + candidate_snapshot, + candidate_sha256, reason="context_graph_contract_release_not_admitted", next_action="install_approved_context_graph_contract_release", - contract_commit_sha=None, - contract_asset_sha256=None, - contract_conformance_receipt_sha256=None, - contract_bundle_manifest_sha256=None, - contract_provenance_attestation_sha256=None, ) 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 ProjectionReadiness( - ready=False, - truth_status=ProjectionTruthStatus.PROPOSED, - source_repository=candidate_snapshot.source_repository, - source_revision=candidate_snapshot.source_revision, - candidate_sha256=candidate_sha256, + return _blocked_readiness( + candidate_snapshot, + candidate_sha256, reason="context_graph_contract_admission_not_verified", next_action="verify_released_context_graph_contract_admission", - contract_commit_sha=contract_release.commit_sha, - contract_asset_sha256=contract_release.asset_sha256, - contract_conformance_receipt_sha256=None, - contract_bundle_manifest_sha256=None, - contract_provenance_attestation_sha256=None, + 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 ProjectionReadiness( ready=True, truth_status=ProjectionTruthStatus.PROPOSED, @@ -334,5 +397,11 @@ def evaluate_projection_readiness( contract_asset_sha256=contract_release.asset_sha256, contract_conformance_receipt_sha256=contract_admission.conformance_receipt_sha256, contract_bundle_manifest_sha256=contract_admission.bundle_manifest_sha256, - contract_provenance_attestation_sha256=contract_admission.provenance_attestation_sha256, + contract_provenance_attestation_sha256=( + contract_admission.provenance_attestation_sha256 + ), + contract_compatibility_receipt_sha256=( + contract_admission.compatibility_receipt_sha256 + ), + contract_migration_receipt_sha256=contract_admission.migration_receipt_sha256, ) diff --git a/packages/enterprise-architecture-projection/tests/test_projection_boundary.py b/packages/enterprise-architecture-projection/tests/test_projection_boundary.py index 9572bc98f..c3aa5fe57 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_boundary.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_boundary.py @@ -23,6 +23,8 @@ 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: @@ -57,7 +59,7 @@ def _release(**overrides: object) -> ContractReleaseEvidence: def _admission(**overrides: object) -> ContractAdmissionEvidence: - """Return trusted conformance and provenance evidence with optional overrides.""" + """Return trusted conformance, provenance, and lifecycle evidence.""" values: dict[str, object] = { "contract_commit_sha": CONTRACT_SHA, "contract_asset_sha256": ARTIFACT_SHA256, @@ -66,6 +68,8 @@ def _admission(**overrides: object) -> ContractAdmissionEvidence: "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) @@ -84,6 +88,8 @@ def test_unreleased_contract_blocks_projection_with_next_action() -> 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: @@ -99,10 +105,12 @@ def test_release_metadata_without_conformance_and_provenance_stays_blocked() -> 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_verified_contract_admission_allows_only_a_candidate_not_ea_truth() -> None: - """Admit only evidence bound to conformance, bundle identity, and provenance.""" + """Admit only evidence bound to conformance, provenance, and lifecycle fitness.""" decision = evaluate_projection_readiness(_candidate(), _release(), _admission()) assert decision.ready is True @@ -114,6 +122,8 @@ def test_verified_contract_admission_allows_only_a_candidate_not_ea_truth() -> N 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( diff --git a/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py b/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py index cc55417bb..aeb38b5ac 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_runtime_integrity.py @@ -92,6 +92,8 @@ def _admission() -> ContractAdmissionEvidence: 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, ) @@ -136,6 +138,8 @@ def test_comparison_overriding_admission_commit_cannot_bind_different_release() 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"): @@ -144,7 +148,7 @@ def test_comparison_overriding_admission_commit_cannot_bind_different_release() def test_behavior_bearing_candidate_timezone_cannot_enter_projection_evidence() -> None: """Reject mutable timezone behavior before it becomes retained bitemporal evidence.""" - timezone = _MutableTimezone() + mutable_timezone = _MutableTimezone() with pytest.raises(TypeError, match="effective_from must use exact built-in datetime and timezone"): ArchitectureProjectionCandidate( @@ -152,7 +156,7 @@ def test_behavior_bearing_candidate_timezone_cannot_enter_projection_evidence() projection_kind=ProjectionKind.APPLICATION, source_revision=_ORGMETRA_SHA, source_repository="ContextualWisdomLab/Orgmetra", - effective_from=datetime(2026, 9, 1, tzinfo=timezone), + 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", diff --git a/packages/enterprise-architecture-projection/tests/test_projection_source_binding.py b/packages/enterprise-architecture-projection/tests/test_projection_source_binding.py index 5568315bc..e43340c1f 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_source_binding.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_source_binding.py @@ -47,7 +47,7 @@ def _release() -> ContractReleaseEvidence: def _admission() -> ContractAdmissionEvidence: - """Build admission evidence bound to the exact released contract bytes.""" + """Build admission evidence bound to exact released and lifecycle evidence.""" return ContractAdmissionEvidence( contract_commit_sha=CONTRACT_SHA, contract_asset_sha256=ARTIFACT_SHA256, @@ -56,6 +56,8 @@ def _admission() -> ContractAdmissionEvidence: 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, ) From d0b2f8f52f33576781794ace93037373f30e8761 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 08:45:19 +0900 Subject: [PATCH 41/58] test(ea): cover each missing lifecycle receipt --- .../test_projection_compatibility_evidence.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py b/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py index 2f0769907..b4c346195 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py @@ -94,6 +94,32 @@ def test_semantic_admission_without_lifecycle_receipts_remains_blocked() -> 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_ready_projection_retains_compatibility_and_migration_receipts() -> None: """Do not authorize handoff without exact compatibility and migration evidence.""" decision = evaluate_projection_readiness(_candidate(), _release(), _admission()) From c225eba85d855dc5e0caf47ea7b3c40acadd874e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 08:52:12 +0900 Subject: [PATCH 42/58] test(ea): reject one receipt reused for two results --- .../tests/test_projection_compatibility_evidence.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py b/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py index b4c346195..39f9992ea 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py @@ -120,6 +120,16 @@ def test_each_missing_lifecycle_receipt_blocks_projection_independently( 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_ready_projection_retains_compatibility_and_migration_receipts() -> None: """Do not authorize handoff without exact compatibility and migration evidence.""" decision = evaluate_projection_readiness(_candidate(), _release(), _admission()) From aae9ca0eca2f44a50aa11c2686f2819bf38b3822 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 08:53:42 +0900 Subject: [PATCH 43/58] fix(ea): keep lifecycle receipts distinct --- .../__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) 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 index 0f918fb77..b73990b44 100644 --- a/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py +++ b/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py @@ -303,6 +303,15 @@ def _validate_contract_admission( "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" + ) def _blocked_readiness( From f929fa4b5f290a696bc172118b0d641446555943 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 08:57:14 +0900 Subject: [PATCH 44/58] test(ea): reject pre-release admission evidence --- .../tests/test_projection_compatibility_evidence.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py b/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py index 39f9992ea..8725642ad 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py @@ -130,6 +130,16 @@ def test_one_lifecycle_receipt_cannot_stand_in_for_both_results() -> None: ) +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_ready_projection_retains_compatibility_and_migration_receipts() -> None: """Do not authorize handoff without exact compatibility and migration evidence.""" decision = evaluate_projection_readiness(_candidate(), _release(), _admission()) From 7e55ec38318a86132d9a5ca59800fc31e8d64bf4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 08:58:15 +0900 Subject: [PATCH 45/58] fix(ea): order release and admission verification --- .../src/orgmetra_enterprise_architecture_projection/__init__.py | 2 ++ 1 file changed, 2 insertions(+) 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 index b73990b44..907509397 100644 --- a/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py +++ b/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py @@ -289,6 +289,8 @@ def _validate_contract_admission( 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: From 8d97d0df6d444cfed850ffbf733868e2e5a743ae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 08:58:42 +0900 Subject: [PATCH 46/58] docs(ea): explain lifecycle verification order --- packages/enterprise-architecture-projection/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/enterprise-architecture-projection/README.md b/packages/enterprise-architecture-projection/README.md index aa8086838..23acb6d30 100644 --- a/packages/enterprise-architecture-projection/README.md +++ b/packages/enterprise-architecture-projection/README.md @@ -10,7 +10,7 @@ A candidate is blocked until a trusted control-plane lookup has observed an immu Release identity alone is deliberately insufficient. The exact released commit and artifact must also be bound to trusted evidence for executable semantic conformance, the complete published contract bundle, and artifact provenance. Until those admission and provenance receipts are independently verified, `evaluate_projection_readiness` remains fail-closed with the next action `verify_released_context_graph_contract_admission`. -Semantic admission still does not prove that a released contract is safe for this consumer. Positive handoff also requires exact compatibility and migration receipts bound to the admitted release. Missing lifecycle receipts produce the separate fail-closed next action `verify_context_graph_contract_compatibility_and_migration`; one receipt cannot stand in for the other, and malformed receipt identity is rejected. +Semantic admission still does not prove that a released contract is safe for this consumer. Positive handoff also requires exact compatibility and migration receipts bound to the admitted release. Missing lifecycle receipts produce the separate fail-closed next action `verify_context_graph_contract_compatibility_and_migration`; one receipt cannot stand in for the other, their digest identities must remain distinct, and malformed receipt identity is rejected. Admission verification must be at or after the trusted release-verification instant, so pre-release lifecycle evidence cannot authorize a production handoff. The package does not guess or reproduce an unreleased Context Assertion, CloudEvent, API schema, profile, admission receipt, provenance format, compatibility result, or migration result. The trusted control plane must obtain those semantics from the immutable released `context-graph-contracts` package and retain their exact evidence identities. When `context-graph-contracts` has no immutable release, the earlier gate remains `install_approved_context_graph_contract_release`. From 6de6e98149ab72303da3f8a6f3e05ab6d94958b2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 09:04:36 +0900 Subject: [PATCH 47/58] test(ea): reject reused admission evidence identities --- ...projection_evidence_identity_separation.py | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 packages/enterprise-architecture-projection/tests/test_projection_evidence_identity_separation.py 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..064a17e5f --- /dev/null +++ b/packages/enterprise-architecture-projection/tests/test_projection_evidence_identity_separation.py @@ -0,0 +1,95 @@ +"""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_remain_admissible() -> None: + """Preserve the positive path when every independent receipt has its own identity.""" + decision = evaluate_projection_readiness(_candidate(), _release(), _admission()) + + assert decision.ready is True From c746bf0a39272e1cabdf7720cf1e4df27aca2440 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 09:05:48 +0900 Subject: [PATCH 48/58] fix(ea): separate admission evidence identities --- .../__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 index 907509397..5d9b76883 100644 --- a/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py +++ b/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py @@ -314,6 +314,17 @@ def _validate_contract_admission( 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( From 07db2baeb013ad924f3116dd939afc692a94ec74 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 09:06:26 +0900 Subject: [PATCH 49/58] docs(ea): document independent evidence identities --- packages/enterprise-architecture-projection/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/enterprise-architecture-projection/README.md b/packages/enterprise-architecture-projection/README.md index 23acb6d30..379048f48 100644 --- a/packages/enterprise-architecture-projection/README.md +++ b/packages/enterprise-architecture-projection/README.md @@ -10,7 +10,7 @@ A candidate is blocked until a trusted control-plane lookup has observed an immu Release identity alone is deliberately insufficient. The exact released commit and artifact must also be bound to trusted evidence for executable semantic conformance, the complete published contract bundle, and artifact provenance. Until those admission and provenance receipts are independently verified, `evaluate_projection_readiness` remains fail-closed with the next action `verify_released_context_graph_contract_admission`. -Semantic admission still does not prove that a released contract is safe for this consumer. Positive handoff also requires exact compatibility and migration receipts bound to the admitted release. Missing lifecycle receipts produce the separate fail-closed next action `verify_context_graph_contract_compatibility_and_migration`; one receipt cannot stand in for the other, their digest identities must remain distinct, and malformed receipt identity is rejected. Admission verification must be at or after the trusted release-verification instant, so pre-release lifecycle evidence cannot authorize a production handoff. +Semantic admission still does not prove that a released contract is safe for this consumer. Positive handoff also requires exact compatibility and migration receipts 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 trusted release-verification instant, so pre-release lifecycle evidence cannot authorize a production handoff. The package does not guess or reproduce an unreleased Context Assertion, CloudEvent, API schema, profile, admission receipt, provenance format, compatibility result, or migration result. The trusted control plane must obtain those semantics from the immutable released `context-graph-contracts` package and retain their exact evidence identities. When `context-graph-contracts` has no immutable release, the earlier gate remains `install_approved_context_graph_contract_release`. From 7b448a17811d720945d793a9082f4c3e05913d9b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:09:32 +0900 Subject: [PATCH 50/58] test: require declared EA projection concept scope --- .../tests/test_projection_boundary.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/enterprise-architecture-projection/tests/test_projection_boundary.py b/packages/enterprise-architecture-projection/tests/test_projection_boundary.py index c3aa5fe57..f1245e852 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_boundary.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_boundary.py @@ -277,3 +277,24 @@ def test_projection_decision_exposes_no_free_form_hr_payload() -> None: 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} From 7546208f5ff38ea06c3cfc8ae8a8aa1068e37e82 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:12:46 +0900 Subject: [PATCH 51/58] fix: align EA projection kinds with declared scope --- .../__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 index 5d9b76883..9f159545d 100644 --- a/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py +++ b/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py @@ -34,11 +34,19 @@ 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" @@ -426,4 +434,4 @@ def evaluate_projection_readiness( contract_admission.compatibility_receipt_sha256 ), contract_migration_receipt_sha256=contract_admission.migration_receipt_sha256, - ) + ) \ No newline at end of file From 920cd4a9987787a82d59a290caeb9dccbfa001a3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:13:01 +0900 Subject: [PATCH 52/58] docs: align EA projection scope with executable kinds --- packages/enterprise-architecture-projection/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/enterprise-architecture-projection/README.md b/packages/enterprise-architecture-projection/README.md index 379048f48..3dbfd72ef 100644 --- a/packages/enterprise-architecture-projection/README.md +++ b/packages/enterprise-architecture-projection/README.md @@ -18,7 +18,7 @@ Only after release identity and the release-bound conformance, complete-bundle, ## Candidate scope -Permitted projection concepts are deployable architecture concerns such as applications, interfaces, technology components and versions, providers, lifecycle, 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 contract-admission evidence identities used for the handoff. +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 contract-admission evidence identities used for the handoff. Every readiness result 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 readiness decision for one candidate therefore cannot be treated as evidence for a different candidate merely because both came from the same Orgmetra commit. Fail-closed and ready outcomes use the same candidate-binding rule. From 6daf46fb883f4234f6165a1358459369a4558d90 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:16:29 +0900 Subject: [PATCH 53/58] test: fail closed without trusted control-plane evidence --- .../tests/test_projection_boundary.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/enterprise-architecture-projection/tests/test_projection_boundary.py b/packages/enterprise-architecture-projection/tests/test_projection_boundary.py index f1245e852..c94a81dd4 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_boundary.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_boundary.py @@ -59,7 +59,7 @@ def _release(**overrides: object) -> ContractReleaseEvidence: def _admission(**overrides: object) -> ContractAdmissionEvidence: - """Return trusted conformance, provenance, and lifecycle evidence.""" + """Return trusted-looking conformance, provenance, and lifecycle evidence.""" values: dict[str, object] = { "contract_commit_sha": CONTRACT_SHA, "contract_asset_sha256": ARTIFACT_SHA256, @@ -109,14 +109,14 @@ def test_release_metadata_without_conformance_and_provenance_stays_blocked() -> assert decision.contract_migration_receipt_sha256 is None -def test_verified_contract_admission_allows_only_a_candidate_not_ea_truth() -> None: - """Admit only evidence bound to conformance, provenance, and lifecycle fitness.""" +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 True + assert decision.ready is False assert decision.truth_status is ProjectionTruthStatus.PROPOSED - assert decision.reason == "projection_candidate_ready" - assert decision.next_action == "submit_candidate_to_enterprise_architecture_owner" + 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 From a0552d97368b0135cef40d70dd55e8effdd04313 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:16:56 +0900 Subject: [PATCH 54/58] test: keep caller evidence blocked after lifecycle validation --- .../tests/test_projection_compatibility_evidence.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py b/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py index 8725642ad..54a9d6854 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_compatibility_evidence.py @@ -140,11 +140,13 @@ def test_admission_verification_cannot_predate_release_verification() -> None: ) -def test_ready_projection_retains_compatibility_and_migration_receipts() -> None: - """Do not authorize handoff without exact compatibility and migration evidence.""" +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 True + 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 From 067177411caf81ae54e2b9452b88510ccf9c7956 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:17:14 +0900 Subject: [PATCH 55/58] test: separate receipt validity from trust authorization --- .../tests/test_projection_evidence_identity_separation.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 index 064a17e5f..f2593a08c 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_evidence_identity_separation.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_evidence_identity_separation.py @@ -88,8 +88,10 @@ def test_semantically_distinct_receipts_cannot_reuse_one_artifact_identity( evaluate_projection_readiness(_candidate(), _release(), admission) -def test_distinct_receipt_identities_remain_admissible() -> None: - """Preserve the positive path when every independent receipt has its own identity.""" +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 True + assert decision.ready is False + assert decision.reason == "trusted_control_plane_evidence_not_available" + assert decision.next_action == "integrate_released_context_graph_trust_contract" From 1791dff166534a2fa2b450a5967798e07e1f295f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:17:26 +0900 Subject: [PATCH 56/58] test: retain source binding while trust gate is closed --- .../tests/test_projection_source_binding.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/enterprise-architecture-projection/tests/test_projection_source_binding.py b/packages/enterprise-architecture-projection/tests/test_projection_source_binding.py index e43340c1f..7c2ce495b 100644 --- a/packages/enterprise-architecture-projection/tests/test_projection_source_binding.py +++ b/packages/enterprise-architecture-projection/tests/test_projection_source_binding.py @@ -61,17 +61,18 @@ def _admission() -> ContractAdmissionEvidence: ) -def test_ready_handoff_preserves_exact_orgmetra_source_identity() -> None: - """A ready handoff must remain attributable to the exact Orgmetra source revision.""" +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 True + 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_readiness_is_bound_to_exact_candidate_evidence_not_only_source_revision() -> None: - """Two candidates from one source revision must never share transferable readiness evidence.""" +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( @@ -83,7 +84,8 @@ def test_readiness_is_bound_to_exact_candidate_evidence_not_only_source_revision _admission(), ) - assert first.ready is True - assert second.ready is True + 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 From 8530450995b4a73e9fc5a452cace62971abe2286 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:18:15 +0900 Subject: [PATCH 57/58] fix: require repository-owned trust before EA handoff --- .../__init__.py | 47 ++++++++----------- 1 file changed, 19 insertions(+), 28 deletions(-) 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 index 9f159545d..8ddc067d6 100644 --- a/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py +++ b/packages/enterprise-architecture-projection/src/orgmetra_enterprise_architecture_projection/__init__.py @@ -1,10 +1,13 @@ """Fail-closed admission boundary for Orgmetra Enterprise Architecture projections. This package validates Orgmetra-owned architecture projection candidates and the -release, semantic-conformance, bundle-identity, provenance, compatibility, and -migration evidence required before they can be handed to the Enterprise -Architecture owner. It does not serialize the foreign context-graph contract, -write Enterprise Architecture state, or transport authoritative HR records. +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 @@ -80,7 +83,7 @@ def __post_init__(self) -> None: class ContractReleaseEvidence(NamedTuple): - """Immutable trusted-control-plane observation of a published context-graph release.""" + """Release-shape evidence that still requires repository-owned trust verification.""" repository: str release_tag: str @@ -91,7 +94,7 @@ class ContractReleaseEvidence(NamedTuple): class ContractAdmissionEvidence(NamedTuple): - """Immutable evidence that exact released bytes passed semantic and lifecycle admission.""" + """Admission-shape evidence that still requires repository-owned trust verification.""" contract_commit_sha: str contract_asset_sha256: str @@ -343,7 +346,7 @@ def _blocked_readiness( release: ContractReleaseEvidence | None = None, admission: ContractAdmissionEvidence | None = None, ) -> ProjectionReadiness: - """Build one immutable fail-closed decision without dropping verified evidence.""" + """Build one immutable fail-closed decision without dropping validated evidence.""" return ProjectionReadiness( ready=False, truth_status=ProjectionTruthStatus.PROPOSED, @@ -377,7 +380,7 @@ def evaluate_projection_readiness( contract_release: ContractReleaseEvidence | None, contract_admission: ContractAdmissionEvidence | None = None, ) -> ProjectionReadiness: - """Return fail-closed readiness without conferring Enterprise Architecture truth.""" + """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) @@ -415,23 +418,11 @@ def evaluate_projection_readiness( release=contract_release, admission=contract_admission, ) - return ProjectionReadiness( - ready=True, - truth_status=ProjectionTruthStatus.PROPOSED, - source_repository=candidate_snapshot.source_repository, - source_revision=candidate_snapshot.source_revision, - candidate_sha256=candidate_sha256, - reason="projection_candidate_ready", - next_action="submit_candidate_to_enterprise_architecture_owner", - contract_commit_sha=contract_release.commit_sha, - contract_asset_sha256=contract_release.asset_sha256, - contract_conformance_receipt_sha256=contract_admission.conformance_receipt_sha256, - contract_bundle_manifest_sha256=contract_admission.bundle_manifest_sha256, - contract_provenance_attestation_sha256=( - contract_admission.provenance_attestation_sha256 - ), - contract_compatibility_receipt_sha256=( - contract_admission.compatibility_receipt_sha256 - ), - contract_migration_receipt_sha256=contract_admission.migration_receipt_sha256, - ) \ No newline at end of file + 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, + ) From dc5768531627de03ff9a5d95c7cf99ea4b5cec85 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 13:18:40 +0900 Subject: [PATCH 58/58] docs: make trusted-control-plane gate explicit --- .../README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/enterprise-architecture-projection/README.md b/packages/enterprise-architecture-projection/README.md index 3dbfd72ef..d9ac15d71 100644 --- a/packages/enterprise-architecture-projection/README.md +++ b/packages/enterprise-architecture-projection/README.md @@ -1,28 +1,28 @@ # Enterprise Architecture projection admission -This package is the Orgmetra-side admission 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. +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 -A candidate is blocked until a trusted control-plane lookup has observed an immutable published `ContextualWisdomLab/context-graph-contracts` release and supplied its stable release tag, exact commit SHA, artifact SHA-256, publication state, and timezone-aware verification time. Shape validation in this package is not proof that the external release exists and must not be populated from untrusted request data. +`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. -Release identity alone is deliberately insufficient. The exact released commit and artifact must also be bound to trusted evidence for executable semantic conformance, the complete published contract bundle, and artifact provenance. Until those admission and provenance receipts are independently verified, `evaluate_projection_readiness` remains fail-closed with the next action `verify_released_context_graph_contract_admission`. +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. Positive handoff also requires exact compatibility and migration receipts 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 trusted release-verification instant, so pre-release lifecycle evidence cannot authorize a production handoff. +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. -The package does not guess or reproduce an unreleased Context Assertion, CloudEvent, API schema, profile, admission receipt, provenance format, compatibility result, or migration result. The trusted control plane must obtain those semantics from the immutable released `context-graph-contracts` package and retain their exact evidence identities. When `context-graph-contracts` has no immutable release, the earlier gate remains `install_approved_context_graph_contract_release`. +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. -Only after release identity and the release-bound conformance, complete-bundle, provenance, compatibility, and migration evidence are all admitted can Orgmetra produce a `proposed` candidate whose next action is `submit_candidate_to_enterprise_architecture_owner`. Acceptance, replacement, lifecycle decisions, and authoritative EA truth remain with the Enterprise Architecture owner. +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 contract-admission evidence identities used for the handoff. +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 readiness result 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 readiness decision for one candidate therefore cannot be treated as evidence for a different candidate merely because both came from the same Orgmetra commit. Fail-closed and ready outcomes use the same candidate-binding rule. +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 readiness 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. +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.