From dfc6c6459474c865745cdc6f8f4ac6c5aac694a5 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Thu, 25 Jun 2026 20:04:55 -0700 Subject: [PATCH] Resolve backend merge conflicts --- CLAUDE.md | 18 +- backend/src/edcmbone_backend/__init__.py | 6 +- backend/src/ucns/ucns_v04.py | 206 +++++++++++++++++++++++ backend/tests/test_backend_contracts.py | 4 +- 4 files changed, 222 insertions(+), 12 deletions(-) create mode 100644 backend/src/ucns/ucns_v04.py diff --git a/CLAUDE.md b/CLAUDE.md index 3634682..32e6d4f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,20 +9,20 @@ This file gives AI assistants (Claude Code and others) the context needed to wor | Field | Value | |---|---| -| Package | `edcmbone` | -| Version | `0.1.0` | -| Description | Structural fidelity measurement for AI interactions — quantifies how much meaning an AI system deletes when transforming structured user input | -| Status | 3 - Alpha | -| Python | >=3.8 (classifiers: 3.8, 3.9, 3.10, 3.11, 3.12) | +| Package | `edcmbone-backend` | +| Version | `0.2.0` | +| Description | MSDMD-compliant UCNS-only backend for EDCM boundary objects | +| Status | hmmm | +| Python | >=3.8 | | License | MPL-2.0 | | Build backend | `hatchling.build` | -| Author(s) | Erin Patrick Spencer | -| Repository | https://github.com/The-Interdependency/edcmbone | +| Author(s) | hmmm | +| Repository | hmmm | | Runtime dependencies | none (stdlib only) | | Optional extras | none | -| Keywords | AI, measurement, structural fidelity, cognitive accessibility, NLP, EDCM | +| Keywords | none | | CI workflows | `ci.yml`, `manifest-check.yml` | -| Top-level directories | `aimmh-lib/` · `backend/` · `canon_eng/` · `core/` · `docs/` · `edcmbone/` · `frontend/` · `tests/` | +| Top-level directories | `aimmh-lib/` · `backend/` · `backend_old/` · `canon_eng/` · `core/` · `docs/` · `edcmbone/` · `frontend/` · `tests/` | Derived from `backend/pyproject.toml` + the repo tree. Unknown fields surface as `hmmm` rather than a guess. diff --git a/backend/src/edcmbone_backend/__init__.py b/backend/src/edcmbone_backend/__init__.py index a5d7072..6963ce7 100644 --- a/backend/src/edcmbone_backend/__init__.py +++ b/backend/src/edcmbone_backend/__init__.py @@ -172,14 +172,16 @@ def hmmm(unresolved=None): def make_boundary(delivered, unresolved=None): """Create a UCNS-backed boundary object from delivered and unresolved text.""" - return BoundaryObject(delivered, hmmm(unresolved), _anchor(0 if unresolved else 1)) + unresolved_text = _coerce_text(unresolved) + return BoundaryObject(delivered, hmmm(unresolved_text), _anchor(0 if unresolved_text else 1)) def merge_boundaries(left, right): """Compose two boundaries while preserving both delivered and hmmm text.""" + unresolved = "\n".join(text for text in (left.hmmm.unresolved, right.hmmm.unresolved) if text) return BoundaryObject( "\n".join(part for part in (left.delivered, right.delivered) if part), - "\n".join(str(part) for part in (left.hmmm, right.hmmm) if part), + hmmm(unresolved), ucns.multiply(left.ucns_object, right.ucns_object), ) diff --git a/backend/src/ucns/ucns_v04.py b/backend/src/ucns/ucns_v04.py new file mode 100644 index 0000000..8614068 --- /dev/null +++ b/backend/src/ucns/ucns_v04.py @@ -0,0 +1,206 @@ +""" +ucns_v04 — UCNS Engine (turn-fraction angle convention) +======================================================== +Angles are stored as Fraction objects representing fractions of a full turn: + 0 = 0 deg, 1/4 = 90 deg, 1/2 = 180 deg, 2 = 720 deg = 0 on doubled cover. + +The algebra operates on the doubled cover of the unit circle, so the +fundamental period is 2 (two full turns = identity). Normalization shifts +the first anchor to theta=0 and reduces all thetas mod 2. + +n_min is the LCM of denominators of all non-zero anchor thetas, computed +directly from the Fraction denominators (no pi-unit conversion). + +Public API +---------- +AnchorPayload(theta, payload) +UCNSObject(n_dec, n_min, anchors_pos, faces_pos) + .anchors_pos : tuple[AnchorPayload, ...] + .faces_pos : tuple[int, ...] + .n_dec : int + .n_min : int + .normalize() : UCNSObject (returns self; normalization done in __init__) + .equivalent(other) : bool +unit_obj() : UCNSObject (multiplicative unit) +is_unit_payload(obj): bool +multiply(A, B) : UCNSObject (A ⊠ B) +""" + +from __future__ import annotations + +from fractions import Fraction +from math import gcd +from functools import reduce +from typing import Optional, Tuple + +__all__ = [ + "AnchorPayload", + "UCNSObject", + "unit_obj", + "is_unit_payload", + "multiply", +] + + +def _lcm(a: int, b: int) -> int: + return a * b // gcd(a, b) + + +def _reduce_lcm(denoms): + return reduce(_lcm, denoms, 1) + + +class AnchorPayload: + """Named container for a (theta, payload) anchor entry.""" + __slots__ = ("theta", "payload") + + def __init__(self, theta, payload): + self.theta = Fraction(theta) + self.payload = payload # UCNSObject or None + + def __repr__(self) -> str: + return f"AnchorPayload(theta={self.theta}, payload={self.payload!r})" + + +class UCNSObject: + """ + A UCNS algebraic object on the doubled unit circle. + + anchors_pos : tuple of AnchorPayload (theta in turn-fractions, payload) + faces_pos : tuple of int (face label per anchor, 0 or 1) + n_dec : declared carrier size (context hint; upper bound on n_min) + n_min : minimal carrier = LCM of denominators of non-zero thetas + """ + + def __init__( + self, + n_dec: int, + n_min: int, + anchors_pos, + faces_pos, + ): + self.n_dec = int(n_dec) + self._anchors_raw = tuple(anchors_pos) + self._faces_raw = tuple(faces_pos) + # Normalization populates .anchors_pos, .faces_pos, .n_min. + self.anchors_pos: Tuple[AnchorPayload, ...] = self._anchors_raw + self.faces_pos: Tuple[int, ...] = self._faces_raw + self.n_min = int(n_min) + self._do_normalize() + + def _do_normalize(self): + """Shift so first anchor is at 0; recompute n_min from thetas.""" + if not self._anchors_raw: + self.anchors_pos = () + self.faces_pos = () + self.n_min = 1 + return + + theta0 = self._anchors_raw[0].theta + normalized = [] + for ap in self._anchors_raw: + new_theta = (ap.theta - theta0) % 2 + normalized.append(AnchorPayload(new_theta, ap.payload)) + + self.anchors_pos = tuple(normalized) + self.faces_pos = self._faces_raw + + non_zero_denoms = [ + ap.theta.denominator + for ap in self.anchors_pos + if ap.theta != 0 + ] + self.n_min = _reduce_lcm(non_zero_denoms) if non_zero_denoms else 1 + + def normalize(self) -> "UCNSObject": + """Return self (normalization happens at construction time).""" + return self + + def equivalent(self, other: "UCNSObject") -> bool: + """Deep structural equivalence.""" + if not isinstance(other, UCNSObject): + return False + a = self + b = other + if len(a.anchors_pos) != len(b.anchors_pos): + return False + if a.faces_pos != b.faces_pos: + return False + for ap, bp in zip(a.anchors_pos, b.anchors_pos): + if ap.theta != bp.theta: + return False + if ap.payload is None and bp.payload is None: + continue + if ap.payload is None or bp.payload is None: + return False + if not ap.payload.equivalent(bp.payload): + return False + return True + + def __repr__(self) -> str: + thetas = [str(ap.theta) for ap in self.anchors_pos] + return f"UCNSObject(n_dec={self.n_dec}, n_min={self.n_min}, thetas={thetas})" + + +def unit_obj() -> UCNSObject: + """Return the multiplicative unit: single anchor at theta=0, no payload.""" + return UCNSObject( + n_dec=1, + n_min=1, + anchors_pos=(AnchorPayload(Fraction(0), None),), + faces_pos=(0,), + ) + + +def is_unit_payload(obj: Optional[UCNSObject]) -> bool: + """True if obj is None (no payload) or structurally equivalent to the unit.""" + if obj is None: + return True + return obj.equivalent(unit_obj()) + + +def multiply(A: UCNSObject, B: UCNSObject) -> UCNSObject: + """ + UCNS product A ⊠ B. + + Each anchor a_k of A is combined with each anchor b_j of B to yield + a result anchor with: + theta = (a_k.theta + b_j.theta) % 2 + payload = multiply(a_k.payload, b_j.payload) [recursive; None is unit] + face = a_k.face XOR b_j.face + + The result has len(A.anchors_pos) * len(B.anchors_pos) anchors, ordered + A-major (outer loop over A, inner loop over B). + + The single-anchor unit_obj() is a two-sided identity under this product. + The product is associative. + """ + new_anchors = [] + new_faces = [] + + for ai, ak in enumerate(A.anchors_pos): + for bi, bj in enumerate(B.anchors_pos): + theta = (ak.theta + bj.theta) % 2 + + pa = ak.payload + pb = bj.payload + if pa is None and pb is None: + payload = None + elif pa is None: + payload = pb + elif pb is None: + payload = pa + else: + payload = multiply(pa, pb) + + face = A.faces_pos[ai] ^ B.faces_pos[bi] + new_anchors.append(AnchorPayload(theta, payload)) + new_faces.append(face) + + n_dec = _lcm(A.n_dec, B.n_dec) + return UCNSObject( + n_dec=n_dec, + n_min=1, + anchors_pos=tuple(new_anchors), + faces_pos=tuple(new_faces), + ) diff --git a/backend/tests/test_backend_contracts.py b/backend/tests/test_backend_contracts.py index a079421..63a7a60 100644 --- a/backend/tests/test_backend_contracts.py +++ b/backend/tests/test_backend_contracts.py @@ -14,6 +14,7 @@ def test_backend_imports_only_ucns(): imports.extend(alias.name for alias in node.names) elif isinstance(node, ast.ImportFrom): imports.append(node.module or "") + assert set(imports) == {"ucns"} assert imports == ["ucns"] @@ -76,6 +77,7 @@ def test_boundaries_record_no_hidden_side_effects(): def test_backend_src_path_is_self_contained(tmp_path): + import os import subprocess import sys @@ -88,7 +90,7 @@ def test_backend_src_path_is_self_contained(tmp_path): result = subprocess.run( [sys.executable, "-c", code], cwd=tmp_path, - env={"PYTHONPATH": str(Path(__file__).resolve().parents[1] / "src")}, + env={**os.environ, "PYTHONPATH": str(Path(__file__).resolve().parents[1] / "src")}, text=True, capture_output=True, check=False,