Releases: Euclid-BG/Euclid-MCP
Releases · Euclid-BG/Euclid-MCP
Release list
v0.4.6 — Backend parity, multi-line continuations, REPL hardening
[0.4.6] — 2026-08-23
Fixed
- Backend divergence on
true/falsebody literals —pred($x) IF false
crashed the SWI-Prolog backend with an opaqueengine_error: the
meta-interpreter had no branch for the literal, so it reachedclause/2,
and SWI-Prolog 10 raisespermission_error(access, private_procedure, fail/0)over the built-infalse/fail; the engine loop's catch-all
discarded the message. The native engine failed cleanly, so the same KB
behaved differently per backend — andIF truewas divergent in the other
direction (succeeded on Prolog, failed natively). Both engines now give the
literals explicit, identical semantics:IF falsenever proves,IF true
proves like a fact (proof nodefact, orrulewhen the rule carries an
ID). This makesIF falsethe legitimate spelling for vocabulary
declarations in rules-only KBs. - Backend divergence on unbound query variables — solutions from
non-range-restricted rules (a head variable absent from the body, e.g.
report($user) IF system_compromised, or negation over an empty declared
predicate with a variable goal) were dropped by the native engine and
stringified as SWI fresh tokens (_28498) — or lost entirely — by
SWI-Prolog. Both engines now keep the solution and surface unbound query
variables as explicitnullbindings ({"any": null}). Proof trees are
also aligned: SWI's nondeterministic fresh-variable names are normalized
to the Euclid-IR wildcard_(quoted literals masked, so user data is
never rewritten), making proof trees byte-identical across backends and
across runs. - Built-in body goals raised instead of failing — a rule body naming any
SWI-Prolog built-in (e.g.atom_length) hitclause/2and surfaced as an
opaqueengine_error. Both meta-interpreter branches now carry a
\+ predicate_property(Goal, built_in)guard: since user clauses can never
exist for a built-in, the goal fails cleanly — matching the native engine,
where unknown predicates simply fail. - Zero-arity predicates leaked across engine loads —
_extract_pred_sig
required parentheses, so bare statements (rainy, or at IF truehead)
were never declared dynamic nor registered in the persistent engine's
workspace; their clauses survivedclear_workspaceand duplicated on every
subsequent load, silently multiplying solutions. Bare atoms now produce a
/0signature. Parity tests pin single-solution behavior across reloads. - Backend divergence on non-ASCII variables —
$città,$ктоand any
other Unicode variable now behave identically on both backends. The
variable-name pattern (language.VAR_NAME_RE, single source of truth for
the native parser, the Prolog translator and the linter) accepts Unicode
letters, and the translator masks variables (__VAR_name__) during
atom-quoting so they can never be wrapped into quoted atoms. Previously
the native engine raised a parse error while SWI-Prolog silently
truncated the name (or quoted it into an atom that unified with nothing). - NFC normalization at every entry point —
parse()normalizes KB text
andstrip_query_prefix()normalizes explicit queries to NFC, so
decomposed (NFD) spellings unify symmetrically on both backends instead
of failing on the native engine and matching only byte-identical forms
on SWI-Prolog. --backendflag silently discarded an exportedEUCLID_BACKEND— the
CLI wrote the flag's default (auto) into the environment on every run,
soEUCLID_BACKEND=native euclid-clistill launched SWI-Prolog. The flag
is now applied only when explicitly passed: an exported value is honored,
and an explicit--backendwins over it.- Multi-line rules with leading
ANDsplit into garbage statements — a
rule continued with the conjunction at the START of each line (the common
Prolog habit) was cut at its first complete body goal, while the leftover
AND ...lines were accepted as facts by the lenient language parser and
then rejected at runtime by both engines. The parser now absorbs
AND-leading continuation lines (and rejects orphan ones with a clear
message), and the interactive REPL holds a completed rule until the next
input settles whether a continuation follows. Continuation detection is
buffer-aware: a line trails the open statement when the buffered text ends
withIF/ANDor the line itself opens withAND, so trailing-style and
leading-style multi-line rules both work in files, seeded KBs and live
sessions (previously a continuation line without an innerIF, e.g.
$y is $x - 1 AND, was flushed as its own statement). :checkcould stay green on statements the engines refuse —
validation relied only on the lenient parser while both backends re-parse
every statement with the strict term parser.run_check_kbnow runs each
fact, rule and the query through that same parser, so a green check
guarantees the KB loads and queries on either backend.- REPL errors vanished into stderr — engine and parse errors printed
only a terse message to stderr, easy to lose next to the tool-call log
lines (and invisible in piped sessions). They now print to stdout. - String literals bind bare values on both backends — IR quoted values
are translated to single-quoted Prolog atoms instead of SWI string
terms; bindings are now identical to the native engine's bare content
(e.g.müller, not"müller"). Operators inside literals were already
inert data and remain so.
Changed
- REPL banner renamed —
Euclid-MCP REPL …→Euclid-IR REPL vX.Y.Z — type facts and rules, then? query.. The REPL calls the tool functions
in-process; no MCP server or protocol is involved, so the banner now names
the language. The version comes from the in-tree package constant (with a
new drift test againstpyproject.toml), not from possibly stale install
metadata.
Added
- Reserved boolean keywords —
true/falsejoinif,and,not,
isas reserved words: rejected as predicate names, bare facts and rule
heads with a clear message (previously such KBs broke the Prolog load with
the same opaque error). As arguments they remain plain atom data. - Vocabulary-declaration idiom documented —
docs/EUCLID_IR.mddocuments
thetrue/falseliterals and the load-once/query-many pattern where a
rules-only stable layer declares its input vocabulary withIF false
bodies (validated green bycheck_kb, zero inference impact); the PCI-DSS
sample was migrated from theIF 1 > 2workaround. prova_unicode.py/divergenze.py— Unicode conformance suite and
backend-divergence isolator used to pin the fixes above.tests/test_true_false_literals.py— backend-parity suite for the boolean
literals, negation over declared predicates, clean built-in failure, the
zero-arity reload regression, and the end-to-end vocabulary +
delta_knowledgeflow.tests/test_unbound_variables.py— backend-parity suite for unbound query
variables: explicitnullbindings, cross-backend proof-tree identity,
run-to-run determinism, and quoted-data safety of the wildcard
normalization.tests/test_multiline_continuations.py— AND-leading rule continuations
in parser and REPL, plus stdout visibility of engine errors.tests/test_check_kb_engine_alignment.py— statements the engines reject
are flagged at check time; unicode/quoted KBs stay green.tests/test_backend_precedence.py—EUCLID_BACKENDsurvives a run
without--backend; an explicit flag overrides it.tests/test_version.py— package version matchespyproject.toml.- REPL:
:kblisting gained a paste-safe counts header (# session KB: N facts, M rules) and the help documents the hidden:listalias.
v0.4.5 — Unicode atoms in the native engine
Added
- Unicode atoms in the native engine — the pure-Python lexer
(euclid_mcp/ir_parser.py) now accepts Unicode predicate/atom names
(父(张三),смертный($x),Бог(Иван)), closing the last documented gap
with SWI-Prolog (seedocs/NATIVE_ENGINE.md). Numbers stay ASCII (0-9),
so Unicode digits are read as name characters, and variables remain ASCII
($name) per the Euclid-IR spec — identical on both backends. - Added examples 09 and 11 - a Turing machine in pure Euclid-IR
demonstrating Turing-completeness of the language and
a Certainty factors implementation with MYCIN-style scoring and exact dyadic
arithmetic.
Changed
- Unicode parity tests —
test_reason_tool_unicodeand
test_what_if_unicodeintests/test_unicode_atoms.pylost their
prolog_onlymarker and now run on both backends; the two
native_onlyrejection tests were removed together with the limitation.
New native-engine-level Unicode tests live intests/test_native_engine.py
(predicates, args, rules with rule IDs, negation, case sensitivity).
Fixed
- Native engine crashed on division by zero —
$x is 1 / 0raised an
uncaughtZeroDivisionErrorthat escaped the tool layer (which only
catchesRuntimeError), crashing thereasoncall instead of returning
an error result. The native evaluator now raises a clean
"Arithmetic error: division by zero", matching the Prolog backend's
engine_error. A deep proof that exhausts the Python stack is likewise
converted into a clear error asking to lowermax_depth. - Native parser silently split malformed numeric literals — the lexer
read only the leading decimal part of unsupported literal forms
(1e3,0x10,1_000,12abc) and the argument parser tolerated
missing commas, so e.g.n(1e3)silently became the arity-2 term
n(1, e3)and queries failed with zero solutions and no error. Both are
now explicit parse errors;p(1-2)is rejected too instead of parsing as
p(1, -2)(seedocs/NATIVE_ENGINE.md). Regression tests in
tests/test_native_engine.py. - Preloaded-KB digest dropped Unicode predicates — the predicate regex in
euclid_mcp/kb_summary.pywas ASCII-lowercase-only, so a preloaded KB with
e.g.Бог(Иван)omitted that predicate from the server-instructions digest. - Prolog engine crashed on compound-term bindings — a query variable bound
to a nested compound term (e.g. a structured state) madejson_write/3
raisetype_error(json_term, ...), surfaced only as an opaque
engine_error. Both snippet generators now route every binding through a
neweuclid_json_value/2engine helper: atoms become strings, numbers pass
through, other terms are rendered viaterm_string/2— matching how the
native engine already renders compound bindings. Regression test in
tests/test_prolog_bridge.py. - Spurious arity warnings/circular-rule errors on nested-compound KBs —
check_kbcounted all commas in a predicate's argument list, so nested
compounds (cfg(run, tape(cell(1, blank)))) reported inflated arities and
triggered falseinconsistent_aritywarnings. Arity counting is now
depth- and quote-aware (shared_arityhelper ineuclid_mcp/validation.py,
also used by the preloaded-KB digest). Additionally, the circular-rule check
now accepts a variable-bearing fact as a legitimate base case for a
recursive rule (e.g.final(cfg(done, $t), cfg(done, $t))).
v0.4.4
v0.4.3
What's new
Added
- Native-only Docker image —
Dockerfile.nativeonpython:3.12-slim(no SWI-Prolog), pure-Python native engine (EUCLID_BACKEND=native); compose serviceeuclid-mcp-native. ~283MB vs ~370MB full image. Verified in-container: MCP stdio handshake,reason,what_if. - Didactic guide —
docs/DIDACTIC.md(12 chapters, incl. Beyond the 1:1 mapping: the audit layer) with reference links.
Fixed
- CI now runs an explicit
EUCLID_BACKEND=nativeleg — the native engine was previously never exercised in CI. - Unicode tests re-marked
prolog_only(they assert SWI-Prolog Unicode behaviour); the native ASCII-only rejection is now asserted bytest_reason_tool_unicode_rejected_native/test_what_if_unicode_rejected_native. - Backend-aware
test_api.pyhealth/metrics tests;EUCLID_BACKENDisolated in in-process CLI tests.
Full changelog: CHANGELOG.md
v0.4.2 — Interactive Euclid-IR REPL
What's new
Added
- Interactive Euclid-IR REPL —
euclid-cliwith no subcommand opens aswipl/psql-style shell: facts and rules accumulate in a session knowledge base across? querylines, multi-line rules continue afterIF/AND(continuation prompt... >), and:meta-commands cover the remaining tools (:check,:kb,:load <file>,:explain [query],:diagnose <query> [why|why_not|what_needs],:what-if <mods>,:reset,:quit). Piped input runs the same loop as a batch script without prompts;-f/--knowledgeseed the session (preload fallback when empty). Docs: README "Via CLI" anddocs/CLI.md.
Full changelog: CHANGELOG.md
v0.3.1
v0.3.1 — 2026-08-13
Added
- HTTP API authentication & TLS (
integrations/euclid_api.py): opt-inEUCLID_API_KEY(or--api-key) requiresAuthorization: Bearer <key>on every POST (constant-time check,401otherwise,/healthstays open for load balancers), andEUCLID_TLS_CERT/EUCLID_TLS_KEY(or--certfile/--keyfile) serve HTTPS directly. Setup and posture documented indocs/PRODUCTION.md→ "Authentication & TLS". - Python 3.13 and 3.14 support: verified end-to-end (lint, mypy, full test suite) on CPython 3.13 and 3.14; CI matrix now runs on Python 3.10–3.14.
- Stress & soak benchmark (
benchmarks/euclid_bench.py): detects response mixing, KB pollution, and engine errors across periodic restarts; regression detector for the load+query atomicity and the periodic-restart policy. - Benchmark documentation: one detail page per benchmark plus a catalog of results and implementation choices (
benchmarks/BENCHMARKS.md). - SWI-Prolog compatibility matrix: full suite passes on SWI-Prolog 8.4.2, 9.0.4, 9.2.9, and 10.0.2 (
benchmarks/docs/06-swi-prolog-versions.md).
Performance
- KB preload optimization: repeated loads of the same knowledge base skip re-parsing and re-asserting. Python-side parse+translate results are cached per KB source (
_translate_cached), and the engine skips the workspace rebuild when theloadcarries the samekb_hash(replyskipped:true). A repeated identical 20 000-fact KB drops from ~196 ms to ~18 ms per load.assert/retractinvalidate the fingerprint.
Fixed
- Engine workspace sweep is SWI-Prolog 9.x-safe:
clear_workspace/0now retracts only the registered workspace predicates (workspace_predicate/1) instead of every dynamic predicate. On SWI-Prolog 9.x the broad sweep corrupted the autoloader (domain_error(file_type, prolog)); the registry fix is ~19% faster (513 vs ~429 req/s). - Periodic engine restart: now fires before the next
load, never after a crossing request, so a relaunched engine always receives its workspace first. - Load+query atomicity gap:
PrologServer.load_and_queryholds the lock across both steps — a single atomic exchange; the workers=4 benchmark flips from FAIL to PASS.
v0.3.0 — persistent SWI-Prolog engine, Unicode atoms
0.3.0
Persistent engine & performance
- Persistent SWI-Prolog engine: single long-lived
swiplprocess, JSON-lines pipe protocol, workspace reloaded per request instead of booting Prolog per call. Same API, identical solution ordering. - Streaming query results: solutions written straight to the engine output via
json_write/3— no double serialization, low memory on large result sets. - Benchmark:
benchmarks/persistent_engine_benchmark.py— ~3×–42× steady-state speedup (mean ~14×).
Language (Euclid-IR)
- Unicode atoms (
\p{L}): CJK, Cyrillic, Greek… names, arguments and rule IDs, e.g.父(张三),Бог(Иван). Case folding is ASCII-only. - Canonical rule-ID marker is now
# RULE:(parser stays case-insensitive).
Docs
- README "Scalability" section; IDEAS/CHANGELOG updates.
Requires SWI-Prolog system package (local / install server, not in-browser).
v0.2.0
v0.2.0
- explain tool: deterministic proof-tree → natural-language reasoning steps with rule ID citations (no LLM)
- KB preload:
EUCLID_KB_PATH/--kb-path+ markdown digest viakb_summary - Rule IDs:
# rule: <id>surfaced asrule_idin proof trees (audit trail) - HTTP API: new
POST /explainendpoint - Docs aligned (README, EUCLID_IR, AGENTS, integrations)
- TODO: lists feature planned next (scope: literals + member)