seren-margin 2.3.0__tar.gz → 2.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 (46) hide show
  1. {seren_margin-2.3.0/seren_margin.egg-info → seren_margin-2.4.0}/PKG-INFO +1 -1
  2. {seren_margin-2.3.0 → seren_margin-2.4.0}/seren_margin/_version.py +3 -3
  3. {seren_margin-2.3.0 → seren_margin-2.4.0}/seren_margin/app.py +13 -1
  4. {seren_margin-2.3.0 → seren_margin-2.4.0/seren_margin.egg-info}/PKG-INFO +1 -1
  5. seren_margin-2.4.0/seren_margin.egg-info/scm_version.json +8 -0
  6. {seren_margin-2.3.0 → seren_margin-2.4.0}/tests/test_stores_snapshot.py +18 -0
  7. seren_margin-2.3.0/seren_margin.egg-info/scm_version.json +0 -8
  8. {seren_margin-2.3.0 → seren_margin-2.4.0}/LICENSE +0 -0
  9. {seren_margin-2.3.0 → seren_margin-2.4.0}/SerenMargin.pyproj +0 -0
  10. {seren_margin-2.3.0 → seren_margin-2.4.0}/pyproject.toml +0 -0
  11. {seren_margin-2.3.0 → seren_margin-2.4.0}/seren-margin.service.sample +0 -0
  12. {seren_margin-2.3.0 → seren_margin-2.4.0}/seren-margin.yaml.sample +0 -0
  13. {seren_margin-2.3.0 → seren_margin-2.4.0}/seren_margin/__init__.py +0 -0
  14. {seren_margin-2.3.0 → seren_margin-2.4.0}/seren_margin/__main__.py +0 -0
  15. {seren_margin-2.3.0 → seren_margin-2.4.0}/seren_margin/_diag.py +0 -0
  16. {seren_margin-2.3.0 → seren_margin-2.4.0}/seren_margin/bookmark.py +0 -0
  17. {seren_margin-2.3.0 → seren_margin-2.4.0}/seren_margin/config.py +0 -0
  18. {seren_margin-2.3.0 → seren_margin-2.4.0}/seren_margin/mcp/__init__.py +0 -0
  19. {seren_margin-2.3.0 → seren_margin-2.4.0}/seren_margin/mcp/server.py +0 -0
  20. {seren_margin-2.3.0 → seren_margin-2.4.0}/seren_margin/mcp/tools.py +0 -0
  21. {seren_margin-2.3.0 → seren_margin-2.4.0}/seren_margin/mcp-manifest.yaml +0 -0
  22. {seren_margin-2.3.0 → seren_margin-2.4.0}/seren_margin/models.py +0 -0
  23. {seren_margin-2.3.0 → seren_margin-2.4.0}/seren_margin/store.py +0 -0
  24. {seren_margin-2.3.0 → seren_margin-2.4.0}/seren_margin.egg-info/SOURCES.txt +0 -0
  25. {seren_margin-2.3.0 → seren_margin-2.4.0}/seren_margin.egg-info/dependency_links.txt +0 -0
  26. {seren_margin-2.3.0 → seren_margin-2.4.0}/seren_margin.egg-info/entry_points.txt +0 -0
  27. {seren_margin-2.3.0 → seren_margin-2.4.0}/seren_margin.egg-info/requires.txt +0 -0
  28. {seren_margin-2.3.0 → seren_margin-2.4.0}/seren_margin.egg-info/scm_file_list.json +0 -0
  29. {seren_margin-2.3.0 → seren_margin-2.4.0}/seren_margin.egg-info/top_level.txt +0 -0
  30. {seren_margin-2.3.0 → seren_margin-2.4.0}/setup.cfg +0 -0
  31. {seren_margin-2.3.0 → seren_margin-2.4.0}/tests/__init__.py +0 -0
  32. {seren_margin-2.3.0 → seren_margin-2.4.0}/tests/conftest.py +0 -0
  33. {seren_margin-2.3.0 → seren_margin-2.4.0}/tests/test_amend_and_topics.py +0 -0
  34. {seren_margin-2.3.0 → seren_margin-2.4.0}/tests/test_bookmark_and_letters.py +0 -0
  35. {seren_margin-2.3.0 → seren_margin-2.4.0}/tests/test_config.py +0 -0
  36. {seren_margin-2.3.0 → seren_margin-2.4.0}/tests/test_exposure_is_wired.py +0 -0
  37. {seren_margin-2.3.0 → seren_margin-2.4.0}/tests/test_manifest_parity.py +0 -0
  38. {seren_margin-2.3.0 → seren_margin-2.4.0}/tests/test_mcp_fallback.py +0 -0
  39. {seren_margin-2.3.0 → seren_margin-2.4.0}/tests/test_mcp_mount.py +0 -0
  40. {seren_margin-2.3.0 → seren_margin-2.4.0}/tests/test_mcp_tools.py +0 -0
  41. {seren_margin-2.3.0 → seren_margin-2.4.0}/tests/test_optional_bearer.py +0 -0
  42. {seren_margin-2.3.0 → seren_margin-2.4.0}/tests/test_reads_stay_private.py +0 -0
  43. {seren_margin-2.3.0 → seren_margin-2.4.0}/tests/test_search.py +0 -0
  44. {seren_margin-2.3.0 → seren_margin-2.4.0}/tests/test_smoke.py +0 -0
  45. {seren_margin-2.3.0 → seren_margin-2.4.0}/tests/test_stats_content_blind.py +0 -0
  46. {seren_margin-2.3.0 → seren_margin-2.4.0}/tests/test_updates_contract.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: seren-margin
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: Private notes-to-self for an AI assistant. Standalone, opt-in, opinionated.
5
5
  Author: Chad Roesler
