# 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. ## A load too big for one program A store refuses a program larger than a size it is set up with. A load that outgrows one program goes through `FactLoad` instead: the same tables, the same `put` and `q`, and as many programs as the size you pass needs. Take that size from your store's documentation or its operator, and stay well inside it. ```python from dke import FactLoad load = FactLoad("inventory", source="warehouse-scan", max_bytes=130_000) load.comment("Stock levels, read 2026-10-08.") load.table("items", rows, [load.put("Item.s.label", "r[1]"), load.put("Item.s.count", "r[2]")]) load.load_marker("warehouse", stamp) for program in load.render(): client.run(program).raise_for_status() ``` `rows` are your rows exactly as `FactProgram.table` takes them, `stamp` is any number that grows from one load to the next (epoch milliseconds is the obvious one), and `client` is a `dke.client.Client`. Every program is whole: its own import, its own tables, its own loops, so each runs on its own. A split falls between rows and never inside a table's loop. Rows that belong together — a sentence and its words — go in through `load.add({"sentences": [...], "words": [...]})` as one group, and no split falls inside a group. A group too big for a program of its own is refused rather than split. Its rows are written into the program's text, so the program's size is the one limit a load meets. With a load marker, each program carries its own, under `warehouse_01`, `warehouse_02` and so on, and each also records `parts`: how many programs the load was split into. Each program commits on its own, so each needs its own proof that it reached its end. The last load finished when the programs whose `started` holds the newest stamp number `parts`, and every one of them has `completed` at least that stamp. A program that never ran shows up as one missing from that count. A load that fits in one program is still written as part `_01` of 1, so the marker keeps its shape on the day the load grows. ## 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`.