brainpick 0.3.2__tar.gz → 0.4.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 (105) hide show
  1. {brainpick-0.3.2 → brainpick-0.4.0}/PKG-INFO +1 -1
  2. {brainpick-0.3.2 → brainpick-0.4.0}/pyproject.toml +1 -1
  3. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/__init__.py +1 -1
  4. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/cli.py +1 -1
  5. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/integrate.py +21 -2
  6. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/mcp_server.py +34 -1
  7. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/scaffold.py +32 -0
  8. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick.egg-info/PKG-INFO +1 -1
  9. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_e2e_serve.py +1 -1
  10. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_integrate.py +15 -0
  11. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_mcp_tools.py +16 -0
  12. {brainpick-0.3.2 → brainpick-0.4.0}/README.md +0 -0
  13. {brainpick-0.3.2 → brainpick-0.4.0}/setup.cfg +0 -0
  14. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/__main__.py +0 -0
  15. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/_skill/SKILL.md +0 -0
  16. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/_static/assets/Editor-x4G_0KV0.js +0 -0
  17. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/_static/assets/index-CTcBQZsl.js +0 -0
  18. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/_static/assets/index-Dn6TTKMy.css +0 -0
  19. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/_static/assets/virtual_pwa-register-Cb81Y-Xh.js +0 -0
  20. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/_static/assets/workbox-window.prod.es5-BBnX5xw4.js +0 -0
  21. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/_static/assets/worker-CVB4_RMu.js +0 -0
  22. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/_static/icons/apple-touch-icon.png +0 -0
  23. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/_static/icons/favicon.svg +0 -0
  24. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/_static/icons/icon-192.png +0 -0
  25. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/_static/icons/icon-512.png +0 -0
  26. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/_static/icons/icon-maskable-512.png +0 -0
  27. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/_static/index.html +0 -0
  28. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/_static/manifest.webmanifest +0 -0
  29. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/_static/sw.js +0 -0
  30. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/_static/workbox-a3c94b52.js +0 -0
  31. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/auth.py +0 -0
  32. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/compile/__init__.py +0 -0
  33. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/compile/pipeline.py +0 -0
  34. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/compile/similarity_gaps.py +0 -0
  35. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/compile/t1.py +0 -0
  36. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/compile/t2.py +0 -0
  37. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/compile/t3.py +0 -0
  38. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/config.py +0 -0
  39. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/core/__init__.py +0 -0
  40. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/core/bundle.py +0 -0
  41. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/core/canonical.py +0 -0
  42. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/core/frontmatter.py +0 -0
  43. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/core/fs.py +0 -0
  44. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/core/links.py +0 -0
  45. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/deltas.py +0 -0
  46. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/detect.py +0 -0
  47. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/embed.py +0 -0
  48. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/federation.py +0 -0
  49. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/kg.py +0 -0
  50. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/kgadapt/__init__.py +0 -0
  51. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/kgadapt/algorithmic.py +0 -0
  52. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/kgadapt/protocol.py +0 -0
  53. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/llm.py +0 -0
  54. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/merge.py +0 -0
  55. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/query/__init__.py +0 -0
  56. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/query/keyword.py +0 -0
  57. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/query/present.py +0 -0
  58. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/query/router.py +0 -0
  59. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/query/vectors.py +0 -0
  60. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/serve/__init__.py +0 -0
  61. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/serve/app.py +0 -0
  62. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/serve/live.py +0 -0
  63. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/serve/rest.py +0 -0
  64. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/serve/state.py +0 -0
  65. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/serve/watcher.py +0 -0
  66. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/timeline.py +0 -0
  67. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick/vectorstore.py +0 -0
  68. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick.egg-info/SOURCES.txt +0 -0
  69. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick.egg-info/dependency_links.txt +0 -0
  70. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick.egg-info/entry_points.txt +0 -0
  71. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick.egg-info/requires.txt +0 -0
  72. {brainpick-0.3.2 → brainpick-0.4.0}/src/brainpick.egg-info/top_level.txt +0 -0
  73. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_auth.py +0 -0
  74. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_bundle.py +0 -0
  75. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_cli.py +0 -0
  76. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_config.py +0 -0
  77. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_conformance.py +0 -0
  78. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_deltas.py +0 -0
  79. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_detect.py +0 -0
  80. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_e2e_mcp.py +0 -0
  81. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_federation.py +0 -0
  82. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_frontmatter.py +0 -0
  83. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_keyword.py +0 -0
  84. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_kg.py +0 -0
  85. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_links.py +0 -0
  86. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_llm.py +0 -0
  87. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_merge.py +0 -0
  88. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_pipeline.py +0 -0
  89. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_present.py +0 -0
  90. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_report.py +0 -0
  91. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_router.py +0 -0
  92. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_scaffold.py +0 -0
  93. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_serve_state.py +0 -0
  94. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_similarity_gaps.py +0 -0
  95. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_similarity_gaps_stage.py +0 -0
  96. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_t1.py +0 -0
  97. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_t2_chunker.py +0 -0
  98. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_t2_embed.py +0 -0
  99. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_t2_stage.py +0 -0
  100. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_t2_store.py +0 -0
  101. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_t3_algorithmic.py +0 -0
  102. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_t3_backend.py +0 -0
  103. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_t3_export.py +0 -0
  104. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_t3_stage.py +0 -0
  105. {brainpick-0.3.2 → brainpick-0.4.0}/tests/test_timeline.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brainpick
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: A turn-key brain stack for agents: compile an OKF markdown bundle into a queryable, servable, visualizable brain.
5
5
  Author: Benquemax
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "brainpick"
7
- version = "0.3.2"
7
+ version = "0.4.0"
8
8
  description = "A turn-key brain stack for agents: compile an OKF markdown bundle into a queryable, servable, visualizable brain."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,4 +1,4 @@
