neurocore-skill-math 0.1.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.
@@ -0,0 +1,72 @@
1
+ """neurocore-skill-math — a math-proof toolchain for NeuroCore.
2
+
3
+ One package exposing many small composable skills (CAS, SMT, ATP, formal proof
4
+ assistants). Each skill detects whether its backend tool is installed and degrades
5
+ gracefully when it is not (status ``tool_unavailable``).
6
+ """
7
+ from neurocore_skill_math.atp import (
8
+ EproverProveTptpSkill,
9
+ Mace4CountermodelSkill,
10
+ Prover9ProveSkill,
11
+ VampireProveTptpSkill,
12
+ )
13
+ from neurocore_skill_math.cas import (
14
+ GapGroupTheorySkill,
15
+ PariGpNumberTheorySkill,
16
+ SagemathComputeSkill,
17
+ )
18
+ from neurocore_skill_math.formal import (
19
+ CoqCheckSkill,
20
+ IsabelleCheckTheorySkill,
21
+ Lean4CheckSkill,
22
+ Lean4FormalizeStatementSkill,
23
+ Lean4RepairSkill,
24
+ )
25
+ from neurocore_skill_math.numeric import MpmathHighPrecisionCheckSkill
26
+ from neurocore_skill_math.planning import LlmProofPlannerSkill, TheoremRetrieverSkill
27
+ from neurocore_skill_math.prep import (
28
+ MathDomainClassifierSkill,
29
+ MathProblemParserSkill,
30
+ MathStatementNormalizerSkill,
31
+ )
32
+ from neurocore_skill_math.report import ProofReportBuilderSkill
33
+ from neurocore_skill_math.smt import Cvc5SmtCheckSkill, Z3SmtCheckSkill
34
+ from neurocore_skill_math.symbolic import (
35
+ SympyCalculusSkill,
36
+ SympySimplifySkill,
37
+ SympySolveSkill,
38
+ )
39
+
40
+ __all__ = [
41
+ # Group 1 — prep
42
+ "MathProblemParserSkill",
43
+ "MathDomainClassifierSkill",
44
+ "MathStatementNormalizerSkill",
45
+ # Group 2 — symbolic / numeric / CAS
46
+ "SympySimplifySkill",
47
+ "SympySolveSkill",
48
+ "SympyCalculusSkill",
49
+ "MpmathHighPrecisionCheckSkill",
50
+ "SagemathComputeSkill",
51
+ "PariGpNumberTheorySkill",
52
+ "GapGroupTheorySkill",
53
+ # Group 3 — counterexample / SMT
54
+ "Z3SmtCheckSkill",
55
+ "Cvc5SmtCheckSkill",
56
+ "Mace4CountermodelSkill",
57
+ # Group 4 — ATP
58
+ "VampireProveTptpSkill",
59
+ "EproverProveTptpSkill",
60
+ "Prover9ProveSkill",
61
+ # Group 4/5 — planning, retrieval
62
+ "LlmProofPlannerSkill",
63
+ "TheoremRetrieverSkill",
64
+ # Group 5 — formalization
65
+ "Lean4FormalizeStatementSkill",
66
+ "Lean4CheckSkill",
67
+ "Lean4RepairSkill",
68
+ "IsabelleCheckTheorySkill",
69
+ "CoqCheckSkill",
70
+ # Group 6 — reporting
71
+ "ProofReportBuilderSkill",
72
+ ]
@@ -0,0 +1,51 @@
1
+ """Detect which math tools (CLIs and Python libs) are available at runtime.
2
+
3
+ Skills use this to degrade gracefully when a backend is not installed, and the
4
+ ``python -m neurocore_skill_math.check`` command prints a full report.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import importlib.util
9
+ import shutil
10
+
11
+ # CLI executables expected on PATH (Ubuntu installer provides these).
12
+ CLI_TOOLS: list[str] = [
13
+ "gp", # PARI/GP
14
+ "gap", # GAP
15
+ "sage", # SageMath (or via docker)
16
+ "docker", # for the SageMath container fallback
17
+ "z3", # z3 CLI (the skill prefers the python binding)
18
+ "cvc5", # cvc5 CLI
19
+ "vampire",
20
+ "eprover",
21
+ "prover9",
22
+ "mace4",
23
+ "lean",
24
+ "lake",
25
+ "isabelle",
26
+ "coqc",
27
+ ]
28
+
29
+ # Python libraries used directly by skills.
30
+ PY_LIBS: list[str] = ["sympy", "mpmath", "z3", "cvc5", "numpy", "scipy", "networkx"]
31
+
32
+
33
+ def tool_available(name: str) -> bool:
34
+ """True if a CLI executable is on PATH."""
35
+ return shutil.which(name) is not None
36
+
37
+
38
+ def lib_available(module: str) -> bool:
39
+ """True if a Python module can be imported."""
40
+ try:
41
+ return importlib.util.find_spec(module) is not None
42
+ except (ImportError, ValueError):
43
+ return False
44
+
45
+
46
+ def availability_report() -> dict[str, dict[str, bool]]:
47
+ """Return {'cli': {tool: bool}, 'lib': {module: bool}}."""
48
+ return {
49
+ "cli": {t: tool_available(t) for t in CLI_TOOLS},
50
+ "lib": {m: lib_available(m) for m in PY_LIBS},
51
+ }
@@ -0,0 +1,112 @@
1
+ """Shared base for all math skills.
2
+
3
+ ``MathSkill`` standardizes:
4
+ - configurable IO keys (``input_key`` / ``output_key`` + extras), per the design doc;
5
+ - a uniform **result envelope** every skill writes to context;
6
+ - **availability detection** + graceful degradation (never crash the flow when a
7
+ backend tool/lib is missing);
8
+ - **port** helpers so skills can drive graph routing.
9
+
10
+ Concrete skills subclass this, set ``default_input_key`` / ``default_output_key`` /
11
+ ``required_tool`` / ``required_lib`` / ``tool_name``, and implement ``_compute``.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import time
16
+ from typing import Any
17
+
18
+ from flowengine import FlowContext
19
+ from neurocore import AsyncSkill
20
+
21
+ from neurocore_skill_math._availability import lib_available, tool_available
22
+
23
+ # Envelope status values.
24
+ STATUS_OK = "ok"
25
+ STATUS_REFUTED = "refuted"
26
+ STATUS_PROVED = "proved"
27
+ STATUS_UNKNOWN = "unknown"
28
+ STATUS_UNAVAILABLE = "tool_unavailable"
29
+ STATUS_ERROR = "error"
30
+ STATUS_TIMEOUT = "timeout"
31
+
32
+
33
+ class MathSkill(AsyncSkill):
34
+ """Base class for math-toolchain skills (not registered itself)."""
35
+
36
+ # Subclasses override these.
37
+ default_input_key: str = "math.normalized"
38
+ default_output_key: str = "math.result"
39
+ required_tool: str | None = None # CLI executable to check on PATH
40
+ required_lib: str | None = None # importable module to check
41
+ tool_name: str = "math"
42
+
43
+ def init(self, config: dict[str, Any]) -> None:
44
+ super().init(config)
45
+ self.in_key: str = self.config.get("input_key", self.default_input_key)
46
+ self.out_key: str = self.config.get("output_key", self.default_output_key)
47
+ self.timeout: float = float(self.config.get("timeout_seconds", 30))
48
+
49
+ # -- availability ------------------------------------------------------
50
+ def is_available(self) -> bool:
51
+ tool_ok = not self.required_tool or tool_available(self.required_tool)
52
+ lib_ok = not self.required_lib or lib_available(self.required_lib)
53
+ return tool_ok and lib_ok
54
+
55
+ def health_check(self) -> bool:
56
+ return self.is_initialized and self.is_available()
57
+
58
+ # -- execution ---------------------------------------------------------
59
+ async def process(self, context: FlowContext) -> FlowContext: # type: ignore[override]
60
+ if not self.is_available():
61
+ missing = self.required_tool or self.required_lib or self.tool_name
62
+ self._write(
63
+ context,
64
+ self.envelope(
65
+ STATUS_UNAVAILABLE,
66
+ available=False,
67
+ error=f"{missing!r} is not installed; skill skipped.",
68
+ ),
69
+ )
70
+ self.port(context, "tool_unavailable")
71
+ return context
72
+
73
+ payload = context.get(self.in_key)
74
+ start = time.perf_counter()
75
+ try:
76
+ env = await self._compute(payload, context)
77
+ except Exception as exc: # noqa: BLE001 — degrade, don't crash the flow
78
+ env = self.envelope(STATUS_ERROR, error=str(exc))
79
+ env.setdefault("duration_ms", round((time.perf_counter() - start) * 1000, 2))
80
+ self._write(context, env)
81
+ return context
82
+
83
+ async def _compute(self, payload: Any, context: FlowContext) -> dict[str, Any]:
84
+ """Run the skill's work and return a result envelope. May also set ports."""
85
+ raise NotImplementedError
86
+
87
+ # -- helpers -----------------------------------------------------------
88
+ def envelope(
89
+ self,
90
+ status: str,
91
+ *,
92
+ result: Any = None,
93
+ log: str = "",
94
+ error: str | None = None,
95
+ available: bool = True,
96
+ **extra: Any,
97
+ ) -> dict[str, Any]:
98
+ return {
99
+ "status": status,
100
+ "tool": self.tool_name,
101
+ "available": available,
102
+ "result": result,
103
+ "log": log,
104
+ "error": error,
105
+ **extra,
106
+ }
107
+
108
+ def _write(self, context: FlowContext, env: dict[str, Any]) -> None:
109
+ context.set(self.out_key, env)
110
+
111
+ def port(self, context: FlowContext, name: str) -> None:
112
+ self.set_output_port(context, name)
@@ -0,0 +1,54 @@
1
+ """Assemble solver inputs (SMT-LIB, TPTP) from skill payloads.
2
+
3
+ A payload may be a ready-made source string, or a structured dict carrying the
4
+ source under a known key (``smtlib`` / ``tptp`` / ``prover9``). Skills accept both
5
+ so an upstream LLM/normalizer can emit either form.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+
12
+ def _coerce(payload: Any, *keys: str) -> str | None:
13
+ """Pull a source string out of a payload (raw string or dict with one of keys)."""
14
+ if isinstance(payload, str):
15
+ return payload
16
+ if hasattr(payload, "get"): # dict / DotDict
17
+ for k in keys:
18
+ v = payload.get(k)
19
+ if isinstance(v, str) and v.strip():
20
+ return v
21
+ return None
22
+
23
+
24
+ def as_smtlib(payload: Any) -> str | None:
25
+ """Return an SMT-LIB v2 script from the payload, or None if not provided.
26
+
27
+ Ensures a ``(check-sat)`` is present so the solver actually runs.
28
+ """
29
+ src = _coerce(payload, "smtlib", "smt", "source")
30
+ if src is None:
31
+ return None
32
+ if "(check-sat)" not in src:
33
+ src = src.rstrip() + "\n(check-sat)\n"
34
+ return src
35
+
36
+
37
+ def as_tptp(payload: Any) -> str | None:
38
+ """Return a TPTP problem string from the payload, or None."""
39
+ return _coerce(payload, "tptp", "fof", "source")
40
+
41
+
42
+ def as_prover9(payload: Any) -> str | None:
43
+ """Return a Prover9/Mace4 input deck from the payload, or None."""
44
+ return _coerce(payload, "prover9", "mace4", "source")
45
+
46
+
47
+ def parse_smt_result(stdout: str) -> str:
48
+ """Map raw solver stdout to 'sat' | 'unsat' | 'unknown'."""
49
+ text = stdout.strip().lower()
50
+ if "unsat" in text:
51
+ return "unsat"
52
+ if "sat" in text:
53
+ return "sat"
54
+ return "unknown"
@@ -0,0 +1,54 @@
1
+ """Base for LLM-backed math skills (problem prep, planning, formalization)."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import re
6
+ from typing import Any
7
+
8
+ from neurocore_skill_math._base import MathSkill
9
+
10
+ _JSON_RE = re.compile(r"\{.*\}", re.DOTALL)
11
+
12
+
13
+ def extract_json(text: str) -> dict[str, Any]:
14
+ """Best-effort: parse the first JSON object in ``text``; else wrap as {'raw': ...}."""
15
+ match = _JSON_RE.search(text or "")
16
+ if match:
17
+ try:
18
+ obj = json.loads(match.group(0))
19
+ if isinstance(obj, dict):
20
+ return obj
21
+ except json.JSONDecodeError:
22
+ pass
23
+ return {"raw": (text or "").strip()}
24
+
25
+
26
+ class LlmMathSkill(MathSkill):
27
+ """MathSkill whose backend is the injected LLM provider (`requires_llm=True`)."""
28
+
29
+ tool_name = "llm"
30
+
31
+ def is_available(self) -> bool:
32
+ return getattr(self, "llm", None) is not None
33
+
34
+ async def _ask(self, user: str, system: str | None = None) -> str:
35
+ from neurocore.llm.provider import LLMMessage
36
+
37
+ response = await self.llm.complete(
38
+ [LLMMessage(role="user", content=user)], system=system
39
+ )
40
+ return response.content
41
+
42
+ async def _ask_json(self, user: str, system: str | None = None) -> dict[str, Any]:
43
+ return extract_json(await self._ask(user, system))
44
+
45
+ @staticmethod
46
+ def _text(payload: Any, *keys: str) -> str:
47
+ if isinstance(payload, str):
48
+ return payload
49
+ if hasattr(payload, "get"):
50
+ for k in (*keys, "problem", "statement", "normalized", "raw"):
51
+ v = payload.get(k)
52
+ if isinstance(v, str) and v.strip():
53
+ return v
54
+ return str(payload or "")
@@ -0,0 +1,81 @@
1
+ """Subprocess helpers with timeouts for external math tools.
2
+
3
+ Kept small and dependency-free. ``run_cli`` is the seam tests monkeypatch so the
4
+ suite needs neither the real provers nor a network.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import subprocess
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass
13
+ class CliResult:
14
+ """Outcome of a CLI invocation."""
15
+
16
+ returncode: int
17
+ stdout: str
18
+ stderr: str
19
+ timed_out: bool = False
20
+
21
+
22
+ def run_cli(
23
+ cmd: list[str],
24
+ *,
25
+ stdin: str | None = None,
26
+ timeout: float = 30.0,
27
+ env: dict[str, str] | None = None,
28
+ cwd: str | None = None,
29
+ ) -> CliResult:
30
+ """Run a command, capturing stdout/stderr, returning a CliResult.
31
+
32
+ A timeout yields ``timed_out=True`` (never raises); other OS errors surface
33
+ as a non-zero return code with the message on stderr.
34
+ """
35
+ try:
36
+ proc = subprocess.run(
37
+ cmd,
38
+ input=stdin,
39
+ capture_output=True,
40
+ text=True,
41
+ timeout=timeout,
42
+ env=env,
43
+ cwd=cwd,
44
+ check=False,
45
+ )
46
+ return CliResult(proc.returncode, proc.stdout, proc.stderr)
47
+ except subprocess.TimeoutExpired as exc:
48
+ out = exc.stdout or ""
49
+ err = exc.stderr or ""
50
+ out = out.decode() if isinstance(out, bytes) else out
51
+ err = err.decode() if isinstance(err, bytes) else err
52
+ return CliResult(-1, out, err, timed_out=True)
53
+ except OSError as exc:
54
+ return CliResult(-1, "", str(exc))
55
+
56
+
57
+ def docker_cmd(
58
+ image: str,
59
+ args: list[str],
60
+ *,
61
+ network: bool = False,
62
+ memory: str | None = "4g",
63
+ cpus: str | None = "2",
64
+ mounts: list[tuple[str, str]] | None = None,
65
+ read_only: bool = True,
66
+ ) -> list[str]:
67
+ """Build a sandboxed ``docker run`` command (per design doc §C).
68
+
69
+ Defaults: no network, capped memory/cpus, read-only mounts.
70
+ """
71
+ cmd = ["docker", "run", "--rm"]
72
+ if not network:
73
+ cmd += ["--network", "none"]
74
+ if memory:
75
+ cmd += ["--memory", memory]
76
+ if cpus:
77
+ cmd += ["--cpus", cpus]
78
+ for src, dst in mounts or []:
79
+ cmd += ["-v", f"{src}:{dst}{':ro' if read_only else ''}"]
80
+ cmd += [image, *args]
81
+ return cmd
@@ -0,0 +1,188 @@
1
+ """Group 3/4 — automated theorem provers and finite-model search.
2
+
3
+ - ``vampire_prove_tptp`` / ``eprover_prove_tptp`` — first-order ATP on TPTP input.
4
+ - ``prover9_prove`` — Prover9 (its own syntax).
5
+ - ``mace4_countermodel`` — Mace4 finite-model search (counterexamples).
6
+
7
+ Provers set port ``proof_found`` / ``no_proof``; Mace4 sets ``counterexample_found`` /
8
+ ``no_counterexample``.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import tempfile
14
+ from typing import Any
15
+
16
+ from flowengine import FlowContext
17
+ from neurocore import SkillMeta
18
+
19
+ from neurocore_skill_math._base import (
20
+ STATUS_ERROR,
21
+ STATUS_OK,
22
+ STATUS_PROVED,
23
+ STATUS_REFUTED,
24
+ STATUS_TIMEOUT,
25
+ STATUS_UNKNOWN,
26
+ MathSkill,
27
+ )
28
+ from neurocore_skill_math._formats import as_prover9, as_tptp
29
+ from neurocore_skill_math._run import run_cli
30
+
31
+ _TPTP_SCHEMA = {"properties": {
32
+ "input_key": {"type": "string"}, "output_key": {"type": "string"},
33
+ "premises_key": {"type": "string"}, "timeout_seconds": {"type": "integer"},
34
+ }}
35
+
36
+
37
+ def _szs_verdict(text: str) -> str:
38
+ """Map prover stdout (SZS ontology / common phrases) to proved/disproved/unknown."""
39
+ t = text.lower()
40
+ if ("refutation found" in t or "szs status theorem" in t
41
+ or "szs status unsatisfiable" in t or "proof found" in t
42
+ or "theorem proved" in t):
43
+ return "proved"
44
+ if ("countersatisfiable" in t or "szs status satisfiable" in t
45
+ or "search failed" in t):
46
+ return "disproved"
47
+ return "unknown"
48
+
49
+
50
+ class _TptpProver(MathSkill):
51
+ default_input_key = "math.normalized"
52
+ cli: str = "" # subclass sets the executable
53
+
54
+ def _write_tmp(self, content: str, suffix: str) -> str:
55
+ fd, path = tempfile.mkstemp(prefix="nc-atp-", suffix=suffix)
56
+ with os.fdopen(fd, "w") as fh:
57
+ fh.write(content)
58
+ return path
59
+
60
+ def _route_proof(self, context: FlowContext, verdict: str, log: str) -> dict[str, Any]:
61
+ if verdict == "proved":
62
+ self.port(context, "proof_found")
63
+ return self.envelope(STATUS_PROVED, result={"proof_found": True}, log=log[:4000])
64
+ self.port(context, "no_proof")
65
+ status = STATUS_OK if verdict == "disproved" else STATUS_UNKNOWN
66
+ return self.envelope(status, result={"proof_found": False, "verdict": verdict},
67
+ log=log[:4000])
68
+
69
+
70
+ class VampireProveTptpSkill(_TptpProver):
71
+ default_output_key = "proof.vampire"
72
+ required_tool = "vampire"
73
+ cli = "vampire"
74
+ tool_name = "vampire"
75
+
76
+ skill_meta = SkillMeta(
77
+ name="vampire_prove_tptp",
78
+ version="0.1.0",
79
+ description="Prove a TPTP first-order conjecture with Vampire.",
80
+ author="NeuroCore Contributors",
81
+ provides=["proof.vampire"], consumes=["math.normalized", "proof.premises"],
82
+ tags=["math", "atp", "tptp", "vampire"], config_schema=_TPTP_SCHEMA,
83
+ )
84
+
85
+ async def _compute(self, payload: Any, context: FlowContext) -> dict[str, Any]:
86
+ tptp = as_tptp(payload)
87
+ if not tptp:
88
+ return self.envelope(STATUS_ERROR, error="no TPTP input provided")
89
+ path = self._write_tmp(tptp, ".p")
90
+ res = run_cli(["vampire", "-t", str(int(self.timeout)), path], timeout=self.timeout + 5)
91
+ if res.timed_out:
92
+ self.port(context, "no_proof")
93
+ return self.envelope(STATUS_TIMEOUT, error="vampire timed out")
94
+ return self._route_proof(context, _szs_verdict(res.stdout), res.stdout)
95
+
96
+
97
+ class EproverProveTptpSkill(_TptpProver):
98
+ default_output_key = "proof.eprover"
99
+ required_tool = "eprover"
100
+ cli = "eprover"
101
+ tool_name = "eprover"
102
+
103
+ skill_meta = SkillMeta(
104
+ name="eprover_prove_tptp",
105
+ version="0.1.0",
106
+ description="Prove a TPTP first-order conjecture with the E prover.",
107
+ author="NeuroCore Contributors",
108
+ provides=["proof.eprover"], consumes=["math.normalized", "proof.premises"],
109
+ tags=["math", "atp", "tptp", "eprover"], config_schema=_TPTP_SCHEMA,
110
+ )
111
+
112
+ async def _compute(self, payload: Any, context: FlowContext) -> dict[str, Any]:
113
+ tptp = as_tptp(payload)
114
+ if not tptp:
115
+ return self.envelope(STATUS_ERROR, error="no TPTP input provided")
116
+ path = self._write_tmp(tptp, ".p")
117
+ res = run_cli(
118
+ ["eprover", "--auto", "--tptp3-format", f"--cpu-limit={int(self.timeout)}", path],
119
+ timeout=self.timeout + 5,
120
+ )
121
+ if res.timed_out:
122
+ self.port(context, "no_proof")
123
+ return self.envelope(STATUS_TIMEOUT, error="eprover timed out")
124
+ return self._route_proof(context, _szs_verdict(res.stdout), res.stdout)
125
+
126
+
127
+ class Prover9ProveSkill(_TptpProver):
128
+ default_output_key = "proof.prover9"
129
+ required_tool = "prover9"
130
+ cli = "prover9"
131
+ tool_name = "prover9"
132
+
133
+ skill_meta = SkillMeta(
134
+ name="prover9_prove",
135
+ version="0.1.0",
136
+ description="Prove a conjecture with Prover9 (Prover9 input syntax).",
137
+ author="NeuroCore Contributors",
138
+ provides=["proof.prover9"], consumes=["math.normalized", "proof.premises"],
139
+ tags=["math", "atp", "prover9"], config_schema=_TPTP_SCHEMA,
140
+ )
141
+
142
+ async def _compute(self, payload: Any, context: FlowContext) -> dict[str, Any]:
143
+ deck = as_prover9(payload)
144
+ if not deck:
145
+ return self.envelope(STATUS_ERROR, error="no Prover9 input provided")
146
+ res = run_cli(["prover9"], stdin=deck, timeout=self.timeout)
147
+ if res.timed_out:
148
+ self.port(context, "no_proof")
149
+ return self.envelope(STATUS_TIMEOUT, error="prover9 timed out")
150
+ return self._route_proof(context, _szs_verdict(res.stdout), res.stdout)
151
+
152
+
153
+ class Mace4CountermodelSkill(MathSkill):
154
+ default_input_key = "math.normalized"
155
+ default_output_key = "counterexamples.mace4"
156
+ required_tool = "mace4"
157
+ tool_name = "mace4"
158
+
159
+ skill_meta = SkillMeta(
160
+ name="mace4_countermodel",
161
+ version="0.1.0",
162
+ description="Search for a finite countermodel with Mace4.",
163
+ author="NeuroCore Contributors",
164
+ provides=["counterexamples.mace4"], consumes=["math.normalized"],
165
+ tags=["math", "counterexample", "finite-model", "mace4"],
166
+ config_schema={"properties": {
167
+ "input_key": {"type": "string"}, "output_key": {"type": "string"},
168
+ "timeout_seconds": {"type": "integer"}, "max_domain_size": {"type": "integer"},
169
+ }},
170
+ )
171
+
172
+ async def _compute(self, payload: Any, context: FlowContext) -> dict[str, Any]:
173
+ deck = as_prover9(payload)
174
+ if not deck:
175
+ return self.envelope(STATUS_ERROR, error="no Mace4 input provided")
176
+ n = int(self.config.get("max_domain_size", 8))
177
+ res = run_cli(["mace4", "-n", str(n)], stdin=deck, timeout=self.timeout)
178
+ if res.timed_out:
179
+ self.port(context, "no_counterexample")
180
+ return self.envelope(STATUS_TIMEOUT, error="mace4 timed out")
181
+ found = "model" in res.stdout.lower() and "exiting with" in res.stdout.lower()
182
+ if found:
183
+ self.port(context, "counterexample_found")
184
+ return self.envelope(STATUS_REFUTED, result={"countermodel": res.stdout.strip()},
185
+ counterexample_found=True)
186
+ self.port(context, "no_counterexample")
187
+ return self.envelope(STATUS_OK, result={"countermodel": None},
188
+ counterexample_found=False)