Fix remaining runtime regressions after contract cleanup

- close versor_apply outputs at algebra boundary
- route backend versor_apply through canonical closure semantics
- keep selected ChatResponse surface equal to ArticulationPlan surface
- derive proposition relation from selected slots
- rank proposition slots with pure CGA metric
This commit is contained in:
Shay 2026-05-14 19:05:36 -07:00 committed by GitHub
parent a683912ad2
commit 2bd70d0a9d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 74 additions and 23 deletions

View file

@ -32,8 +32,13 @@ def geometric_product(A: np.ndarray, B: np.ndarray) -> np.ndarray:
def versor_apply(V: np.ndarray, F: np.ndarray) -> np.ndarray: def versor_apply(V: np.ndarray, F: np.ndarray) -> np.ndarray:
if _RUST and np.result_type(V, F) != np.dtype(np.float64): """Apply a versor through the canonical algebra closure boundary.
return np.asarray(_rs.versor_apply(V, F), dtype=np.float32)
The Rust extension's raw sandwich path is intentionally bypassed here
until it enforces the same closure semantics as algebra.versor. Runtime
invariants depend on this operator returning a closed field; generation,
propagation, and vault recall are not allowed to repair it downstream.
"""
from algebra.versor import versor_apply as _va from algebra.versor import versor_apply as _va
return _va(V, F) return _va(V, F)

View file

@ -95,13 +95,28 @@ def construction_seed_versor(v: np.ndarray) -> np.ndarray:
def _close_applied_versor(v: np.ndarray, dtype: np.dtype) -> np.ndarray:
"""Close an algebra-produced sandwich result at the algebra boundary.
Generation, propagation, and vault recall are forbidden from normalizing
results. The algebra sandwich operator is the single place that owns this
closure because it is where numerical drift or table-level operator drift
becomes observable.
"""
try:
return unitize_versor(v).astype(dtype)
except ValueError:
return construction_seed_versor(v).astype(dtype)
def versor_apply(V: np.ndarray, F: np.ndarray) -> np.ndarray: def versor_apply(V: np.ndarray, F: np.ndarray) -> np.ndarray:
dtype = np.result_type(V, F) dtype = np.result_type(V, F)
if dtype not in (np.dtype(np.float32), np.dtype(np.float64)): if dtype not in (np.dtype(np.float32), np.dtype(np.float64)):
dtype = np.dtype(np.float32) dtype = np.dtype(np.float32)
V = np.asarray(V, dtype=dtype) V = np.asarray(V, dtype=dtype)
F = np.asarray(F, dtype=dtype) F = np.asarray(F, dtype=dtype)
return geometric_product(geometric_product(V, F), reverse(V)).astype(dtype) applied = geometric_product(geometric_product(V, F), reverse(V)).astype(dtype)
return _close_applied_versor(applied, dtype)
def versor_unit_residual(v: np.ndarray, *, allow_negative: bool = False) -> float: def versor_unit_residual(v: np.ndarray, *, allow_negative: bool = False) -> float:

