memtrust-cli 0.3.0__py3-none-any.whl

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 (42) hide show
  1. memtrust/__init__.py +11 -0
  2. memtrust/adapters/__init__.py +62 -0
  3. memtrust/adapters/base.py +2020 -0
  4. memtrust/adapters/mem0_adapter.py +456 -0
  5. memtrust/adapters/mem0_direct_adapter.py +1217 -0
  6. memtrust/adapters/mempalace_adapter.py +1166 -0
  7. memtrust/adapters/openviking_adapter.py +570 -0
  8. memtrust/adapters/zep_graphiti_adapter.py +181 -0
  9. memtrust/adapters/zep_graphiti_selfhosted_adapter.py +882 -0
  10. memtrust/cli.py +1201 -0
  11. memtrust/evals/__init__.py +5 -0
  12. memtrust/evals/compression.py +235 -0
  13. memtrust/evals/contradiction.py +451 -0
  14. memtrust/evals/crash_recovery.py +290 -0
  15. memtrust/evals/embedder_cost.py +163 -0
  16. memtrust/evals/embedding_drift.py +273 -0
  17. memtrust/evals/episode_temporal_leak.py +182 -0
  18. memtrust/evals/extraction_quality.py +373 -0
  19. memtrust/evals/filter_injection.py +371 -0
  20. memtrust/evals/language_degradation.py +189 -0
  21. memtrust/evals/lock_contention.py +263 -0
  22. memtrust/evals/locomo.py +392 -0
  23. memtrust/evals/longmemeval.py +249 -0
  24. memtrust/evals/mempalace_metadata_scale.py +494 -0
  25. memtrust/evals/migration_rollback.py +276 -0
  26. memtrust/evals/orphan_cleanup.py +235 -0
  27. memtrust/evals/ranking_quality.py +303 -0
  28. memtrust/evals/resource_sync_safety.py +336 -0
  29. memtrust/evals/result_consistency.py +239 -0
  30. memtrust/evals/scale_fixtures.py +189 -0
  31. memtrust/evals/scale_stress.py +502 -0
  32. memtrust/evals/stats_accuracy.py +240 -0
  33. memtrust/evals/temporal_kg_boundary.py +281 -0
  34. memtrust/receipt.py +318 -0
  35. memtrust/scoring/__init__.py +1 -0
  36. memtrust/scoring/cost_tracker.py +199 -0
  37. memtrust/scoring/llm_judge.py +161 -0
  38. memtrust_cli-0.3.0.dist-info/METADATA +624 -0
  39. memtrust_cli-0.3.0.dist-info/RECORD +42 -0
  40. memtrust_cli-0.3.0.dist-info/WHEEL +4 -0
  41. memtrust_cli-0.3.0.dist-info/entry_points.txt +2 -0
  42. memtrust_cli-0.3.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,494 @@
