eyekind 0.5.0__tar.gz

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,9 @@
1
+ # MIT License
2
+
3
+ ### Copyright 2021-2026 Jos De Roo, KNoWS office of IDLab, Ghent University - imec
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
eyekind-0.5.0/PKG-INFO ADDED
@@ -0,0 +1,438 @@
1
+ Metadata-Version: 2.4
2
+ Name: eyekind
3
+ Version: 0.5.0
4
+ Summary: A dependency-free Python rule language with forward and backward reasoning and checkable proofs.
5
+ Author: Jos De Roo
6
+ License: # MIT License
7
+
8
+ ### Copyright 2021-2026 Jos De Roo, KNoWS office of IDLab, Ghent University - imec
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15
+
16
+ Project-URL: Homepage, https://github.com/eyereasoner/eyekind
17
+ Project-URL: Documentation, https://eyereasoner.github.io/eyekind/
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: License :: OSI Approved :: MIT License
20
+ Classifier: Operating System :: OS Independent
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE.md
25
+ Dynamic: license-file
26
+
27
+ # eyekind
28
+
29
+ [![PyPI version](https://img.shields.io/badge/pypi-v0.5.0-blue.svg)](https://pypi.org/project/eyekind/)
30
+ [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.23191223-blue.svg)](https://doi.org/10.5281/zenodo.23191223)
31
+
32
+ <img src="https://josd.github.io/images/eye.png" alt="EYE" width="100">
33
+
34
+ *eyekind — reasoning you can see.*
35
+
36
+ A standalone, dependency-free **Python rule language** with forward and backward
37
+ reasoning and checkable proofs.
38
+
39
+ **[Playground](https://eyereasoner.github.io/eyekind/playground/)** ·
40
+ **[Examples](https://eyereasoner.github.io/eyekind/examples/)** ·
41
+ **[Example decks](https://eyereasoner.github.io/eyekind/examples/deck/)** ·
42
+ **[Specification](https://eyereasoner.github.io/eyekind/SPEC)** ·
43
+ **[Conformance suite](conformance/)** ([run it in your browser](https://eyereasoner.github.io/eyekind/playground/conformance)) ·
44
+ **[PyPI](https://pypi.org/project/eyekind/)**
45
+
46
+ Most software computes an answer and the reasoning evaporates. eyekind makes a
47
+ different bargain: **an answer can arrive together with the reasoning that
48
+ supports it, in a form another program can check.** You write facts and rules;
49
+ eyekind draws conclusions, and every conclusion can come with a proof that a
50
+ separate checker verifies against your program.
51
+
52
+ - [The idea in a few lines](#the-idea-in-a-few-lines)
53
+ - [Run it](#run-it)
54
+ - [Writing programs](#writing-programs)
55
+ - [Proofs, and what checking one means](#proofs-and-what-checking-one-means)
56
+ - [Checking the translation](#checking-the-translation)
57
+ - [From Python](#from-python)
58
+ - [The examples](#the-examples)
59
+ - [What is deliberately absent](#what-is-deliberately-absent)
60
+ - [The name](#the-name)
61
+
62
+ ## The idea in a few lines
63
+
64
+ ```python
65
+ from eyekind import *
66
+
67
+ fact(human('socrates'))
68
+ implies(human(X), mortal(X))
69
+ ```
70
+
71
+ A fact and a rule. eyekind concludes `mortal('socrates')`. Ask for the reasoning
72
+ and you get a document that names the fact it used, the rule it applied, and
73
+ the substitution that connects them:
74
+
75
+ ```python
76
+ mortal('socrates')
77
+
78
+ clause(1, fact(human('socrates')))
79
+ clause(2, implies(human(X), mortal(X)))
80
+
81
+ step(mortal('socrates'), clause(2), {'X': 'socrates'}, [human('socrates')])
82
+ step(human('socrates'), clause(1), {}, [])
83
+ ```
84
+
85
+ That proof is itself Python, one expression per line, and a checker that never
86
+ consults the reasoner re-establishes every step against the program. Save the
87
+ two lines above, with their import, as `socrates.py` and try it:
88
+
89
+ ```sh
90
+ python -m eyekind --proof socrates.py | python -m eyekind --check-proof - socrates.py
91
+ ```
92
+
93
+ The checker prints a report ending in `verdict('checked')`. The certificate for
94
+ a ten-thousand-step derivation works exactly the same way.
95
+
96
+ What a checked derivation adds is a place for disagreement to land. When a
97
+ conclusion is wrong, there are only three possibilities, and a proof tells you
98
+ which: the inference was invalid, the rule did not say what you meant, or the
99
+ input fact was wrong. A checked proof settles the first, so you can spend your
100
+ attention on the other two.
101
+
102
+ ## Run it
103
+
104
+ Python 3.9 or newer. No dependencies, no build step. From the root of a
105
+ checkout:
106
+
107
+ ```sh
108
+ python -m eyekind examples/socrates.py
109
+ python -m eyekind --proof examples/socrates.py
110
+ python -m eyekind --proof examples/socrates.py | python -m eyekind --check-proof - examples/socrates.py
111
+ ./test
112
+ ```
113
+
114
+ `./test` runs every test, one numbered line each: the unit tests, the
115
+ examples (each must reproduce its saved output, proof and report), and the
116
+ conformance suite, in process and through the command line. `./test examples`
117
+ runs one section, and `./test -k skolem` only the tests that mention skolem.
118
+
119
+ To use eyekind from anywhere, install it; an editable install keeps using the
120
+ checkout, so your edits take effect at once:
121
+
122
+ ```sh
123
+ pip install -e .
124
+ eyekind --proof examples/socrates.py
125
+ python examples/socrates.py --proof
126
+ ```
127
+
128
+ Once eyekind is installed, a program is also a script: `python program.py` runs
129
+ it with the same options as `eyekind program.py`. Run `eyekind --help` for the full
130
+ command line, which [SPEC §14](SPEC.md#14-command-line) describes.
131
+
132
+ Or in the browser: the [playground](https://eyereasoner.github.io/eyekind/playground/)
133
+ edits, runs and checks any example, with eyekind running in the page through
134
+ Pyodide. To run it from a checkout, serve it (`python -m http.server`) and open
135
+ `/playground/`.
136
+
137
+ ## Writing programs
138
+
139
+ A program is a Python module that starts with `from eyekind import *` and states
140
+ clauses with these calls:
141
+
142
+ | Call | Meaning |
143
+ | --- | --- |
144
+ | `fact(*Heads)` | Each `Head` holds; one fact per argument. |
145
+ | `implies(Premise, Conclusion)` | A forward rule: whenever `Premise` holds, conclude `Conclusion`, until nothing new follows. |
146
+ | `implied_by(Conclusion, Premise)` | A backward rule: `Conclusion` holds when `Premise` does, decided when a goal asks for it. |
147
+ | `query(*Body)` | Publish every instance of `Body` that holds. |
148
+ | `contradiction(*Body)` | Stop with exit code 65 when `Body` holds: an integrity constraint. |
149
+ | `facts_from(path)`, `facts_from(text=...)` | State every expression of a document as a fact, without running it. |
150
+
151
+ A premise joins its goals with `&`, and so can a forward rule's conclusion: `implies(p(X) & q(X), r(X) & s(X))`. A conclusion
152
+ is never `true` or `false`: `query(...)` and `contradiction(...)` say those,
153
+ and proofs show them that way.
154
+
155
+ **Data that is not a program.** Everything eyekind writes, conclusions, proofs
156
+ and check reports, is a *document*: plain expressions, one per line, with no
157
+ `from eyekind import *` and no `fact(...)` around them, so it cannot be imported.
158
+ `facts_from` reads such a document as data and states each expression as a
159
+ fact. It never executes the text, so it is also the safe way to take in data
160
+ you do not trust; a data file written as a program would run as Python code.
161
+ It is what lets one run reason over another, as
162
+ [audited-grants](examples/audited-grants.py) reasons over its own check report:
163
+
164
+ ```python
165
+ facts_from('data.py') # a file of expressions such as score('ann', 3)
166
+ facts_from(text=check_report(result.proof_report)) # or text, such as another run's report
167
+ ```
168
+
169
+ The two kinds of rule compose. A forward rule's body may call backward
170
+ definitions, and a backward goal may use facts that forward reasoning
171
+ established, so you let knowledge accumulate and then ask a focused question:
172
+
173
+ ```python
174
+ from eyekind import *
175
+
176
+ fact(parent('alice', 'bob'))
177
+ fact(parent('bob', 'carol'))
178
+
179
+ implies(parent(X, Y), ancestor(X, Y))
180
+ implies(ancestor(X, Y) & parent(Y, Z), ancestor(X, Z))
181
+
182
+ implied_by(related(X, Y), ancestor(X, Y))
183
+ implied_by(related(X, Y), ancestor(Y, X))
184
+
185
+ query(related('alice', 'carol'))
186
+ ```
187
+
188
+ With nothing asked, a run prints the facts it derived; `query(...)` asks the
189
+ program's own questions, and `--goal "Goal"` asks from outside instead.
190
+
191
+ **Nothing to declare.** A name the program uses without defining it is a
192
+ variable when it starts with a capital or an underscore, like `X` or `_Rest`,
193
+ and a predicate otherwise, like `parent`. Python's builtins keep their meaning
194
+ in ordinary Python code, so a program can compute with `int`, `getattr` or
195
+ `sorted`; inside a statement such as `fact(type('socrates', 'human'))` a
196
+ builtin name is a predicate, except for the few a program computes with there:
197
+ `print`, `range`, `len`, `list`, `dict`, `set`, `tuple`, `str`, `enumerate`,
198
+ `zip`, `sorted`, `reversed`, `isinstance`, `open`, `repr`, `chr`, `ord`,
199
+ `iter`, `any`, `all`, `map` and `filter`, and builtins beginning with an
200
+ uppercase letter. A program that wants one of those as
201
+ a predicate says so, `range = preds('range')`. A call of
202
+ an undeclared predicate on its own, such as a misspelled `fcat(p(1))`, states
203
+ nothing, so eyekind stops with an error instead of ignoring it.
204
+
205
+ **Terms are Python values.** An atom is a string, `'socrates'`; a number is an
206
+ `int` of any size or a finite `float`; a list is a list, and `[H, *T]` has head `H`
207
+ and tail `T`; a compound term is a call, `parent('alice', 'bob')`; and `_` is a
208
+ fresh variable at every occurrence. Goals join with `&` (and), `|` (or) and `~`
209
+ (not), alongside `call`, `once` and `findall`. Python reads `&` and `|` before
210
+ comparisons, so write `(X > 1) & (Y < 2)`; a chained comparison such as
211
+ `X > 1 & Y < 2` stops the program with an explanation instead of meaning
212
+ something else.
213
+
214
+ **Numbers are Python's.** Integers are exact and unbounded, so the Fibonacci
215
+ example computes F(10000), a 2,090-digit integer, exactly. An expression means
216
+ what it means in Python: `4 / 2` is `2.0`, `//` and `%` are floor division and
217
+ modulo, `round` rounds half to even. `1` and `1.0` do not unify, yet compare
218
+ exactly. Infinity and NaN are rejected, including overflowing float literals
219
+ such as `1e999` in programs, goals and documents. A result that is not a
220
+ finite real number stops the run rather than becoming a value.
221
+
222
+ The primitives (`unify`, `is_`, `eq`, comparisons, type tests, `functor`,
223
+ `univ`, `atom_concat` and the rest) are listed in [SPEC §5](SPEC.md#5-goals-controls-and-primitives),
224
+ and the arithmetic functions in [SPEC §6](SPEC.md#6-arithmetic). Everything
225
+ else, such as membership, mapping or graph traversal, is written as ordinary
226
+ clauses.
227
+
228
+ **Domains get representations, not syntax.** eyekind has no built-in notion of
229
+ RDF or of anything else: IRIs, typed literals, triples and quoted graphs are
230
+ just terms, such as `literal(V, lang(L))` or `triple(S, P, O)`. A forward head
231
+ may contain variables its body never binds, and then says that something
232
+ exists. Each such unknown becomes a Skolem function of the activation:
233
+ `implies(person(X), has_parent(X, P))` concludes
234
+ `has_parent('ann', skolem(2, 'P', ['ann']))`, the `P` of rule 2 for Ann. The
235
+ same activation always gives the same term, in any round and any run, which
236
+ keeps conclusions deduplicated, and a different one a different term, so two
237
+ unknowns never pass for one.
238
+
239
+ ## Proofs, and what checking one means
240
+
241
+ A proof holds claims, the source clauses it displays, and one inference record
242
+ per step. The checker establishes seven conditions:
243
+
244
+ | | Condition | What it establishes |
245
+ | --- | --- | --- |
246
+ | **C1** | resolution | Every step is an instance of a clause of the program, with conclusion and premises agreeing under one substitution |
247
+ | **C2** | well-foundedness | The derivation has no cycles |
248
+ | **C3** | justification | Every step's justification is known, well-formed and unique |
249
+ | **C4** | coverage | Every claim and every premise is accounted for |
250
+ | **C5** | re-decision | Built-in calculations are recomputed rather than trusted |
251
+ | **C6** | boundary consistency | No trusted absence or collection is contradicted by the evidence at hand |
252
+ | **C7** | relevance | Every claim answers the question asked, and every step serves a claim |
253
+
254
+ Two properties make this worth more than a log. **The checker follows the
255
+ certificate:** it never calls the reasoner to fill a gap, so a missing step
256
+ fails the check rather than being quietly re-derived. **The source is the
257
+ authority:** the clauses a proof displays cannot override the program, so you
258
+ cannot smuggle a rule into a proof, nor pad it with steps no claim needs.
259
+
260
+ The report is Python data too, so one program's evidence is material another
261
+ program can reason over, with `facts_from('report.py')`:
262
+
263
+ ```python
264
+ condition('C1', 'resolution', 'ok', 2)
265
+ condition('C2', 'well_founded', 'ok', 2)
266
+ condition('C3', 'justification', 'ok', 2)
267
+ condition('C4', 'coverage', 'ok', 2)
268
+ condition('C5', 're_decision', 'ok', 0)
269
+ condition('C6', 'boundary_consistency', 'ok', 0)
270
+ condition('C7', 'relevance', 'ok', 3)
271
+ steps(2)
272
+ verified(2)
273
+ recomputed(0)
274
+ composed(0)
275
+ trusted(0)
276
+ claims(1)
277
+ verdict('checked')
278
+ ```
279
+
280
+ Failures appear as `failed(N)` outcomes with `failure(Condition, Conclusion,
281
+ Detail)` facts, an invalid proof gives `verdict(failed(N))` and exit code 2,
282
+ and `--json` gives the same report as JSON. Proofs are parsed as data and never
283
+ executed. Simple lines use a direct reader; other syntax uses Python's `ast`
284
+ module, with the same accepted terms, so checking a proof from someone else
285
+ runs none of their code. Every proof eyekind generates is checked before it is
286
+ returned.
287
+
288
+ ### Why negation and aggregation are safe
289
+
290
+ A negation (`~G`) concludes something from what is *not* there, and a
291
+ collection (`findall(T, G, L)`) from *everything* that is there. Both are only
292
+ right once nothing more can be derived: a fact added later would make an
293
+ earlier absence false, or a collected list incomplete. eyekind guards them in
294
+ four ways.
295
+
296
+ **Ask only once the answer is final.** When it loads a program, eyekind works
297
+ out which clauses each body goal depends on. A dependency is *closed* when the
298
+ goal sits inside `~` or `findall`. Every clause gets a rank, at least that of
299
+ everything it depends on, plus one across a closed dependency, and forward
300
+ rules run rank by rank, each to its fixpoint. By the time a rule asks
301
+ `~already_received(P)` or collects `findall(X, p(X), L)`, every rule that
302
+ could still produce such a fact has finished. A program with no such ranking
303
+ is rejected with "unstratified negation or collection dependency":
304
+
305
+ ```python
306
+ implies(~q, p)
307
+ implies(p, q) # p needs q to be absent, yet p yields q: rejected
308
+ ```
309
+
310
+ Positive cycles, such as transitive closure, are fine. Ranks compare whole
311
+ terms rather than predicate names, so `t(X, 'type', Y)` and
312
+ `t(X, 'status', Y)` can sit at different ranks, which matters when
313
+ everything is a triple. A goal that is a variable, reachable from a forward
314
+ rule, is rejected, because nobody can tell what it depends on.
315
+
316
+ **Negate only what is bound.** The variables of `~G` must be bound when it
317
+ is solved. With a variable, as in `~p(X)`, the question would quietly become
318
+ "is there no `X` at all?", so eyekind stops with an error instead. Bind the
319
+ variables first: `applicant(P), income(P, I), I < 2500,
320
+ ~already_received(P)`. When "no value at all" is what you mean, say so with
321
+ `_`: `~q(X, _)` holds when `q(X, Y)` has no solution for any `Y`.
322
+
323
+ **Say what was taken on trust.** A search that found nothing cannot be proved
324
+ the way a derivation can. So a proof records each negation as an `'absent'`
325
+ step and each collection as a `'collected'` step, the report lists them as
326
+ `obligation('absent', 'theory_scoped', Goal)`, and the verdict is
327
+ `checked_with_obligations` rather than `checked`. *Theory-scoped* says what
328
+ the claim is: absent according to this program, a closed-world assumption
329
+ made visible. `--strict-proof` rejects any proof that leans on one.
330
+
331
+ **Refute them when the evidence allows.** The checker cannot prove an
332
+ obligation, but it can catch one out (C6). An absence fails when a fact of
333
+ the program, a step of the proof or a recomputed primitive is a solution
334
+ after all, and a collection fails when such a solution is missing from its
335
+ list. A valid proof with obligations is valid *conditional on* them, and the
336
+ report says which they are.
337
+
338
+ ## Checking the translation
339
+
340
+ A checked proof shows that the conclusions follow from the program. It cannot
341
+ show that the program says what the law, the policy or the textbook says; the
342
+ person who knows the source has to judge that. What eyekind can do is point at the
343
+ parts of a program that make no difference to its conclusions, because those
344
+ are where a translation is most likely to be decorative, incomplete or untested:
345
+
346
+ ```python
347
+ from eyekind import *
348
+
349
+ fact(p('a'))
350
+ fact(q('b'))
351
+ implied_by(s(X), q(X))
352
+ fact(z('c'))
353
+ implies(p('a') & ~s('a'), 'ok')
354
+ ```
355
+
356
+ ```text
357
+ $ eyekind --unused program.py
358
+ unused(line(4), fact(q('b')))
359
+ unused(line(5), implied_by(s(X), q(X)))
360
+ unused(line(6), fact(z('c')))
361
+ ```
362
+
363
+ A clause is listed when no conclusion's proof uses it. A negation or a
364
+ collection may consult a clause without its proof recording that search, as
365
+ `~s('a')` consults `q('b')` and the rule for `s`; for each such clause eyekind
366
+ leaves it out, runs the program again, and lists it only when the conclusions
367
+ stay the same. The example programs `research-portal.py` and
368
+ `package-holiday.py` list no clauses.
369
+
370
+ ## From Python
371
+
372
+ ```python
373
+ from eyekind import check_proof, load_text, run
374
+
375
+ socrates = load_text("""
376
+ from eyekind import *
377
+ fact(human('socrates'))
378
+ implies(human(X), mortal(X))
379
+ """)
380
+ result = run(socrates, goal='mortal(X)', proof=True)
381
+ print(result.answers) # ["mortal('socrates')"]
382
+ print(result.bindings) # [{'X': "'socrates'"}]
383
+ print(check_proof(socrates, result.proof)['valid']) # True
384
+ ```
385
+
386
+ `load('program.py', ...)` loads program files and `load_text(source)` source
387
+ text. `run()` returns a `Result` with `answers`, `bindings`, `inferred`,
388
+ `stdout`, `proof`, `proof_report`, `stats` and `halt_code`; options are `goal`,
389
+ `goals`, `proof`, `max_depth` (1000000), `max_iterations` (1000 per stratum)
390
+ and `max_inferences` (1000000), and exceeding a bound raises `EyekindError` rather
391
+ than return a partial result. `check_report(report)` formats a report and
392
+ `read_term(text)` reads a term back. A goal given as text reads a bare name as
393
+ a variable, so a goal with no arguments is written `p()` or `'p'`.
394
+
395
+ ## The examples
396
+
397
+ The [example collection](https://eyereasoner.github.io/eyekind/examples/) is 63
398
+ complete programs, each with its conclusions, proof and C1-C7 report saved
399
+ beside it (`examples/output/`, `examples/proof/`, `examples/check/`), and a
400
+ [card deck](https://eyereasoner.github.io/eyekind/examples/deck/) that explains it
401
+ for a wide audience.
402
+
403
+ These are meant to be edited: change a fact, run it, look at what changed.
404
+ Because the artifacts are saved, you can see exactly what your change did:
405
+
406
+ ```sh
407
+ ./test # every test, one numbered line each
408
+ python tools/update_examples.py # regenerate artifacts after an intended change
409
+ python conformance/run.py # the conformance suite of SPEC.md
410
+ ```
411
+
412
+ ## What is deliberately absent
413
+
414
+ Cut, conditional commitment, mutable databases, attributed variables,
415
+ constraint libraries, tabling, and filesystem or network built-ins. Several of
416
+ these omissions are load-bearing: without cut and without a mutable database, a
417
+ derivation is a function of the program and its input, which is what makes a
418
+ certificate meaningful. A program is free to use Python to *compute* its
419
+ clauses (read a CSV, loop over a range), but once stated a clause is a term,
420
+ and reasoning over it calls no Python code of yours. Undefined predicates fail,
421
+ under the closed-world convention. How the engine itself works is described in
422
+ [SPEC Appendix C](SPEC.md#appendix-c-implementation-notes).
423
+
424
+ Begin with one question and a few facts. Give the relationships names, write
425
+ the rules you already believe, and let the program show you what they imply.
426
+ The first *unexpected* answer is often worth the most, because it points at a
427
+ specific rule or assumption to revisit.
428
+
429
+ ## The name
430
+
431
+ **eyekind** joins a *p*, from the Prolog its reasoning comes from, to *eye*, after
432
+ the [EYE](https://github.com/eyereasoner/eye) family of reasoners it belongs
433
+ to. And said aloud it sounds like *py*: in eyekind everything, from the rules and
434
+ the data to the proofs and the check reports, is done in Python.
435
+
436
+ ## License
437
+
438
+ [MIT](LICENSE.md)