# 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`.