1
+ """MemPalace MCP metadata-tool scale eval (mempalace_status/list_wings/
2
+ list_rooms).
3
+
4
+ MemPalace/mempalace#1871 (contributor alionar) found that the MCP
5
+ server's metadata/histogram-listing tools -- `mempalace_status`,
6
+ `mempalace_list_wings`, `mempalace_list_rooms` -- did a full-collection
7
+ scan on every call against a Qdrant-backed palace: O(N^2) against
8
+ repeated calls, hanging the MCP server at 158K+ drawers (fixed by
9
+ server-side Qdrant faceting; see PR#1871's diff for the merged fix).
10
+
11
+ Before this eval, evals/scale_stress.py was memtrust's only scale-oriented
12
+ coverage, and it only ever measures store()/query() latency -- it has no
13
+ path at all to the metadata/histogram-listing code path alionar's bug
14
+ lives in, because MemPalaceAdapter never invoked the MCP-server surface,
15
+ only the guessed remember()/recall()/invalidate() library concepts.
16
+
17
+ adapters/mempalace_adapter.py's module docstring ("MCP metadata-tool
18
+ coverage" section) documents what this build's investigation confirmed
19
+ against the real, installed `mempalace` package: `mempalace.mcp_server`
20
+ ships real, plain module-level functions (`tool_status()`,
21
+ `tool_list_wings()`, `tool_list_rooms(wing=None)`) that are the actual
22
+ implementation the MCP tool calls dispatch to, callable directly with no
23
+ MCP stdio/HTTP transport involved. `MemPalaceAdapter.metadata_overview()`/
24
+ `list_metadata_categories()`/`list_metadata_subcategories()` wrap those
25
+ three real functions -- this eval drives them at increasing checkpoint
26
+ sizes, the same "measure the real thing as a function of corpus size"
27
+ shape evals/scale_stress.py established for store()/query().
28
+
29
+ Two things this eval checks at every checkpoint, neither trusting "the
30
+ call didn't raise" as proof anything worked (same rule every other eval
31
+ in this package applies):
32
+
33
+ * Correctness: the wing/room counts `metadata_overview()`/
34
+ `list_metadata_categories()`/`list_metadata_subcategories()` report
35
+ must match the seeder's own ground-truth counts for however many
36
+ records have actually been seeded so far. A backend that responds
37
+ quickly but reports the wrong counts is not "working at scale" --
38
+ see MetadataScaleSignal.INCORRECT_COUNTS_AT_SCALE.
39
+ * Latency growth shape: per-call latency at the largest checkpoint is
40
+ compared against the smallest checkpoint's latency, scaled by how
41
+ much the record count itself grew. A latency ratio that outpaces the
42
+ record-count ratio by more than a generous superlinear margin is
43
+ flagged MetadataScaleSignal.SUPERLINEAR_LATENCY_GROWTH -- a
44
+ regression guard for the exact repeated-full-collection-scan shape
45
+ alionar's bug report describes, not a claim that this eval reproduces
46
+ that bug's specific 158K-drawer failure.
47
+
48
+ **Honest limitation, stated plainly (mirrors evals/scale_stress.py's own
49
+ "Honest limitation" section).** `build_chroma_metadata_seeder()` below
50
+ seeds a real, local, chromadb-backed palace via the real, installed
51
+ `mempalace` package (confirmed working: seeding 20,000 records and
52
+ re-querying `tool_status()`/`tool_list_wings()` against them, in this
53
+ build's own investigation, returned correct ground-truth wing/room counts
54
+ in well under 100ms). That genuinely exercises the real metadata-listing
55
+ code path this bug lives in, on the real chroma backend.
56
+
57
+ What it does NOT do is reproduce alionar's *exact* repro shape. Re-reading
58
+ PR#1871's own review thread (gemini-code-assist's review comment)
59
+ confirms the merged fix has two parts: (1) server-side Qdrant faceting,
60
+ replacing the O(N^2) client-side scan for the Qdrant backend specifically,
61
+ and (2) a secondary optimization to the *Chroma fallback path* in
62
+ `_fetch_all_metadata` (making it reuse already-fetched records). Neither
63
+ part is what this eval's live seeder exercises: `mempalace.mcp_server`'s
64
+ `_sqlite_taxonomy()` fast path (a separate, already-merged optimization,
65
+ #1748/#1379) intercepts every `tool_status()`/`tool_list_wings()`/
66
+ `tool_list_rooms()` call against a standard-layout chroma-backed palace
67
+ *before* `_fetch_all_metadata` is ever reached -- confirmed in this
68
+ build's own investigation (sub-100ms responses at N=20,000, and the
69
+ source comment on `_sqlite_taxonomy()` itself: "fast wing->room tally
70
+ straight from chroma.sqlite3... to signal the caller to fall back to the
71
+ ChromaDB client pagination path"). So the live scale run this eval
72
+ performs is real, but it currently measures the fast path staying fast at
73
+ scale (a legitimate, previously-uncovered regression guard), not the
74
+ `_fetch_all_metadata` full-collection-scan path PR#1871 actually patches.
75
+
76
+ Two things could not be reproduced live in this build's environment,
77
+ for two different reasons:
78
+
79
+ * PR#1871's fix is specifically for MemPalace's Qdrant backend
80
+ (`mempalace/backends/qdrant.py`), which is REST-only against a live
81
+ external Qdrant server (`http://localhost:6333` by default) with no
82
+ embedded/local-storage mode at all -- confirmed by reading that
83
+ backend's source. This build's environment has neither `docker` nor a
84
+ local Qdrant binary available (both checked and confirmed absent), so
85
+ a live, 158K-drawer, Qdrant-backed repro of alionar's exact bug could
86
+ not be run here. A contributor with a live Qdrant instance available
87
+ can point `MEMPALACE_BACKEND=qdrant` at one and run this eval
88
+ unchanged -- the adapter methods and this eval's checkpoint/
89
+ classification logic make no chroma-specific assumption.
90
+ * Even on the chroma backend, deliberately forcing `_sqlite_taxonomy()`
91
+ to fail/decline (so a run actually exercises `_fetch_all_metadata`
92
+ instead of short-circuiting past it) would require constructing a
93
+ non-standard palace layout or a corrupted sqlite index -- not
94
+ attempted here, since a deliberately-broken palace is a different,
95
+ narrower scenario than "this backend at real scale," and getting it
96
+ wrong risks a misleading result more than a missing one.
97
+
98
+ Both are the same class of environmental gap this repo already draws
99
+ honestly elsewhere (see docs/methodology.md and
100
+ evals/crash_recovery.py's process-lifecycle caveat): this eval's
101
+ checkpoint/classification harness and the adapter methods it drives are
102
+ real, working, and verified end-to-end against a real backend at real
103
+ scale -- what has not happened yet is pointing that real harness at the
104
+ one specific code path (Qdrant faceting, or a forced chroma fallback)
105
+ alionar's report and PR#1871's fix actually touch.
106
+ """
107
+
108
+ from __future__ import annotations
109
+
110
+ from dataclasses import dataclass, field
111
+ from enum import StrEnum
112
+ from typing import Protocol
113
+
114
+ from memtrust.adapters.base import BackendAPIError, MemoryBackendAdapter
115
+
116
+ #: Fast-by-default record count -- mirrors scale_stress.py's
117
+ #: DEFAULT_N_RECORDS reasoning: large enough to move past every bundled
118
+ #: fixture's single-digit scale, small enough to seed and check quickly
119
+ #: in CI against a real local chromadb-backed palace (seeding 20,000
120
+ #: records took ~3s in this build's own investigation).
121
+ DEFAULT_N_RECORDS = 2000
122
+
123
+ #: How many non-anchor top-level categories (MemPalace: wings) the
124
+ #: default seeder spreads records across. Kept small and fixed so every
125
+ #: checkpoint has a meaningful per-category count to check, rather than
126
+ #: scaling with n_records the way real palaces rarely would either.
127
+ DEFAULT_N_CATEGORIES = 8
128
+
129
+ #: Minimum latency ratio (largest checkpoint / smallest checkpoint)
130
+ #: before growth is even considered for the superlinear check below --
131
+ #: guards against noise on two calls that are both already sub-millisecond
132
+ #: (a healthy backend's status() call at N=5 and N=50 can easily differ by
133
+ #: 2-3x on pure measurement noise; that is not evidence of anything).
134
+ _MIN_LATENCY_RATIO_TO_FLAG = 3.0
135
+
136
+ #: Exponent applied to the record-count ratio when deciding whether an
137
+ #: observed latency ratio counts as "superlinear." A perfectly linear
138
+ #: scan scores exactly 1.0 here; the true O(N^2) shape alionar reported
139
+ #: (repeated full-collection re-scans) would score close to 2.0. 1.5 is a
140
+ #: deliberately generous middle threshold: high enough that ordinary
141
+ #: linear-scaling noise does not false-positive, low enough to still catch
142
+ #: a real quadratic-ish regression before it reaches production scale.
143
+ _SUPERLINEAR_EXPONENT = 1.5
144
+
145
+
146
+ class MetadataScaleSignal(StrEnum):
147
+ """How a backend's metadata-overview calls (status/list_categories/
148
+ list_subcategories) behaved as the corpus grew, per
149
+ run_mempalace_metadata_scale_eval()."""
150
+
151
+ WORKED_AT_SCALE = "worked_at_scale"
152
+ """Every checkpoint's reported counts matched ground truth, and
153
+ latency did not grow superlinearly relative to record count."""
154
+
155
+ INCORRECT_COUNTS_AT_SCALE = "incorrect_counts_at_scale"
156
+ """At least one checkpoint's reported category/subcategory counts
157
+ did not match the seeder's ground truth -- a correctness failure,
158
+ checked independently of and before any latency judgment."""
159
+
160
+ SUPERLINEAR_LATENCY_GROWTH = "superlinear_latency_growth"
161
+ """Counts were correct everywhere, but latency grew far faster than
162
+ the record count did between the smallest and largest checkpoint --
163
+ the regression shape alionar's MemPalace/mempalace#1871 report
164
+ describes (repeated full-collection scans behind the MCP metadata
165
+ tools)."""
166
+
167
+ ERROR = "error"
168
+ """A BackendAPIError was raised during the run that this eval could
169
+ not route around."""
170
+
171
+ NOT_APPLICABLE = "not_applicable"
172
+ """Fewer than 2 checkpoints produced a scoreable result (e.g.
173
+ n_records too small), or the adapter under test does not implement
174
+ this capability at all (see MetadataScaleResult.skipped)."""
175
+
176
+
177
+ @dataclass
178
+ class MetadataScaleCheckpointResult:
179
+ """A snapshot taken after `checkpoint_n` records had been seeded."""
180
+
181
+ checkpoint_n: int
182
+ overview_latency_ms: float | None
183
+ categories_latency_ms: float | None
184
+ subcategories_latency_ms: float | None
185
+ total_records_reported: int | None
186
+ categories_reported: dict[str, int]
187
+ categories_expected: dict[str, int]
188
+ subcategories_reported: dict[str, int]
189
+ subcategories_expected: dict[str, int]
190
+ counts_correct: bool
191
+ error: str | None = None
192
+
193
+
194
+ @dataclass
195
+ class MetadataScaleResult:
196
+ """Result of `run_mempalace_metadata_scale_eval()` for one backend,
197
+ one (n_records, seed) corpus."""
198
+
199
+ backend_name: str
200
+ n_records_requested: int
201
+ checkpoints: list[MetadataScaleCheckpointResult] = field(default_factory=list)
202
+ signal: MetadataScaleSignal = MetadataScaleSignal.NOT_APPLICABLE
203
+ latency_ratio: float | None = None
204
+ """(largest checkpoint's overview_latency_ms) / (smallest checkpoint's
205
+ overview_latency_ms), or None if fewer than 2 checkpoints produced a
206
+ latency to compare."""
207
+ record_ratio: float | None = None
208
+ """(largest checkpoint_n) / (smallest checkpoint_n), the baseline
209
+ `latency_ratio` above is judged against."""
210
+ skipped: bool = False
211
+ skip_reason: str | None = None
212
+ error: str | None = None
213
+
214
+
215
+ class MetadataSeeder(Protocol):
216
+ """Populates a MemPalace-backed collection so it holds exactly
217
+ `total_n` synthetic drawers (cumulative -- calling this again with a
218
+ larger `total_n` adds only the delta, never re-seeds from scratch),
219
+ and returns the resulting ground-truth (categories, subcategories)
220
+ count dicts for all `total_n` records.
221
+
222
+ `build_chroma_metadata_seeder()` below is the real, live
223
+ implementation this eval is meant to be run with. Tests inject a
224
+ lightweight in-memory fake implementing this same Protocol so the
225
+ checkpoint/classification logic can be verified without chromadb
226
+ installed -- see tests/test_mempalace_metadata_scale.py.
227
+ """
228
+
229
+ def __call__(self, total_n: int) -> tuple[dict[str, int], dict[str, int]]: ...
230
+
231
+
232
+ def _default_checkpoints(n: int) -> list[int]:
233
+ """Same shape as scale_stress.py's `_default_checkpoints`: a small,
234
+ ascending set of checkpoint sizes from "the same scale every bundled
235
+ fixture already runs at" up to the full requested N.
236
+ """
237
+ candidates = [5, max(1, n // 10), max(1, n // 2), n]
238
+ return sorted({c for c in candidates if 1 <= c <= n})
239
+
240
+
241
+ def _run_checkpoint(
242
+ adapter: MemoryBackendAdapter, seeder: MetadataSeeder, checkpoint_n: int
243
+ ) -> MetadataScaleCheckpointResult:
244
+ try:
245
+ categories_expected, subcategories_expected = seeder(checkpoint_n)
246
+ except BackendAPIError as exc:
247
+ return MetadataScaleCheckpointResult(
248
+ checkpoint_n=checkpoint_n,
249
+ overview_latency_ms=None,
250
+ categories_latency_ms=None,
251
+ subcategories_latency_ms=None,
252
+ total_records_reported=None,
253
+ categories_reported={},
254
+ categories_expected={},
255
+ subcategories_reported={},
256
+ subcategories_expected={},
257
+ counts_correct=False,
258
+ error=f"seeding failed: {exc}",
259
+ )
260
+
261
+ try:
262
+ overview = adapter.metadata_overview()
263
+ categories = adapter.list_metadata_categories()
264
+ subcategories = adapter.list_metadata_subcategories()
265
+ except BackendAPIError as exc:
266
+ return MetadataScaleCheckpointResult(
267
+ checkpoint_n=checkpoint_n,
268
+ overview_latency_ms=None,
269
+ categories_latency_ms=None,
270
+ subcategories_latency_ms=None,
271
+ total_records_reported=None,
272
+ categories_reported={},
273
+ categories_expected=categories_expected,
274
+ subcategories_reported={},
275
+ subcategories_expected=subcategories_expected,
276
+ counts_correct=False,
277
+ error=str(exc),
278
+ )
279
+
280
+ call_error = overview.error or categories.error or subcategories.error
281
+ counts_correct = (
282
+ call_error is None
283
+ and overview.total_records == checkpoint_n
284
+ and overview.categories == categories_expected
285
+ and categories.counts == categories_expected
286
+ and subcategories.counts == subcategories_expected
287
+ )
288
+
289
+ return MetadataScaleCheckpointResult(
290
+ checkpoint_n=checkpoint_n,
291
+ overview_latency_ms=overview.latency_ms,
292
+ categories_latency_ms=categories.latency_ms,
293
+ subcategories_latency_ms=subcategories.latency_ms,
294
+ total_records_reported=overview.total_records,
295
+ categories_reported=categories.counts,
296
+ categories_expected=categories_expected,
297
+ subcategories_reported=subcategories.counts,
298
+ subcategories_expected=subcategories_expected,
299
+ counts_correct=counts_correct,
300
+ error=call_error,
301
+ )
302
+
303
+
304
+ def classify_metadata_scale_result(
305
+ checkpoints: list[MetadataScaleCheckpointResult],
306
+ ) -> tuple[MetadataScaleSignal, float | None, float | None]:
307
+ """Classify a completed run's checkpoints. Returns
308
+ (signal, latency_ratio, record_ratio) -- never a blind pass/fail on
309
+ "did every call succeed," the same ground-truth-driven pattern every
310
+ other eval's classify_* function in this package follows.
311
+ """
312
+ if any(c.error is not None for c in checkpoints):
313
+ return MetadataScaleSignal.ERROR, None, None
314
+
315
+ scoreable = [c for c in checkpoints if c.error is None]
316
+ if len(scoreable) < 2:
317
+ return MetadataScaleSignal.NOT_APPLICABLE, None, None
318
+
319
+ if any(not c.counts_correct for c in scoreable):
320
+ return MetadataScaleSignal.INCORRECT_COUNTS_AT_SCALE, None, None
321
+
322
+ first, last = scoreable[0], scoreable[-1]
323
+ if (
324
+ first.overview_latency_ms is None
325
+ or last.overview_latency_ms is None
326
+ or first.checkpoint_n >= last.checkpoint_n
327
+ or first.overview_latency_ms <= 0
328
+ ):
329
+ return MetadataScaleSignal.WORKED_AT_SCALE, None, None
330
+
331
+ record_ratio = last.checkpoint_n / first.checkpoint_n
332
+ latency_ratio = last.overview_latency_ms / first.overview_latency_ms
333
+
334
+ if (
335
+ latency_ratio > _MIN_LATENCY_RATIO_TO_FLAG
336
+ and latency_ratio > record_ratio**_SUPERLINEAR_EXPONENT
337
+ ):
338
+ return MetadataScaleSignal.SUPERLINEAR_LATENCY_GROWTH, latency_ratio, record_ratio
339
+ return MetadataScaleSignal.WORKED_AT_SCALE, latency_ratio, record_ratio
340
+
341
+
342
+ def run_mempalace_metadata_scale_eval(
343
+ adapter: MemoryBackendAdapter,
344
+ seeder: MetadataSeeder,
345
+ n_records: int = DEFAULT_N_RECORDS,
346
+ checkpoints: list[int] | None = None,
347
+ ) -> MetadataScaleResult:
348
+ """Seed `seeder` up to `n_records` synthetic drawers at a series of
349
+ checkpoints, and at each one check `adapter`'s metadata_overview()/
350
+ list_metadata_categories()/list_metadata_subcategories() for both
351
+ correctness and latency-growth shape.
352
+
353
+ Args:
354
+ adapter: the backend under test. Skipped (not run) unless
355
+ `adapter.supports_metadata_overview` is True -- same
356
+ convention evals/filter_injection.py already established for
357
+ supports_raw_filter_probe.
358
+ seeder: populates the backend with ground-truth-tracked synthetic
359
+ records -- see MetadataSeeder above and
360
+ build_chroma_metadata_seeder() for the real implementation.
361
+ n_records: how many records to seed by the final checkpoint.
362
+ checkpoints: explicit checkpoint sizes. Defaults to
363
+ `_default_checkpoints(n_records)`.
364
+
365
+ Raises:
366
+ ValueError: if n_records < 1.
367
+ """
368
+ if n_records < 1:
369
+ raise ValueError(f"n_records must be >= 1, got {n_records}")
370
+
371
+ result = MetadataScaleResult(backend_name=adapter.name, n_records_requested=n_records)
372
+
373
+ if not adapter.supports_metadata_overview:
374
+ result.skipped = True
375
+ result.skip_reason = (
376
+ f"{adapter.name} does not support a metadata overview "
377
+ "(supports_metadata_overview=False) -- skipped, not run. Only an adapter that "
378
+ "holds a direct, in-process handle to a vendor's own MCP metadata-tool "
379
+ "implementation (or an equivalent library-level function) can genuinely "
380
+ "exercise this; see adapters/base.py's supports_metadata_overview and this "
381
+ "module's docstring."
382
+ )
383
+ return result
384
+
385
+ resolved_checkpoints = (
386
+ sorted({c for c in checkpoints if 1 <= c <= n_records})
387
+ if checkpoints is not None
388
+ else _default_checkpoints(n_records)
389
+ )
390
+
391
+ result.checkpoints = [_run_checkpoint(adapter, seeder, cp) for cp in resolved_checkpoints]
392
+ signal, latency_ratio, record_ratio = classify_metadata_scale_result(result.checkpoints)
393
+ result.signal = signal
394
+ result.latency_ratio = latency_ratio
395
+ result.record_ratio = record_ratio
396
+
397
+ if signal == MetadataScaleSignal.ERROR:
398
+ first_error = next((c.error for c in result.checkpoints if c.error), None)
399
+ result.error = first_error
400
+
401
+ return result
402
+
403
+
404
+ def build_chroma_metadata_seeder(
405
+ storage_path: str,
406
+ *,
407
+ collection_name: str = "mempalace_drawers",
408
+ n_categories: int = DEFAULT_N_CATEGORIES,
409
+ n_subcategories: int = 40,
410
+ embedding_dim: int = 8,
411
+ ) -> MetadataSeeder:
412
+ """Real, live `MetadataSeeder` implementation: seeds a local,
413
+ chromadb-backed palace at `storage_path` directly through the real,
414
+ installed `mempalace` package's backend layer
415
+ (`mempalace.palace.get_backend_for_palace` +
416
+ `ChromaCollection.add(..., embeddings=...)`), bypassing
417
+ MemPalaceAdapter.store() deliberately -- see
418
+ adapters/mempalace_adapter.py's module docstring: `store()` is written
419
+ against an unconfirmed `mempalace.Palace(...)` guess that does not
420
+ exist in the real package, so it cannot be used to populate a real
421
+ corpus at all. This seeder instead uses the confirmed-real backend
422
+ API the real `mine`/CLI ingestion path itself is built on, passing
423
+ explicit dummy embeddings so seeding never depends on network access
424
+ (MemPalace's default embedder otherwise downloads an ONNX model on
425
+ first use -- see mempalace_adapter.py's module docstring).
426
+
427
+ Every record gets deterministic `wing`/`room` metadata
428
+ (`wing_{i % n_categories}` / `room_{i % n_subcategories}`), matching
429
+ the same "deterministic, ground-truth-trackable" design
430
+ evals/scale_fixtures.py's generate_scale_corpus() already uses for
431
+ scale_stress.py.
432
+
433
+ Raises:
434
+ BackendAPIError: if the `mempalace` package (or its chromadb
435
+ dependency) is not installed, or a seeding call itself fails.
436
+ """
437
+ try:
438
+ from mempalace.palace import get_backend_for_palace # type: ignore[import-not-found]
439
+ except ImportError as exc:
440
+ raise BackendAPIError(
441
+ "mempalace",
442
+ "the `mempalace` package is not installed. Install it with "
443
+ "`pip install mempalace` to run a real, live metadata-scale "
444
+ "stress test -- see this module's docstring for what "
445
+ "classification-logic-only tests can still verify without it.",
446
+ ) from exc
447
+
448
+ try:
449
+ backend = get_backend_for_palace(storage_path, explicit="chroma")
450
+ collection = backend.get_collection(
451
+ storage_path, collection_name=collection_name, create=True
452
+ )
453
+ except Exception as exc: # noqa: BLE001 - real vendor call, wrap uniformly
454
+ raise BackendAPIError("mempalace", f"opening seed collection failed: {exc}") from exc
455
+
456
+ seeded_so_far = 0
457
+
458
+ def _seed(total_n: int) -> tuple[dict[str, int], dict[str, int]]:
459
+ nonlocal seeded_so_far
460
+ if total_n < seeded_so_far:
461
+ raise ValueError(
462
+ "metadata seeder is append-only: cannot shrink from "
463
+ f"{seeded_so_far} already-seeded records down to {total_n}"
464
+ )
465
+ if total_n > seeded_so_far:
466
+ documents = []
467
+ ids = []
468
+ metadatas = []
469
+ embeddings = []
470
+ for i in range(seeded_so_far, total_n):
471
+ wing = f"wing_{i % n_categories}"
472
+ room = f"room_{i % n_subcategories}"
473
+ documents.append(f"synthetic metadata-scale drawer {i} (marker SCALEMETA{i:06d})")
474
+ ids.append(f"scale-drawer-{i}")
475
+ metadatas.append({"wing": wing, "room": room})
476
+ embeddings.append([float((i * 7 + j) % 11) / 11.0 for j in range(embedding_dim)])
477
+ try:
478
+ collection.add(
479
+ documents=documents, ids=ids, metadatas=metadatas, embeddings=embeddings
480
+ )
481
+ except Exception as exc: # noqa: BLE001 - real vendor call, wrap uniformly
482
+ raise BackendAPIError("mempalace", f"seeding collection failed: {exc}") from exc
483
+ seeded_so_far = total_n
484
+
485
+ categories_expected: dict[str, int] = {}
486
+ subcategories_expected: dict[str, int] = {}
487
+ for i in range(total_n):
488
+ w = f"wing_{i % n_categories}"
489
+ r = f"room_{i % n_subcategories}"
490
+ categories_expected[w] = categories_expected.get(w, 0) + 1
491
+ subcategories_expected[r] = subcategories_expected.get(r, 0) + 1
492
+ return categories_expected, subcategories_expected
493
+
494
+ return _seed