core/evals/logos/fa1_gate.py
Shay cbfc8ccbf7 feat(evals,tests,docs,adr): FA-1 ruled — cross-language holonomy does not discriminate meaning (NO-GO)
The CORE-Logos "Crown Proof" — ADR-0015's claim that cross-language holonomy
closure is THE validation gate of meaning — was measured against a criterion
registered before the run, and refused.

  G1  separation, all negatives      AUC 0.557   (chance 0.500, bar 0.80)   FAIL
  G2  hardest class, cross-pairing   AUC 0.664   (bar 0.75)                 FAIL
  G3  word-order sensitivity         0.644       (bar 0.90)                 FAIL
  G4  no collapse re-entry           0 lost      (bar: 0)                   PASS

G4 is the half that succeeded: the repairs work. The claim is what failed --
which is the outcome the registration called "worth as much as a GO", because
it retires the largest piece of unearned design in the system.

Both preconditions had to be BUILT before the question could be asked at all,
which is why the 2026-06-14 negative constrained nothing: the shipped compiler
collapsed 37-53 coordinates, and holonomy_encode never closed. Measured on the
repaired ground with a genuine loop, over 1,016 aligned pairs and 58,375
mechanically-generated negatives in three classes, nothing hand-picked.

Diagnosis, not just a number: the encoding reacts more strongly to REORDERING a
clause (median deviation 41.2) than to CHANGING WHAT IT IS ABOUT (32.2). It
measures path shape, not content -- and that follows from the algebra, since
permutation acts on an ordered product through non-commutativity, a first-order
effect, while substituting one of three factors perturbs it only through that
factor. The negative classes do order correctly by meaning-distance
(aligned 27.2 < lexical 32.2 < order 41.2 < cross-pair 53.2), so the geometry is
weak signal rather than noise -- a correlation where the design asked for a gate.

Consequences, all landed here:

  * ADR-0005 and ADR-0015 AMENDED to record the refusal. The three-language
    architecture, the pack contract, morphology-as-structure and Hebrew root
    folding into geometry all survive untouched; only the claim that alignment
    or closure VALIDATES meaning is retired.
  * tests/test_fa1_gate_verdict.py is the tripwire: if a future encoding makes
    the gate real, it fails, and the amendment is withdrawn in the same commit
    that replaces it with a proof. Two sabotages observed red.
  * allow_cross_language_recall RULED by measurement (work-order item 3): it
    gates vault-recall depth over field states, has no language argument and no
    cross-language path, and reaches only walk_surface, which runtime_contracts
    declares telemetry. Stays ON, reclassified DEPLOYMENT, flip condition
    recorded. G-25's own claim that it was "the pillar's own switch" is
    CORRECTED -- that read the flag's name instead of its call graph, which is
    the error the audit exists to find.
  * A sixth defect found while building the harness: en_collapse_anchors_v1
    declares role "collapse_anchor", which is not a LanguageRole member, so
    load_pack() raises. It is registered in the resolver, consumed by opening
    lexicon.jsonl on a raw path that bypasses the loader, and is the target of
    all 24 alignment edges that still resolve to nothing. The enum's own comment
    records the identical failure once before (ADR-0097, domain_seed).

The repairs cross to the keel on their own merit regardless of the verdict:
geodesic blending removes every coordinate collision, and mount-wide edge
resolution connects 39 of the 63 dead edges. The collapse was damaging English's
own vocabulary; seeing that required no theory of meaning.

FA-1 is CLOSED: all four work-order items discharged, L2 verdict recorded as
defective-but-repairable with its central design claim retired.

