diff --git a/loopx/capabilities/decision_context/packets.py b/loopx/capabilities/decision_context/packets.py index 1bec8af9a8..3b3e07c4b2 100644 --- a/loopx/capabilities/decision_context/packets.py +++ b/loopx/capabilities/decision_context/packets.py @@ -15,6 +15,7 @@ SECRET_LIKE_SURFACE_PATTERN, find_public_safe_local_path, ) +from ...public_safe_text import COMPACT_TOKEN_PATTERN as _TOKEN_RE DECISION_EVIDENCE_PACKET_SCHEMA_VERSION = "decision_evidence_packet_v0" DECISION_PROPOSAL_SCHEMA_VERSION = "decision_proposal_v0" @@ -40,7 +41,6 @@ "no_change", } -_TOKEN_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$") # Refs #5136, direction 3: "does this text carry a local path?" is decided once # by find_public_safe_local_path; this site keeps its own rejection message and # length limit for whatever the owner recognizes. diff --git a/loopx/capabilities/decision_context/profile.py b/loopx/capabilities/decision_context/profile.py index 27da5ce34d..ea82eeb87b 100644 --- a/loopx/capabilities/decision_context/profile.py +++ b/loopx/capabilities/decision_context/profile.py @@ -3,12 +3,12 @@ from __future__ import annotations import json -import re from collections.abc import Collection, Mapping, Sequence from dataclasses import dataclass, field from pathlib import Path from typing import Any +from ...public_safe_text import COMPACT_TOKEN_PATTERN as _TOKEN_RE from .providers import decision_source_provider_registered from .sources import DecisionSourceSpec @@ -19,7 +19,6 @@ MAX_DECISION_SOURCES = 64 MAX_SOURCE_PROVIDER_BINDINGS = 16 -_TOKEN_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$") _CONFIG_FIELDS = { "schema_version", "goal_id", diff --git a/loopx/capabilities/decision_context/sources.py b/loopx/capabilities/decision_context/sources.py index 471bfa87b4..3ab0cce955 100644 --- a/loopx/capabilities/decision_context/sources.py +++ b/loopx/capabilities/decision_context/sources.py @@ -4,16 +4,16 @@ import hashlib import json -import re from collections.abc import Sequence from dataclasses import dataclass, field from datetime import datetime from typing import Protocol +from ...public_safe_text import COMPACT_TOKEN_PATTERN as _TOKEN_RE + DECISION_SOURCE_MANIFEST_SCHEMA_VERSION = "decision_source_manifest_v0" DECISION_SOURCE_SCAN_RECEIPT_SCHEMA_VERSION = "decision_source_scan_receipt_v0" -_TOKEN_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$") _SOURCE_KINDS = { "artifact", "conversation", diff --git a/loopx/capabilities/material_lifecycle/_validation.py b/loopx/capabilities/material_lifecycle/_validation.py index 544c7b6348..3c84295479 100644 --- a/loopx/capabilities/material_lifecycle/_validation.py +++ b/loopx/capabilities/material_lifecycle/_validation.py @@ -14,8 +14,8 @@ SECRET_LIKE_SURFACE_PATTERN, find_public_safe_local_path, ) +from ...public_safe_text import COMPACT_TOKEN_PATTERN as _TOKEN_RE -_TOKEN_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$") # Refs #5136, direction 3: "does this text carry a local path?" is decided once # by find_public_safe_local_path; this site keeps its own rejection message and # length limit for whatever the owner recognizes. diff --git a/loopx/capabilities/reward_memory/application.py b/loopx/capabilities/reward_memory/application.py index cec5912a62..5b028579a9 100644 --- a/loopx/capabilities/reward_memory/application.py +++ b/loopx/capabilities/reward_memory/application.py @@ -2,7 +2,6 @@ import hashlib import json -import re from collections import Counter from collections.abc import Callable, Mapping, Sequence from dataclasses import dataclass @@ -10,6 +9,10 @@ from typing import Any, Literal, get_args from ...control_plane.runtime.public_safety import public_safe_compact_text +from ...public_safe_text import ( + MODULE_QUALIFIED_SURFACE_PATTERN as SURFACE_RE, + PUBLIC_SAFE_REFERENCE_PATTERN as TOKEN_RE, +) from ..context_providers import build_context_provider from ..context_providers.base import ( ContextProvider, @@ -50,8 +53,6 @@ "soft_preference", "procedural_experience", } -TOKEN_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/#-]{0,199}$") -SURFACE_RE = re.compile(r"^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)+$") MAX_QUERY_STEPS = 3 MAX_RESULTS = 8 MAX_SETUP_HINT = 500 diff --git a/loopx/capabilities/reward_memory/candidate_review.py b/loopx/capabilities/reward_memory/candidate_review.py index 0848dbb753..ae7bed7b49 100644 --- a/loopx/capabilities/reward_memory/candidate_review.py +++ b/loopx/capabilities/reward_memory/candidate_review.py @@ -2,12 +2,15 @@ import hashlib import json -import re from collections.abc import Mapping, Sequence from datetime import datetime from typing import Any from ...control_plane.runtime.public_safety import public_safe_compact_text +from ...public_safe_text import ( + MODULE_QUALIFIED_SURFACE_PATTERN as SURFACE_RE, + PUBLIC_SAFE_REFERENCE_PATTERN as TOKEN_RE, +) from .experience_quality import ( normalize_procedural_experience, procedural_experience_quality, @@ -34,8 +37,6 @@ "verified_repository_core_contributor", "verified_project_owner_or_operator", } -TOKEN_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/#-]{0,199}$") -SURFACE_RE = re.compile(r"^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)+$") MAX_SURFACES = 12 MAX_ACTION_SCOPES = 12 diff --git a/loopx/capabilities/reward_memory/dogfood.py b/loopx/capabilities/reward_memory/dogfood.py index 2112869120..2bf6f94cc6 100644 --- a/loopx/capabilities/reward_memory/dogfood.py +++ b/loopx/capabilities/reward_memory/dogfood.py @@ -2,12 +2,12 @@ import hashlib import json -import re from collections.abc import Mapping, Sequence from copy import deepcopy from typing import Any from ...control_plane.runtime.public_safety import public_safe_compact_text +from ...public_safe_text import PUBLIC_SAFE_REFERENCE_PATTERN as TOKEN_RE from .application import REWARD_MEMORY_APPLICATION_RECEIPT_SCHEMA_VERSION from .candidate_review import ( REWARD_MEMORY_REVIEW_SCHEMA_VERSION, @@ -31,7 +31,6 @@ UTILITY_EVALUATION_STATUSES = {"accepted", "rejected", "not_requested"} DOMAIN_FAMILIES = {"issue_fix", "loopx"} OPERATOR_ACTIONS = {"edit", "retire"} -TOKEN_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/#-]{0,199}$") MAX_RECEIPTS = 24 MAX_OPERATOR_CONTROLS = 8 diff --git a/loopx/capabilities/reward_memory/experience_quality.py b/loopx/capabilities/reward_memory/experience_quality.py index 0d51c3f7c4..6b5a6cdaf9 100644 --- a/loopx/capabilities/reward_memory/experience_quality.py +++ b/loopx/capabilities/reward_memory/experience_quality.py @@ -2,11 +2,11 @@ import hashlib import json -import re from collections.abc import Mapping, Sequence from typing import Any from ...control_plane.runtime.public_safety import public_safe_compact_text +from ...public_safe_text import PUBLIC_SAFE_REFERENCE_PATTERN as OPAQUE_REF_RE PROCEDURAL_EXPERIENCE_SCHEMA_VERSION = "procedural_experience_contract_v0" @@ -20,7 +20,6 @@ "evidence_refs", } FUTURE_BEHAVIOR_FIELDS = {"trigger", "action", "validation", "stop_condition"} -OPAQUE_REF_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/#-]{0,199}$") MAX_CONTEXT_ITEMS = 5 MAX_EVIDENCE_REFS = 8 diff --git a/loopx/capabilities/reward_memory/ingestion.py b/loopx/capabilities/reward_memory/ingestion.py index b8c3b5da31..e03c1c8f23 100644 --- a/loopx/capabilities/reward_memory/ingestion.py +++ b/loopx/capabilities/reward_memory/ingestion.py @@ -2,13 +2,16 @@ import hashlib import json -import re from collections.abc import Mapping, Sequence from pathlib import Path from tempfile import TemporaryDirectory from typing import Any from ..context_providers import build_context_provider +from ...public_safe_text import ( + MODULE_QUALIFIED_SURFACE_PATTERN as SURFACE_RE, + PUBLIC_SAFE_REFERENCE_PATTERN as TOKEN_RE, +) from ..context_providers.base import ( ContextProvider, canonical_context_text, @@ -32,8 +35,6 @@ REWARD_MEMORY_STANDING_POLICY_SCHEMA_VERSION = "reward_memory_standing_policy_v0" REWARD_MEMORY_INGEST_RECEIPT_SCHEMA_VERSION = "reward_memory_ingest_receipt_v0" -TOKEN_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/#-]{0,199}$") -SURFACE_RE = re.compile(r"^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)+$") MAX_POLICY_VALUES = 12 _POLICY_FIELDS = { diff --git a/loopx/capabilities/reward_memory/memory_utility.py b/loopx/capabilities/reward_memory/memory_utility.py index cbff3a3762..8d02f5cc8c 100644 --- a/loopx/capabilities/reward_memory/memory_utility.py +++ b/loopx/capabilities/reward_memory/memory_utility.py @@ -9,6 +9,7 @@ from pathlib import PurePosixPath, PureWindowsPath from typing import Any +from ...public_safe_text import PUBLIC_SAFE_REFERENCE_PATTERN as _EXISTING_TOKEN_RE from .application import ( APPLICATION_OUTCOMES, RECALL_MODES, @@ -31,7 +32,6 @@ } ) -_EXISTING_TOKEN_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/#-]{0,199}$") _OPAQUE_REF_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}$") _MEMORY_REF_DIGEST_RE = re.compile(r"^(?:[0-9a-f]{16}|sha256:[0-9a-f]{64})$") _MAX_MEMORY_DIGESTS = 16 diff --git a/loopx/capabilities/reward_memory/registry.py b/loopx/capabilities/reward_memory/registry.py index 61bb951235..754ecd4776 100644 --- a/loopx/capabilities/reward_memory/registry.py +++ b/loopx/capabilities/reward_memory/registry.py @@ -5,6 +5,10 @@ from collections.abc import Mapping, Sequence from typing import Any +from ...public_safe_text import ( + MODULE_QUALIFIED_SURFACE_PATTERN as SURFACE_RE, + PUBLIC_SAFE_REFERENCE_PATTERN as TOKEN_RE, +) from .architecture import MEMORY_CLASS_IDS @@ -16,8 +20,6 @@ MAX_CORPORA = 50 MAX_SURFACES = 20 CORPUS_ID_RE = re.compile(r"^[a-z][a-z0-9_-]{0,63}$") -TOKEN_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/#-]{0,199}$") -SURFACE_RE = re.compile(r"^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)+$") READ_AUTHORITIES = { "goal_run_scoped", diff --git a/loopx/capabilities/semantic_preference/contract.py b/loopx/capabilities/semantic_preference/contract.py index b3f04f0dd8..9b58273bcc 100644 --- a/loopx/capabilities/semantic_preference/contract.py +++ b/loopx/capabilities/semantic_preference/contract.py @@ -8,6 +8,10 @@ from pathlib import Path from typing import Any, Mapping, Sequence +from ...public_safe_text import ( + MODULE_QUALIFIED_SURFACE_PATTERN as SURFACE_RE, + PUBLIC_SAFE_REFERENCE_PATTERN as TOKEN_RE, +) from ...extensions.manifest import validate_extension_id from ...extensions.runtime import ( default_extension_state_file, @@ -26,8 +30,6 @@ MAINTENANCE_GUIDANCE_SCHEMA = "semantic_preference_maintenance_guidance_v0" MAINTENANCE_RECEIPT_SCHEMA = "semantic_preference_maintenance_receipt_v0" DOCTOR_SCHEMA = "semantic_preference_provider_doctor_v0" -SURFACE_RE = re.compile(r"^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)+$") -TOKEN_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/#-]{0,199}$") CORPUS_ID_RE = re.compile(r"^[a-z][a-z0-9_-]{0,63}$") MAX_PROVIDER_OUTPUT_BYTES = 256_000 MAX_ITEM_BYTES = 8_000 diff --git a/loopx/public_safe_text.py b/loopx/public_safe_text.py index db3fbbf998..88b30cef49 100644 --- a/loopx/public_safe_text.py +++ b/loopx/public_safe_text.py @@ -147,6 +147,17 @@ r"\btoken[\"']?\s*[=:]\s*[\"']?[^\s`'\"<>]{12,})" ) +# Shared identifier shapes stay here with the sibling public-safe text shapes. +# They express syntax only: each consuming contract retains its own field names, +# error text, and any additional validation policy. +PUBLIC_SAFE_REFERENCE_PATTERN = re.compile( + r"^[A-Za-z0-9][A-Za-z0-9._:/#-]{0,199}$" +) +COMPACT_TOKEN_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$") +MODULE_QUALIFIED_SURFACE_PATTERN = re.compile( + r"^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)+$" +) + # The text-owner rule set (feedback / authority / boundary_authority / the # TypeScript Vision checkpoint). Each entry carries an explicit category and a diff --git a/tests/control_plane/test_compact_identifier_shape_owner.py b/tests/control_plane/test_compact_identifier_shape_owner.py new file mode 100644 index 0000000000..3f1ab58cd5 --- /dev/null +++ b/tests/control_plane/test_compact_identifier_shape_owner.py @@ -0,0 +1,77 @@ +"""Shared compact identifier shapes have one compiled owner.""" + +from __future__ import annotations + +from pathlib import Path + +from loopx.capabilities.decision_context import packets, profile, sources +from loopx.capabilities.material_lifecycle import _validation +from loopx.capabilities.reward_memory import ( + application, + candidate_review, + dogfood, + experience_quality, + ingestion, + memory_utility, + registry, +) +from loopx.capabilities.semantic_preference import contract +from loopx.public_safe_text import ( + COMPACT_TOKEN_PATTERN, + MODULE_QUALIFIED_SURFACE_PATTERN, + PUBLIC_SAFE_REFERENCE_PATTERN, +) + +REPOSITORY_ROOT = Path(__file__).resolve().parents[2] +OWNER_MODULE = "loopx/public_safe_text.py" +REFERENCE_SHAPE = "^[A-Za-z0-9][A-Za-z0-9._:/#-]{0,199}$" +COMPACT_SHAPE = "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$" +SURFACE_SHAPE = "^[a-z][a-z0-9_-]*(?:\\.[a-z][a-z0-9_-]*)+$" + + +SHAPE_CONSUMERS = { + REFERENCE_SHAPE: ( + "loopx/capabilities/reward_memory/application.py", + "loopx/capabilities/reward_memory/candidate_review.py", + "loopx/capabilities/reward_memory/dogfood.py", + "loopx/capabilities/reward_memory/experience_quality.py", + "loopx/capabilities/reward_memory/ingestion.py", + "loopx/capabilities/reward_memory/memory_utility.py", + "loopx/capabilities/reward_memory/registry.py", + "loopx/capabilities/semantic_preference/contract.py", + ), + COMPACT_SHAPE: ( + "loopx/capabilities/decision_context/packets.py", + "loopx/capabilities/decision_context/profile.py", + "loopx/capabilities/decision_context/sources.py", + "loopx/capabilities/material_lifecycle/_validation.py", + ), + SURFACE_SHAPE: ( + "loopx/capabilities/reward_memory/application.py", + "loopx/capabilities/reward_memory/candidate_review.py", + "loopx/capabilities/reward_memory/ingestion.py", + "loopx/capabilities/reward_memory/registry.py", + "loopx/capabilities/semantic_preference/contract.py", + ), +} + + +def test_compact_identifier_shapes_are_declared_once() -> None: + for shape, consumers in SHAPE_CONSUMERS.items(): + assert shape in (REPOSITORY_ROOT / OWNER_MODULE).read_text(encoding="utf-8") + for consumer in consumers: + assert shape not in (REPOSITORY_ROOT / consumer).read_text(encoding="utf-8") + + +def test_reward_memory_and_semantic_preference_share_reference_and_surface_shapes() -> None: + for module in (application, candidate_review, ingestion, registry, contract): + assert module.TOKEN_RE is PUBLIC_SAFE_REFERENCE_PATTERN + assert module.SURFACE_RE is MODULE_QUALIFIED_SURFACE_PATTERN + assert dogfood.TOKEN_RE is PUBLIC_SAFE_REFERENCE_PATTERN + assert experience_quality.OPAQUE_REF_RE is PUBLIC_SAFE_REFERENCE_PATTERN + assert memory_utility._EXISTING_TOKEN_RE is PUBLIC_SAFE_REFERENCE_PATTERN + + +def test_decision_and_material_contracts_share_the_compact_token_shape() -> None: + for module in (packets, profile, sources, _validation): + assert module._TOKEN_RE is COMPACT_TOKEN_PATTERN