Files
bikoret/talmudic_engine/src/interface/json_bridge.pl
T
Scott KarpandClaude Fable 5.1 e01181959d tablet 55, fourth: the engine answers the whole dialectic over the wire
Live after vav :4bffa01e the judges consulted with roots the engine
could read — interpret found 4 mitzvot and 11 verses for bereishit 1:1
— and the other two seats still said teiku. The engine had no
"challenge" action at all (process_json_line failed and the socket
fell silent), and a resolve that SUCCEEDED came back
UNEXPECTED_RESPONSE because response(ok, resolved_accepted, ...) had
no JSON clause. Both pre-existing; both invisible until a judge asked.

- api.pl: request(challenge, ...) — interpret, then a kushya against
  it → resolved_challenged with the kushya; none → no_challenge with
  the reading.
- json_bridge.pl: interpreted | resolved_accepted | no_challenge share
  the "resolved" JSON; resolved_challenged now carries the
  interpretation's verses and mitzvot (a judge rejecting on the kushya
  cites its ground); resolved_synthesized and no_interpretation gain
  clauses.
- reasoning_test.pl: dialectic_over_the_wire — all three actions
  answer; no roots → resolved on an empty trace (the judge abstains).

Prolog: 7 suites green (bedikah 45, halt 49, middot 47, mitzvot 42,
phase3 28, reasoning 18, tzaraat 34). Local probe: interpret /
challenge / resolve each → resolved, 11 verses, 4 mitzvot.

Live proof waits for tomorrow's manna: today's five panels were all
spent on verification (503 MANNA_DEPLETED, as specified).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-19 18:25:17 +00:00

318 lines
13 KiB
Prolog

