unicode-logic-kit 0.31.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. unicode_logic_kit/__init__.py +385 -0
  2. unicode_logic_kit/__main__.py +520 -0
  3. unicode_logic_kit/_deadline.py +219 -0
  4. unicode_logic_kit/ace/__init__.py +126 -0
  5. unicode_logic_kit/ace/_align.py +135 -0
  6. unicode_logic_kit/ace/chem_lexicon.py +128 -0
  7. unicode_logic_kit/ace/drs_reader.py +570 -0
  8. unicode_logic_kit/ace/mapping.py +666 -0
  9. unicode_logic_kit/ace/reverse_modal.py +138 -0
  10. unicode_logic_kit/ace/runner.py +551 -0
  11. unicode_logic_kit/ace/translate.py +452 -0
  12. unicode_logic_kit/ace/verbalize.py +1070 -0
  13. unicode_logic_kit/api.py +1284 -0
  14. unicode_logic_kit/atp/__init__.py +177 -0
  15. unicode_logic_kit/atp/_ascii_names.py +113 -0
  16. unicode_logic_kit/atp/_html.py +72 -0
  17. unicode_logic_kit/atp/_substructural_input.py +228 -0
  18. unicode_logic_kit/atp/_tff_problem.py +715 -0
  19. unicode_logic_kit/atp/_tptp_problem.py +1111 -0
  20. unicode_logic_kit/atp/_writer_support.py +289 -0
  21. unicode_logic_kit/atp/clingo_backend.py +1180 -0
  22. unicode_logic_kit/atp/cvc5_backend.py +1385 -0
  23. unicode_logic_kit/atp/eprover_backend.py +732 -0
  24. unicode_logic_kit/atp/finite_domain.py +1055 -0
  25. unicode_logic_kit/atp/fitch.py +1547 -0
  26. unicode_logic_kit/atp/fitch_search.py +551 -0
  27. unicode_logic_kit/atp/hets_backend.py +339 -0
  28. unicode_logic_kit/atp/hybrid_down.py +120 -0
  29. unicode_logic_kit/atp/incremental.py +250 -0
  30. unicode_logic_kit/atp/kripke_enum.py +741 -0
  31. unicode_logic_kit/atp/lambek.py +436 -0
  32. unicode_logic_kit/atp/leo3_backend.py +332 -0
  33. unicode_logic_kit/atp/linear.py +738 -0
  34. unicode_logic_kit/atp/lj.py +705 -0
  35. unicode_logic_kit/atp/logic_backends.py +566 -0
  36. unicode_logic_kit/atp/ltl_tableau.py +1084 -0
  37. unicode_logic_kit/atp/minizinc_backend.py +1402 -0
  38. unicode_logic_kit/atp/modal_tableau.py +1382 -0
  39. unicode_logic_kit/atp/nanocop_backend.py +410 -0
  40. unicode_logic_kit/atp/portfolio.py +489 -0
  41. unicode_logic_kit/atp/protocol.py +1803 -0
  42. unicode_logic_kit/atp/prover9_entailment.py +1153 -0
  43. unicode_logic_kit/atp/resolution.py +1376 -0
  44. unicode_logic_kit/atp/resolution_check.py +1114 -0
  45. unicode_logic_kit/atp/sequent.py +1050 -0
  46. unicode_logic_kit/atp/tableau.py +921 -0
  47. unicode_logic_kit/atp/tableau_check.py +543 -0
  48. unicode_logic_kit/atp/tptp_ncl.py +811 -0
  49. unicode_logic_kit/atp/tptp_tff.py +1546 -0
  50. unicode_logic_kit/atp/tstp.py +1333 -0
  51. unicode_logic_kit/atp/tstp_check.py +1096 -0
  52. unicode_logic_kit/atp/twee_backend.py +236 -0
  53. unicode_logic_kit/atp/twee_check.py +711 -0
  54. unicode_logic_kit/atp/twee_entailment.py +953 -0
  55. unicode_logic_kit/atp/vampire_entailment.py +540 -0
  56. unicode_logic_kit/atp/z3_arith.py +470 -0
  57. unicode_logic_kit/atp/z3_equivalence.py +36 -0
  58. unicode_logic_kit/atp/z3_fuzzy.py +362 -0
  59. unicode_logic_kit/atp/z3_input.py +500 -0
  60. unicode_logic_kit/atp/z3_models.py +208 -0
  61. unicode_logic_kit/chem/__init__.py +88 -0
  62. unicode_logic_kit/chem/_naming.py +284 -0
  63. unicode_logic_kit/chem/cache.py +185 -0
  64. unicode_logic_kit/chem/interop.py +244 -0
  65. unicode_logic_kit/chem/mol.py +525 -0
  66. unicode_logic_kit/chem/signature.py +112 -0
  67. unicode_logic_kit/comorphism.py +497 -0
  68. unicode_logic_kit/dl/__init__.py +384 -0
  69. unicode_logic_kit/dl/classification.py +227 -0
  70. unicode_logic_kit/dl/concepts.py +632 -0
  71. unicode_logic_kit/dl/datatypes.py +818 -0
  72. unicode_logic_kit/dl/owl_functional.py +2433 -0
  73. unicode_logic_kit/dl/owl_manchester.py +1637 -0
  74. unicode_logic_kit/dl/owl_reasoner.py +790 -0
  75. unicode_logic_kit/dl/parser.py +391 -0
  76. unicode_logic_kit/dl/tableau.py +4048 -0
  77. unicode_logic_kit/dl/translate.py +2704 -0
  78. unicode_logic_kit/drt/__init__.py +94 -0
  79. unicode_logic_kit/drt/export.py +179 -0
  80. unicode_logic_kit/drt/nodes.py +506 -0
  81. unicode_logic_kit/drt/parser.py +965 -0
  82. unicode_logic_kit/drt/resolve.py +195 -0
  83. unicode_logic_kit/drt/reverse.py +175 -0
  84. unicode_logic_kit/eval/__init__.py +106 -0
  85. unicode_logic_kit/eval/batch.py +382 -0
  86. unicode_logic_kit/eval/canonical.py +663 -0
  87. unicode_logic_kit/eval/chem_batch.py +606 -0
  88. unicode_logic_kit/eval/converses.py +200 -0
  89. unicode_logic_kit/eval/datasets/__init__.py +136 -0
  90. unicode_logic_kit/eval/datasets/_base.py +263 -0
  91. unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
  92. unicode_logic_kit/eval/datasets/c3po.py +678 -0
  93. unicode_logic_kit/eval/datasets/folio.py +158 -0
  94. unicode_logic_kit/eval/datasets/fracas.py +418 -0
  95. unicode_logic_kit/eval/datasets/groves.py +191 -0
  96. unicode_logic_kit/eval/datasets/logicbench.py +467 -0
  97. unicode_logic_kit/eval/datasets/logicnli.py +303 -0
  98. unicode_logic_kit/eval/datasets/malls.py +133 -0
  99. unicode_logic_kit/eval/datasets/pfolio.py +594 -0
  100. unicode_logic_kit/eval/datasets/pmb.py +242 -0
  101. unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
  102. unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
  103. unicode_logic_kit/eval/datasets/proverqa.py +674 -0
  104. unicode_logic_kit/eval/datasets/willow.py +478 -0
  105. unicode_logic_kit/eval/equivalence.py +466 -0
  106. unicode_logic_kit/eval/exercise_gen.py +533 -0
  107. unicode_logic_kit/eval/explain.py +791 -0
  108. unicode_logic_kit/eval/generality.py +750 -0
  109. unicode_logic_kit/eval/metric_hf.py +458 -0
  110. unicode_logic_kit/eval/predicate_match.py +343 -0
  111. unicode_logic_kit/eval/theory_check.py +1170 -0
  112. unicode_logic_kit/eval/validate.py +306 -0
  113. unicode_logic_kit/fol/__init__.py +177 -0
  114. unicode_logic_kit/fol/_atom_keys.py +510 -0
  115. unicode_logic_kit/fol/_fol_nodes.py +3586 -0
  116. unicode_logic_kit/fol/_free_parameters.py +105 -0
  117. unicode_logic_kit/fol/_ho_nodes.py +448 -0
  118. unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
  119. unicode_logic_kit/fol/_identifiers.py +1091 -0
  120. unicode_logic_kit/fol/_lambek_nodes.py +112 -0
  121. unicode_logic_kit/fol/_linear_nodes.py +352 -0
  122. unicode_logic_kit/fol/_modal_nodes.py +1467 -0
  123. unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
  124. unicode_logic_kit/fol/_numeral_symbols.py +231 -0
  125. unicode_logic_kit/fol/_so_nodes.py +200 -0
  126. unicode_logic_kit/fol/_symbol_names.py +81 -0
  127. unicode_logic_kit/fol/_team_nodes.py +181 -0
  128. unicode_logic_kit/fol/_tptp_symbols.py +551 -0
  129. unicode_logic_kit/fol/_truth_constants.py +117 -0
  130. unicode_logic_kit/fol/casl_export.py +1135 -0
  131. unicode_logic_kit/fol/casl_import.py +929 -0
  132. unicode_logic_kit/fol/derivation.py +367 -0
  133. unicode_logic_kit/fol/dialect_detect.py +70 -0
  134. unicode_logic_kit/fol/dialect_repair.py +537 -0
  135. unicode_logic_kit/fol/frames.py +637 -0
  136. unicode_logic_kit/fol/grammars/terminals.lark +31 -0
  137. unicode_logic_kit/fol/lambda_tools.py +297 -0
  138. unicode_logic_kit/fol/latex_input.py +429 -0
  139. unicode_logic_kit/fol/modal_translation.py +944 -0
  140. unicode_logic_kit/fol/msflparser.py +1033 -0
  141. unicode_logic_kit/fol/naming.py +422 -0
  142. unicode_logic_kit/fol/nodes.py +241 -0
  143. unicode_logic_kit/fol/normalforms.py +492 -0
  144. unicode_logic_kit/fol/pal.py +287 -0
  145. unicode_logic_kit/fol/prolog_export.py +566 -0
  146. unicode_logic_kit/fol/prolog_input.py +505 -0
  147. unicode_logic_kit/fol/prover9_input.py +1325 -0
  148. unicode_logic_kit/fol/qml.py +1760 -0
  149. unicode_logic_kit/fol/qmltp_input.py +525 -0
  150. unicode_logic_kit/fol/sanitize.py +221 -0
  151. unicode_logic_kit/fol/serialize.py +79 -0
  152. unicode_logic_kit/fol/signature.py +1290 -0
  153. unicode_logic_kit/fol/simplify_check.py +544 -0
  154. unicode_logic_kit/fol/spans.py +594 -0
  155. unicode_logic_kit/fol/tptp_input.py +1503 -0
  156. unicode_logic_kit/fol/tptp_repair.py +941 -0
  157. unicode_logic_kit/fol/unification.py +157 -0
  158. unicode_logic_kit/fol/verbalize.py +263 -0
  159. unicode_logic_kit/hets/__init__.py +163 -0
  160. unicode_logic_kit/hets/bridge.py +142 -0
  161. unicode_logic_kit/hets/client.py +748 -0
  162. unicode_logic_kit/hets/docker.py +420 -0
  163. unicode_logic_kit/hets/dol.py +712 -0
  164. unicode_logic_kit/hets/haskell_json.py +355 -0
  165. unicode_logic_kit/hets/owl_backend.py +794 -0
  166. unicode_logic_kit/hets/owl_cli.py +598 -0
  167. unicode_logic_kit/hets/symbols.py +512 -0
  168. unicode_logic_kit/hol/__init__.py +140 -0
  169. unicode_logic_kit/hol/_ho_common.py +323 -0
  170. unicode_logic_kit/hol/_isabelle_binders.py +125 -0
  171. unicode_logic_kit/hol/classical.py +812 -0
  172. unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
  173. unicode_logic_kit/hol/deepshallow/_common.py +177 -0
  174. unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
  175. unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
  176. unicode_logic_kit/hol/deepshallow/modal.py +217 -0
  177. unicode_logic_kit/hol/deepshallow/qml.py +406 -0
  178. unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
  179. unicode_logic_kit/hol/free.py +753 -0
  180. unicode_logic_kit/hol/goedel.py +336 -0
  181. unicode_logic_kit/hol/ho_modal.py +1743 -0
  182. unicode_logic_kit/hol/intuitionistic.py +403 -0
  183. unicode_logic_kit/hol/isabelle_conditional.py +593 -0
  184. unicode_logic_kit/hol/isabelle_modal.py +1908 -0
  185. unicode_logic_kit/hol/isabelle_relevant.py +412 -0
  186. unicode_logic_kit/hol/isabelle_runner.py +1147 -0
  187. unicode_logic_kit/hol/isabelle_substructural.py +884 -0
  188. unicode_logic_kit/hol/lean.py +1018 -0
  189. unicode_logic_kit/hol/manyvalued.py +921 -0
  190. unicode_logic_kit/hol/secondorder.py +687 -0
  191. unicode_logic_kit/hol/thf_modal.py +941 -0
  192. unicode_logic_kit/hol/thirdorder.py +397 -0
  193. unicode_logic_kit/ilp/__init__.py +89 -0
  194. unicode_logic_kit/ilp/readback.py +389 -0
  195. unicode_logic_kit/ilp/separation.py +153 -0
  196. unicode_logic_kit/ilp/task.py +730 -0
  197. unicode_logic_kit/logic.py +163 -0
  198. unicode_logic_kit/mcp/__init__.py +28 -0
  199. unicode_logic_kit/mcp/__main__.py +5 -0
  200. unicode_logic_kit/mcp/chem_tools.py +1031 -0
  201. unicode_logic_kit/mcp/server.py +2453 -0
  202. unicode_logic_kit/mcp/syntax_spec.py +681 -0
  203. unicode_logic_kit/prob/__init__.py +53 -0
  204. unicode_logic_kit/prob/_bdd.py +225 -0
  205. unicode_logic_kit/prob/_column_gen.py +668 -0
  206. unicode_logic_kit/prob/distribution.py +686 -0
  207. unicode_logic_kit/prob/nilsson.py +470 -0
  208. unicode_logic_kit/py.typed +0 -0
  209. unicode_logic_kit/semantics/__init__.py +137 -0
  210. unicode_logic_kit/semantics/_modal_reject.py +156 -0
  211. unicode_logic_kit/semantics/action_models.py +466 -0
  212. unicode_logic_kit/semantics/asp_models.py +1200 -0
  213. unicode_logic_kit/semantics/conditional.py +580 -0
  214. unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
  215. unicode_logic_kit/semantics/free_logic.py +913 -0
  216. unicode_logic_kit/semantics/fuzzy.py +384 -0
  217. unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
  218. unicode_logic_kit/semantics/intuitionistic.py +581 -0
  219. unicode_logic_kit/semantics/kripke.py +1139 -0
  220. unicode_logic_kit/semantics/manyvalued.py +580 -0
  221. unicode_logic_kit/semantics/matrix.py +342 -0
  222. unicode_logic_kit/semantics/model_eval.py +1135 -0
  223. unicode_logic_kit/semantics/modelfinder.py +1036 -0
  224. unicode_logic_kit/semantics/nonmonotonic.py +372 -0
  225. unicode_logic_kit/semantics/relevant.py +331 -0
  226. unicode_logic_kit/semantics/secondorder.py +657 -0
  227. unicode_logic_kit/semantics/structures.py +352 -0
  228. unicode_logic_kit/semantics/tarski.py +975 -0
  229. unicode_logic_kit/semantics/team.py +315 -0
  230. unicode_logic_kit/semantics/team_translation.py +416 -0
  231. unicode_logic_kit/semantics/thirdorder.py +358 -0
  232. unicode_logic_kit/semantics/tnorm.py +85 -0
  233. unicode_logic_kit/semantics/truthtable.py +201 -0
  234. unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
  235. unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
  236. unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
  237. unicode_logic_kit-0.31.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,748 @@
