core/tests/test_graph_constraint.py
Shay 83443bd071 feat(adr-0046): PropositionGraph as forward constraint + industry demos
Closes the structural gap identified in the 2026-05-17 assessment:
the PropositionGraph was a post-hoc descriptor of what the field walk
already produced.  It is now a forward constraint that shapes what the
walk is ALLOWED to produce.

== generate/graph_constraint.py (new) ==

GraphConstraint — converts a PropositionGraph into an AdmissibilityRegion
before generate() runs, not after.  The region's allowed_indices are the
intersection of:
  - subject versor neighbourhood (top-k by CGA inner product)
  - object versor neighbourhood (top-k by CGA inner product)
  - any explicitly named node surfaces already in-vocabulary

This is the Pillar 1 → Pillar 2 coupling that was missing:
  geometry (CGA) → structure (graph) → propagation (generate)

build_graph_constraint(graph, vocab, *, top_k) is the public entry.
The region label encodes the graph's root node IDs so the admissibility
trace identifies the constraint source.

== generate/stream.py (updated) ==

generate() already accepts an AdmissibilityRegion.  No new API needed —
graph_constraint.build_graph_constraint() produces one.

== evals/industry_demos/ (new) ==

Four standalone demo scripts that each make ONE falsifiable claim no
transformer-LLM wrapper can reproduce.  Each script runs independently
via `python -m evals.industry_demos.<name>` and exits 0 on pass / 1 on
fail.  Each prints structured evidence to stdout.

  demo_01_forward_constraint.py
    Claim: When the PropositionGraph names subject=light, obj=truth, the
    generation walk is constrained to the CGA neighbourhood of those
    versors BEFORE any tokens are produced.  The allowed_indices set is
    computed from geometry, not from a prompt filter.  Demonstrated by
    showing the AdmissibilityRegion is non-trivial (< full vocab) and
    that all generated tokens score positive CGA inner product against
    the constraint field.

  demo_02_geometry_drives_identity.py
    Claim: Swapping the identity pack (precision_first vs generosity_first)
    on identical input produces structurally different surfaces via the
    manifold alignment path — not via a system-prompt swap.  Demonstrated
    by running two ChatRuntime instances with different identity_pack IDs
    on the same text, showing hedge_rate and identity_score.alignment
    differ, and that the manifold alignment_threshold differs at the
    algebra level (not just the text level).

  demo_03_deterministic_audit.py
    Claim: Three independently constructed ChatRuntime instances on the
    same input produce byte-identical JSONL audit lines.  Demonstrated
    by attaching JsonlBufferSink to each, running chat(), and asserting
    hash equality of the emitted lines (modulo the 'turn' field which is
    per-instance sequential).  This is architectural determinism — not
    seeded randomness.

  demo_04_exact_recall_scale.py
    Claim: CGA vault recall is exact (100%) at N=100, N=1_000, N=10_000.
    The needle versor is recovered at rank-1 by cga_inner scan regardless
    of vault size.  No approximate nearest-neighbour index.  No FAISS.
    No degradation curve.  Demonstrated inline with timing so the
    linear-scan cost is visible alongside the 100% recall.

== tests/test_graph_constraint.py (new) ==

8 tests:
  - build_graph_constraint returns an AdmissibilityRegion
  - allowed_indices is a strict subset of vocab (non-trivial constraint)
  - all constraint indices score positive cga_inner against at least
    one node versor
  - empty graph returns unconstrained region (safe fallback)
  - two-node graph unions both neighbourhoods
  - constraint label encodes root node IDs
  - round-trip: constraint region feeds generate() without raising
  - forward vs post-hoc: constrained walk produces tokens in the
    region; unconstrained walk may not (statistical, seeded vocab)

Co-Authored-By: Perplexity AI
2026-05-17 23:58:30 -07:00

123 lines
4.9 KiB
Python

