From 2bd70d0a9d0b2a370bb20c8e775d77f614c9921d Mon Sep 17 00:00:00 2001 From: Shay Date: Thu, 14 May 2026 19:05:36 -0700 Subject: [PATCH] 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 --- algebra/backend.py | 9 +++++-- algebra/versor.py | 17 ++++++++++++- chat/runtime.py | 16 ++++++------ generate/proposition.py | 55 ++++++++++++++++++++++++++++++++--------- 4 files changed, 74 insertions(+), 23 deletions(-) diff --git a/algebra/backend.py b/algebra/backend.py index 8f6a6066..ef65fd74 100644 --- a/algebra/backend.py +++ b/algebra/backend.py @@ -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) diff --git a/algebra/versor.py b/algebra/versor.py index 3bb98ad2..e5d557ae 100644 --- a/algebra/versor.py +++ b/algebra/versor.py @@ -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: diff --git a/chat/runtime.py b/chat/runtime.py index e6e807f8..cd23b7ed 100644 --- a/chat/runtime.py +++ b/chat/runtime.py @@ -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( diff --git a/generate/proposition.py b/generate/proposition.py index f75161a9..f35bc5c3 100644 --- a/generate/proposition.py +++ b/generate/proposition.py @@ -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)