Z3 encoder and seven core satisfiability checks (decidability Step 2)#63
Merged
Conversation
Adds the check optional-dependency extra (z3-solver>=4.12) and the checker.py module skeleton: CheckerUnavailable/UnencodableConstruct/ NonlinearArithmetic exceptions, the lazy _import_z3() helper, and the Finding/CheckResult dataclasses. z3 is never imported at module scope, so `import liminate.checker` succeeds with zero third-party runtime dependencies whether or not z3 is installed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_Encoder allocates Z3 constants from the symbol table: number->Real,
string->String, date->Int (epoch-day ordinals), one constant per
record field named {record}__{field}. list_of_* entries get no
constant -- membership expands to a disjunction at encode time
instead (Phase 3). Names are sanitized to legal SMT identifiers with
a reverse map for diagnostics, since Liminate allows hyphens.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
encode_condition mirrors interpreter._eval_condition/_apply_op exactly: is/equal_to/not_equal_to/above/below, not_above/not_below as fused <=/>= (never a structural Not() wrapper), within with value=tolerance and value2=target (the counter-intuitive operand order verified against the parser), includes/not_includes over a statically-known list with the empty-list edge case special-cased before splatting. PredicateApplicationNode substitutes EachPronoun with the application's subject (never by name) and encodes the body recursively, with a depth guard mirroring the interpreter's own defensive posture. Every other name in a predicate body resolves normally. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Closes TI-Q13. The symbol table only stores computed values, never the defining expression, so check_agreement scans RememberValueNode in the program AST into a definitions map. Value-position name resolution now inlines a name's defining expression when it contains an ArithmeticNode anywhere, guarded by a visited-set against self-reference. After inlining, multiplied_by/divided_by raise NonlinearArithmetic when neither operand is a compile-time constant -- a name reference never counts as one here, even when its own value happens to be a literal, because it is a free Z3 constant to the solver regardless. This is the case PR #62's syntactic linearity check provably misses: remember a number called beta with 4 remember a number called z from beta multiplied by beta forbid alpha is above z The analyzer sees only a bare name in the condition and passes it (tests/test_arithmetic.py's test_KNOWN_GAP_value_indirection_bypasses_ the_linearity_restriction pins this, unmodified). The encoder inlines z to beta*beta and rejects it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
encode_deontic reduces a Require/Forbid/Permit/Expect node to (effect, allowed): require fires on ¬C∧¬E (allowed C∨E); forbid fires on C∧¬E (allowed ¬C∨E); permit and expect fire on C∧¬E / ¬C∧¬E respectively but are non-blocking and get no allowed-space, since they never participate in the require/forbid contradiction check (locked DT-Q3: permit never contradicts). exception=None collapses to BoolVal(False) uniformly across all four verbs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each check is a separate assert_and_track solver query (tracked names
stmt_{index}_{verb}, 5000ms timeout -> an "inconclusive" info finding
rather than hanging): (1) always-deny / vacuous require, (2) dead
forbid, (3) require-forbid contradiction, (4) unless-swallows-the-rule
(the highest-value finding -- runs first and suppresses check 2 for
the same statement since the two share a query shape for forbid), (5)
redundant forbid, (6) dead permit, (7) constant predicate bodies
(contradiction or tautology), which needs a fresh Z3 constant standing
in for a predicate's implicit subject, sorted by inferring how the
body compares it.
Checks 3 and 5 are pairwise (O(n^2)); above 200 deontic statements
they're skipped with an informational finding while the per-statement
checks still run.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
check_agreement(statements, symbol_table) -> CheckResult ties the encoder and the seven checks together. Input mirrors analyzer.detect_contradictions(statements) -- a list of top-level statement ASTs; the checker implements its own SequenceNode-descending walk (_iter_statements) rather than reusing detect_contradictions' _iter_deontic_nodes, which only yields Require/ForbidNode and would silently drop Permit/Expect/Define. UnencodableConstruct and NonlinearArithmetic are caught at this boundary and reported as CheckResult(encodable=False, skipped_reason=...); so is any other exception (invariant 8 -- no unhandled exception escapes check_agreement for any input, malformed or out-of-fragment). Every statement rendering in a Finding goes through renderer.render() so checker output matches canonical form everywhere else in the system. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
5 tasks
rmichaelthomas
added a commit
that referenced
this pull request
Jul 22, 2026
…nt public API) Publishes the check extra (z3-solver) and the check_source/check_agreement public API that landed on main via PRs #63-67 without a version bump. liminate-dev's hosted /check endpoint depends on this release.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Step 2 of the decidability path (Trust Infrastructure Addendum v1.0s §91/§92.6). Adds
src/liminate/checker.py: a satisfiability checker for Liminate's enforcement fragment — the four deontic verbs (require/forbid/permit/expect), their conditions, and thedefinepredicates they reference — encoded into SMT constraints (QF_UFLIRA via Z3) with seven authoring-time checks.Phases completed (one commit each)
checkoptional-dependency extra (z3-solver>=4.12), lazy z3 import,CheckerUnavailable/UnencodableConstruct/NonlinearArithmeticexceptions,Finding/CheckResultdataclasses.interpreter._eval_condition/_apply_opexactly: all comparison operators,within(value=tolerance, value2=target),includes/not_includes, predicate application withEachPronounsubstitution and a depth guard.definitionsmap scanned fromRememberValueNodes, inlining of arithmetic-containing defining expressions, andNonlinearArithmeticdetection when neither operand ofmultiplied_by/divided_byis a compile-time constant.encode_deonticreduces each verb to (effect, allowed) per the locked semantics table.assert_and_tracksolver query with a 5000ms timeout and a 200-statement pairwise cap.check_agreement(statements, symbol_table) -> CheckResult, with every encoding failure (including unexpected ones) caught at the boundary — never a traceback.Files created / modified
src/liminate/checker.py(new)tests/test_checker.py(new, 69 tests)pyproject.toml(+1 line: thecheckextra)interpreter.py,analyzer.py,tests/test_arithmetic.py,tests/test_contradiction_detection.py,tests/test_vocabulary.pyare untouched (confirmed viagit diff— empty).Suite
1763 passed / 0 failed / 0 skipped — up from the 1694 baseline on
main@95daaf5, +69 new tests, zero regressions.Vocabulary
Confirmed still 61 words —
tests/test_vocabulary.py(18/18) unchanged and passing.TI-Q13 (value-provenance indirection) — closed
liminate.run()/ the analyzer: accepts and enforces it (PROHIBITION_VIOLATED) — PR Accept bare dates in temporal prefixes; restrict arithmetic to linear forms #62's syntactic linearity check only sees a bare name in the condition, never theArithmeticNodebehind it.check_agreement(): rejects it —encodable=False,skipped_reasonnames'doubled'and the nonlinearbeta multiplied by betaterm, by inlining the defining expression at encode time.Design decisions beyond literal spec text
Five points were genuinely ambiguous in the build prompt; each is backed by a dedicated test:
UnencodableConstructfires lazily only when a condition actually references one._is_compile_time_constantnever resolves through name indirection, even when a name's own value is a literal — it's a free Z3 constant to the solver regardless.divided_byuses the same uniform "neither operand constant" rule asmultiplied_by(no asymmetric divisor-only rule).C∧¬Enotation applied invariantly (which is meaningless forrequire).Invariants exercised by tests
All nine consistency invariants from the build prompt are covered: vocabulary frozen at 61 (§11.1), zero new runtime deps behind a lazy import (§11.2), operator semantics matching the interpreter (§11.3, throughout
test_op_*),unlessuniformlycondition ∧ ¬exception(test_exception_none_collapses_to_false),interpreter.py/detect_contradictionsuntouched (§11.5–6, confirmed viagit diff), the pinnedKNOWN_GAPtest unmodified (§11.7), no unhandled exception escapescheck_agreement(§11.8,test_check_agreement_never_raises_for_malformed_symbol_table), and the suite growing monotonically with zero regressions (§11.9).Manual gate
Reviewed and passed by Rob — full suite diff and check-4
CheckResultoutput confirmed clear before this PR was opened.🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com