%% 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/shorashim_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 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)). %% ============================================================ %% 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 response_to_json(response(ok, interpreted, result(ref(Ref), mitzvot(Mitzvot), verses(Verses), reading(Reading), confidence(Conf))), Dict) :- 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 response_to_json(response(ok, resolved_challenged, result(interpretation(Interp), challenge(Kushya))), Dict) :- term_to_string(Interp, InterpStr), term_to_string(Kushya, KushyaStr), Dict = json{status: "challenged", interpretation: json{text: InterpStr, prolog_form: InterpStr, grounding: [], confidence: "medium"}, challenge: json{kushya: KushyaStr, contradicting: "", strength: "moderate"}}. %% 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}. %% 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).