Skip to content
This repository was archived by the owner on Jul 29, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ edcmbone/
│ ├── parse/ # parser refactor (scaffolded)
│ └── routing/ # routing layer (scaffolded)
├── core/ # Core framework package (IN PROGRESS — stubs)
├── core/ # Core framework package (IN PROGRESS — partially implemented)
│ ├── __init__.py
│ ├── behavioral/
│ ├── bridge/
Expand Down Expand Up @@ -114,9 +114,9 @@ The repo currently has **two parallel package structures**:
| Layout | Path | Status | When to Use |
|--------|------|--------|-------------|
| **Stable** | `Backend/src/edcmbone/` | 87 tests passing, production-ready | Installing and consuming the library |
| **Refactor** | `edcmbone/` (root) and `core/` | Scaffolded stubs, 0-byte files | Adding new architecture — fill in stubs here |
| **Refactor** | `edcmbone/` (root) and `core/` | Partially implemented refactor-in-progress; see individual module docstrings for what is and isn't complete | Adding new architecture — fill in stubs here |

Do not assume files in `edcmbone/` (root) or `core/` contain working implementations — most are 0-byte stubs. The **stable** code lives under `Backend/src/edcmbone/`.
Do not assume all files in `edcmbone/` (root) or `core/` contain working implementations. `core/parsing/`, `core/operator/`, and `core/bridge/` now contain substantive implementations; other subdirectories may still be stubs. The **stable** code lives under `Backend/src/edcmbone/`.

---

Expand Down Expand Up @@ -237,7 +237,8 @@ npm run build
- No CI/CD pipeline
- No linting configs (prefer `ruff` when adding)
- No pre-commit hooks
- `edcmbone/` (root) and `core/` modules are stubs — not yet implemented
- `edcmbone/` (root) stubs — not yet fully implemented (see individual module docstrings)
- `core/` — partially implemented refactor-in-progress; `core/parsing/`, `core/operator/`, and `core/bridge/` contain substantive implementations; other subdirectories may still be stubs
- `aimmh-lib/backend/server.py` is a stub
- `tailwind.config.js` needs `content` globs before use
- Frontend tests
Expand Down
33 changes: 33 additions & 0 deletions Tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- edcmbone.parser (parse_transcript, Turn, Round, BoneToken, FleshToken)
- edcmbone.metrics (stats, risk, compute, projection, matrix)
- edcmbone.compress (encode/decode round-trip, compression_stats)
- closed_tokens (dispatch collision resolution)
"""

import math
Expand Down Expand Up @@ -585,3 +586,35 @@ def test_diff_detects_change(self):
changes = diff(A_MATRIX, modified)
assert ("F", "rep_b") in changes
assert changes[("F", "rep_b")] == (0.30, 0.99)


# ---------------------------------------------------------------------------
# closed_tokens
# ---------------------------------------------------------------------------

class TestClosedTokens:
"""Verify collision-resolution convention for the DISPATCH table."""

def test_since_resolves_to_preposition(self):
pytest.importorskip("ucns_v04")
from closed_tokens import DISPATCH, CLASS_PREPOSITION
class_idx, _ = DISPATCH["since"]
assert class_idx == CLASS_PREPOSITION

def test_until_resolves_to_preposition(self):
pytest.importorskip("ucns_v04")
from closed_tokens import DISPATCH, CLASS_PREPOSITION
class_idx, _ = DISPATCH["until"]
assert class_idx == CLASS_PREPOSITION

def test_as_resolves_to_preposition(self):
pytest.importorskip("ucns_v04")
from closed_tokens import DISPATCH, CLASS_PREPOSITION
class_idx, _ = DISPATCH["as"]
assert class_idx == CLASS_PREPOSITION

def test_that_resolves_to_determiner(self):
pytest.importorskip("ucns_v04")
from closed_tokens import DISPATCH, CLASS_DETERMINER
class_idx, _ = DISPATCH["that"]
assert class_idx == CLASS_DETERMINER
61 changes: 34 additions & 27 deletions closed_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,23 @@
- face bit unused at the host level (set to 0)

The pairing-mark rule: smart open quote and curly open brackets go in class 10;
their close counterparts in class 11. The disk-flip of an open-mark UCNS
object equals the close-mark object (verified by test).
their close counterparts in class 11. The disk-flip property (open-mark ↔
close-mark symmetry) is a design invariant with no implementation yet
(TODO: add disk-flip op + explicit test).

Dispatch collision convention: when a token appears in multiple class tables,
the first table wins by ordering. Specifically: for 'since' and 'until' the
preposition sense wins; for 'as' the preposition sense wins; for 'that' the
determiner sense wins.
"""

from __future__ import annotations
Comment thread
erinepshovel-code marked this conversation as resolved.

from fractions import Fraction
from typing import Dict, List, Optional, Tuple

from ucns_v04 import (
UCNSObject, AnchorPayload,
unit_obj, multiply,
)


Expand All @@ -57,7 +63,7 @@
CLASS_PUNCT_JUNCTURE = 9
CLASS_PUNCT_OPEN = 10
CLASS_PUNCT_CLOSE = 11
CLASS_PUNCT_QUOTE = 12 # reserved; quote glyphs are routed to OPEN/CLOSE
CLASS_PUNCT_QUOTE = 12 # reserved but unpopulated; quote glyphs are routed to OPEN/CLOSE
CLASS_PUNCT_AFFIX = 13
CLASS_PUNCT_MODAL = 14
CLASS_NUMERAL = 15
Expand All @@ -75,25 +81,20 @@

