diff --git a/alignment/__init__.py b/alignment/__init__.py new file mode 100644 index 00000000..9cee880e --- /dev/null +++ b/alignment/__init__.py @@ -0,0 +1,5 @@ +"""Alignment graph — cross-language resonance edges and holonomy proof cases.""" + +from .graph import AlignmentGraph, load_alignment + +__all__ = ["AlignmentGraph", "load_alignment"] diff --git a/alignment/graph.py b/alignment/graph.py new file mode 100644 index 00000000..e1e478d9 --- /dev/null +++ b/alignment/graph.py @@ -0,0 +1,88 @@ +""" +Alignment graph — cross-language resonance edges. + +AlignmentEdge records live in a pack's alignment.jsonl alongside its +lexicon.jsonl. This module loads them into a queryable in-memory graph. + +Design constraints: + - No numpy, no algebra imports. The graph is pure schema + stdlib. + Geometric verification belongs in tests or holonomy proofs, not here. + - load_alignment() is the single entry point. It reads bytes, not strings, + so the caller can checksum if needed. + - AlignmentGraph is immutable after construction (frozen edges tuple). +""" + +from __future__ import annotations + +import json +from pathlib import Path + +from language_packs.schema import AlignmentEdge + +_DATA_DIR = Path(__file__).parent.parent / "language_packs" / "data" + + +class AlignmentGraph: + """Immutable in-memory graph of AlignmentEdge records for one pack.""" + + def __init__(self, edges: list[AlignmentEdge]) -> None: + self._edges: tuple[AlignmentEdge, ...] = tuple(edges) + # Index by source_id for O(1) lookup on hot path + self._by_source: dict[str, list[AlignmentEdge]] = {} + for edge in self._edges: + self._by_source.setdefault(edge.source_id, []).append(edge) + + def __len__(self) -> int: + return len(self._edges) + + def edges_from(self, source_id: str) -> list[AlignmentEdge]: + """Return all edges originating from source_id.""" + return list(self._by_source.get(source_id, [])) + + def aligned_pairs(self, relation_prefix: str) -> list[AlignmentEdge]: + """Return all edges whose relation starts with relation_prefix.""" + return [ + e for e in self._edges + if e.relation.startswith(relation_prefix) + ] + + def get_edge(self, source_id: str, target_id: str) -> AlignmentEdge | None: + """Return the edge between source and target, or None.""" + for edge in self._by_source.get(source_id, []): + if edge.target_id == target_id: + return edge + return None + + @property + def edges(self) -> tuple[AlignmentEdge, ...]: + return self._edges + + +def _parse_edge(payload: dict) -> AlignmentEdge: + return AlignmentEdge( + source_id=payload["source_id"], + target_id=payload["target_id"], + relation=payload["relation"], + weight=float(payload["weight"]), + evidence_ids=tuple(payload.get("evidence_ids", [])), + ) + + +def load_alignment(pack_id: str) -> AlignmentGraph: + """ + Load AlignmentEdge records from language_packs/data//alignment.jsonl. + + Returns an empty AlignmentGraph if the file does not exist. + This is intentional: operational_base packs (en_minimal_v1) do not + currently carry cross-language alignment edges. + """ + alignment_path = _DATA_DIR / pack_id / "alignment.jsonl" + if not alignment_path.exists(): + return AlignmentGraph([]) + + edges: list[AlignmentEdge] = [] + for line in alignment_path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if line: + edges.append(_parse_edge(json.loads(line))) + return AlignmentGraph(edges) diff --git a/language_packs/data/grc_logos_micro_v1/alignment.jsonl b/language_packs/data/grc_logos_micro_v1/alignment.jsonl new file mode 100644 index 00000000..c9b910db --- /dev/null +++ b/language_packs/data/grc_logos_micro_v1/alignment.jsonl @@ -0,0 +1,7 @@ +{"source_id": "grc-001", "target_id": "he-001", "relation": "cross_lang.logos.utterance", "weight": 0.95, "evidence_ids": ["John1:1", "Gen1:1"]} +{"source_id": "grc-002", "target_id": "he-002", "relation": "cross_lang.logos.genesis", "weight": 0.93, "evidence_ids": ["John1:1", "Gen1:1"]} +{"source_id": "grc-003", "target_id": "he-003", "relation": "cross_lang.logos.illumination", "weight": 0.95, "evidence_ids": ["John1:4", "Gen1:3"]} +{"source_id": "grc-004", "target_id": "he-004", "relation": "cross_lang.logos.vitality", "weight": 0.92, "evidence_ids": ["John1:4"]} +{"source_id": "grc-005", "target_id": "he-005", "relation": "cross_lang.logos.aletheia", "weight": 0.94, "evidence_ids": ["John14:6"]} +{"source_id": "grc-006", "target_id": "he-006", "relation": "cross_lang.logos.pneuma", "weight": 0.96, "evidence_ids": ["Gen1:2", "John3:8"]} +{"source_id": "grc-007", "target_id": "he-007", "relation": "cross_lang.logos.ktizo", "weight": 0.91, "evidence_ids": ["Gen1:1", "John1:3"]} diff --git a/language_packs/data/he_logos_micro_v1/alignment.jsonl b/language_packs/data/he_logos_micro_v1/alignment.jsonl new file mode 100644 index 00000000..c2ec3515 --- /dev/null +++ b/language_packs/data/he_logos_micro_v1/alignment.jsonl @@ -0,0 +1,7 @@ +{"source_id": "he-001", "target_id": "grc-001", "relation": "cross_lang.logos.utterance", "weight": 0.95, "evidence_ids": ["John1:1", "Gen1:1"]} +{"source_id": "he-002", "target_id": "grc-002", "relation": "cross_lang.logos.genesis", "weight": 0.93, "evidence_ids": ["John1:1", "Gen1:1"]} +{"source_id": "he-003", "target_id": "grc-003", "relation": "cross_lang.logos.illumination", "weight": 0.95, "evidence_ids": ["John1:4", "Gen1:3"]} +{"source_id": "he-004", "target_id": "grc-004", "relation": "cross_lang.logos.vitality", "weight": 0.92, "evidence_ids": ["John1:4"]} +{"source_id": "he-005", "target_id": "grc-005", "relation": "cross_lang.logos.aletheia", "weight": 0.94, "evidence_ids": ["John14:6"]} +{"source_id": "he-006", "target_id": "grc-006", "relation": "cross_lang.logos.pneuma", "weight": 0.96, "evidence_ids": ["Gen1:2", "John3:8"]} +{"source_id": "he-007", "target_id": "grc-007", "relation": "cross_lang.logos.ktizo", "weight": 0.91, "evidence_ids": ["Gen1:1", "John1:3"]} diff --git a/tests/test_alignment_graph.py b/tests/test_alignment_graph.py new file mode 100644 index 00000000..1616e6fa --- /dev/null +++ b/tests/test_alignment_graph.py @@ -0,0 +1,154 @@ +"""Tests for the alignment graph and HolonomyAlignmentCase formal proof.""" + +from __future__ import annotations + +import numpy as np +import pytest + +from alignment.graph import load_alignment +from language_packs.schema import AlignmentEdge, HolonomyAlignmentCase +from algebra.holonomy import holonomy_encode, holonomy_similarity +from language_packs import load_pack + + +# --------------------------------------------------------------------------- +# Alignment graph loading +# --------------------------------------------------------------------------- + +def test_load_he_alignment_returns_seven_edges(): + graph = load_alignment("he_logos_micro_v1") + assert len(graph) == 7 + for edge in graph.edges: + assert isinstance(edge, AlignmentEdge) + assert 0.0 <= edge.weight <= 1.0 + assert edge.relation.startswith("cross_lang.") + + +def test_load_grc_alignment_returns_seven_edges(): + graph = load_alignment("grc_logos_micro_v1") + assert len(graph) == 7 + + +def test_load_en_alignment_returns_empty_graph(): + """Operational base packs carry no cross-language edges yet.""" + graph = load_alignment("en_minimal_v1") + assert len(graph) == 0 + + +def test_davar_logos_edge_weight_above_threshold(): + """דבר ↔ λόγος edge weight must be >= 0.9 (logos.utterance canonical pair).""" + graph = load_alignment("he_logos_micro_v1") + edge = graph.get_edge("he-001", "grc-001") + assert edge is not None, "he-001 → grc-001 edge missing" + assert edge.weight >= 0.9 + assert edge.relation == "cross_lang.logos.utterance" + + +def test_aligned_pairs_by_relation_prefix(): + """aligned_pairs() should filter by relation prefix correctly.""" + graph = load_alignment("he_logos_micro_v1") + all_cross = graph.aligned_pairs("cross_lang.logos") + assert len(all_cross) == 7 + + logos_only = graph.aligned_pairs("cross_lang.logos.utterance") + assert len(logos_only) == 1 + assert logos_only[0].source_id == "he-001" + + +def test_edges_from_source(): + graph = load_alignment("grc_logos_micro_v1") + edges = graph.edges_from("grc-001") + assert len(edges) == 1 + assert edges[0].target_id == "he-001" + + +# --------------------------------------------------------------------------- +# HolonomyAlignmentCase formal proof +# --------------------------------------------------------------------------- + +def _encode(manifold, tokens: list[str]) -> np.ndarray: + return holonomy_encode([manifold.get_versor(t) for t in tokens]) + + +def test_holonomy_alignment_case_positive_closer_than_negative(): + """ + Crown proof case: positive aligned triple must be geometrically closer + than the negative (misaligned) triple. + + This wraps the geometry proven in test_holonomy_resonance.py into the + formal HolonomyAlignmentCase schema type, so the proof is both + machine-checkable and linked to the schema's contract. + """ + case = HolonomyAlignmentCase( + case_id="HAC-001", + description=( + "Aligned Logos clause (word/דבר/λόγος + beginning/ראשית/ἀρχή + truth/אמת/ἀλήθεια) " + "produces closer holonomies across three languages than a misaligned clause " + "substituting ζωή (vitality) for ἀλήθεια (truth)." + ), + source_refs=("Gen1:1", "John1:1", "John14:6"), + pack_ids=("en_minimal_v1", "he_logos_micro_v1", "grc_logos_micro_v1"), + expected_relation="cross_lang.closer_than_negative", + negative_source_refs=("John1:4",), + tolerance=0.0, + ) + + # Validate the case schema itself + assert case.case_id == "HAC-001" + assert len(case.pack_ids) == 3 + assert len(case.source_refs) >= 2 + + # Load packs + _, en = load_pack("en_minimal_v1") + _, he = load_pack("he_logos_micro_v1") + _, grc = load_pack("grc_logos_micro_v1") + + # Positive triple: aligned canonical clause across all three languages + en_h = _encode(en, ["word", "beginning", "truth"]) + he_h = _encode(he, ["\u05d3\u05d1\u05e8", "\u05e8\u05d0\u05e9\u05d9\u05ea", "\u05d0\u05de\u05ea"]) + grc_h = _encode(grc, ["\u03bb\u03cc\u03b3\u03bf\u03c2", "\u1f00\u03c1\u03c7\u03ae", "\u1f00\u03bb\u03ae\u03b8\u03b5\u03b9\u03b1"]) + + # Negative: replace ἀλήθεια with ζωή — different semantic domain + grc_neg_h = _encode(grc, ["\u03bb\u03cc\u03b3\u03bf\u03c2", "\u1f00\u03c1\u03c7\u03ae", "\u03b6\u03c9\u03ae"]) + + # Positive score: mean distance of aligned cross-language pair + positive_dist = ( + np.linalg.norm(en_h - he_h) + + np.linalg.norm(en_h - grc_h) + + np.linalg.norm(he_h - grc_h) + ) / 3.0 + + # Negative score: distance when Greek clause uses misaligned token + negative_dist = ( + np.linalg.norm(en_h - he_h) + + np.linalg.norm(en_h - grc_neg_h) + + np.linalg.norm(he_h - grc_neg_h) + ) / 3.0 + + # The formal case assertion: aligned closer than misaligned + assert positive_dist < negative_dist, ( + f"HolonomyAlignmentCase {case.case_id} failed: " + f"positive_dist={positive_dist:.6f} >= negative_dist={negative_dist:.6f}. " + f"Case: {case.description}" + ) + + +def test_holonomy_alignment_case_schema_validation(): + """HolonomyAlignmentCase must reject under-specified instances.""" + with pytest.raises(ValueError, match="at least two source_refs"): + HolonomyAlignmentCase( + case_id="BAD-001", + description="missing refs", + source_refs=("Gen1:1",), # only one + pack_ids=("en_minimal_v1", "he_logos_micro_v1"), + expected_relation="cross_lang.closer_than_negative", + ) + + with pytest.raises(ValueError, match="at least two pack_ids"): + HolonomyAlignmentCase( + case_id="BAD-002", + description="missing packs", + source_refs=("Gen1:1", "John1:1"), + pack_ids=("en_minimal_v1",), # only one + expected_relation="cross_lang.closer_than_negative", + )