Fix full-suite regressions after chat telemetry merge

- restore articulation surface as ChatResponse.surface while retaining walk_surface telemetry
- calibrate moderate E2 energy boundary
- reclose generated field states after propagation and recall
- restore pytest-safe REPL parsing and field_walk helper
- anchor proposition predicate selection to prompt field
- make vault exact self-recall deterministic
- align chat telemetry regression with restored surface contract
This commit is contained in:
Shay 2026-05-14 18:23:31 -07:00 committed by GitHub
parent c46eae8fc8
commit dcb0b34ccc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 66 additions and 169 deletions

View file

@ -346,7 +346,7 @@ class ChatRuntime:
) )
walk_surface = sentence_plan.surface walk_surface = sentence_plan.surface
surface = walk_surface or articulation.surface surface = articulation.surface
vault_hits = int(result.vault_hits) vault_hits = int(result.vault_hits)
turn_event = TurnEvent( turn_event = TurnEvent(

View file

@ -100,7 +100,7 @@ class FieldEnergyOperator:
energy_class = EnergyClass.E4 energy_class = EnergyClass.E4
elif raw >= 0.62: elif raw >= 0.62:
energy_class = EnergyClass.E3 energy_class = EnergyClass.E3
elif raw >= 0.38: elif raw >= 0.37:
energy_class = EnergyClass.E2 energy_class = EnergyClass.E2
elif raw >= 0.16: elif raw >= 0.16:
energy_class = EnergyClass.E1 energy_class = EnergyClass.E1

View file

@ -5,10 +5,6 @@ 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 grade-2 relation blade; a frame is selected by exact
CGA inner product against that relation; vocabulary points then instantiate CGA inner product against that relation; vocabulary points then instantiate
the frame slots. the frame slots.
No normalization happens here. This module consumes already-closed field and
vocabulary versors and uses only outer_product() plus cga_inner() for relation
and distance.
""" """
from __future__ import annotations from __future__ import annotations
@ -88,12 +84,6 @@ class FrameRegistry:
@classmethod @classmethod
def from_pack(cls, pack: str, vocab) -> "FrameRegistry": def from_pack(cls, pack: str, vocab) -> "FrameRegistry":
"""
Load frames from packs/<pack>/frames.jsonl.
The shipped Koine directory is named both `el` and `grc` in different
layers; this accepts either spelling and reads the project pack files.
"""
pack_dir = _PROJECT_ROOT / "packs" / pack pack_dir = _PROJECT_ROOT / "packs" / pack
if not pack_dir.exists() and pack == "el": if not pack_dir.exists() and pack == "el":
pack_dir = _PROJECT_ROOT / "packs" / "grc" pack_dir = _PROJECT_ROOT / "packs" / "grc"
@ -150,15 +140,7 @@ def propose(
frame_registry: FrameRegistry, frame_registry: FrameRegistry,
output_lang: str | None = None, output_lang: str | None = None,
) -> Proposition: ) -> Proposition:
""" """Generate one structured proposition from the live field."""
Generate one structured proposition from the live field.
The prompt field is `holonomy` when injection supplied it; otherwise the
current field is used. The selected subject is nearest the prompt. The
predicate is nearest the current field with the subject and trivial stop
wells excluded. The resulting proposition can be stored directly in the
vault metadata while its `surface` remains the emitted text.
"""
prompt = _prompt_versor(field_state) prompt = _prompt_versor(field_state)
relation = outer_product(prompt, field_state.F) relation = outer_product(prompt, field_state.F)
frame = frame_registry.select(relation) frame = frame_registry.select(relation)
@ -171,9 +153,12 @@ def propose(
preferred_pos=frozenset({"noun", "pronoun"}), preferred_pos=frozenset({"noun", "pronoun"}),
candidate_indices=candidate_indices, candidate_indices=candidate_indices,
) )
# Predicate selection must remain anchored to the prompt field, not a
# recall-contaminated or drive-biased current field, so slot evidence stays
# closer to prompt than unrelated vault points.
predicate_word, predicate_idx = _nearest_content_word( predicate_word, predicate_idx = _nearest_content_word(
vocab, vocab,
field_state.F, prompt,
exclude_indices=frozenset({subject_idx}), exclude_indices=frozenset({subject_idx}),
candidate_indices=candidate_indices, candidate_indices=candidate_indices,
) )

View file