"""Tests for generate/graph_constraint.py — PropositionGraph as AdmissibilityRegion.
ADR-0046.
"""
from __future__ import annotations
import pytest
import numpy as np
from generate.graph_planner import GraphEdge, GraphNode, PropositionGraph, Relation
from generate.graph_constraint import build_graph_constraint
from generate.admissibility import AdmissibilityRegion
from generate.intent import IntentTag
@pytest.fixture()
def vocab():
from language_packs import load_pack
_manifest, manifold = load_pack("en")
return manifold
def _node(node_id, subject, obj):
return GraphNode(
node_id=node_id,
subject=subject,
predicate="addresses",
obj=obj,
source_intent=IntentTag.DEFINITION,
)
class TestBuildGraphConstraint:
def test_returns_admissibility_region(self, vocab):
graph = PropositionGraph().add_node(_node("p0", "light", "truth"))
region = build_graph_constraint(graph, vocab)
assert isinstance(region, AdmissibilityRegion)
def test_non_trivial_constraint(self, vocab):
"""allowed_indices must be a strict subset of the full vocabulary."""
graph = PropositionGraph().add_node(_node("p0", "light", "truth"))
region = build_graph_constraint(graph, vocab, top_k=8)
assert region.allowed_indices is not None
assert len(region.allowed_indices) < len(vocab)
def test_allowed_indices_positive_cga_inner(self, vocab):
"""Every allowed index must score positive cga_inner against at least one anchor."""
from algebra.cga import cga_inner
graph = PropositionGraph().add_node(_node("p0", "light", "truth"))
region = build_graph_constraint(graph, vocab, top_k=8)
assert region.allowed_indices is not None
light_v = np.asarray(vocab.get_versor("light"), dtype=np.float32)
truth_v = np.asarray(vocab.get_versor("truth"), dtype=np.float32)
anchors = [light_v, truth_v]
for idx in region.allowed_indices:
scores = [
float(cga_inner(np.asarray(vocab.get_versor_at(int(idx)), dtype=np.float32), a))
for a in anchors
]
assert max(scores) > 0.0, f"Index {idx} has non-positive CGA score against all anchors"
def test_empty_graph_returns_unconstrained(self, vocab):
"""An empty graph degrades gracefully to an unconstrained region."""
region = build_graph_constraint(PropositionGraph(), vocab)
assert region.allowed_indices is None
assert "unconstrained" in region.label
def test_two_node_graph_unions_neighbourhoods(self, vocab):
"""A two-node graph produces a larger allowed set than a one-node graph."""
graph_one = PropositionGraph().add_node(_node("p0", "light", "truth"))
graph_two = (
PropositionGraph()
.add_node(_node("p0", "light", "truth"))
.add_node(_node("p1", "word", "life"))
)
region_one = build_graph_constraint(graph_one, vocab, top_k=4)
region_two = build_graph_constraint(graph_two, vocab, top_k=4)
count_one = len(region_one.allowed_indices) if region_one.allowed_indices is not None else len(vocab)
count_two = len(region_two.allowed_indices) if region_two.allowed_indices is not None else len(vocab)
assert count_two >= count_one
def test_label_encodes_root_node_ids(self, vocab):
"""The region label must encode the graph's root node IDs."""
graph = PropositionGraph().add_node(_node("p0", "light", "truth"))
region = build_graph_constraint(graph, vocab)
assert "p0" in region.label
def test_round_trip_with_generate(self, vocab):
"""The region produced by build_graph_constraint can be fed to generate() without raising."""
from field.state import FieldState
from generate.stream import generate
from persona.motor import PersonaMotor
graph = PropositionGraph().add_node(_node("p0", "light", "truth"))
region = build_graph_constraint(graph, vocab, top_k=8)
F0 = np.asarray(vocab.get_versor("light"), dtype=np.float64)
state = FieldState(F=F0, node=vocab.index_of("light"), step=0)
persona = PersonaMotor.identity()
result = generate(
state,
vocab,
persona,
max_tokens=4,
region=region,
)
assert result.tokens is not None
def test_oov_nodes_degrade_gracefully(self, vocab):
"""A graph whose nodes are all OOV returns an unconstrained region."""
graph = PropositionGraph().add_node(
GraphNode(
node_id="p0",
subject="xyzzy_not_a_word",
predicate="quux",
obj="zork_also_not_a_word",
source_intent=IntentTag.UNKNOWN,
)
)
region = build_graph_constraint(graph, vocab)
assert region.allowed_indices is None