1
+ r"""HTTP client for the HETS (Heterogeneous Tool Set) REST server.
2
+
3
+ Stdlib ``urllib`` only — no ``requests`` dependency, matching this kit's
4
+ external-tool adapters elsewhere (``atp.prover9_entailment`` /
5
+ ``atp.vampire_entailment`` shell out via ``subprocess`` with no extra
6
+ dependency either; this one talks HTTP instead of spawning a process, but the
7
+ "no new hard dependency for one adapter" principle is the same).
8
+ :class:`HetsClient` is deliberately dumb about *how* it got its ``base_url``
9
+ — see :mod:`~unicode_logic_kit.hets.docker` for discovery/lifecycle.
10
+
11
+ Wire protocol (verified live against ``spechub2/hets:latest``, HETS 0.108.0,
12
+ 2026-08-12 — treat every claim below as ground truth for THIS client, not
13
+ general HETS documentation, since the REST API is not versioned separately
14
+ from the server and has changed shape across releases before)
15
+
16
+ --------------------------------------------------------------------------
17
+
18
+ 1. ``GET /version`` → plain text, e.g. ``"The Heterogeneous Tool Set, version
19
+ 0.108.0"``.
20
+ 2. Upload is a two-step handshake: ``GET /folder`` → a plain-text absolute
21
+ scratch-folder path (e.g. ``/tmp/hetsUserFolder_zvNMsf``); then
22
+ ``POST /uploadFile/<folderBasename>/<filename>`` with the raw file text as
23
+ the request body → a plain-text STORED path (e.g.
24
+ ``/tmp/hetsUserFolder_zvNMsf/probe.casl``). An error body starts with
25
+ ``"*** Error"`` regardless of HTTP status.
26
+ 3. The stored path IS the IRI for every subsequent endpoint below, but
27
+ URL-ENCODED IN FULL — including the ``/`` characters — e.g.
28
+ ``/tmp/hetsUserFolder_zvNMsf/probe.casl`` becomes
29
+ ``%2Ftmp%2FhetsUserFolder_zvNMsf%2Fprobe.casl``. :meth:`upload` returns the
30
+ RAW (unencoded) path; every method that takes ``iri`` encodes it
31
+ internally via ``urllib.parse.quote(iri, safe="")`` — ``safe=""`` matters,
32
+ the stdlib default ``safe="/"`` would leave the path unencoded and every
33
+ endpoint below would 404 on it.
34
+ 4. ``GET /dg/<iri>?format=json`` → the parsed development graph:
35
+ ``{"DGraph": {"filename", "libname", "dgnodes", "DGNode": [{"name",
36
+ "logic", "Declarations": [...], "Axioms": [...]}], ...}}``. A bad IRI
37
+ comes back as an error BODY (``"*** Error:\nfile does not exist: ..."``)
38
+ — HETS can still answer HTTP 200 for this, so the body-prefix check in
39
+ :meth:`_get`/:meth:`_post` is load-bearing, not defensive boilerplate.
40
+ 5. ``GET /provers/<iri>?format=json`` → ``{"provers": [{"identifier":
41
+ "eprover", "name": "eprover"}, ...]}``.
42
+ 6. ``GET /translations/<iri>`` → XML (the one endpoint that is NOT JSON):
43
+ ``<Translations><translations><li>CASL2SoftFOL</li>
44
+ <li>CASL2NNF:CASL2SoftFOL</li>...</translations></Translations>``.
45
+ Composed comorphisms use a ``:`` separator.
46
+ 7. ``POST /prove/<iri>`` with JSON body
47
+ ``{"format":"json","goals":[{"node":"<NodeName>",
48
+ "translation":"<optional comorphism>",
49
+ "reasonerConfiguration":{"timeLimit":10,"reasoner":"<optional id>"}}]}``
50
+ → a large, deeply-nested JSON object in which each attempted goal appears
51
+ as ``{"name":"Ax3","result":"Proved\n"|"Disproved\n"|"Open\n",
52
+ "used_prover":{"identifier":...},"used_translation":"CASL2TPTP_FOF",
53
+ "tactic_script":{...},"proof_tree":"","used_time":{...},
54
+ "used_axioms":[...],"prover_output":"..."}`` — the EXACT nesting path
55
+ varies (goals come from ``%implied`` axioms and get axiom-item names,
56
+ e.g. ``"Ax3"`` = third axiom item in the spec), so this client extracts
57
+ goal objects with a recursive structural walk (see
58
+ :func:`_extract_goal_objects`) rather than hard-coding a JSON path.
59
+ 8. ``POST /consistency-check/<iri>`` — same request/response SHAPE as
60
+ ``/prove``, with ``"result"`` values like ``"Consistent\n"``.
61
+ 9. ``"Open\n"`` means UNKNOWN (budget/prover gave up), never REFUTED — see
62
+ :mod:`~unicode_logic_kit.hets.docker`'s module docstring for which
63
+ reasoners in the shipped image actually work (``SPASS``, ``darwin``,
64
+ ``darwin-non-fd``) versus which are broken in this image and always
65
+ report ``Open`` (``eprover``, ``Vampire``).
66
+ """
67
+
68
+ from __future__ import annotations
69
+
70
+ import json
71
+ import re
72
+ import urllib.error
73
+ import http.client
74
+ import urllib.parse
75
+ import urllib.request
76
+ from typing import Dict, List, Optional, Tuple
77
+
78
+ from ..fol.tptp_input import _HETS_LOGIC_REFERENCE
79
+ from .haskell_json import repair_haskell_json
80
+
81
+ __all__ = [
82
+ "HetsClient",
83
+ "HetsNoTranslationsError",
84
+ "HetsSublogicError",
85
+ "strip_hets_theory_header",
86
+ ]
87
+
88
+ _ERROR_PREFIX = "*** Error"
89
+ _EXCERPT_CHARS = 2000
90
+
91
+ #: HETS' own 422 body for a theory whose sublogic the requested comorphism
92
+ #: does not cover. Verified verbatim against the live server::
93
+ #:
94
+ #: *** Error:
95
+ #: for 'OWL22CASL;CASL2TPTP_FOF' expected sublogic 'NP-sROIQux-D|-|'
96
+ #: but found sublogic 'NP-sROIQ-D|Literal|dateTime|decimal|integer|string|' with signature sublogic 'ELQLRL-ALC'
97
+ #:
98
+ #: A stable, machine-readable signature — which is what makes
99
+ #: :class:`HetsSublogicError` branchable rather than prose a caller has to
100
+ #: grep. Note HETS writes the composition with ``;`` here while the URL (and
101
+ #: the command line) take ``:``; the exception carries HETS' spelling
102
+ #: verbatim rather than normalising it, because that is the string HETS'
103
+ #: other messages use.
104
+ _SUBLOGIC_RE = re.compile(
105
+ r"for '([^']+)' expected sublogic '([^']*)'\s*\n?\s*"
106
+ r"but found sublogic '([^']*)'")
107
+
108
+ #: A TPTP problem must contain at least one of these. Used by
109
+ #: :meth:`HetsClient.theory_tptp` to refuse a CASL (or error) body rather
110
+ #: than hand back something that would parse to an empty formula list.
111
+ _TPTP_STATEMENT_RE = re.compile(r"(?:^|[\s)(.])(fof|cnf|tff|thf)\s*\(")
112
+
113
+ #: The DOL ``logic <Name>.<Sublogic>`` line HETS puts in front of every
114
+ #: ``/theory`` rendering, and CASL's ``%{ ... }%`` block comment (which
115
+ #: carries HETS' own ``constants:``/``predicates:``/``sorts:`` signature
116
+ #: listing). Neither is TPTP syntax — see :func:`strip_hets_theory_header`.
117
+ #:
118
+ #: The WHOLE line is ``logic`` plus one DOL logic reference — the SAME notion of
119
+ #: "a Hets header" as :mod:`unicode_logic_kit.fol.tptp_input`'s refusal pointer
120
+ #: (it is imported from there, so the two cannot disagree): a logic NAME, an
121
+ #: identifier, optionally ``.`` and a free-form sublogic. The previous ``logic``
122
+ #: plus any whitespace-free word also matched the first line of a bare formula
123
+ #: such as ``logic &p``, and the splitter then ate it as a "header".
124
+ _LOGIC_LINE_RE = re.compile(
125
+ r"logic[ \t]+" + _HETS_LOGIC_REFERENCE + r"[ \t\r]*$", re.MULTILINE)
126
+
127
+ # The stable field set every normalized goal-result dict carries, whatever
128
+ # extra keys this HETS version's raw JSON happens to include.
129
+ _GOAL_FIELDS = (
130
+ "name", "result", "used_prover", "used_translation",
131
+ "prover_output", "used_time", "tactic_script",
132
+ )
133
+
134
+
135
+ class HetsNoTranslationsError(RuntimeError):
136
+ """``GET /translations`` answered a well-formed list with no comorphism.
137
+
138
+ A ``RuntimeError`` subclass, like :class:`HetsOwlError`
139
+ (:mod:`~unicode_logic_kit.hets.owl_backend`) and for the same reason: the
140
+ server is up and the request was understood, the ANSWER is the problem.
141
+ Every existing ``except RuntimeError`` caller therefore keeps working.
142
+
143
+ Why this is raised rather than returned as ``[]``: an empty list reads as
144
+ "this logic has no comorphisms", and what HETS actually means is "I am not
145
+ telling you why". Measured on the real server — for an ontology whose
146
+ sublogic ``OWL22CASL`` does not cover, ``/translations`` answers
147
+ ``<Translations><translations></translations></Translations>`` with HTTP
148
+ 200, no exception and no reason, while ``/theory`` for the very same
149
+ library answers HTTP 422 with the sublogic mismatch spelled out. So the
150
+ reason exists; it just lives at a different endpoint, and this exception's
151
+ message is where that is written down.
152
+
153
+ ``translations(..., allow_empty=True)`` restores the old return-``[]``
154
+ behaviour for a caller that genuinely wants it.
155
+ """
156
+
157
+
158
+ class HetsSublogicError(RuntimeError):
159
+ """HETS refused a translation because the theory's sublogic is too rich.
160
+
161
+ Carries HETS' own three strings, parsed from the 422 body by
162
+ :data:`_SUBLOGIC_RE`, so a caller can BRANCH on the refusal instead of
163
+ grepping prose:
164
+
165
+ Attributes:
166
+ comorphism: the comorphism HETS was asked for, as HETS spells it
167
+ (``"OWL22CASL;CASL2TPTP_FOF"`` — with a semicolon, even though
168
+ the URL and the command line both take ``:``).
169
+ expected: the sublogic the comorphism covers
170
+ (``"NP-sROIQux-D|-|"``).
171
+ found: the sublogic the theory actually is
172
+ (``"NP-sROIQ-D|Literal|dateTime|decimal|integer|string|"``).
173
+ body: the 422 body, verbatim.
174
+
175
+ A ``RuntimeError`` subclass for the same reason as
176
+ :class:`HetsNoTranslationsError`.
177
+ """
178
+
179
+ def __init__(self, message: str, *, comorphism: str, expected: str,
180
+ found: str, body: str):
181
+ super().__init__(message)
182
+ self.comorphism = comorphism
183
+ self.expected = expected
184
+ self.found = found
185
+ self.body = body
186
+
187
+
188
+ def strip_hets_theory_header(text: str) -> Tuple[str, str]:
189
+ r"""Split HETS' theory rendering into ``(header, body)``. Never raises.
190
+
191
+ ``GET /theory?...&format=dol`` prefixes its output with DOL/CASL syntax
192
+ that is NOT part of the target logic::
193
+
194
+ logic TPTP.FOF
195
+
196
+ %{
197
+
198
+ constants: op_a,
199
+ op_b
200
+
201
+ predicates: pred_p: $i > $o
202
+
203
+ }%
204
+
205
+ fof(ax_ax1, axiom, ...).
206
+
207
+ TPTP has exactly two comment forms, ``%`` to end of line and
208
+ ``/* ... */``. ``%{ ... }%`` is CASL's BLOCK comment and ``logic
209
+ <Name>.<Sublogic>`` is DOL library syntax, so a TPTP reader must not
210
+ learn either: ``%`` already swallows ``%{`` as an ordinary line comment
211
+ and the reader then chokes on the block's first content line, and
212
+ teaching it the block form would make it accept a CASL theory header,
213
+ treat the whole CASL body as a comment and return an EMPTY formula list
214
+ — a silent empty answer to a wrong-translation request. So the split
215
+ happens here, in the adapter that knows its input is a HETS rendering,
216
+ exactly as :func:`unicode_logic_kit.ace.runner._repair_ape_tptp` repairs
217
+ APE's pretty-printer inside the ACE adapter and
218
+ :mod:`unicode_logic_kit.fol.tptp_repair` sits OVER the reader rather than
219
+ inside its grammar.
220
+
221
+ The split is structural, never ``text[text.index("fof("):]``: a blind cut
222
+ would also swallow a HETS ``*** Error`` body whole, and a symbol whose
223
+ name happens to end in ``_fof`` inside the 2267-line signature block
224
+ would move the cut point. The rule, verified against both the real TPTP
225
+ and the real CASL rendering:
226
+
227
+ 1. skip leading whitespace; if what follows is a WHOLE line of the form
228
+ ``logic <Name>`` or ``logic <Name>.<Sublogic>`` (a logic name is an
229
+ identifier, so ``logic &p`` — a formula — is not a header), consume
230
+ through the end of that line;
231
+ 2. repeatedly skip whitespace and, while what follows starts with
232
+ ``%{``, consume through the first following ``}%`` (CASL block
233
+ comments do not nest);
234
+ 3. what remains is the body.
235
+
236
+ An unterminated ``%{`` is left in the body rather than truncating the
237
+ text to nothing — the caller then gets a named refusal from
238
+ :meth:`HetsClient.theory_tptp` instead of a silently empty result.
239
+
240
+ Args:
241
+ text: a ``/theory`` rendering, or any text at all.
242
+
243
+ Returns:
244
+ ``(header, body)``. ``("", text)`` when there is no header, so the
245
+ call is safe to make unconditionally.
246
+
247
+ Example:
248
+ >>> strip_hets_theory_header("fof(a, axiom, p).")
249
+ ('', 'fof(a, axiom, p).')
250
+ """
251
+ index = 0
252
+ length = len(text)
253
+ while index < length and text[index].isspace():
254
+ index += 1
255
+ match = _LOGIC_LINE_RE.match(text, index)
256
+ if match:
257
+ end = text.find("\n", match.end())
258
+ index = length if end == -1 else end + 1
259
+ else:
260
+ index = 0
261
+ while True:
262
+ cursor = index
263
+ while cursor < length and text[cursor].isspace():
264
+ cursor += 1
265
+ if not text.startswith("%{", cursor):
266
+ break
267
+ close = text.find("}%", cursor + 2)
268
+ if close == -1:
269
+ # Unterminated: consume nothing, so the text survives for the
270
+ # caller's own refusal rather than vanishing into the header.
271
+ break
272
+ index = close + 2
273
+ return text[:index], text[index:]
274
+
275
+
276
+ def _encode_iri(iri: str) -> str:
277
+ """Percent-encode a stored-file path for use as a HETS REST IRI segment.
278
+
279
+ ``safe=""`` is required (not the ``urllib.parse.quote`` default of
280
+ ``safe="/"``): HETS's IRI convention encodes the path separators too —
281
+ see point 3 of this module's wire-protocol docstring.
282
+ """
283
+ return urllib.parse.quote(iri, safe="")
284
+
285
+
286
+ def _extract_goal_objects(node) -> List[dict]:
287
+ """Recursively collect every dict shaped like a goal result.
288
+
289
+ A "goal result" is identified structurally — any dict carrying BOTH a
290
+ ``"result"`` and a ``"used_prover"`` key — rather than by a fixed JSON
291
+ path, because the nesting HETS wraps goals in has already varied between
292
+ ``/prove`` and ``/consistency-check`` responses and across HETS
293
+ versions (see point 7 of the module docstring). A dict matching the
294
+ signature is not itself recursed into further: its own values (e.g.
295
+ ``used_prover``, ``used_time``) are metadata about THIS goal, not
296
+ containers of further sibling goals.
297
+ """
298
+ found: List[dict] = []
299
+ if isinstance(node, dict):
300
+ if "result" in node and "used_prover" in node:
301
+ found.append(node)
302
+ else:
303
+ for value in node.values():
304
+ found.extend(_extract_goal_objects(value))
305
+ elif isinstance(node, list):
306
+ for item in node:
307
+ found.extend(_extract_goal_objects(item))
308
+ return found
309
+
310
+
311
+ def _normalize_goal(raw: dict) -> dict:
312
+ """Project a raw goal-result dict onto :data:`_GOAL_FIELDS`.
313
+
314
+ ``"result"`` is stripped of surrounding whitespace — HETS appends a
315
+ trailing newline (``"Proved\n"``) — so callers can compare against
316
+ plain ``"Proved"`` / ``"Disproved"`` / ``"Open"`` / ``"Consistent"``
317
+ without repeating ``.strip()`` at every call site. Missing fields become
318
+ ``None`` rather than raising ``KeyError``, since which optional fields a
319
+ given prover populates is itself prover-specific (e.g. a failed run may
320
+ omit ``used_time``).
321
+ """
322
+ out = {field: raw.get(field) for field in _GOAL_FIELDS}
323
+ if isinstance(out["result"], str):
324
+ out["result"] = out["result"].strip()
325
+ return out
326
+
327
+
328
+ class HetsClient:
329
+ """Thin REST client for one HETS server instance.
330
+
331
+ Holds no state about the server beyond ``base_url`` — every call is a
332
+ fresh HTTP request. Errors are never silent: an HTTP error status, an
333
+ HETS ``"*** Error"`` body (which can arrive with HTTP 200 — see point 4
334
+ of the module docstring), or a JSON body that fails to parse all raise
335
+ ``RuntimeError`` carrying an excerpt of the offending body, so a failure
336
+ is always visible in the traceback rather than degrading to an empty
337
+ result the caller might mistake for "no goals found".
338
+ """
339
+
340
+ def __init__(self, base_url: str, *, timeout: float = 30.0):
341
+ self.base_url = base_url.rstrip("/")
342
+ self.timeout = timeout
343
+
344
+ # -- low-level HTTP -----------------------------------------------------
345
+
346
+ def _get(self, path: str) -> str:
347
+ return self._request("GET", path)
348
+
349
+ def _post(self, path: str, data: bytes, content_type: str) -> str:
350
+ return self._request("POST", path, data=data, content_type=content_type)
351
+
352
+ def _request(self, method: str, path: str, *, data: Optional[bytes] = None,
353
+ content_type: Optional[str] = None) -> str:
354
+ url = self.base_url + path
355
+ headers = {"Content-Type": content_type} if content_type else {}
356
+ req = urllib.request.Request(url, data=data, method=method, headers=headers)
357
+ try:
358
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
359
+ body = resp.read().decode("utf-8", "replace")
360
+ except urllib.error.HTTPError as exc:
361
+ body = exc.read().decode("utf-8", "replace") if exc.fp else ""
362
+ generic = RuntimeError(
363
+ f"hets: {method} {path} -> HTTP {exc.code}: "
364
+ f"{body[:_EXCERPT_CHARS]!r}"
365
+ )
366
+ # HTTP 422 with HETS' sublogic signature is the one HTTP error
367
+ # worth a TYPED exception: it is the server saying "this theory
368
+ # is richer than the comorphism you asked for", which a caller
369
+ # can act on (fall back to the lossy command-line route, or
370
+ # reduce the theory). Any OTHER 422 keeps the generic wording
371
+ # above, so the typed exception never swallows an unrelated
372
+ # failure.
373
+ if exc.code == 422:
374
+ signature = _SUBLOGIC_RE.search(body)
375
+ if signature:
376
+ comorphism, expected, found = signature.groups()
377
+ raise HetsSublogicError(
378
+ f"hets: {method} {path} -> HTTP 422: HETS refuses the "
379
+ f"comorphism {comorphism!r} for this theory — it "
380
+ f"covers sublogic {expected!r} but the theory is "
381
+ f"{found!r}. Either reduce the theory to that "
382
+ "sublogic, or take the LOSSY command-line route, "
383
+ "which translates anyway and reports what it omitted: "
384
+ "unicode_logic_kit.hets.owl_to_tptp(path, lossy=True) "
385
+ "(hets-server's -Y switch; the REST API has no "
386
+ f"equivalent). HETS' body was: "
387
+ f"{body[:_EXCERPT_CHARS]!r}",
388
+ comorphism=comorphism, expected=expected, found=found,
389
+ body=body,
390
+ ) from exc
391
+ raise generic from exc
392
+ except urllib.error.URLError as exc:
393
+ raise RuntimeError(f"hets: {method} {path} failed: {exc.reason}") from exc
394
+ except http.client.HTTPException as exc:
395
+ # e.g. http.client.InvalidURL for control characters that
396
+ # slipped past encoding — a sibling of URLError, not a subclass,
397
+ # so it needs its own clause to honour the RuntimeError contract
398
+ # (review-confirmed leak).
399
+ raise RuntimeError(
400
+ f"hets: {method} {path} failed: {type(exc).__name__}: {exc}"
401
+ ) from exc
402
+ self._check_error_body(method, path, body)
403
+ return body
404
+
405
+ @staticmethod
406
+ def _check_error_body(method: str, path: str, body: str) -> None:
407
+ """Raise if ``body`` is a HETS ``"*** Error"`` payload despite HTTP 200."""
408
+ if body.lstrip().startswith(_ERROR_PREFIX):
409
+ raise RuntimeError(
410
+ f"hets: {method} {path} returned an error body: "
411
+ f"{body[:_EXCERPT_CHARS]!r}"
412
+ )
413
+
414
+ @staticmethod
415
+ def _parse_json(path: str, body: str) -> dict:
416
+ try:
417
+ return json.loads(body)
418
+ except json.JSONDecodeError as exc:
419
+ raise RuntimeError(
420
+ f"hets: {path} response was not valid JSON ({exc}); "
421
+ f"body excerpt: {body[:_EXCERPT_CHARS]!r}"
422
+ ) from exc
423
+
424
+ # -- endpoints ------------------------------------------------------------
425
+
426
+ def version(self) -> str:
427
+ """``GET /version`` -> the server's plain-text version banner, stripped."""
428
+ return self._get("/version").strip()
429
+
430
+ def upload(self, text: str, filename: str) -> str:
431
+ """Upload ``text`` as ``filename``; return the stored-path IRI, RAW.
432
+
433
+ Two round trips per the wire protocol (points 2-3 above): fetch a
434
+ scratch folder, then POST the file into it. The return value is
435
+ deliberately the raw (unencoded) stored path — every other method on
436
+ this class takes that same raw ``iri`` and encodes it internally
437
+ (:func:`_encode_iri`), so callers never have to think about encoding,
438
+ and a caller that wants to log/join/inspect the path is not fighting
439
+ a pre-escaped string.
440
+ """
441
+ folder = self._get("/folder").strip()
442
+ folder_basename = folder.rsplit("/", 1)[-1]
443
+ # Both path segments are percent-encoded like every IRI is: an
444
+ # unencoded '#' in a filename would be parsed as a URL fragment and
445
+ # SILENTLY truncate the stored name (review-confirmed live), and a
446
+ # space would crash urllib below the URLError layer.
447
+ stored_path = self._post(
448
+ f"/uploadFile/{urllib.parse.quote(folder_basename, safe='')}"
449
+ f"/{urllib.parse.quote(filename, safe='')}",
450
+ data=text.encode("utf-8"),
451
+ content_type="text/plain; charset=utf-8",
452
+ ).strip()
453
+ return stored_path
454
+
455
+ def dg_raw(self, iri: str) -> str:
456
+ """``GET /dg/<iri>?format=json`` -> the body exactly as HETS sent it.
457
+
458
+ No JSON parsing and NO escape repair. This is the method to reach for
459
+ when :meth:`dg` raises and the body itself needs inspecting — see
460
+ :mod:`~unicode_logic_kit.hets.haskell_json` for the one known reason a
461
+ HETS development-graph body is not valid JSON.
462
+ """
463
+ return self._get(f"/dg/{_encode_iri(iri)}?format=json")
464
+
465
+ def dg(self, iri: str) -> dict:
466
+ """``GET /dg/<iri>?format=json`` -> the parsed development-graph dict.
467
+
468
+ ``json.loads`` is tried FIRST and the repair runs only when it
469
+ raises. That ordering is the whole design: a body the standard
470
+ library already accepts is never touched at all, so no legitimate
471
+ escape can be mangled by the repair, and the identity invariant is
472
+ structural rather than argued.
473
+
474
+ When the body IS invalid, :func:`~unicode_logic_kit.hets.haskell_json.repair_haskell_json`
475
+ gets one attempt at HETS' Haskell-``show`` escapes (``\\226\\128\\153``
476
+ for ``’`` and friends — see that module for why this is lossless
477
+ recovery of a known emitter). If it finds nothing to repair, the
478
+ original error is re-raised with its original wording; if it repairs
479
+ something and the result STILL does not parse, the error names the
480
+ repair, so a failure is attributable rather than mysterious.
481
+
482
+ Raises:
483
+ RuntimeError: the body is not valid JSON, with or without the
484
+ repair.
485
+ ~unicode_logic_kit.hets.haskell_json.HaskellJsonRepairError: a
486
+ Haskell escape in the body is not a Unicode scalar value.
487
+ """
488
+ body = self.dg_raw(iri)
489
+ try:
490
+ return json.loads(body)
491
+ except json.JSONDecodeError:
492
+ pass
493
+ repair = repair_haskell_json(body)
494
+ if not repair:
495
+ return self._parse_json("dg", body)
496
+ return self._parse_json(f"dg (after repairing {repair.summary()})",
497
+ repair.text)
498
+
499
+ def provers(self, iri: str) -> List[str]:
500
+ """``GET /provers/<iri>?format=json`` -> prover identifiers HETS can invoke."""
501
+ body = self._get(f"/provers/{_encode_iri(iri)}?format=json")
502
+ data = self._parse_json("provers", body)
503
+ return [p["identifier"] for p in data.get("provers", [])]
504
+
505
+ def translations(self, iri: str, *, node: Optional[str] = None,
506
+ allow_empty: bool = False) -> List[str]:
507
+ """``GET /translations/<iri>`` -> comorphism names, from the ``<li>`` XML list.
508
+
509
+ The one endpoint that answers XML rather than JSON (point 6 of the
510
+ module docstring). Parsed with :mod:`xml.etree.ElementTree`, not a
511
+ regex, since comorphism names are free text HETS controls, not this
512
+ client.
513
+
514
+ HETS' own IDENTITY entry — an ``<li/>`` with no text — is DROPPED,
515
+ and so is a whitespace-only one. It is not a comorphism name and not
516
+ a legal ``translation=`` value: a caller looping
517
+ ``for t in translations(iri): theory(iri, node=n, translation=t)``
518
+ would send ``translation=`` for it, and
519
+ :func:`~unicode_logic_kit.hets.bridge.register_hets_comorphisms` would
520
+ register an edge literally named ``hets:``. "No translation" is
521
+ already expressible as ``translation=None``, so dropping it loses no
522
+ capability. Order and duplicates among the real names are preserved,
523
+ and each name is returned STRIPPED of surrounding whitespace (a name
524
+ with a stray space or newline round-trips into ``translation=`` as a
525
+ different, unknown comorphism).
526
+
527
+ The root element must be ``<Translations>``: any other well-formed
528
+ document is the server refusing the request, which is raised, never
529
+ read as an empty list.
530
+
531
+ Args:
532
+ iri: a stored-path IRI as returned by :meth:`upload`.
533
+ node: a development-graph node name, sent as ``?node=<name>``.
534
+ Purely additive and worth passing: measured on the real
535
+ server, 3.21 s without it against 0.12 s with it for the same
536
+ library. For a single-ontology library the node name is the
537
+ ontology IRI (which :meth:`dg` reports).
538
+ allow_empty: return ``[]`` instead of raising when HETS offers
539
+ nothing.
540
+
541
+ Raises:
542
+ HetsNoTranslationsError: the list came back with no comorphism in
543
+ it and ``allow_empty`` is false. See that class for why an
544
+ empty list is not an answer.
545
+ RuntimeError: the body was not valid XML, or was a well-formed
546
+ document whose root is not ``<Translations>`` (raised even
547
+ with ``allow_empty=True``).
548
+ """
549
+ import xml.etree.ElementTree as ET
550
+
551
+ path = f"/translations/{_encode_iri(iri)}"
552
+ if node is not None:
553
+ path += f"?node={urllib.parse.quote(node, safe='')}"
554
+ body = self._get(path)
555
+ try:
556
+ root = ET.fromstring(body)
557
+ except ET.ParseError as exc:
558
+ raise RuntimeError(
559
+ f"hets: /translations response was not valid XML ({exc}); "
560
+ f"body excerpt: {body[:_EXCERPT_CHARS]!r}"
561
+ ) from exc
562
+ if root.tag != "Translations":
563
+ # A well-formed document of another shape is the server REFUSING
564
+ # (or answering something else), not "this library has no
565
+ # comorphism": with allow_empty=True it used to come back as [],
566
+ # and without it as HetsNoTranslationsError, whose wording
567
+ # ("offers no comorphism") blames the library for it.
568
+ raise RuntimeError(
569
+ f"hets: GET {path} answered an XML document whose root is "
570
+ f"<{root.tag}>, not the <Translations> list this endpoint "
571
+ "returns — the server refused the request or answered "
572
+ "something else, so there is no list to read (and an empty "
573
+ "list is not substituted for it, allow_empty or not); "
574
+ f"body excerpt: {body[:_EXCERPT_CHARS]!r}")
575
+ names = [(li.text or "").strip() for li in root.iter("li")]
576
+ kept = [name for name in names if name]
577
+ if not kept and not allow_empty:
578
+ raise HetsNoTranslationsError(
579
+ f"hets: GET {path} offers no comorphism for this library "
580
+ f"({len(names)} <li> entr{'y' if len(names) == 1 else 'ies'}, "
581
+ "none of them a name). That is not the same as 'there are "
582
+ "none': this endpoint reports no reason at all, and the "
583
+ "reason lives on /theory, which answers HTTP 422 with the "
584
+ "sublogic mismatch spelled out — call "
585
+ "theory(iri, node=..., translation=...) and catch "
586
+ "HetsSublogicError to read it, or take the lossy "
587
+ "command-line route, unicode_logic_kit.hets.owl_to_tptp("
588
+ "path, lossy=True), which translates anyway and reports the "
589
+ "axioms it omitted. Pass allow_empty=True to get [] back "
590
+ "instead of this exception.")
591
+ return kept
592
+
593
+ def theory(self, iri: str, *, node: Optional[str] = None,
594
+ translation: Optional[str] = None) -> str:
595
+ """``GET /theory/<iri>?…&format=dol`` -> the theory as rendered text.
596
+
597
+ Without ``translation`` this is the (sublogic-annotated) CASL theory
598
+ itself; with a comorphism name (from :meth:`translations`, e.g.
599
+ ``CASL2SoftFOL``) it is the TRANSLATED theory in the target logic's
600
+ own concrete syntax (verified live: ``CASL2SoftFOL`` renders
601
+ SoftFOL/DFG ``list_of_symbols``/``formula(...)`` text). ``node``
602
+ selects a development-graph node by name and is EFFECTIVELY
603
+ MANDATORY: this HETS version answers HTTP 500 ``development graph
604
+ node missing`` when it is omitted — with AND without a translation,
605
+ even for single-node libraries (review-corrected; the bridge
606
+ resolves the node from :meth:`dg` for exactly this reason). Callers
607
+ should always pass it; the parameter stays optional only so a
608
+ future server that grows a default keeps working.
609
+ """
610
+ params = ["format=dol"]
611
+ if node is not None:
612
+ params.append(f"node={urllib.parse.quote(node, safe='')}")
613
+ if translation is not None:
614
+ params.append(
615
+ f"translation={urllib.parse.quote(translation, safe='')}")
616
+ return self._get(f"/theory/{_encode_iri(iri)}?{'&'.join(params)}")
617
+
618
+ def theory_tptp(self, iri: str, *, node: str,
619
+ translation: str = "OWL22CASL:CASL2TPTP_FOF") -> str:
620
+ """``GET /theory`` with a TPTP-target comorphism, header stripped.
621
+
622
+ The text :meth:`theory` returns for a TPTP comorphism is NOT a TPTP
623
+ problem: HETS puts a DOL ``logic TPTP.FOF`` line and a CASL
624
+ ``%{ ... }%`` signature block in front of it, and
625
+ :func:`unicode_logic_kit.fol.tptp_input.parse_tptp` refuses both by
626
+ name. This method returns what the reader actually accepts, so the
627
+ usual call is ``parse_tptp(client.theory_tptp(iri, node=n))``.
628
+
629
+ Measured against the command-line route: for the real 1,554,903-char
630
+ rendering the stripped remainder is byte-identical (after leading
631
+ newlines) to the 1,367,212-byte file ``hets-server -o tptp`` writes,
632
+ so the REST route and the CLI route deliver the same text.
633
+
634
+ Args:
635
+ iri: a stored-path IRI as returned by :meth:`upload`.
636
+ node: the development-graph node name. Mandatory here (unlike
637
+ :meth:`theory`, where it stays optional for a future server):
638
+ this HETS version answers HTTP 500 without it.
639
+ translation: a comorphism whose TARGET is TPTP. The default is
640
+ the one the OWL route needs; ``"OWL22CASL"`` alone returns
641
+ CASL and is refused below rather than silently parsed to an
642
+ empty formula list.
643
+
644
+ Returns:
645
+ The TPTP problem text, header removed.
646
+
647
+ Raises:
648
+ HetsSublogicError: the server answered HTTP 422 because the
649
+ theory is richer than the comorphism covers.
650
+ RuntimeError: the stripped text holds no ``fof``/``cnf``/``tff``
651
+ statement — the comorphism's target was not TPTP, or HETS
652
+ answered something else entirely.
653
+ """
654
+ text = self.theory(iri, node=node, translation=translation)
655
+ _header, body = strip_hets_theory_header(text)
656
+ if not _TPTP_STATEMENT_RE.search(body):
657
+ first_line = body.strip().split("\n", 1)[0] if body.strip() else ""
658
+ raise RuntimeError(
659
+ f"hets: /theory for node {node!r} with "
660
+ f"translation={translation!r} did not return a TPTP problem "
661
+ "— after stripping HETS' theory header the text has no "
662
+ f"fof/cnf/tff statement; its first line is {first_line!r}. "
663
+ "Pass a translation whose target is TPTP (e.g. "
664
+ "'OWL22CASL:CASL2TPTP_FOF'); 'OWL22CASL' alone returns CASL.")
665
+ return body
666
+
667
+ @staticmethod
668
+ def _prove_body(node: str, *, reasoner: Optional[str], translation: Optional[str],
669
+ time_limit: int) -> dict:
670
+ """Build the JSON body shared by ``/prove`` and ``/consistency-check``.
671
+
672
+ ``translation`` is omitted from the goal dict entirely when ``None``
673
+ (not sent as ``null``) — HETS picks its own default translation for
674
+ the chosen reasoner in that case (e.g. SPASS defaults to
675
+ ``CASL2TPTP_FOF``), and an explicit ``null`` is a different, untested
676
+ wire shape this client does not want to invent.
677
+ """
678
+ goal: Dict[str, object] = {"node": node}
679
+ if translation is not None:
680
+ goal["translation"] = translation
681
+ reasoner_config: Dict[str, object] = {"timeLimit": time_limit}
682
+ if reasoner is not None:
683
+ reasoner_config["reasoner"] = reasoner
684
+ goal["reasonerConfiguration"] = reasoner_config
685
+ return {"format": "json", "goals": [goal]}
686
+
687
+ def _run_goal_endpoint(self, endpoint: str, iri: str, node: str, *,
688
+ reasoner: Optional[str], translation: Optional[str],
689
+ time_limit: int) -> List[dict]:
690
+ body = self._prove_body(node, reasoner=reasoner, translation=translation,
691
+ time_limit=time_limit)
692
+ response = self._post(
693
+ f"/{endpoint}/{_encode_iri(iri)}",
694
+ data=json.dumps(body).encode("utf-8"),
695
+ content_type="application/json",
696
+ )
697
+ # Wire-protocol special case (review-discovered): a node with ZERO
698
+ # %implied goals answers the literal plain-text "nothing to prove"
699
+ # instead of JSON. That is the legitimate empty result, not a
700
+ # malformed response — "one dict per attempted goal" of zero goals.
701
+ if response.strip().lower() == "nothing to prove":
702
+ return []
703
+ parsed = self._parse_json(endpoint, response)
704
+ return [_normalize_goal(g) for g in _extract_goal_objects(parsed)]
705
+
706
+ def prove(self, iri: str, node: str, *, reasoner: Optional[str] = None,
707
+ translation: Optional[str] = None, time_limit: int = 10) -> List[dict]:
708
+ """``POST /prove/<iri>`` for one goal node; normalized goal-result dicts.
709
+
710
+ Args:
711
+ iri: a stored-path IRI as returned by :meth:`upload` (raw,
712
+ unencoded — this method encodes it).
713
+ node: the development-graph node name to prove goals for.
714
+ reasoner: prover identifier (e.g. ``"SPASS"``, ``"darwin"``);
715
+ ``None`` lets HETS pick its default for the node's logic.
716
+ translation: comorphism name (e.g. ``"CASL2TPTP_FOF"``); ``None``
717
+ lets HETS pick its default for the chosen reasoner.
718
+ time_limit: per-goal seconds budget (HETS's ``timeLimit``).
719
+
720
+ Returns:
721
+ One dict per attempted goal (typically one per ``%implied``
722
+ axiom in the node), each with keys ``"name"``, ``"result"``
723
+ (whitespace-stripped — ``"Proved"`` / ``"Disproved"`` /
724
+ ``"Open"``), ``"used_prover"``, ``"used_translation"``,
725
+ ``"prover_output"``, ``"used_time"``, ``"tactic_script"``.
726
+ ``"Open"`` is UNKNOWN, never a disproof — see this module's
727
+ docstring point 9.
728
+ """
729
+ return self._run_goal_endpoint(
730
+ "prove", iri, node, reasoner=reasoner, translation=translation,
731
+ time_limit=time_limit)
732
+
733
+ def consistency_check(self, iri: str, node: str, *,
734
+ reasoner: str = "darwin-non-fd",
735
+ time_limit: int = 10) -> List[dict]:
736
+ """``POST /consistency-check/<iri>`` for one node; same shape as :meth:`prove`.
737
+
738
+ Defaults ``reasoner`` to ``"darwin-non-fd"`` (unlike :meth:`prove`,
739
+ which defaults to ``None``/HETS's own choice) because
740
+ consistency-checking specifically wants a FINITE model finder able
741
+ to certify ``"Consistent"`` by exhibiting one — plain ``darwin`` and
742
+ the broken ``eprover``/``Vampire`` reasoners in this image are not
743
+ reliable for that (see :mod:`~unicode_logic_kit.hets.docker`'s
744
+ docstring on image quirks).
745
+ """
746
+ return self._run_goal_endpoint(
747
+ "consistency-check", iri, node, reasoner=reasoner, translation=None,
748
+ time_limit=time_limit)