diff --git a/generate/determine/__init__.py b/generate/determine/__init__.py new file mode 100644 index 00000000..8a3f76ef --- /dev/null +++ b/generate/determine/__init__.py @@ -0,0 +1,5 @@ +"""DETERMINE — reason over realized structure → assert (as-told) / refuse (roadmap Step 4).""" + +from generate.determine.determine import Determined, Undetermined, determine + +__all__ = ["Determined", "Undetermined", "determine"] diff --git a/generate/determine/determine.py b/generate/determine/determine.py new file mode 100644 index 00000000..17cac98b --- /dev/null +++ b/generate/determine/determine.py @@ -0,0 +1,119 @@ +"""DETERMINE (roadmap Step 4) — reason over realized structure → the honest gear. + +A question (a query-bearing ``Comprehension``) is answered ONLY from what the held +self has already REALIZED (R0/R1 structural recall) — never from the field, never +from an LLM, never from absence. The verdict is **as-told, never "verified"**: every +realizable record is SPECULATIVE, and ``ADMISSIBLE_AS_EVIDENCE = {COHERENT}``, so a +determination grounded in SPECULATIVE records carries ``basis="as_told"`` — "based on +what I was told (unverified)". Until COHERENT promotion exists (out of scope), D0 +produces only ``as_told`` assertions or ``Undetermined`` refusals. No estimation, no +corpus mutation (teaching stays HITL proposal-only). + +wrong=0 / soundness (open-world): D0 asserts an answer ONLY when the asked relation +is DIRECTLY entailed by a realized fact. Absence of a fact never refutes it +(open-world), so D0 never asserts an answer from missing knowledge — it refuses +(``Undetermined``). It asserts only ``answer=True`` on a direct hit; it never asserts +False. + +Slice D0 supports the realized, non-negated ``member`` relation (subsumption / +"Is X a Y?"). Negated questions and other query predicates are an honest +``Undetermined`` until their realized form + entailment predicate land — D0 ships no +entailment path it cannot exercise. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +from generate.meaning_graph.reader import Comprehension, Refusal +from generate.realize import RealizedRecord, recall_realized +from session.context import SessionContext +from teaching.epistemic import ADMISSIBLE_AS_EVIDENCE, EpistemicStatus + +#: The only realized relation D0 can reason over in this slice. +_SUPPORTED_PREDICATE = "member" + + +@dataclass(frozen=True, slots=True) +class Determined: + """An answer reasoned from realized structure. + + ``basis`` is the epistemic standing of the grounding: ``"as_told"`` when the + grounds are SPECULATIVE (candidate memory — the only case today), ``"verified"`` + only if every ground is admissible-as-evidence (COHERENT — not yet reachable). + ``answer`` is the truth of the asked (possibly negated) question. + """ + + answer: bool + basis: str + predicate: str + subject: str + object: str + grounds: tuple[RealizedRecord, ...] + + +@dataclass(frozen=True, slots=True) +class Undetermined: + """No honest answer (refusal). ``reason`` is for audit, not control.""" + + reason: str + + +def _basis(grounds: tuple[RealizedRecord, ...]) -> str: + """Carry the grounds' epistemic standing forward — never overclaim "verified".""" + statuses = {EpistemicStatus(g.epistemic_status) for g in grounds} + return "verified" if statuses and statuses <= ADMISSIBLE_AS_EVIDENCE else "as_told" + + +def determine( + question: Comprehension | Refusal, ctx: SessionContext +) -> Determined | Undetermined: + """Answer a membership question from realized structure, or refuse. + + Eligibility: a query-bearing ``Comprehension`` with exactly one ``member`` query. + The answer is asserted ONLY on direct structural entailment by a realized + ``member`` fact; everything else is a typed ``Undetermined`` (open-world: absence + never asserts a positive answer). + """ + if isinstance(question, Refusal): + return Undetermined("refusal") + if not isinstance(question, Comprehension): + return Undetermined("not_a_comprehension") + if len(question.queries) != 1: + return Undetermined("not_single_query") # a determination answers one question + + query = question.queries[0] + if query.predicate != _SUPPORTED_PREDICATE: + return Undetermined("unsupported_query") # honest: only `member` in D0 + if len(query.arguments) != 2: + return Undetermined("malformed_query") # member is binary by construction + if query.negated: + # Realized facts are all positive (R0/R1 refuse negated relations), so a + # negated question would only ever be answered from the positive's presence. + # D0 declines it explicitly rather than ship an entailment path the reader + # cannot exercise (it refuses negated membership questions upstream anyway). + return Undetermined("negated_query_unsupported") + + subject, target = query.arguments[0], query.arguments[1] + + # Structural recall: the realized member facts about this subject (R1a). Exact, + # deterministic, versor-collision-irrelevant — never a metric call. + facts = recall_realized(ctx, subject=subject, predicate=_SUPPORTED_PREDICATE) + if not facts: + return Undetermined("ungrounded") # nothing realized about the subject + + # Direct entailment: a realized member(subject, target) holds as-told. Open-world: + # member facts about the subject that do NOT match the asked target never refute + # it, so a miss is a refusal (``not_entailed``), never an asserted False. + grounding = next((f for f in facts if f.relation_arguments == (subject, target)), None) + if grounding is None: + return Undetermined("not_entailed") + + return Determined( + answer=True, + basis=_basis((grounding,)), + predicate=_SUPPORTED_PREDICATE, + subject=subject, + object=target, + grounds=(grounding,), + ) diff --git a/tests/test_determine_d0.py b/tests/test_determine_d0.py new file mode 100644 index 00000000..d44ebd96 --- /dev/null +++ b/tests/test_determine_d0.py @@ -0,0 +1,147 @@ +"""DETERMINE slice D0 — answer a membership question from realized structure, or refuse. + +The pinned tests BITE: D0 asserts only on direct entailment by a realized fact and +carries ``basis="as_told"`` (never "verified", since realized facts are SPECULATIVE); +a present-but-non-entailing question (a record about the subject exists, but not the +asked relation) must REFUSE, and removing the grounding flips Determined→Undetermined. +""" + +from __future__ import annotations + +import pytest + +from chat.runtime import ChatRuntime +from generate.determine import Determined, Undetermined, determine +from generate.meaning_graph.reader import comprehend +from generate.realize import realize_comprehension +from session.context import SessionContext + +_HIGH_INTERVAL = 10**9 + + +@pytest.fixture(scope="module") +def vocab_persona(): + rt = ChatRuntime(no_load_state=True) + return rt._context.vocab, rt._context.persona + + +def _ctx(vocab_persona) -> SessionContext: + vocab, persona = vocab_persona + return SessionContext(vocab=vocab, persona=persona, vault_reproject_interval=_HIGH_INTERVAL) + + +def _tell(text: str, ctx: SessionContext): + return realize_comprehension(comprehend(text), ctx) + + +def _ask(text: str, ctx: SessionContext): + return determine(comprehend(text), ctx) + + +# --------------------------------------------------------------------------- # +# Refuse the ungrounded — never assert from absence (open-world, wrong=0) +# --------------------------------------------------------------------------- # + + +def test_ask_before_realizing_is_undetermined(vocab_persona) -> None: + ctx = _ctx(vocab_persona) + res = _ask("Is truth a concept?", ctx) + assert isinstance(res, Undetermined) and res.reason == "ungrounded" + + +def test_non_question_is_undetermined(vocab_persona) -> None: + ctx = _ctx(vocab_persona) + _tell("Truth is a concept.", ctx) + # a declarative (no query) is intake, not a question to determine + assert isinstance(_ask("Truth is a concept.", ctx), Undetermined) + # a Refusal question + assert isinstance(determine(comprehend("The weather is nice today."), ctx), Undetermined) + + +# --------------------------------------------------------------------------- # +# Assert (as-told) on direct entailment by a realized fact +# --------------------------------------------------------------------------- # + + +def test_realized_fact_answers_its_own_question_as_told(vocab_persona) -> None: + ctx = _ctx(vocab_persona) + _tell("Truth is a concept.", ctx) + res = _ask("Is truth a concept?", ctx) + assert isinstance(res, Determined) + assert res.answer is True + assert res.basis == "as_told" # SPECULATIVE -> as-told, NEVER "verified" + assert res.predicate == "member" and res.subject == "truth" and res.object == "concept" + assert len(res.grounds) == 1 + assert res.grounds[0].relation_arguments == ("truth", "concept") + + +def test_negated_query_is_refused(vocab_persona) -> None: + # The reader refuses negated membership questions upstream, so determine() can + # only meet a negated `member` query via a hand-built comprehension. D0 declines + # it explicitly (ships no entailment path the reader cannot exercise). + from generate.meaning_graph.model import MeaningGraph, MeaningSpan + from generate.meaning_graph.reader import Comprehension, Query + + ctx = _ctx(vocab_persona) + _tell("Truth is a concept.", ctx) # positive member(truth, concept) realized + span = MeaningSpan(source_id="input", start=0, end=5, text="dummy") + negated = Comprehension( + meaning_graph=MeaningGraph(), + queries=(Query("member", ("truth", "concept"), span, negated=True),), + ) + res = determine(negated, ctx) + assert isinstance(res, Undetermined) and res.reason == "negated_query_unsupported" + + +# --------------------------------------------------------------------------- # +# Present-but-non-entailing — a record about the subject exists, but not the +# asked relation. MUST refuse (the discriminant bites: not "a record exists"). +# --------------------------------------------------------------------------- # + + +def test_present_but_non_entailing_is_refused(vocab_persona) -> None: + ctx = _ctx(vocab_persona) + _tell("Truth is a concept.", ctx) # member(truth, concept) realized + res = _ask("Is truth a number?", ctx) # member(truth, number) NOT realized + assert isinstance(res, Undetermined) and res.reason == "not_entailed" + + +def test_other_subject_does_not_ground(vocab_persona) -> None: + ctx = _ctx(vocab_persona) + _tell("Knowledge is a concept.", ctx) + # nothing realized about `truth` -> ungrounded, not a stray match on `concept` + assert isinstance(_ask("Is truth a concept?", ctx), Undetermined) + + +# --------------------------------------------------------------------------- # +# Mutation bite — the verdict is entailment, not "a record exists" +# --------------------------------------------------------------------------- # + + +def test_verdict_is_entailment_not_mere_presence(vocab_persona) -> None: + # With the grounding present -> Determined; the SAME question against a fresh + # context with NO realized facts -> Undetermined. Proves the Determined verdict + # is carried by the realized fact, not by the question alone. + grounded = _ctx(vocab_persona) + _tell("Truth is a concept.", grounded) + assert isinstance(_ask("Is truth a concept?", grounded), Determined) + + empty = _ctx(vocab_persona) + assert isinstance(_ask("Is truth a concept?", empty), Undetermined) + + +def test_unsupported_query_predicate_is_refused(vocab_persona) -> None: + # A non-`member` query (e.g. subset/compare) is an honest refusal in D0 — built + # by hand so the branch bites regardless of which predicates the reader emits. + from generate.meaning_graph.model import MeaningGraph, MeaningSpan + from generate.meaning_graph.reader import Comprehension, Query + + ctx = _ctx(vocab_persona) + _tell("Truth is a concept.", ctx) + span = MeaningSpan(source_id="input", start=0, end=5, text="dummy") + subset_q = Comprehension( + meaning_graph=MeaningGraph(), + queries=(Query("subset", ("concept", "thought"), span),), + ) + res = determine(subset_q, ctx) + assert isinstance(res, Undetermined) and res.reason == "unsupported_query"