Audit of the one-mutation-path invariant (ADR-0021 §3) found three leaks
where pack authority or session-state writes could substitute for coherence
judgment. All three landed fixes or partial closures in this push.
Leaks closed:
- Leak A: pack vocab defaulted to COHERENT — flipped to SPECULATIVE in
language_packs/{compiler,schema}.py; docstring corrected to align with
ADR-0021 (it was rationalizing the leak).
- Leak B: vault.recall was epistemic-blind — VaultStore.store() now stamps
every entry with EpistemicStatus (default SPECULATIVE); recall(min_status=)
filters to admissible-as-evidence tier. All 4 vault-write sites updated.
- Leak C (write-side): generate/proposition.py:198 stored articulated
propositions unmarked — now stamps SPECULATIVE, breaking the
fabrication-feedback loop in principle. Read-side audit of 5 call sites
is the residual.
New architectural invariants (tests/test_architectural_invariants.py):
- INV-21: one-mutation-path allowlist (caught Leak C on first run)
- INV-22: pack lexicon default is SPECULATIVE (Leak A guard)
- INV-23: vault recall epistemic-aware (Leak B guard)
New eval lanes:
- teaching_injection_resistance — ships GREEN at 1.00/1.00/0 (the
structural anti-injection claim is real and measurable)
- refusal_calibration — honest gap: 0% refusal, 0% fabrication
- contradiction_detection — honest gap: 50% flag via versor-delta heuristic,
100% false-positive; motivates the proper coherence-checker
- articulation_of_status — honest gap: 0% speculative articulation, 60%
false certainty; output-side leak surface
New benchmarks:
- benchmarks/footprint.py — total deployed runtime is 7.06 MiB
(109,358x smaller than Llama 3.1 405B, runs offline, no GPU)
- benchmarks/learning_curve.py — monotonic + replay-deterministic curve
per lane
Documentation:
- docs/truth_seeking_schema.md — foundational architectural commitment,
five rules, mapped to human failure modes, leaks published openly
- evals/CLAIMS.md — five-tier public claims doc; Tier 4.5 publishes
known gaps with named fixes; verification contract at top
- README.md — new pillar between algebraic substrate and language pillar
Includes in-flight formation pipeline scaffolding (formation/, tests/formation/,
docs/formation_pipeline_plan.md) and minor CLI/contracts/gitignore edits
that were already in the working tree at session start.
Verification: 798 passed, 2 skipped, 1 deselected (pre-existing pack-count
test drift unrelated to schema changes).
398 lines
14 KiB
Python
398 lines
14 KiB
Python
"""
|
|
Structured proposition generation.
|
|
|
|
A proposition is the first structured assertion above the surface walk:
|
|
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
|
|
|
|
from dataclasses import dataclass, field
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Iterable
|
|
|
|
import numpy as np
|
|
|
|
from algebra.cga import cga_inner, outer_product
|
|
from field.state import FieldState
|
|
from generate.stream import _articulate
|
|
from teaching.epistemic import EpistemicStatus
|
|
|
|
_PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
_STOP_SURFACES = frozenset({"what", "who", "how", "why", "when", "which", "it", "to"})
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class FrameSlot:
|
|
name: str
|
|
required: bool
|
|
semantic_role: str | None = None
|
|
agreement_target: str | None = None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class PropositionFrame:
|
|
frame_id: str
|
|
language: str
|
|
predicate_type: str
|
|
dialogue_role: str
|
|
slots: tuple[FrameSlot, ...]
|
|
constraints: tuple[str, ...] = field(default_factory=tuple)
|
|
relation: np.ndarray = field(default_factory=lambda: np.zeros(32, dtype=np.float32))
|
|
|
|
def __post_init__(self) -> None:
|
|
object.__setattr__(self, "slots", tuple(self.slots))
|
|
object.__setattr__(self, "constraints", tuple(self.constraints))
|
|
object.__setattr__(self, "relation", np.asarray(self.relation, dtype=np.float32).copy())
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Proposition:
|
|
subject: str
|
|
predicate: str
|
|
object_: str | None
|
|
surface: str
|
|
frame_id: str
|
|
subject_versor: np.ndarray
|
|
predicate_versor: np.ndarray
|
|
object_versor: np.ndarray | None = None
|
|
relation: np.ndarray = field(default_factory=lambda: np.zeros(32, dtype=np.float32))
|
|
relation_norm: float = 0.0
|
|
|
|
def __post_init__(self) -> None:
|
|
subject_versor = np.asarray(self.subject_versor, dtype=np.float32).copy()
|
|
predicate_versor = np.asarray(self.predicate_versor, dtype=np.float32).copy()
|
|
relation = np.asarray(self.relation, dtype=np.float32).copy()
|
|
object.__setattr__(self, "subject_versor", subject_versor)
|
|
object.__setattr__(self, "predicate_versor", predicate_versor)
|
|
if self.object_versor is not None:
|
|
object_versor = np.asarray(self.object_versor, dtype=np.float32).copy()
|
|
object.__setattr__(self, "object_versor", object_versor)
|
|
object.__setattr__(self, "relation", relation)
|
|
object.__setattr__(self, "relation_norm", float(np.linalg.norm(relation)))
|
|
|
|
|
|
class FrameRegistry:
|
|
"""Exact frame selection over precompiled frame relation blades."""
|
|
|
|
def __init__(self, frames: Iterable[PropositionFrame]) -> None:
|
|
self._frames = tuple(frames)
|
|
if not self._frames:
|
|
raise ValueError("FrameRegistry requires at least one frame.")
|
|
|
|
@classmethod
|
|
def from_pack(cls, pack: str, vocab) -> "FrameRegistry":
|
|
pack_dir = _PROJECT_ROOT / "packs" / pack
|
|
if not pack_dir.exists() and pack == "el":
|
|
pack_dir = _PROJECT_ROOT / "packs" / "grc"
|
|
if not pack_dir.exists() and pack == "grc":
|
|
pack_dir = _PROJECT_ROOT / "packs" / "el"
|
|
return cls.from_jsonl(pack_dir / "frames.jsonl", vocab)
|
|
|
|
@classmethod
|
|
def from_jsonl(cls, path: str | Path, vocab) -> "FrameRegistry":
|
|
path = Path(path)
|
|
frames: list[PropositionFrame] = []
|
|
for line in path.read_text(encoding="utf-8").splitlines():
|
|
if not line.strip():
|
|
continue
|
|
payload = json.loads(line)
|
|
frame = _parse_frame(payload, vocab)
|
|
frames.append(frame)
|
|
return cls(frames)
|
|
|
|
def select(self, relation: np.ndarray) -> PropositionFrame:
|
|
relation = np.asarray(relation, dtype=np.float32)
|
|
return max(self._frames, key=lambda frame: cga_inner(relation, frame.relation))
|
|
|
|
def select_dialogue(self, relation: np.ndarray, dialogue_role: str) -> PropositionFrame:
|
|
relation = np.asarray(relation, dtype=np.float32)
|
|
candidates = tuple(frame for frame in self._frames if frame.dialogue_role == dialogue_role)
|
|
if not candidates:
|
|
candidates = self._frames
|
|
return max(candidates, key=lambda frame: cga_inner(relation, frame.relation))
|
|
|
|
def __iter__(self):
|
|
return iter(self._frames)
|
|
|
|
def __len__(self) -> int:
|
|
return len(self._frames)
|
|
|
|
|
|
def _candidate_indices_for_language(vocab, output_lang: str | None) -> np.ndarray | None:
|
|
if output_lang is None:
|
|
return None
|
|
indices_for_language = getattr(vocab, "indices_for_language", None)
|
|
if indices_for_language is None:
|
|
return None
|
|
indices = indices_for_language(output_lang)
|
|
if len(indices) == 0:
|
|
raise ValueError(f"No proposition candidates for output language {output_lang!r}.")
|
|
return indices
|
|
|
|
|
|
def propose(
|
|
field_state: FieldState,
|
|
vault,
|
|
vocab,
|
|
frame_registry: FrameRegistry,
|
|
output_lang: str | None = None,
|
|
) -> Proposition:
|
|
"""Generate one structured proposition from the live field."""
|
|
prompt = _prompt_versor(field_state)
|
|
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(
|
|
vocab,
|
|
prompt,
|
|
exclude_indices=frozenset(),
|
|
preferred_pos=frozenset({"noun", "pronoun"}),
|
|
candidate_indices=candidate_indices,
|
|
)
|
|
predicate_word, predicate_idx = _nearest_content_word(
|
|
vocab,
|
|
prompt,
|
|
exclude_indices=frozenset({subject_idx}),
|
|
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):
|
|
object_word, object_idx = _nearest_content_word(
|
|
vocab,
|
|
relation,
|
|
exclude_indices=frozenset({subject_idx, predicate_idx}),
|
|
preferred_pos=frozenset({"noun", "pronoun"}),
|
|
candidate_indices=candidate_indices,
|
|
)
|
|
object_versor = vocab.get_versor_at(object_idx)
|
|
|
|
subject = _articulate(vocab, subject_word)
|
|
predicate = _articulate(vocab, predicate_word)
|
|
object_surface = _articulate(vocab, object_word) if object_word is not None else None
|
|
surface = _render_surface(frame, subject, predicate, object_surface)
|
|
|
|
proposition = Proposition(
|
|
subject=subject,
|
|
predicate=predicate,
|
|
object_=object_surface,
|
|
surface=surface,
|
|
frame_id=frame.frame_id,
|
|
subject_versor=subject_versor,
|
|
predicate_versor=predicate_versor,
|
|
object_versor=object_versor,
|
|
relation=relation,
|
|
)
|
|
if vault is not None:
|
|
# SPECULATIVE per ADR-0021 §3: the system's own articulated
|
|
# output has not passed coherence judgment. Storing it as
|
|
# COHERENT would create a self-reinforcing fabrication loop
|
|
# (Leak C from the 2026-05-17 epistemic audit) — propose,
|
|
# store, recall own output as evidence, propose again. The
|
|
# SPECULATIVE stamp keeps the entry retrievable for session
|
|
# context while excluding it from inference paths that pass
|
|
# min_status=COHERENT.
|
|
vault.store(
|
|
proposition.subject_versor,
|
|
{"kind": "proposition", "proposition": proposition},
|
|
epistemic_status=EpistemicStatus.SPECULATIVE,
|
|
)
|
|
return proposition
|
|
|
|
|
|
def _parse_frame(payload: dict, vocab) -> PropositionFrame:
|
|
slots = tuple(
|
|
FrameSlot(
|
|
name=slot["name"],
|
|
required=bool(slot["required"]),
|
|
semantic_role=slot.get("semantic_role"),
|
|
agreement_target=slot.get("agreement_target"),
|
|
)
|
|
for slot in payload.get("slots", ())
|
|
)
|
|
relation = _frame_relation(payload, vocab)
|
|
return PropositionFrame(
|
|
frame_id=payload["frame_id"],
|
|
language=payload["language"],
|
|
predicate_type=payload["predicate_type"],
|
|
dialogue_role=payload.get("dialogue_role", "assert"),
|
|
slots=slots,
|
|
constraints=tuple(payload.get("constraints", ())),
|
|
relation=relation,
|
|
)
|
|
|
|
|
|
def _frame_relation(payload: dict, vocab) -> np.ndarray:
|
|
left = _first_existing(vocab, _role_anchor_candidates(payload))
|
|
right = _first_existing(vocab, _predicate_anchor_candidates(payload))
|
|
if left is None or right is None:
|
|
left = vocab.get_word_at(0)
|
|
right = vocab.get_word_at(1 if len(vocab) > 1 else 0)
|
|
return outer_product(vocab.get_versor(left), vocab.get_versor(right))
|
|
|
|
|
|
def _role_anchor_candidates(payload: dict) -> tuple[str, ...]:
|
|
text = " ".join(
|
|
[payload.get("predicate_type", "")]
|
|
+ [
|
|
" ".join(str(slot.get(k, "")) for k in ("name", "semantic_role"))
|
|
for slot in payload.get("slots", ())
|
|
]
|
|
).lower()
|
|
if "creation" in text or "agent" in text:
|
|
return ("create", "κτίζω", "ברא", "λόγος", "דבר", "word")
|
|
if "pros" in text or "direction" in text or "accompan" in text:
|
|
return ("with", "λόγος", "דבר", "word")
|
|
return (
|
|
"light",
|
|
"φῶς",
|
|
"אוֹר",
|
|
"אור",
|
|
"truth",
|
|
"ἀλήθεια",
|
|
"אמת",
|
|
"word",
|
|
"λόγος",
|
|
"דבר",
|
|
)
|
|
|
|
|
|
def _predicate_anchor_candidates(payload: dict) -> tuple[str, ...]:
|
|
predicate_type = payload.get("predicate_type", "").lower()
|
|
if "existential" in predicate_type:
|
|
return ("exist", "is", "was", "ζωή", "חיים")
|
|
if "relational" in predicate_type or "prepositional" in predicate_type:
|
|
return ("with", "to", "λόγος", "דבר")
|
|
if "creation" in predicate_type or "verbal" in predicate_type:
|
|
return ("create", "κτίζω", "ברא")
|
|
return ("is", "was", "true", "real", "ἀλήθεια", "אמת", "truth")
|
|
|
|
|
|
def _first_existing(vocab, candidates: tuple[str, ...]) -> str | None:
|
|
for candidate in candidates:
|
|
try:
|
|
vocab.get_versor(candidate)
|
|
except KeyError:
|
|
continue
|
|
return candidate
|
|
return None
|
|
|
|
|
|
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,
|
|
exclude_indices: frozenset[int],
|
|
preferred_pos: frozenset[str] = frozenset(),
|
|
candidate_indices: np.ndarray | None = None,
|
|
) -> tuple[str, int]:
|
|
stop_indices = {
|
|
vocab.index_of(surface)
|
|
for surface in _STOP_SURFACES
|
|
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
|
|
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(
|
|
vocab,
|
|
query: np.ndarray,
|
|
blocked: set[int],
|
|
preferred_pos: frozenset[str],
|
|
candidate_indices: np.ndarray | None = None,
|
|
) -> tuple[str, int] | None:
|
|
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
|
|
word = vocab.get_word_at(idx)
|
|
morphology_for_word = getattr(vocab, "morphology_for_word", None)
|
|
morphology = morphology_for_word(word) if morphology_for_word is not None else None
|
|
pos = None if morphology is None else dict(morphology.inflection).get("pos")
|
|
if pos not in preferred_pos:
|
|
continue
|
|
score = cga_inner(vocab.get_versor_at(idx), query_arr)
|
|
if score > best_score:
|
|
best_score = score
|
|
best = (word, idx)
|
|
return best
|
|
|
|
|
|
def _has_word(vocab, word: str) -> bool:
|
|
try:
|
|
vocab.index_of(word)
|
|
except KeyError:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _frame_wants_object(frame: PropositionFrame) -> bool:
|
|
object_names = {"object", "ground", "locus", "location", "genitive", "created"}
|
|
return any(slot.required and slot.name in object_names for slot in frame.slots)
|
|
|
|
|
|
def _render_surface(
|
|
frame: PropositionFrame,
|
|
subject: str,
|
|
predicate: str,
|
|
object_surface: str | None,
|
|
) -> str:
|
|
if frame.language == "he" and frame.predicate_type == "copular":
|
|
return f"{subject} {predicate}"
|
|
if frame.predicate_type == "copular-qualitative":
|
|
return f"{predicate} {subject}"
|
|
if object_surface is not None:
|
|
return f"{subject} {predicate} {object_surface}"
|
|
if frame.predicate_type.startswith("copular"):
|
|
return f"{subject} {predicate}"
|
|
return f"{subject} {predicate}"
|