# dkepy
Write facts into a [DKE](https://dke.langsyn.com/) store, from Python, for
kinds you define yourself.
Distribution name `dkepy`, import name `dke` — they differ because `dke` is
taken on PyPI, and the package provides the module `dke` so that
`from dke import ...` reads the way it should.
**Where to get it.** The source is served, file by file and as a reproducible
tarball, at . It is **not on PyPI**: the
distribution name is unregistered there as of 2026-08-25, so `pip install dkepy`
installs nothing of ours and would install whatever a stranger registers under
that name. Put the `src/` directory on your path, or install from the tarball.
## What it is for
A DKE reasoning module supplies the reasoning — the schema, the standing
rules, the queries. You supply the facts. This package is the second half: it
turns whatever you have read into a DKE Python program that records it, which
you submit to your store through whichever DKE client you already use.
```python
from dke import FactProgram
prog = FactProgram("inventory", source="warehouse-scan")
prog.comment("Stock levels, read 2026-08-21.")
prog.open()
prog.table(
"items",
['("widget", "Widget", 12)', '("sprocket", "Sprocket", 0)'],
[prog.put("Item.s.label", "r[1]"),
prog.put("Item.s.count", "r[2]")],
)
print(prog.render())
```
The output is a program you can read before you run it. Every fact it would
record is a line you can see.
## Why not just write the program by hand
You can, and for a handful of facts you should. What this package is for is
the case where a machine produces them, because three things are easy to get
wrong there and wrong in the direction that stays quiet:
- **A subject that moves between runs.** Re-ingesting then records a second
set of facts beside the first instead of revising it, and every query that
joined on the old subject still answers — with stale numbers.
- **The write verb.** A producer means "replace my own earlier reading", and
that is what `update` says. `put()` emits it.
- **An optional cell written as an empty string.** A rule that concludes from
the *absence* of a cell is silenced by a value stored everywhere, including
a falsy one. `table()` is shaped so a row can carry a placeholder while the
write stays behind a guard.
A producer with a broken reader records nothing, which is loud. A producer
with a broken fact-writer records plausible facts, and every query still
answers.
## The code readers
`dke.ingest` holds producers for one domain — source code, for the `code`
reasoning module:
```
dke-ingest-python ./src > facts.dpy
dke-ingest-cpp ./src ./include > facts.dpy
```
Both are lower bounds by construction. Every edge they record is one the
source makes plain; a call through a variable, a dict of handlers, `getattr`
or any dynamic dispatch is not visible to a reader and is not recorded. Each
module documents its own limits — read those before trusting a count.
They are also the worked example. Your own reader supplies the rows, and
everything from the rows onward is the same code.
## Submitting what it produced
What comes out is a PROGRAM — statements at top level — so it goes to your
store through the same verb as any other question you ask. Nothing needs
compiling: `compile` stores a definition, and a fact load defines nothing.
Over MCP, that is one call:
```
run(source=)
```
and this package can make it for you. The client is a separate import, on
purpose — see **No network, no subprocess** below:
```python
from dke.client import Client
client = Client.from_env() # reads DKE_API_KEY
result = client.run(program).raise_for_status()
print(result.text, result.ops)
```
Either way the reply lists the claims it wrote, which is the same list you
could read in the file first.
This section named an internal command-line tool until 0.5.0. It is not one
you have — it ships to nobody — so the only routes were your own MCP client
or hand-built JSON, which is what `dke.client` replaces.
The reasoning module is the other half and goes in the other way, because a
module IS a definition — its schema, rules and queries are meant to persist:
```
compile(name="code", source=)
```
Do that once. Re-running a producer afterwards revises the facts underneath it
and leaves the reasoning alone, which is what `update` is for.
## Telling whether a load finished
A fact load is not all-or-nothing. If a submission stops part-way — a dropped
connection, a service restart, a refusal on one slot — the claims it already
made stay made. Re-running the producer repairs that, because it writes
`update` and revises its own earlier reading rather than adding beside it. What
re-running cannot do is tell you there was anything to repair: a partially
loaded store looks exactly like a complete one, and every query answers from it
with the same confidence.
So each program records a marker for the source it loads:
```
Load..started when the load began
Load..declared how many rows it meant to write
Load..completed written last, and only if the load reached the end
```
`completed` is the final statement of the program, so an interrupted load
simply never writes it. That is the point: you are told a load finished only by
a load that finished, and a run that died says nothing at all. A load is
complete when `completed` is at least `started`; if it is older, or missing,
the last load of that source did not finish and `declared` says how much it was
going to write.
```python
st = current(Load.python.started)
sv = st.value
fi = current(Load.python.completed)
fv = fi.value
match sv:
case int as a:
match fv:
case int as b:
print(b >= a)
case default:
print(False)
case default:
print(False)
```
A load that legitimately wrote fewer facts — because your codebase shrank — is
not the same thing and does not read as one: it finished, so `completed` is
current and `declared` is simply smaller.
`Load` is deliberately not under the reasoning module's name, so you can ask
this question without importing the module first — which matters most in the
case where a load may have failed. If your own module declares a `Load` of its
own, `FactProgram.load_marker` takes the kind name as an argument.
## No network, no subprocess
**Producing facts never needs your key.** `import dke`, `dke.factwriter` and
everything under `dke.ingest` open no socket and spawn no process, so you can
point a reader at your own source without granting it anything, and read the
program it wrote before deciding to send it.
`dke.client` is the exception, and it is one you have to ask for: it is the
only module here that talks to a network, it is reached by nothing the
producing half imports, and `from dke.client import Client` is what grants it.
Naming it IS the grant.
None of that is promised in prose. `test/test_ingest.py` asserts it three
ways, because they fail differently: every producing module is read for a
networking import; the import GRAPH is walked from each producing root to show
the client is unreachable; and a fresh interpreter imports the producing half
and is asked what it actually loaded. A file scan cannot see an edge, and a
graph walk cannot see an import performed at run time.
The exemption for the client is checked in **both** directions — a module that
is exempt and imports no networking module fails too — so it cannot outlive
the reason it was granted.
## Requirements
Python 3.10 or later. No runtime dependencies.
## Licence
MIT. See `LICENSE`.