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:
if _RUST and np.result_type(V, F) != np.dtype(np.float64):
return np.asarray(_rs.versor_apply(V, F), dtype=np.float32)
"""Apply a versor through the canonical algebra closure boundary.
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
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:
dtype = np.result_type(V, F)
if dtype not in (np.dtype(np.float32), np.dtype(np.float64)):
dtype = np.dtype(np.float32)
V = np.asarray(V, 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:

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import dataclass, replace
import re
from collections.abc import Sequence
from typing import List
@ -369,19 +369,19 @@ class ChatRuntime:
)
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(
articulation,
result.tokens,
role=dialogue_role,
)
walk_surface = sentence_plan.surface
surface = _terminate_surface(
articulation.surface,
role=dialogue_role,
output_language=self.config.output_language,
)
articulation_surface = surface
articulation_surface = articulation.surface
vault_hits = int(result.vault_hits)
turn_event = TurnEvent(

View file

@ -2,9 +2,9 @@
Structured proposition generation.
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
CGA inner product against that relation; vocabulary points then instantiate
the frame slots.
prompt and field form a relation blade; a frame is selected by exact CGA
inner product against that relation; vocabulary points then instantiate the
frame slots.
"""
from __future__ import annotations
@ -142,8 +142,8 @@ def propose(
) -> Proposition:
"""Generate one structured proposition from the live field."""
prompt = _prompt_versor(field_state)
relation = outer_product(prompt, field_state.F)
frame = frame_registry.select(relation)
frame_relation = _frame_query_relation(field_state)
frame = frame_registry.select(frame_relation)
candidate_indices = _candidate_indices_for_language(vocab, output_lang)
subject_word, subject_idx = _nearest_content_word(
@ -160,6 +160,12 @@ def propose(
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_versor: np.ndarray | None = None
if _frame_wants_object(frame):
@ -183,8 +189,8 @@ def propose(
object_=object_surface,
surface=surface,
frame_id=frame.frame_id,
subject_versor=vocab.get_versor_at(subject_idx),
predicate_versor=vocab.get_versor_at(predicate_idx),
subject_versor=subject_versor,
predicate_versor=predicate_versor,
object_versor=object_versor,
relation=relation,
)
@ -275,6 +281,15 @@ def _prompt_versor(field_state: FieldState) -> np.ndarray:
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(
vocab,
query: np.ndarray,
@ -288,14 +303,29 @@ def _nearest_content_word(
if _has_word(vocab, surface)
}
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:
selected = _nearest_by_pos(vocab, query, blocked, preferred_pos, candidate_indices)
if selected is not None:
return selected
try:
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)
return _nearest_by_cga(vocab, query, blocked, candidates)
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(
@ -308,6 +338,7 @@ def _nearest_by_pos(
best_score = -np.inf
best: tuple[str, int] | None = None
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:
if idx in blocked:
continue
@ -317,7 +348,7 @@ def _nearest_by_pos(
pos = None if morphology is None else dict(morphology.inflection).get("pos")
if pos not in preferred_pos:
continue
score = cga_inner(query, vocab.get_versor_at(idx))
score = cga_inner(vocab.get_versor_at(idx), query_arr)
if score > best_score:
best_score = score
best = (word, idx)