memstack-skill-loader 4.13.0__tar.gz → 4.13.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 (62) hide show
  1. {memstack_skill_loader-4.13.0/src/memstack_skill_loader.egg-info → memstack_skill_loader-4.13.1}/PKG-INFO +1 -1
  2. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/pyproject.toml +1 -1
  3. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/__init__.py +1 -1
  4. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/diary_ingest.py +22 -2
  5. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1/src/memstack_skill_loader.egg-info}/PKG-INFO +1 -1
  6. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/tests/test_diary_ingest.py +69 -1
  7. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/MANIFEST.in +0 -0
  8. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/README.md +0 -0
  9. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/setup.cfg +0 -0
  10. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/__main__.py +0 -0
  11. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/agent_runner.py +0 -0
  12. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/categories.py +0 -0
  13. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/compression.py +0 -0
  14. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/config.py +0 -0
  15. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/dashboard.html +0 -0
  16. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/dashboard.py +0 -0
  17. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/goal.py +0 -0
  18. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/goal_engine.py +0 -0
  19. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/hooks/__init__.py +0 -0
  20. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/hooks/deny_git.py +0 -0
  21. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/hooks/deny_manager_write.py +0 -0
  22. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/indexer.py +0 -0
  23. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/license.py +0 -0
  24. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/memory_db.py +0 -0
  25. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/memory_engine.py +0 -0
  26. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/proxy/__init__.py +0 -0
  27. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/proxy/_diag.py +0 -0
  28. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/proxy/body_parser.py +0 -0
  29. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/proxy/compressor.py +0 -0
  30. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/proxy/forwarder.py +0 -0
  31. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/proxy/pro_compressor.py +0 -0
  32. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/proxy/server.py +0 -0
  33. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/proxy/stats_tracker.py +0 -0
  34. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/search.py +0 -0
  35. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/server.py +0 -0
  36. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/session_context.py +0 -0
  37. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/skill_config.py +0 -0
  38. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/skill_descriptions.json +0 -0
  39. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/stats.py +0 -0
  40. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/tfidf_search.py +0 -0
  41. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader/version_check.py +0 -0
  42. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader.egg-info/SOURCES.txt +0 -0
  43. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader.egg-info/dependency_links.txt +0 -0
  44. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader.egg-info/entry_points.txt +0 -0
  45. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader.egg-info/requires.txt +0 -0
  46. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/src/memstack_skill_loader.egg-info/top_level.txt +0 -0
  47. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/tests/test_advisor_strategy.py +0 -0
  48. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/tests/test_body_parser_metric.py +0 -0
  49. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/tests/test_builder_scope.py +0 -0
  50. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/tests/test_code_read_savings.py +0 -0
  51. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/tests/test_failed_approaches.py +0 -0
  52. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/tests/test_license_environment.py +0 -0
  53. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/tests/test_license_grace.py +0 -0
  54. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/tests/test_memory_engine.py +0 -0
  55. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/tests/test_memory_inspector.py +0 -0
  56. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/tests/test_memory_scoring.py +0 -0
  57. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/tests/test_memory_tools.py +0 -0
  58. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/tests/test_pro_compressor.py +0 -0
  59. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/tests/test_pro_skills_update.py +0 -0
  60. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/tests/test_search_memory.py +0 -0
  61. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/tests/test_session_context.py +0 -0
  62. {memstack_skill_loader-4.13.0 → memstack_skill_loader-4.13.1}/tests/test_skill_drift.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memstack-skill-loader
3
- Version: 4.13.0
3
+ Version: 4.13.1
4
4
  Summary: MCP server that vector-indexes MemStack Pro skills for on-demand loading
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: mcp>=1.0.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memstack-skill-loader"
7
- version = "4.13.0"
7
+ version = "4.13.1"
8
8
  description = "MCP server that vector-indexes MemStack Pro skills for on-demand loading"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -1,3 +1,3 @@