[Verification]: uv run core test --suite smoke -q -> 786 passed in 239.45s, EXIT=0
                (+4 tests, +15s vs the prior 782/224s -- the FA-1 pin's stated cost).
                Doc-parsing pins re-run after the ADR and register amendments:
                test_adr_status_governance + test_adr_index -> 321 passed;
                test_flag_register -> 6 passed.
2026-07-28 14:37:07 -07:00

273 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""FA-1 — the pre-registered holonomy-gate experiment.
Criterion, corpus rules, negative classes and anti-gaming rules are registered in
`docs/analysis/fa1-holonomy-gate-preregistration.md` — committed at `94f05ba8`, and amended
once (A1, the corpus bound) before any discrimination number existed. This module executes
that registration; every threshold it applies is quoted from it, and none is computed here.
Run: ``uv run python -m evals.logos.fa1_gate``
"""
from __future__ import annotations
import json
from collections import defaultdict
from dataclasses import dataclass
from itertools import combinations, permutations
import numpy as np
from algebra.cl41 import geometric_product, reverse as cl_reverse
from algebra.versor import unitize_versor
from alignment.graph import load_alignment
from evals.logos.manifold_collapse import _census
from evals.logos.repaired_ground import IDENTITY, LOGOS_MOUNT, forward_walk, repaired_compiler
from packs.compiler import load_mounted_packs, load_pack_entries
#: Packs whose alignment.jsonl is read. Every edge they author participates; none is dropped
#: by hand. An edge whose endpoints do not resolve inside LOGOS_MOUNT is counted and reported.
ALIGNED_PACKS = ("he_logos_micro_v1", "grc_logos_micro_v1", "he_core_cognition_v1", "grc_logos_cognition_v1")
#: Three content words is ADR-0015's own canonical clause shape ("Logos, beginning, truth"),
#: and the shortest length at which word order can be broken in more than one way.
CLAUSE_LEN = 3
#: Pre-registered thresholds — anti-gaming rule 1 forbids editing these after numbers exist.
G1_AUC = 0.80
G2_AUC = 0.75
G3_ORDER_SENSITIVITY = 0.90
#: Amendment A1: cross-pair negatives per aligned pair, drawn deterministically from the
#: sorted enumeration. Classes 1 and 2 stay complete.
CROSS_NEGATIVES_PER_PAIR = 5
@dataclass(frozen=True)
class Concept:
"""One aligned meaning and its realisation in each language that carries it."""
key: str
surfaces: tuple[tuple[str, str], ...] # (language, surface), sorted — hashable
@property
def by_language(self) -> dict[str, str]:
return dict(self.surfaces)
@property
def languages(self) -> frozenset[str]:
return frozenset(language for language, _ in self.surfaces)
def _relation_stem(relation: str) -> str:
"""`cross_lang.logos.illumination.pointed.en` → `cross_lang.logos.illumination`."""
return relation.replace(".pointed", "").replace(".en_collapse", "").replace(".en", "")
def build_concepts() -> tuple[list[Concept], dict]:
"""Group every resolvable alignment edge into meanings. Mechanical; nothing hand-picked.
Where a language offers several surfaces for one meaning (`אור` / `אוֹר`), the
lexicographically first is taken — one rule, applied uniformly, fixed before the run.
"""
surface_of: dict[str, tuple[str, str]] = {}
for pack_id in LOGOS_MOUNT:
for entry in load_pack_entries(pack_id):
surface_of[entry.entry_id] = (entry.language, entry.surface)
grouped: dict[str, dict[str, set[str]]] = defaultdict(lambda: defaultdict(set))
edges = unresolved = 0
for pack_id in ALIGNED_PACKS:
for edge in load_alignment(pack_id).aligned_pairs("cross_lang"):
edges += 1
source, target = surface_of.get(edge.source_id), surface_of.get(edge.target_id)
if source is None or target is None:
unresolved += 1
continue
stem = _relation_stem(edge.relation)
grouped[stem][source[0]].add(source[1])
grouped[stem][target[0]].add(target[1])
concepts = [
Concept(key=stem, surfaces=tuple(sorted((lang, sorted(surfaces)[0]) for lang, surfaces in langs.items())))
for stem, langs in sorted(grouped.items())
if len(langs) >= 2
]
census = {
"edges_authored": edges,
"edges_unresolved_under_R3": unresolved,
"concepts": len(concepts),
"trilingual_concepts": sum(1 for concept in concepts if len(concept.languages) >= 3),
"clause_sets": len(list(combinations(range(len(concepts)), CLAUSE_LEN))),
}
return concepts, census
def _auc(positive: list[float], negative: list[float]) -> float:
"""P(a random aligned pair deviates LESS than a random negative). Ties count a half.
Computed by sorting rather than by the O(n·m) double loop — the corpus is ~10^5 negatives
and the naive form is the difference between seconds and an hour.
"""
if not positive or not negative:
return float("nan")
pos = np.sort(np.asarray(positive, dtype=np.float64))
neg = np.asarray(negative, dtype=np.float64)
strictly_less = np.searchsorted(pos, neg, side="left").sum() # positives below each negative
less_or_equal = np.searchsorted(pos, neg, side="right").sum()
ties = less_or_equal - strictly_less
return float((strictly_less + 0.5 * ties) / (len(pos) * len(neg)))
def run() -> dict:
concepts, census = build_concepts()
index_of = {concept: position for position, concept in enumerate(concepts)}
with repaired_compiler(LOGOS_MOUNT):
manifold = load_mounted_packs(LOGOS_MOUNT)
surfaces, distinct, groups = _census(manifold)
walk_cache: dict[tuple[tuple[str, ...], str], np.ndarray | None] = {}
def walk(sequence: tuple[Concept, ...], language: str) -> np.ndarray | None:
"""`F(X)` for one clause, memoised — the same ordered tuple recurs constantly."""
key = (tuple(concept.key for concept in sequence), language)
if key not in walk_cache:
try:
versors = [manifold.get_versor(concept.by_language[language]) for concept in sequence]
except KeyError:
walk_cache[key] = None
else:
walk_cache[key] = forward_walk(versors)
return walk_cache[key]
def deviation(sequence_a: tuple[Concept, ...], la: str, sequence_b: tuple[Concept, ...], lb: str) -> float | None:
"""`d(A,B) = ‖unitize(F(A)·reverse(F(B))) 1‖`, versor sign-ambiguity resolved."""
forward, backward = walk(sequence_a, la), walk(sequence_b, lb)
if forward is None or backward is None:
return None
holonomy = unitize_versor(geometric_product(forward, cl_reverse(backward)))
return float(min(np.linalg.norm(holonomy - IDENTITY), np.linalg.norm(holonomy + IDENTITY)))
# ---- the aligned corpus: every C(24,3) concept set, every language pair carrying it
aligned: list[float] = []
pairs: list[tuple[tuple[Concept, ...], str, str]] = []
clause_sets = list(combinations(concepts, CLAUSE_LEN))
for concept_set in clause_sets:
shared = sorted(frozenset.intersection(*(concept.languages for concept in concept_set)))
for position, la in enumerate(shared):
for lb in shared[position + 1 :]:
value = deviation(concept_set, la, concept_set, lb)
if value is None:
continue
aligned.append(value)
pairs.append((concept_set, la, lb))
# ---- class 1, lexical substitution (complete: every position × every replacement)
n_lexical: list[float] = []
for concept_set, la, lb in pairs:
for position in range(CLAUSE_LEN):
for replacement in concepts:
if replacement in concept_set or lb not in replacement.languages:
continue
broken = concept_set[:position] + (replacement,) + concept_set[position + 1 :]
value = deviation(concept_set, la, broken, lb)
if value is not None:
n_lexical.append(value)
# ---- class 2, word order (complete: every non-identity permutation of the return clause)
n_order: list[float] = []
order_sensitive = order_total = 0
for index, (concept_set, la, lb) in enumerate(pairs):
base = aligned[index]
for permuted in permutations(concept_set):
if permuted == concept_set:
continue
value = deviation(concept_set, la, permuted, lb)
if value is None:
continue
n_order.append(value)
order_total += 1
order_sensitive += int(value > base)
# ---- class 3, cross-pairing (Amendment A1: first 5 disjoint sets in sorted order)
n_cross: list[float] = []
cross_offered = 0
for concept_set, la, lb in pairs:
start = max(index_of[concept] for concept in concept_set)
taken = 0
for other in clause_sets:
if taken >= CROSS_NEGATIVES_PER_PAIR:
break
if min(index_of[concept] for concept in other) <= start:
continue
if set(other) & set(concept_set):
continue
cross_offered += 1
value = deviation(concept_set, la, other, lb)
if value is None:
continue
n_cross.append(value)
taken += 1
# ---- controls, reported regardless of outcome (anti-gaming rule 4)
self_loops = [
value
for value in (deviation(concept_set, la, concept_set, la) for concept_set, la, _ in pairs)
if value is not None
]
negatives = n_lexical + n_order + n_cross
g1 = _auc(aligned, negatives)
g2 = _auc(aligned, n_cross)
g3 = order_sensitive / order_total if order_total else float("nan")
g4_lost = surfaces - distinct
verdict: dict = {
"corpus": census,
"ground_after_repair": {
"mount": list(LOGOS_MOUNT),
"surfaces": surfaces,
"distinct_coordinates": distinct,
"coordinates_lost": g4_lost,
"collision_groups": len(groups),
},
"controls": {
"max_self_loop_deviation": max(self_loops) if self_loops else float("nan"),
"aligned_pairs": len(aligned),
"negatives_generated": {
"lexical_substitution": len(n_lexical),
"word_order": len(n_order),
"cross_pair": len(n_cross),
"cross_pair_candidates_offered": cross_offered,
},
},
"measurements": {
"median_aligned": float(np.median(aligned)) if aligned else float("nan"),
"median_negative": float(np.median(negatives)) if negatives else float("nan"),
# Per-class, for diagnosis. These are the pre-registered classes reported
# separately; no criterion is attached to them that is not already in §4.
"median_lexical": float(np.median(n_lexical)) if n_lexical else float("nan"),
"median_word_order": float(np.median(n_order)) if n_order else float("nan"),
"median_cross_pair": float(np.median(n_cross)) if n_cross else float("nan"),
"auc_vs_lexical": _auc(aligned, n_lexical),
"auc_vs_word_order": _auc(aligned, n_order),
"auc_vs_cross_pair": _auc(aligned, n_cross),
},
"criterion": {
"G1_separation": {"required": f"AUC >= {G1_AUC}", "observed": g1, "pass": bool(g1 >= G1_AUC)},
"G2_cross_pair": {"required": f"AUC >= {G2_AUC}", "observed": g2, "pass": bool(g2 >= G2_AUC)},
"G3_word_order": {"required": f">= {G3_ORDER_SENSITIVITY}", "observed": g3, "pass": bool(g3 >= G3_ORDER_SENSITIVITY)},
"G4_no_collapse": {"required": "coordinates_lost == 0", "observed": g4_lost, "pass": bool(g4_lost == 0)},
},
}
verdict["VERDICT"] = "GO" if all(gate["pass"] for gate in verdict["criterion"].values()) else "NO-GO"
return verdict
def main() -> int:
print(json.dumps(run(), indent=2, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())