diff --git a/.github/workflows/english-full-corpus.yml b/.github/workflows/english-full-corpus.yml index f6033ca..abd7dcc 100644 --- a/.github/workflows/english-full-corpus.yml +++ b/.github/workflows/english-full-corpus.yml @@ -4,9 +4,9 @@ name: English full-corpus replay on: push: branches: [main] - paths: ['.github/workflows/english-full-corpus.yml', 'human/**', 'programming/**', 'pyproject.toml', 'README.md', 'LICENSE', 'tools/**'] + paths: ['.github/workflows/english-full-corpus.yml', 'human/**', 'programming/**', 'shared/**', 'pyproject.toml', 'README.md', 'LICENSE', 'tools/**'] pull_request: - paths: ['.github/workflows/english-full-corpus.yml', 'human/**', 'programming/**', 'pyproject.toml', 'README.md', 'LICENSE', 'tools/**'] + paths: ['.github/workflows/english-full-corpus.yml', 'human/**', 'programming/**', 'shared/**', 'pyproject.toml', 'README.md', 'LICENSE', 'tools/**'] permissions: contents: read jobs: @@ -53,6 +53,7 @@ jobs: .venv/bin/python -m pytest -q source/human/english/tests --ignore=source/human/english/tests/test_language_relational_bridge.py --junitxml=evidence/english.xml (cd source/human/english && ../../../.venv/bin/python -m pytest -q tests/test_language_relational_bridge.py --junitxml=../../../evidence/relational.xml) PYTHONPATH="$PWD/source/programming/python" .venv/bin/python -m pytest -q source/programming/python/tests --junitxml=evidence/python.xml + PYTHONPATH="$PWD/source/shared/polyglot" .venv/bin/python -m pytest -q source/shared/polyglot/tests --junitxml=evidence/polyglot.xml for file in source/human/english/english_gonol/inference_input.py source/human/english/tests/test_inference_input.py source/tools/verify_inference_corpus.py; do .venv/bin/python doctrine/ratios/ratios_check.py "$file" done diff --git a/.github/workflows/polyglot-identity.yml b/.github/workflows/polyglot-identity.yml new file mode 100644 index 0000000..26e82e8 --- /dev/null +++ b/.github/workflows/polyglot-identity.yml @@ -0,0 +1,103 @@ +# Usage: verifies that UCHC label attachments consume one exact UCNS +# axis-circle identity without reimplementing geometry or asserting translation. +name: Polyglot identity integration +on: + push: + branches: [main] + paths: ['shared/polyglot/**', 'docs/POLYGLOT.md', 'docs/work-graphs/polyglot-circle-identity-v0.json', 'pyproject.toml', '.github/workflows/polyglot-identity.yml'] + pull_request: + paths: ['shared/polyglot/**', 'docs/POLYGLOT.md', 'docs/work-graphs/polyglot-circle-identity-v0.json', 'pyproject.toml', '.github/workflows/polyglot-identity.yml'] +permissions: + contents: read + +jobs: + polyglot-identity: + runs-on: ubuntu-24.04 + steps: + - name: Resolve exact producer and consumer + shell: bash + env: + SOURCE_SHA: ${{ github.sha }} + run: | + set -euo pipefail + fetch_source() { + local repository="$1" directory="$2" sha="$3" + [[ "$sha" =~ ^[0-9a-f]{40}$ ]] + git init -q "$directory" + git -C "$directory" remote add origin "https://github.com/$repository.git" + git -C "$directory" fetch --quiet --depth=1 origin "$sha" + git -C "$directory" checkout -q --detach FETCH_HEAD + test "$(git -C "$directory" rev-parse HEAD)" = "$sha" + } + fetch_source The-Interdependency/uchc source "$SOURCE_SHA" + UCNS_SHA="$(python3 - <<'PY' + import json + from pathlib import Path + + graph = json.loads( + Path('source/docs/work-graphs/polyglot-circle-identity-v0.json').read_text( + encoding='utf-8' + ) + ) + matches = [ + row['commit'] + for row in graph['repositories'] + if row['repository'] == 'The-Interdependency/ucns' + ] + assert len(matches) == 1, matches + print(matches[0]) + PY + )" + fetch_source The-Interdependency/ucns ucns "$UCNS_SHA" + python3 -m venv .venv + .venv/bin/python -m pip install pytest==8.3.5 + - name: Run polyglot contracts + shell: bash + run: | + set -euo pipefail + PYTHONPATH="$PWD/source/shared/polyglot:$PWD/ucns/src" .venv/bin/python -m pytest -q source/shared/polyglot/tests + - name: Prove rename and language attachment over one UCNS position + shell: bash + run: | + set -euo pipefail + PYTHONPATH="$PWD/source/shared/polyglot:$PWD/ucns/src" .venv/bin/python - <<'PY' + from ucns import build_axis_circle_position + from uchc_polyglot import PolyglotReferent, attach_label, build_label_attachment, same_referent + + position = build_axis_circle_position( + origin_sha256="a" * 64, + axis_count=3, + axis_ordinal=1, + ) + referent = PolyglotReferent(ucns_identity_sha256=position.identity_sha256) + heart = build_label_attachment( + ucns_identity_sha256=position.identity_sha256, + language_tag="en", + surface="heart", + scope="focus-locus", + source_id="integration:v1", + ) + before = attach_label(referent, heart) + cardiac = build_label_attachment( + ucns_identity_sha256=position.identity_sha256, + language_tag="en", + surface="cardiac", + scope="focus-locus", + source_id="integration:v2", + supersedes=heart.attachment_sha256, + ) + after = attach_label(before, cardiac) + spanish = build_label_attachment( + ucns_identity_sha256=position.identity_sha256, + language_tag="es", + surface="cardíaco", + scope="focus-locus", + source_id="integration:es", + ) + after = attach_label(after, spanish) + + assert same_referent(before, after) + assert before.referent_identity == after.referent_identity == position.identity_sha256 + assert [x.surface for x in after.current_labels(language_tag="en")] == ["cardiac"] + assert [x.surface for x in after.current_labels(language_tag="es")] == ["cardíaco"] + PY diff --git a/README.md b/README.md index 543d289..2d4172e 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ programming/ python/ extracted from Stack; see migration gate evidence typescript/ forthcoming rust/ forthcoming + +shared/ + polyglot/ cross-language label attachments to UCNS identities ``` The construct is the implementation. UCHC does not separate a nominal specification layer from a hidden implementation that actually defines the construct. @@ -90,10 +93,34 @@ The current UCHC O/S/C inference and epicyclic graph paths remain downstream candidate operators/readouts. Their executable channel tuples do not define the Hilbert basis. +## Polyglot identity layer + +UCHC now has a shared candidate relation layer for language labels that attach +to one UCNS geometric identity. The UCNS producer owns the axis-circle position +and identity digest; UCHC records language/register surfaces, provenance, and +supersession history without recomputing geometry or asserting direct +word-to-word equality. + +A rename therefore changes the attachment, not the referent: + +```text +heart ─┐ +cardiac ─┼──► same UCNS identity +cardíaco ┘ +``` + +See [Polyglot identity layer](docs/POLYGLOT.md). Language-specific admission and +semantic partitioning remain owned by the individual language implementations. +The integration gate consumes the exact UCNS producer recorded in the shared +[polyglot work graph](docs/work-graphs/polyglot-circle-identity-v0.json), currently +merged UCNS `905e66964a495d7596a577bb64e4158db9465864`; the graph is the single checkout +pin and changing it reruns the cross-repository proof. + ## Repository layout - `human/` — human-language UCHC implementations. - `programming/` — programming-language UCHC implementations. +- `shared/polyglot/` — UCHC-owned cross-language label attachments; no UCNS geometry is reimplemented here. - `docs/` — authority, migration, and domain-boundary records. - `schemas/` — machine contracts after their owning implementation requires them. - `tests/` — cross-domain conformance tests after migration begins. diff --git a/docs/POLYGLOT.md b/docs/POLYGLOT.md new file mode 100644 index 0000000..1cef6b4 --- /dev/null +++ b/docs/POLYGLOT.md @@ -0,0 +1,85 @@ +# Polyglot identity layer + +## Purpose + +UCHC attaches language surfaces to a stable UCNS geometric identity. + +The referent is not a word and is not a translation pair. It is the UCNS +identity produced by the axis-circle position candidate. UCHC records language, +register/scope, provenance, and label history around that referent. + + UCNS position identity + <- English label + <- Spanish label + <- technical label + <- historical label + +The arrows are attachments to one referent, not pairwise equality assertions. + +## Rename behavior + +A later label may supersede an earlier attachment while preserving both records. + +Example: + + heart -> same UCNS identity + cardiac -> same UCNS identity, supersedes heart + +The referent identity does not change. + +## Multiple languages + +Independent language implementations may attach their own surfaces: + + cardiac (en) + cardíaco (es) + cardiaque (fr) + +Sharing a referent does not claim that these strings have identical lexical +partitioning in every context. Language-specific construction and admission +remain owned by each language implementation. + +## Usage guidance + + from uchc_polyglot import ( + PolyglotReferent, + attach_label, + build_label_attachment, + ) + + referent = PolyglotReferent(ucns_identity_sha256=position.identity_sha256) + referent = attach_label( + referent, + build_label_attachment( + ucns_identity_sha256=position.identity_sha256, + language_tag="en", + surface="cardiac", + scope="technical", + source_id="source:example", + ), + ) + +The caller obtains position.identity_sha256 from UCNS. UCHC does not recompute +or redefine the geometry. + +The integration gate reads its exact UCNS producer commit from +[`polyglot-circle-identity-v0.json`](work-graphs/polyglot-circle-identity-v0.json) +rather than carrying a second version pin. The current graph consumes merged +UCNS commit `905e66964a495d7596a577bb64e4158db9465864`; update the graph and let its +path trigger the integration proof whenever that producer identity changes. + +## Authority boundary + +- UCNS owns the geometric position and its identity. +- UCHC owns language-scoped attachments and their history. +- A language implementation owns its source/admission/construction evidence. +- METAPAT domain restraint prevents shared structure from silently transferring + domain-specific meaning. +- EDCM may later measure constructed outputs; it does not define the relation. + +## hmmm + +Language-specific semantic partitioning, admission profiles, and evidence for +particular cross-language referent attachments remain separate work. Shared +geometric identity makes the relation representable; it does not make every +dictionary translation exact. diff --git a/docs/work-graphs/polyglot-circle-identity-v0.json b/docs/work-graphs/polyglot-circle-identity-v0.json new file mode 100644 index 0000000..87fa696 --- /dev/null +++ b/docs/work-graphs/polyglot-circle-identity-v0.json @@ -0,0 +1,41 @@ +{ + "schema": "the-interdependency.stack-manifest", + "version": "1.0.0", + "work_graph_sha256": "eddd8d2fd269b74a9c7999109a94406b64073f2ad142669e78557e145348998c", + "repositories": [ + { + "repository": "The-Interdependency/skill-lib", + "commit": "f94ac2e1e0089f22f20d37f3ec41a3ea37e16efa", + "authority": "reusable build/evidence discipline", + "relation": "governing skill source" + }, + { + "repository": "The-Interdependency/metapat", + "commit": "1cdfb09dd00a451cee30eec2e78624df8c682662", + "authority": "semantic/domain-restraint authority", + "relation": "conceptual boundary check" + }, + { + "repository": "The-Interdependency/ucns", + "commit": "905e66964a495d7596a577bb64e4158db9465864", + "authority": "geometric axis-circle position identity", + "relation": "producer baseline" + }, + { + "repository": "The-Interdependency/uchc", + "commit": "4ad94e92be10d2c4c875a848addfda46f3c7cfdb", + "authority": "language-hyperspace label attachments", + "relation": "consumer baseline" + } + ], + "boundaries": { + "authority_transfer": false, + "proof_status_transfer": false, + "measurement_status_transfer": false, + "semantic_mapping": "declared mapping", + "agent_scope": "cross-repository-work-graph", + "hmmm": [ + "ratification of axis-circle position as universal UCNS object identity beyond the declared candidate scope" + ] + } +} diff --git a/pyproject.toml b/pyproject.toml index e3364c1..3d9c1d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,4 +22,5 @@ packages = ["english_gonol", "english_gonol.language", "python_gonol"] package-dir = {english_gonol = "human/english/english_gonol", python_gonol = "programming/python/python_gonol"} [tool.pytest.ini_options] -testpaths = ["human/english/tests", "programming/python/tests"] +testpaths = ["human/english/tests", "programming/python/tests", "shared/polyglot/tests"] +pythonpath = ["human/english", "programming/python", "shared/polyglot"] diff --git a/shared/polyglot/tests/test_polyglot.py b/shared/polyglot/tests/test_polyglot.py new file mode 100644 index 0000000..2c78fee --- /dev/null +++ b/shared/polyglot/tests/test_polyglot.py @@ -0,0 +1,231 @@ +# === CHECKS === +# id: check_polyglot_referent_identity_is_ucns_identity +# proves: polyglot_referent_identity_is_ucns_identity +# call: self::test_rename_preserves_ucns_referent +# requires: python3 +# timeout: 10 +# mutates: none +# cleanup: none +# +# id: check_polyglot_labels_are_scoped_attachments +# proves: polyglot_labels_are_scoped_attachments +# call: self::test_multiple_languages_attach_to_one_referent +# requires: python3 +# timeout: 10 +# mutates: none +# cleanup: none +# +# id: check_polyglot_does_not_assert_pairwise_translation_identity +# proves: polyglot_does_not_assert_pairwise_translation_identity +# call: self::test_labels_share_referent_without_pairwise_equality +# requires: python3 +# timeout: 10 +# mutates: none +# cleanup: none +# +# id: check_polyglot_supersession_preserves_history +# proves: polyglot_supersession_preserves_history +# call: self::test_supersession_preserves_history +# requires: python3 +# timeout: 10 +# mutates: none +# cleanup: none +# +# id: check_polyglot_fails_closed +# proves: polyglot_fails_closed +# call: self::test_polyglot_fails_closed +# requires: python3 +# timeout: 10 +# mutates: none +# cleanup: none +# === END CHECKS === + +import pytest + +from uchc_polyglot import ( + PolyglotError, + PolyglotReferent, + attach_label, + build_label_attachment, + same_referent, +) + + +UCNS_ID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + +def test_rename_preserves_ucns_referent() -> None: + referent = PolyglotReferent(ucns_identity_sha256=UCNS_ID) + heart = build_label_attachment( + ucns_identity_sha256=UCNS_ID, + language_tag="en", + surface="heart", + scope="ordinary", + source_id="thread:initial", + ) + first = attach_label(referent, heart) + cardiac = build_label_attachment( + ucns_identity_sha256=UCNS_ID, + language_tag="en", + surface="cardiac", + scope="technical", + source_id="thread:rename", + supersedes=heart.attachment_sha256, + ) + renamed = attach_label(first, cardiac) + + assert first.referent_identity == UCNS_ID + assert renamed.referent_identity == UCNS_ID + assert same_referent(first, renamed) + assert [item.surface for item in renamed.current_labels()] == ["cardiac"] + + +def test_multiple_languages_attach_to_one_referent() -> None: + referent = PolyglotReferent(ucns_identity_sha256=UCNS_ID) + for language, surface in ( + ("en", "cardiac"), + ("es", "cardíaco"), + ("fr", "cardiaque"), + ): + referent = attach_label( + referent, + build_label_attachment( + ucns_identity_sha256=UCNS_ID, + language_tag=language, + surface=surface, + scope="technical", + source_id=f"fixture:{language}", + ), + ) + + assert referent.referent_identity == UCNS_ID + assert [item.surface for item in referent.current_labels(language_tag="es")] == ["cardíaco"] + assert len(referent.current_labels()) == 3 + + +def test_labels_share_referent_without_pairwise_equality() -> None: + english = build_label_attachment( + ucns_identity_sha256=UCNS_ID, + language_tag="en", + surface="cardiac", + scope="technical", + source_id="fixture:en", + ) + spanish = build_label_attachment( + ucns_identity_sha256=UCNS_ID, + language_tag="es", + surface="cardíaco", + scope="technical", + source_id="fixture:es", + ) + referent = PolyglotReferent( + ucns_identity_sha256=UCNS_ID, + labels=(english, spanish), + ) + payload = referent.as_dict() + + assert english.surface != spanish.surface + assert payload["ucns_identity_sha256"] == UCNS_ID + assert "translation" not in payload + assert "equals" not in payload + + other = build_label_attachment( + ucns_identity_sha256="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + language_tag="en", + surface="cardiac", + scope="technical", + source_id="fixture:en", + ) + assert english.attachment_sha256 != other.attachment_sha256 + + +def test_supersession_preserves_history() -> None: + ordinary = build_label_attachment( + ucns_identity_sha256=UCNS_ID, + language_tag="en", + surface="heart", + scope="focus-locus", + source_id="fixture:v1", + ) + technical = build_label_attachment( + ucns_identity_sha256=UCNS_ID, + language_tag="en", + surface="cardiac", + scope="focus-locus", + source_id="fixture:v2", + supersedes=ordinary.attachment_sha256, + ) + referent = PolyglotReferent( + ucns_identity_sha256=UCNS_ID, + labels=(ordinary, technical), + ) + + assert [item.surface for item in referent.labels] == ["heart", "cardiac"] + assert [item.surface for item in referent.current_labels()] == ["cardiac"] + + +def test_polyglot_fails_closed() -> None: + with pytest.raises(PolyglotError): + PolyglotReferent(ucns_identity_sha256="not-a-digest") + + first = build_label_attachment( + ucns_identity_sha256=UCNS_ID, + language_tag="en", + surface="heart", + scope="ordinary", + source_id="fixture:1", + ) + dangling = build_label_attachment( + ucns_identity_sha256=UCNS_ID, + language_tag="en", + surface="cardiac", + scope="technical", + source_id="fixture:2", + supersedes="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + ) + with pytest.raises(PolyglotError): + PolyglotReferent( + ucns_identity_sha256=UCNS_ID, + labels=(first, dangling), + ) + + with pytest.raises(PolyglotError): + PolyglotReferent( + ucns_identity_sha256=UCNS_ID, + labels=(first, first), + ) + + spanish_wrong_supersession = build_label_attachment( + ucns_identity_sha256=UCNS_ID, + language_tag="es", + surface="cardíaco", + scope="ordinary", + source_id="fixture:3", + supersedes=first.attachment_sha256, + ) + with pytest.raises(PolyglotError): + PolyglotReferent( + ucns_identity_sha256=UCNS_ID, + labels=(first, spanish_wrong_supersession), + ) + + for field in ("language_tag", "scope", "source_id"): + kwargs = { + "ucns_identity_sha256": UCNS_ID, + "language_tag": "en", + "surface": "cardiac", + "scope": "technical", + "source_id": "fixture:blank", + } + kwargs[field] = " " + with pytest.raises(PolyglotError): + build_label_attachment(**kwargs) + + whitespace_surface = build_label_attachment( + ucns_identity_sha256=UCNS_ID, + language_tag="en", + surface=" ", + scope="literal", + source_id="fixture:whitespace-surface", + ) + assert whitespace_surface.surface == " " diff --git a/shared/polyglot/uchc_polyglot/__init__.py b/shared/polyglot/uchc_polyglot/__init__.py new file mode 100644 index 0000000..8b3bfab --- /dev/null +++ b/shared/polyglot/uchc_polyglot/__init__.py @@ -0,0 +1,327 @@ +# === MODULE_BUILD === +# id: uchc_polyglot_label_attachment +# module_name: uchc_polyglot +# module_kind: relation +# summary: attaches language- and register-scoped surfaces to one UCNS geometric identity without making labels identity-bearing or asserting word-to-word translation +# owner: Erin Spencer +# public_surface: LabelAttachment, PolyglotReferent, PolyglotError, build_label_attachment, attach_label, same_referent +# internal_surface: deterministic attachment digest, supersession-chain validation, current-label projection +# auth_boundary: UCNS geometric identity is consumed by exact digest and schema/version only; UCHC does not recompute UCNS geometry +# storage_boundary: immutable records only +# network_boundary: none +# user_data_boundary: none +# admin_only: false +# tests: shared/polyglot/tests/test_polyglot.py +# rollout: candidate shared UCHC polyglot layer +# rollback: remove package, tests, docs, and packaging entry +# requires: ucns_axis_circle_position_candidate +# since: 2026-10-05 +# unresolved: language-specific admission/construction profiles and semantic partitioning remain owned by each language implementation +# === END MODULE_BUILD === +# +# === CONTRACTS === +# id: polyglot_referent_identity_is_ucns_identity +# given: any UCHC polyglot referent +# then: its referent identity is exactly the consumed UCNS identity digest and is unchanged by label additions or supersession +# class: correctness +# since: 2026-10-05 +# +# id: polyglot_labels_are_scoped_attachments +# given: one UCNS identity +# then: multiple language/register surfaces may attach independently with provenance and history +# class: correctness +# since: 2026-10-05 +# +# id: polyglot_does_not_assert_pairwise_translation_identity +# given: two labels on one referent +# then: UCHC records that both attach to the same UCNS identity; it does not encode label A equals label B +# class: doctrine +# since: 2026-10-05 +# +# id: polyglot_supersession_preserves_history +# given: a later attachment supersedes an earlier attachment +# then: both records remain addressable while current-label projection excludes the superseded attachment +# class: correctness +# since: 2026-10-05 +# +# id: polyglot_fails_closed +# given: malformed UCNS identity, duplicate attachment identity, dangling supersession, or malformed label +# then: construction raises PolyglotError +# class: safety +# since: 2026-10-05 +# === END CONTRACTS === + +"""Language labels attached to stable UCNS geometric identity. + +This package owns no geometry. It consumes one exact UCNS identity digest and +allows language/register surfaces to attach to that referent. + +A rename is a new attachment that may supersede an older one. The underlying +UCNS identity never changes. Multiple languages may attach concurrently. + +No pairwise translation equality is created: two labels are related only by +their independent attachment to the same geometric referent. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from hashlib import sha256 +import json +import re +from typing import Iterable + +UCNS_SCHEMA = "ucns.axis-circle-position-candidate" +UCNS_VERSION = "0.1.0" +_SHA256_RE = re.compile(r"^[0-9a-f]{64}$") + + +class PolyglotError(ValueError): + """Raised when a polyglot attachment record fails closed.""" + + +def _canonical(payload: dict[str, object]) -> bytes: + return json.dumps( + payload, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=False, + allow_nan=False, + ).encode("utf-8") + + +def _require_metadata_text(name: str, value: object) -> str: + if not isinstance(value, str) or not value.strip(): + raise PolyglotError(f"{name} must contain non-whitespace text") + return value + + +def _require_surface(value: object) -> str: + if not isinstance(value, str) or not value: + raise PolyglotError("surface must be a non-empty string") + return value + + +@dataclass(frozen=True, slots=True) +class LabelAttachment: + """One provenance-bearing surface attached to a UCNS referent.""" + + ucns_identity_sha256: str + language_tag: str + surface: str + scope: str + source_id: str + supersedes: str | None + attachment_sha256: str + + def __post_init__(self) -> None: + if not isinstance(self.ucns_identity_sha256, str) or not _SHA256_RE.fullmatch( + self.ucns_identity_sha256 + ): + raise PolyglotError("ucns_identity_sha256 must be a lowercase hexadecimal SHA-256") + for name in ("language_tag", "scope", "source_id"): + _require_metadata_text(name, getattr(self, name)) + _require_surface(self.surface) + if self.supersedes is not None and ( + not isinstance(self.supersedes, str) + or not _SHA256_RE.fullmatch(self.supersedes) + ): + raise PolyglotError("supersedes must be a lowercase hexadecimal SHA-256 or None") + expected = _attachment_digest( + ucns_identity_sha256=self.ucns_identity_sha256, + language_tag=self.language_tag, + surface=self.surface, + scope=self.scope, + source_id=self.source_id, + supersedes=self.supersedes, + ) + if self.attachment_sha256 != expected: + raise PolyglotError("attachment_sha256 does not match attachment content") + + def as_dict(self) -> dict[str, object]: + return { + "ucns_identity_sha256": self.ucns_identity_sha256, + "language_tag": self.language_tag, + "surface": self.surface, + "scope": self.scope, + "source_id": self.source_id, + "supersedes": self.supersedes, + "attachment_sha256": self.attachment_sha256, + } + + +def _attachment_digest( + *, + ucns_identity_sha256: str, + language_tag: str, + surface: str, + scope: str, + source_id: str, + supersedes: str | None, +) -> str: + payload = { + "ucns_schema": UCNS_SCHEMA, + "ucns_version": UCNS_VERSION, + "ucns_identity_sha256": ucns_identity_sha256, + "language_tag": language_tag, + "surface": surface, + "scope": scope, + "source_id": source_id, + "supersedes": supersedes, + } + return sha256(_canonical(payload)).hexdigest() + + +def build_label_attachment( + *, + ucns_identity_sha256: str, + language_tag: str, + surface: str, + scope: str, + source_id: str, + supersedes: str | None = None, +) -> LabelAttachment: + """Build one immutable label attachment.""" + + if not isinstance(ucns_identity_sha256, str) or not _SHA256_RE.fullmatch( + ucns_identity_sha256 + ): + raise PolyglotError("ucns_identity_sha256 must be a lowercase hexadecimal SHA-256") + language_tag = _require_metadata_text("language_tag", language_tag) + surface = _require_surface(surface) + scope = _require_metadata_text("scope", scope) + source_id = _require_metadata_text("source_id", source_id) + if supersedes is not None and ( + not isinstance(supersedes, str) or not _SHA256_RE.fullmatch(supersedes) + ): + raise PolyglotError("supersedes must be a lowercase hexadecimal SHA-256 or None") + return LabelAttachment( + ucns_identity_sha256=ucns_identity_sha256, + language_tag=language_tag, + surface=surface, + scope=scope, + source_id=source_id, + supersedes=supersedes, + attachment_sha256=_attachment_digest( + ucns_identity_sha256=ucns_identity_sha256, + language_tag=language_tag, + surface=surface, + scope=scope, + source_id=source_id, + supersedes=supersedes, + ), + ) + + +@dataclass(frozen=True, slots=True) +class PolyglotReferent: + """One UCNS identity plus zero or more external language attachments.""" + + ucns_identity_sha256: str + ucns_schema: str = UCNS_SCHEMA + ucns_version: str = UCNS_VERSION + labels: tuple[LabelAttachment, ...] = () + + def __post_init__(self) -> None: + if not isinstance(self.ucns_identity_sha256, str) or not _SHA256_RE.fullmatch( + self.ucns_identity_sha256 + ): + raise PolyglotError("ucns_identity_sha256 must be a lowercase hexadecimal SHA-256") + if self.ucns_schema != UCNS_SCHEMA or self.ucns_version != UCNS_VERSION: + raise PolyglotError("unsupported UCNS identity schema or version") + if not isinstance(self.labels, tuple): + raise PolyglotError("labels must be an immutable tuple") + + seen: set[str] = set() + prior_by_digest: dict[str, LabelAttachment] = {} + for attachment in self.labels: + if not isinstance(attachment, LabelAttachment): + raise PolyglotError("labels must contain LabelAttachment records") + if attachment.ucns_identity_sha256 != self.ucns_identity_sha256: + raise PolyglotError("label attachment referent does not match PolyglotReferent") + if attachment.attachment_sha256 in seen: + raise PolyglotError("duplicate label attachment identity") + if attachment.supersedes is not None: + prior = prior_by_digest.get(attachment.supersedes) + if prior is None: + raise PolyglotError("supersedes must reference an earlier attachment on this referent") + if prior.language_tag != attachment.language_tag: + raise PolyglotError("supersession must stay within one language") + seen.add(attachment.attachment_sha256) + prior_by_digest[attachment.attachment_sha256] = attachment + + @property + def referent_identity(self) -> str: + """The stable identity is exactly the consumed UCNS identity.""" + + return self.ucns_identity_sha256 + + def current_labels( + self, + *, + language_tag: str | None = None, + scope: str | None = None, + ) -> tuple[LabelAttachment, ...]: + """Return attachments not superseded by a later attachment.""" + + superseded = { + attachment.supersedes + for attachment in self.labels + if attachment.supersedes is not None + } + result = [] + for attachment in self.labels: + if attachment.attachment_sha256 in superseded: + continue + if language_tag is not None and attachment.language_tag != language_tag: + continue + if scope is not None and attachment.scope != scope: + continue + result.append(attachment) + return tuple(result) + + def as_dict(self) -> dict[str, object]: + return { + "ucns_identity_sha256": self.ucns_identity_sha256, + "ucns_schema": self.ucns_schema, + "ucns_version": self.ucns_version, + "labels": [attachment.as_dict() for attachment in self.labels], + } + + +def attach_label( + referent: PolyglotReferent, + attachment: LabelAttachment, +) -> PolyglotReferent: + """Return the same referent identity with one additional label attachment.""" + + if not isinstance(referent, PolyglotReferent): + raise PolyglotError("referent must be a PolyglotReferent") + if not isinstance(attachment, LabelAttachment): + raise PolyglotError("attachment must be a LabelAttachment") + return PolyglotReferent( + ucns_identity_sha256=referent.ucns_identity_sha256, + ucns_schema=referent.ucns_schema, + ucns_version=referent.ucns_version, + labels=referent.labels + (attachment,), + ) + + +def same_referent(first: PolyglotReferent, second: PolyglotReferent) -> bool: + """Compare only the underlying UCNS identity, never the attached labels.""" + + if not isinstance(first, PolyglotReferent) or not isinstance(second, PolyglotReferent): + raise PolyglotError("same_referent requires PolyglotReferent values") + return first.ucns_identity_sha256 == second.ucns_identity_sha256 + + +__all__ = [ + "UCNS_SCHEMA", + "UCNS_VERSION", + "LabelAttachment", + "PolyglotError", + "PolyglotReferent", + "attach_label", + "build_label_attachment", + "same_referent", +]