1
1
  """MemStack Skill Loader — MCP server for semantic skill search."""
2
2
 
3
- __version__ = "4.13.0"
3
+ __version__ = "4.13.1"
@@ -7,7 +7,8 @@ module by path, exactly like the devlog-webhook:
7
7
  python -m memstack_skill_loader.diary_ingest "<markdown-backup-path>"
8
8
 
9
9
  Each fact line is stored through :class:`MemoryStore` with ``source_type='diary'``
10
- and ``source_ref`` set to the diary file path.
10
+ and ``source_ref`` set to the diary file path, canonicalized to an absolute
11
+ forward-slash path so the same file re-ingested from any cwd/arg form dedupes.
11
12
 
12
13
  FAIL-OPEN CONTRACT (do not "improve" away)
13
14
  ------------------------------------------
@@ -141,6 +142,25 @@ def _project_from_path(path: str) -> str:
141
142
  return slug or "default"
142
143
 
143
144
 
145
+ def _canonical_source_ref(path: str) -> str:
146
+ """Canonicalize a diary path into a stable dedupe key.
147
+
148
+ The Memory Engine dedupe key is ``(source_ref, subject, claim)`` compared as
149
+ raw strings. A diary saved once via a relative path and once via an absolute
150
+ path therefore produced two different ``source_ref`` strings and re-stored
151
+ every fact as a duplicate live row. Resolving to a real absolute path
152
+ (collapsing ``..`` segments and following symlinks) and emitting it with
153
+ forward slashes makes every path form of the same file collapse to one key.
154
+
155
+ Fail-open: this runs in the never-break-the-diary-save path, so if resolution
156
+ fails for any reason, fall back to the raw string rather than raise.
157
+ """
158
+ try:
159
+ return Path(path).resolve().as_posix()
160
+ except OSError:
161
+ return str(path)
162
+
163
+
144
164
  def _warn(message: str) -> None:
145
165
  print(f"{_STDERR_PREFIX} {message}", file=sys.stderr)
146
166
 
@@ -169,7 +189,7 @@ def ingest_diary_file(path: str, store: MemoryStore | None = None) -> dict:
169
189
  if store is None:
170
190
  store = MemoryStore()
171
191
  project = _project_from_path(path)
172
- source_ref = str(path)
192
+ source_ref = _canonical_source_ref(path)
173
193
 
174
194
  ingested = 0
175
195
  duplicates = 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memstack-skill-loader
3
- Version: 4.13.0
3
+ Version: 4.13.1
4
4
  Summary: MCP server that vector-indexes MemStack Pro skills for on-demand loading
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: mcp>=1.0.0
@@ -116,9 +116,13 @@ def test_ingest_stores_facts_with_diary_provenance(tmp_path):
116
116
  store = MemoryStore()
117
117
  rows = store.list_facts(project="demoproj", limit=50)
118
118
  assert len(rows) == 4
119
+ # source_ref is stored in canonical form: an absolute, forward-slash path
120
+ # (resolve + as_posix), so re-ingests from any cwd/arg form dedupe.
121
+ expected_ref = Path(p).resolve().as_posix()
119
122
  for r in rows:
120
123
  assert r["source_type"] == "diary"
121
- assert r["source_ref"] == str(p)
124
+ assert r["source_ref"] == expected_ref
125
+ assert "\\" not in r["source_ref"]
122
126
  # entities landed
123
127
  supa = [r for r in rows if r["subject"] == "adminstack.portal.auth"][0]
124
128
  assert set(store.get_entities(supa["id"])) == {"supabase", "auth"}
@@ -198,6 +202,70 @@ def test_fact_exists_dedupe_key(tmp_path):
198
202
  assert store.fact_exists("s.x", "the claim", "/other.md") is False
199
203
 
200
204
 