View file

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass, replace
import re import re
from collections.abc import Sequence from collections.abc import Sequence
from typing import List from typing import List
@ -369,19 +369,19 @@ class ChatRuntime:
) )
self._context.turn += 1 self._context.turn += 1
surface = _terminate_surface(
articulation.surface,
role=dialogue_role,
output_language=self.config.output_language,
)
articulation = replace(articulation, surface=surface)
sentence_plan: SentencePlan = SentenceAssembler().assemble( sentence_plan: SentencePlan = SentenceAssembler().assemble(
articulation, articulation,
result.tokens, result.tokens,
role=dialogue_role, role=dialogue_role,
) )
walk_surface = sentence_plan.surface walk_surface = sentence_plan.surface
articulation_surface = articulation.surface
surface = _terminate_surface(
articulation.surface,
role=dialogue_role,
output_language=self.config.output_language,
)
articulation_surface = surface
vault_hits = int(result.vault_hits) vault_hits = int(result.vault_hits)
turn_event = TurnEvent( turn_event = TurnEvent(

View file

@ -2,9 +2,9 @@
Structured proposition generation. Structured proposition generation.
A proposition is the first structured assertion above the surface walk: A proposition is the first structured assertion above the surface walk:
prompt and field form a grade-2 relation blade; a frame is selected by exact prompt and field form a relation blade; a frame is selected by exact CGA
CGA inner product against that relation; vocabulary points then instantiate inner product against that relation; vocabulary points then instantiate the
the frame slots. frame slots.
""" """
from __future__ import annotations from __future__ import annotations
@ -142,8 +142,8 @@ def propose(
) -> Proposition: ) -> Proposition:
"""Generate one structured proposition from the live field.""" """Generate one structured proposition from the live field."""
prompt = _prompt_versor(field_state) prompt = _prompt_versor(field_state)
relation = outer_product(prompt, field_state.F) frame_relation = _frame_query_relation(field_state)
frame = frame_registry.select(relation) frame = frame_registry.select(frame_relation)
candidate_indices = _candidate_indices_for_language(vocab, output_lang) candidate_indices = _candidate_indices_for_language(vocab, output_lang)
subject_word, subject_idx = _nearest_content_word( subject_word, subject_idx = _nearest_content_word(
@ -160,6 +160,12 @@ def propose(
candidate_indices=candidate_indices, candidate_indices=candidate_indices,
) )
subject_versor = vocab.get_versor_at(subject_idx)
predicate_versor = vocab.get_versor_at(predicate_idx)
relation = outer_product(subject_versor, predicate_versor)
if float(np.linalg.norm(relation)) < 1e-8:
relation = frame_relation
object_word: str | None = None object_word: str | None = None
object_versor: np.ndarray | None = None object_versor: np.ndarray | None = None
if _frame_wants_object(frame): if _frame_wants_object(frame):
@ -183,8 +189,8 @@ def propose(
object_=object_surface, object_=object_surface,
surface=surface, surface=surface,
frame_id=frame.frame_id, frame_id=frame.frame_id,
subject_versor=vocab.get_versor_at(subject_idx), subject_versor=subject_versor,
predicate_versor=vocab.get_versor_at(predicate_idx), predicate_versor=predicate_versor,
object_versor=object_versor, object_versor=object_versor,
relation=relation, relation=relation,
) )
@ -275,6 +281,15 @@ def _prompt_versor(field_state: FieldState) -> np.ndarray:
return field_state.F return field_state.F
def _frame_query_relation(field_state: FieldState) -> np.ndarray:
left = field_state.holonomy if field_state.holonomy is not None else field_state.F
relation = outer_product(left, field_state.F)
if float(np.linalg.norm(relation)) >= 1e-8:
return relation
shifted = np.roll(np.asarray(field_state.F, dtype=np.float32), 1)
return outer_product(field_state.F, shifted)
def _nearest_content_word( def _nearest_content_word(
vocab, vocab,
query: np.ndarray, query: np.ndarray,
@ -288,14 +303,29 @@ def _nearest_content_word(
if _has_word(vocab, surface) if _has_word(vocab, surface)
} }
blocked = set(exclude_indices) | stop_indices blocked = set(exclude_indices) | stop_indices
candidates = range(len(vocab)) if candidate_indices is None else [int(idx) for idx in candidate_indices]
if preferred_pos: if preferred_pos:
selected = _nearest_by_pos(vocab, query, blocked, preferred_pos, candidate_indices) selected = _nearest_by_pos(vocab, query, blocked, preferred_pos, candidate_indices)
if selected is not None: if selected is not None:
return selected return selected
try: return _nearest_by_cga(vocab, query, blocked, candidates)
return vocab.nearest(query, exclude_indices=blocked, candidate_indices=candidate_indices)
except ValueError:
return vocab.nearest(query, exclude_indices=set(exclude_indices), candidate_indices=candidate_indices) def _nearest_by_cga(vocab, query: np.ndarray, blocked: set[int], candidates) -> tuple[str, int]:
best_score = -np.inf
best_idx = -1
query_arr = np.asarray(query, dtype=np.float32)
for idx in candidates:
idx = int(idx)
if idx in blocked:
continue
score = cga_inner(vocab.get_versor_at(idx), query_arr)
if score > best_score:
best_score = score
best_idx = idx
if best_idx < 0:
raise ValueError("No candidate word available after exclusions.")
return vocab.get_word_at(best_idx), best_idx
def _nearest_by_pos( def _nearest_by_pos(
@ -308,6 +338,7 @@ def _nearest_by_pos(
best_score = -np.inf best_score = -np.inf
best: tuple[str, int] | None = None best: tuple[str, int] | None = None
candidates = range(len(vocab)) if candidate_indices is None else [int(idx) for idx in candidate_indices] candidates = range(len(vocab)) if candidate_indices is None else [int(idx) for idx in candidate_indices]
query_arr = np.asarray(query, dtype=np.float32)
for idx in candidates: for idx in candidates:
if idx in blocked: if idx in blocked:
continue continue
@ -317,7 +348,7 @@ def _nearest_by_pos(
pos = None if morphology is None else dict(morphology.inflection).get("pos") pos = None if morphology is None else dict(morphology.inflection).get("pos")
if pos not in preferred_pos: if pos not in preferred_pos:
continue continue
score = cga_inner(query, vocab.get_versor_at(idx)) score = cga_inner(vocab.get_versor_at(idx), query_arr)
if score > best_score: if score > best_score:
best_score = score best_score = score
best = (word, idx) best = (word, idx)