6
6
  License-Expression: AGPL-3.0-or-later
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.3.0'
22
- __version_tuple__ = version_tuple = (2, 3, 0)
21
+ __version__ = version = '2.4.0'
22
+ __version_tuple__ = version_tuple = (2, 4, 0)
23
23
 
24
- __commit_id__ = commit_id = 'g3339c1e00'
24
+ __commit_id__ = commit_id = 'g3a8586044'
@@ -230,11 +230,23 @@ def create_app(config: Optional[MarginConfig] = None) -> FastAPI:
230
230
  notes = None
231
231
  return {"version": APP_VERSION, "counts": {"notes": notes}}
232
232
 
233
+ def _margin_check(restored, manifest, snapshot_dir) -> dict:
234
+ """A rehearsal's look at a restored COPY: opened as a MarginStore
235
+ (schema and migrations run on the copy) and counted. A count and
236
+ nothing else - a rehearsal's report must not say what a note says."""
237
+ import gc
238
+ st = MarginStore(restored["notes"] / cfg.resolved_db_path().name)
239
+ try:
240
+ return {"counts": {"notes": len(st.list_all(limit=1_000_000))}}
241
+ finally:
242
+ del st
243
+ gc.collect() # its connections, so the scratch copy can be removed
244
+
233
245
  app.state.stores = StoreKeeper(
234
246
  "seren-margin",
235
247
  lambda: [Store("notes", "sqlite", str(cfg.resolved_db_path()),
236
248
  "the margin: private notes, letters and the dedication")],
237
- cfg.resolved_backup_dir(), extra=_margin_extra,
249
+ cfg.resolved_backup_dir(), extra=_margin_extra, check=_margin_check,
238
250
  keep_daily=cfg.backup.keep_daily, keep_weekly=cfg.backup.keep_weekly,
239
251
  log=lambda m: diag(f"[seren-margin] {m}")) if cfg.backup.enabled else None
240
252
  add_store_routes(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: seren-margin
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: Private notes-to-self for an AI assistant. Standalone, opt-in, opinionated.
5
5
  Author: Chad Roesler
6
6
  License-Expression: AGPL-3.0-or-later
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.4.0",
3
+ "distance": 0,
4
+ "node": "g3a8586044f8f6d20e80e3232ee20f060a5a39f16",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-04"
8
+ }
@@ -81,3 +81,21 @@ def test_off_and_no_restore_or_delete(tmp_path):
81
81
  sid = c.post("/stores/snapshot").json()["snapshot"]["id"]
82
82
  for path in ("/stores/restore", f"/stores/snapshots/{sid}"):
83
83
  assert c.post(path).status_code in (404, 405) and c.delete(path).status_code in (404, 405)
84
+
85
+
86
+ def test_a_rehearsal_counts_the_notes_and_says_nothing_of_them(tmp_path):
87
+ """A restore's dry run (seren_sinew.stores). It needs no allow_pull: the
88
+ copy never leaves the box and the report is a count."""
89
+ from seren_sinew.stores import pack_snapshot
90
+ with _client(tmp_path) as c:
91
+ c.post("/notes", json={"content": SECRET, "topic": "private"})
92
+ snap = c.post("/stores/snapshot").json()["snapshot"]
93
+ c.post("/notes", json={"content": "written after", "topic": "later"})
94
+ r = c.post(f"/stores/snapshots/{snap['id']}/rehearse")
95
+ assert r.status_code == 200 and SECRET not in r.text, r.text
96
+ rep = r.json()
97
+ assert rep["ok"] and rep["check"] == {"counts": {"notes": 1}} and rep["live_store_touched"] is False, rep
98
+ assert len(c.app.state.store.list_all()) == 2
99
+ r = c.post("/stores/rehearse", content=pack_snapshot(Path(snap["path"])))
100
+ assert r.status_code == 200 and r.json()["ok"] and SECRET not in r.text, "a stash can send it back to be checked"
101
+ assert not list((Path(snap["path"]).parent / ".rehearsal").iterdir())
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "2.3.0",
3
- "distance": 0,
4
- "node": "g3339c1e00691cab41191c0b5903aa93cb1df799b",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-10-03"
8
- }
File without changes
File without changes