# nodum

> DB-native knowledge graph: a typed graph of Markdown-content nodes and typed edges in **one SQLite file** — not files with an index over them. Every mutation flows through a deterministic, LLM-free service layer that validates it, moves it through a `proposed → active → archived` state machine, appends it to an event log with full before/after payloads, snapshots a node version, and leaves it reversible via `undo`. `[[wikilinks]]` in content materialize as `mentions` edges on write. Retrieval is hybrid search: an FTS5 BM25 index and a sqlite-vec chunk-embedding index (local in-process model, no daemon and no API key), fused by reciprocal rank fusion and re-ranked by graph expansion. Four surfaces share that one store — a JSON-emitting CLI, an HTTP API, an MCP server for external agents, and a React web UI. Installable from PyPI with `pipx install nodum`; there is no server to run and no database URL.

## Docs

- [Overview](https://nodum.vcoeur.com/): what nodum is, why one file, what makes it agent-native, and the current phase status.
- [Install](https://nodum.vcoeur.com/install/): `pipx install nodum`, the optional `embeddings` extra, database path resolution, and verifying an install.
- [Quick start](https://nodum.vcoeur.com/quick-start/): build a graph, watch a wikilink become an edge, search it, read its history, then the agent proposal flow.
- [Concepts](https://nodum.vcoeur.com/concepts/): typed graph, the state machine, the event log, actors and privilege, projectors, assets, and why the surfaces cannot drift.
- [Architecture](https://nodum.vcoeur.com/architecture/): the service spine and the thin adapters over it, module by module.
- [Commands](https://nodum.vcoeur.com/commands/): the full CLI surface plus the conventions every command shares.

## For agents

- **Self-orient with `nodum schema-dump`.** It prints this CLI's entire command tree as JSON — every command, its one-line help, its parameters, and one level of subcommands. It needs no database, so it works immediately after install. Note it is a different thing from `nodum schema <type>`, which reports one *node or edge type's* catalog entry read from the database.
- **Every command prints exactly one JSON object on stdout** and nothing else on the success path, so parse stdout directly. There is no `--json` flag — JSON is the only output format. Human-facing messages and errors go to stderr. Errors are always one line with exit 1, never a traceback.
- **List commands wrap rows in a named key plus a count**: `{"nodes": [...], "count": 2}`. The HTTP API returns the same envelope bytes, so a client parses one shape whichever surface it talks to.
- **You are not a human, and the surfaces enforce that.** The CLI is human-only: every command that touches the graph requires `--as human:<id>` — reads included, since reads are grant-scoped too — and there is no way to name an agent on it. You drive the MCP server, which authenticates you with the agent token in `NODUM_AGENT_TOKEN` — minted by `nodum agent create <name>` (shown once, stored hashed; the human carries it in the MCP client config's env block, never on a command line). Your writes land per your grants: `suggest` on a space queues them as `proposed` in the review queue, `edit` lands them `active`. There is no auto-accept mechanism — an agent earns `edit`, or it waits.
- **Editing does not overwrite — under `suggest`.** An agent `node update` on a `suggest` grant stages a `proposed` *version* recording which fields it named. Accepting applies **only those fields** to the node as it stands at that moment, so a human edit made while your proposal waited is not reverted. **On an `edit` grant the same call overwrites the node in place**, which is why the MCP `update_node` tool is annotated destructive while the additive tools are not.
- **Your `[[wikilinks]]` cross spaces only as far as your grants do.** A link into a space you may only suggest in stays a `proposed` `mentions` edge even when the node carrying it is live, and a reviewer who holds nothing on that space cannot activate it by accepting your node. Equally, saving a node never retires a mention into a space you cannot read — an unresolvable link is not a removed one.
- **Review authority is a human, or `edit` on the item's space.** `accept`, `reject`, `archive`, and every `review` subcommand are limited to those; `undo` stays human-only. Account and grant administration (`nodum human …`, `nodum agent …`, `nodum grant/revoke/grants`, `nodum space-*`) is human-only too — you cannot mint or re-level your own access.
- **Over MCP the read and additive tiers are the whole registry.** The review tools and the curative tools are never registered — they are absent from the tool list, not refused at call time — so whatever your grants, you can propose but never dispose over MCP. This is structural, not a filter — do not try to route around it.
- **The HTTP API is the human's surface.** It runs on password-login sessions (`POST /api/login`, server-side session rows, an `HttpOnly; SameSite=Strict` cookie), every `/api` route but login requires one, and no request field, header, or query parameter can set an identity. There is no agent authentication there by design.
- **Search is hybrid.** `nodum search <query>` returns `{query, k, hits}` where each hit carries `node_id`, `type`, `title`, `snippet`, `score`, and a per-signal `signals` breakdown (`bm25`, and `vector` when the embedding extra is installed). Without that extra, ranking degrades to BM25 rather than failing.
- **Derived indexes are projections of the event log**, so they can lag. `nodum projector status` reports each projector's checkpoint, backlog, and availability; `nodum projector run` applies pending entries; `nodum projector rebuild <name>` replays from event 0.
- **Graph reads**: `node get <id> --depth N` for a neighbourhood, `traverse`, `subgraph <root-id>` (bounded by node and edge caps, with a `truncated` flag), `find-path`, and `suggest-links <prefix>` for `[[` autocomplete — the last reads `nodes` directly, so it answers on a database whose projectors have never run.
- **Assets**: registration is content-addressed by sha256 and idempotent. `asset get` returns metadata and never bytes; `asset rendition` returns a generated, cached `thumb`/`preview`. **Agents receive renditions only — originals are never served over MCP.** Assets are not space-scoped until Phase 4: the interim rule is that any grant reaches them and no grant reaches none of them, and an `asset_ref` node id only resolves as a handle if you can read its space.
- **`--set key=value` is repeatable**, and values are parsed as JSON with a raw-string fallback: `--set year=1815` yields an integer, `--set venue=Nature` a string.
- **A rejection needs a reason.** Both `reject <id> --reason` and `review reject … --reason` require it and record it in the reject event's payload.

## Optional

- [Source on GitHub](https://github.com/vcoeur/nodum)
- [nodum on PyPI](https://pypi.org/project/nodum/)
