{"id":48977735,"url":"https://github.com/ctoth/gunray","last_synced_at":"2026-04-18T10:14:53.114Z","repository":{"id":350767221,"uuid":"1208182105","full_name":"ctoth/gunray","owner":"ctoth","description":"Defeasible Datalog Python implementation","archived":false,"fork":false,"pushed_at":"2026-04-12T00:01:01.000Z","size":200,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-04-12T02:11:14.266Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ctoth.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-11T23:41:52.000Z","updated_at":"2026-04-12T00:01:07.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ctoth/gunray","commit_stats":null,"previous_names":["ctoth/gunray"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/ctoth/gunray","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ctoth%2Fgunray","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ctoth%2Fgunray/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ctoth%2Fgunray/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ctoth%2Fgunray/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ctoth","download_url":"https://codeload.github.com/ctoth/gunray/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ctoth%2Fgunray/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31964784,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-18T00:39:45.007Z","status":"online","status_checked_at":"2026-04-18T02:00:07.018Z","response_time":103,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-18T10:14:51.918Z","updated_at":"2026-04-18T10:14:53.106Z","avatar_url":"https://github.com/ctoth.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Gunray\n\n*(named for Donald Nute, by way of Nute Gunray)*\n\nA defeasible logic engine in pure Python. Tell it rules that contradict\neach other and it works out which conclusions survive — and, if you ask,\nexactly why. Zero runtime dependencies, MIT, Python 3.11+.\n\n```python\nfrom gunray import DefeasibleTheory, GunrayEvaluator, Policy, Rule\n\ntheory = DefeasibleTheory(\n    facts={\"bird\": {(\"tweety\",), (\"opus\",)}, \"penguin\": {(\"opus\",)}},\n    strict_rules=[Rule(id=\"s1\", head=\"bird(X)\", body=[\"penguin(X)\"])],\n    defeasible_rules=[\n        Rule(id=\"r1\", head=\"flies(X)\",  body=[\"bird(X)\"]),\n        Rule(id=\"r2\", head=\"~flies(X)\", body=[\"penguin(X)\"]),\n    ],\n)\n\nmodel = GunrayEvaluator().evaluate(theory, Policy.BLOCKING)\n# model.sections[\"defeasibly\"] contains flies(tweety) and ~flies(opus).\n```\n\n`~flies(X) :- penguin(X)` is a *defeasible* rule, not a strict one. It's\nweaker than anything classical logic would let you write — but strong\nenough to win against `flies(X) :- bird(X)` on the penguin case without\ncontradicting the strict fact that penguins are still birds.\n\n## `UNDECIDED` is a first-class answer\n\nSometimes the evidence genuinely does not resolve. Nixon was a Quaker\n*and* a Republican; one defeasible rule says Quakers are pacifists,\nanother says Republicans aren't. Neither argument out-specifies the\nother — so Gunray returns `Answer.UNDECIDED` rather than inventing a\nwinner.\n\n```python\nfrom gunray import (\n    Answer, DefeasibleTheory, GeneralizedSpecificity, Rule, answer,\n)\nfrom gunray.types import GroundAtom\n\ntheory = DefeasibleTheory(\n    facts={\"republican\": {(\"nixon\",)}, \"quaker\": {(\"nixon\",)}},\n    defeasible_rules=[\n        Rule(id=\"r1\", head=\"~pacifist(X)\", body=[\"republican(X)\"]),\n        Rule(id=\"r2\", head=\"pacifist(X)\",  body=[\"quaker(X)\"]),\n    ],\n)\n\npacifist_nixon = GroundAtom(predicate=\"pacifist\", arguments=(\"nixon\",))\ncriterion      = GeneralizedSpecificity(theory)\n\nassert answer(theory, pacifist_nixon, criterion) is Answer.UNDECIDED\n```\n\n`Answer` has four values: `YES` (the literal is warranted),\n`NO` (its complement is warranted), `UNDECIDED` (arguments exist on\nboth sides and neither wins), `UNKNOWN` (the predicate is not in the\nlanguage of the theory). This is García \u0026 Simari 2004 Def 5.3.\n\n## Install\n\n```bash\npip install git+https://github.com/ctoth/gunray.git\n# or\nuv add git+https://github.com/ctoth/gunray.git\n```\n\nFor development:\n\n```bash\ngit clone https://github.com/ctoth/gunray.git\ncd gunray\nuv sync --extra dev\n```\n\n## Three evaluators, one dispatcher\n\n`GunrayEvaluator.evaluate` dispatches on the input type.\n\n- **`DefeasibleTheory`** → the DeLP pipeline: arguments, dialectical\n  trees, Procedure 5.1 marking, four-valued answers. The main event.\n- **`Program`** → stratified Datalog with Apt-Blair-Walker safety and a\n  choice of negation semantics (see below).\n- **Propositional defaults** → KLM rational / lexicographic / relevant\n  closure via `gunray.closure.ClosureEvaluator`.\n\n```python\nfrom gunray import GunrayEvaluator, Program\n\nmodel = GunrayEvaluator().evaluate(Program(\n    facts={\"edge\": {(\"a\", \"b\"), (\"b\", \"c\")}},\n    rules=[\n        \"path(X, Y) :- edge(X, Y).\",\n        \"path(X, Z) :- edge(X, Y), path(Y, Z).\",\n    ],\n))\n# model.facts[\"path\"] == {(\"a\", \"b\"), (\"b\", \"c\"), (\"a\", \"c\")}\n```\n\n`DefeasibleEvaluator`, `SemiNaiveEvaluator`, and `ClosureEvaluator` are\nexported directly if you'd rather skip the dispatcher.\n\n## Explanations — why did the engine decide that?\n\nWhat was concluded is usually less interesting than why. For any\nconclusion, Gunray gives you the dialectical tree, a marking, and a\nprose transcript of the argument-and-defeater chain. Using the Tweety\ntheory from the opening example:\n\n```python\nfrom gunray import (\n    GeneralizedSpecificity, build_arguments, build_tree,\n    explain, mark, render_tree,\n)\nfrom gunray.types import GroundAtom\n\nflies_opus = GroundAtom(predicate=\"flies\", arguments=(\"opus\",))\ncriterion  = GeneralizedSpecificity(theory)\n\nfor arg in build_arguments(theory):\n    if arg.conclusion == flies_opus and arg.rules:\n        tree = build_tree(arg, criterion, theory)\n        print(render_tree(tree))          # Unicode tree diagram\n        print(mark(tree))                 # \"U\" (warranted) or \"D\" (defeated)\n        print(explain(tree, criterion))   # prose transcript\n        break\n```\n\n```\nflies(opus)  [r1]  (D)\n└─ ~flies(opus)  [r2]  (U)\nD\nflies(opus) is NO.\nAn argument supports flies(opus) from {bird(opus)} via r1.\nIt is defeated by an argument for ~flies(opus) from {penguin(opus)} via r2, which is strictly more specific.\n```\n\nTrees also render to Mermaid via `render_tree_mermaid`. Here is the\npeer-review conflict-of-interest case from\n[`examples/reviewer_assignment.py`](examples/reviewer_assignment.py) —\na disqualification waiver (`df1`) is specific enough to lift\ninstitutional COI (`d3`), but an explicit superiority pair keeps\nadvisor COI (`d4`) above the waiver:\n\n```mermaid\nflowchart TD\n    n0[\"eligible(dave, author_d) [d1] D\"]\n    n1[\"~eligible(dave, author_d) [d3] U\"]\n    n2[\"eligible(dave, author_d) [df1] D\"]\n    n3[\"~eligible(dave, author_d) [d4] U\"]\n    n4[\"~eligible(dave, author_d) [d4] U\"]\n    n2 --\u003e n3\n    n1 --\u003e n2\n    n0 --\u003e n1\n    n0 --\u003e n4\n```\n\nThe `[d1] D` / `[d3] U` markings are the Procedure 5.1 verdicts at each\nnode. The tree is how a contested conclusion becomes legible when the\nanswer disagrees with your intuition.\n\nFor the full `evaluate_with_trace` API — stratum-by-stratum rule-fire\nlogs for Datalog, `tree_for` / `marking_for` /\n`arguments_for_conclusion` lookups for defeasible theories — see\n[`ARCHITECTURE.md`](ARCHITECTURE.md).\n\n## `SAFE` vs `NEMO` — negation semantics\n\nRules with variables in negated body literals have two competing\nreadings in the literature. Gunray ships both.\n\n- `NegationSemantics.SAFE` (default) — Apt, Blair \u0026 Walker 1988\n  stratified-Datalog safety. Every variable in a negated body literal\n  must be bound by a positive body literal. Unsafe programs raise\n  `SafetyViolationError`.\n- `NegationSemantics.NEMO` — Ivliev et al. 2024 (KR 2024,\n  [doi:10.24963/kr.2024/70](https://doi.org/10.24963/kr.2024/70)):\n  variables in negated literals are interpreted existentially over the\n  active Herbrand universe. Used by the conformance suite's Nemo\n  fixtures.\n\nSame theory, different answers. See\n[`examples/safe_vs_nemo.py`](examples/safe_vs_nemo.py).\n\n## Tests\n\n```bash\nuv run pytest tests -q\nuv run pytest tests/test_conformance.py \\\n  --datalog-evaluator=gunray.conformance_adapter.GunrayConformanceEvaluator -q\nuv run pyright\nuv run ruff check\nuv run ruff format --check\n```\n\nThe conformance suite is\n[`ctoth/datalog-conformance-suite`](https://github.com/ctoth/datalog-conformance-suite).\nA handful of fixtures are explicitly out-of-contract and marked skip —\nsee [`ARCHITECTURE.md`](ARCHITECTURE.md#out-of-contract) for the list\nand the reasons.\n\n## More\n\n- [`examples/`](examples/) — the full catalogue. Showcase cases, domain\n  depth (clinical, GDPR, data fusion, access control), engine breadth\n  (Datalog, KLM closure, SAFE vs NEMO), and Mermaid visuals.\n- [`ARCHITECTURE.md`](ARCHITECTURE.md) — module layout, DeLP pipeline\n  internals, preference composition, strict-only fast path, pitfalls.\n- [`CITATIONS.md`](CITATIONS.md) — the paper trail.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fctoth%2Fgunray","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fctoth%2Fgunray","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fctoth%2Fgunray/lists"}