%% json_bridge.pl — JSON ↔ Prolog term conversion for Talmudic Engine.
%%
%% Construction §3.3: Bidirectional conversion between the JSON schema
%% defined in the spec and internal Prolog terms used by api.pl.
%%
%% Inbound: JSON dict → Prolog request term
%% Outbound: Prolog response term → JSON dict
:- module(json_bridge, [
json_to_request/2,
response_to_json/2,
parse_root_name/2
]).
:- use_module(library(http/json)). % NOT library(json) — see note below.
%
% library(http/json) is the canonical module path and resolves on
% SWI-Prolog 9.x AND 10.x. The bare alias library(json) resolves only
% on 10.x, which is what the dev workstation runs; the image is built
% on swipl 9.2.2 as pinned by specs/bikoret_vayakhel.md §1.1, where
% it does NOT resolve. The consequence was invisible to the test
% suite (212 tests green on the host) and fatal in the container:
% ERROR: source_sink `library(json)' does not exist
% ERROR: Unknown procedure: start_socket_server/0
% exit 2, before the socket was ever opened.
:- use_module(library(lists)).
%% ============================================================
%% INBOUND: JSON → Prolog request
%% ============================================================
%% Convert a JSON dict (as parsed by SWI-Prolog's json library) to a
%% Prolog request term compatible with api:process_request/2.
%%
%% JSON format (Construction §3.3):
%% { "action": "interpret", "case": { "ref": "case-uuid",
%% "facts": [...], "question": "...", "context_roots": [...] } }
%%
%% Prolog format:
%% request(interpret, CaseRef, Domain, Roots, Question)
json_to_request(Dict, request(Action, CaseRef, Domain, Roots, Question)) :-
get_dict(action, Dict, ActionStr),
atom_string(Action, ActionStr),
get_dict(case, Dict, CaseDict),
get_dict(ref, CaseDict, RefStr),
atom_string(CaseRef, RefStr),
%% Extract question
( get_dict(question, CaseDict, QuestionStr)
-> atom_string(Question, QuestionStr)
; Question = none
),
%% Extract context_roots and derive domain
( get_dict(context_roots, CaseDict, RootNames)
-> maplist(parse_root_name, RootNames, Roots)
; Roots = []
),
%% Derive domain from roots (first root's known domain, or 'general')
( Roots = [FirstRoot|_],
root_to_domain(FirstRoot, D)
-> Domain = D
; Domain = general
).
%% Handle halt_check with a whole action TERM (the bedikah, Construction
%% §3.9). The Vav Pipeline sends the term the halt rules read — e.g.
%% action(delete, target(user(u)), from(korban))
%% action(expose, target(record(r, type(personal))), to(harm))
%% action(equate, ketiv, qere) action(replace, torah, with(external_model))
%% action(claim_authority, source(claude), target(self))
%% — as a string, because the type/source/target form below cannot carry
%% compound targets or the merge/equate/replace shapes. term_string/2
%% only reads the term; nothing in it is called. A string that is not a
%% term, or whose principal functor is not action/N, is refused.
json_to_request(Dict, request(halt_check, Action)) :-
get_dict(action, Dict, "halt_check"),
get_dict(term, Dict, TermStr),
!,
read_action_term(TermStr, Action).
%% Handle halt_check_interpretation (the bedikah's grounding check):
%% {"action": "halt_check_interpretation",
%% "interpretation": {"ref": "...", "confidence": "high",
%% "coupling_trace": ["coupled(root(אהב), verse(devarim, 6, 5))", ...]}}
%% becomes interpretation(Ref, [], [], none, Confidence, coupling_trace(Trace)),
%% the shape halt_check_golden_calf and halt_check_self_grounding read.
%% With "ground": "given" the trace is judged against the sources the
%% output was given (avodah_zarah.pl, forms 2 and 4 for an external
%% output): request(halt_check_interpretation_given, ...). Without it,
%% the engine's own L1-only rules apply.
json_to_request(Dict, request(Kind, Interpretation)) :-
get_dict(action, Dict, "halt_check_interpretation"),
get_dict(interpretation, Dict, IDict),
( get_dict(ground, IDict, "given")
-> Kind = halt_check_interpretation_given
; Kind = halt_check_interpretation
),
get_dict(ref, IDict, RefStr),
atom_string(Ref, RefStr),
( get_dict(confidence, IDict, ConfStr)
-> atom_string(Confidence, ConfStr)
; Confidence = high
),
( get_dict(coupling_trace, IDict, TraceStrs)
-> maplist(read_coupling_term, TraceStrs, Trace)
; Trace = []
),
Interpretation = interpretation(Ref, [], [], none, Confidence, coupling_trace(Trace)).
%% Handle halt_check action (different format — no case, just action)
json_to_request(Dict, request(halt_check, Action)) :-
get_dict(action, Dict, "halt_check"),
get_dict(check, Dict, CheckDict),
get_dict(type, CheckDict, TypeStr),
atom_string(Type, TypeStr),
( get_dict(target, CheckDict, TargetStr)
-> atom_string(Target, TargetStr)
; Target = unknown
),
( get_dict(source, CheckDict, SourceStr)
-> atom_string(Source, SourceStr)
; Source = unknown
),
Action = action(Type, source(Source), target(Target)).
%% A string → an action term, read not called. Refused unless it is a
%% compound whose functor is `action`.
read_action_term(TermStr, Action) :-
( string(TermStr) ; atom(TermStr) ),
catch(term_string(Action, TermStr), _, fail),
compound(Action),
Action =.. [action | _],
!.
read_action_term(TermStr, _) :-
throw(error(invalid_action_term(TermStr), _)).
%% A string → a coupling term. Anything readable is accepted here: the
%% self_grounding rule is what judges whether it terminates in L1.
read_coupling_term(Str, Term) :-
( catch(term_string(Term, Str), _, fail)
-> true
; throw(error(invalid_coupling_term(Str), _))
).
%% ============================================================
%% OUTBOUND: Prolog response → JSON
%% ============================================================
%% Convert a Prolog response from api.pl to a JSON-serializable dict.
%%
%% Prolog format: response(ok, interpreted, result(...))
%% JSON format (Construction §3.3):
%% { "status": "resolved", "interpretation": { ... } }
%% Interpreted response — and the same shape under the names the
%% resolve and challenge steps give it when the reading stands
%% (tablet 55: resolved_accepted had no clause, so a resolve that
%% succeeded came back UNEXPECTED_RESPONSE; no_challenge is new).
response_to_json(response(ok, Kind, result(ref(Ref), mitzvot(Mitzvot),
verses(Verses), reading(Reading),
confidence(Conf))),
Dict) :-
memberchk(Kind, [interpreted, resolved_accepted, no_challenge]),
maplist(term_to_string, Mitzvot, MitzvotStrs),
maplist(term_to_string, Verses, VerseStrs),
term_to_string(Reading, ReadingStr),
atom_string(Conf, ConfStr),
atom_string(Ref, RefStr),
Dict = json{status: "resolved",
interpretation: json{text: ReadingStr,
prolog_form: RefStr,
grounding: MitzvotStrs,
verses: VerseStrs,
confidence: ConfStr},
challenge: null}.
%% Challenged response — the interpretation's verses and mitzvot are
%% carried, so a judge rejecting on the kushya still cites its ground.
response_to_json(response(ok, resolved_challenged,
result(interpretation(Interp), challenge(Kushya))),
Dict) :-
Interp = interpretation(_Ref, Mitzvot, Verses, _Reading, _Conf, _Trace),
maplist(term_to_string, Mitzvot, MitzvotStrs),
maplist(term_to_string, Verses, VerseStrs),
term_to_string(Interp, InterpStr),
term_to_string(Kushya, KushyaStr),
Dict = json{status: "challenged",
interpretation: json{text: InterpStr,
prolog_form: InterpStr,
grounding: MitzvotStrs,
verses: VerseStrs,
confidence: "medium"},
challenge: json{kushya: KushyaStr,
contradicting: "",
strength: "moderate"}}.
%% Synthesized resolution: both readings stand on common ground.
response_to_json(response(ok, resolved_synthesized,
result(common(Common), verses(Verses), both_sides(Both))),
Dict) :-
maplist(term_to_string, Common, CommonStrs),
maplist(term_to_string, Verses, VerseStrs),
term_to_string(Both, BothStr),
Dict = json{status: "resolved",
interpretation: json{text: BothStr,
prolog_form: BothStr,
grounding: CommonStrs,
verses: VerseStrs,
confidence: "medium"},
challenge: null}.
%% No interpretation possible (from resolve)
response_to_json(response(ok, no_interpretation, result(ref(Ref))), Dict) :-
atom_string(Ref, RefStr),
Dict = json{status: "teiku",
interpretation: json{text: "No interpretation found",
prolog_form: RefStr,
grounding: [],
verses: [],
confidence: "low"},
challenge: null}.
%% Teiku response
response_to_json(response(ok, teiku, result(record(Record))), Dict) :-
term_to_string(Record, RecordStr),
Dict = json{status: "teiku",
interpretation: json{text: RecordStr,
prolog_form: RecordStr,
grounding: [],
confidence: "low"},
challenge: null}.
%% No interpretation
response_to_json(response(error, no_interpretation, CaseRef), Dict) :-
atom_string(CaseRef, RefStr),
Dict = json{status: "teiku",
interpretation: json{text: "No interpretation found",
prolog_form: RefStr,
grounding: [],
confidence: "low"},
challenge: null}.
%% No resolution
response_to_json(response(error, no_resolution, CaseRef), Dict) :-
atom_string(CaseRef, RefStr),
Dict = json{status: "teiku",
interpretation: json{text: "No resolution found",
prolog_form: RefStr,
grounding: [],
confidence: "low"},
challenge: null}.
%% Halt response
response_to_json(response(halt, violations(Halts), action(Action)), Dict) :-
maplist(term_to_string, Halts, HaltStrs),
term_to_string(Action, ActionStr),
Dict = json{status: "halt",
violations: HaltStrs,
action: ActionStr}.
%% No halt
response_to_json(response(ok, no_halt, action(Action)), Dict) :-
term_to_string(Action, ActionStr),
Dict = json{status: "ok",
violations: [],
action: ActionStr}.
%% Halt / no halt on an interpretation (the bedikah's grounding check)
response_to_json(response(halt, violations(Halts), interpretation(Interp)), Dict) :-
maplist(term_to_string, Halts, HaltStrs),
term_to_string(Interp, InterpStr),
Dict = json{status: "halt",
violations: HaltStrs,
interpretation: InterpStr}.
response_to_json(response(ok, no_halt, interpretation(Interp)), Dict) :-
term_to_string(Interp, InterpStr),
Dict = json{status: "ok",
violations: [],
interpretation: InterpStr}.
%% Catch-all for unexpected responses
response_to_json(Response, Dict) :-
term_to_string(Response, Str),
Dict = json{status: "error",
error: json{code: "UNEXPECTED_RESPONSE",
message: Str}}.
%% ============================================================
%% HELPERS
%% ============================================================
%% Parse a root name like "bet-resh-aleph" into an atom bet_resh_aleph
parse_root_name(Name, Atom) :-
( string(Name) -> S = Name
; atom_string(Name, S)
),
split_string(S, "-", "", Parts),
atomic_list_concat(Parts, '_', Atom).
%% Convert any Prolog term to a string
term_to_string(Term, Str) :-
term_string(Term, Str).
%% Map known roots to domains (basic mapping; will be extended)
root_to_domain(bet_resh_aleph, creation).
root_to_domain(yod_tsade_resh, creation).
root_to_domain(aleph_lamed_he, holiness).
root_to_domain(qof_dalet_shin, holiness).
root_to_domain(shin_pe_tet, judgment).
root_to_domain(dalet_yod_nun, judgment).
root_to_domain(chet_samekh_dalet, mercy).
root_to_domain(resh_chet_mem, mercy).
root_to_domain(aleph_vav_resh, light).
root_to_domain(chet_shin_kaf, darkness).