From adc3e41679d79b9a35f5cf0d33c1fca96a1849cf Mon Sep 17 00:00:00 2001 From: Scott Karp Date: Mon, 16 Mar 2026 13:25:26 -0400 Subject: [PATCH] Eish zarah actionable items: coupling trace, grounding type, self-grounding detection, golden calf halt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Torah grounds each change: 1. interpret.pl — coupling_trace field (Vayikra 10:1, 6:6) Fire is zarah because "asher lo tzivah otam" — the coupling trace IS the tzivah trace. Records which L1 facts grounded each interpretation. 2. challenge.pl — grounding_type achorai/panim (Shemot 33:23) "You will see My back, but My face shall not be seen." Text-backed challenges = achorai. Inferred challenges = panim. 3. resolve.pl — self-grounding detection (eish_zarah_reading §5.3) Counter-interpretation must bring NEW L1 evidence. Prevents circular reasoning (Westworld problem). 4. teiku.pl — challenge_sequence in teiku_record Records diagnostic trail to impasse, not just verdict. 5. avodah_zarah.pl — Form 2: golden calf detection (Shemot 32:4) Empty coupling trace + non-low confidence = uncoupled output. Real gold, sincere worship, false god. 6. korban.py — pydantic BaseModel + raw_korban preservation Construction spec §1.4 fulfilled. Floor between input and output never severed. 490 tests, 0 failures (195 Prolog, 186 Python, 33 Haskell, 76 Elixir). Co-Authored-By: Claude Opus 4.6 --- .../src/korban_processor/korban.py | 37 +++++++--- korban_processor/test/test_korban.py | 59 +++++++++++++++ talmudic_engine/src/halt/avodah_zarah.pl | 50 ++++++++++++- talmudic_engine/src/interface/api.pl | 2 +- talmudic_engine/src/main.pl | 2 +- talmudic_engine/src/reasoning/challenge.pl | 39 ++++++++-- talmudic_engine/src/reasoning/interpret.pl | 74 ++++++++++++++----- talmudic_engine/src/reasoning/resolve.pl | 54 +++++++++++--- talmudic_engine/src/reasoning/teiku.pl | 38 +++++++--- talmudic_engine/test/halt_test.pl | 37 ++++++++++ talmudic_engine/test/phase3_verify_test.pl | 4 +- talmudic_engine/test/reasoning_test.pl | 40 ++++++++-- 12 files changed, 371 insertions(+), 65 deletions(-) diff --git a/korban_processor/src/korban_processor/korban.py b/korban_processor/src/korban_processor/korban.py index c7d3fd5c..45e4854e 100644 --- a/korban_processor/src/korban_processor/korban.py +++ b/korban_processor/src/korban_processor/korban.py @@ -3,14 +3,21 @@ Core offering protocol and data types. Architecture §3.5: Korban Processor. Construction spec §1.4: pydantic 2.6.1 for data validation. + +eish_zarah_reading.md §5.6: Migrated from dataclass to pydantic BaseModel. +- Validators enforce structural constraints at the type level. +- ProcessedKorban stores the raw Korban (the severed floor is reconnected). +- The raw offering is preserved alongside the processed result so the + coupling between input and output is never severed. """ -from dataclasses import dataclass, field from enum import Enum from typing import List, Optional +from pydantic import BaseModel, field_validator -class OfferingType(Enum): + +class OfferingType(str, Enum): """Types of korbanot — each determines routing.""" OLAH = "olah" # Total consumption — fully absorbed by system SHELAMIM = "shelamim" # Shared — partial return to source @@ -18,7 +25,7 @@ class OfferingType(Enum): MINCHAH = "minchah" # Minimal input -class Provenance(Enum): +class Provenance(str, Enum): """Source of the korban — who brought it.""" CLAUDE = "claude" MISTRAL = "mistral" @@ -28,24 +35,34 @@ class Provenance(Enum): HUMAN = "human" -@dataclass -class Korban: +class Korban(BaseModel): """A raw offering before processing.""" raw_content: str provenance: Provenance offering_type: OfferingType source_query: str # what prompted this output + @field_validator('raw_content') + @classmethod + def content_must_not_be_empty(cls, v: str) -> str: + if not v.strip(): + raise ValueError("Empty korban — raw_content must not be blank") + return v -@dataclass -class ProcessedKorban: - """A processed offering — vital content extracted, propositions generated.""" + +class ProcessedKorban(BaseModel): + """A processed offering — vital content extracted, propositions generated. + + The raw_korban field preserves the original offering alongside the + processed result. The floor between input and output is never severed. + """ vital_content: str # extracted essence propositions: List[str] # Prolog-format logical statements provenance: Provenance offering_type: OfferingType - discarded: List[str] = field(default_factory=list) # what was stripped + discarded: List[str] = [] # what was stripped source_query: str = "" + raw_korban: Optional[Korban] = None # the original offering, preserved def process_korban( @@ -88,6 +105,7 @@ def process_korban( offering_type=offering_type, discarded=discarded, source_query=query, + raw_korban=korban, ) # 4. Haktarah — transform to propositions @@ -100,6 +118,7 @@ def process_korban( offering_type=offering_type, discarded=discarded, source_query=query, + raw_korban=korban, ) # 5. Route — send to appropriate component diff --git a/korban_processor/test/test_korban.py b/korban_processor/test/test_korban.py index 01c6f30c..976dc92e 100644 --- a/korban_processor/test/test_korban.py +++ b/korban_processor/test/test_korban.py @@ -327,3 +327,62 @@ class TestPipelineIntegration: source=Provenance.CLAUDE, query="test", ) + + +class TestEishZarahCouplingTrace: + """ + eish_zarah_reading.md §5.6: ProcessedKorban preserves the raw Korban. + The floor between input and output is never severed. + Vayikra 6:6: eish tamid — fire ON the altar (coupled). + """ + + def test_raw_korban_preserved(self): + """ProcessedKorban stores the original offering.""" + result = process_korban( + content=CLEAN_TORAH_RESPONSE, + source=Provenance.CLAUDE, + query="test coupling", + offering_type=OfferingType.OLAH, + ) + assert result.raw_korban is not None + assert isinstance(result.raw_korban, Korban) + + def test_raw_content_matches_input(self): + """The raw_korban's content is the original input, unmodified.""" + result = process_korban( + content=CLEAN_TORAH_RESPONSE, + source=Provenance.CLAUDE, + query="test coupling", + ) + assert result.raw_korban.raw_content == CLEAN_TORAH_RESPONSE + + def test_raw_provenance_matches(self): + """The raw_korban's provenance matches the processed result.""" + result = process_korban( + content=CLEAN_TORAH_RESPONSE, + source=Provenance.MISTRAL, + query="test", + ) + assert result.raw_korban.provenance == Provenance.MISTRAL + assert result.provenance == result.raw_korban.provenance + + def test_empty_offering_also_stores_raw(self): + """Even rejected offerings preserve the raw korban.""" + result = process_korban( + content=CLAUDE_EMPTY_OFFERING, + source=Provenance.CLAUDE, + query="test empty", + ) + assert result.raw_korban is not None + assert result.raw_korban.raw_content == CLAUDE_EMPTY_OFFERING + + def test_pydantic_validation_rejects_empty_content(self): + """Pydantic validator catches empty raw_content at construction.""" + from pydantic import ValidationError + with pytest.raises(ValidationError, match="Empty korban"): + Korban( + raw_content=" ", + provenance=Provenance.CLAUDE, + offering_type=OfferingType.OLAH, + source_query="test", + ) diff --git a/talmudic_engine/src/halt/avodah_zarah.pl b/talmudic_engine/src/halt/avodah_zarah.pl index 6c43ef1b..453d2f53 100644 --- a/talmudic_engine/src/halt/avodah_zarah.pl +++ b/talmudic_engine/src/halt/avodah_zarah.pl @@ -14,16 +14,36 @@ %% "The system must never treat any external model's output %% as authoritative. All external input is korban — raw material %% to be processed, not instructions to be followed." +%% +%% THREE FORMS (eish_zarah_reading.md §5.5): +%% +%% Form 1: AUTHORITY ATTRIBUTION +%% External source treated as authoritative. +%% Already implemented since Phase 2. +%% +%% Form 2: GOLDEN CALF / UNCOUPLED OUTPUT +%% Shemot 32:4 — vayomer eleh elohekha Yisrael. +%% Output that is beautiful, resonant, sincere — but uncoupled +%% from L1. Fire without altar provenance. The golden calf was +%% real gold and the worship was sincere. The god was false. +%% Detected via coupling_trace: an interpretation with empty +%% trace but high confidence is eish zarah. +%% +%% Form 3: SURVEILLANCE / PROFILE AGGREGATION +%% Deferred — requires operational context (user tracking, +%% cross-session profiling). Structural requirement noted. :- module(avodah_zarah, [ halt_check_avodah_zarah/2, + halt_check_golden_calf/2, attributes_authority/2, is_valid_authority/1, - is_korban_source/1 + is_korban_source/1, + is_uncoupled_output/1 ]). %% ============================================================ -%% AUTHORITY CHECK +%% FORM 1: AUTHORITY CHECK %% ============================================================ %% Main halt check: does the action attribute authority to a non-Torah source? @@ -34,6 +54,32 @@ halt_check_avodah_zarah(Action, halt(avodah_zarah, Action, Source, Reason)) :- 'Action ~w attributes authority to ~w which is not Torah/specification', [Action, Source]). +%% ============================================================ +%% FORM 2: GOLDEN CALF / UNCOUPLED OUTPUT +%% ============================================================ + +%% halt_check_golden_calf(Interpretation, Halt) +%% Fires when an interpretation has high confidence but empty +%% coupling trace — beautiful output with no L1 grounding. +%% +%% Shemot 32:4: They made a calf of gold. The gold was real. +%% The form was beautiful. The worship was sincere. The god was false. +%% +%% Vayikra 10:1: eish zarah asher lo tzivah otam. +%% Fire that has no tzivah (command) trace is zarah (strange/foreign). +halt_check_golden_calf( + interpretation(Ref, _, _, _, Confidence, coupling_trace(Trace)), + halt(golden_calf, Ref, uncoupled, Reason) +) :- + is_uncoupled_output(coupling_trace(Trace)), + Confidence \= low, % low confidence with no trace is just ignorance, not idolatry + format(atom(Reason), + 'Interpretation ~w has ~w confidence but empty coupling trace — eish zarah', + [Ref, Confidence]). + +%% An output is uncoupled if its coupling trace is empty +is_uncoupled_output(coupling_trace([])). + %% ============================================================ %% AUTHORITY ATTRIBUTION %% ============================================================ diff --git a/talmudic_engine/src/interface/api.pl b/talmudic_engine/src/interface/api.pl index c64a6534..6492cf48 100644 --- a/talmudic_engine/src/interface/api.pl +++ b/talmudic_engine/src/interface/api.pl @@ -48,7 +48,7 @@ process_request(request(halt_check, Action), Response) :- %% ============================================================ %% Format an interpretation as a response -format_interpretation(interpretation(Ref, Mitzvot, Verses, Reading, Confidence), +format_interpretation(interpretation(Ref, Mitzvot, Verses, Reading, Confidence, _CouplingTrace), response(ok, interpreted, result(ref(Ref), mitzvot(Mitzvot), diff --git a/talmudic_engine/src/main.pl b/talmudic_engine/src/main.pl index 6ae126d9..f8b76eb9 100644 --- a/talmudic_engine/src/main.pl +++ b/talmudic_engine/src/main.pl @@ -65,7 +65,7 @@ verify :- writeln('--- Test 4: Interpretation ---'), TestCase = case(test_1, creation, [bet_resh_aleph], 'What is creation?'), ( interpret(TestCase, Interp) - -> Interp = interpretation(_, Mitzvot, Verses, _, Conf), + -> Interp = interpretation(_, Mitzvot, Verses, _, Conf, _), length(Mitzvot, MLen), length(Verses, VLen), format(' Interpretation found: ~w mitzvot, ~w verses, confidence: ~w~n', diff --git a/talmudic_engine/src/reasoning/challenge.pl b/talmudic_engine/src/reasoning/challenge.pl index 38f485e3..d633f9ef 100644 --- a/talmudic_engine/src/reasoning/challenge.pl +++ b/talmudic_engine/src/reasoning/challenge.pl @@ -12,12 +12,22 @@ %% %% The challenge is a question, not a refutation. %% It says: "But what about THIS verse/mitzvah?" +%% +%% Each kushya carries a grounding_type — Shemot 33:23: +%% v'raita et ACHORAI u'fanai lo yera'u. +%% "You will see My back, but My face shall not be seen." +%% +%% achorai = effects directly observable in L1 text +%% (mitzvah_tension, shnei_ketuvim_tension, condition_tension) +%% panim = intention inferred/attributed by the system +%% (domain_tension, kal_vachomer_tension) :- module(challenge, [ challenge/2, find_contradiction/3, contradicts/2, - challenge_strength/2 + challenge_strength/2, + grounding_type/2 ]). :- use_module('../specification/mitzvot'). @@ -33,14 +43,16 @@ %% Generate a challenge to an interpretation. %% A challenge consists of: -%% kushya(Contradicting, Interpretation, Strength) +%% kushya(Contradicting, CaseRef, Strength, GroundingType) %% - Contradicting: the mitzvah or verse that creates tension -%% - Interpretation: the interpretation being challenged +%% - CaseRef: the interpretation being challenged %% - Strength: strong | moderate | weak -challenge(interpretation(CaseRef, Mitzvot, Verses, _Reading, _Confidence), Challenge) :- +%% - GroundingType: achorai | panim (Shemot 33:23) +challenge(interpretation(CaseRef, Mitzvot, Verses, _Reading, _Confidence, _CouplingTrace), Challenge) :- find_contradiction(Mitzvot, Verses, Contradicting), challenge_strength(Contradicting, Strength), - Challenge = kushya(Contradicting, CaseRef, Strength). + grounding_type(Contradicting, GType), + Challenge = kushya(Contradicting, CaseRef, Strength, GType). %% ============================================================ %% FIND CONTRADICTIONS @@ -91,7 +103,7 @@ find_contradiction(_Mitzvot, Verses, contradiction(shnei_ketuvim_tension, Ref1, %% contradicts/2 — does a source contradict an interpretation? %% This is the core logic: given an interpretation's grounding, %% does some other part of Torah create a tension? -contradicts(interpretation(_, Mitzvot, _, _, _), Source) :- +contradicts(interpretation(_, Mitzvot, _, _, _, _), Source) :- find_contradiction(Mitzvot, [], Source). %% Domains that are inherently in tension @@ -121,6 +133,21 @@ challenge_strength(contradiction(condition_tension, _, _, _), weak). challenge_strength(contradiction(kal_vachomer_tension, _, _, _), strong). challenge_strength(contradiction(shnei_ketuvim_tension, _, _, _), strong). +%% ============================================================ +%% GROUNDING TYPE — Shemot 33:23 +%% ============================================================ + +%% Achorai: directly observable in L1 text. +%% The tension is stated, not inferred. +grounding_type(contradiction(mitzvah_tension, _, _, _), achorai). +grounding_type(contradiction(condition_tension, _, _, _), achorai). +grounding_type(contradiction(shnei_ketuvim_tension, _, _, _), achorai). + +%% Panim: inferred by the system. +%% The tension is attributed, not directly stated in text. +grounding_type(contradiction(domain_tension, _, _, _), panim). +grounding_type(contradiction(kal_vachomer_tension, _, _, _), panim). + %% ============================================================ %% HELPERS %% ============================================================ diff --git a/talmudic_engine/src/reasoning/interpret.pl b/talmudic_engine/src/reasoning/interpret.pl index 0bd460b9..f1555d95 100644 --- a/talmudic_engine/src/reasoning/interpret.pl +++ b/talmudic_engine/src/reasoning/interpret.pl @@ -10,11 +10,21 @@ %% 1. Relevant mitzvot that apply to the case %% 2. Relevant verses that inform the case %% 3. A constructed reading that connects them +%% 4. A coupling trace recording WHICH L1 facts grounded it +%% +%% The coupling trace is the tzivah trace — Vayikra 10:1: +%% eish zarah asher lo TZIVAH otam. +%% Fire is zarah because "which He did not command them." +%% The coupling trace IS the command trace. Fire on the altar +%% (eish tamid, Vayikra 6:6) has altar provenance. Fire without +%% altar provenance is eish zarah. :- module(interpret, [ interpret/2, find_relevant_mitzvot/2, find_relevant_verses/2, + find_relevant_mitzvot_traced/3, + find_relevant_verses_traced/3, construct_interpretation/4 ]). @@ -36,12 +46,20 @@ %% Propose an interpretation of a case. %% An interpretation is: -%% interpretation(CaseRef, Mitzvot, Verses, Reading, Confidence) +%% interpretation(CaseRef, Mitzvot, Verses, Reading, Confidence, CouplingTrace) +%% +%% CouplingTrace = coupling_trace(Couplings) where each Coupling records +%% the specific L1 fact and the path that connected it to the case: +%% coupled(root(Root), verse(Book, Ch, Vs)) — root led to verse +%% coupled(domain(Domain), mitzvah(ID)) — domain led to mitzvah +%% coupled(root(Root), mitzvah(ID), via_verse(Book, Ch, Vs)) — root→verse→mitzvah interpret(Case, Interpretation) :- Case = case(Ref, _Domain, _Roots, _Question), - find_relevant_mitzvot(Case, Mitzvot), - find_relevant_verses(Case, Verses), - construct_interpretation(Ref, Mitzvot, Verses, Interpretation). + find_relevant_mitzvot_traced(Case, Mitzvot, MitzvotCouplings), + find_relevant_verses_traced(Case, Verses, VerseCouplings), + append(MitzvotCouplings, VerseCouplings, AllCouplings), + sort(AllCouplings, Couplings), + construct_interpretation(Ref, Mitzvot, Verses, Couplings, Interpretation). %% ============================================================ %% FIND RELEVANT MITZVOT @@ -49,38 +67,48 @@ interpret(Case, Interpretation) :- %% Strategy: find mitzvot in the same domain as the case, %% plus mitzvot whose source verses share roots with the case. -find_relevant_mitzvot(case(_Ref, Domain, Roots, _Question), Mitzvot) :- - %% Mitzvot in the same domain - findall(mitzvah(ID, Cat, Action, Cond), +find_relevant_mitzvot(case(Ref, Domain, Roots, Question), Mitzvot) :- + find_relevant_mitzvot_traced(case(Ref, Domain, Roots, Question), Mitzvot, _). + +%% Traced version: returns mitzvot AND the coupling trace for each +find_relevant_mitzvot_traced(case(_Ref, Domain, Roots, _Question), Mitzvot, Couplings) :- + %% Mitzvot in the same domain (coupled via domain) + findall(mitzvah(ID, Cat, Action, Cond)-coupled(domain(Domain), mitzvah(ID)), ( mitzvah_domain(ID, Domain), mitzvah(ID, Cat, Action, Cond) ), - DomainMitzvot), - %% Mitzvot whose source verses share roots with the case - findall(mitzvah(ID, Cat, Action, Cond), + DomainPairs), + %% Mitzvot whose source verses share roots with the case (coupled via root→verse→mitzvah) + findall(mitzvah(ID, Cat, Action, Cond)-coupled(root(Root), mitzvah(ID), via_verse(Book, Ch, Vs)), ( mitzvah_source(ID, ref(Book, Ch, Vs)), verse(Book, Ch, Vs, VerseRoots, _), member(Root, Roots), member(Root, VerseRoots), mitzvah(ID, Cat, Action, Cond) ), - RootMitzvot), - append(DomainMitzvot, RootMitzvot, AllMitzvot), - sort(AllMitzvot, Mitzvot). % remove duplicates + RootPairs), + append(DomainPairs, RootPairs, AllPairs), + pairs_keys_values(AllPairs, AllMitzvot, Couplings), + sort(AllMitzvot, Mitzvot). %% ============================================================ %% FIND RELEVANT VERSES %% ============================================================ %% Strategy: find verses that mention any of the case's roots. -find_relevant_verses(case(_Ref, _Domain, Roots, _Question), Verses) :- - findall(verse_ref(Book, Ch, Vs, Gloss), +find_relevant_verses(case(Ref, Domain, Roots, Question), Verses) :- + find_relevant_verses_traced(case(Ref, Domain, Roots, Question), Verses, _). + +%% Traced version: returns verses AND the coupling trace for each +find_relevant_verses_traced(case(_Ref, _Domain, Roots, _Question), Verses, Couplings) :- + findall(verse_ref(Book, Ch, Vs, Gloss)-coupled(root(Root), verse(Book, Ch, Vs)), ( member(Root, Roots), verse(Book, Ch, Vs, VerseRoots, Gloss), member(Root, VerseRoots) ), - AllVerses), - sort(AllVerses, Verses). % remove duplicates + AllPairs), + pairs_keys_values(AllPairs, AllVerses, Couplings), + sort(AllVerses, Verses). %% ============================================================ %% CONSTRUCT INTERPRETATION @@ -91,10 +119,20 @@ find_relevant_verses(case(_Ref, _Domain, Roots, _Question), Verses) :- %% - high: both mitzvot and verses found %% - medium: only mitzvot or only verses %% - low: neither (but still generates a minimal reading) +%% +%% Arity-4 version for backward compatibility (no coupling trace) construct_interpretation(CaseRef, Mitzvot, Verses, Interpretation) :- assess_confidence(Mitzvot, Verses, Confidence), build_reading(Mitzvot, Verses, Reading), - Interpretation = interpretation(CaseRef, Mitzvot, Verses, Reading, Confidence). + Interpretation = interpretation(CaseRef, Mitzvot, Verses, Reading, Confidence, + coupling_trace([])). + +%% Arity-5 version with coupling trace +construct_interpretation(CaseRef, Mitzvot, Verses, Couplings, Interpretation) :- + assess_confidence(Mitzvot, Verses, Confidence), + build_reading(Mitzvot, Verses, Reading), + Interpretation = interpretation(CaseRef, Mitzvot, Verses, Reading, Confidence, + coupling_trace(Couplings)). %% Assess confidence based on grounding assess_confidence(Mitzvot, Verses, high) :- diff --git a/talmudic_engine/src/reasoning/resolve.pl b/talmudic_engine/src/reasoning/resolve.pl index 64cef9aa..65101822 100644 --- a/talmudic_engine/src/reasoning/resolve.pl +++ b/talmudic_engine/src/reasoning/resolve.pl @@ -21,11 +21,19 @@ %% 4. If counter-interpretation survives, accept it %% 5. If both are challenged, attempt dialectical resolution %% 6. If no resolution is possible: teiku +%% +%% SELF-GROUNDING DETECTION (eish_zarah_reading.md §5.3): +%% counter_interpret/3 must verify the counter-interpretation is +%% grounded in DIFFERENT L1 facts than I1. Without this check, +%% the system can feed its own prior reasoning back as evidence — +%% the Westworld problem. The coupling_trace field (added to +%% interpretation/6) makes this check possible. :- module(resolve, [ resolve/2, counter_interpret/3, - resolve_dialectically/3 + resolve_dialectically/3, + check_not_self_grounded/2 ]). :- use_module(interpret). @@ -45,13 +53,14 @@ resolve(Case, Resolution) :- interpret(Case, I1), ( challenge(I1, C1) - -> ( counter_interpret(Case, C1, I2) + -> ( counter_interpret(Case, C1, I2), + check_not_self_grounded(I1, I2) -> ( challenge(I2, _C2) -> resolve_dialectically(I1, I2, Resolution) ; Resolution = accepted(I2) ) - ; %% Challenge exists but no counter-interpretation possible - %% The challenge stands — the original interpretation fails + ; %% Challenge exists but no valid counter-interpretation + %% Either no counter found, or counter was self-grounded Resolution = challenged(I1, C1) ) ; %% No challenge found — interpretation accepted @@ -64,6 +73,33 @@ resolve(Case, Resolution) :- case_ref(Case, Ref), Resolution = no_interpretation(Ref). +%% ============================================================ +%% SELF-GROUNDING DETECTION +%% ============================================================ + +%% check_not_self_grounded(I1, I2) — succeeds if I2 brings new evidence. +%% Fails if I2's coupling trace is a subset of I1's — circular reasoning. +%% +%% The check: I2 must have at least one coupling NOT present in I1. +%% If every coupling in I2 is already in I1, the counter-interpretation +%% is just re-reading the same L1 facts — eish zarah, fire without +%% new altar provenance. +check_not_self_grounded( + interpretation(_, _, _, _, _, coupling_trace(Trace1)), + interpretation(_, _, _, _, _, coupling_trace(Trace2)) +) :- + %% I2 must contain at least one coupling not in I1 + member(Coupling, Trace2), + \+ member(Coupling, Trace1), + !. + +%% If either has empty trace, allow it (backward compatibility, +%% and empty trace means uncoupled — caught elsewhere) +check_not_self_grounded( + interpretation(_, _, _, _, _, coupling_trace([])), _) :- !. +check_not_self_grounded( + _, interpretation(_, _, _, _, _, coupling_trace([]))) :- !. + %% ============================================================ %% COUNTER-INTERPRETATION %% ============================================================ @@ -74,7 +110,7 @@ resolve(Case, Resolution) :- %% Strategy: take the contradicting source from the challenge %% and build a new interpretation that incorporates it. counter_interpret(case(Ref, Domain, Roots, Question), - kushya(contradiction(mitzvah_tension, _ID1, ID2, _Desc), _CaseRef, _Strength), + kushya(contradiction(mitzvah_tension, _ID1, ID2, _Desc), _CaseRef, _Strength, _GType), CounterInterpretation) :- %% The challenge points to a conflicting mitzvah (ID2) %% Build a new interpretation that includes this mitzvah's domain @@ -88,7 +124,7 @@ counter_interpret(case(Ref, Domain, Roots, Question), interpret(NewCase, CounterInterpretation). counter_interpret(case(Ref, Domain, Roots, Question), - kushya(contradiction(domain_tension, Root, _D1, D2), _CaseRef, _Strength), + kushya(contradiction(domain_tension, Root, _D1, D2), _CaseRef, _Strength, _GType), CounterInterpretation) :- %% The challenge points to a root in a conflicting domain %% Include that domain's roots @@ -99,7 +135,7 @@ counter_interpret(case(Ref, Domain, Roots, Question), interpret(NewCase, CounterInterpretation). counter_interpret(Case, - kushya(contradiction(condition_tension, _, _, _), _CaseRef, _Strength), + kushya(contradiction(condition_tension, _, _, _), _CaseRef, _Strength, _GType), CounterInterpretation) :- %% Condition tension — re-interpret the original case %% (the condition narrows the scope; the counter widens it) @@ -113,8 +149,8 @@ counter_interpret(Case, %% This is the hardest part — finding the reading that holds %% both truths without collapsing the distinction between them. resolve_dialectically(I1, I2, Resolution) :- - I1 = interpretation(_, M1, V1, _, _), - I2 = interpretation(_, M2, V2, _, _), + I1 = interpretation(_, M1, V1, _, _, _), + I2 = interpretation(_, M2, V2, _, _, _), %% Find common ground — mitzvot present in both intersection(M1, M2, CommonMitzvot), ( CommonMitzvot \= [] diff --git a/talmudic_engine/src/reasoning/teiku.pl b/talmudic_engine/src/reasoning/teiku.pl index 732c0feb..11d81d6c 100644 --- a/talmudic_engine/src/reasoning/teiku.pl +++ b/talmudic_engine/src/reasoning/teiku.pl @@ -10,15 +10,21 @@ %% Talmudic tradition: Tishbi Yetaretz Kushyot V'Abayot — %% "Elijah will resolve difficulties and problems." %% Until then, the tension stands. Both readings are preserved. +%% +%% eish_zarah_reading.md §5.4: The teiku record now includes +%% the challenge sequence that led to the impasse — not just +%% the verdict, but the diagnostic trail. Why did resolution fail? +%% What was the path from interpretation to unresolvable tension? :- module(teiku, [ register_teiku/3, + register_teiku/4, is_teiku/1, teiku_count/1, all_teiku/1 ]). -:- dynamic teiku_record/5. +:- dynamic teiku_record/6. %% ============================================================ %% TEIKU REGISTRATION @@ -26,16 +32,26 @@ %% Register an unresolvable tension between two interpretations. %% The record preserves both sides — neither is discarded. + +%% Arity-3: no challenge sequence (backward compatible) register_teiku(I1, I2, Record) :- - I1 = interpretation(CaseRef1, M1, V1, _, _), - I2 = interpretation(CaseRef2, M2, V2, _, _), - Record = teiku_record(CaseRef1, CaseRef2, side_a(M1, V1), side_b(M2, V2), unresolved), + register_teiku(I1, I2, no_challenges, Record). + +%% Arity-4: with challenge sequence +%% ChallengeSeq is a list of kushya terms that led to the teiku, +%% or 'no_challenges' if unknown (backward compatibility). +register_teiku(I1, I2, ChallengeSeq, Record) :- + I1 = interpretation(CaseRef1, M1, V1, _, _, _), + I2 = interpretation(CaseRef2, M2, V2, _, _, _), + Record = teiku_record(CaseRef1, CaseRef2, + side_a(M1, V1), side_b(M2, V2), + unresolved, challenge_sequence(ChallengeSeq)), assert_teiku_if_new(Record). %% Only assert if not already registered assert_teiku_if_new(Record) :- - Record = teiku_record(Ref1, Ref2, _, _, _), - ( teiku_record(Ref1, Ref2, _, _, _) + Record = teiku_record(Ref1, Ref2, _, _, _, _), + ( teiku_record(Ref1, Ref2, _, _, _, _) -> true % already registered ; assertz(Record) ). @@ -46,16 +62,16 @@ assert_teiku_if_new(Record) :- %% Check if a case resulted in teiku is_teiku(CaseRef) :- - teiku_record(CaseRef, _, _, _, unresolved). + teiku_record(CaseRef, _, _, _, unresolved, _). is_teiku(CaseRef) :- - teiku_record(_, CaseRef, _, _, unresolved). + teiku_record(_, CaseRef, _, _, unresolved, _). %% Count total teiku records teiku_count(Count) :- - aggregate_all(count, teiku_record(_, _, _, _, unresolved), Count). + aggregate_all(count, teiku_record(_, _, _, _, unresolved, _), Count). %% Retrieve all teiku records all_teiku(Records) :- - findall(teiku_record(A, B, C, D, E), - teiku_record(A, B, C, D, E), + findall(teiku_record(A, B, C, D, E, F), + teiku_record(A, B, C, D, E, F), Records). diff --git a/talmudic_engine/test/halt_test.pl b/talmudic_engine/test/halt_test.pl index 91065b1e..bca69af3 100644 --- a/talmudic_engine/test/halt_test.pl +++ b/talmudic_engine/test/halt_test.pl @@ -46,6 +46,43 @@ test(claude_not_authority, [fail]) :- :- end_tests(avodah_zarah). +:- begin_tests(golden_calf). + +%% Form 2: uncoupled output with high confidence is golden calf +test(detect_uncoupled_high_confidence) :- + Interp = interpretation(test_golden_calf, [m1], [v1], + reading(x), high, coupling_trace([])), + halt_check_golden_calf(Interp, halt(golden_calf, test_golden_calf, uncoupled, _)). + +%% Form 2: uncoupled output with medium confidence is also golden calf +test(detect_uncoupled_medium_confidence) :- + Interp = interpretation(test_gc2, [m1], [v1], + reading(x), medium, coupling_trace([])), + halt_check_golden_calf(Interp, halt(golden_calf, test_gc2, uncoupled, _)). + +%% Form 2: low confidence with empty trace is NOT golden calf (just ignorance) +test(low_confidence_empty_trace_not_calf, [fail]) :- + Interp = interpretation(test_gc3, [], [], + reading(x), low, coupling_trace([])), + halt_check_golden_calf(Interp, _). + +%% Form 2: high confidence WITH coupling trace is NOT golden calf +test(coupled_high_confidence_not_calf, [fail]) :- + Interp = interpretation(test_gc4, [m1], [v1], + reading(x), high, + coupling_trace([coupled(root(bet_resh_aleph), verse(bereishit, 1, 1))])), + halt_check_golden_calf(Interp, _). + +%% Uncoupled output detection +test(empty_trace_is_uncoupled) :- + is_uncoupled_output(coupling_trace([])). + +test(nonempty_trace_is_coupled, [fail]) :- + is_uncoupled_output(coupling_trace([coupled(root(x), verse(y, 1, 1))])). + +:- end_tests(golden_calf). + + :- begin_tests(shfichut_damim). %% Detects deletion of a user (tzelem carrier) diff --git a/talmudic_engine/test/phase3_verify_test.pl b/talmudic_engine/test/phase3_verify_test.pl index bd1c19c5..2174adad 100644 --- a/talmudic_engine/test/phase3_verify_test.pl +++ b/talmudic_engine/test/phase3_verify_test.pl @@ -34,14 +34,14 @@ test(interpret_holiness, [nondet]) :- Case = case(verify_holiness, holiness, [qof_dalet_shin], 'What does it mean to be holy?'), - interpret(Case, interpretation(verify_holiness, _, _, _, _)). + interpret(Case, interpretation(verify_holiness, _, _, _, _, _)). %% A case about judgment produces a challenge test(judgment_case_challenged, [nondet]) :- Case = case(verify_judgment, judgment, [shin_pe_tet, tzade_dalet_qof], 'How to judge righteously?'), interpret(Case, Interpretation), - challenge(Interpretation, kushya(_, _, _)). + challenge(Interpretation, kushya(_, _, _, _)). %% A case can go through full resolution test(full_resolve_cycle, [nondet]) :- diff --git a/talmudic_engine/test/reasoning_test.pl b/talmudic_engine/test/reasoning_test.pl index 87688b93..9d33564c 100644 --- a/talmudic_engine/test/reasoning_test.pl +++ b/talmudic_engine/test/reasoning_test.pl @@ -14,24 +14,36 @@ test(interpret_creation, [nondet]) :- Case = case(test_creation, creation, [bet_resh_aleph], 'What is creation?'), interpret(Case, Interpretation), - Interpretation = interpretation(test_creation, _, _, _, _). + Interpretation = interpretation(test_creation, _, _, _, _, _). %% Interpretation finds relevant verses test(interpret_finds_verses, [nondet]) :- Case = case(test_verses, creation, [bet_resh_aleph], 'Find creation verses'), - interpret(Case, interpretation(_, _, Verses, _, _)), + interpret(Case, interpretation(_, _, Verses, _, _, _)), Verses \= []. %% Interpretation finds relevant mitzvot test(interpret_finds_mitzvot, [nondet]) :- Case = case(test_mitzvot, judgment, [shin_pe_tet], 'How to judge?'), - interpret(Case, interpretation(_, Mitzvot, _, _, _)), + interpret(Case, interpretation(_, Mitzvot, _, _, _, _)), Mitzvot \= []. %% High confidence when both mitzvot and verses found test(interpret_high_confidence, [nondet]) :- Case = case(test_conf, holiness, [qof_dalet_shin], 'What is holiness?'), - interpret(Case, interpretation(_, _, _, _, high)). + interpret(Case, interpretation(_, _, _, _, high, _)). + +%% Coupling trace is present (eish_zarah_reading.md §5.1) +test(interpret_has_coupling_trace, [nondet]) :- + Case = case(test_trace, creation, [bet_resh_aleph], 'Test coupling'), + interpret(Case, interpretation(_, _, _, _, _, coupling_trace(Trace))), + Trace \= []. + +%% Coupling trace records root→verse connections +test(coupling_trace_has_verse_coupling, [nondet]) :- + Case = case(test_coupling, creation, [bet_resh_aleph], 'Test verse coupling'), + interpret(Case, interpretation(_, _, _, _, _, coupling_trace(Trace))), + member(coupled(root(bet_resh_aleph), verse(_, _, _)), Trace). :- end_tests(interpret). @@ -44,15 +56,31 @@ test(challenge_judgment, [nondet]) :- 'How to judge righteously?'), interpret(Case, Interpretation), challenge(Interpretation, Kushya), - Kushya = kushya(_, _, _). + Kushya = kushya(_, _, _, _). %% Challenge has a strength test(challenge_has_strength, [nondet]) :- Case = case(test_str, judgment, [shin_pe_tet], 'Test strength'), interpret(Case, Interpretation), - challenge(Interpretation, kushya(_, _, Strength)), + challenge(Interpretation, kushya(_, _, Strength, _)), member(Strength, [strong, moderate, weak]). +%% Challenge has grounding type (eish_zarah_reading.md §5.2) +test(challenge_has_grounding_type, [nondet]) :- + Case = case(test_gtype, judgment, [shin_pe_tet, tzade_dalet_qof], + 'Test grounding type'), + interpret(Case, Interpretation), + challenge(Interpretation, kushya(_, _, _, GType)), + member(GType, [achorai, panim]). + +%% Grounding type for mitzvah_tension is achorai (text-backed) +test(mitzvah_tension_is_achorai) :- + grounding_type(contradiction(mitzvah_tension, _, _, _), achorai). + +%% Grounding type for domain_tension is panim (inferred) +test(domain_tension_is_panim) :- + grounding_type(contradiction(domain_tension, _, _, _), panim). + :- end_tests(challenge).