lorekeep 0.7.0__tar.gz → 0.7.1__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 (136) hide show
  1. lorekeep-0.7.1/.release-please-manifest.json +3 -0
  2. {lorekeep-0.7.0 → lorekeep-0.7.1}/CHANGELOG.md +7 -0
  3. {lorekeep-0.7.0 → lorekeep-0.7.1}/PKG-INFO +1 -1
  4. {lorekeep-0.7.0 → lorekeep-0.7.1}/pyproject.toml +1 -1
  5. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/__init__.py +1 -1
  6. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/compile/extract.py +5 -2
  7. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/mcp_server.py +19 -4
  8. lorekeep-0.7.1/tests/test_fts.py +109 -0
  9. lorekeep-0.7.0/.release-please-manifest.json +0 -3
  10. lorekeep-0.7.0/tests/test_fts.py +0 -31
  11. {lorekeep-0.7.0 → lorekeep-0.7.1}/.commitlintrc.json +0 -0
  12. {lorekeep-0.7.0 → lorekeep-0.7.1}/.github/workflows/auto-merge-release-please.yml +0 -0
  13. {lorekeep-0.7.0 → lorekeep-0.7.1}/.github/workflows/ci.yml +0 -0
  14. {lorekeep-0.7.0 → lorekeep-0.7.1}/.github/workflows/lint-commits.yml +0 -0
  15. {lorekeep-0.7.0 → lorekeep-0.7.1}/.github/workflows/release-please.yml +0 -0
  16. {lorekeep-0.7.0 → lorekeep-0.7.1}/.gitignore +0 -0
  17. {lorekeep-0.7.0 → lorekeep-0.7.1}/.lorekeep/config.yaml.example +0 -0
  18. {lorekeep-0.7.0 → lorekeep-0.7.1}/.lorekeep/schema.json +0 -0
  19. {lorekeep-0.7.0 → lorekeep-0.7.1}/.pre-commit-config.yaml +0 -0
  20. {lorekeep-0.7.0 → lorekeep-0.7.1}/AGENTS.md +0 -0
  21. {lorekeep-0.7.0 → lorekeep-0.7.1}/CLAUDE.md +0 -0
  22. {lorekeep-0.7.0 → lorekeep-0.7.1}/LICENSE +0 -0
  23. {lorekeep-0.7.0 → lorekeep-0.7.1}/README.md +0 -0
  24. {lorekeep-0.7.0 → lorekeep-0.7.1}/SECURITY.md +0 -0
  25. {lorekeep-0.7.0 → lorekeep-0.7.1}/cover.jpeg +0 -0
  26. {lorekeep-0.7.0 → lorekeep-0.7.1}/docs/README.md +0 -0
  27. {lorekeep-0.7.0 → lorekeep-0.7.1}/docs/architecture/agent.md +0 -0
  28. {lorekeep-0.7.0 → lorekeep-0.7.1}/docs/architecture/data-model.md +0 -0
  29. {lorekeep-0.7.0 → lorekeep-0.7.1}/docs/architecture/evaluation.md +0 -0
  30. {lorekeep-0.7.0 → lorekeep-0.7.1}/docs/architecture/journal.md +0 -0
  31. {lorekeep-0.7.0 → lorekeep-0.7.1}/docs/architecture/overview.md +0 -0
  32. {lorekeep-0.7.0 → lorekeep-0.7.1}/docs/architecture/permission.md +0 -0
  33. {lorekeep-0.7.0 → lorekeep-0.7.1}/docs/architecture/pipeline.md +0 -0
  34. {lorekeep-0.7.0 → lorekeep-0.7.1}/docs/architecture/serve-mcp.md +0 -0
  35. {lorekeep-0.7.0 → lorekeep-0.7.1}/docs/architecture/temporal.md +0 -0
  36. {lorekeep-0.7.0 → lorekeep-0.7.1}/docs/guides/backup.md +0 -0
  37. {lorekeep-0.7.0 → lorekeep-0.7.1}/docs/guides/compile.md +0 -0
  38. {lorekeep-0.7.0 → lorekeep-0.7.1}/docs/guides/data-home.md +0 -0
  39. {lorekeep-0.7.0 → lorekeep-0.7.1}/docs/guides/getting-started.md +0 -0
  40. {lorekeep-0.7.0 → lorekeep-0.7.1}/docs/guides/import.md +0 -0
  41. {lorekeep-0.7.0 → lorekeep-0.7.1}/docs/guides/serve.md +0 -0
  42. {lorekeep-0.7.0 → lorekeep-0.7.1}/docs/guides/wiki.md +0 -0
  43. {lorekeep-0.7.0 → lorekeep-0.7.1}/docs/superpowers/plans/2026-06-26-lorekeep-dotdir-backup.md +0 -0
  44. {lorekeep-0.7.0 → lorekeep-0.7.1}/docs/superpowers/specs/2026-06-26-lorekeep-dotdir-layout-design.md +0 -0
  45. {lorekeep-0.7.0 → lorekeep-0.7.1}/release-please-config.json +0 -0
  46. {lorekeep-0.7.0 → lorekeep-0.7.1}/scripts/check-conventional-commit.py +0 -0
  47. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/agent.py +0 -0
  48. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/backup.py +0 -0
  49. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/cli.py +0 -0
  50. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/compile/__init__.py +0 -0
  51. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/compile/ingest.py +0 -0
  52. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/compile/providers.py +0 -0
  53. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/compile/resolve.py +0 -0
  54. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/compile/writer.py +0 -0
  55. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/config.py +0 -0
  56. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/defaults.py +0 -0
  57. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/eval/__init__.py +0 -0
  58. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/eval/construction.py +0 -0
  59. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/eval/gold.py +0 -0
  60. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/eval/retrieval.py +0 -0
  61. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/facts_io.py +0 -0
  62. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/importer/__init__.py +0 -0
  63. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/importer/claude.py +0 -0
  64. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/importer/codex.py +0 -0
  65. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/importer/cursor.py +0 -0
  66. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/importer/opencode.py +0 -0
  67. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/integrations/__init__.py +0 -0
  68. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/integrations/claude_code.py +0 -0
  69. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/integrations/codex.py +0 -0
  70. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/integrations/common.py +0 -0
  71. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/integrations/cursor.py +0 -0
  72. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/integrations/detect.py +0 -0
  73. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/integrations/opencode.py +0 -0
  74. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/journal.py +0 -0
  75. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/models.py +0 -0
  76. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/paths.py +0 -0
  77. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/perm/__init__.py +0 -0
  78. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/perm/ns.py +0 -0
  79. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/pipeline.py +0 -0
  80. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/schema_io.py +0 -0
  81. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/store/__init__.py +0 -0
  82. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/store/fts.py +0 -0
  83. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/store/graph.py +0 -0
  84. {lorekeep-0.7.0 → lorekeep-0.7.1}/src/lorekeep/wiki.py +0 -0
  85. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/conftest.py +0 -0
  86. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/fixtures/claude-session/memory/MEMORY.md +0 -0
  87. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/fixtures/claude-session/memory/lorekeep-design.md +0 -0
  88. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/fixtures/claude-session/memory/lorekeep-project-state.md +0 -0
  89. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/fixtures/claude-session/test.jsonl +0 -0
  90. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/fixtures/gold/payments.facts.jsonl +0 -0
  91. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/fixtures/raw/backend/payments.md +0 -0
  92. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/fixtures/retrieval/questions.json +0 -0
  93. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/fixtures/schema.json +0 -0
  94. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_agent_cli.py +0 -0
  95. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_agent_detect.py +0 -0
  96. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_backup.py +0 -0
  97. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_backup_cli.py +0 -0
  98. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_compile_cli.py +0 -0
  99. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_config.py +0 -0
  100. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_construction_eval.py +0 -0
  101. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_defaults.py +0 -0
  102. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_determinism.py +0 -0
  103. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_doctor_cli.py +0 -0
  104. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_eval_cli.py +0 -0
  105. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_extract.py +0 -0
  106. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_graph_store.py +0 -0
  107. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_import_claude.py +0 -0
  108. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_import_codex.py +0 -0
  109. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_import_cursor.py +0 -0
  110. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_import_opencode.py +0 -0
  111. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_ingest.py +0 -0
  112. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_init_chain.py +0 -0
  113. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_init_cli.py +0 -0
  114. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_integrations.py +0 -0
  115. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_mcp_add_cli.py +0 -0
  116. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_mcp_reload.py +0 -0
  117. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_mcp_server.py +0 -0
  118. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_mcp_write_tools.py +0 -0
  119. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_merge_journals.py +0 -0
  120. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_models.py +0 -0
  121. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_paths.py +0 -0
  122. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_perm.py +0 -0
  123. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_pipeline.py +0 -0
  124. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_providers.py +0 -0
  125. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_resolve.py +0 -0
  126. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_retrieval_eval.py +0 -0
  127. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_schema_io.py +0 -0
  128. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_scope_awareness.py +0 -0
  129. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_serve_cli.py +0 -0
  130. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_session_hook.py +0 -0
  131. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_smoke.py +0 -0
  132. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_watch_e2e.py +0 -0
  133. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_watch_sessions.py +0 -0
  134. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_wiki.py +0 -0
  135. {lorekeep-0.7.0 → lorekeep-0.7.1}/tests/test_writer.py +0 -0
  136. {lorekeep-0.7.0 → lorekeep-0.7.1}/uv.lock +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.7.1"
