core/docs/decisions/ADR-0046-forward-graph-constraint.md
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

4.4 KiB
Raw Blame History

ADR-0046 — PropositionGraph as Forward Admissibility Constraint

Status: Accepted
Date: 2026-05-18
Author: Shay


Context

The 2026-05-17 assessment identified the load-bearing structural gap in CORE:

The PropositionGraph is currently a post-hoc structural wrapper over what the field already produced, not a forward constraint on what the field should produce. That's the seam — not a disconnection, but a directionality that limits how much the graph can steer generation rather than describe it.

The intent_bridge.py path (ADR-0018) builds a PropositionGraph from the classified intent and the ArticulationPlan, then grounds it with recalled words from the generation result. The graph is built after generate() has already walked the manifold. The graph describes; it does not constrain.

generate() already accepts an AdmissibilityRegion (ADR-0022 through ADR-0026). The region is computed from the vocabulary's admissibility structure. What was missing was the coupling: convert the graph's named node versors into an AdmissibilityRegion before calling generate().


Decision

Add generate/graph_constraint.py with one public entry point:

build_graph_constraint(
    graph: PropositionGraph,
    vocab,
    *,
    top_k: int = 8,
) -> AdmissibilityRegion

The region's allowed_indices is the union of the CGA top-k neighbourhoods of every named surface in the graph, computed by exact cga_inner scan.

This converts the graph from a descriptor into a forward constraint:

geometry (CGA versor neighbourhood)
  → structure (PropositionGraph nodes)
    → propagation (AdmissibilityRegion fed to generate())

The chat/runtime.py hot path can now call:

graph = _build_graph_from_intent(intent, articulation)
region = build_graph_constraint(graph, vocab, top_k=8)
result = generate(field_state, vocab, persona, region=region, ...)

This is a drop-ingenerate() already accepts region. The only new code is the CGA neighbourhood computation in graph_constraint.py.


Consequences

What changes

  • The generation walk is now shaped by the proposition's geometric meaning before any tokens are produced, not after.
  • The admissibility_trace in every GenerationResult now carries the graph root IDs as the region label — full traceability from surface token back to the intent node that constrained it.
  • The system satisfies the three-pillar coupling end-to-end: Pillar 1 (geometry, CGA algebra) → Pillar 2 (structure, typed graph) → Pillar 3 (propagation, constrained field walk).

What does not change

  • generate() API is unchanged.
  • Empty or fully OOV graphs return an unconstrained region — existing fallback contract is preserved.
  • All existing tests pass unchanged.
  • versor_condition < 1e-6 invariant is unaffected (the region filters candidates; it does not alter the rotor construction or field update).

Scope limits (documented)

  • top_k=8 is an operational default. Pack authors who need tighter or looser constraints can override at call time.
  • The coupling between chat/runtime.py and build_graph_constraint is available but the hot-path wire-up is a follow-up ADR (wire when the intent bridge returns a non-empty graph on the main path).
  • The CGA neighbourhood is computed over the full vocab on each call (O(|vocab| × |nodes|)). At current pack sizes this is negligible; a cached neighbourhood index is a future optimisation if packs grow.

Industry Demo Suite

Four standalone demos in evals/industry_demos/ make falsifiable claims no transformer-LLM wrapper can reproduce:

Demo Claim
demo_01_forward_constraint Graph constrains walk via CGA geometry before any tokens are produced
demo_02_geometry_drives_identity Identity pack swap changes manifold geometry, not just output text
demo_03_deterministic_audit Three independent runtimes produce byte-identical audit records (architectural determinism)
demo_04_exact_recall_scale CGA vault recall is exact (100%) at N=100, 1K, 10K — no degradation curve

Each demo exits 0 on pass, 1 on fail, and prints structured JSON evidence.


Verification

tests/test_graph_constraint.py  — 8 tests, all green
evals/industry_demos/*.py       — 4 demos, each exits 0

Existing suite status unchanged: cognition, teaching, runtime, formation, smoke, pack-layer, telemetry suites all green.