sqlite-graph-memory 0.3.2__tar.gz → 0.3.3__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.
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/CHANGELOG.md +22 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/PKG-INFO +13 -3
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/README.md +12 -2
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/bench/README.md +1 -1
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/eval/build_gold.py +6 -3
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/examples/README.md +1 -1
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/pyproject.toml +1 -1
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/src/sqlite_graph_memory/__init__.py +1 -1
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/src/sqlite_graph_memory/brain_ask.py +3 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/src/sqlite_graph_memory/index_notes.py +6 -6
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/src/sqlite_graph_memory/mcp_server.py +0 -1
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/tests/test_eval.py +19 -0
- sqlite_graph_memory-0.3.3/tests/test_feedback_link.py +29 -0
- sqlite_graph_memory-0.3.3/tests/test_index_notes_dotfiles.py +81 -0
- sqlite_graph_memory-0.3.3/tests/test_mcp_script_mode.py +59 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/.gitignore +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/CITATION.cff +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/LICENSE +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/bench/requirements.txt +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/docs/RELEASING.md +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/docs/bitemporal.md +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/eval/run_eval.py +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/examples/claude-code-stop-hook.json +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/examples/mcp-config.json +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/requirements.txt +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/src/sqlite_graph_memory/ppr.py +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/src/sqlite_graph_memory/schema.sql +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/src/sqlite_graph_memory/turnstate_hook.py +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/src/sqlite_graph_memory/turnstate_show.py +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/src/sqlite_graph_memory/vault_stats.py +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/tests/test_bench.py +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/tests/test_brain_ask.py +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/tests/test_index_notes.py +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/tests/test_llms_txt.py +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/tests/test_mcp_server.py +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/tests/test_optional_extra.py +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/tests/test_ppr.py +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/tests/test_star_prompt.py +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/tests/test_turnstate_hook.py +0 -0
- {sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/tests/test_vault_stats.py +0 -0
|
@@ -4,6 +4,28 @@ What shipped, in plain words. Entries below v0.1.3 were written on 2026-09-05 fr
|
|
|
4
4
|
release notes that already existed — the file itself did not exist until then, so those three are
|
|
5
5
|
recorded after the fact rather than backdated to look contemporaneous.
|
|
6
6
|
|
|
7
|
+
## v0.3.3 — 2026-10-26
|
|
8
|
+
|
|
9
|
+
**Hidden note files are skipped like hidden folders.** A note whose filename starts with a
|
|
10
|
+
dot, such as `.ideas.md` or `.draft.md`, used to be indexed when it sat in the vault root or a
|
|
11
|
+
visible folder, while the same file inside `.drafts/` was skipped. Now both are skipped, so
|
|
12
|
+
after re-indexing those notes stop showing up in answers. `BRAIN_INDEX_HIDDEN=1` brings back
|
|
13
|
+
the old behaviour and indexes hidden files and folders alike. `eval/build_gold.py` now walks the
|
|
14
|
+
vault with the indexer's own filter, so it follows the same rule and the same variable instead
|
|
15
|
+
of keeping its own copy (issue #25).
|
|
16
|
+
|
|
17
|
+
**The MCP server works from a source checkout and finds your index where you launch it**
|
|
18
|
+
(issue #24, reported by @Kaap10). `sgm-mcp` used to run the recall child with its working
|
|
19
|
+
directory set to the package folder, so a relative `BRAIN_INDEX_DIR=index` resolved inside
|
|
20
|
+
site-packages and your own index was never found; from a plain clone without `pip install -e`,
|
|
21
|
+
`python src/sqlite_graph_memory/mcp_server.py --test` failed with ModuleNotFoundError. Recall now
|
|
22
|
+
runs in the server's own working directory, and CI reproduces the user's launch: installed wheel,
|
|
23
|
+
relative index, started from another directory.
|
|
24
|
+
|
|
25
|
+
**A three-question feedback form.** "What were you trying to do, what broke, which machine": an
|
|
26
|
+
issue template, a README section, and a link printed next to the missing-index error, because
|
|
27
|
+
this project collects no telemetry and that form is how we learn what is missing.
|
|
28
|
+
|
|
7
29
|
## v0.3.2 — 2026-10-24
|
|
8
30
|
|
|
9
31
|
**The PageRank lane the README credits is now in the package** (#51). `ppr.py` runs
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sqlite-graph-memory
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Associative memory for LLM agents: Graph RAG over a folder of markdown notes, in one SQLite file, with no server to run.
|
|
5
5
|
Project-URL: Homepage, https://github.com/tonydzi/sqlite-graph-memory
|
|
6
6
|
Project-URL: Repository, https://github.com/tonydzi/sqlite-graph-memory
|
|
@@ -126,7 +126,7 @@ points, the `[[wikilinks]]` you already wrote by hand are the graph, and a cross
|
|
|
126
126
|
reranks what the hop dragged in.
|
|
127
127
|
SQLite is the only database ([`schema.sql`](src/sqlite_graph_memory/schema.sql)).
|
|
128
128
|
|
|
129
|
-
Status: **pilot**, and the word is load-bearing: it runs daily in one real setup (a ~
|
|
129
|
+
Status: **pilot**, and the word is load-bearing: it runs daily in one real setup (an index of ~28k curated notes, ~93k chunks, rebuilt nightly from a ~270k-file
|
|
130
130
|
Obsidian vault driven by Claude Code), but it is deliberately minimal and makes no attempt
|
|
131
131
|
to be general — see [What's intentionally missing](#whats-intentionally-missing), which is
|
|
132
132
|
where the uneven test coverage is described.
|
|
@@ -252,7 +252,7 @@ present, and CPU works fine for small corpora.
|
|
|
252
252
|
|
|
253
253
|
### What `--ab` looks like
|
|
254
254
|
|
|
255
|
-
Output of `brain_ask.py` with `--ab`, on a toy 4-note corpus (real runs use
|
|
255
|
+
Output of `brain_ask.py` with `--ab`, on a toy 4-note corpus (real runs use an index of ~28k notes / ~93k chunks):
|
|
256
256
|
|
|
257
257
|
```
|
|
258
258
|
A/B RECALL: how should agent memory work
|
|
@@ -491,6 +491,16 @@ Questions, war stories, or you want to run this on your own fleet:
|
|
|
491
491
|
- 🌐 [palo-alto.ai](https://palo-alto.ai) · [Palo Alto AI Research Lab](https://github.com/tonydzi)
|
|
492
492
|
- 🧪 **Engineers: want to test-drive this setup?** Message me — I hand out free starter seeds to engineers who test and report back.
|
|
493
493
|
|
|
494
|
+
## Tell us what is missing
|
|
495
|
+
|
|
496
|
+
You tried this on your own notes and something was missing or broke. That is the most useful
|
|
497
|
+
thing anyone can tell us.
|
|
498
|
+
|
|
499
|
+
- **You started and it did not work:** [open the 3-question form](https://github.com/tonydzi/sqlite-graph-memory/issues/new?template=feature_request.yml) (what you tried, what broke, which machine).
|
|
500
|
+
- **Not sure it is a bug, or just an idea:** [Discussions → Ideas](https://github.com/tonydzi/sqlite-graph-memory/discussions/categories/ideas).
|
|
501
|
+
|
|
502
|
+
Nothing in this package phones home. There is no telemetry, so we only know what you write to us.
|
|
503
|
+
|
|
494
504
|
## Contributors welcome — and there is a queue
|
|
495
505
|
|
|
496
506
|
Issues labelled [`accepted`](https://github.com/tonydzi/sqlite-graph-memory/issues?q=is%3Aissue+is%3Aopen+label%3Aaccepted)
|
|
@@ -75,7 +75,7 @@ points, the `[[wikilinks]]` you already wrote by hand are the graph, and a cross
|
|
|
75
75
|
reranks what the hop dragged in.
|
|
76
76
|
SQLite is the only database ([`schema.sql`](src/sqlite_graph_memory/schema.sql)).
|
|
77
77
|
|
|
78
|
-
Status: **pilot**, and the word is load-bearing: it runs daily in one real setup (a ~
|
|
78
|
+
Status: **pilot**, and the word is load-bearing: it runs daily in one real setup (an index of ~28k curated notes, ~93k chunks, rebuilt nightly from a ~270k-file
|
|
79
79
|
Obsidian vault driven by Claude Code), but it is deliberately minimal and makes no attempt
|
|
80
80
|
to be general — see [What's intentionally missing](#whats-intentionally-missing), which is
|
|
81
81
|
where the uneven test coverage is described.
|
|
@@ -201,7 +201,7 @@ present, and CPU works fine for small corpora.
|
|
|
201
201
|
|
|
202
202
|
### What `--ab` looks like
|
|
203
203
|
|
|
204
|
-
Output of `brain_ask.py` with `--ab`, on a toy 4-note corpus (real runs use
|
|
204
|
+
Output of `brain_ask.py` with `--ab`, on a toy 4-note corpus (real runs use an index of ~28k notes / ~93k chunks):
|
|
205
205
|
|
|
206
206
|
```
|
|
207
207
|
A/B RECALL: how should agent memory work
|
|
@@ -440,6 +440,16 @@ Questions, war stories, or you want to run this on your own fleet:
|
|
|
440
440
|
- 🌐 [palo-alto.ai](https://palo-alto.ai) · [Palo Alto AI Research Lab](https://github.com/tonydzi)
|
|
441
441
|
- 🧪 **Engineers: want to test-drive this setup?** Message me — I hand out free starter seeds to engineers who test and report back.
|
|
442
442
|
|
|
443
|
+
## Tell us what is missing
|
|
444
|
+
|
|
445
|
+
You tried this on your own notes and something was missing or broke. That is the most useful
|
|
446
|
+
thing anyone can tell us.
|
|
447
|
+
|
|
448
|
+
- **You started and it did not work:** [open the 3-question form](https://github.com/tonydzi/sqlite-graph-memory/issues/new?template=feature_request.yml) (what you tried, what broke, which machine).
|
|
449
|
+
- **Not sure it is a bug, or just an idea:** [Discussions → Ideas](https://github.com/tonydzi/sqlite-graph-memory/discussions/categories/ideas).
|
|
450
|
+
|
|
451
|
+
Nothing in this package phones home. There is no telemetry, so we only know what you write to us.
|
|
452
|
+
|
|
443
453
|
## Contributors welcome — and there is a queue
|
|
444
454
|
|
|
445
455
|
Issues labelled [`accepted`](https://github.com/tonydzi/sqlite-graph-memory/issues?q=is%3Aissue+is%3Aopen+label%3Aaccepted)
|
|
@@ -140,7 +140,7 @@ has seen.
|
|
|
140
140
|
|
|
141
141
|
## Limits, stated up front
|
|
142
142
|
|
|
143
|
-
- 202 notes is small. Dense retrieval at this size is easier than in
|
|
143
|
+
- 202 notes is small. Dense retrieval at this size is easier than in the ~28k-note production index,
|
|
144
144
|
so the bench measures failure modes, and it does not show how a system scales.
|
|
145
145
|
- The text is English only and comes from templates. The sentences are cleaner than real
|
|
146
146
|
notes, and that flatters extractive systems.
|
|
@@ -40,6 +40,9 @@ import sys
|
|
|
40
40
|
from collections import Counter
|
|
41
41
|
from pathlib import Path
|
|
42
42
|
|
|
43
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'src'))
|
|
44
|
+
from sqlite_graph_memory.index_notes import iter_notes # noqa: E402
|
|
45
|
+
|
|
43
46
|
WIKILINK_RX = re.compile(r'\[\[([^\]\|#]+)(?:[|#][^\]]*)?\]\]')
|
|
44
47
|
CYRILLIC_RX = re.compile(r'[а-яё]', re.I)
|
|
45
48
|
|
|
@@ -121,10 +124,10 @@ def read_notes(notes_dir, stems, folders=None):
|
|
|
121
124
|
"""Every indexed note, parsed into the fields the four classes need."""
|
|
122
125
|
root = Path(notes_dir)
|
|
123
126
|
paths = []
|
|
124
|
-
|
|
127
|
+
# The indexer's own walk: hidden files and dirs, conflict copies and BRAIN_INDEX_HIDDEN
|
|
128
|
+
# are decided in one place, so a gold answer is never a note the indexer would skip.
|
|
129
|
+
for p in iter_notes(root):
|
|
125
130
|
rel = p.relative_to(root)
|
|
126
|
-
if any(part.startswith('.') for part in rel.parts[:-1]): continue
|
|
127
|
-
if 'sync-conflict' in p.name: continue
|
|
128
131
|
if folders and (not rel.parts[:-1] or rel.parts[0] not in folders): continue
|
|
129
132
|
if p.stem.lower() not in stems: continue
|
|
130
133
|
paths.append(p)
|
|
@@ -109,7 +109,7 @@ All components share standard configuration variables:
|
|
|
109
109
|
| `BRAIN_INDEX_DIR` | `./index` | Directory where `index_notes.py` writes and `brain_ask.py` reads `_brain_e5.npy` and metadata. |
|
|
110
110
|
| `TURNSTATE_DB` | `./turnstate.db` | SQLite database holding per-turn ledger (`turns`) and A/B telemetry (`ab_recall`). |
|
|
111
111
|
| `BRAIN_ANSWER_OUT` | `./_brain_answer.txt` | File where the formatted context bundle or answer is mirrored. |
|
|
112
|
-
| `BRAIN_INDEX_HIDDEN` | `0` | If `1`, indexes hidden dot-directories too (by default skipped). |
|
|
112
|
+
| `BRAIN_INDEX_HIDDEN` | `0` | If `1`, indexes hidden dot-files (`.draft.md`) and dot-directories too (by default both are skipped). |
|
|
113
113
|
| `BRAIN_AB_JSON` | `None` | If set, `--ab` mode writes a structured JSON diff to this file path. |
|
|
114
114
|
| `AB_SOURCE` | `work` | Telemetry label written to `ab_recall.source` (`work`, `eval`, etc.). |
|
|
115
115
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sqlite-graph-memory"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.3"
|
|
8
8
|
description = "Associative memory for LLM agents: Graph RAG over a folder of markdown notes, in one SQLite file, with no server to run."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
{sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/src/sqlite_graph_memory/brain_ask.py
RENAMED
|
@@ -31,6 +31,7 @@ for _s in (sys.stdout, sys.stderr):
|
|
|
31
31
|
try: _s.reconfigure(encoding='utf-8')
|
|
32
32
|
except Exception: pass
|
|
33
33
|
|
|
34
|
+
FEEDBACK_URL = 'https://github.com/tonydzi/sqlite-graph-memory/discussions/categories/ideas'
|
|
34
35
|
INDEX_DIR = Path(os.getenv('BRAIN_INDEX_DIR', 'index'))
|
|
35
36
|
AB_DB = Path(os.getenv('TURNSTATE_DB', 'turnstate.db'))
|
|
36
37
|
EMB = INDEX_DIR / '_brain_e5.npy'
|
|
@@ -299,6 +300,8 @@ def main():
|
|
|
299
300
|
query = ' '.join(a for a in args if a not in flags)
|
|
300
301
|
if not query or not EMB.exists():
|
|
301
302
|
sys.stderr.write('need an embedding index (%s) and a query — run index_notes.py first\n' % EMB)
|
|
303
|
+
if query and not EMB.exists():
|
|
304
|
+
print('Stuck? Tell us what you were trying to do: %s' % FEEDBACK_URL, file=sys.stderr)
|
|
302
305
|
raise SystemExit(1)
|
|
303
306
|
|
|
304
307
|
dev = pick_device()
|
{sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/src/sqlite_graph_memory/index_notes.py
RENAMED
|
@@ -12,9 +12,9 @@ USAGE:
|
|
|
12
12
|
python index_notes.py <notes_dir>
|
|
13
13
|
Config (env, optional):
|
|
14
14
|
BRAIN_INDEX_DIR output dir (default: ./index)
|
|
15
|
-
BRAIN_INDEX_HIDDEN 1 = also index dot-directories (default:
|
|
16
|
-
so .stversions/.obsidian/.git and
|
|
17
|
-
copies never enter the index)
|
|
15
|
+
BRAIN_INDEX_HIDDEN 1 = also index dot-files and dot-directories (default:
|
|
16
|
+
skip them, so .draft.md, .stversions/.obsidian/.git and
|
|
17
|
+
Syncthing conflict copies never enter the index)
|
|
18
18
|
"""
|
|
19
19
|
import os, re, sys, pickle
|
|
20
20
|
from pathlib import Path
|
|
@@ -77,15 +77,15 @@ CONFLICT_MARKER = 'sync-conflict'
|
|
|
77
77
|
def iter_notes(notes_dir):
|
|
78
78
|
"""Files that count as a note under ``notes_dir``.
|
|
79
79
|
|
|
80
|
-
Skips dot-directories and Syncthing conflict copies.
|
|
81
|
-
to index hidden
|
|
80
|
+
Skips dot-directories, dot-files (``.draft.md``) and Syncthing conflict copies.
|
|
81
|
+
Set ``BRAIN_INDEX_HIDDEN=1`` to index hidden paths too — someone will legitimately keep notes in one.
|
|
82
82
|
"""
|
|
83
83
|
root = Path(notes_dir)
|
|
84
84
|
include_hidden = os.getenv('BRAIN_INDEX_HIDDEN', '') not in ('', '0', 'false', 'no')
|
|
85
85
|
out = []
|
|
86
86
|
for p in root.rglob('*.md'):
|
|
87
87
|
rel = p.relative_to(root)
|
|
88
|
-
if not include_hidden and any(part.startswith('.') for part in rel.parts
|
|
88
|
+
if not include_hidden and any(part.startswith('.') for part in rel.parts):
|
|
89
89
|
continue
|
|
90
90
|
if CONFLICT_MARKER in p.name:
|
|
91
91
|
continue
|
{sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/src/sqlite_graph_memory/mcp_server.py
RENAMED
|
@@ -131,7 +131,6 @@ def run_recall(query: str, mode: str = "associative") -> tuple[str, bool]:
|
|
|
131
131
|
call_env["PYTHONPATH"] = f"{src_dir}{os.pathsep}{existing_pp}" if existing_pp else src_dir
|
|
132
132
|
res = subprocess.run(
|
|
133
133
|
cmd,
|
|
134
|
-
cwd=str(ROOT_DIR),
|
|
135
134
|
env=call_env,
|
|
136
135
|
capture_output=True,
|
|
137
136
|
text=True,
|
|
@@ -158,6 +158,25 @@ def test_a_bridge_question_records_how_many_links_were_cut(tmp_path):
|
|
|
158
158
|
assert bridge['gold_truncated'] == 4
|
|
159
159
|
|
|
160
160
|
|
|
161
|
+
def test_gold_skips_hidden_notes_exactly_like_the_indexer(tmp_path, monkeypatch):
|
|
162
|
+
"""build_gold walks the vault with the indexer's own filter (issue #25).
|
|
163
|
+
|
|
164
|
+
A root-level `.draft.md` is never indexed, so it must never become a question either,
|
|
165
|
+
even when a stale index still lists its stem. `BRAIN_INDEX_HIDDEN=1` lets it into the
|
|
166
|
+
index, so it must let it into the gold set too: one rule, one place.
|
|
167
|
+
"""
|
|
168
|
+
vault = _vault(tmp_path)
|
|
169
|
+
(vault / '.draft.md').write_text('---\ntitle: "Hidden draft"\n---\n\n' + PARA + '\n',
|
|
170
|
+
encoding='utf-8')
|
|
171
|
+
stems = INDEXED | {'.draft'}
|
|
172
|
+
|
|
173
|
+
monkeypatch.delenv('BRAIN_INDEX_HIDDEN', raising=False)
|
|
174
|
+
assert '.draft' not in {n['stem'] for n in read_notes(vault, stems)}
|
|
175
|
+
|
|
176
|
+
monkeypatch.setenv('BRAIN_INDEX_HIDDEN', '1')
|
|
177
|
+
assert '.draft' in {n['stem'] for n in read_notes(vault, stems)}
|
|
178
|
+
|
|
179
|
+
|
|
161
180
|
def test_a_filename_living_in_two_folders_is_reported_as_ambiguous(tmp_path):
|
|
162
181
|
"""Found by the review panel: `[[foo]]` names both files, so neither can be gold."""
|
|
163
182
|
vault = _vault(tmp_path)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""A missing index must say where to tell us about it (stderr), without needing a model."""
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
|
|
8
|
+
|
|
9
|
+
from sqlite_graph_memory import brain_ask # noqa: E402
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_missing_index_prints_feedback_link(tmp_path, monkeypatch, capsys):
|
|
13
|
+
monkeypatch.setattr(brain_ask, "EMB", tmp_path / "nope.npy")
|
|
14
|
+
monkeypatch.setattr(sys, "argv", ["sgm-ask", "anything"])
|
|
15
|
+
with pytest.raises(SystemExit) as exc:
|
|
16
|
+
brain_ask.main()
|
|
17
|
+
assert exc.value.code == 1
|
|
18
|
+
out = capsys.readouterr()
|
|
19
|
+
assert "need an embedding index" in out.err
|
|
20
|
+
assert brain_ask.FEEDBACK_URL in out.err
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_empty_query_does_not_nag(tmp_path, monkeypatch, capsys):
|
|
24
|
+
monkeypatch.setattr(brain_ask, "EMB", tmp_path / "nope.npy")
|
|
25
|
+
monkeypatch.setattr(sys, "argv", ["sgm-ask"])
|
|
26
|
+
with pytest.raises(SystemExit) as exc:
|
|
27
|
+
brain_ask.main()
|
|
28
|
+
assert exc.value.code == 1
|
|
29
|
+
assert brain_ask.FEEDBACK_URL not in capsys.readouterr().err
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Dot-FILES are skipped the same way dot-DIRECTORIES are (issue #25).
|
|
3
|
+
|
|
4
|
+
`iter_notes` filtered hidden *directories* by checking `rel.parts[:-1]` — every
|
|
5
|
+
path component except the last. The last component is the filename, so a note
|
|
6
|
+
called `.draft.md` or `.trash.md` sitting in the vault root (or in any visible
|
|
7
|
+
folder) was indexed as if it were a real note, while the same file one level
|
|
8
|
+
down inside `.drafts/` was correctly skipped. Reproduced 2026-10-13 with
|
|
9
|
+
`.secret.md` next to `ok.md`: `iter_notes()` returned both.
|
|
10
|
+
|
|
11
|
+
RED-FIRST: restore `rel.parts[:-1]` on the filter line and
|
|
12
|
+
`test_dot_file_in_root_is_skipped` fails with `.draft.md` in the result.
|
|
13
|
+
|
|
14
|
+
Both halves are pinned on purpose — the directory case already worked and must
|
|
15
|
+
keep working, the file case is the fix — and `BRAIN_INDEX_HIDDEN=1` must still
|
|
16
|
+
let both through, because someone legitimately keeps notes in a hidden folder.
|
|
17
|
+
|
|
18
|
+
No model download, no network: pure file work.
|
|
19
|
+
"""
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
24
|
+
from sqlite_graph_memory.index_notes import iter_notes # noqa: E402
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _vault(tmp_path: Path) -> Path:
|
|
28
|
+
(tmp_path / "ok.md").write_text("visible note", encoding="utf-8")
|
|
29
|
+
(tmp_path / ".draft.md").write_text("hidden file in root", encoding="utf-8")
|
|
30
|
+
(tmp_path / "sub").mkdir()
|
|
31
|
+
(tmp_path / "sub" / ".trash.md").write_text("hidden file in visible dir", encoding="utf-8")
|
|
32
|
+
(tmp_path / ".drafts").mkdir()
|
|
33
|
+
(tmp_path / ".drafts" / "inside.md").write_text("visible name, hidden dir", encoding="utf-8")
|
|
34
|
+
return tmp_path
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _names(root: Path) -> set[str]:
|
|
38
|
+
# as_posix(): the assertions spell paths with "/". str() gives a backslash on
|
|
39
|
+
# Windows, so "sub/.trash.md" would never match and the skip test would pass
|
|
40
|
+
# for the wrong reason.
|
|
41
|
+
return {p.relative_to(root).as_posix() for p in iter_notes(root)}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_dot_file_in_root_is_skipped(tmp_path, monkeypatch):
|
|
45
|
+
monkeypatch.delenv("BRAIN_INDEX_HIDDEN", raising=False)
|
|
46
|
+
assert ".draft.md" not in _names(_vault(tmp_path))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_dot_file_in_visible_dir_is_skipped(tmp_path, monkeypatch):
|
|
50
|
+
monkeypatch.delenv("BRAIN_INDEX_HIDDEN", raising=False)
|
|
51
|
+
assert "sub/.trash.md" not in _names(_vault(tmp_path))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_dot_dir_still_skipped_and_visible_note_still_kept(tmp_path, monkeypatch):
|
|
55
|
+
"""The pre-existing behaviour must survive the fix."""
|
|
56
|
+
monkeypatch.delenv("BRAIN_INDEX_HIDDEN", raising=False)
|
|
57
|
+
names = _names(_vault(tmp_path))
|
|
58
|
+
assert ".drafts/inside.md" not in names
|
|
59
|
+
assert "ok.md" in names
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_hidden_flag_admits_dot_files_and_dot_dirs(tmp_path, monkeypatch):
|
|
63
|
+
"""Opt-in must cover both kinds of hidden path, not just directories."""
|
|
64
|
+
monkeypatch.setenv("BRAIN_INDEX_HIDDEN", "1")
|
|
65
|
+
names = _names(_vault(tmp_path))
|
|
66
|
+
assert {"ok.md", ".draft.md", "sub/.trash.md", ".drafts/inside.md"} <= names
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_vault_root_that_is_itself_a_dot_directory_is_indexed(tmp_path, monkeypatch):
|
|
70
|
+
"""Only path components BELOW the vault root decide what is hidden.
|
|
71
|
+
|
|
72
|
+
A vault kept at ``~/.vault`` or ``~/.notes`` is ordinary. A filter that checks
|
|
73
|
+
the absolute path (``p.parts``) instead of the relative one (``rel.parts``)
|
|
74
|
+
sees ``.vault`` in every path and silently returns nothing.
|
|
75
|
+
"""
|
|
76
|
+
monkeypatch.delenv("BRAIN_INDEX_HIDDEN", raising=False)
|
|
77
|
+
root = tmp_path / ".vault"
|
|
78
|
+
root.mkdir()
|
|
79
|
+
names = _names(_vault(root))
|
|
80
|
+
assert "ok.md" in names
|
|
81
|
+
assert ".draft.md" not in names and ".drafts/inside.md" not in names
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Script-mode and working-directory behaviour of the MCP server.
|
|
3
|
+
|
|
4
|
+
Kept in its own file so it does not collide with edits to test_mcp_server.py.
|
|
5
|
+
Runs offline: no model, no index, no network.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
SRC = Path(__file__).resolve().parents[1] / "src"
|
|
14
|
+
sys.path.insert(0, str(SRC))
|
|
15
|
+
from sqlite_graph_memory import __version__ # noqa: E402
|
|
16
|
+
from sqlite_graph_memory import mcp_server # noqa: E402
|
|
17
|
+
|
|
18
|
+
SCRIPT = SRC / "sqlite_graph_memory" / "mcp_server.py"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_script_mode_starts_without_installed_package(tmp_path):
|
|
22
|
+
"""`python src/sqlite_graph_memory/mcp_server.py` must not need the package on sys.path.
|
|
23
|
+
|
|
24
|
+
Python puts the script's own directory (not src/) on sys.path, so an
|
|
25
|
+
absolute `from sqlite_graph_memory import ...` at import time crashed with
|
|
26
|
+
ModuleNotFoundError before the server answered anything (issue #24).
|
|
27
|
+
`-S` drops site-packages so an installed or editable copy cannot mask it.
|
|
28
|
+
"""
|
|
29
|
+
env = {k: v for k, v in os.environ.items() if k != "PYTHONPATH"}
|
|
30
|
+
req = {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}
|
|
31
|
+
res = subprocess.run(
|
|
32
|
+
[sys.executable, "-S", str(SCRIPT)],
|
|
33
|
+
input=json.dumps(req) + "\n",
|
|
34
|
+
capture_output=True, text=True, cwd=str(tmp_path), env=env, timeout=30,
|
|
35
|
+
)
|
|
36
|
+
assert res.returncode == 0, res.stderr
|
|
37
|
+
resp = json.loads(res.stdout.strip().splitlines()[-1])
|
|
38
|
+
assert resp["result"]["serverInfo"]["version"] == __version__
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_recall_subprocess_inherits_server_cwd(tmp_path, monkeypatch):
|
|
42
|
+
"""Relative BRAIN_INDEX_DIR / TURNSTATE_DB must resolve against the user's cwd.
|
|
43
|
+
|
|
44
|
+
On an installed wheel the package dir is site-packages/sqlite_graph_memory;
|
|
45
|
+
running brain_ask there made `BRAIN_INDEX_DIR=index` miss the user's ./index.
|
|
46
|
+
"""
|
|
47
|
+
work = tmp_path / "work"
|
|
48
|
+
work.mkdir()
|
|
49
|
+
stub = tmp_path / "stub_brain_ask.py"
|
|
50
|
+
stub.write_text(
|
|
51
|
+
"import os, pathlib\n"
|
|
52
|
+
"pathlib.Path(os.environ['BRAIN_ANSWER_OUT']).write_text(os.getcwd(), encoding='utf-8')\n",
|
|
53
|
+
encoding="utf-8",
|
|
54
|
+
)
|
|
55
|
+
monkeypatch.setattr(mcp_server, "_brain_ask_cmd", lambda: [sys.executable, str(stub)])
|
|
56
|
+
monkeypatch.chdir(work)
|
|
57
|
+
text, is_error = mcp_server.run_recall("anything")
|
|
58
|
+
assert is_error is False, text
|
|
59
|
+
assert Path(text).resolve() == work.resolve()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/src/sqlite_graph_memory/turnstate_hook.py
RENAMED
|
File without changes
|
{sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/src/sqlite_graph_memory/turnstate_show.py
RENAMED
|
File without changes
|
{sqlite_graph_memory-0.3.2 → sqlite_graph_memory-0.3.3}/src/sqlite_graph_memory/vault_stats.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|