root-kg 2.0.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.
Files changed (40) hide show
  1. root_kg-2.0.0/LICENSE +21 -0
  2. root_kg-2.0.0/PKG-INFO +384 -0
  3. root_kg-2.0.0/README.md +354 -0
  4. root_kg-2.0.0/pyproject.toml +57 -0
  5. root_kg-2.0.0/root_kg/.env.example +13 -0
  6. root_kg-2.0.0/root_kg/__init__.py +3 -0
  7. root_kg-2.0.0/root_kg/__main__.py +5 -0
  8. root_kg-2.0.0/root_kg/adapters/__init__.py +0 -0
  9. root_kg-2.0.0/root_kg/adapters/vault.py +122 -0
  10. root_kg-2.0.0/root_kg/chunker.py +53 -0
  11. root_kg-2.0.0/root_kg/cli.py +380 -0
  12. root_kg-2.0.0/root_kg/config.example.yaml +66 -0
  13. root_kg-2.0.0/root_kg/db.py +833 -0
  14. root_kg-2.0.0/root_kg/embeddings.py +39 -0
  15. root_kg-2.0.0/root_kg/extractor.py +251 -0
  16. root_kg-2.0.0/root_kg/indexer.py +450 -0
  17. root_kg-2.0.0/root_kg/llm.py +525 -0
  18. root_kg-2.0.0/root_kg/merge_cli.py +135 -0
  19. root_kg-2.0.0/root_kg/paths.py +25 -0
  20. root_kg-2.0.0/root_kg/query.py +127 -0
  21. root_kg-2.0.0/root_kg/rootd.py +78 -0
  22. root_kg-2.0.0/root_kg/server.py +619 -0
  23. root_kg-2.0.0/root_kg/tools/__init__.py +0 -0
  24. root_kg-2.0.0/root_kg/tools/correlations.py +278 -0
  25. root_kg-2.0.0/root_kg/tools/graph.py +285 -0
  26. root_kg-2.0.0/root_kg/tools/intelligence.py +206 -0
  27. root_kg-2.0.0/root_kg/tools/patterns.py +204 -0
  28. root_kg-2.0.0/root_kg/tools/search.py +110 -0
  29. root_kg-2.0.0/root_kg.egg-info/PKG-INFO +384 -0
  30. root_kg-2.0.0/root_kg.egg-info/SOURCES.txt +38 -0
  31. root_kg-2.0.0/root_kg.egg-info/dependency_links.txt +1 -0
  32. root_kg-2.0.0/root_kg.egg-info/entry_points.txt +4 -0
  33. root_kg-2.0.0/root_kg.egg-info/requires.txt +7 -0
  34. root_kg-2.0.0/root_kg.egg-info/top_level.txt +1 -0
  35. root_kg-2.0.0/setup.cfg +4 -0
  36. root_kg-2.0.0/tests/test_db.py +518 -0
  37. root_kg-2.0.0/tests/test_extractor.py +214 -0
  38. root_kg-2.0.0/tests/test_multi_root.py +118 -0
  39. root_kg-2.0.0/tests/test_paths.py +37 -0
  40. root_kg-2.0.0/tests/test_server_import.py +23 -0
root_kg-2.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shadman Rahman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
root_kg-2.0.0/PKG-INFO ADDED
@@ -0,0 +1,384 @@
1
+ Metadata-Version: 2.4
2
+ Name: root-kg
3
+ Version: 2.0.0
4
+ Summary: Personal knowledge graph with entity extraction, GraphRAG, and MCP integration. Turns your notes into searchable intelligence.
5
+ Author-email: Shadman Rahman <connectshadman@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/mshadmanrahman/root-kg
8
+ Project-URL: Repository, https://github.com/mshadmanrahman/root-kg
9
+ Project-URL: Issues, https://github.com/mshadmanrahman/root-kg/issues
10
+ Keywords: knowledge-graph,mcp,rag,obsidian,personal-knowledge,entity-extraction,semantic-search
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Classifier: Topic :: Text Processing :: Indexing
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: sentence-transformers>=3.0.0
24
+ Requires-Dist: sqlite-vec>=0.1.1
25
+ Requires-Dist: mcp<2,>=1.0.0
26
+ Requires-Dist: pyyaml>=6.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=8.0; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ <p align="center">
32
+ <a href="https://pypi.org/project/root-kg/"><img src="https://img.shields.io/pypi/v/root-kg?style=flat-square&color=blue" alt="PyPI"></a>
33
+ <img src="https://img.shields.io/badge/python-3.11+-blue?style=flat-square&logo=python&logoColor=white" alt="Python 3.11+">
34
+ <img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="MIT License">
35
+ <img src="https://img.shields.io/badge/MCP-native-purple?style=flat-square" alt="MCP Native">
36
+ <img src="https://img.shields.io/badge/LLM-Anthropic%20%7C%20OpenRouter%20%7C%20Ollama-orange?style=flat-square" alt="Multi-LLM">
37
+ <img src="https://img.shields.io/badge/storage-SQLite-lightgrey?style=flat-square&logo=sqlite" alt="SQLite">
38
+ </p>
39
+
40
+ <h1 align="center">ROOT</h1>
41
+
42
+ <p align="center">
43
+ <strong>Ask questions across all your knowledge. Get cited answers.</strong><br>
44
+ Turn your Obsidian vault, meeting notes, and emails into a queryable intelligence layer.
45
+ </p>
46
+
47
+ Read the story behind it: [My knowledge graph got fifteen stars](https://shadmanrahman.substack.com/p/my-knowledge-graph-got-fifteen-stars)
48
+
49
+ <p align="center">
50
+ <img src="assets/hero.png" alt="ROOT: your notes, indexed and queryable from Claude Code" width="800">
51
+ </p>
52
+
53
+ <p align="center">
54
+ <a href="#quick-start">Quick Start</a> &bull;
55
+ <a href="#what-you-can-ask-root">What You Can Ask</a> &bull;
56
+ <a href="#how-it-works">How It Works</a> &bull;
57
+ <a href="#the-18-mcp-tools">Tools</a> &bull;
58
+ <a href="#running-costs">Running Costs</a> &bull;
59
+ <a href="#comparison">Comparison</a>
60
+ </p>
61
+
62
+ ---
63
+
64
+ You wrote it down. You know you did. It was in a meeting note, or a chat thread you pasted into Obsidian, or a document you made before the planning session. Now you are searching and finding nothing, or finding five things that contradict each other, and you are holding the whole mental model in your head again.
65
+
66
+ ROOT connects your notes, meetings and emails into one queryable layer. You ask in plain English, the way you would ask a colleague who had read everything you ever wrote.
67
+
68
+ ---
69
+
70
+ ## Quick Start
71
+
72
+ ```bash
73
+ # Install. A venv or pipx keeps the embedding model's dependencies out of your system Python
74
+ pip install root-kg
75
+
76
+ # Answer the setup wizard: where your notes live, which LLM backend
77
+ root-kg init
78
+
79
+ # Index your notes. Embeddings run locally and cost nothing
80
+ root-index
81
+ # Extract entities into the graph. This step calls an LLM
82
+ root-index --extract
83
+
84
+ # Register ROOT with Claude Code, using the absolute path to the server
85
+ claude mcp add root "$(which root-server)"
86
+ ```
87
+
88
+ `root-kg init` writes `config.yaml`, `data/` and `logs/` to `~/.root-kg`. Set `ROOT_KG_HOME` to put them somewhere else. A git clone keeps them in the repo root instead, see [Development](#development).
89
+
90
+ Indexing prints what it touched, so you can see the incremental behaviour on the second run:
91
+
92
+ ```
93
+ ROOT indexer started at 2026-09-11T08:30:04Z
94
+ Scanning vault: ~/Documents/My Vault
95
+ Embedding 23 notes (9 new, 14 updated)...
96
+ Embedded 118/118 chunks...
97
+ Done. 9 new, 14 updated, 412 unchanged, 3 removed, 0 errors.
98
+ Extraction complete: 23 processed, 147 entities, 96 relations, 0 errors
99
+ ```
100
+
101
+ Now ask ROOT something from Claude Code: `root_search("your topic")`, `root_ask("your question")`, `root_graph("person name", 2)`. Two more worth knowing: `root-kg stats` prints what the index holds, and `root-index --extract-only --limit 10` tries extraction on ten notes before you pay for the rest.
102
+
103
+ > ROOT talks to Claude Code as an MCP server. If Claude Code is new to you, [claudecodeguide.dev](https://claudecodeguide.dev) gets you set up in under an hour.
104
+
105
+ ## What You Can Ask ROOT
106
+
107
+ The questions that otherwise mean opening six tabs and rebuilding the story from memory:
108
+
109
+ - "What did I commit to Maya last week?"
110
+ - "How did the pricing decision evolve over the last month?"
111
+ - "Who has been working on Project X, and through what?"
112
+ - "What action items from last quarter are still open?"
113
+ - "Brief me on this person before my 1:1, everything we have discussed."
114
+
115
+ You ask in plain English. ROOT synthesizes an answer and cites the notes it came from:
116
+
117
+ ```
118
+ > root_ask("What decisions were made about the Search Redesign?")
119
+
120
+ # ROOT Answer
121
+
122
+ Leadership APPROVED the Search Redesign project on March 17, 2026.
123
+ Scope was locked at the kick-off meeting on March 23: consolidate
124
+ 1,200 product categories down to 85 across 12 groups, following the
125
+ industry taxonomy. Owner: Alex Chen. Sprint start: April 7.
126
+
127
+ *Based on 5 search results and 2 entity matches.*
128
+ ```
129
+
130
+ That answer came from five notes and two separate meetings. ROOT also knows that "Alex" in the kick-off note and "Alex Chen" in the planning doc are the same person, so you can walk the connections without opening a file.
131
+
132
+ ## How It Works
133
+
134
+ <p align="center">
135
+ <img src="assets/architecture.svg" alt="ROOT architecture: ingest, embed, extract, query" width="800">
136
+ </p>
137
+
138
+ Four steps, in order.
139
+
140
+ **Ingest.** The vault adapter scans your markdown and hashes each file with SHA-256. Meetings, emails and anything else arrive through `root_ingest`, which takes content from any other MCP server.
141
+
142
+ **Embed.** Notes are split at heading boundaries and embedded with `all-MiniLM-L6-v2`, a local model that runs on your CPU. Nothing leaves the machine and nothing is billed. Vectors go into SQLite through sqlite-vec.
143
+
144
+ **Extract.** For each new or changed note, an LLM pulls out entities (person, project, decision, event, concept, organization, tool, document, skill) and typed relations (`works_with`, `owns`, `decided`, `attended`, `discussed`, `blocked_by`, `depends_on`, `manages`, `created`, `reviewed`). Each relation carries a confidence score: 0.9 and above for explicit statements, 0.7 for implied, 0.5 for weak signals. Aliases are captured too, so "Fredrik" and "Frederick" resolve to one entity.
145
+
146
+ **Query.** `root_ask` runs semantic search for the most relevant chunks, pulls the graph neighborhood of the entities it finds, and hands both to the synthesis model for a cited answer. Graph traversal is a breadth-first walk in Python over indexed SQLite reads, with a visited set for cycle detection.
147
+
148
+ ### The two-model split
149
+
150
+ Extraction runs on every changed note, so it uses Haiku (`llm.extraction_model` in `config.yaml`). Synthesis only runs when you ask a question, so it uses Sonnet (`llm.synthesis_model`). Embeddings never call an API at all.
151
+
152
+ ### What is in the repo
153
+
154
+ ```
155
+ root-kg/
156
+ ├── root_kg/
157
+ │ ├── server.py # MCP server: exposes the 18 tools over stdio
158
+ │ ├── indexer.py # Reads configured sources, embeds them, orchestrates extraction
159
+ │ ├── extractor.py # Incremental, hash-tracked entity and relation extraction
160
+ │ ├── db.py # SQLite + sqlite-vec storage and entity graph with BFS traversal
161
+ │ ├── llm.py # Multi-backend LLM client for extraction and synthesis
162
+ │ ├── embeddings.py # Local embeddings via sentence-transformers, zero API cost
163
+ │ ├── chunker.py # Splits long notes at heading boundaries
164
+ │ ├── cli.py # Setup wizard, stats, and cron-callable search and note ingest
165
+ │ ├── paths.py # Where config, data and logs live: ROOT_KG_HOME, the checkout, or ~/.root-kg
166
+ │ ├── config.example.yaml # Template that root-kg init copies into config.yaml
167
+ │ ├── .env.example # Template that root-kg init copies into .env
168
+ │ ├── query.py # Calls any ROOT tool from a shell or another agent
169
+ │ ├── rootd.py # Warm daemon: keeps DB and embedder loaded for fast local search
170
+ │ ├── merge_cli.py # Folds duplicate entity shards into one canonical entity
171
+ │ ├── adapters/vault.py # Markdown scanner for the vault and any extra root
172
+ │ └── tools/ # search, patterns, correlations, graph, intelligence
173
+ ├── tests/ # 70 tests over the DB, extractor and multi-root paths
174
+ ├── templates/root-instructions.md # Drop-in usage instructions for an agent
175
+ ├── .github/workflows/ # tests.yml runs pytest on every PR, release.yml publishes to PyPI on a v* tag
176
+ └── run-indexer.sh # Wrapper that activates the venv and runs an incremental pass
177
+ ```
178
+
179
+ Design principles worth knowing before you read the code: one SQLite file and no other server, no vendor SDK for LLM calls (stdlib `urllib` only), incremental everything through content hashing, and per-root safety guards so an unmounted drive skips its stale sweep instead of purging your index.
180
+
181
+ ## The 18 MCP Tools
182
+
183
+ | Tool | What it does |
184
+ |------|--------------|
185
+ | **Search and discovery** | |
186
+ | `root_search(query)` | Semantic search across all indexed knowledge |
187
+ | `root_search_folder(query, folder)` | Semantic search scoped to one vault folder |
188
+ | `root_note(path)` | Read the full content of a note by path |
189
+ | `root_connections(path)` | Notes that are related but live in a different folder |
190
+ | `root_themes(scope)` | Recurring themes, found by clustering similar notes |
191
+ | `root_gaps(topic)` | What is mentioned but never explored, and which domains are absent |
192
+ | `root_stats()` | Notes, chunks, sources, top folders, last indexed time |
193
+ | **Multi-source intelligence** | |
194
+ | `root_ingest(source_type, title, content, path)` | Ingest a meeting, email or message from another MCP |
195
+ | `root_ingest_batch(items)` | Ingest many items in one call |
196
+ | `root_about(person)` | Everything ROOT knows about a person across all sources |
197
+ | `root_open_loops(scope)` | Things discussed or promised but never followed up |
198
+ | `root_project_pulse(project)` | Recent activity for a project across every source |
199
+ | **Entity graph and GraphRAG** | |
200
+ | `root_graph(entity, depth)` | An entity's neighborhood up to N hops |
201
+ | `root_influence_map(project)` | Who influenced a project, through which actions |
202
+ | `root_decision_trail(topic)` | How decisions around a topic evolved over time |
203
+ | `root_blind_spots()` | Entities gone quiet: 30+ days inactive after 3+ mentions |
204
+ | `root_ask(question)` | Free-form Q&A over search, graph and synthesis |
205
+ | `root_weekly_digest()` | New entities, new relations, and activity by source |
206
+
207
+ ## Indexing More Than One Folder
208
+
209
+ Most people start with one vault. Your knowledge usually is not in one place: there is the vault, and then there is the project docs folder, and the notes your coding agent writes, and the memory store some tool keeps in a dotfile directory.
210
+
211
+ Add them as roots:
212
+
213
+ ```yaml
214
+ vault:
215
+ path: "~/Documents/My Vault"
216
+
217
+ roots:
218
+ - name: project-docs
219
+ path: "~/code/myproject/docs"
220
+ extract: true
221
+ - name: agent-memory
222
+ path: "~/.config/agent/memory"
223
+ extract: false
224
+ ```
225
+
226
+ Each root is indexed separately and keeps its own identity. `root_stats` breaks the counts out per root, `root_search` can filter to one of them, and each root's stale sweep only ever removes its own notes. If a root goes missing, say an external drive is unmounted, that root is skipped with a warning and its notes are left alone.
227
+
228
+ `extract` is the part worth understanding, because it is where the money is.
229
+
230
+ Indexing is free. It chunks your notes and embeds them with a local MiniLM model on your CPU, so a root becomes semantically searchable at zero cost. Entity extraction is the step that calls an LLM once per note to pull out people, projects and relationships for the graph.
231
+
232
+ Those are now separate decisions. `extract: false` gives you a root you can search but never pay for. Point a large, repetitive, low-entity corpus at it and your bill does not move. In my own setup a folder of agent memory files went in that way. Every file is searchable, and the extraction queue barely moved.
233
+
234
+ One detail if you already have an index: note paths are unique, and two folders can both contain `index.md`, so each extra root namespaces its paths with a prefix (its name, by default). The main `vault.path` keeps no prefix, which means adding roots never re-embeds the vault you already indexed.
235
+
236
+ ## Running Costs
237
+
238
+ Setup costs a few dollars. After that you pay for changed notes and for questions you actually ask.
239
+
240
+ | Activity | When it runs | Cost |
241
+ |----------|--------------|------|
242
+ | Indexing and embedding | Every run | $0, local model on CPU |
243
+ | Entity extraction | New and changed notes only | about $0.003 per note on Haiku |
244
+ | `root_ask` | On demand | about $0.01 per query on Sonnet |
245
+ | Scheduled incremental pass | Every 2 hours | $0.01 to $0.05 per day |
246
+ | A root with `extract: false` | Every run | $0, searchable, never sent to an LLM |
247
+
248
+ Those are ranges measured on my corpus with the Anthropic backend. Mine settles between $1 and $3 a month. Yours will land somewhere else, depending on how long your notes are and how many of them change each day. On Ollama the LLM column goes to zero and the quality drops with it.
249
+
250
+ ## LLM Backends
251
+
252
+ Three backends handle entity extraction and Q&A synthesis:
253
+
254
+ | Backend | Cost | Quality | Setup |
255
+ |---------|------|---------|-------|
256
+ | **Anthropic** (default) | about $0.003 per note | Best | `ANTHROPIC_API_KEY` in `.env` |
257
+ | **OpenRouter** | Free $1 credit to start | Good | `OPENROUTER_API_KEY` in `.env` |
258
+ | **Ollama** | Free, runs locally | Lower | `ollama pull llama3.1` |
259
+
260
+ Set it in `config.yaml`:
261
+
262
+ ```yaml
263
+ llm:
264
+ backend: "anthropic" # or "openrouter" or "ollama"
265
+ ```
266
+
267
+ ## Keeping the index fresh
268
+
269
+ Run `root-index --extract` on a schedule and ROOT stays current. Only changed notes are reprocessed, so a typical incremental run finishes in well under a minute.
270
+
271
+ Cron and launchd both need the absolute path to `root-index`, which `which root-index` prints. Cron is the simpler option on macOS. A launchd agent cannot read `~/Documents` or iCloud paths without a Full Disk Access grant, and cron sidesteps that:
272
+
273
+ ```bash
274
+ crontab -e
275
+
276
+ # Runs at :30, every two hours during the day
277
+ 30 8,10,12,14,16,18,20,22 * * * ANTHROPIC_API_KEY=your-key /path/to/root-index --extract >> ~/Library/Logs/root-indexer.log 2>&1
278
+ ```
279
+
280
+ If your vault sits outside those protected folders, launchd works and survives reboots. Save this as `~/Library/LaunchAgents/com.root-kg.refresh.plist`:
281
+
282
+ ```xml
283
+ <?xml version="1.0" encoding="UTF-8"?>
284
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
285
+ <plist version="1.0">
286
+ <dict>
287
+ <key>Label</key><string>com.root-kg.refresh</string>
288
+ <key>ProgramArguments</key>
289
+ <array>
290
+ <string>/path/to/root-index</string>
291
+ <string>--extract</string>
292
+ </array>
293
+ <key>StartInterval</key><integer>7200</integer>
294
+ </dict>
295
+ </plist>
296
+ ```
297
+
298
+ Load it with `launchctl load ~/Library/LaunchAgents/com.root-kg.refresh.plist`. On Linux the equivalent is a systemd timer calling the same command, and a contribution there is welcome. One scheduling tip: if a meeting-notes tool syncs into your vault, offset ROOT by half an hour so the fresh notes are on disk before ROOT reads them.
299
+
300
+ ## Comparison
301
+
302
+ | Feature | ROOT | Obsidian Graph | Mem.ai | Khoj | Rewind |
303
+ |---------|------|----------------|--------|------|--------|
304
+ | Entity extraction | LLM-powered | None | None | None | None |
305
+ | Typed relations | Yes, 10 types | Backlinks only | No | No | No |
306
+ | GraphRAG | Yes | No | Basic RAG | Basic RAG | No |
307
+ | Multi-source | Notes, meetings, email | Notes only | Yes | Notes only | Everything |
308
+ | MCP native | Yes | No | No | No | No |
309
+ | Self-hosted | Yes | Yes | No | Yes | No |
310
+ | Single file DB | Yes | N/A | Cloud | Postgres | Cloud |
311
+ | Free embeddings | Yes, local | N/A | No | Yes | No |
312
+
313
+ The ROOT column is checked against this repo. The other columns come from each product's public description, not from code I have read.
314
+
315
+ ## Requirements
316
+
317
+ - Python 3.11 or newer
318
+ - Disk space for the local embedding model and its dependencies, downloaded on first run
319
+ - One of: an Anthropic API key, an OpenRouter key (free $1 credit), or Ollama running locally
320
+ - Four runtime dependencies: `sentence-transformers`, `sqlite-vec`, `mcp`, `pyyaml`. No Postgres, no Neo4j, no Docker
321
+
322
+ ## Development
323
+
324
+ Clone, install in editable mode with the dev extra, and run the suite from the repo root:
325
+
326
+ ```bash
327
+ git clone https://github.com/mshadmanrahman/root-kg.git
328
+ cd root-kg
329
+ python -m venv .venv && source .venv/bin/activate
330
+ pip install -e ".[dev]"
331
+ pytest
332
+ ```
333
+
334
+ 70 tests pass, covering the database layer, the extraction pipeline, the multi-root indexing paths and where config lives. In a checkout, `root-kg init` writes `config.yaml`, `data/` and `logs/` next to `pyproject.toml` rather than to `~/.root-kg`.
335
+
336
+ Releases go out from a tag: bump the version in `pyproject.toml` and `root_kg/__init__.py`, merge, then `git tag vX.Y.Z && git push origin vX.Y.Z`. The `release` workflow builds the sdist and wheel, checks the tag against the version, and publishes through PyPI trusted publishing.
337
+
338
+ ## What went wrong and what I learned
339
+
340
+ `clear_extraction_for_note()` had a docstring saying it removed all entities and relations sourced from a note. It deleted relations, note links, and the extraction record. It never deleted a single entity row.
341
+
342
+ Extraction runs through an LLM, so it is nondeterministic. Re-index a note and the fresh pass returns a slightly different entity set. Everything the old pass found and the new one missed stayed in the database forever, holding no relation and no note link. A few per note, every run, compounding.
343
+
344
+ When I finally counted, 11,172 of 21,696 entities were unreachable. That is 51% of the graph. Search could not return them, traversal could not reach them, and `root_stats()` counted every one as real. That last part is why it took months to notice. The graph was reporting roughly double its true size and I believed it.
345
+
346
+ The obvious fix was wrong. I swept entities holding a note link to the cleared note, and a three-note re-extraction still leaked two orphans. `_extract_note()` resolves a relation's endpoints through `resolve_entity()`, which matches an entity that already exists elsewhere and does not link it to the current note. That entity is held up by the relation alone. Both sets have to go: entities linked to the note, and both endpoints of every relation sourced from it.
347
+
348
+ Fixing that exposed a second bug that had been unreachable until then. `entity_aliases` declares `ON DELETE CASCADE`, but SQLite ignores foreign keys unless the connection sets `PRAGMA foreign_keys = ON`, and this code never did. Deleted entities left their alias rows behind. `alias` is `UNIQUE`, so the dead row squats the name: `add_alias()` for a new entity hits `INSERT OR IGNORE`, does nothing, and that alias resolves to `None` permanently. It could not happen before, because nothing was ever deleted. The fix created the conditions for it.
349
+
350
+ Last one, and it is the least technical. I fixed this in the copy I run on 2026-08-07 and did not push it here until 2026-08-10. Anyone who cloned in between got the orphan factory. A fix that only exists in the copy you run is not a fix.
351
+
352
+ ## Contributing
353
+
354
+ PRs welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md) first. The codebase stays deliberately plain: Python 3.11+, no frameworks, small files.
355
+
356
+ Areas where help goes furthest:
357
+
358
+ - **Adapters**: LogSeq, Notion, Apple Notes, Google Docs
359
+ - **Backends**: Google Gemini, local models via llama.cpp
360
+ - **Visualization**: a web UI for exploring the entity graph
361
+ - **Platforms**: a systemd timer for Linux, Task Scheduler for Windows
362
+
363
+ ## See also
364
+
365
+ Other things I have built for the same workflow:
366
+
367
+ - [pm-pilot](https://github.com/mshadmanrahman/pm-pilot): Claude Code configured for PMs. Meeting prep, PRDs, market sizing, 25 skills.
368
+ - [morning-digest](https://github.com/mshadmanrahman/morning-digest): morning briefing automation. Calendar, email and news in one digest.
369
+ - [discovery-md](https://github.com/mshadmanrahman/discovery-md): AI product discovery for PMs.
370
+ - [ceremonies](https://github.com/mshadmanrahman/ceremonies): agile ceremonies that do not waste the hour.
371
+ - [claudecode-guide](https://github.com/mshadmanrahman/claudecode-guide): a friendly guide to Claude Code, also at [claudecodeguide.dev](https://claudecodeguide.dev).
372
+ - [riff](https://github.com/mshadmanrahman/riff): LinkedIn engagement assistant. Extracts posts and comments for AI-drafted replies.
373
+
374
+ ## License
375
+
376
+ MIT. See [LICENSE](LICENSE).
377
+
378
+ ## Support
379
+
380
+ Questions and bugs go to [GitHub Issues](https://github.com/mshadmanrahman/root-kg/issues); ideas and setups go to [Discussions](https://github.com/mshadmanrahman/root-kg/discussions).
381
+
382
+ <p align="center">
383
+ Built by <a href="https://github.com/mshadmanrahman">Shadman Rahman</a>
384
+ </p>