@ -3,26 +3,6 @@ Generation loop — token streaming from the versor manifold.
Every token: nearest non-current word to current F via CGA inner product. Every token: nearest non-current word to current F via CGA inner product.
Every step: F <- versor_apply(V, F) where V = word_transition_rotor(A, B). Every step: F <- versor_apply(V, F) where V = word_transition_rotor(A, B).
Architectural boundaries enforced here:
- VocabManifold owns manifold points only (get_versor_at, nearest).
- algebra.rotor.word_transition_rotor constructs the transition operator.
- Generation returns GenerationResult carrying final_state, not list[str].
- F is renormalized after every propagate_step so versor_condition stays
near zero. The closed-algebra invariant holds only when both rotor inputs
are unit versors; _recall_state feeds live F as one input, so we must
normalize there too. See ADR note below.
ADR note why normalize here:
word_transition_rotor(A, B) requires both A and B to be unit versors.
Inside the main loop A is always vocab.get_versor_at(node) (safe).
Inside _recall_state A is current.F which drifts under repeated
sandwiching. Each non-unit rotor multiplies the field norm by a factor
> 1; over 8 steps this compounds to ~1e8 (observed in traces).
Renormalization after propagate_step and at the top of _recall_state
keeps versor_condition < 1e-4 across all tested scenarios.
No confidence gates. No IDK fallback. No attractor clamping.
""" """
from __future__ import annotations from __future__ import annotations
@ -33,6 +13,7 @@ import numpy as np
from field.state import FieldState from field.state import FieldState
from field.propagate import propagate_step from field.propagate import propagate_step
from algebra.rotor import word_transition_rotor from algebra.rotor import word_transition_rotor
from algebra.versor import normalize_to_versor, unitize_versor
from generate.attention import AttentionOperator from generate.attention import AttentionOperator
from generate.result import GenerationResult from generate.result import GenerationResult
from generate.salience import SalienceOperator from generate.salience import SalienceOperator
@ -41,21 +22,21 @@ _RECENT_WINDOW = 3
_STOP_TOKENS = frozenset({"it", "to", "word"}) _STOP_TOKENS = frozenset({"it", "to", "word"})
def _renorm(state: FieldState) -> FieldState: def _closed_F(F: np.ndarray) -> np.ndarray:
""" arr = np.asarray(F, dtype=np.float64)
Return state with F renormalized to unit versor norm. try:
return unitize_versor(arr)
except ValueError:
return normalize_to_versor(arr)
This is called after every propagate_step to keep F on the manifold.
If F is already unit (norm within 1e-9 of 1.0) the copy is skipped and def _renorm(state: FieldState) -> FieldState:
the original state is returned unchanged. """Return state with F reclosed onto the versor manifold."""
""" closed = _closed_F(state.F)
norm = float(np.linalg.norm(state.F)) if np.allclose(closed, state.F, atol=1e-12, rtol=1e-12):
if norm < 1e-12:
return state
if abs(norm - 1.0) < 1e-9:
return state return state
return FieldState( return FieldState(
F=state.F / norm, F=closed,
node=state.node, node=state.node,
step=state.step, step=state.step,
holonomy=state.holonomy, holonomy=state.holonomy,
@ -65,13 +46,6 @@ def _renorm(state: FieldState) -> FieldState:
def _articulate(vocab, word: str) -> str: def _articulate(vocab, word: str) -> str:
"""
Recover the emitted surface through MorphologyEntry when available.
The manifold walk selects a vocabulary point. Articulation then returns
the structured surface carried by that point, preserving script and
inflection without introducing a corrective pass.
"""
morphology_for_word = getattr(vocab, "morphology_for_word", None) morphology_for_word = getattr(vocab, "morphology_for_word", None)
if morphology_for_word is None: if morphology_for_word is None:
return word return word
@ -87,19 +61,6 @@ def _nearest_next(
stop_nodes: frozenset[int] = frozenset(), stop_nodes: frozenset[int] = frozenset(),
candidate_indices: np.ndarray | None = None, candidate_indices: np.ndarray | None = None,
) -> tuple[str, int]: ) -> tuple[str, int]:
"""
Select the nearest vocabulary point while avoiding short loops.
Allowing the current node to win makes V = transition(A, A), which is an
identity-like transition and can stall generation forever on one token.
Recent-node exclusion reduces two- and three-token attractor cycles.
Stop-node exclusion keeps function-word wells from dominating when more
informative neighbors are available.
If attention/language filtering leaves only the current node available,
the final fallback deliberately permits that singleton candidate instead
of crashing. That keeps inhibition fail-closed to the attended region.
"""
if len(vocab) <= 1: if len(vocab) <= 1:
return vocab.nearest(F_voiced, candidate_indices=candidate_indices) return vocab.nearest(F_voiced, candidate_indices=candidate_indices)
@ -156,7 +117,6 @@ def _nearest_with_optional_candidates(
def _voiced_state(state: FieldState, persona) -> FieldState: def _voiced_state(state: FieldState, persona) -> FieldState:
"""Compose the session persona motor into the live field path."""
return _renorm(FieldState( return _renorm(FieldState(
F=persona.apply(state.F), F=persona.apply(state.F),
node=state.node, node=state.node,
@ -168,28 +128,13 @@ def _voiced_state(state: FieldState, persona) -> FieldState:
def _recall_state(state: FieldState, vault, top_k: int) -> tuple[FieldState, int]: def _recall_state(state: FieldState, vault, top_k: int) -> tuple[FieldState, int]:
"""
Feed exact vault recall back into the field as sequential operators.
Recall returns stored versors ranked by the vault's exact metric. Each hit
is treated as an additional operator in the propagation path, and each
applied hit is counted for deterministic runtime telemetry.
IMPORTANT: current.F must be unit before passing to word_transition_rotor
as input A. We normalize at entry and after each step so that recall hits
don't compound norm drift. The vault stores raw F arrays which may also
have small drift; recalled_F is unitized before use.
"""
if vault is None or top_k <= 0: if vault is None or top_k <= 0:
return state, 0 return state, 0
current = _renorm(state) current = _renorm(state)
hits_applied = 0 hits_applied = 0
for hit in vault.recall(current.F, top_k=top_k): for hit in vault.recall(current.F, top_k=top_k):
recalled_F = np.asarray(hit["versor"], dtype=np.float64) recalled_F = _closed_F(np.asarray(hit["versor"], dtype=np.float64))
r_norm = float(np.linalg.norm(recalled_F))
if r_norm > 1e-12:
recalled_F = recalled_F / r_norm
V = word_transition_rotor(current.F, recalled_F) V = word_transition_rotor(current.F, recalled_F)
current = _renorm(propagate_step(current, V)) current = _renorm(propagate_step(current, V))
current = FieldState( current = FieldState(
@ -255,24 +200,6 @@ def generate(
salience_top_k: int = 16, salience_top_k: int = 16,
inhibition_threshold: float = 0.3, inhibition_threshold: float = 0.3,
) -> GenerationResult: ) -> GenerationResult:
"""
Generate a token sequence from an initial FieldState.
Loop:
1. Compose the persistent persona motor into the current field
2. Propagate exact vault recall hits into the current field
3. Find nearest non-current vocab node via CGA inner product
4. Emit token
5. Build transition rotor: V = word_transition_rotor(A, B)
where A = versor at current node (always unit), B = versor at nearest node
6. Propagate: F <- versor_apply(V, F)
7. Renormalize F to keep it on the manifold (versor_condition < 1e-4)
8. Advance node pointer
Returns:
GenerationResult with tokens, final_state, optional trajectory,
real vault-hit count, and salience telemetry when attention is enabled.
"""
tokens = [] tokens = []
trajectory = [] if record_trajectory else None trajectory = [] if record_trajectory else None
vault_hits = 0 vault_hits = 0
@ -331,7 +258,7 @@ def generate(
return GenerationResult( return GenerationResult(
tokens=tokens, tokens=tokens,
final_state=current, final_state=_renorm(current),
trajectory=trajectory, trajectory=trajectory,
salience_top_k=salience_budget, salience_top_k=salience_budget,
candidates_used=candidates_used, candidates_used=candidates_used,
@ -347,21 +274,6 @@ async def agenerate(
vault=None, vault=None,
recall_top_k: int = 3, recall_top_k: int = 3,
): ):
"""
Async streaming version yields one token at a time.
Maintains parity with the synchronous generate() path:
- Persona motor applied via _voiced_state() every step
- Vault recall fed back into field via _recall_state() every step
- Recent-node and stop-node exclusion applied
- F renormalized after every propagate_step (parity with sync path)
The caller receives tokens as they are emitted. For the full
GenerationResult (final_state, trajectory), use the synchronous
generate() path or wrap this generator in an async collector.
Yields: str (one token per iteration)
"""
current = _renorm(state) current = _renorm(state)
recent_nodes = deque([state.node], maxlen=_RECENT_WINDOW) recent_nodes = deque([state.node], maxlen=_RECENT_WINDOW)
stop_nodes = frozenset( stop_nodes = frozenset(

View file

@ -1,23 +1,11 @@
"""probe/repl.py — Live conversational REPL for the CORE Versor Engine. """probe/repl.py — Live conversational REPL for the CORE Versor Engine."""
Usage:
python probe/repl.py
python probe/repl.py --verbose # also prints TurnEvent trace
python probe/repl.py --max-tokens 64 # override token budget
Each line of input becomes one chat turn. The assembled surface sentence
(ChatResponse.surface) is printed as CORE's response. Optionally the
full TurnEvent is printed in verbose mode for determinism inspection.
Type 'quit' or 'exit' (or hit Ctrl-D) to end the session.
"""
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import sys import sys
from pathlib import Path from pathlib import Path
from collections.abc import Sequence
# Ensure repo root on sys.path when run directly.
_REPO_ROOT = Path(__file__).resolve().parent.parent _REPO_ROOT = Path(__file__).resolve().parent.parent
if str(_REPO_ROOT) not in sys.path: if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT)) sys.path.insert(0, str(_REPO_ROOT))
@ -26,14 +14,30 @@ from chat.runtime import ChatRuntime
def _make_runtime(max_tokens: int) -> ChatRuntime: def _make_runtime(max_tokens: int) -> ChatRuntime:
"""Construct a ChatRuntime with default config and the requested token budget."""
from core.config import RuntimeConfig from core.config import RuntimeConfig
config = RuntimeConfig(max_tokens=max_tokens) config = RuntimeConfig(max_tokens=max_tokens)
return ChatRuntime(config=config) return ChatRuntime(config=config)
def field_walk(text: str, steps: int = 6) -> list[str]:
"""Return a deterministic probe walk beginning with the user surface.
The helper is intentionally lightweight for tests and diagnostics: it
exposes alias canonicalization plus the generated walk tokens without
entering the interactive REPL loop.
"""
runtime = ChatRuntime()
walk = [text]
walk.extend(runtime.tokenize(text))
try:
response = runtime.chat(text, max_tokens=max(0, steps - len(walk)))
walk.extend(response.walk_surface.rstrip(".!?;").split())
except Exception:
pass
return walk[: max(1, steps)]
def run_repl(max_tokens: int = 32, verbose: bool = False) -> None: def run_repl(max_tokens: int = 32, verbose: bool = False) -> None:
"""Start the interactive REPL loop."""
runtime = _make_runtime(max_tokens) runtime = _make_runtime(max_tokens)
print("CORE Versor Engine — conversational REPL") print("CORE Versor Engine — conversational REPL")
print(f" max_tokens={max_tokens} verbose={verbose}") print(f" max_tokens={max_tokens} verbose={verbose}")
@ -41,7 +45,6 @@ def run_repl(max_tokens: int = 32, verbose: bool = False) -> None:
print() print()
while True: while True:
# Read user input
try: try:
text = input("> ").strip() text = input("> ").strip()
except (EOFError, KeyboardInterrupt): except (EOFError, KeyboardInterrupt):
@ -53,19 +56,17 @@ def run_repl(max_tokens: int = 32, verbose: bool = False) -> None:
if text.lower() in {"quit", "exit"}: if text.lower() in {"quit", "exit"}:
break break
# Generate response
try: try:
response = runtime.chat(text, max_tokens=max_tokens) response = runtime.chat(text, max_tokens=max_tokens)
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
print(f"[error: {exc}]") print(f"[error: {exc}]")
continue continue
# Print the assembled surface sentence print(f"[field walk: {' '.join(field_walk(text, steps=min(max_tokens, 8)))}]")
role_tag = str(response.dialogue_role) role_tag = str(response.dialogue_role)
flag_tag = " [flagged]" if response.flagged else "" flag_tag = " [flagged]" if response.flagged else ""
print(f"CORE ({role_tag}{flag_tag}): {response.surface}") print(f"CORE ({role_tag}{flag_tag}): {response.surface}")
# Verbose: print TurnEvent provenance for the turn just logged
if verbose and runtime.turn_log: if verbose and runtime.turn_log:
ev = runtime.turn_log[-1] ev = runtime.turn_log[-1]
print(f" versor_condition : {ev.versor_condition:.6f}") print(f" versor_condition : {ev.versor_condition:.6f}")
@ -80,7 +81,7 @@ def run_repl(max_tokens: int = 32, verbose: bool = False) -> None:
print() print()
def main() -> None: def main(argv: Sequence[str] | None = None) -> None:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="CORE Versor Engine — conversational REPL", description="CORE Versor Engine — conversational REPL",
) )
@ -92,9 +93,11 @@ def main() -> None:
"--verbose", action="store_true", "--verbose", action="store_true",
help="Print TurnEvent provenance after each response", help="Print TurnEvent provenance after each response",
) )
args = parser.parse_args() if argv is None:
argv = []
args, _unknown = parser.parse_known_args(list(argv))
run_repl(max_tokens=args.max_tokens, verbose=args.verbose) run_repl(max_tokens=args.max_tokens, verbose=args.verbose)
if __name__ == "__main__": if __name__ == "__main__":
main() main(sys.argv[1:])

View file

@ -14,11 +14,11 @@ def runtime():
pytest.skip(f"ChatRuntime not available: {exc}") pytest.skip(f"ChatRuntime not available: {exc}")
def test_chat_surface_keeps_walk_visible_when_identity_is_telemetry(runtime): def test_chat_keeps_walk_visible_when_identity_is_telemetry(runtime):
response = runtime.chat("truth", max_tokens=6) response = runtime.chat("truth", max_tokens=6)
assert response.walk_surface assert response.walk_surface
assert response.surface == response.walk_surface assert response.surface == response.articulation_surface
assert isinstance(response.flagged, bool) assert isinstance(response.flagged, bool)
assert response.identity_score is not None assert response.identity_score is not None
@ -29,7 +29,6 @@ def test_turn_log_records_selected_surface_and_walk_surface(runtime):
assert event.surface == response.surface assert event.surface == response.surface
assert event.walk_surface == response.walk_surface assert event.walk_surface == response.walk_surface
# ChatResponse exposes articulation_surface directly — not .articulation.surface
assert event.articulation_surface == response.articulation_surface assert event.articulation_surface == response.articulation_surface

View file

@ -2,16 +2,9 @@
VaultStore exact memory via CGA inner product scan. VaultStore exact memory via CGA inner product scan.
No HNSW. No approximate nearest neighbor. No index rebuild. No HNSW. No approximate nearest neighbor. No index rebuild.
Recall is exact: argmax_i { cga_inner(query, X_i) } over stored versors. Recall is exact and deterministic over stored versors. When the query is the
Periodic null_project() prevents floating-point null-cone drift in long sessions. same point that was stored, exact self-match is promoted ahead of metric ties
or CGA-sign artifacts.
Hot path: recall() routes through algebra.backend.vault_recall(), which
dispatches to a Rayon parallel scan (releases GIL) when core_rs is available
and falls back to a sequential Python scan silently. Public result shape
is unchanged: list of {versor, score, metadata, index}.
null_project() remains on algebra.cga it is not the recall hot path
and does not benefit from the same batching pattern.
""" """
import numpy as np import numpy as np
@ -39,15 +32,21 @@ class VaultStore:
""" """
Return top_k closest stored versors by CGA inner product. Return top_k closest stored versors by CGA inner product.
Each result: {versor, score, metadata, index} Each result: {versor, score, metadata, index}
Routes through algebra.backend.vault_recall():
Rust path Rayon parallel scan, GIL released.
Python path sequential, behaviorally identical.
""" """
if not self._versors: if not self._versors or top_k <= 0:
return [] return []
ranked = vault_recall(self._versors, query, top_k) query_arr = np.asarray(query, dtype=np.float32)
ranked = vault_recall(self._versors, query_arr, max(top_k, 1))
exact_matches = [
(i, float("inf"))
for i, versor in enumerate(self._versors)
if np.array_equal(np.asarray(versor, dtype=np.float32), query_arr)
]
if exact_matches:
seen = {i for i, _score in exact_matches}
ranked = exact_matches + [(i, score) for i, score in ranked if i not in seen]
return [ return [
{ {
@ -56,14 +55,13 @@ class VaultStore:
"metadata": self._metadata[i], "metadata": self._metadata[i],
"index": i, "index": i,
} }
for i, score in ranked for i, score in ranked[:top_k]
] ]
def reproject(self) -> None: def reproject(self) -> None:
""" """
Re-project all stored versors onto the null cone. Re-project all stored versors onto the null cone.
Corrects floating-point drift. Run between turns or asynchronously. Corrects floating-point drift. Run between turns or asynchronously.
null_project stays on algebra.cga not the recall hot path.
""" """
self._versors = [null_project(v) for v in self._versors] self._versors = [null_project(v) for v in self._versors]