3
+ }
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.1](https://github.com/manhhailua/lorekeep/compare/v0.7.0...v0.7.1) (2026-06-30)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * wire FTS5 search index + include model in extraction cache key ([#74](https://github.com/manhhailua/lorekeep/issues/74)) ([e9cd60a](https://github.com/manhhailua/lorekeep/commit/e9cd60ab573ae42b172f888cf3af5647e747c3f7))
9
+
3
10
  ## [0.7.0](https://github.com/manhhailua/lorekeep/compare/v0.6.1...v0.7.0) (2026-06-30)
4
11
 
5
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lorekeep
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: Temporal knowledge graph for AI agents via MCP
5
5
  Project-URL: Homepage, https://github.com/manhhailua/lorekeep
6
6
  Project-URL: Repository, https://github.com/manhhailua/lorekeep
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lorekeep"
3
- version = "0.7.0"
3
+ version = "0.7.1"
4
4
  description = "Temporal knowledge graph for AI agents via MCP"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -1,3 +1,3 @@
1
1
  """Lorekeep — temporal knowledge graph for AI agents via MCP."""
2
2
 
3
- __version__ = "0.7.0"
3
+ __version__ = "0.7.1"
@@ -119,10 +119,12 @@ class ExtractionCache:
119
119
  if self.path.exists():
120
120
  self._data = json.loads(self.path.read_text(encoding="utf-8"))
121
121
 
122
- def key(self, chunk: DocChunk, schema_version: int) -> str:
122
+ def key(self, chunk: DocChunk, schema_version: int, model: str = "") -> str:
123
123
  h = hashlib.sha256()
124
124
  h.update(str(schema_version).encode("utf-8"))
125
125
  h.update(b"\n")
126
+ h.update(model.encode("utf-8"))
127
+ h.update(b"\n")
126
128
  h.update(chunk.hash.encode("utf-8"))
127
129
  return h.hexdigest()
128
130
 
@@ -142,7 +144,8 @@ class ExtractionCache:
142
144
  def extract_chunk(
143
145
  chunk: DocChunk, schema: Schema, provider: LLMProvider, cache: ExtractionCache,
144
146
  ) -> tuple[list[Node], list[Edge], dict[str, list[str]]]:
145
- key = cache.key(chunk, schema.version)
147
+ model = getattr(provider, "model", "")
148
+ key = cache.key(chunk, schema.version, model)
146
149
  raw = cache.get(key)
147
150
  if raw is None:
148
151
  raw = provider.extract_json(SYSTEM_PROMPT, build_prompt(chunk, schema))
@@ -19,13 +19,15 @@ from lorekeep.models import JournalEntry, Manifest, Schema
19
19
  from lorekeep.perm.ns import ScopedGraph
20
20
  from lorekeep.schema_io import load_schema
21
21
  from lorekeep.store.graph import GraphStore, parse_date
22
+ from lorekeep.store.fts import FTSIndex
22
23
 
23
24
  mcp = FastMCP("lorekeep")
24
25
 
25
- _state: dict = {} # graph_dir, allowed_ns, schema_path, pending_dir, facts_mtime
26
+ _state: dict = {} # graph_dir, allowed_ns, schema_path, pending_dir, fts_path, facts_mtime
26
27
  _scope: ScopedGraph | None = None
27
28
  _schema: Schema | None = None
28
29
  _manifest: Manifest | None = None
30
+ _fts: FTSIndex | None = None
29
31
 
30
32
 
31
33
  def configure(graph_dir, allowed_ns, schema_path=None, fts_path=None, pending_dir=None) -> None:
@@ -34,12 +36,16 @@ def configure(graph_dir, allowed_ns, schema_path=None, fts_path=None, pending_di
34
36
  _state["allowed_ns"] = list(allowed_ns)
35
37
  _state["schema_path"] = Path(schema_path) if schema_path else None
36
38
  _state["pending_dir"] = Path(pending_dir) if pending_dir else None
39
+ if fts_path:
40
+ _state["fts_path"] = Path(fts_path)
41
+ else:
42
+ _state["fts_path"] = _state["graph_dir"] / "fts.sqlite"
37
43
  _rebuild()
38
44
 
39
45
 
40
46
  def _rebuild() -> None:
41
- """(Re)load the graph + schema + manifest from disk into a fresh ScopedGraph."""
42
- global _scope, _schema, _manifest
47
+ """(Re)load the graph + schema + manifest + FTS from disk into a fresh ScopedGraph."""
48
+ global _scope, _schema, _manifest, _fts
43
49
  facts = _state["graph_dir"] / "facts.jsonl"
44
50
  store = GraphStore.from_jsonl(facts)
45
51
  sp = _state.get("schema_path")
@@ -50,6 +56,15 @@ def _rebuild() -> None:
50
56
  _manifest = Manifest.from_json(manifest_path.read_text(encoding="utf-8"))
51
57
  else:
52
58
  _manifest = None
59
+ try:
60
+ fts_path = _state.get("fts_path")
61
+ if fts_path:
62
+ _fts = FTSIndex(fts_path)
63
+ _fts.build(store.all_nodes())
64
+ else:
65
+ _fts = None
66
+ except Exception:
67
+ _fts = None
53
68
  _state["facts_mtime"] = facts.stat().st_mtime if facts.exists() else 0
54
69
 
55
70
 
@@ -125,7 +140,7 @@ def changes(from_t: str, to_t: str) -> dict:
125
140
  @mcp.tool()
126
141
  def search(query: str, limit: int = 10) -> list:
127
142
  """Text search over node ids/props, scoped to the caller."""
128
- return _require().search(query, limit)
143
+ return _require().search(query, limit, fts=_fts)
129
144
 
130
145
 
131
146
  @mcp.tool()
@@ -0,0 +1,109 @@
1
+ from pathlib import Path
2
+ from lorekeep.models import Node
3
+ from lorekeep.store.fts import FTSIndex, scan_search, node_text
4
+
5
+
6
+ def nd(id, name, lang=None):
7
+ props = {"name": name}
8
+ if lang:
9
+ props["lang"] = lang
10
+ return Node(id=id, type="service", ns=("teams/backend",), props=props)
11
+
12
+
13
+ def test_node_text_concatenates_id_name_props():
14
+ t = node_text(nd("svc:x", "auth", "go"))
15
+ assert "svc:x" in t and "auth" in t and "go" in t
16
+
17
+
18
+ def test_scan_search_substring(tmp_path: Path):
19
+ nodes = [nd("svc:a", "payments-api"), nd("svc:b", "auth")]
20
+ assert scan_search(nodes, "pay") == ["svc:a"]
21
+ assert scan_search(nodes, "auth") == ["svc:b"]
22
+ assert scan_search(nodes, "zzz") == []
23
+
24
+
25
+ def test_fts_index_build_and_match(tmp_path: Path):
26
+ idx = FTSIndex(tmp_path / "fts.sqlite")
27
+ idx.build([nd("svc:a", "payments"), nd("svc:b", "auth")])
28
+ assert "svc:a" in idx.search("payments")
29
+ assert idx.search("payments") == ["svc:a"]
30
+ assert idx.search("nomatch*") == []
31
+ idx.close()
32
+
33
+
34
+ def test_fts_different_keys_for_different_models(tmp_path: Path):
35
+ """Cache key must differ when model changes."""
36
+ from lorekeep.models import DocChunk
37
+ from lorekeep.compile.extract import ExtractionCache
38
+
39
+ cache = ExtractionCache(tmp_path / "cache.json")
40
+ chunk = DocChunk(
41
+ path="raw/test.md", start_line=1, end_line=10,
42
+ text="hello", namespace="test",
43
+ )
44
+ key_a = cache.key(chunk, schema_version=1, model="gpt-4o-mini")
45
+ key_b = cache.key(chunk, schema_version=1, model="claude-sonnet-4")
46
+ assert key_a != key_b
47
+
48
+
49
+ def test_fts_cache_key_backward_compat(tmp_path: Path):
50
+ """Old cache keys (no model) should not collide with new keys (empty model)."""
51
+ from lorekeep.models import DocChunk
52
+ from lorekeep.compile.extract import ExtractionCache
53
+
54
+ cache = ExtractionCache(tmp_path / "cache.json")
55
+ chunk = DocChunk(
56
+ path="raw/test.md", start_line=1, end_line=10,
57
+ text="hello", namespace="test",
58
+ )
59
+ key_default = cache.key(chunk, schema_version=1)
60
+ key_empty = cache.key(chunk, schema_version=1, model="")
61
+ assert key_default == key_empty
62
+
63
+
64
+ def test_mcp_search_uses_fts(tmp_path: Path):
65
+ """mcp_server search tool uses FTSIndex when configured."""
66
+ from lorekeep.mcp_server import configure, search
67
+ from lorekeep.compile.writer import write_graph
68
+ from lorekeep.models import Edge, Manifest
69
+
70
+ nodes = [
71
+ Node(id="svc:payments", type="service", ns=("backend",), props={"name": "payments", "lang": "go"}),
72
+ Node(id="svc:auth", type="service", ns=("backend",), props={"name": "auth"}),
73
+ ]
74
+ graph = tmp_path / "graph"
75
+ write_graph(graph, nodes, [], Manifest(
76
+ schema_version=1, chunk_count=0, node_count=2, edge_count=0,
77
+ run_id="x", facts_hash="y",
78
+ ))
79
+
80
+ configure(graph_dir=graph, allowed_ns=["backend"])
81
+ results = search("payments")
82
+ assert "svc:payments" in results
83
+ assert "svc:auth" not in results
84
+
85
+ results = search("auth")
86
+ assert "svc:auth" in results
87
+
88
+ fts_db = graph / "fts.sqlite"
89
+ assert fts_db.exists()
90
+
91
+
92
+ def test_mcp_search_fts_fallback_on_error(tmp_path: Path):
93
+ """If FTS fails to build, search falls back to scan."""
94
+ from lorekeep.mcp_server import configure, search
95
+ from lorekeep.compile.writer import write_graph
96
+ from lorekeep.models import Manifest
97
+
98
+ nodes = [
99
+ Node(id="svc:payments", type="service", ns=("backend",), props={"name": "payments"}),
100
+ ]
101
+ graph = tmp_path / "graph"
102
+ write_graph(graph, nodes, [], Manifest(
103
+ schema_version=1, chunk_count=0, node_count=1, edge_count=0,
104
+ run_id="x", facts_hash="y",
105
+ ))
106
+
107
+ configure(graph_dir=graph, allowed_ns=["backend"], fts_path=tmp_path / "nonexistent" / "deep" / "fts.sqlite")
108
+ results = search("payments")
109
+ assert "svc:payments" in results
@@ -1,3 +0,0 @@
1
- {
2
- ".": "0.7.0"
3
- }
@@ -1,31 +0,0 @@
1
- from pathlib import Path
2
- from lorekeep.models import Node
3
- from lorekeep.store.fts import FTSIndex, scan_search, node_text
4
-
5
-
6
- def nd(id, name, lang=None):
7
- props = {"name": name}
8
- if lang:
9
- props["lang"] = lang
10
- return Node(id=id, type="service", ns=("teams/backend",), props=props)
11
-
12
-
13
- def test_node_text_concatenates_id_name_props():
14
- t = node_text(nd("svc:x", "auth", "go"))
15
- assert "svc:x" in t and "auth" in t and "go" in t
16
-
17
-
18
- def test_scan_search_substring(tmp_path: Path):
19
- nodes = [nd("svc:a", "payments-api"), nd("svc:b", "auth")]
20
- assert scan_search(nodes, "pay") == ["svc:a"]
21
- assert scan_search(nodes, "auth") == ["svc:b"]
22
- assert scan_search(nodes, "zzz") == []
23
-
24
-
25
- def test_fts_index_build_and_match(tmp_path: Path):
26
- idx = FTSIndex(tmp_path / "fts.sqlite")
27
- idx.build([nd("svc:a", "payments"), nd("svc:b", "auth")])
28
- assert "svc:a" in idx.search("payments")
29
- assert idx.search("payments") == ["svc:a"]
30
- assert idx.search("nomatch*") == []
31
- idx.close()
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
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes