memgres 0.3.0__tar.gz → 0.3.2__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 (49) hide show
  1. {memgres-0.3.0 → memgres-0.3.2}/PKG-INFO +1 -1
  2. {memgres-0.3.0 → memgres-0.3.2}/memgres/diffing.py +11 -0
  3. {memgres-0.3.0 → memgres-0.3.2}/memgres/mcp_server.py +4 -1
  4. {memgres-0.3.0 → memgres-0.3.2}/memgres/store.py +6 -1
  5. {memgres-0.3.0 → memgres-0.3.2}/memgres.egg-info/PKG-INFO +1 -1
  6. {memgres-0.3.0 → memgres-0.3.2}/pyproject.toml +1 -1
  7. {memgres-0.3.0 → memgres-0.3.2}/tests/test_diffing.py +23 -0
  8. {memgres-0.3.0 → memgres-0.3.2}/tests/test_store_integration.py +16 -1
  9. {memgres-0.3.0 → memgres-0.3.2}/LICENSE +0 -0
  10. {memgres-0.3.0 → memgres-0.3.2}/README.md +0 -0
  11. {memgres-0.3.0 → memgres-0.3.2}/memgres/__init__.py +0 -0
  12. {memgres-0.3.0 → memgres-0.3.2}/memgres/blame.py +0 -0
  13. {memgres-0.3.0 → memgres-0.3.2}/memgres/config.py +0 -0
  14. {memgres-0.3.0 → memgres-0.3.2}/memgres/embeddings.py +0 -0
  15. {memgres-0.3.0 → memgres-0.3.2}/memgres/identity.py +0 -0
  16. {memgres-0.3.0 → memgres-0.3.2}/memgres/info.py +0 -0
  17. {memgres-0.3.0 → memgres-0.3.2}/memgres/migrations/0001_core.sql +0 -0
  18. {memgres-0.3.0 → memgres-0.3.2}/memgres/migrations/0002_identity.sql +0 -0
  19. {memgres-0.3.0 → memgres-0.3.2}/memgres/schema.py +0 -0
  20. {memgres-0.3.0 → memgres-0.3.2}/memgres/search.py +0 -0
  21. {memgres-0.3.0 → memgres-0.3.2}/memgres/segments.py +0 -0
  22. {memgres-0.3.0 → memgres-0.3.2}/memgres/server.py +0 -0
  23. {memgres-0.3.0 → memgres-0.3.2}/memgres/vector/__init__.py +0 -0
  24. {memgres-0.3.0 → memgres-0.3.2}/memgres/vector/base.py +0 -0
  25. {memgres-0.3.0 → memgres-0.3.2}/memgres/vector/pgvector.py +0 -0
  26. {memgres-0.3.0 → memgres-0.3.2}/memgres/vector/qdrant.py +0 -0
  27. {memgres-0.3.0 → memgres-0.3.2}/memgres.egg-info/SOURCES.txt +0 -0
  28. {memgres-0.3.0 → memgres-0.3.2}/memgres.egg-info/dependency_links.txt +0 -0
  29. {memgres-0.3.0 → memgres-0.3.2}/memgres.egg-info/entry_points.txt +0 -0
  30. {memgres-0.3.0 → memgres-0.3.2}/memgres.egg-info/requires.txt +0 -0
  31. {memgres-0.3.0 → memgres-0.3.2}/memgres.egg-info/top_level.txt +0 -0
  32. {memgres-0.3.0 → memgres-0.3.2}/setup.cfg +0 -0
  33. {memgres-0.3.0 → memgres-0.3.2}/tests/test_blame_integration.py +0 -0
  34. {memgres-0.3.0 → memgres-0.3.2}/tests/test_config.py +0 -0
  35. {memgres-0.3.0 → memgres-0.3.2}/tests/test_embeddings.py +0 -0
  36. {memgres-0.3.0 → memgres-0.3.2}/tests/test_identity_integration.py +0 -0
  37. {memgres-0.3.0 → memgres-0.3.2}/tests/test_lexical_match.py +0 -0
  38. {memgres-0.3.0 → memgres-0.3.2}/tests/test_limits.py +0 -0
  39. {memgres-0.3.0 → memgres-0.3.2}/tests/test_list.py +0 -0
  40. {memgres-0.3.0 → memgres-0.3.2}/tests/test_mcp_recall_schema.py +0 -0
  41. {memgres-0.3.0 → memgres-0.3.2}/tests/test_qdrant_ca.py +0 -0
  42. {memgres-0.3.0 → memgres-0.3.2}/tests/test_qdrant_integration.py +0 -0
  43. {memgres-0.3.0 → memgres-0.3.2}/tests/test_search_integration.py +0 -0
  44. {memgres-0.3.0 → memgres-0.3.2}/tests/test_security_integration.py +0 -0
  45. {memgres-0.3.0 → memgres-0.3.2}/tests/test_segments.py +0 -0
  46. {memgres-0.3.0 → memgres-0.3.2}/tests/test_segments_store.py +0 -0
  47. {memgres-0.3.0 → memgres-0.3.2}/tests/test_server_info.py +0 -0
  48. {memgres-0.3.0 → memgres-0.3.2}/tests/test_server_integration.py +0 -0
  49. {memgres-0.3.0 → memgres-0.3.2}/tests/test_snippets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memgres
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Drop-in memory for AI agents: one Postgres, lexical + semantic recall, diff-versioned history, GDPR-erasable.
5
5
  Author: mozgsml
6
6
  License-Expression: MIT
@@ -69,6 +69,7 @@ def apply_diff(old: str, patch: str) -> str:
69
69
  lines = patch.split("\n")
70
70
  n = len(lines)
71
71
  p = 0
72
+ hunks = 0 # guard: a non-empty patch must apply ≥1 hunk
72
73
 
73
74
  def marker_next(idx: int) -> bool:
74
75
  return idx + 1 < n and lines[idx + 1].startswith("\\ No newline")
@@ -80,8 +81,13 @@ def apply_diff(old: str, patch: str) -> str:
80
81
  continue
81
82
  m = _HUNK.match(line)
82
83
  if not m:
84
+ # A `@@`-line that isn't a valid header is a malformed hunk, not a
85
+ # skippable stray — reject it rather than silently dropping the edit.
86
+ if line.startswith("@@"):
87
+ raise DiffConflict(f"malformed hunk header: {line!r}")
83
88
  p += 1
84
89
  continue
90
+ hunks += 1
85
91
 
86
92
  old_start = int(m.group(1))
87
93
  # @@ lines are 1-based; a hunk adding to an empty side uses -0 → 0.
@@ -114,6 +120,11 @@ def apply_diff(old: str, patch: str) -> str:
114
120
  p += 1
115
121
 
116
122
  out.extend(src[i:])
123
+ # A non-empty patch that matched no hunk header applied nothing — that is a
124
+ # malformed / non-unified-diff payload, not a legitimate no-op. Fail loudly
125
+ # instead of silently returning the body unchanged.
126
+ if hunks == 0:
127
+ raise DiffConflict("patch contains no @@ hunk — malformed or not a unified diff")
117
128
  return "".join(out)
118
129
 
119
130
 
@@ -27,7 +27,10 @@ from __future__ import annotations
27
27
 
28
28
  from typing import List, Literal, Optional
29
29
 
30
- from mcp.server.fastmcp import Context
30
+ try: # mcp SDK >= 2.0 renamed the module fastmcp -> mcpserver
31
+ from mcp.server.mcpserver import Context
32
+ except ImportError: # mcp SDK 1.x
33
+ from mcp.server.fastmcp import Context
31
34
 
32
35
  from . import identity
33
36
  from .config import Config, load
@@ -24,7 +24,7 @@ import psycopg
24
24
 
25
25
  from . import identity
26
26
  from .config import Config
27
- from .diffing import apply_diff, byte_len, content_hash, make_diff
27
+ from .diffing import DiffConflict, apply_diff, byte_len, content_hash, make_diff
28
28
  from .embeddings import Embedder, get_embedder
29
29
  from .vector import make_backend
30
30
 
@@ -221,6 +221,11 @@ class Store:
221
221
  raise Conflict(f"stale diff: base {base_hash[:12]} != current {cur_hash[:12]}")
222
222
  self._check_write_size(diff)
223
223
  new_body = apply_diff(cur_body, diff)
224
+ # A diff that leaves the body identical applied nothing meaningful —
225
+ # never bump seq on a silent no-op (belt to apply_diff's malformed
226
+ # guard: also catches an empty or net-zero diff).
227
+ if new_body == cur_body:
228
+ raise DiffConflict("diff applied but changed nothing — empty or no-op diff")
224
229
  op = "diff"
225
230
  elif body is not None:
226
231
  if base_hash is not None and base_hash != cur_hash:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memgres
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Drop-in memory for AI agents: one Postgres, lexical + semantic recall, diff-versioned history, GDPR-erasable.
5
5
  Author: mozgsml
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "memgres"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "Drop-in memory for AI agents: one Postgres, lexical + semantic recall, diff-versioned history, GDPR-erasable."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -53,6 +53,29 @@ def test_apply_to_changed_base_raises():
53
53
  apply_diff(drifted, patch)
54
54
 
55
55
 
56
+ # ─── malformed patches must fail loudly, never silently no-op ────────────
57
+ def test_malformed_hunk_header_raises():
58
+ # A bare `@@` (no `-a,b +c,d` line spec) is not a valid hunk header. It used
59
+ # to be silently skipped, so the whole patch applied nothing and the body
60
+ # came back unchanged — a silent no-op. Now it raises.
61
+ bad = "@@\n-old line\n+new line\n"
62
+ with pytest.raises(DiffConflict):
63
+ apply_diff("old line\n", bad)
64
+
65
+
66
+ def test_patch_without_any_hunk_raises():
67
+ # Non-empty payload that contains no @@ hunk at all applied nothing → raise,
68
+ # rather than returning the source body untouched.
69
+ with pytest.raises(DiffConflict):
70
+ apply_diff("body\n", "-body\n+new\n")
71
+
72
+
73
+ def test_empty_patch_is_still_a_noop():
74
+ # A genuinely empty patch is the one legitimate no-op (unchanged behavior).
75
+ assert apply_diff("keep\n", "") == "keep\n"
76
+ assert apply_diff("keep\n", " \n") == "keep\n"
77
+
78
+
56
79
  # ─── byte length is measured in UTF-8, not characters ────────────────────
57
80
  def test_byte_len_utf8():
58
81
  assert byte_len("abc") == 3
@@ -14,7 +14,7 @@ import pytest # noqa: E402
14
14
  psycopg = pytest.importorskip("psycopg")
15
15
 
16
16
  from memgres.config import load # noqa: E402
17
- from memgres.diffing import make_diff, content_hash # noqa: E402
17
+ from memgres.diffing import make_diff, content_hash, DiffConflict # noqa: E402
18
18
  from memgres.schema import migrate # noqa: E402
19
19
  from memgres.store import Store, Conflict, NotFound, TooLarge, NoParent # noqa: E402
20
20
 
@@ -80,6 +80,21 @@ def test_diff_requires_base_hash(store):
80
80
  store.write(id=m.id, diff=make_diff("x\n", "y\n"))
81
81
 
82
82
 
83
+ def test_malformed_diff_raises_and_does_not_bump_seq(store):
84
+ # Regression (diff_silently_noops): a diff whose hunk header doesn't parse
85
+ # used to silently apply nothing — body unchanged but seq++ / updated_at
86
+ # moved, looking like a successful write. Now it raises and touches nothing.
87
+ m = store.write(body="alpha\nbeta\n")
88
+ assert m.seq == 1
89
+ bad = "@@\n-alpha\n+ALPHA\n" # bare @@ — not a valid unified-diff hunk
90
+ with pytest.raises(DiffConflict):
91
+ store.write(id=m.id, diff=bad, base_hash=m.content_hash)
92
+ again = store.get(None, m.id)
93
+ assert again.body == "alpha\nbeta\n" # body untouched
94
+ assert again.content_hash == m.content_hash # hash unchanged
95
+ assert again.seq == 1 # seq NOT bumped
96
+
97
+
83
98
  def test_replace_and_retag(store):
84
99
  m = store.write(body="body\n", tags=["old"])
85
100
  m2 = store.write(id=m.id, body="new body\n", tags=["new"], reason="replace")
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