# ---------- helpers ----------

def _class_anchor(class_idx: int) -> Fraction:
"""Anchor position on the 16-gonal lattice (in turns)."""
return Fraction(class_idx, HOST_CARRIER)


def _wrap_with_class(class_idx: int, payload: Optional[UCNSObject]) -> UCNSObject:
"""
Encode class and feature payload as a 2-anchor object on a 32-gonal lattice.

Anchor 0: at position 0 (structural marker, no payload).
Anchor 1: at position (class_idx + 1) / 32, carrying the feature payload.
Anchor 1: at position (class_idx + 1) / (2 * HOST_CARRIER), carrying the feature payload.

After normalize, the first anchor stays at 0 (it's already there), and
the second stays at (class_idx + 1) / 32 — a relative offset that
the second stays at (class_idx + 1) / (2 * HOST_CARRIER) — a relative offset that
encodes the class index uniquely for class_idx ∈ {0..15}.
"""
class_offset = Fraction(class_idx + 1, 32)
class_offset = Fraction(class_idx + 1, 2 * HOST_CARRIER)
return UCNSObject(
n_dec=32,
n_dec=2 * HOST_CARRIER,
n_min=1, # normalize will recompute
anchors_pos=(
AnchorPayload(Fraction(0), None),
Expand Down Expand Up @@ -352,11 +353,11 @@ def _lcm(a, b):

# Whitespace: kind 0=space, 1=tab, 2=newline, 3=double-newline, 4=non-breaking
WHITESPACE_TABLE = {
" ": {"kind": (0, 5), "breaking": (0, 2)},
"\t": {"kind": (1, 5), "breaking": (0, 2)},
"\n": {"kind": (2, 5), "breaking": (0, 2)},
" ": {"kind": (0, 5), "breaking": (0, 2)},
"\t": {"kind": (1, 5), "breaking": (0, 2)},
"\n": {"kind": (2, 5), "breaking": (0, 2)},
"\n\n":{"kind": (3, 5), "breaking": (0, 2)},
" ": {"kind": (4, 5), "breaking": (1, 2)}, # NBSP
"\xa0": {"kind": (4, 5), "breaking": (1, 2)}, # U+00A0 NON-BREAKING SPACE
}

# Sentence terminals (modality)
Expand All @@ -379,15 +380,15 @@ def _lcm(a, b):
"(": {"shape": (0, 5)},
"[": {"shape": (1, 5)},
"{": {"shape": (2, 5)},
"“": {"shape": (3, 5)}, # “
"‘": {"shape": (4, 5)}, # ‘
"“": {"shape": (3, 5)}, # “ LEFT DOUBLE QUOTATION MARK
"‘": {"shape": (4, 5)}, # ‘ LEFT SINGLE QUOTATION MARK
}
PAIR_CLOSE_TABLE = {
")": {"shape": (0, 5)},
"]": {"shape": (1, 5)},
"}": {"shape": (2, 5)},
"”": {"shape": (3, 5)}, # ”
"’": {"shape": (4, 5)}, # ’
"”": {"shape": (3, 5)}, # ” RIGHT DOUBLE QUOTATION MARK
"’": {"shape": (4, 5)}, # ’ RIGHT SINGLE QUOTATION MARK
}

# Affix and connective marks
Expand Down Expand Up @@ -424,8 +425,15 @@ def _build_dispatch_table():

Phonetic variants ('a' / 'an') are canonically merged: both produce the
same UCNS object since they differ only in surface phonology.

Collision convention: when a token appears in multiple class tables the
first entry wins (silent; collisions are documented in the module docstring).
By table-ordering convention:
- 'since' and 'until' resolve to the preposition sense
- 'as' resolves to the preposition sense
- 'that' resolves to the determiner sense
"""
PHONETIC_VARIANTS = {"an": "a"} # an a as canonical encoding
PHONETIC_VARIANTS = {"an": "a"} # an -> a as canonical encoding

out: Dict[str, Tuple[int, Dict[str, Tuple[int, int]]]] = {}

Expand All @@ -434,7 +442,7 @@ def add(token, class_idx, feats):
# Skip — variant will be aliased after the canonical is in place.
return
if token in out:
return # earlier class entry wins
return
out[token] = (class_idx, feats)

for w, feats in PRONOUN_TABLE.items():
Expand Down Expand Up @@ -530,11 +538,10 @@ def class_of(obj: UCNSObject) -> Optional[int]:
return None
if obj.anchors_pos[0].payload is not None:
return None
# Second anchor's theta is (class_idx + 1) / 32 by construction.
# Second anchor's theta is (class_idx + 1) / (2 * HOST_CARRIER) by construction.
second_theta = obj.anchors_pos[1].theta
expected_denominator_factor = 32
# Reconstruct class_idx: theta * 32 - 1.
# Use exact rational arithmetic: theta = (k+1)/32 → k = theta*32 - 1.
expected_denominator_factor = 2 * HOST_CARRIER
# Reconstruct class_idx: theta * (2 * HOST_CARRIER) - 1.
candidate = second_theta * expected_denominator_factor - 1
if candidate.denominator != 1:
return None
Expand Down
Loading