%% 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).