1
1
  """brainpick — a turn-key brain stack for agents."""
2
2
 
3
- __version__ = "0.3.2"
3
+ __version__ = "0.4.0"
4
4
  SPEC_VERSION = "0.1"
@@ -500,7 +500,7 @@ def build_parser() -> argparse.ArgumentParser:
500
500
 
501
501
  p_integrate = sub.add_parser("integrate", help="install brainpick into an agent harness (skill, MCP, report)")
502
502
  p_integrate.add_argument("target", metavar="<target>",
503
- help="the harness to wire up: claude-code | opencode | agents-md")
503
+ help="the harness to wire up: claude-code | opencode | agents-md | dsh")
504
504
  p_integrate.add_argument("--root", default=".", help="bundle root (default: current directory)")
505
505
  p_integrate.add_argument("--dry-run", action="store_true",
506
506
  help="print what integrate would do without writing anything")
@@ -9,6 +9,9 @@ Three targets, one family voice (mirrors henxels' `integrate`):
9
9
  opencode.json MCP snippet.
10
10
  - `agents-md` — ensure an AGENTS.md exists (the one place integrate may create a
11
11
  file), install the brain-report markers if absent, and compile so the block fills.
12
+ - `dsh` — write the Agent Skill (the `.claude/skills` convention dsh shares),
13
+ then PRINT the `cordis.patch.yml` insert that mounts brainpick through
14
+ `@deepseek-ai/dsh-mcp-client` (dsh has no `claude mcp add`; a server is a config row).
12
15
 
13
16
  The shipped Agent Skill (integrations/skill/SKILL.md, canonical) rides inside the
14
17
  package; the parity test asserts the shipped copy is byte-identical to the canonical.
@@ -22,15 +25,17 @@ from pathlib import Path
22
25
  from brainpick.compile.pipeline import run_compile
23
26
  from brainpick.compile.t1 import REPORT_BEGIN_PREFIX, REPORT_END_MARKER
24
27
  from brainpick.detect import find_repo_root
25
- from brainpick.scaffold import _Voice, mcp_snippets
28
+ from brainpick.scaffold import _Voice, dsh_snippet, mcp_snippets
26
29
 
27
- TARGETS = ("claude-code", "opencode", "agents-md")
30
+ TARGETS = ("claude-code", "opencode", "agents-md", "dsh")
28
31
  HENXELS_BEGIN = "<!-- henxels:begin -->"
29
32
 
30
33
  # harness -> where its Agent Skill lands, relative to the repo root
31
34
  SKILL_DESTINATIONS = {
32
35
  "claude-code": Path(".claude") / "skills" / "brainpick" / "SKILL.md",
33
36
  "opencode": Path(".opencode") / "skills" / "brainpick" / "SKILL.md",
37
+ # dsh loads Agent Skills from the same .claude/skills convention as Claude Code.
38
+ "dsh": Path(".claude") / "skills" / "brainpick" / "SKILL.md",
34
39
  }
35
40
 
36
41
  _MINIMAL_AGENTS = "# AGENTS.md\n\nWorking notes for agents in this repository.\n"
@@ -128,6 +133,18 @@ def _integrate_opencode(voice: _Voice, root: Path, repo: Path, dry_run: bool) ->
128
133
  return 0
129
134
 
130
135
 
136
+ def _integrate_dsh(voice: _Voice, root: Path, repo: Path, dry_run: bool) -> int:
137
+ dest, existed = _write_skill(repo, "dsh", dry_run)
138
+ verb = "would write" if dry_run else ("updated" if existed else "wrote")
139
+ voice.line("✓", f"skill: {verb} {dest}")
140
+ if dry_run:
141
+ voice.step("• print the cordis.patch.yml insert for @deepseek-ai/dsh-mcp-client")
142
+ return 0
143
+ voice.raw()
144
+ voice.raw(dsh_snippet(root))
145
+ return 0
146
+
147
+
131
148
  def _integrate_agents_md(voice: _Voice, root: Path, repo: Path, dry_run: bool) -> int:
132
149
  agents = repo / "AGENTS.md"
133
150
  existed = agents.is_file()
@@ -177,4 +194,6 @@ def run_integrate(target: str, root: str | Path, dry_run: bool = False) -> int:
177
194
  return _integrate_claude_code(voice, root, repo, dry_run)
178
195
  if target == "opencode":
179
196
  return _integrate_opencode(voice, root, repo, dry_run)
197
+ if target == "dsh":
198
+ return _integrate_dsh(voice, root, repo, dry_run)
180
199
  return _integrate_agents_md(voice, root, repo, dry_run)
@@ -26,6 +26,7 @@ from brainpick.federation import (
26
26
  )
27
27
  from brainpick.llm import make_chat
28
28
  from brainpick.merge import find_base, resolve
29
+ from brainpick.query.keyword import tokenize
29
30
  from brainpick.query.router import KNOWN_MODES, run_search
30
31
  from brainpick.serve.state import ServeState, bfs_neighborhood, jsonable, resolve_doc
31
32
  from brainpick.serve.watcher import recompile_and_broadcast
@@ -106,6 +107,25 @@ def _single_overview(state: ServeState, budget_tokens: int | None = None) -> dic
106
107
  # -- brain_search ------------------------------------------------------------------
107
108
 
108
109
 
110
+ def _matched_terms(query: str, *fields: str) -> list[str]:
111
+ """The query tokens that actually occur in the given fields, in query order,
112
+ de-duplicated. Uses the same tokenizer keyword search uses, so 'why' reports
113
+ what really matched instead of echoing the whole query string (issue #2)."""
114
+ field_tokens = set()
115
+ for field in fields:
116
+ if field:
117
+ field_tokens.update(tokenize(field))
118
+ seen: dict[str, None] = {}
119
+ for token in tokenize(query):
120
+ if token in field_tokens and token not in seen:
121
+ seen[token] = None
122
+ return list(seen)
123
+
124
+
125
+ def _quote_terms(terms: list[str]) -> str:
126
+ return ", ".join(f"'{t}'" for t in terms)
127
+
128
+
109
129
  def _why(hit: dict, query: str) -> str:
110
130
  lowered = query.lower()
111
131
  if lowered in str(hit["title"]).lower():
@@ -116,7 +136,20 @@ def _why(hit: dict, query: str) -> str:
116
136
  return f"semantically close to '{query}'"
117
137
  if hit.get("source") == "graph":
118
138
  return f"connected in the entity graph to '{query}'"
119
- return f"body mentions '{query}'" if hit.get("snippet") else "keyword match"
139
+ # Keyword-ish hit: name the query tokens that actually occur, and where, rather
140
+ # than claiming the whole query appears verbatim (issue #2).
141
+ total = len(dict.fromkeys(tokenize(query)))
142
+ title_terms = _matched_terms(query, str(hit.get("title") or ""))
143
+ desc_terms = _matched_terms(query, hit.get("description") or "")
144
+ body_terms = _matched_terms(query, hit.get("snippet") or "")
145
+ if title_terms:
146
+ return f"title mentions {_quote_terms(title_terms)}"
147
+ if desc_terms:
148
+ return f"description mentions {_quote_terms(desc_terms)}"
149
+ if body_terms:
150
+ suffix = f" ({len(body_terms)}/{total} terms)" if total > 1 else ""
151
+ return f"body mentions {_quote_terms(body_terms)}{suffix}"
152
+ return "keyword match"
120
153
 
121
154
 
122
155
  def _single_search(state: ServeState, query: str, mode: str = "auto", limit: int = 8,
@@ -212,6 +212,38 @@ def mcp_snippets(bundle: Path) -> str:
212
212
  return "\n".join(parts)
213
213
 
214
214
 
215
+ def dsh_snippet(bundle: Path) -> str:
216
+ """The DeepSeek Harness (dsh) wiring: an insert entry for the web profile's
217
+ `cordis.patch.yml` that mounts brainpick through `@deepseek-ai/dsh-mcp-client`
218
+ as a stdio server. dsh has no `claude mcp add` equivalent — a server is a
219
+ config row, so we print the exact row with the resolved absolute root."""
220
+ command = brainpick_command() + ["mcp", "--root", str(bundle)]
221
+ entry = (
222
+ " - id: mcp-brainpick\n"
223
+ " name: '@deepseek-ai/dsh-mcp-client'\n"
224
+ " config:\n"
225
+ " serverName: brainpick\n"
226
+ " transport: stdio\n"
227
+ f" command: {command[0]}\n"
228
+ f" args: {json.dumps(command[1:])}\n"
229
+ " failOnStartupError: false"
230
+ )
231
+ parts = [
232
+ "Wire brainpick into DeepSeek Harness (dsh):",
233
+ "",
234
+ " Add this insert entry to your web profile's cordis.patch.yml",
235
+ " (~/.dsh/profiles/web/cordis.patch.yml), then restart the service",
236
+ " (e.g. systemctl --user restart dsh.service) and hard-refresh:",
237
+ "",
238
+ " - insert:",
239
+ entry,
240
+ "",
241
+ " Tools then surface as mcp__brainpick__brain_* — brain_overview first,",
242
+ " then brain_search / brain_read / brain_neighbors.",
243
+ ]
244
+ return "\n".join(parts)
245
+
246
+
215
247
  def henxels_fragment(contract: Path, bundle: Path) -> str:
216
248
  """The freshness gate, paste-able into an existing contract — never applied for you."""
217
249
  root = os.path.relpath(bundle, contract.parent)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brainpick
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: A turn-key brain stack for agents: compile an OKF markdown bundle into a queryable, servable, visualizable brain.
5
5
  Author: Benquemax
6
6
  License: MIT
@@ -107,7 +107,7 @@ def test_sse_frame_format():
107
107
  def test_health(kotiaurinko):
108
108
  with TestClient(make_app(kotiaurinko)) as client:
109
109
  assert client.get("/api/health").json() == {
110
- "impl": "python", "name": "brainpick", "spec_version": "0.1", "version": "0.3.2",
110
+ "impl": "python", "name": "brainpick", "spec_version": "0.1", "version": "0.4.0",
111
111
  }
112
112
 
113
113
 
@@ -53,10 +53,25 @@ def test_integrate_opencode_writes_skill_under_its_convention(repo, capsys):
53
53
  assert "opencode" in capsys.readouterr().out.lower()
54
54
 
55
55
 
56
+ def test_integrate_dsh_writes_skill_and_prints_cordis_insert(repo, capsys):
57
+ root, bundle = repo
58
+ assert run_integrate("dsh", bundle) == 0
59
+ skill = root / SKILL_DESTINATIONS["dsh"]
60
+ assert skill.read_text(encoding="utf-8") == CANONICAL.read_text(encoding="utf-8")
61
+ out = capsys.readouterr().out
62
+ assert "@deepseek-ai/dsh-mcp-client" in out # the mcp-client bundle
63
+ assert "cordis.patch.yml" in out # where the row goes
64
+ assert "serverName: brainpick" in out
65
+ assert str(bundle) in out # resolved absolute root, not a stale path
66
+ assert "claude mcp add" not in out # dsh has no such command
67
+
68
+
56
69
  def test_integrate_dry_run_is_inert(repo, capsys):
57
70
  root, bundle = repo
58
71
  assert run_integrate("claude-code", bundle, dry_run=True) == 0
59
72
  assert not (root / ".claude").exists()
73
+ assert run_integrate("dsh", bundle, dry_run=True) == 0
74
+ assert not (root / ".claude").exists()
60
75
  assert run_integrate("agents-md", bundle, dry_run=True) == 0
61
76
  assert not (root / "AGENTS.md").exists()
62
77
 
@@ -102,6 +102,22 @@ def test_search_hits_have_why_not_bodies(kotiaurinko):
102
102
  assert result["truncated"] is False
103
103
 
104
104
 
105
+ def test_search_why_names_matched_terms_not_whole_query(kotiaurinko):
106
+ """issue #2: a multi-word query must not claim the WHOLE query appears in a
107
+ doc that only matched some of its terms. The 'why' names the tokens that
108
+ actually occur, never a term (like 'banana') that appears nowhere."""
109
+ result = search_payload(make_state(kotiaurinko), "aurinko komeetta banana")
110
+ assert result["hits"], "expected keyword hits for a partially-matching query"
111
+ for hit in result["hits"]:
112
+ why = hit["why"]
113
+ # never echo the full query, and never claim a non-existent term matched
114
+ assert "aurinko komeetta banana" not in why
115
+ assert "banana" not in why
116
+ # a keyword-ish 'mentions' reason must quote the actual matched token(s)
117
+ if "mentions" in why:
118
+ assert "aurinko" in why or "komeetta" in why
119
+
120
+
105
121
  def test_search_budget_trims_hits(kotiaurinko):
106
122
  state = make_state(kotiaurinko)
107
123
  full = search_payload(state, "aurinko")
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