205
+ # ---------------------------------------------------------------------------
206
+ # source_ref canonicalization: the SAME file ingested via different path forms
207
+ # (relative, absolute, ../-laden, symlinked) must dedupe to ONE fact. Regression
208
+ # guard for the relative-vs-absolute duplicate-fact gap (4 dupes in the real
209
+ # store on 2026-07-23). Genuinely different files must still stay separate.
210
+ # ---------------------------------------------------------------------------
211
+
212
+ def test_canonical_source_ref_is_absolute_forward_slash(tmp_path, monkeypatch):
213
+ p = _diary(tmp_path, "placeholder")
214
+ monkeypatch.chdir(tmp_path)
215
+ canon = diary_ingest._canonical_source_ref(p.name) # relative input
216
+ assert canon == Path(p).resolve().as_posix()
217
+ assert Path(canon).is_absolute()
218
+ assert "\\" not in canon # forward-slash only
219
+
220
+
221
+ def test_relative_and_absolute_path_dedupe_to_one_fact(tmp_path, monkeypatch):
222
+ p = _diary(tmp_path, CANONICAL_EXAMPLES)
223
+ monkeypatch.chdir(tmp_path)
224
+ first = diary_ingest.ingest_diary_file(p.name) # relative path
225
+ second = diary_ingest.ingest_diary_file(str(p.resolve())) # absolute path
226
+ assert first["ingested"] == 4
227
+ assert second["ingested"] == 0
228
+ assert second["duplicates"] == 4
229
+ assert MemoryStore().stats(project="demoproj")["total"] == 4
230
+
231
+
232
+ def test_dotdot_segments_canonicalize_to_same_fact(tmp_path):
233
+ p = _diary(tmp_path, CANONICAL_EXAMPLES)
234
+ sub = tmp_path / "sub"
235
+ sub.mkdir()
236
+ detour = sub / ".." / p.name # resolves back to the same file
237
+ first = diary_ingest.ingest_diary_file(str(p))
238
+ second = diary_ingest.ingest_diary_file(str(detour))
239
+ assert first["ingested"] == 4
240
+ assert second["duplicates"] == 4
241
+ assert MemoryStore().stats(project="demoproj")["total"] == 4
242
+
243
+
244
+ def test_symlinked_path_canonicalizes_to_same_fact(tmp_path):
245
+ p = _diary(tmp_path, CANONICAL_EXAMPLES)
246
+ link = tmp_path / "2026-07-20-demoproj-link.md"
247
+ try:
248
+ link.symlink_to(p)
249
+ except (OSError, NotImplementedError):
250
+ pytest.skip("symlink creation not permitted on this platform")
251
+ first = diary_ingest.ingest_diary_file(str(p))
252
+ second = diary_ingest.ingest_diary_file(str(link)) # follows to real path
253
+ assert first["ingested"] == 4
254
+ assert second["duplicates"] == 4
255
+ assert MemoryStore().stats(project="demoproj")["total"] == 4
256
+
257
+
258
+ def test_two_different_diaries_store_as_separate_facts(tmp_path):
259
+ p1 = _diary(tmp_path, CANONICAL_EXAMPLES, name="2026-07-20-demoproj.md")
260
+ p2 = _diary(tmp_path, CANONICAL_EXAMPLES, name="2026-07-21-demoproj.md")
261
+ a = diary_ingest.ingest_diary_file(str(p1))
262
+ b = diary_ingest.ingest_diary_file(str(p2))
263
+ assert a["ingested"] == 4
264
+ assert b["ingested"] == 4 # different source_ref => not dupes
265
+ assert b["duplicates"] == 0
266
+ assert MemoryStore().stats(project="demoproj")["total"] == 8
267
+
268
+
201
269
  # ---------------------------------------------------------------------------
202
270
  # cross-repo drift guard: the diary SKILL.md examples must parse to exactly the
203
271
  # facts CANONICAL_EXAMPLES parses to. The skill lives in a SIBLING repo