Step 14: Connect components — chamishim karsay zahav

Three connections built and tested:
- Haskell Shoresh Engine via Erlang Port (JSON lines over stdin/stdout)
- Prolog Talmudic Engine via Unix domain socket (JSON over /tmp/shorashim/talmudic.sock)
- Erlang Sanhedrin via native BEAM calls

New: Server.hs (JSON-line server loop), json_bridge.pl + socket_server.pl
(Prolog socket interface), ShoreshEnginePort + TalmudicEngineSocket +
SanhedrinClient (Elixir GenServer wrappers). BereishitCycle now routes
unresolved cases through Talmudic Engine and Sanhedrin. All connections
gracefully degrade — pipeline falls back to L1 data when engines unavailable.

46 Elixir tests, 33 Haskell tests, 57 Prolog tests — 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott KarpandClaude Opus 4.6 committed 2026-03-12 17:39:21 -04:00
1 parent f8f70c77e5
commit c67672777b
18 files changed
+1435 -121

No files matched your search

@@ -0,0 +1,194 @@
%% 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(json)).
:- 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).
@@ -0,0 +1,145 @@
%% socket_server.pl — Unix domain socket listener for Talmudic Engine.
%%
%% Construction §3.3: "JSON over Unix domain socket"
%% Step 14 (Architecture §4): "Connect components"
%%
%% Listens on a Unix domain socket, accepts JSON requests,
%% dispatches to api:process_request/2 via json_bridge,
%% returns JSON responses.
%%
%% Protocol: One JSON object per line (same as Haskell server).
%% Connection model: accept, handle one or more requests per
%% connection, close when client disconnects.
:- module(socket_server, [
serve/0,
serve/1,
stop_server/0
]).
:- use_module(library(socket)).
:- use_module(library(json)).
:- use_module(library(readutil)).
:- use_module('../interface/api').
:- use_module('../interface/json_bridge').
%% Default socket path — dev path; production uses /var/run/shorashim/talmudic.sock
default_socket_path('/tmp/shorashim/talmudic.sock').
%% Dynamic flag to control server loop
:- dynamic server_running/1.
%% ============================================================
%% SERVER ENTRY POINTS
%% ============================================================
%% serve/0 — Start server on default socket path
serve :-
default_socket_path(Path),
serve(Path).
%% serve/1 — Start server on given socket path
serve(Path) :-
%% Clean up any stale socket file
( exists_file(Path)
-> delete_file(Path)
; true
),
%% Ensure parent directory exists
file_directory_name(Path, Dir),
( exists_directory(Dir)
-> true
; make_directory_path(Dir)
),
%% Create Unix domain socket
unix_domain_socket(ServerSocket),
tcp_bind(ServerSocket, Path),
tcp_listen(ServerSocket, 5),
format('Talmudic Engine listening on ~w~n', [Path]),
flush_output,
%% Set running flag
assert(server_running(true)),
%% Accept loop
accept_loop(ServerSocket, Path).
%% Stop the server gracefully
stop_server :-
retractall(server_running(_)).
%% ============================================================
%% CONNECTION HANDLING
%% ============================================================
%% Main accept loop — waits for connections
accept_loop(ServerSocket, Path) :-
( server_running(true)
-> catch(
( tcp_accept(ServerSocket, ClientSocket, _Peer),
tcp_open_socket(ClientSocket, InStream, OutStream),
%% Handle this client's requests
catch(
handle_connection(InStream, OutStream),
Error,
( format(user_error, 'Connection error: ~w~n', [Error]),
flush_output(user_error)
)
),
close(InStream, [force(true)]),
close(OutStream, [force(true)]),
accept_loop(ServerSocket, Path)
),
AcceptError,
( format(user_error, 'Accept error: ~w~n', [AcceptError]),
flush_output(user_error),
%% Clean up
tcp_close_socket(ServerSocket),
( exists_file(Path) -> delete_file(Path) ; true ),
fail
)
)
; %% Server stopped — clean up
tcp_close_socket(ServerSocket),
( exists_file(Path) -> delete_file(Path) ; true ),
format('Talmudic Engine stopped.~n'),
flush_output
).
%% Handle a single connection — read lines until EOF
handle_connection(InStream, OutStream) :-
read_line_to_string(InStream, Line),
( Line == end_of_file
-> true
; process_json_line(Line, ResponseJson),
%% Write JSON response on a single line
json_write_dict(OutStream, ResponseJson),
nl(OutStream),
flush_output(OutStream),
%% Continue reading from this connection
handle_connection(InStream, OutStream)
).
%% ============================================================
%% JSON REQUEST PROCESSING
%% ============================================================
%% Process a JSON line string and produce a JSON response string
process_json_line(Line, ResponseJson) :-
catch(
( %% Parse JSON string to dict
atom_json_dict(Line, RequestDict, []),
%% Convert JSON to Prolog request
json_to_request(RequestDict, Request),
%% Dispatch to API
process_request(Request, Response),
%% Convert response to JSON
response_to_json(Response, ResponseJson)
),
Error,
( term_string(Error, ErrorStr),
ResponseJson = json{status: "error",
error: json{code: "PROCESSING_ERROR",
message: ErrorStr}}
)
).
+2
View File
@@ -17,6 +17,8 @@
:- use_module(halt/shfichut_damim).
:- use_module(halt/gilui_arayot).
:- use_module(interface/api).
:- use_module(interface/json_bridge).
:- use_module(interface/socket_server).
%% Load test files
:- use_module(library(plunit)).