From 12814b67e872b7c75e3cb1e842723ab09656d5ae Mon Sep 17 00:00:00 2001 From: karenchuu <25980598+karenchuu@users.noreply.github.com> Date: Mon, 28 Sep 2026 14:40:04 +0800 Subject: [PATCH 1/3] refactor(control-plane): decide the stored digest shape in one owner Nineteen modules in loopx/ each answered "is this stored value a SHA-256?" for themselves, in three textual spellings of two questions: ten required the `sha256:` envelope, five matched bare `^[a-f0-9]{64}$` and three matched the same character set written `^[0-9a-f]{64}$`. The last two groups can never disagree, so eight of those copies were restated knowledge rather than policy. `control_plane/content_digest.py` now owns both envelopes and eighteen modules read it; capabilities/benchmark_toolkit/behavior_finding.py no longer reaches into a sibling module's private `_DIGEST_RE` to avoid writing a twentieth copy. Per-surface messages, length ceilings and normalisation stay where they are. Whole-value shapes only: literals that embed a hex digest in a larger grammar, the hand-built `"sha256:" + hexdigest` producers, the TypeScript mirror and the copies under packages/ are each a different question and are recorded instead of absorbed. content_ops/item_lifecycle.py keeps its private copy because open PR #3313 edits that file; the guard fails if that entry stops being true. Signed-off-by: karenchuu <25980598+karenchuu@users.noreply.github.com> --- .../benchmark_toolkit/behavior_finding.py | 9 +- .../benchmark_toolkit/study_projection.py | 4 +- loopx/capabilities/periodic_report/archive.py | 4 +- .../periodic_report/incremental.py | 4 +- .../periodic_report/machine_defaults.py | 5 +- loopx/capabilities/progress_review/receipt.py | 4 +- loopx/chat_action_normalization.py | 4 +- loopx/configuration_transaction.py | 8 +- loopx/control_plane/content_digest.py | 20 + .../local_authority_shadow_outbox.py | 7 +- .../control_plane/goals/activation_service.py | 5 +- loopx/control_plane/goals/deletion_service.py | 12 +- .../goals/goal_amendment_proposal.py | 5 +- .../control_plane/projects/registry_codec.py | 5 +- .../testing/release_commit_qualification.py | 4 +- .../governed_transition_proposal.py | 4 +- .../work_items/progress_review_policy.py | 5 +- .../history_export.py | 4 +- loopx/extensions/presentation.py | 4 +- .../test_content_digest_single_owner.py | 414 ++++++++++++++++++ 20 files changed, 482 insertions(+), 49 deletions(-) create mode 100644 loopx/control_plane/content_digest.py create mode 100644 tests/architecture/test_content_digest_single_owner.py diff --git a/loopx/capabilities/benchmark_toolkit/behavior_finding.py b/loopx/capabilities/benchmark_toolkit/behavior_finding.py index 3669a68a9e..db3b96f07d 100644 --- a/loopx/capabilities/benchmark_toolkit/behavior_finding.py +++ b/loopx/capabilities/benchmark_toolkit/behavior_finding.py @@ -10,8 +10,8 @@ from collections.abc import Iterable, Mapping from typing import Any +from ...control_plane.content_digest import BARE_SHA256_PATTERN from .study_projection import ( - _DIGEST_RE, _active_envelopes, _bounded_text, _finite_number, @@ -118,7 +118,7 @@ def normalize_benchmark_behavior_finding(payload: Mapping[str, Any]) -> dict[str if basis == "all_available" and sample != population: raise ValueError("all_available requires sample_count == population_count") digest = selection["cohort_digest"] - if not isinstance(digest, str) or not _DIGEST_RE.fullmatch(digest): + if not isinstance(digest, str) or not BARE_SHA256_PATTERN.fullmatch(digest): raise ValueError("cohort_digest must be SHA-256") measures = [] for item in _items(p["measures"], "measures", minimum=0): @@ -165,7 +165,10 @@ def normalize_benchmark_behavior_finding(payload: Mapping[str, Any]) -> dict[str e = _object( item, {"kind", "digest", "label", "relation", "summary"}, "evidence" ) - if not isinstance(e["digest"], str) or not _DIGEST_RE.fullmatch(e["digest"]): + item_digest = e["digest"] + if not isinstance(item_digest, str) or not BARE_SHA256_PATTERN.fullmatch( + item_digest + ): raise ValueError("evidence digest must be SHA-256") evidence.append( { diff --git a/loopx/capabilities/benchmark_toolkit/study_projection.py b/loopx/capabilities/benchmark_toolkit/study_projection.py index 9cad9739a1..94ae4b1ef6 100644 --- a/loopx/capabilities/benchmark_toolkit/study_projection.py +++ b/loopx/capabilities/benchmark_toolkit/study_projection.py @@ -15,6 +15,7 @@ from typing import Any from ...file_lock import exclusive_file_lock +from ...control_plane.content_digest import BARE_SHA256_PATTERN from .experiment_board import ( BENCHMARK_EXPERIMENT_BOARD_ROW_SCHEMA_VERSION, benchmark_experiment_board_row_key, @@ -41,7 +42,6 @@ BENCHMARK_STUDY_DASHBOARD_SCHEMA_VERSION = "benchmark_study_dashboard_v0" _TOKEN_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:@+-]{0,127}$") -_DIGEST_RE = re.compile(r"^[0-9a-f]{64}$") _ARM_ROLES = {"baseline", "control", "treatment", "explore"} _METRIC_ROLES = {"primary", "guardrail", "supporting"} _RECORD_KINDS = { @@ -579,7 +579,7 @@ def normalize_benchmark_upload_envelope( if payload.get("record_id") != rebuilt["record_id"]: raise ValueError("benchmark upload record_id does not match envelope identity") digest = str(payload.get("payload_digest") or "") - if not _DIGEST_RE.fullmatch(digest) or digest != rebuilt["payload_digest"]: + if not BARE_SHA256_PATTERN.fullmatch(digest) or digest != rebuilt["payload_digest"]: raise ValueError("benchmark upload payload digest mismatch") return rebuilt diff --git a/loopx/capabilities/periodic_report/archive.py b/loopx/capabilities/periodic_report/archive.py index aeb9d2940d..0ebf2d188c 100644 --- a/loopx/capabilities/periodic_report/archive.py +++ b/loopx/capabilities/periodic_report/archive.py @@ -14,6 +14,7 @@ from typing import Any from urllib.parse import unquote, urlsplit +from ...control_plane.content_digest import ENVELOPED_SHA256_PATTERN from .adapters import ARTIFACT_SCHEMA, DOCUMENT_SCHEMA from .core import _normalize_trigger_receipt, _reject_raw_keys @@ -24,7 +25,6 @@ MEMORY_REFERENCE_SCHEMA = "periodic_report_memory_reference_v0" _TOKEN_RE = re.compile(r"^[a-z][a-z0-9_.-]{0,127}$") -_SHA256_RE = re.compile(r"^sha256:[0-9a-f]{64}$") ArchiveReadback = Callable[[str], Mapping[str, Any]] @@ -68,7 +68,7 @@ def _token(value: object, label: str) -> str: def _sha256(value: object, label: str) -> str: digest = _text(value, label, maximum=80) - if not _SHA256_RE.fullmatch(digest): + if not ENVELOPED_SHA256_PATTERN.fullmatch(digest): raise ValueError(f"{label} must use sha256") return digest diff --git a/loopx/capabilities/periodic_report/incremental.py b/loopx/capabilities/periodic_report/incremental.py index c17a1c6596..0fec0820f6 100644 --- a/loopx/capabilities/periodic_report/incremental.py +++ b/loopx/capabilities/periodic_report/incremental.py @@ -8,6 +8,7 @@ from pathlib import Path from typing import Any +from ...control_plane.content_digest import ENVELOPED_SHA256_PATTERN from ...file_lock import LockAcquisitionPolicy, exclusive_file_lock from ...registry import atomic_write_json, read_json @@ -17,7 +18,6 @@ INCREMENTAL_BASELINE_SCHEMA = "periodic_report_incremental_baseline_v0" _IDENTITY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$") -_SHA256_RE = re.compile(r"^sha256:[0-9a-f]{64}$") def _canonical_digest(value: object) -> str: @@ -60,7 +60,7 @@ def _timestamp(value: object, label: str) -> str: def _digest(value: object, label: str) -> str: digest = _required_text(value, label, maximum=80) - if not _SHA256_RE.fullmatch(digest): + if not ENVELOPED_SHA256_PATTERN.fullmatch(digest): raise ValueError(f"{label} must use sha256") return digest diff --git a/loopx/capabilities/periodic_report/machine_defaults.py b/loopx/capabilities/periodic_report/machine_defaults.py index 28bd3a90e9..31f801222f 100644 --- a/loopx/capabilities/periodic_report/machine_defaults.py +++ b/loopx/capabilities/periodic_report/machine_defaults.py @@ -2,7 +2,6 @@ import hashlib import json -import re from collections.abc import Mapping from datetime import datetime, timezone from typing import Any @@ -10,6 +9,7 @@ from .cadence import normalize_report_cadence +from ...control_plane.content_digest import ENVELOPED_SHA256_PATTERN from ...control_plane.todos.contract import normalize_todo_claimed_by from ..configuration_ui import resolve_capability_configuration from ..machine_configuration.contract import ( @@ -31,7 +31,6 @@ SUBSCRIPTION_ERROR_SCHEMA = "periodic_report_subscription_error_v0" _INHERITANCE_MODE = "live_machine_default" -_REVISION_RE = re.compile(r"^sha256:[0-9a-f]{64}$") class PeriodicReportSubscriptionConfigurationError(ValueError): @@ -400,7 +399,7 @@ def normalize_periodic_report_delivery_authority(raw: object) -> dict[str, Any]: authority.get("effective_revision"), "delivery_authority.effective_revision", ) - if not _REVISION_RE.fullmatch(effective_revision): + if not ENVELOPED_SHA256_PATTERN.fullmatch(effective_revision): raise ValueError("delivery_authority.effective_revision is invalid") return { "schema_version": DELIVERY_AUTHORITY_SCHEMA, diff --git a/loopx/capabilities/progress_review/receipt.py b/loopx/capabilities/progress_review/receipt.py index d538a4e2b1..336abe35ee 100644 --- a/loopx/capabilities/progress_review/receipt.py +++ b/loopx/capabilities/progress_review/receipt.py @@ -16,6 +16,7 @@ import re import tempfile from typing import Any +from ...control_plane.content_digest import BARE_SHA256_PATTERN PROGRESS_REVIEW_RECEIPT_SCHEMA_VERSION = "progress_review_receipt_v0" PROGRESS_REVIEW_RECEIPT_STATUSES: tuple[str, ...] = ( @@ -39,7 +40,6 @@ PROGRESS_REVIEW_PENDING_REASON = "pending_evaluation" MAX_RECEIPT_BYTES = 65536 MAX_LOADED_RECEIPTS = 256 -_HEX64 = re.compile(r"^[a-f0-9]{64}$") _TEXT_LIMIT = 200 @@ -71,7 +71,7 @@ def _text(value: Any, *, field: str, required: bool = True) -> str | None: def _hex64(value: Any, *, field: str) -> str: text = _text(value, field=field) - if text is None or not _HEX64.fullmatch(text): + if text is None or not BARE_SHA256_PATTERN.fullmatch(text): raise ValueError(f"receipt.{field} must be a sha256 hex digest") return text diff --git a/loopx/chat_action_normalization.py b/loopx/chat_action_normalization.py index f137d0c735..94f42e8282 100644 --- a/loopx/chat_action_normalization.py +++ b/loopx/chat_action_normalization.py @@ -7,12 +7,12 @@ from typing import Any, Mapping from .agent_registry import registered_agent_ids_for_goal +from .control_plane.content_digest import BARE_SHA256_PATTERN from .control_plane.runtime.time import now_utc, parse_timestamp, utc_isoformat from .control_plane.todos.contract import require_supported_todo_resume_when from .registry import registry_goals -_SHA256 = re.compile(r"^[0-9a-f]{64}$") _AUTHORITY_PRINCIPAL = re.compile(r"^[a-z][a-z0-9._-]{0,30}:[A-Za-z0-9._:-]{1,200}$") @@ -65,7 +65,7 @@ def _normalize( if not isinstance(payload, Mapping): raise ValueError("operation payload must be an object") payload_digest = str(values.get("payload_digest") or "").strip() - if not _SHA256.fullmatch(payload_digest): + if not BARE_SHA256_PATTERN.fullmatch(payload_digest): raise ValueError("operation payload_digest must be lowercase SHA-256") if _digest(payload) != payload_digest: raise ValueError("operation payload_digest does not match payload") diff --git a/loopx/configuration_transaction.py b/loopx/configuration_transaction.py index 842408e376..254d9bdab5 100644 --- a/loopx/configuration_transaction.py +++ b/loopx/configuration_transaction.py @@ -2,14 +2,13 @@ import hashlib import json -import re from collections.abc import Mapping from copy import deepcopy from typing import Any +from .control_plane.content_digest import ENVELOPED_SHA256_PATTERN CONFIGURATION_REVISION_MISSING = "absent" -_REVISION_RE = re.compile(r"^sha256:[0-9a-f]{64}$") def configuration_payload_revision(value: object) -> str: @@ -51,8 +50,9 @@ def goal_capability_configuration_revision( def _validated_revision(value: str, *, label: str) -> str: revision = str(value or "").strip() - if revision != CONFIGURATION_REVISION_MISSING and not _REVISION_RE.fullmatch( - revision + if ( + revision != CONFIGURATION_REVISION_MISSING + and not ENVELOPED_SHA256_PATTERN.fullmatch(revision) ): raise ValueError(f"{label} must be absent or a sha256 revision") return revision diff --git a/loopx/control_plane/content_digest.py b/loopx/control_plane/content_digest.py new file mode 100644 index 0000000000..4eb81f56c3 --- /dev/null +++ b/loopx/control_plane/content_digest.py @@ -0,0 +1,20 @@ +"""One owner for the two shapes a stored SHA-256 digest can take. + +A digest reaches a record in one of two envelopes: the bare 64 lowercase hex +characters, or that same hex behind the ``sha256:`` prefix that the +periodic-report and content-ops writers concatenate. Before this module the +decision was compiled independently in eighteen modules under three spellings. + +Patterns that merely contain a hex digest inside a larger grammar (a +``cadence_…`` identifier, a journal filename, a ``40|64`` Git object id, a +compound cursor) answer a different question and stay with the surface that +owns that grammar. Producers that build the envelope by hand are the other half +of this decision and are deliberately unchanged here. +""" + +from __future__ import annotations + +import re + +ENVELOPED_SHA256_PATTERN = re.compile(r"^sha256:[0-9a-f]{64}$") +BARE_SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$") diff --git a/loopx/control_plane/coordination/local_authority_shadow_outbox.py b/loopx/control_plane/coordination/local_authority_shadow_outbox.py index 40e5eebd9b..6b6c8b2440 100644 --- a/loopx/control_plane/coordination/local_authority_shadow_outbox.py +++ b/loopx/control_plane/coordination/local_authority_shadow_outbox.py @@ -42,6 +42,7 @@ LOCAL_AUTHORITY_SHADOW_READ_REQUEST_SCHEMA, LOCAL_AUTHORITY_SHADOW_READ_RESULT_SCHEMA, ) +from ..content_digest import ENVELOPED_SHA256_PATTERN from .shadow_management import ( read_shadow_capture_binding, shadow_maintenance_lock_target, @@ -260,7 +261,6 @@ def _index_entry_files( _WRITER_RUNTIMES = frozenset({WRITER_RUNTIME_PYTHON, WRITER_RUNTIME_TYPESCRIPT}) _SOURCE_KINDS = frozenset({SOURCE_MARKDOWN, SOURCE_STATE_EVENT_LOG, SOURCE_TASK_LEASE}) -_DIGEST_PATTERN = re.compile(r"^sha256:[0-9a-f]{64}$") def _load_prepared_record( @@ -299,7 +299,7 @@ def _load_prepared_record( and bool(writer.get("write_class")) and source.get("kind") in _SOURCE_KINDS and isinstance(root_digest, str) - and _DIGEST_PATTERN.match(root_digest) is not None + and ENVELOPED_SHA256_PATTERN.match(root_digest) is not None and isinstance(lineage_id, str) and bool(lineage_id) and source_ref is not None @@ -508,7 +508,8 @@ def invalid() -> OutboxError: or ( digest is not None and ( - not isinstance(digest, str) or _DIGEST_PATTERN.fullmatch(digest) is None + not isinstance(digest, str) + or ENVELOPED_SHA256_PATTERN.fullmatch(digest) is None ) ) or any( diff --git a/loopx/control_plane/goals/activation_service.py b/loopx/control_plane/goals/activation_service.py index 99eab4e745..edaec5db1e 100644 --- a/loopx/control_plane/goals/activation_service.py +++ b/loopx/control_plane/goals/activation_service.py @@ -5,7 +5,6 @@ from enum import Enum import hashlib from pathlib import Path -import re from typing import Any from ..projects.registry_codec import project_registry_transaction @@ -23,6 +22,7 @@ goal_activation_state, normalize_goal_activation_state, ) +from ..content_digest import BARE_SHA256_PATTERN from .configure_goal_service import resolve_configure_goal_sync_target @@ -34,7 +34,6 @@ GOAL_ACTIVATION_AUTHORITY_ROUTE_SCHEMA_VERSION = ( "loopx_goal_activation_authority_route_v1" ) -_SHA256 = re.compile(r"^[a-f0-9]{64}$") class GoalActivationAuthorityRouteMode(str, Enum): @@ -285,7 +284,7 @@ def set_goal_activation_state( source_bytes = source_registry.read_bytes() source_goal = _goal(load_registry(source_registry), normalized_goal_id) normalized_fingerprint = str(expected_state_fingerprint or "").strip() or None - if normalized_fingerprint is not None and not _SHA256.fullmatch( + if normalized_fingerprint is not None and not BARE_SHA256_PATTERN.fullmatch( normalized_fingerprint ): raise ValueError("expected state fingerprint must be a SHA-256 digest") diff --git a/loopx/control_plane/goals/deletion_service.py b/loopx/control_plane/goals/deletion_service.py index 8de6cc02d0..2aee5d89e9 100644 --- a/loopx/control_plane/goals/deletion_service.py +++ b/loopx/control_plane/goals/deletion_service.py @@ -37,6 +37,7 @@ _source_and_target, _source_status, ) +from ..content_digest import BARE_SHA256_PATTERN GOAL_DELETION_SCHEMA_VERSION = "loopx_goal_deletion_v1" @@ -46,7 +47,6 @@ ) GOAL_DELETION_RECOVERY_SCHEMA_VERSION = "loopx_goal_deletion_recovery_v1" _OPAQUE_ID = re.compile(r"^[A-Za-z0-9._:-]{1,200}$") -_SHA256 = re.compile(r"^[a-f0-9]{64}$") _MAX_RECOVERY_RECEIPT_BYTES = 64 * 1024 @@ -111,9 +111,9 @@ def _normalize_source_basis(value: Mapping[str, Any] | None) -> dict[str, str] | route_mode = str(value.get("route_mode") or "") if schema_version != GOAL_DELETION_SOURCE_BASIS_SCHEMA_VERSION: raise ValueError("expected source basis has an unsupported schema version") - if not _SHA256.fullmatch(source_identity): + if not BARE_SHA256_PATTERN.fullmatch(source_identity): raise ValueError("expected source identity must be a SHA-256 digest") - if not _SHA256.fullmatch(source_content_sha256): + if not BARE_SHA256_PATTERN.fullmatch(source_content_sha256): raise ValueError("expected source content digest must be a SHA-256 digest") if route_mode not in {mode.value for mode in GoalActivationAuthorityRouteMode}: raise ValueError("expected source route mode is unsupported") @@ -335,7 +335,7 @@ def _validated_recovery_record( if value.get("goal_id") != goal_id: raise ValueError("Goal deletion recovery goal identity does not match") state_fingerprint = str(value.get("state_fingerprint") or "") - if not _SHA256.fullmatch(state_fingerprint): + if not BARE_SHA256_PATTERN.fullmatch(state_fingerprint): raise ValueError("Goal deletion recovery state fingerprint is invalid") source_basis_value = value.get("source_basis") if not isinstance(source_basis_value, Mapping): @@ -390,7 +390,7 @@ def _validated_recovery_record( field="snapshot backup path", ) preimage_sha256 = str(snapshot_value.get("preimage_sha256") or "") - if not _SHA256.fullmatch(preimage_sha256): + if not BARE_SHA256_PATTERN.fullmatch(preimage_sha256): raise ValueError("Goal deletion recovery preimage digest is invalid") if backup_path.parent != registry_path.parent or not ( backup_path.name.startswith(f"{registry_path.name}.goal-delete-") @@ -1180,7 +1180,7 @@ def delete_stopped_goal( normalized_goal_id = _require_opaque_id(goal_id, field="goal_id") requested_registry = Path(registry_path) normalized_fingerprint = str(expected_state_fingerprint or "").strip() or None - if normalized_fingerprint is not None and not _SHA256.fullmatch( + if normalized_fingerprint is not None and not BARE_SHA256_PATTERN.fullmatch( normalized_fingerprint ): raise ValueError("expected state fingerprint must be a SHA-256 digest") diff --git a/loopx/control_plane/goals/goal_amendment_proposal.py b/loopx/control_plane/goals/goal_amendment_proposal.py index 29f28cad0c..bb40ff6f1c 100644 --- a/loopx/control_plane/goals/goal_amendment_proposal.py +++ b/loopx/control_plane/goals/goal_amendment_proposal.py @@ -57,7 +57,6 @@ from __future__ import annotations import json -import re from collections.abc import Mapping from pathlib import Path from typing import Any @@ -82,6 +81,7 @@ autonomous_replan_is_required, autonomous_replan_scope_decision, ) +from ..content_digest import ENVELOPED_SHA256_PATTERN from .shared_goal_work_source import read_shared_goal_work_source from .shared_goal_alignment import ( DEFAULT_REGISTRY_RELATIVE_PATH, @@ -110,7 +110,6 @@ ) AMENDMENT_PROPOSAL_JOURNAL_DIRNAME = "amendment-proposals" AMENDMENT_PROPOSAL_JOURNAL_BASENAME = "journal.jsonl" -_SHA256_DIGEST_PATTERN = re.compile(r"^sha256:[0-9a-f]{64}$") def amendment_proposal_journal_path( @@ -441,7 +440,7 @@ def _check_admission_shape( != str(proposal.get("proposal_id") or "").strip().lower() or admission.get("base_revision_basis") != str(proposal.get("base_revision_basis") or "").strip() - or not _SHA256_DIGEST_PATTERN.fullmatch( + or not ENVELOPED_SHA256_PATTERN.fullmatch( str(admission.get("proposal_digest") or "") ) ): diff --git a/loopx/control_plane/projects/registry_codec.py b/loopx/control_plane/projects/registry_codec.py index a7899c3196..cfa40b37b5 100644 --- a/loopx/control_plane/projects/registry_codec.py +++ b/loopx/control_plane/projects/registry_codec.py @@ -9,10 +9,10 @@ import json import os from pathlib import Path -import re import tempfile from typing import Any, TypeVar +from ..content_digest import ENVELOPED_SHA256_PATTERN from ...file_lock import exclusive_cross_runtime_file_lock from ...paths import GLOBAL_REGISTRY_FILENAME @@ -27,7 +27,6 @@ "minimum_writer_protocol", "payload_sha256", } -_SHA256_PATTERN = re.compile(r"^sha256:[0-9a-f]{64}$") T = TypeVar("T") @@ -146,7 +145,7 @@ def _decode_document(raw_bytes: bytes) -> _ProjectRegistryDocument: "strict project registry minimum_writer_protocol must be nonempty" ) digest = header["payload_sha256"] - if not isinstance(digest, str) or not _SHA256_PATTERN.fullmatch(digest): + if not isinstance(digest, str) or not ENVELOPED_SHA256_PATTERN.fullmatch(digest): raise ProjectRegistryError( "strict project registry payload_sha256 is malformed" ) diff --git a/loopx/control_plane/testing/release_commit_qualification.py b/loopx/control_plane/testing/release_commit_qualification.py index 994369fad3..e85d7c5674 100644 --- a/loopx/control_plane/testing/release_commit_qualification.py +++ b/loopx/control_plane/testing/release_commit_qualification.py @@ -7,6 +7,7 @@ from pathlib import Path from typing import Any, Callable, Mapping +from ..content_digest import ENVELOPED_SHA256_PATTERN from ..runtime.public_safety import public_safe_compact_text from .actual_default_model_behavior_portfolio import ( ACTUAL_DEFAULT_MODEL_BEHAVIOR_CONTRAST_COUNT, @@ -45,7 +46,6 @@ } _HEX_ID_RE = re.compile(r"^[0-9a-f]{40}(?:[0-9a-f]{24})?$") -_DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$") _VERSION_RE = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+(?:[A-Za-z0-9.+-]*)?$") _TOKEN_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:@/+\-]{0,159}$") @@ -91,7 +91,7 @@ def _hex_id(value: Any, *, field: str) -> str: def _digest(value: Any, *, field: str) -> str: text = str(value or "").strip().lower() - if not _DIGEST_RE.fullmatch(text): + if not ENVELOPED_SHA256_PATTERN.fullmatch(text): raise ValueError(f"{field} must be a sha256 digest") return text diff --git a/loopx/control_plane/work_items/governed_transition_proposal.py b/loopx/control_plane/work_items/governed_transition_proposal.py index 6fca748ce3..82f9731ac0 100644 --- a/loopx/control_plane/work_items/governed_transition_proposal.py +++ b/loopx/control_plane/work_items/governed_transition_proposal.py @@ -19,6 +19,7 @@ ) from ..coordination.coordination_state_contract_generated import COORDINATION_STATE_CONTRACT from ..runtime.public_safety import validate_public_safe_value +from ..content_digest import ENVELOPED_SHA256_PATTERN from ..todos.contract import ( TODO_STATUS_DONE, TODO_STATUS_OPEN, @@ -57,7 +58,6 @@ } _LANE_TODO_ID_LIMIT = 8 _LANE_TODO_ID = re.compile(r"^todo_[A-Za-z0-9]{1,40}$") -_INTENT_BASIS = re.compile(r"^sha256:[0-9a-f]{64}$") _LANE_SETTLEMENT_FIELDS = { "lane_id", "agent_id", @@ -178,7 +178,7 @@ def validate_governed_transition_receipts( intent_basis = receipt.get("intent_basis") if intent_basis is not None and ( not isinstance(intent_basis, str) - or not _INTENT_BASIS.fullmatch(intent_basis) + or not ENVELOPED_SHA256_PATTERN.fullmatch(intent_basis) ): raise ValueError( "governed transition proposal receipt intent_basis is invalid" diff --git a/loopx/control_plane/work_items/progress_review_policy.py b/loopx/control_plane/work_items/progress_review_policy.py index b5c5f5a888..4448a5ccb6 100644 --- a/loopx/control_plane/work_items/progress_review_policy.py +++ b/loopx/control_plane/work_items/progress_review_policy.py @@ -11,8 +11,8 @@ from __future__ import annotations from collections.abc import Mapping -import re from typing import Any +from ..content_digest import BARE_SHA256_PATTERN PROGRESS_REVIEW_POLICY_SCHEMA_VERSION = "progress_review_policy_v0" PROGRESS_REVIEW_MODES: tuple[str, ...] = ("off", "shadow", "assist") @@ -22,7 +22,6 @@ PROGRESS_REVIEW_DEFAULT_DRIFT_THRESHOLD = 2 PROGRESS_REVIEW_MIN_DRIFT_THRESHOLD = 2 PROGRESS_REVIEW_MAX_DRIFT_THRESHOLD = 20 -_HEX64 = re.compile(r"^[a-f0-9]{64}$") def normalize_progress_review_mode(value: Any) -> str: @@ -70,7 +69,7 @@ def normalize_progress_review_contract_revision(value: Any) -> str | None: # An explicit empty value clears a pin at the change layer; the # effective policy reads it back as "no pin". return "" - if not _HEX64.fullmatch(text): + if not BARE_SHA256_PATTERN.fullmatch(text): raise ValueError( "progress_review.contract_revision must be a sha256 hex digest" ) diff --git a/loopx/extensions/openviking_semantic_preference/history_export.py b/loopx/extensions/openviking_semantic_preference/history_export.py index e37476d9c2..4c00829fd3 100644 --- a/loopx/extensions/openviking_semantic_preference/history_export.py +++ b/loopx/extensions/openviking_semantic_preference/history_export.py @@ -29,6 +29,7 @@ from pathlib import Path from typing import Any +from ...control_plane.content_digest import BARE_SHA256_PATTERN from ...control_plane.runtime.public_safety import public_safe_compact_text from ...history import collect_history, validate_goal_id_path_segment @@ -40,7 +41,6 @@ _MANIFEST_FILE = ".loopx-history-conclusion-export.json" _MANIFEST_SCHEMA_VERSION = "loopx_history_conclusion_export_manifest_v0" _SLUG_RE = re.compile(r"[^A-Za-z0-9._-]+") -_SHA256_RE = re.compile(r"^[a-f0-9]{64}$") # Bounded ISO-8601-ish timestamp: digits, T/space, colon, dot, +/- and Z only. _TIMESTAMP_RE = re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}[T ][0-9:.+\-]{1,20}Z?$") _PUBLIC_GOAL_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,120}$") @@ -162,7 +162,7 @@ def _read_owned_manifest(path: Path, *, goal_id: str) -> dict[str, str]: not name or Path(name).name != name or not name.endswith(".md") - or not _SHA256_RE.fullmatch(digest) + or not BARE_SHA256_PATTERN.fullmatch(digest) or name in owned ): raise RuntimeError("history export manifest contains an invalid file entry") diff --git a/loopx/extensions/presentation.py b/loopx/extensions/presentation.py index 4d8bb701a3..508b8de0c1 100644 --- a/loopx/extensions/presentation.py +++ b/loopx/extensions/presentation.py @@ -23,6 +23,7 @@ run_standalone_extension, ) from .process_runtime import run_capped_process +from ..control_plane.content_digest import BARE_SHA256_PATTERN from .readiness import ( CORE_VIEW_VALIDATORS, ResolvedRuntimeEntrypoint, @@ -44,7 +45,6 @@ _ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$") _ANCHOR_RE = re.compile(r"^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$") -_SHA256_RE = re.compile(r"^[0-9a-f]{64}$") _MARKUP_RE = re.compile(r"<[^>]*>|javascript:", re.IGNORECASE) _LOCAL_PATH_RE = re.compile( r"(?:^|[\s(])(?:~[/\\]|/+(?:Users|home|tmp|private|var|etc|opt)/|" @@ -313,7 +313,7 @@ def _evidence_reference(value: Any, *, context: str) -> str: def _sha256(value: Any, *, context: str) -> str: text = _plain_text(value, context=context, max_length=64) - if not _SHA256_RE.fullmatch(text): + if not BARE_SHA256_PATTERN.fullmatch(text): raise ValueError(f"{context} must be a lowercase SHA-256") return text diff --git a/tests/architecture/test_content_digest_single_owner.py b/tests/architecture/test_content_digest_single_owner.py new file mode 100644 index 0000000000..c08006b00a --- /dev/null +++ b/tests/architecture/test_content_digest_single_owner.py @@ -0,0 +1,414 @@ +"""One owner decides what a stored SHA-256 looks like, and this keeps it that way. + +Two halves, both required. The source scan catches a module that restates a +whole-value digest shape as its own literal even when the verdict is identical, +so an equal-by-accident copy still fails. The per-site cases catch a surface +wired to the wrong envelope. Neither half implies the other. + +Only whole-value shapes are owned here. A hex digest embedded in a larger +grammar (a ``cadence_…`` id, a journal filename, a ``40|64`` Git object id, a +compound cursor) answers that grammar's question and stays with its surface, and +producers that concatenate ``"sha256:"`` by hand are a separate decision. +""" + +from __future__ import annotations + +import ast +import re +from pathlib import Path +from typing import Any + +import pytest + +from loopx.control_plane import content_digest +from loopx.control_plane.content_digest import ( + BARE_SHA256_PATTERN, + ENVELOPED_SHA256_PATTERN, +) + +PACKAGE_ROOT = Path(__file__).resolve().parents[2] / "loopx" +OWNER_MODULE = "loopx/control_plane/content_digest.py" +HEX_CLASSES = ("[0-9a-f]", "[a-f0-9]") + +# Restatements this branch records instead of absorbing, each with the reason the +# reviewer needs. A new entry has to earn its place; an entry that stops being +# true fails the test below rather than ageing quietly. +DEFERRED_WHOLE_VALUE_SITES = { + "loopx/capabilities/content_ops/item_lifecycle.py": ( + "open PR #3313 is editing this file; migrating it here would collide, so " + "the copy stays until that PR lands" + ), + "loopx/capabilities/manager_context/inspection.py": ( + "the shape is a JSON-schema `pattern` string consumed by a schema " + "validator, not a compiled Python pattern; pinned equal to the owner below" + ), +} + +# Every module that now reads the owner instead of deciding for itself. +MIGRATED_SITE_MODULES: dict[str, tuple[str, ...]] = { + "loopx.capabilities.benchmark_toolkit.behavior_finding": ("BARE_SHA256_PATTERN",), + "loopx.capabilities.benchmark_toolkit.study_projection": ("BARE_SHA256_PATTERN",), + "loopx.capabilities.periodic_report.archive": ("ENVELOPED_SHA256_PATTERN",), + "loopx.capabilities.periodic_report.incremental": ("ENVELOPED_SHA256_PATTERN",), + "loopx.capabilities.periodic_report.machine_defaults": ( + "ENVELOPED_SHA256_PATTERN", + ), + "loopx.capabilities.progress_review.receipt": ("BARE_SHA256_PATTERN",), + "loopx.chat_action_normalization": ("BARE_SHA256_PATTERN",), + "loopx.configuration_transaction": ("ENVELOPED_SHA256_PATTERN",), + "loopx.control_plane.coordination.local_authority_shadow_outbox": ( + "ENVELOPED_SHA256_PATTERN", + ), + "loopx.control_plane.goals.activation_service": ("BARE_SHA256_PATTERN",), + "loopx.control_plane.goals.deletion_service": ("BARE_SHA256_PATTERN",), + "loopx.control_plane.goals.goal_amendment_proposal": ("ENVELOPED_SHA256_PATTERN",), + "loopx.control_plane.projects.registry_codec": ("ENVELOPED_SHA256_PATTERN",), + "loopx.control_plane.testing.release_commit_qualification": ( + "ENVELOPED_SHA256_PATTERN", + ), + "loopx.control_plane.work_items.governed_transition_proposal": ( + "ENVELOPED_SHA256_PATTERN", + ), + "loopx.control_plane.work_items.progress_review_policy": ("BARE_SHA256_PATTERN",), + "loopx.extensions.openviking_semantic_preference.history_export": ( + "BARE_SHA256_PATTERN", + ), + "loopx.extensions.presentation": ("BARE_SHA256_PATTERN",), +} + +HEX64 = "a" * 64 +MIXED_HEX64 = "0123456789abcdef" * 4 +ENVELOPED = f"sha256:{HEX64}" + +ENVELOPED_ACCEPTS = (ENVELOPED, f"sha256:{MIXED_HEX64}") +ENVELOPED_REJECTS = ( + HEX64, # the envelope is part of the stored shape + f"sha256:{HEX64[:-1]}", + f"sha256:{HEX64}0", + f"sha256:{HEX64.upper()}", + f"sha256:{'z' * 64}", + f"prefix-sha256:{HEX64}", + f"sha256:{HEX64} trailing", + "", +) +BARE_ACCEPTS = (HEX64, MIXED_HEX64, "f" * 64) +BARE_REJECTS = ( + ENVELOPED, + HEX64[:-1], + f"{HEX64}0", + HEX64.upper(), + "z" * 64, + f"x{HEX64}", + f"{HEX64} ", + "", + "sha256:" + HEX64[:-1], +) + + +def _regex_literals(module_path: Path) -> list[tuple[int, str]]: + tree = ast.parse(module_path.read_text(encoding="utf-8")) + found = [] + for node in ast.walk(tree): + if isinstance(node, ast.Constant) and isinstance(node.value, str): + if _whole_value_digest_shape(node.value) is not None: + found.append((node.lineno, node.value)) + return found + + +def _whole_value_digest_shape(text: str) -> str | None: + """Return the envelope for `^`[sha256:]`{64}`$`, else None. + + Exact on purpose: a literal with anything else in it is a different question + and belongs to the grammar that wrote it. + """ + + if not (text.startswith("^") and text.endswith("$") and text.endswith("{64}$")): + return None + body = text[1:-1] + enveloped = body.startswith("sha256:") + remainder = body[len("sha256:") :] if enveloped else body + for hex_class in HEX_CLASSES: + if remainder == f"{hex_class}{{64}}": + return "enveloped" if enveloped else "bare" + return None + + +def _whole_value_sites() -> dict[str, list[tuple[int, str]]]: + sites: dict[str, list[tuple[int, str]]] = {} + for path in sorted(PACKAGE_ROOT.rglob("*.py")): + if "__pycache__" in path.parts: + continue + relative = f"loopx/{path.relative_to(PACKAGE_ROOT).as_posix()}" + literals = _regex_literals(path) + if literals: + sites[relative] = literals + return sites + + +def test_only_the_owner_module_states_a_whole_value_digest_shape(): + unexpected = { + relative: literals + for relative, literals in _whole_value_sites().items() + if relative not in DEFERRED_WHOLE_VALUE_SITES and relative != OWNER_MODULE + } + assert not unexpected, f"second owner(s) of the digest shape: {unexpected}" + + +def test_owner_module_defines_each_shape_exactly_once(): + literals = _regex_literals(PACKAGE_ROOT / "control_plane" / "content_digest.py") + shapes = [_whole_value_digest_shape(text) for _, text in literals] + # Counted, not set-compared: a second literal of a shape already exported here + # would leave the set unchanged and this branch's whole point unsaid. + assert sorted(shapes) == ["bare", "enveloped"], literals + assert len(literals) == 2, literals + + +def test_deferred_sites_are_still_the_ones_this_branch_recorded(): + for relative, reason in DEFERRED_WHOLE_VALUE_SITES.items(): + assert isinstance(reason, str) and reason, relative + path = Path(__file__).resolve().parents[2] / relative + assert path.is_file(), f"{relative} moved or vanished; update the allowlist" + assert _regex_literals(path), f"{relative} no longer restates the shape" + + +def test_migrated_modules_hold_the_owner_object_not_an_equal_copy(): + import importlib + + for module_name, owned in MIGRATED_SITE_MODULES.items(): + module = importlib.import_module(module_name) + for attribute in owned: + assert getattr(module, attribute) is getattr(content_digest, attribute), ( + f"{module_name}.{attribute} is a second definition" + ) + + +@pytest.mark.parametrize("value", ENVELOPED_ACCEPTS) +def test_enveloped_pattern_accepts_a_prefixed_digest(value: str) -> None: + assert ENVELOPED_SHA256_PATTERN.fullmatch(value) is not None + + +@pytest.mark.parametrize("value", ENVELOPED_REJECTS) +def test_enveloped_pattern_rejects_every_other_shape(value: object) -> None: + assert ENVELOPED_SHA256_PATTERN.fullmatch(value) is None + + +@pytest.mark.parametrize("value", BARE_ACCEPTS) +def test_bare_pattern_accepts_lowercase_hex(value: str) -> None: + assert BARE_SHA256_PATTERN.fullmatch(value) is not None + + +@pytest.mark.parametrize("value", BARE_REJECTS) +def test_bare_pattern_rejects_everything_else(value: object) -> None: + assert BARE_SHA256_PATTERN.fullmatch(value) is None + + +@pytest.mark.parametrize("value", [HEX64, MIXED_HEX64, "A" * 64, "a" * 63, "g" * 64]) +def test_the_two_retired_bare_spellings_could_never_disagree(value: str) -> None: + """Merging `[a-f0-9]` into `[0-9a-f]` cannot change a verdict. + + The character class lists the same six letters and digits in a different + order, so both copies accepted and rejected the same strings. This is the + evidence that collapsing them is not a behaviour change. + """ + + first = re.compile(r"^[a-f0-9]{64}$") + second = re.compile(r"^[0-9a-f]{64}$") + assert bool(first.fullmatch(value)) == bool(second.fullmatch(value)) + assert bool(second.fullmatch(value)) == bool(BARE_SHA256_PATTERN.fullmatch(value)) + + +def test_schema_string_site_is_the_same_question_as_the_owner_bare_shape() -> None: + """`inspection.py` carries the shape as a JSON-schema string, not a pattern. + + It is deliberately not an f-string of the owner: the schema is data published + to callers. This asserts it still describes the bare hex64 envelope, so the + two cannot drift into different verdicts unnoticed. + """ + + from loopx.capabilities.manager_context import inspection + + literals = _regex_literals(Path(inspection.__file__)) + assert literals, "the recorded schema site no longer states the shape" + for _, text in literals: + assert _whole_value_digest_shape(text) == "bare" + declared = re.compile(text) + for value in (*BARE_ACCEPTS, *BARE_REJECTS): + assert bool(declared.fullmatch(value)) is bool( + BARE_SHA256_PATTERN.fullmatch(value) + ), value + + +# --- per-site wiring: every migrated surface is entered through its own reader -- + + +def test_periodic_report_archive_requires_the_envelope() -> None: + from loopx.capabilities.periodic_report import archive + + assert archive._sha256(ENVELOPED, "revision") == ENVELOPED + with pytest.raises(ValueError, match="must use sha256"): + archive._sha256(HEX64, "revision") + + +def test_periodic_report_incremental_requires_the_envelope() -> None: + from loopx.capabilities.periodic_report import incremental + + assert incremental._digest(ENVELOPED, "fact_fingerprint") == ENVELOPED + with pytest.raises(ValueError, match="must use sha256"): + incremental._digest(HEX64, "fact_fingerprint") + + +def test_progress_review_receipt_rejects_the_envelope_it_never_stored() -> None: + from loopx.capabilities.progress_review import receipt + + assert receipt._hex64(HEX64, field="basis_digest") == HEX64 + with pytest.raises(ValueError, match="must be a sha256 hex digest"): + receipt._hex64(ENVELOPED, field="basis_digest") + + +def test_presentation_extension_keeps_its_own_message_and_bare_shape() -> None: + from loopx.extensions import presentation + + assert presentation._sha256(HEX64, context="artifact") == HEX64 + with pytest.raises(ValueError, match="must be a lowercase SHA-256"): + presentation._sha256("z" * 64, context="artifact") + # This surface also caps the field at 64 characters, so an enveloped digest + # never reaches the shape check here. That limit is the surface's own policy + # and is left alone; it is why the rejected probe above is same-length. + with pytest.raises(ValueError, match="at most 64 characters"): + presentation._sha256(ENVELOPED, context="artifact") + + +def test_release_commit_qualification_normalises_before_the_owner_check() -> None: + from loopx.control_plane.testing import release_commit_qualification + + assert release_commit_qualification._digest(ENVELOPED, field="commit") == ENVELOPED + with pytest.raises(ValueError, match="must be a sha256 digest"): + release_commit_qualification._digest(HEX64, field="commit") + + +def test_configuration_revision_allows_the_absent_sentinel() -> None: + from loopx.configuration_transaction import _validated_revision + + assert _validated_revision("absent", label="revision") == "absent" + assert _validated_revision(ENVELOPED, label="revision") == ENVELOPED + with pytest.raises(ValueError, match="must be absent or a sha256 revision"): + _validated_revision(HEX64, label="revision") + + +def test_progress_review_policy_keeps_clearing_and_null_as_distinct_values() -> None: + from loopx.control_plane.work_items.progress_review_policy import ( + normalize_progress_review_contract_revision, + ) + + assert normalize_progress_review_contract_revision(None) is None + assert normalize_progress_review_contract_revision("") == "" + assert ( + normalize_progress_review_contract_revision(HEX64) == HEX64 + ) # positive control + with pytest.raises(ValueError, match="must be a sha256 hex digest"): + normalize_progress_review_contract_revision(ENVELOPED) + + +def test_deletion_source_basis_checks_both_digest_fields() -> None: + from loopx.control_plane.goals.deletion_service import ( + GOAL_DELETION_SOURCE_BASIS_SCHEMA_VERSION, + _normalize_source_basis, + ) + + basis: dict[str, Any] = { + "schema_version": GOAL_DELETION_SOURCE_BASIS_SCHEMA_VERSION, + "source_identity": HEX64, + "source_content_sha256": MIXED_HEX64, + "route_mode": "source_to_global", + } + assert _normalize_source_basis(basis)["source_content_sha256"] == MIXED_HEX64 + with pytest.raises(ValueError, match="source identity"): + _normalize_source_basis({**basis, "source_identity": ENVELOPED}) + with pytest.raises(ValueError, match="content digest"): + _normalize_source_basis({**basis, "source_content_sha256": HEX64.upper()}) + + +def test_periodic_report_delivery_authority_checks_its_effective_revision() -> None: + from loopx.capabilities.periodic_report.machine_defaults import ( + DELIVERY_AUTHORITY_SCHEMA, + normalize_periodic_report_delivery_authority, + ) + + authority = { + "schema_version": DELIVERY_AUTHORITY_SCHEMA, + "kind": "enabled_periodic_report_subscription", + "goal_id": "goal-1", + "source": "machine_default", + "effective_revision": ENVELOPED, + "route_ref": "route-1", + } + assert ( + normalize_periodic_report_delivery_authority(authority)["effective_revision"] + == ENVELOPED + ) + with pytest.raises(ValueError, match="effective_revision is invalid"): + normalize_periodic_report_delivery_authority( + {**authority, "effective_revision": HEX64} + ) + + +def test_governed_transition_receipt_checks_the_intent_basis_field_only() -> None: + """`proposal_digest` is not checked by this shape, so the probe names the field. + + An earlier draft of this case passed a bare `proposal_digest` and concluded + nothing; the migrated pattern guards the optional `intent_basis`. + """ + + from loopx.control_plane.work_items.governed_transition_proposal import ( + GOVERNED_TRANSITION_RECEIPT_SCHEMA_VERSION as RECEIPT_SCHEMA, + validate_governed_transition_receipts, + ) + + receipt = { + "schema_version": RECEIPT_SCHEMA, + "kind": "continuous_monitor_upsert", + "status": "committed", + "proposal_id": "prop-1", + "proposal_digest": ENVELOPED, + "action": "upsert", + "todo_id": "todo-1", + "monitor_key": "monitor-1", + "target_key": "target-1", + "intent_basis": ENVELOPED, + } + assert ( + validate_governed_transition_receipts([receipt])[0]["intent_basis"] == ENVELOPED + ) + with pytest.raises(ValueError, match="intent_basis is invalid"): + validate_governed_transition_receipts([{**receipt, "intent_basis": HEX64}]) + + +def test_shadow_outbox_cursor_keeps_its_envelope_and_its_absent_option() -> None: + from loopx.control_plane.coordination.local_authority_shadow_outbox import ( + DRAIN_CURSOR_SCHEMA, + OutboxError, + decode_cursor, + ) + from loopx.control_plane.coordination.local_authority_shadow_projection import ( + PARTITIONS, + ) + + def cursor(digest: object) -> dict[str, Any]: + return { + "schema_version": DRAIN_CURSOR_SCHEMA, + "partition": PARTITIONS[0], + "last_seq": 1, + "last_entry_id": f"local-shadow-tx-{HEX64}", + "last_partition_digest": digest, + "last_cursor": "cursor-opaque", + "last_provider_revision": "revision-opaque", + "updated_at": "2026-09-28T00:00:00+00:00", + } + + partition = PARTITIONS[0] + assert decode_cursor(cursor(ENVELOPED), partition=partition)["last_seq"] == 1 + assert ( + decode_cursor(cursor(None), partition=partition) is not None + ) # an unbound cursor is legal on this surface + with pytest.raises(OutboxError, match="cursor binding"): + decode_cursor(cursor(HEX64), partition=partition) From e8a7ee667947cb568dfd6b0459632388f21af867 Mon Sep 17 00:00:00 2001 From: karenchuu <25980598+karenchuu@users.noreply.github.com> Date: Mon, 28 Sep 2026 19:20:19 +0800 Subject: [PATCH 2/3] chore(registry): refresh the project registry IO census line anchors Removing each migrated module-level pattern constant deleted one line above the surviving registry I/O calls in activation_service.py, goal_amendment_proposal.py and registry_codec.py, so the tracked census recorded twelve call sites one line too low. Regenerate with the owning generator, the same way a87508553 and fb3c90b9b did for their imports. The thirteenth row is not this change's: loopx/contract.py::check_contract already reads line 1027 at base 6643f3670 while the census still says 1014, which is why test_project_registry_io_census and test_goal_instance_binding_inventory both fail on main at that commit. Signed-off-by: karenchuu <25980598+karenchuu@users.noreply.github.com> --- .../project_registry_io_manifest_v1.json | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/loopx/semantics/project_registry_io_manifest_v1.json b/loopx/semantics/project_registry_io_manifest_v1.json index 09c271ee70..c8b7b0487a 100644 --- a/loopx/semantics/project_registry_io_manifest_v1.json +++ b/loopx/semantics/project_registry_io_manifest_v1.json @@ -887,7 +887,7 @@ }, { "site": "loopx/contract.py::.check_contract::codec_read:load_registry#1", - "line": 1014, + "line": 1027, "column": 20, "kind": "codec_read", "api": "load_registry", @@ -975,7 +975,7 @@ }, { "site": "loopx/control_plane/goals/activation_service.py::._projected_source_identity::codec_read:load_registry#1", - "line": 91, + "line": 90, "column": 33, "kind": "codec_read", "api": "load_registry", @@ -983,7 +983,7 @@ }, { "site": "loopx/control_plane/goals/activation_service.py::._readback::codec_read:load_registry#1", - "line": 231, + "line": 230, "column": 15, "kind": "codec_read", "api": "load_registry", @@ -991,7 +991,7 @@ }, { "site": "loopx/control_plane/goals/activation_service.py::._readback::codec_read:load_registry#2", - "line": 234, + "line": 233, "column": 15, "kind": "codec_read", "api": "load_registry", @@ -999,7 +999,7 @@ }, { "site": "loopx/control_plane/goals/activation_service.py::._source_and_target::codec_read:load_registry#1", - "line": 172, + "line": 171, "column": 25, "kind": "codec_read", "api": "load_registry", @@ -1007,7 +1007,7 @@ }, { "site": "loopx/control_plane/goals/activation_service.py::._source_status::codec_read:load_registry#1", - "line": 156, + "line": 155, "column": 26, "kind": "codec_read", "api": "load_registry", @@ -1015,7 +1015,7 @@ }, { "site": "loopx/control_plane/goals/activation_service.py::.set_goal_activation_state::codec_read:load_registry#1", - "line": 286, + "line": 285, "column": 25, "kind": "codec_read", "api": "load_registry", @@ -1023,7 +1023,7 @@ }, { "site": "loopx/control_plane/goals/activation_service.py::.set_goal_activation_state::codec_transaction:project_registry_transaction#1", - "line": 381, + "line": 380, "column": 10, "kind": "codec_transaction", "api": "project_registry_transaction", @@ -1239,7 +1239,7 @@ }, { "site": "loopx/control_plane/goals/goal_amendment_proposal.py::.admit_goal_amendment_proposal::codec_read:load_registry#1", - "line": 199, + "line": 198, "column": 28, "kind": "codec_read", "api": "load_registry", @@ -1359,7 +1359,7 @@ }, { "site": "loopx/control_plane/projects/registry_codec.py::.add_project_registry_backend::codec_write:mutate_project_registry#1", - "line": 572, + "line": 571, "column": 12, "kind": "codec_write", "api": "mutate_project_registry", @@ -1367,7 +1367,7 @@ }, { "site": "loopx/control_plane/projects/registry_codec.py::.decode_registry_snapshot::codec_read:decode_project_registry#1", - "line": 207, + "line": 206, "column": 15, "kind": "codec_read", "api": "decode_project_registry", @@ -1375,7 +1375,7 @@ }, { "site": "loopx/control_plane/projects/registry_codec.py::.load_registry::codec_read:decode_registry_snapshot#1", - "line": 221, + "line": 220, "column": 12, "kind": "codec_read", "api": "decode_registry_snapshot", @@ -1383,7 +1383,7 @@ }, { "site": "loopx/control_plane/projects/registry_codec.py::.mutate_project_registry::codec_transaction:project_registry_transaction#1", - "line": 544, + "line": 543, "column": 10, "kind": "codec_transaction", "api": "project_registry_transaction", From 785f4d206a1bb41f4ebb84b066ce64111f8ca35d Mon Sep 17 00:00:00 2001 From: karenchuu <25980598+karenchuu@users.noreply.github.com> Date: Tue, 29 Sep 2026 00:59:53 +0800 Subject: [PATCH 3/3] refactor(control-plane): own the unanchored digest matchers too The review on this PR found that the guard proved less than it claimed: it recognised only literals written `^...$`, while `re.fullmatch(r"sha256:[0-9a-f]{64}", value)` states the identical decision with no anchors at all. That left 37 production rules in 25 modules outside the owner and let a wrong-envelope edit to one of them pass every case. The scan now reads usage as well as text: an unanchored literal inside `re.fullmatch`, a compiled `... \Z` form, and an anchored literal anywhere are all violations, and a synthetic case proves the context detector exists rather than being a literal search. A compound-grammar case asserts `cadence_...` stays out. Every one of those rules is now migrated rather than excused: 23 modules and 34 sites, including `content_ops/item_lifecycle.py`, whose only prior justification was that an open pull request touched the file. Six modules lost their last use of `re` and the import went with it. `presentation/chat_bundle.py` imports the owner absolutely because `scripts/chat_bundle.py` execs it by file path with no package context; a relative import there breaks that builder, which is what the collection error in `tests/presentation/test_chat_bundle.py` showed. The registry census is regenerated for the line moves, so `tests/architecture` is 869 passed / 0 failed against main's 818 passed / 2 failed; the 140 test files that import any migrated module are 2505 passed / 0 failed, and `mypy` plus `loopx check --scan-path` are clean. Signed-off-by: karenchuu <25980598+karenchuu@users.noreply.github.com> --- .../benchmark_toolkit/continuation.py | 4 +- .../benchmark_toolkit/factorial_contrast.py | 3 +- .../benchmark_toolkit/runtime_continuity.py | 4 +- .../content_ops/item_lifecycle.py | 3 +- .../issue_fix/reviewer_notification.py | 15 +- .../machine_configuration/store.py | 5 +- .../capabilities/manager_context/roundtrip.py | 4 +- .../capabilities/manager_context/tracking.py | 8 +- .../capabilities/periodic_report/adapters.py | 3 +- .../capabilities/periodic_report/bindings.py | 5 +- .../periodic_report/cadence_journal.py | 3 +- .../collaboration/delegation_inventory.py | 4 +- loopx/control_plane/collaboration/inbox.py | 7 +- loopx/control_plane/collaboration/peers.py | 3 +- .../coordination/shadow_management.py | 3 +- loopx/control_plane/effect_runtime.py | 3 +- .../control_plane/todos/completion_result.py | 3 +- .../todos/completion_transaction.py | 18 +- .../todos/completion_validation.py | 4 +- .../todos/completion_validation_store.py | 3 +- .../todos/machine_section_projection.py | 3 +- loopx/control_plane/work_items/task_lease.py | 3 +- loopx/presentation/chat_bundle.py | 5 +- .../project_registry_io_manifest_v1.json | 4 +- .../test_content_digest_single_owner.py | 298 ++++++++++-------- 25 files changed, 246 insertions(+), 172 deletions(-) diff --git a/loopx/capabilities/benchmark_toolkit/continuation.py b/loopx/capabilities/benchmark_toolkit/continuation.py index 4de013c661..d6296ecb6c 100644 --- a/loopx/capabilities/benchmark_toolkit/continuation.py +++ b/loopx/capabilities/benchmark_toolkit/continuation.py @@ -2,10 +2,10 @@ from __future__ import annotations -import re from collections.abc import Mapping from enum import Enum from typing import Any +from ...control_plane.content_digest import BARE_SHA256_PATTERN BENCHMARK_PUBLIC_PROGRESS_SCHEMA_VERSION = "benchmark_public_progress_v0" BENCHMARK_CONTINUATION_DECISION_SCHEMA_VERSION = "benchmark_continuation_decision_v0" @@ -34,7 +34,7 @@ def _non_negative_int(value: Any, *, field: str) -> int: def _sha256_digest(value: Any, *, field: str) -> str: text = str(value or "").strip().lower() - if not re.fullmatch(r"[0-9a-f]{64}", text): + if not BARE_SHA256_PATTERN.fullmatch(text): raise ValueError(f"{field} must be a lowercase SHA-256 digest") return text diff --git a/loopx/capabilities/benchmark_toolkit/factorial_contrast.py b/loopx/capabilities/benchmark_toolkit/factorial_contrast.py index f4e94458b4..96399d1af2 100644 --- a/loopx/capabilities/benchmark_toolkit/factorial_contrast.py +++ b/loopx/capabilities/benchmark_toolkit/factorial_contrast.py @@ -10,6 +10,7 @@ BENCHMARK_FOUR_ARM_CONTRACT_SCHEMA_VERSION, BENCHMARK_FOUR_ARM_QUALIFICATION_SCOPE, ) +from ...control_plane.content_digest import BARE_SHA256_PATTERN BENCHMARK_FACTORIAL_CONTRAST_SCHEMA_VERSION = "benchmark_factorial_contrast_v0" @@ -124,7 +125,7 @@ def _normalize_four_arm_design(contract: Mapping[str, Any]) -> dict[str, Any]: if arm_role != expected_role: raise ValueError("four-arm contract role does not match its factor cell") task_goal_sha256 = str(raw_arm.get("task_goal_sha256") or "").strip() - if not re.fullmatch(r"[0-9a-f]{64}", task_goal_sha256): + if not BARE_SHA256_PATTERN.fullmatch(task_goal_sha256): raise ValueError("four-arm task-goal hash must be sha256") arm = { "arm_id": arm_id, diff --git a/loopx/capabilities/benchmark_toolkit/runtime_continuity.py b/loopx/capabilities/benchmark_toolkit/runtime_continuity.py index 177e91efcd..4a30b8fadf 100644 --- a/loopx/capabilities/benchmark_toolkit/runtime_continuity.py +++ b/loopx/capabilities/benchmark_toolkit/runtime_continuity.py @@ -2,12 +2,12 @@ from __future__ import annotations -import re from enum import Enum from typing import Any +from ...control_plane.content_digest import BARE_SHA256_PATTERN BENCHMARK_RUNTIME_CONTINUITY_SCHEMA_VERSION = "benchmark_runtime_continuity_v0" -_SHA256_DIGEST = re.compile(r"[0-9a-f]{64}\Z") +_SHA256_DIGEST = BARE_SHA256_PATTERN class BenchmarkEventWindowState(str, Enum): diff --git a/loopx/capabilities/content_ops/item_lifecycle.py b/loopx/capabilities/content_ops/item_lifecycle.py index 262b619378..3653e83b85 100644 --- a/loopx/capabilities/content_ops/item_lifecycle.py +++ b/loopx/capabilities/content_ops/item_lifecycle.py @@ -18,6 +18,7 @@ CONTENT_OPS_QUEUE_PROJECTION_SCHEMA_VERSION, CONTENT_OPS_QUEUE_STATUS_PACKET_SCHEMA_VERSION, ) +from ...control_plane.content_digest import ENVELOPED_SHA256_PATTERN ALLOWED_ITEM_KINDS = {"article", "post", "profile_update", "reply", "repost"} ALLOWED_ITEM_STATES = { @@ -35,7 +36,7 @@ TERMINAL_STATES = {"readback_verified", "skipped", "superseded"} _TOKEN_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$") -_DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$") +_DIGEST_RE = ENVELOPED_SHA256_PATTERN _ITEM_KEYS = { "schema_version", "item_id", diff --git a/loopx/capabilities/issue_fix/reviewer_notification.py b/loopx/capabilities/issue_fix/reviewer_notification.py index 0766153668..f3d620b71e 100644 --- a/loopx/capabilities/issue_fix/reviewer_notification.py +++ b/loopx/capabilities/issue_fix/reviewer_notification.py @@ -15,6 +15,7 @@ reviewer_artifact_notification_gate, reviewer_notification_before_send_gate, ) +from ...control_plane.content_digest import ENVELOPED_SHA256_PATTERN ISSUE_FIX_REVIEWER_NOTIFICATION_SINKS_INPUT_SCHEMA_VERSION = ( @@ -122,7 +123,7 @@ def reviewer_notification_receipts_from_state( dict.fromkeys( str(value) for value in (values if isinstance(values, list) else []) - if re.fullmatch(r"sha256:[a-f0-9]{64}", str(value)) + if ENVELOPED_SHA256_PATTERN.fullmatch(str(value)) ) ) @@ -140,7 +141,7 @@ def reviewer_notification_queue_from_state( if ( value.get("schema_version") != ISSUE_FIX_REVIEWER_NOTIFICATION_QUEUE_RECEIPT_SCHEMA_VERSION - or not re.fullmatch(r"sha256:[a-f0-9]{64}", key) + or not ENVELOPED_SHA256_PATTERN.fullmatch(key) or key in seen ): continue @@ -164,7 +165,7 @@ def reviewer_notification_legacy_queue_from_state( if ( value.get("schema_version") != ISSUE_FIX_REVIEWER_NOTIFICATION_LEGACY_QUEUE_RECEIPT_SCHEMA_VERSION - or not re.fullmatch(r"sha256:[a-f0-9]{64}", key) + or not ENVELOPED_SHA256_PATTERN.fullmatch(key) or key in seen ): continue @@ -183,7 +184,7 @@ def with_reviewer_notification_state( ) for value in sinks_input.get("receipts") or []: text = str(value) - if re.fullmatch(r"sha256:[a-f0-9]{64}", text) and text not in merged_receipts: + if ENVELOPED_SHA256_PATTERN.fullmatch(text) and text not in merged_receipts: merged_receipts.append(text) queue = reviewer_notification_queue_from_state( @@ -517,7 +518,7 @@ def validate_issue_fix_reviewer_notification_sinks_result( errors.append(f"sink result {field} must be false") receipts = packet.get("receipts") if not isinstance(receipts, list) or any( - not re.fullmatch(r"sha256:[a-f0-9]{64}", str(value)) + not ENVELOPED_SHA256_PATTERN.fullmatch(str(value)) for value in (receipts if isinstance(receipts, list) else []) ): errors.append("receipts must contain only stable sha256 keys") @@ -535,7 +536,7 @@ def validate_issue_fix_reviewer_notification_sinks_result( if ( receipt.get("schema_version") != ISSUE_FIX_REVIEWER_NOTIFICATION_QUEUE_RECEIPT_SCHEMA_VERSION - or not re.fullmatch(r"sha256:[a-f0-9]{64}", key) + or not ENVELOPED_SHA256_PATTERN.fullmatch(key) or key in queued_keys or receipt.get("status") != "queued" or not public_safe_compact_text(receipt.get("sink_kind"), limit=50) @@ -693,7 +694,7 @@ def build_issue_fix_reviewer_notification_sinks_result( receipts = { str(value) for value in (raw_receipts if isinstance(raw_receipts, list) else []) - if re.fullmatch(r"sha256:[a-f0-9]{64}", str(value)) + if ENVELOPED_SHA256_PATTERN.fullmatch(str(value)) } semantic_history_pr_refs = { public_safe_compact_text(value, limit=300) diff --git a/loopx/capabilities/machine_configuration/store.py b/loopx/capabilities/machine_configuration/store.py index c804a91ce1..c9bee293e1 100644 --- a/loopx/capabilities/machine_configuration/store.py +++ b/loopx/capabilities/machine_configuration/store.py @@ -21,6 +21,7 @@ normalize_machine_configuration, project_machine_configuration, ) +from ...control_plane.content_digest import ENVELOPED_SHA256_PATTERN MACHINE_CONFIGURATION_UPDATE_PLAN_SCHEMA = "machine_configuration_update_plan_v0" @@ -346,8 +347,8 @@ def _read_transaction(runtime_root: Path, transaction_id: str) -> dict[str, Any] raise ValueError("machine-configuration transaction receipt is invalid") receipt["receipt_revision"] = receipt_revision applied_revision = str(receipt.get("applied_revision") or "") - if applied_revision != _MISSING_REVISION and not re.fullmatch( - r"sha256:[0-9a-f]{64}", applied_revision + if applied_revision != _MISSING_REVISION and not ( + ENVELOPED_SHA256_PATTERN.fullmatch(applied_revision) ): raise ValueError("machine-configuration transaction revision is invalid") return receipt diff --git a/loopx/capabilities/manager_context/roundtrip.py b/loopx/capabilities/manager_context/roundtrip.py index 21ca14c8bd..5012243c6b 100644 --- a/loopx/capabilities/manager_context/roundtrip.py +++ b/loopx/capabilities/manager_context/roundtrip.py @@ -7,7 +7,6 @@ from __future__ import annotations import logging -import re import threading from datetime import datetime, timezone, timedelta @@ -19,6 +18,7 @@ from ...control_plane.effect_runtime import EffectRuntimeRejected, effect_runtime_result from ...control_plane.collaboration.inbox import needs_conclusion as needs_conclusion +from ...control_plane.content_digest import BARE_SHA256_PATTERN PHASES = ("decision", "conclusion") DELIVERY_STATUSES = { @@ -247,7 +247,7 @@ def project_chat_return_deliveries(root, session_id, messages): if route.get("session_id") != session_id: continue request_id = str(route.get("request_id") or "") - if path.stem != request_id or not re.fullmatch(r"[a-f0-9]{64}", request_id): + if path.stem != request_id or not BARE_SHA256_PATTERN.fullmatch(request_id): continue route_message_ids = { "handoff." + _hash([request_id, phase]) for phase in PHASES diff --git a/loopx/capabilities/manager_context/tracking.py b/loopx/capabilities/manager_context/tracking.py index 004f82908b..bc82bd158f 100644 --- a/loopx/capabilities/manager_context/tracking.py +++ b/loopx/capabilities/manager_context/tracking.py @@ -14,6 +14,10 @@ from ...file_lock import exclusive_file_lock from ...todos import list_goal_todos from ...chat_manager_details import _text +from ...control_plane.content_digest import ( + BARE_SHA256_PATTERN, + ENVELOPED_SHA256_PATTERN, +) def _core_todos(registry_path, root, goal_id): @@ -33,7 +37,7 @@ def link(root, registry_path, goal_id, agent_id, request_id, todo_ids, evidence_ raise ValueError("too many context links") if any(not re.fullmatch(r"todo_[a-f0-9]{12}", x) for x in todo_ids): raise ValueError("invalid Core Todo id") - if any(not re.fullmatch(r"sha256:[a-f0-9]{64}", x) for x in evidence_ids): + if any(not ENVELOPED_SHA256_PATTERN.fullmatch(x) for x in evidence_ids): raise ValueError("evidence references must be opaque SHA256 identifiers") if todo_ids: rows = _core_todos(registry_path, root, goal_id) @@ -79,7 +83,7 @@ def query( limit=8, ): """External callers see only requests from their exact audience, never raw text.""" - if request_id is not None and not re.fullmatch(r"[a-f0-9]{64}", request_id): + if request_id is not None and not BARE_SHA256_PATTERN.fullmatch(request_id): raise ValueError("invalid context request id") if not owner_scope and not channel_id: raise ValueError("handoff audience required") diff --git a/loopx/capabilities/periodic_report/adapters.py b/loopx/capabilities/periodic_report/adapters.py index ff87b28b02..2888f67ce4 100644 --- a/loopx/capabilities/periodic_report/adapters.py +++ b/loopx/capabilities/periodic_report/adapters.py @@ -15,6 +15,7 @@ _SINK_STATUSES, _SOURCE_STATUSES, ) +from ...control_plane.content_digest import ENVELOPED_SHA256_PATTERN SOURCE_RESULT_SCHEMA = "periodic_report_source_result_v0" @@ -756,7 +757,7 @@ def _normalize_artifact_result( document_digest = _text( artifact.get("document_digest"), "artifact.document_digest", maximum=80 ) - if not re.fullmatch(r"sha256:[0-9a-f]{64}", document_digest): + if not ENVELOPED_SHA256_PATTERN.fullmatch(document_digest): raise ValueError("artifact.document_digest must use sha256") if expected_document is not None: expected_document_digest = ( diff --git a/loopx/capabilities/periodic_report/bindings.py b/loopx/capabilities/periodic_report/bindings.py index 82bbee9f9d..b9e34e09e6 100644 --- a/loopx/capabilities/periodic_report/bindings.py +++ b/loopx/capabilities/periodic_report/bindings.py @@ -16,6 +16,7 @@ _SINK_ROLES, _SINK_STATUSES, ) +from ...control_plane.content_digest import ENVELOPED_SHA256_PATTERN GENERATION_BUNDLE_SCHEMA = "periodic_report_generation_bundle_v0" GENERATION_RECEIPT_SCHEMA = "periodic_report_generation_receipt_v0" @@ -192,7 +193,7 @@ def _generation_receipt(raw: object) -> dict[str, Any]: document_digest = _text( receipt.get("document_digest"), "document_digest", maximum=80 ) - if not re.fullmatch(r"sha256:[0-9a-f]{64}", document_digest): + if not ENVELOPED_SHA256_PATTERN.fullmatch(document_digest): raise ValueError("generation_receipt.document_digest must use sha256") artifacts = _sequence(receipt.get("artifact_receipts"), "artifact_receipts") if not artifacts: @@ -209,7 +210,7 @@ def _generation_receipt(raw: object) -> dict[str, Any]: content_digest = _text( artifact.get("content_digest"), f"{label}.content_digest", maximum=80 ) - if not re.fullmatch(r"sha256:[0-9a-f]{64}", content_digest): + if not ENVELOPED_SHA256_PATTERN.fullmatch(content_digest): raise ValueError(f"{label}.content_digest must use sha256") normalized_artifacts.append( { diff --git a/loopx/capabilities/periodic_report/cadence_journal.py b/loopx/capabilities/periodic_report/cadence_journal.py index 629b8648a7..26bb1bf368 100644 --- a/loopx/capabilities/periodic_report/cadence_journal.py +++ b/loopx/capabilities/periodic_report/cadence_journal.py @@ -16,6 +16,7 @@ from ...file_lock import LockAcquisitionPolicy, exclusive_file_lock from ...registry import atomic_write_json from .cadence import report_cadence_window +from ...control_plane.content_digest import ENVELOPED_SHA256_PATTERN CADENCE_WINDOW_SCHEMA = "periodic_report_cadence_window_v0" JOURNAL_SCHEMA = "periodic_report_cadence_journal_v0" @@ -47,7 +48,7 @@ def validate_cadence_window(raw: object) -> dict[str, Any]: for key in ("goal_id", "agent_id") ): raise ValueError("cadence window identity is invalid") - if not re.fullmatch(r"sha256:[0-9a-f]{64}", str(value["subscription_revision"])): + if not ENVELOPED_SHA256_PATTERN.fullmatch(str(value["subscription_revision"])): raise ValueError("cadence subscription revision is invalid") if not isinstance(value["profile_ref"], Mapping) or not isinstance(value["trigger_policy"], Mapping): raise ValueError("cadence profile facts are invalid") diff --git a/loopx/control_plane/collaboration/delegation_inventory.py b/loopx/control_plane/collaboration/delegation_inventory.py index 193f13cf25..66bd66a9f2 100644 --- a/loopx/control_plane/collaboration/delegation_inventory.py +++ b/loopx/control_plane/collaboration/delegation_inventory.py @@ -2,12 +2,12 @@ from __future__ import annotations import heapq -import re from typing import TYPE_CHECKING from ..effect_runtime import EffectRuntimeRemoteError, effect_runtime_result from .inbox import _read from .peers import _goal, require_operation_id +from ..content_digest import BARE_SHA256_PATTERN if TYPE_CHECKING: from ...collaboration_mcp import Delegations @@ -26,7 +26,7 @@ def addresses(): for path in entries: if path.suffix != ".json": continue - if not re.fullmatch(r"[a-f0-9]{64}", path.stem): + if not BARE_SHA256_PATTERN.fullmatch(path.stem): raise ValueError("unexpected delegation record address; reconcile inventory storage") if query["cursor"] is None or path.stem > query["cursor"]: yield path.stem diff --git a/loopx/control_plane/collaboration/inbox.py b/loopx/control_plane/collaboration/inbox.py index 8f845a9c8e..8f45bd38d9 100644 --- a/loopx/control_plane/collaboration/inbox.py +++ b/loopx/control_plane/collaboration/inbox.py @@ -15,6 +15,7 @@ from pathlib import Path from typing import Any from ...file_lock import exclusive_file_lock +from ..content_digest import BARE_SHA256_PATTERN ENTRY_SCHEMA = "loopx_manager_context_entry_v1" REQUEST_TRIAGE_INSTRUCTION = ( @@ -98,7 +99,7 @@ def pending( for path in paths: if path.suffix != ".json": continue - if not re.fullmatch(r"[a-f0-9]{64}", path.stem): + if not BARE_SHA256_PATTERN.fullmatch(path.stem): raise ValueError("invalid context request filename") if path.stem <= after: continue @@ -147,7 +148,7 @@ def acknowledge( decision: str, reason: str, ) -> dict: - if not re.fullmatch(r"[a-f0-9]{64}", request_id): + if not BARE_SHA256_PATTERN.fullmatch(request_id): raise ValueError("invalid context request id") target = dict(goal_id=goal_id, agent_id=agent_id) entry = _read( @@ -177,7 +178,7 @@ def _now(): def _entry(root, goal_id, agent_id, request_id): - if not isinstance(request_id, str) or not re.fullmatch(r"[a-f0-9]{64}", request_id): + if not isinstance(request_id, str) or not BARE_SHA256_PATTERN.fullmatch(request_id): raise ValueError("invalid context request id") target = dict(goal_id=goal_id, agent_id=agent_id) row = _read(_root(root) / "entries" / _hash(target) / (request_id + ".json")) diff --git a/loopx/control_plane/collaboration/peers.py b/loopx/control_plane/collaboration/peers.py index b8d338d90b..93163f1586 100644 --- a/loopx/control_plane/collaboration/peers.py +++ b/loopx/control_plane/collaboration/peers.py @@ -19,6 +19,7 @@ from ...file_lock import exclusive_file_lock from ...history import load_registry from ...thread_agent_binding import resolve_thread_agent_binding +from ..content_digest import BARE_SHA256_PATTERN PEER_INSTRUCTION = ( "This is a peer's request for help or independent review, not an owner instruction. " @@ -298,7 +299,7 @@ def consume_return(root, goal_id, agent_id, request_id): def _request_id(value): - if not isinstance(value, str) or not re.fullmatch(r"[a-f0-9]{64}", value): + if not isinstance(value, str) or not BARE_SHA256_PATTERN.fullmatch(value): raise ValueError("invalid context request id") return value diff --git a/loopx/control_plane/coordination/shadow_management.py b/loopx/control_plane/coordination/shadow_management.py index 445b66d6e2..e803f42d73 100644 --- a/loopx/control_plane/coordination/shadow_management.py +++ b/loopx/control_plane/coordination/shadow_management.py @@ -17,9 +17,10 @@ SHADOW_MANAGEMENT_MANIFEST_SCHEMA, SHADOW_MANAGEMENT_STATE_SCHEMA, ) from .local_authority_shadow_projection import sha256_digest +from ..content_digest import ENVELOPED_SHA256_PATTERN SHADOW_CAPTURE_PROFILE = "file_outbox_v1" -_DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z") +_DIGEST = ENVELOPED_SHA256_PATTERN _STATE_KEYS = { "schema_version", "goal_id", "source_root_digest", "status", "binding", "operation", "previous_operation_id", "result", diff --git a/loopx/control_plane/effect_runtime.py b/loopx/control_plane/effect_runtime.py index ed9ec18316..2c256983c9 100644 --- a/loopx/control_plane/effect_runtime.py +++ b/loopx/control_plane/effect_runtime.py @@ -21,6 +21,7 @@ from typing import IO, Any from ..file_lock import process_is_alive +from .content_digest import BARE_SHA256_PATTERN EFFECT_RUNTIME_REQUEST_SCHEMA_VERSION = "loopx_effect_runtime_request_v0" EFFECT_RUNTIME_RESPONSE_SCHEMA_VERSION = "loopx_effect_runtime_response_v1" @@ -662,7 +663,7 @@ def _read_local_snapshot_response( size, digest = ref.get("byte_count"), ref.get("sha256") if (not isinstance(size, int) or isinstance(size, bool) or size <= 0 or size > MAX_LOCAL_SNAPSHOT_BYTES or not isinstance(digest, str) - or re.fullmatch(r"[a-f0-9]{64}", digest) is None): + or BARE_SHA256_PATTERN.fullmatch(digest) is None): raise ValueError("invalid local snapshot reference") descriptor = os.open(sink, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)) with os.fdopen(descriptor, "rb") as file: diff --git a/loopx/control_plane/todos/completion_result.py b/loopx/control_plane/todos/completion_result.py index a0c31878b7..59abfe1deb 100644 --- a/loopx/control_plane/todos/completion_result.py +++ b/loopx/control_plane/todos/completion_result.py @@ -14,10 +14,11 @@ import tempfile from pathlib import Path from typing import Any +from ..content_digest import BARE_SHA256_PATTERN MAX_RESULT_BYTES = 128_000 _CONTENT_TYPES = {".json": "application/json", ".md": "text/markdown", ".txt": "text/plain"} -_DIGEST = re.compile(r"[a-f0-9]{64}\Z") +_DIGEST = BARE_SHA256_PATTERN def _object_path(runtime_root: Path, goal_id: str, digest: str) -> Path: diff --git a/loopx/control_plane/todos/completion_transaction.py b/loopx/control_plane/todos/completion_transaction.py index 3f550fa7dc..221eb0c827 100644 --- a/loopx/control_plane/todos/completion_transaction.py +++ b/loopx/control_plane/todos/completion_transaction.py @@ -2,12 +2,12 @@ from __future__ import annotations -import re from collections.abc import Mapping from typing import Any from ..effect_runtime import EffectRuntimeRejected, effect_runtime_result from .contract import normalize_todo_id_list +from ..content_digest import BARE_SHA256_PATTERN TODO_COMPLETION_TRANSACTION_REQUEST_SCHEMA = "loopx_todo_completion_transaction_v0" @@ -289,10 +289,10 @@ def _valid_receipt(value: Any) -> bool: value.get("validation_declaration_sha256") is None or ( isinstance(value.get("validation_declaration_sha256"), str) - and re.fullmatch( - r"[a-f0-9]{64}", - value.get("validation_declaration_sha256"), - ) is not None + and BARE_SHA256_PATTERN.fullmatch( + value.get("validation_declaration_sha256") + ) + is not None ) ) ) @@ -404,10 +404,10 @@ def _valid_execute_validation_result(result: Mapping[str, Any]) -> bool: effect.get("validation_declaration_sha256") is None or ( isinstance(effect.get("validation_declaration_sha256"), str) - and re.fullmatch( - r"[a-f0-9]{64}", - effect.get("validation_declaration_sha256"), - ) is not None + and BARE_SHA256_PATTERN.fullmatch( + effect.get("validation_declaration_sha256") + ) + is not None ) ) ) diff --git a/loopx/control_plane/todos/completion_validation.py b/loopx/control_plane/todos/completion_validation.py index 85ac24563a..1284b96972 100644 --- a/loopx/control_plane/todos/completion_validation.py +++ b/loopx/control_plane/todos/completion_validation.py @@ -1,6 +1,5 @@ from __future__ import annotations -import re import subprocess from collections.abc import Mapping from json import loads as json_loads @@ -36,6 +35,7 @@ read_completion_validation_declaration, ) from .contract import TODO_STATUS_DONE, normalize_todo_status +from ..content_digest import BARE_SHA256_PATTERN # Kept safely under the 30s outer CLI/MCP subprocess budget so a timed-out # validation still produces a typed receipt before the outer call is killed. @@ -383,7 +383,7 @@ def run_declared_completion_validation_effect( declaration_digest = effect.get("validation_declaration_sha256") if declaration_digest is not None and ( not isinstance(declaration_digest, str) - or not re.fullmatch(r"[a-f0-9]{64}", declaration_digest) + or not BARE_SHA256_PATTERN.fullmatch(declaration_digest) ): raise ValueError( "validation_effect.validation_declaration_sha256 must be a SHA-256 digest" diff --git a/loopx/control_plane/todos/completion_validation_store.py b/loopx/control_plane/todos/completion_validation_store.py index 9b76e2fca8..8a8681c83d 100644 --- a/loopx/control_plane/todos/completion_validation_store.py +++ b/loopx/control_plane/todos/completion_validation_store.py @@ -15,6 +15,7 @@ completion_validation_declaration, completion_validation_declaration_sha256, ) +from ..content_digest import BARE_SHA256_PATTERN DECLARATION_SCHEMA_VERSION = "loopx_todo_completion_validation_declaration_v0" @@ -101,7 +102,7 @@ def prepare_completion_validation_declaration( def _read_prepared_declaration(path: Path, goal_id: str, digest: str) -> dict[str, Any] | None: - if not re.fullmatch(r"[a-f0-9]{64}", digest): + if not BARE_SHA256_PATTERN.fullmatch(digest): raise ValueError("canonical validation digest must be SHA-256") try: value = read_json(path.parent / "blobs" / f"{digest}.json") diff --git a/loopx/control_plane/todos/machine_section_projection.py b/loopx/control_plane/todos/machine_section_projection.py index bf167d20ee..9a28f3b01f 100644 --- a/loopx/control_plane/todos/machine_section_projection.py +++ b/loopx/control_plane/todos/machine_section_projection.py @@ -51,6 +51,7 @@ todo_marker_for_status, ) from .todo_summary import canonical_todo_read_record, todo_priority_parts, normalize_todo_text +from ..content_digest import BARE_SHA256_PATTERN TODO_SECTION_PROJECTION_SCHEMA_VERSION = "loopx_todo_section_projection_v0" @@ -459,7 +460,7 @@ def render_canonical_todo_sections( f"Todo {todo_id!r} has a validation digest without authority" ) continue - if not isinstance(digest, str) or not re.fullmatch(r"[a-f0-9]{64}", digest): + if not isinstance(digest, str) or not BARE_SHA256_PATTERN.fullmatch(digest): raise TodoSectionProjectionError( f"Todo {todo_id!r} requires validation but omits its declaration digest" ) diff --git a/loopx/control_plane/work_items/task_lease.py b/loopx/control_plane/work_items/task_lease.py index bc585f8a16..6dae0c4d8d 100644 --- a/loopx/control_plane/work_items/task_lease.py +++ b/loopx/control_plane/work_items/task_lease.py @@ -44,6 +44,7 @@ require_expected_version as require_expected_version, write_lease as write_lease, ) +from ..content_digest import BARE_SHA256_PATTERN DEFAULT_TASK_LEASE_TTL_SECONDS = 45 * 60 MAX_TASK_LEASE_TTL_SECONDS = 24 * 60 * 60 @@ -147,7 +148,7 @@ def _native_fence_payload( operation_id = raw.get("fence_operation_id") if operation_id is not None and ( not isinstance(operation_id, str) - or not re.fullmatch(r"[a-f0-9]{64}", operation_id) + or not BARE_SHA256_PATTERN.fullmatch(operation_id) ): raise TaskLeaseError( f"native task-lease {operation} result has an invalid fence operation id", diff --git a/loopx/presentation/chat_bundle.py b/loopx/presentation/chat_bundle.py index c8dd04c971..55c35f6138 100644 --- a/loopx/presentation/chat_bundle.py +++ b/loopx/presentation/chat_bundle.py @@ -6,6 +6,9 @@ import json import re from pathlib import Path, PurePosixPath +# Absolute by necessity: scripts/chat_bundle.py execs this module by file path, so +# it has no package context for a relative import. +from loopx.control_plane.content_digest import BARE_SHA256_PATTERN MANIFEST = "bundle-manifest.json" CHAT_BUNDLE_SCHEMA_VERSION = "loopx_chat_bundle_v1" @@ -108,7 +111,7 @@ def validate_bundle(bundle: Path, *, source_root: Path | None = None) -> dict: if ( not safe_relative(name) or not isinstance(expected, str) - or not re.fullmatch(r"[0-9a-f]{64}", expected) + or not BARE_SHA256_PATTERN.fullmatch(expected) ): raise ValueError("invalid bundle witness") path = bundle / name diff --git a/loopx/semantics/project_registry_io_manifest_v1.json b/loopx/semantics/project_registry_io_manifest_v1.json index c8b7b0487a..e4a3cab68b 100644 --- a/loopx/semantics/project_registry_io_manifest_v1.json +++ b/loopx/semantics/project_registry_io_manifest_v1.json @@ -903,7 +903,7 @@ }, { "site": "loopx/control_plane/collaboration/peers.py::._goal::codec_read:load_registry#1", - "line": 35, + "line": 36, "column": 21, "kind": "codec_read", "api": "load_registry", @@ -1511,7 +1511,7 @@ }, { "site": "loopx/control_plane/work_items/task_lease.py::.runtime_root_from_registry::codec_read:load_registry#1", - "line": 563, + "line": 564, "column": 16, "kind": "codec_read", "api": "load_registry", diff --git a/tests/architecture/test_content_digest_single_owner.py b/tests/architecture/test_content_digest_single_owner.py index c08006b00a..50c3ebed13 100644 --- a/tests/architecture/test_content_digest_single_owner.py +++ b/tests/architecture/test_content_digest_single_owner.py @@ -1,19 +1,25 @@ """One owner decides what a stored SHA-256 looks like, and this keeps it that way. -Two halves, both required. The source scan catches a module that restates a -whole-value digest shape as its own literal even when the verdict is identical, -so an equal-by-accident copy still fails. The per-site cases catch a surface -wired to the wrong envelope. Neither half implies the other. - -Only whole-value shapes are owned here. A hex digest embedded in a larger -grammar (a ``cadence_…`` id, a journal filename, a ``40|64`` Git object id, a -compound cursor) answers that grammar's question and stays with its surface, and -producers that concatenate ``"sha256:"`` by hand are a separate decision. +Three halves, none of which substitutes for the others: + +* a **scan** that fails when any module states a whole-value digest shape itself. It + unions two detectors, because `re.fullmatch(r"[0-9a-f]{64}", value)` states exactly + the same decision as `^...$` while carrying no anchors at all, and an anchored-text + search alone would be blind to it; +* an **identity** check that every consumer holds the owner object rather than a copy; +* **per-surface cases** that enter through each surface's own reader, which is the only + half that can notice a surface wired to the wrong envelope. + +Only whole-value shapes are owned. A hex digest inside a larger grammar (a `cadence_…` +id, a journal filename, a `40|64` Git object id, a compound cursor) answers that +grammar's question and stays with its surface, and producers that concatenate +`"sha256:"` by hand are a separate decision. """ from __future__ import annotations import ast +import importlib import re from pathlib import Path from typing import Any @@ -30,52 +36,17 @@ OWNER_MODULE = "loopx/control_plane/content_digest.py" HEX_CLASSES = ("[0-9a-f]", "[a-f0-9]") -# Restatements this branch records instead of absorbing, each with the reason the -# reviewer needs. A new entry has to earn its place; an entry that stops being -# true fails the test below rather than ageing quietly. -DEFERRED_WHOLE_VALUE_SITES = { - "loopx/capabilities/content_ops/item_lifecycle.py": ( - "open PR #3313 is editing this file; migrating it here would collide, so " - "the copy stays until that PR lands" - ), +# Recorded instead of absorbed, each with a reason that stands on the field contract +# rather than on which other pull request happens to be open. An entry that stops being +# true fails the test below instead of ageing quietly. +DEFERRED_WHOLE_VALUE_SITES: dict[str, str] = { "loopx/capabilities/manager_context/inspection.py": ( - "the shape is a JSON-schema `pattern` string consumed by a schema " - "validator, not a compiled Python pattern; pinned equal to the owner below" + "published as a JSON-schema `pattern` string, so it is schema data handed to a " + "validator rather than a matcher this owner may replace; pinned verdict for " + "verdict to the owner instead" ), } -# Every module that now reads the owner instead of deciding for itself. -MIGRATED_SITE_MODULES: dict[str, tuple[str, ...]] = { - "loopx.capabilities.benchmark_toolkit.behavior_finding": ("BARE_SHA256_PATTERN",), - "loopx.capabilities.benchmark_toolkit.study_projection": ("BARE_SHA256_PATTERN",), - "loopx.capabilities.periodic_report.archive": ("ENVELOPED_SHA256_PATTERN",), - "loopx.capabilities.periodic_report.incremental": ("ENVELOPED_SHA256_PATTERN",), - "loopx.capabilities.periodic_report.machine_defaults": ( - "ENVELOPED_SHA256_PATTERN", - ), - "loopx.capabilities.progress_review.receipt": ("BARE_SHA256_PATTERN",), - "loopx.chat_action_normalization": ("BARE_SHA256_PATTERN",), - "loopx.configuration_transaction": ("ENVELOPED_SHA256_PATTERN",), - "loopx.control_plane.coordination.local_authority_shadow_outbox": ( - "ENVELOPED_SHA256_PATTERN", - ), - "loopx.control_plane.goals.activation_service": ("BARE_SHA256_PATTERN",), - "loopx.control_plane.goals.deletion_service": ("BARE_SHA256_PATTERN",), - "loopx.control_plane.goals.goal_amendment_proposal": ("ENVELOPED_SHA256_PATTERN",), - "loopx.control_plane.projects.registry_codec": ("ENVELOPED_SHA256_PATTERN",), - "loopx.control_plane.testing.release_commit_qualification": ( - "ENVELOPED_SHA256_PATTERN", - ), - "loopx.control_plane.work_items.governed_transition_proposal": ( - "ENVELOPED_SHA256_PATTERN", - ), - "loopx.control_plane.work_items.progress_review_policy": ("BARE_SHA256_PATTERN",), - "loopx.extensions.openviking_semantic_preference.history_export": ( - "BARE_SHA256_PATTERN", - ), - "loopx.extensions.presentation": ("BARE_SHA256_PATTERN",), -} - HEX64 = "a" * 64 MIXED_HEX64 = "0123456789abcdef" * 4 ENVELOPED = f"sha256:{HEX64}" @@ -101,30 +72,25 @@ f"x{HEX64}", f"{HEX64} ", "", - "sha256:" + HEX64[:-1], + f"sha256:{HEX64[:-1]}", ) -def _regex_literals(module_path: Path) -> list[tuple[int, str]]: - tree = ast.parse(module_path.read_text(encoding="utf-8")) - found = [] - for node in ast.walk(tree): - if isinstance(node, ast.Constant) and isinstance(node.value, str): - if _whole_value_digest_shape(node.value) is not None: - found.append((node.lineno, node.value)) - return found - - -def _whole_value_digest_shape(text: str) -> str | None: - """Return the envelope for `^`[sha256:]`{64}`$`, else None. +def _whole_value_shape(text: Any) -> str | None: + """Classify a literal as a whole-value digest shape, ignoring how it is anchored. - Exact on purpose: a literal with anything else in it is a different question - and belongs to the grammar that wrote it. + Anchoring is the call's business: `re.fullmatch` gives whole-string semantics to an + unanchored literal and `\\Z` is equivalent to `$`. Anything carrying extra grammar + - a prefix, an alternation, a suffix - answers a different question and is skipped. """ - if not (text.startswith("^") and text.endswith("$") and text.endswith("{64}$")): + if not isinstance(text, str) or "{64}" not in text: return None - body = text[1:-1] + body = text[1:] if text.startswith("^") else text + for tail in ("$", "\\Z"): + if body.endswith(tail): + body = body[: -len(tail)] + break enveloped = body.startswith("sha256:") remainder = body[len("sha256:") :] if enveloped else body for hex_class in HEX_CLASSES: @@ -133,53 +99,140 @@ def _whole_value_digest_shape(text: str) -> str | None: return None -def _whole_value_sites() -> dict[str, list[tuple[int, str]]]: - sites: dict[str, list[tuple[int, str]]] = {} - for path in sorted(PACKAGE_ROOT.rglob("*.py")): - if "__pycache__" in path.parts: +def _module_sites(path: Path) -> list[tuple[int, str, str]]: + """Every whole-value digest literal this module states, and how it became one.""" + + tree = ast.parse(path.read_text(encoding="utf-8")) + found: dict[int, tuple[str, str]] = {} + + for node in ast.walk(tree): + if not isinstance(node, ast.Call) or not node.args: + continue + attribute = getattr(node.func, "attr", None) + receiver = getattr(getattr(node.func, "value", None), "id", None) + first = node.args[0] + if attribute not in {"compile", "fullmatch"}: continue - relative = f"loopx/{path.relative_to(PACKAGE_ROOT).as_posix()}" - literals = _regex_literals(path) - if literals: - sites[relative] = literals - return sites + if not isinstance(first, ast.Constant) or not isinstance(first.value, str): + continue + shape = _whole_value_shape(first.value) + if shape is None: + continue + if attribute == "compile": + anchored = first.value.startswith("^") and first.value.endswith(("$", "\\Z")) + if anchored: + found[first.lineno] = (first.value, "compiled whole-value pattern") + elif receiver == "re": + found[first.lineno] = (first.value, "re.fullmatch literal") + + for node in ast.walk(tree): + if ( + isinstance(node, ast.Constant) + and isinstance(node.value, str) + and _whole_value_shape(node.value) + and node.value.startswith("^") + and node.value.endswith("$") + ): + found.setdefault(node.lineno, (node.value, "anchored literal")) + + return sorted((line, literal, how) for line, (literal, how) in found.items()) + + +def _scan_modules() -> list[Path]: + return sorted( + path + for path in PACKAGE_ROOT.rglob("*.py") + if "__pycache__" not in path.parts + ) + + +def _relative(path: Path) -> str: + return f"loopx/{path.relative_to(PACKAGE_ROOT).as_posix()}" + + +def _consumer_modules() -> list[str]: + """Module names of every file that imports the owner - derived, not listed.""" + + consumers = [] + for path in _scan_modules(): + source = path.read_text(encoding="utf-8") + if "content_digest import" not in source: + continue + parts = list(path.relative_to(PACKAGE_ROOT.parent).parts) + parts[-1] = parts[-1][: -len(".py")] + consumers.append(".".join(parts)) + return sorted(consumers) def test_only_the_owner_module_states_a_whole_value_digest_shape(): - unexpected = { - relative: literals - for relative, literals in _whole_value_sites().items() - if relative not in DEFERRED_WHOLE_VALUE_SITES and relative != OWNER_MODULE - } - assert not unexpected, f"second owner(s) of the digest shape: {unexpected}" + offenders = {} + for path in _scan_modules(): + relative = _relative(path) + if relative in DEFERRED_WHOLE_VALUE_SITES or relative == OWNER_MODULE: + continue + sites = _module_sites(path) + if sites: + offenders[relative] = sites + assert not offenders, f"second owner(s) of the digest shape: {offenders}" def test_owner_module_defines_each_shape_exactly_once(): - literals = _regex_literals(PACKAGE_ROOT / "control_plane" / "content_digest.py") - shapes = [_whole_value_digest_shape(text) for _, text in literals] - # Counted, not set-compared: a second literal of a shape already exported here - # would leave the set unchanged and this branch's whole point unsaid. - assert sorted(shapes) == ["bare", "enveloped"], literals - assert len(literals) == 2, literals + sites = _module_sites(PACKAGE_ROOT / "control_plane" / "content_digest.py") + shapes = [_whole_value_shape(literal) for _, literal, _ in sites] + # Counted, not set-compared: a second literal of an already-exported shape would + # leave the set unchanged and leave this branch's whole point unsaid. + assert sorted(shapes) == ["bare", "enveloped"], sites + assert len(sites) == 2, sites + + +def test_the_scan_reads_usage_not_only_anchor_text(tmp_path: Path) -> None: + """A `re.fullmatch` copy with no anchors at all must still be a violation. + + Without this case the scan cannot be told apart from a plain literal search, which + is precisely how several production sites were written before this branch. + """ + + sample = tmp_path / "loopx" / "restated.py" + sample.parent.mkdir(parents=True) + sample.write_text( + "import re\n\n\ndef check(value):\n" + ' return re.fullmatch(r"[0-9a-f]{64}", value)\n', + encoding="utf-8", + ) + assert _module_sites(sample), "an unanchored whole-value restatement escaped the scan" + + grammar = tmp_path / "loopx" / "grammar.py" + grammar.write_text( + "import re\n\n\nPATTERN = re.compile(r\"cadence_[0-9a-f]{64}\")\n", + encoding="utf-8", + ) + assert not _module_sites(grammar), "a compound id must not be pulled into the owner" def test_deferred_sites_are_still_the_ones_this_branch_recorded(): for relative, reason in DEFERRED_WHOLE_VALUE_SITES.items(): - assert isinstance(reason, str) and reason, relative - path = Path(__file__).resolve().parents[2] / relative + assert reason, relative + path = PACKAGE_ROOT.parent / relative assert path.is_file(), f"{relative} moved or vanished; update the allowlist" - assert _regex_literals(path), f"{relative} no longer restates the shape" + assert _module_sites(path), f"{relative} no longer restates the shape" -def test_migrated_modules_hold_the_owner_object_not_an_equal_copy(): - import importlib - - for module_name, owned in MIGRATED_SITE_MODULES.items(): +def test_every_consumer_holds_the_owner_object_not_an_equal_copy(): + imported = 0 + for module_name in _consumer_modules(): module = importlib.import_module(module_name) + owned = [ + name + for name in ("BARE_SHA256_PATTERN", "ENVELOPED_SHA256_PATTERN") + if name in vars(module) + ] + assert owned, f"{module_name} imports neither owner name" for attribute in owned: assert getattr(module, attribute) is getattr(content_digest, attribute), ( f"{module_name}.{attribute} is a second definition" ) + imported += 1 + assert imported >= 40, f"expected the migrated surfaces to be imported, saw {imported}" @pytest.mark.parametrize("value", ENVELOPED_ACCEPTS) @@ -202,13 +255,15 @@ def test_bare_pattern_rejects_everything_else(value: object) -> None: assert BARE_SHA256_PATTERN.fullmatch(value) is None -@pytest.mark.parametrize("value", [HEX64, MIXED_HEX64, "A" * 64, "a" * 63, "g" * 64]) +@pytest.mark.parametrize( + "value", [HEX64, MIXED_HEX64, "A" * 64, "a" * 63, "g" * 64, HEX64 + " "] +) def test_the_two_retired_bare_spellings_could_never_disagree(value: str) -> None: """Merging `[a-f0-9]` into `[0-9a-f]` cannot change a verdict. - The character class lists the same six letters and digits in a different - order, so both copies accepted and rejected the same strings. This is the - evidence that collapsing them is not a behaviour change. + The character class lists the same six letters and ten digits in a different order, + so both copies accepted and rejected the same strings; this is the evidence that + collapsing them is not a behaviour change. """ first = re.compile(r"^[a-f0-9]{64}$") @@ -217,20 +272,23 @@ def test_the_two_retired_bare_spellings_could_never_disagree(value: str) -> None assert bool(second.fullmatch(value)) == bool(BARE_SHA256_PATTERN.fullmatch(value)) -def test_schema_string_site_is_the_same_question_as_the_owner_bare_shape() -> None: - """`inspection.py` carries the shape as a JSON-schema string, not a pattern. +@pytest.mark.parametrize("value", [HEX64, ENVELOPED, "A" * 64, HEX64 + "0"]) +def test_the_dropped_unicode_anchor_variant_agrees_with_the_owner(value: str) -> None: + r"""Sites written `...\Z` are collapsed into `$` without changing a verdict.""" + + with_backslash = re.compile(r"^[0-9a-f]{64}\Z") + assert bool(with_backslash.fullmatch(value)) is bool( + BARE_SHA256_PATTERN.fullmatch(value) + ), value - It is deliberately not an f-string of the owner: the schema is data published - to callers. This asserts it still describes the bare hex64 envelope, so the - two cannot drift into different verdicts unnoticed. - """ +def test_schema_string_site_is_the_same_question_as_the_owner_bare_shape() -> None: from loopx.capabilities.manager_context import inspection - literals = _regex_literals(Path(inspection.__file__)) - assert literals, "the recorded schema site no longer states the shape" - for _, text in literals: - assert _whole_value_digest_shape(text) == "bare" + sites = _module_sites(Path(inspection.__file__)) + assert sites, "the recorded schema site no longer states the shape" + for _, text, _ in sites: + assert _whole_value_shape(text) == "bare" declared = re.compile(text) for value in (*BARE_ACCEPTS, *BARE_REJECTS): assert bool(declared.fullmatch(value)) is bool( @@ -238,7 +296,7 @@ def test_schema_string_site_is_the_same_question_as_the_owner_bare_shape() -> No ), value -# --- per-site wiring: every migrated surface is entered through its own reader -- +# --- per-surface wiring: every case enters through that surface's own reader ------ def test_periodic_report_archive_requires_the_envelope() -> None: @@ -271,9 +329,9 @@ def test_presentation_extension_keeps_its_own_message_and_bare_shape() -> None: assert presentation._sha256(HEX64, context="artifact") == HEX64 with pytest.raises(ValueError, match="must be a lowercase SHA-256"): presentation._sha256("z" * 64, context="artifact") - # This surface also caps the field at 64 characters, so an enveloped digest - # never reaches the shape check here. That limit is the surface's own policy - # and is left alone; it is why the rejected probe above is same-length. + # This surface also caps the field at 64 characters, so an enveloped digest never + # reaches the shape check here. That limit is the surface's own policy and is left + # alone; it is why the rejected probe above is same-length. with pytest.raises(ValueError, match="at most 64 characters"): presentation._sha256(ENVELOPED, context="artifact") @@ -302,9 +360,7 @@ def test_progress_review_policy_keeps_clearing_and_null_as_distinct_values() -> assert normalize_progress_review_contract_revision(None) is None assert normalize_progress_review_contract_revision("") == "" - assert ( - normalize_progress_review_contract_revision(HEX64) == HEX64 - ) # positive control + assert normalize_progress_review_contract_revision(HEX64) == HEX64 # positive control with pytest.raises(ValueError, match="must be a sha256 hex digest"): normalize_progress_review_contract_revision(ENVELOPED) @@ -353,12 +409,6 @@ def test_periodic_report_delivery_authority_checks_its_effective_revision() -> N def test_governed_transition_receipt_checks_the_intent_basis_field_only() -> None: - """`proposal_digest` is not checked by this shape, so the probe names the field. - - An earlier draft of this case passed a bare `proposal_digest` and concluded - nothing; the migrated pattern guards the optional `intent_basis`. - """ - from loopx.control_plane.work_items.governed_transition_proposal import ( GOVERNED_TRANSITION_RECEIPT_SCHEMA_VERSION as RECEIPT_SCHEMA, validate_governed_transition_receipts, @@ -412,3 +462,5 @@ def cursor(digest: object) -> dict[str, Any]: ) # an unbound cursor is legal on this surface with pytest.raises(OutboxError, match="cursor binding"): decode_cursor(cursor(HEX64), partition=partition) + +