seren-probe 1.0.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 (94) hide show
  1. seren_probe-1.0.0/Dockerfile +250 -0
  2. seren_probe-1.0.0/PKG-INFO +337 -0
  3. seren_probe-1.0.0/ProbeConfig.template.yml +80 -0
  4. seren_probe-1.0.0/README.md +313 -0
  5. seren_probe-1.0.0/SerenProbe.pyproj +129 -0
  6. seren_probe-1.0.0/docker-compose.yml +123 -0
  7. seren_probe-1.0.0/pyproject.toml +60 -0
  8. seren_probe-1.0.0/seren_probe/ProbeConfig.yml +52 -0
  9. seren_probe-1.0.0/seren_probe/README.md +65 -0
  10. seren_probe-1.0.0/seren_probe/__init__.py +48 -0
  11. seren_probe-1.0.0/seren_probe/__main__.py +65 -0
  12. seren_probe-1.0.0/seren_probe/_version.py +24 -0
  13. seren_probe-1.0.0/seren_probe/app.py +167 -0
  14. seren_probe-1.0.0/seren_probe/config.py +134 -0
  15. seren_probe-1.0.0/seren_probe/core/__init__.py +15 -0
  16. seren_probe-1.0.0/seren_probe/core/docket.py +208 -0
  17. seren_probe-1.0.0/seren_probe/core/knob_caps.py +51 -0
  18. seren_probe-1.0.0/seren_probe/core/lint_cli.py +105 -0
  19. seren_probe-1.0.0/seren_probe/core/linters/__init__.py +18 -0
  20. seren_probe-1.0.0/seren_probe/core/linters/adapters.py +70 -0
  21. seren_probe-1.0.0/seren_probe/core/linters/checks.py +211 -0
  22. seren_probe-1.0.0/seren_probe/core/linters/model.py +134 -0
  23. seren_probe-1.0.0/seren_probe/core/linters/plan.py +230 -0
  24. seren_probe-1.0.0/seren_probe/core/linters/quiet.py +116 -0
  25. seren_probe-1.0.0/seren_probe/core/linters/text.py +39 -0
  26. seren_probe-1.0.0/seren_probe/core/metrics.py +268 -0
  27. seren_probe-1.0.0/seren_probe/core/resolve.py +264 -0
  28. seren_probe-1.0.0/seren_probe/core/seed_dataset.py +892 -0
  29. seren_probe-1.0.0/seren_probe/core/topology.py +872 -0
  30. seren_probe-1.0.0/seren_probe/core/topology_emit.py +331 -0
  31. seren_probe-1.0.0/seren_probe/dockerfiles/corpus.Dockerfile +27 -0
  32. seren_probe-1.0.0/seren_probe/dockerfiles/loci.Dockerfile +112 -0
  33. seren_probe-1.0.0/seren_probe/dockerfiles/memory.Dockerfile +20 -0
  34. seren_probe-1.0.0/seren_probe/mcp/__init__.py +14 -0
  35. seren_probe-1.0.0/seren_probe/mcp/server.py +147 -0
  36. seren_probe-1.0.0/seren_probe/mcp/tools.py +75 -0
  37. seren_probe-1.0.0/seren_probe/routes/__init__.py +13 -0
  38. seren_probe-1.0.0/seren_probe/routes/config.py +35 -0
  39. seren_probe-1.0.0/seren_probe/routes/docker.py +604 -0
  40. seren_probe-1.0.0/seren_probe/routes/eval.py +873 -0
  41. seren_probe-1.0.0/seren_probe/runtime/__init__.py +11 -0
  42. seren_probe-1.0.0/seren_probe/runtime/docker_env.py +1149 -0
  43. seren_probe-1.0.0/seren_probe/runtime/live_eval.py +942 -0
  44. seren_probe-1.0.0/seren_probe/runtime/live_import.py +119 -0
  45. seren_probe-1.0.0/seren_probe/runtime/progress.py +127 -0
  46. seren_probe-1.0.0/seren_probe/runtime/regrade.py +889 -0
  47. seren_probe-1.0.0/seren_probe/runtime/regrade_live.py +621 -0
  48. seren_probe-1.0.0/seren_probe/runtime/write_guard.py +148 -0
  49. seren_probe-1.0.0/seren_probe/viewer/ui/body.html +112 -0
  50. seren_probe-1.0.0/seren_probe/viewer/ui/header_aside.html +2 -0
  51. seren_probe-1.0.0/seren_probe/viewer/ui/scripts.js +1803 -0
  52. seren_probe-1.0.0/seren_probe/viewer/ui/styles.css +1327 -0
  53. seren_probe-1.0.0/seren_probe/viewer/ui/tabs.html +3 -0
  54. seren_probe-1.0.0/seren_probe.egg-info/PKG-INFO +337 -0
  55. seren_probe-1.0.0/seren_probe.egg-info/SOURCES.txt +92 -0
  56. seren_probe-1.0.0/seren_probe.egg-info/dependency_links.txt +1 -0
  57. seren_probe-1.0.0/seren_probe.egg-info/entry_points.txt +2 -0
  58. seren_probe-1.0.0/seren_probe.egg-info/requires.txt +17 -0
  59. seren_probe-1.0.0/seren_probe.egg-info/scm_file_list.json +87 -0
  60. seren_probe-1.0.0/seren_probe.egg-info/scm_version.json +8 -0
  61. seren_probe-1.0.0/seren_probe.egg-info/top_level.txt +1 -0
  62. seren_probe-1.0.0/serenprobe.service.sample +42 -0
  63. seren_probe-1.0.0/serenprobe.yaml.sample +43 -0
  64. seren_probe-1.0.0/setup.cfg +4 -0
  65. seren_probe-1.0.0/tests/ProbeConfig.yml +63 -0
  66. seren_probe-1.0.0/tests/compare_loci.py +151 -0
  67. seren_probe-1.0.0/tests/fixtures/meridian.loci.yaml +96 -0
  68. seren_probe-1.0.0/tests/fixtures/meridian.memory.yaml +94 -0
  69. seren_probe-1.0.0/tests/fixtures/meridian.questions.yaml +75 -0
  70. seren_probe-1.0.0/tests/test_config_hierarchy.py +111 -0
  71. seren_probe-1.0.0/tests/test_docket.py +97 -0
  72. seren_probe-1.0.0/tests/test_eval_inputs.py +91 -0
  73. seren_probe-1.0.0/tests/test_eval_parallel.py +194 -0
  74. seren_probe-1.0.0/tests/test_healthcheck_grace.py +36 -0
  75. seren_probe-1.0.0/tests/test_knob_caps.py +58 -0
  76. seren_probe-1.0.0/tests/test_layering.py +228 -0
  77. seren_probe-1.0.0/tests/test_lint_cli.py +49 -0
  78. seren_probe-1.0.0/tests/test_live_import.py +114 -0
  79. seren_probe-1.0.0/tests/test_live_resolve.py +52 -0
  80. seren_probe-1.0.0/tests/test_metrics.py +170 -0
  81. seren_probe-1.0.0/tests/test_question_lint.py +201 -0
  82. seren_probe-1.0.0/tests/test_regrade_concurrency.py +336 -0
  83. seren_probe-1.0.0/tests/test_regrade_grid.py +82 -0
  84. seren_probe-1.0.0/tests/test_regrades.py +76 -0
  85. seren_probe-1.0.0/tests/test_resolve.py +129 -0
  86. seren_probe-1.0.0/tests/test_seed_dataset.py +58 -0
  87. seren_probe-1.0.0/tests/test_seed_flat.py +90 -0
  88. seren_probe-1.0.0/tests/test_seed_parallel.py +210 -0
  89. seren_probe-1.0.0/tests/test_seed_plan.py +95 -0
  90. seren_probe-1.0.0/tests/test_topo_eval.py +113 -0
  91. seren_probe-1.0.0/tests/test_topology.py +201 -0
  92. seren_probe-1.0.0/tests/test_topology_emit.py +110 -0
  93. seren_probe-1.0.0/tests/test_topology_schema.py +196 -0
  94. seren_probe-1.0.0/tests/test_write_guard.py +124 -0
@@ -0,0 +1,250 @@
1
+ # ═══════════════════════════════════════════════════════════════════════
2
+ # Dockerfile - Seren Live Store Environment
3
+ #
4
+ # Five services in one image, pre-configured to talk to each other:
5
+ #
6
+ # 7420 SerenMemory (right brain - 3-tier chroma)
7
+ # 7421 SerenLoci-Vector (left brain - exact + FTS5 + vector)
8
+ # 7422 SerenLoci (left brain - exact + FTS5 only)
9
+ # 7423 SerenCorpusCallosum (callosum - fans memory + loci-nv)
10
+ # 7424 SerenCorpusCallosum-Vector (callosum - fans memory + loci-vector)
11
+ #
12
+ # SerenProbe (or any client) connects from the host or another container.
13
+ # The vector instances use sqlite-vec + sentence-transformers so the
14
+ # associative "this smells like that CUDA thing" jump works.
15
+ #
16
+ # USAGE
17
+ # docker build -t seren-live-stores .
18
+ # docker run -d \
19
+ # -p 7420:7420 -p 7421:7421 -p 7422:7422 \
20
+ # -p 7423:7423 -p 7424:7424 \
21
+ # --name seren-test seren-live-stores
22
+ #
23
+ # Then point SerenProbe at the mapped ports and run evaluations.
24
+ #
25
+ # To seed data before evaluating:
26
+ # docker exec seren-test python -m serenprobe.runner --seed-only
27
+ # ═══════════════════════════════════════════════════════════════════════
28
+
29
+ FROM python:3.11-slim AS base
30
+
31
+ # ── Install system deps ──────────────────────────────────────────────
32
+ RUN apt-get update -qq && \
33
+ apt-get install -y -qq --no-install-recommends \
34
+ curl \
35
+ git \
36
+ && \
37
+ rm -rf /var/lib/apt/lists/*
38
+
39
+ # ── Create the seren user ────────────────────────────────────────────
40
+ RUN groupadd -r seren -g 999 && \
41
+ useradd -r -g seren -u 999 -m -d /home/seren -s /bin/bash seren && \
42
+ chmod 0775 /home/seren
43
+
44
+ # ── Install SerenMemory (port 7420, right brain) ─────────────────────
45
+ RUN python3 -m venv /home/seren/venvs/memory && \
46
+ /home/seren/venvs/memory/bin/pip install --no-cache-dir \
47
+ "seren-memory" && \
48
+ /home/seren/venvs/memory/bin/pip check
49
+
50
+ # ── Install SerenLoci-Vector (port 7421, with associative finder) ────
51
+ # Separate venv so the [vector] deps (torch, sentence-transformers)
52
+ # don't leak into the NV instance.
53
+ RUN python3 -m venv /home/seren/venvs/loci-vector && \
54
+ /home/seren/venvs/loci-vector/bin/pip install --no-cache-dir \
55
+ "seren-loci[vector]" && \
56
+ /home/seren/venvs/loci-vector/bin/pip check
57
+
58
+ # ── Install SerenLoci (port 7422, embedding-free floor) ──────────────
59
+ RUN python3 -m venv /home/seren/venvs/loci && \
60
+ /home/seren/venvs/loci/bin/pip install --no-cache-dir \
61
+ "seren-loci" && \
62
+ /home/seren/venvs/loci/bin/pip check
63
+
64
+ # ── Install SerenCorpusCallosum × 2 ──────────────────────────────────
65
+ # Both NV (7423) and Vector (7424) use the same package - the
66
+ # difference is only which stores the YAML points to. One venv is
67
+ # enough; we launch two processes from it with different configs.
68
+ RUN python3 -m venv /home/seren/venvs/callosum && \
69
+ /home/seren/venvs/callosum/bin/pip install --no-cache-dir \
70
+ "seren-corpus-callosum" && \
71
+ /home/seren/venvs/callosum/bin/pip check
72
+
73
+ # ── Write config files ───────────────────────────────────────────────
74
+ RUN mkdir -p /home/seren/config && chown -R seren:seren /home/seren/config
75
+
76
+ # seren-memory.yaml
77
+ RUN cat > /home/seren/config/seren-memory.yaml <<'CFG_EOF'
78
+ server:
79
+ host: 0.0.0.0
80
+ port: 7420
81
+ bearer_token: ""
82
+ storage:
83
+ persist_dir: /home/seren/data/memory
84
+ lifetimes:
85
+ short_term_seconds: 691200
86
+ near_term_review_seconds: 2592000
87
+ consolidator:
88
+ enabled: false
89
+ CFG_EOF
90
+
91
+ # seren-loci-vector.yaml - vector finder enabled
92
+ RUN cat > /home/seren/config/seren-loci-vector.yaml <<'CFG_EOF'
93
+ server:
94
+ host: 0.0.0.0
95
+ port: 7421
96
+ bearer_token: ""
97
+ storage:
98
+ db_path: /home/seren/data/loci-vector/loci.db
99
+ embedding_model: sentence-transformers/all-MiniLM-L6-v2
100
+ embedding_device: cpu
101
+ CFG_EOF
102
+
103
+ # seren-loci.yaml - nano floor, no vector
104
+ RUN cat > /home/seren/config/seren-loci.yaml <<'CFG_EOF'
105
+ server:
106
+ host: 0.0.0.0
107
+ port: 7422
108
+ bearer_token: ""
109
+ storage:
110
+ db_path: /home/seren/data/loci/loci.db
111
+ embedding_model: null
112
+ CFG_EOF
113
+
114
+ # seren-corpus-callosum.yaml - fans memory + loci-nv (7422)
115
+ RUN cat > /home/seren/config/seren-corpus-callosum.yaml <<'CFG_EOF'
116
+ server:
117
+ host: 0.0.0.0
118
+ port: 7423
119
+ bearer_token: ""
120
+ tls:
121
+ trust_system_store: false
122
+ federation:
123
+ k: 60
124
+ n_results: 10
125
+ fetch_multiplier: 2
126
+ per_store_timeout_s: 5.0
127
+ fusion_mode: rrf
128
+ authority_margin: 0.035
129
+ min_per_store: 1
130
+ edges_enabled: true
131
+ edge_budget: 3
132
+ stores:
133
+ - name: memory
134
+ type: seren_memory
135
+ url: http://127.0.0.1:7420
136
+ weight: 1.0
137
+ floor: 0.0
138
+ - name: loci
139
+ type: seren_loci
140
+ url: http://127.0.0.1:7422
141
+ weight: 1.0
142
+ floor: 0.0
143
+ CFG_EOF
144
+
145
+ # seren-corpus-callosum-vector.yaml - fans memory + loci-vector (7421)
146
+ RUN cat > /home/seren/config/seren-corpus-callosum-vector.yaml <<'CFG_EOF'
147
+ server:
148
+ host: 0.0.0.0
149
+ port: 7424
150
+ bearer_token: ""
151
+ tls:
152
+ trust_system_store: false
153
+ federation:
154
+ k: 60
155
+ n_results: 10
156
+ fetch_multiplier: 2
157
+ per_store_timeout_s: 5.0
158
+ fusion_mode: rrf
159
+ authority_margin: 0.035
160
+ min_per_store: 1
161
+ edges_enabled: true
162
+ edge_budget: 3
163
+ stores:
164
+ - name: memory
165
+ type: seren_memory
166
+ url: http://127.0.0.1:7420
167
+ weight: 1.0
168
+ floor: 0.0
169
+ - name: loci-vector
170
+ type: seren_loci
171
+ url: http://127.0.0.1:7421
172
+ weight: 1.0
173
+ floor: 0.0
174
+ CFG_EOF
175
+
176
+ # ── Data directories ──────────────────────────────────────────────────
177
+ RUN mkdir -p /home/seren/data/memory && \
178
+ mkdir -p /home/seren/data/loci && \
179
+ mkdir -p /home/seren/data/loci-vector && \
180
+ chown -R seren:seren /home/seren/data
181
+
182
+ # ── Startup script ────────────────────────────────────────────────────
183
+ # Launches all five services in the background. On SIGTERM/SIGINT it
184
+ # forwards the signal to all children so a docker stop cleans up cleanly.
185
+ RUN cat > /home/seren/start.sh <<'START_EOF'
186
+ #!/usr/bin/env bash
187
+ set -euo pipefail
188
+
189
+ echo "==> Starting SerenMemory on :7420 ..."
190
+ /home/seren/venvs/memory/bin/python -m seren_memory \
191
+ --config /home/seren/config/seren-memory.yaml &
192
+ MEMORY_PID=$!
193
+
194
+ sleep 1
195
+
196
+ echo "==> Starting SerenLoci-Vector on :7421 ..."
197
+ /home/seren/venvs/loci-vector/bin/python -m seren_loci \
198
+ --config /home/seren/config/seren-loci-vector.yaml &
199
+ LOCI_V_PID=$!
200
+
201
+ sleep 1
202
+
203
+ echo "==> Starting SerenLoci on :7422 ..."
204
+ /home/seren/venvs/loci/bin/python -m seren_loci \
205
+ --config /home/seren/config/seren-loci.yaml &
206
+ LOCI_NV_PID=$!
207
+
208
+ sleep 1
209
+
210
+ echo "==> Starting SerenCorpusCallosum on :7423 ..."
211
+ /home/seren/venvs/callosum/bin/python -m seren_corpus_callosum \
212
+ --config /home/seren/config/seren-corpus-callosum.yaml &
213
+ SCC_NV_PID=$!
214
+
215
+ sleep 1
216
+
217
+ echo "==> Starting SerenCorpusCallosum-Vector on :7424 ..."
218
+ /home/seren/venvs/callosum/bin/python -m seren_corpus_callosum \
219
+ --config /home/seren/config/seren-corpus-callosum-vector.yaml &
220
+ SCC_V_PID=$!
221
+
222
+ echo ""
223
+ echo "╔══════════════════════════════════════════════════════════╗"
224
+ echo "║ Seren Live Store Environment ║"
225
+ echo "║ ║"
226
+ echo "║ Memory :7420 Loci-V :7421 Loci :7422 ║"
227
+ echo "║ SCC-NV :7423 SCC-V :7424 ║"
228
+ echo "║ ║"
229
+ echo "║ All stores ready for evaluation. ║"
230
+ echo "╚══════════════════════════════════════════════════════════╝"
231
+ echo ""
232
+
233
+ trap 'echo "Shutting down..."; \
234
+ kill $MEMORY_PID $LOCI_V_PID $LOCI_NV_PID $SCC_NV_PID $SCC_V_PID 2>/dev/null; \
235
+ exit 0' SIGTERM SIGINT
236
+
237
+ wait -n
238
+ kill $MEMORY_PID $LOCI_V_PID $LOCI_NV_PID $SCC_NV_PID $SCC_V_PID 2>/dev/null
239
+ START_EOF
240
+ RUN chmod +x /home/seren/start.sh
241
+
242
+ # ── Finalize ──────────────────────────────────────────────────────────
243
+ RUN chown -R seren:seren /home/seren
244
+
245
+ USER seren
246
+ WORKDIR /home/seren
247
+
248
+ EXPOSE 7420 7421 7422 7423 7424
249
+
250
+ CMD ["/home/seren/start.sh"]
@@ -0,0 +1,337 @@
1
+ Metadata-Version: 2.4
2
+ Name: seren-probe
3
+ Version: 1.0.0
4
+ Summary: RAG evaluation toolkit for Seren's memory architecture
5
+ Author: Chad Roesler
6
+ License: GPL-3.0-or-later
7
+ Keywords: llm,memory,eval,rag,seren
8
+ Requires-Python: <3.14,>=3.10
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: fastapi>=0.115.0
11
+ Requires-Dist: uvicorn[standard]>=0.32.0
12
+ Requires-Dist: pydantic>=2.9.0
13
+ Requires-Dist: pyyaml>=6.0.2
14
+ Requires-Dist: httpx>=0.27.0
15
+ Requires-Dist: seren-meninges<2,>=1.0.0
16
+ Requires-Dist: seren-sinew<2,>=1.0.0
17
+ Provides-Extra: corp
18
+ Requires-Dist: truststore>=0.9; extra == "corp"
19
+ Provides-Extra: mcp
20
+ Requires-Dist: mcp>=1.0.0; extra == "mcp"
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8.0; extra == "dev"
23
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
24
+
25
+ # SerenProbe
26
+
27
+ **A push-button RAG evaluation harness for the Seren memory constellation.**
28
+
29
+ SerenProbe exists to answer one deceptively hard question: *did that change make retrieval better or worse?* You tweaked an embedder, flipped Loci from lexical to hybrid, retuned the fusion knobs on the callosum — and now you need a number that tells you whether the model is going to get handed a better briefing or a worse one. SerenProbe seeds a known corpus, asks known questions, and scores what comes back, honestly, across every store configuration side by side.
30
+
31
+ It's the empiricist's tool. You don't reason about whether the change helped. You see it.
32
+
33
+ ---
34
+
35
+ ## What it probes
36
+
37
+ Seren's memory lives in three services, and SerenProbe knows how to grade all of them:
38
+
39
+ - **SerenMemory** — the episodic "right brain." Three-tier vector store (short / near / long).
40
+ - **SerenLoci** — the deterministic "left brain." SQLite facts, addressable by exact key, searchable by FTS5 or FTS5+vector hybrid.
41
+ - **SerenCorpusCallosum (SCC)** — the "callosum." Fans a query across Memory + Loci and fuses the results into one briefing packet.
42
+
43
+ Each brain has a **no-vector (NV)** and a **vector (V)** flavor, and they're not interchangeable — the whole reason SCC exists is to build a *diverse, provenance-tagged* packet, not to crown a single winner. So SerenProbe scores Loci and Memory with the standard retrieval metrics, and scores SCC with those *plus* docket-quality metrics that measure whether the briefing actually covered the ground. (SCC is a chief-of-staff assembling a packet, not Judge Judy picking rank one. The metrics reflect that.)
44
+
45
+ ---
46
+
47
+ ## Two ways to run it
48
+
49
+ ### 1. Against stores you already have running
50
+
51
+ Point SerenProbe at the URLs of live services and grade them in place:
52
+
53
+ ```bash
54
+ pip install seren-probe
55
+ seren-probe # boots the operator dashboard on :7430
56
+ ```
57
+
58
+ Open **http://127.0.0.1:7430/viewer**, or drive it headless via `POST /eval/run`. Configure the store URLs in `serenprobe.yaml` (or via `SEREN_PROBE_*` env vars) — see [Configuration](#configuration).
59
+
60
+ ### 2. The self-contained Docker harness
61
+
62
+ Don't have five stores wired up? Declare the shape you want in `ProbeConfig.yml` and let SerenProbe stand the whole thing up, seed it, grade it, and tear it down:
63
+
64
+ ```
65
+ ProbeConfig.yml → compile → emit compose → docker up → seed → eval → results
66
+ ```
67
+
68
+ The compiler is **correct-by-construction**: it wires inter-container traffic with container-DNS URLs (never `localhost`), publishes host ports only for the eval harness, and — critically — wires each SCC to *its own* Loci. Pointing `scc-nv` and `scc-v` at the same Loci instance is the one silent failure that quietly poisons a whole comparison, so the topology compiler refuses to let you do it by accident and *tells you* when your config drifts toward it.
69
+
70
+ Kick it off from the dashboard (Docker tab) or `POST /docker/run-eval`.
71
+
72
+ ---
73
+
74
+ ## The ProbeConfig topology compiler
75
+
76
+ `ProbeConfig.yml` is a declarative description of the stores you want to probe — how many Loci, how many Memory, which flavors, and how the Corpus nodes fan across them. You describe the *shape*; the compiler handles ports, wiring, and the footguns.
77
+
78
+ ```yaml
79
+ ProbeConfig:
80
+ StartingPort: 7420
81
+ Loci:
82
+ LociCount: 2
83
+ LociConfigs:
84
+ - { Name: loci-v, Port: 7421, Flags: [vector] } # hybrid
85
+ - { Name: loci-nv, Port: 7422 } # FTS5-only (nano floor)
86
+ Memory:
87
+ MemoryCount: 1
88
+ MemoryConfigs:
89
+ - { Name: mem, Port: 7420 }
90
+ Corpus:
91
+ CorpusCount: 2
92
+ CorpusConfigs:
93
+ - { Name: scc-v, Port: 7424, Stores: [{ Store: loci-v }, { Store: mem }] }
94
+ - { Name: scc-nv, Port: 7423, Stores: [{ Store: loci-nv }, { Store: mem }] }
95
+ ```
96
+
97
+ What it does for you:
98
+
99
+ - **Autogen + catch-all.** Declare a count higher than your explicit configs and it fills the gap; any store you don't reference gets swept into a catch-all Corpus so nothing is silently stranded.
100
+ - **Compassion-first validation.** Bad flag for a store type, a Corpus that references another Corpus, a duplicate port, a store name that isn't declared, two Corpus nodes with identical store sets — every one of these fails *loud and kind*, naming the exact rule and the exact node, at compile time. Config bugs are the expensive kind; this catches them before a container ever starts.
101
+ - **Deterministic, type-grouped ports.** Auto-assigned ports are stable run-to-run and grouped by kind, so a diff of two runs is a diff of your *intent*, not port-assignment noise.
102
+
103
+ A full working `ProbeConfig.yml` ships in the package, plus a heavily-commented `ProbeConfig.template.yml` to copy from.
104
+
105
+ ---
106
+
107
+ ## Bring your own corpus + questions
108
+
109
+ The topology says *what stores* to stand up; **flat seed files** and a **question set** say what to put in them and what to ask — and you wire them into the ProbeConfig itself, so one config is self-contained. No separate dataset upload: hand SerenProbe the ProbeConfig and it seeds + scores from what the config points at.
110
+
111
+ A **seed file** is a flat list of items (the store→data mapping lives in the config, not the file). Shapes are the real write contracts — Loci `{project?, key, value, why?}`, Memory `{content|intent, topic?, tier?, ref?}`:
112
+
113
+ ```yaml
114
+ # meridian.loci.yaml
115
+ - { project: meridian, key: api_port, value: "8080", why: "the REST API listens here" }
116
+ # meridian.memory.yaml
117
+ - { tier: short, ref: auth-incident, content: "auth threw 401s after a clock-skew bug", topic: auth }
118
+ ```
119
+
120
+ Wire them into the ProbeConfig — a **shared** `Questions` (scored across every store, which is what keeps the comparison honest), per-kind default seeds, and optional per-node `Seed` overrides:
121
+
122
+ ```yaml
123
+ ProbeConfig:
124
+ Questions: examples/meridian.questions.yaml
125
+ DefaultLociSeed: examples/meridian.loci.yaml # every Loci without its own Seed
126
+ DefaultMemorySeed: examples/meridian.memory.yaml # every Memory without its own Seed
127
+ Loci:
128
+ LociConfigs:
129
+ - { Name: loci-v, Port: 7421, Flags: [vector] }
130
+ - { Name: decoy, Port: 7429, NegativeTest: true, Seed: examples/unrelated.yaml }
131
+ # ... Memory / Corpus ...
132
+ ```
133
+
134
+ A **question set** scores each query against honest ground truth — `expect_key` (Loci's canonical id, retrieval-independent), `expect_ref` (a Memory entry you tagged at seed time), or `expect_content` (a fuzzy substring):
135
+
136
+ ```yaml
137
+ questions:
138
+ - { asks: loci, query: "what port does the API use?", expect_key: ["meridian/api_port"] }
139
+ - { asks: corpus, query: "brief me on the auth setup",
140
+ expect_content: ["JWT bearer tokens", "15 minute", "100 requests per minute", "team-atlas"] }
141
+ ```
142
+
143
+ The **corpus** questions are where docket coverage earns its keep: give each *several* expected facts that live across both Loci and Memory, and coverage becomes a real "did the briefing assemble all of it?" fraction instead of a binary hit.
144
+
145
+ **Negative (decoy) stores.** Mark a store `NegativeTest: true` and give it an unrelated `Seed`, and it's seeded with *only* that decoy — never the defaults — so you can prove a store *stays quiet* on questions it shouldn't answer. It's kept out of the catch-all, and the Eval tab reads it as ✓ *stayed quiet* / ✗ *leaked* instead of a bare zero.
146
+
147
+ A complete, validated example ships in [`examples/`](examples/): `meridian.loci.yaml` + `meridian.memory.yaml` + `meridian.questions.yaml` — a small interlocking fictional stack with three multi-fact briefing questions. Point the ProbeConfig's `Default*Seed` / `Questions` at them (or inline the content for a fully self-contained upload), then just `POST /eval/run` with an empty body.
148
+
149
+ Validation is **compassion-first**, same as the compiler: an item with the wrong shape, a question with no way to score it, a negative store with no decoy — each fails loud and kind, all at once, naming the fix.
150
+
151
+ ---
152
+
153
+ ## Per-node configuration (defaults + overrides)
154
+
155
+ The same **default-then-override** pattern as seeds and questions applies to *config* — the knobs each store boots at. Nothing is required; anything you don't set falls through to the service's own installed default. The probe only writes what you specify.
156
+
157
+ Each section (`Loci`, `Memory`, `Corpus`) may carry a `DefaultConfig`, and each node may carry its own `Config`. The precedence, lowest to highest, is:
158
+
159
+ ```
160
+ service installed default ← section DefaultConfig ← per-node Config
161
+ ```
162
+
163
+ For **Loci** and **Memory**, `DefaultConfig` and a node's `Config` are a *partial of that service's own yaml* (nested — `storage`, `lifetimes`, `consolidator`, …), **deep-merged** and mounted into the container as its config file. Set only the keys you care about; the rest default.
164
+
165
+ ```yaml
166
+ Memory:
167
+ MemoryCount: 3
168
+ DefaultConfig: # every Memory node inherits this
169
+ consolidator: { enabled: false }
170
+ MemoryConfigs:
171
+ - Name: fast-mem
172
+ Config: # overrides DefaultConfig for THIS node (deep-merged)
173
+ lifetimes: { short_term_seconds: 3600 }
174
+ - Name: plain-mem # no Config → just DefaultConfig
175
+ Loci:
176
+ LociCount: 1
177
+ # no DefaultConfig → every Loci boots on its installed defaults
178
+ LociConfigs:
179
+ - Name: only-loci
180
+ ```
181
+
182
+ For **Corpus**, the config is the *fusion* layer, which adds a second axis — per-store `weight`/`floor` — so it uses flat knobs plus a per-kind shorthand rather than nested yaml (the probe builds the federation store list for you, container DNS and all). A corpus store's `floor`/`weight` resolves:
183
+
184
+ ```
185
+ per-store Floor:/Weight: (on the Stores entry)
186
+ ← per-kind shorthand loci_floor / loci_weight / mem_floor / mem_weight (in Config)
187
+ ← Corpus.DefaultConfig shorthand
188
+ ← the SCC's installed default
189
+ ```
190
+
191
+ `loci_floor` floors *every* loci member of the corpus (and leaves memory alone); `mem_floor` does the same for memory. A per-store `Floor:` on a `Stores` entry is the escape hatch that beats the shorthand for that one store. Federation-level knobs — `authority_margin`, `n_results`, `rrf_k`, `fusion_mode`, `min_per_store`, `hops` — live in `Config`/`DefaultConfig` and go straight onto the federation block.
192
+
193
+ ```yaml
194
+ Corpus:
195
+ CorpusCount: 1
196
+ DefaultConfig: # every corpus inherits these
197
+ authority_margin: 0.035
198
+ loci_floor: 0.5 # all loci members, unless overridden
199
+ CorpusConfigs:
200
+ - Name: big-scc
201
+ Config:
202
+ loci_floor: 0.6 # this corpus floors its loci at 0.6
203
+ Stores:
204
+ - Store: sharp-loci
205
+ Floor: 0.7 # …except this one, pinned to 0.7
206
+ - Store: some-mem # no mem_floor set → SCC's own default
207
+ ```
208
+
209
+ The boot config the container mounts **and** the live `/configure` a regrade sweep pushes are resolved through the *same* ladder, so what you sweep and what you boot can't disagree — a value you swept and liked pastes straight into `Config` as the new baseline.
210
+
211
+ ---
212
+
213
+ ## What the numbers mean
214
+
215
+ Every store gets the standard retrieval battery, computed at top-*k*:
216
+
217
+ | Metric | Reads as |
218
+ |---|---|
219
+ | **HitRate@k** | Did *any* relevant doc land in the top-k? (0 or 1) |
220
+ | **MRR@k** | How high was the *first* relevant hit? (rank 1 → 1.0, rank 3 → 0.333) |
221
+ | **Precision@k** | Fraction of the top-k that were relevant |
222
+ | **Recall@k** | Fraction of all relevant docs the top-k caught |
223
+ | **NDCG@k** | Rank-aware quality — right answers near the top score higher |
224
+ | **IoU@k** | Jaccard overlap of retrieved vs. relevant |
225
+ | **PΩ@k** | Rank-weighted precision — top ranks count for more |
226
+
227
+ SCC additionally reports **docket_coverage** (fraction of expected facts found *anywhere* in the packet — did the briefing cover the ground?) and **docket_density** (fraction of the top-k that carry expected content — how much of the packet is signal?). These are the metrics that actually reflect SCC's job.
228
+
229
+ ### The docket comparison — with vs without edges
230
+
231
+ Run a topology with both a vector SCC and a lexical one (the canonical `scc-v` / `scc-nv` pair) and SerenProbe doesn't just score them independently — it **pairs them and reports the delta**, answering the question an SCC exists for: *when the callosum fans a vector Loci (semantic edges between nodes) instead of a lexical, FTS5-only one (no edges), does the assembled briefing carry more of the relevant info?*
232
+
233
+ The comparison rides in every `/eval/run` result under `result["docket"]` and renders in the Eval tab as a side-by-side block:
234
+
235
+ ```
236
+ SCC Docket — with vs without edges 30 questions · k=10
237
+ without edges scc-nv coverage 0.5850 density 0.7033 recall 0.9544
238
+ with edges scc-v coverage 0.5331 density 0.6967 recall 0.9250
239
+ Δ edges (with − without) coverage -0.0519 · density -0.0066 · recall -0.0294
240
+ → edges COST -0.0519 docket coverage
241
+ ```
242
+
243
+ "Edges" is read authoritatively off the compiled topology — an SCC's flavor is whether the Loci it fans carries the `[vector]` flag — so the labels are never guessed. And mind the sign: in that run the semantic edges *hurt* coverage. Surfacing the delta is the whole point; two columns side by side make you do that subtraction in your head.
244
+
245
+ > **On grading honesty:** content relevance is matched fuzzily (so `rate_limit`, `rate-limit`, and `rate limit` compare equal), which means the occasional false-positive on a shared phrase and false-negative on a paraphrase. Trust the *relative* numbers across configs; be cautious with absolutes. The harness never pretends to more precision than it has.
246
+
247
+ ---
248
+
249
+ ## Tuning the callosum without a hundred restarts
250
+
251
+ `regrade.py` is the SCC fusion tuner, and it's built on one insight: the fusion knobs (RRF *k*, per-store weight, floor, `authority_margin`, `min_per_store`, packet size) all operate on candidates the stores *already returned*. So you query each store **once** per question, freeze the raw responses to a trace, and then replay the **real** Federation against that frozen capture for every knob combination — in-process. A 300-point grid that used to be 300 service bounces becomes a sub-second sweep.
252
+
253
+ It's **read-only** (only ever `POST /search`, never seeds or mutates), refuses to run without explicit dev-store URLs, and can sweep a saved trace fully offline with nothing live attached. Capture once on the dev rig, then tune all day on a plane.
254
+
255
+ ```bash
256
+ # capture on the dev rig + sweep, save the trace
257
+ python -m seren_probe.regrade \
258
+ --memory-url http://127.0.0.1:7420 \
259
+ --loci-nv-url http://127.0.0.1:7422 \
260
+ --loci-v-url http://127.0.0.1:7421 \
261
+ --save-capture /tmp/scc_capture.json
262
+
263
+ # later, re-sweep the frozen trace, nothing live attached
264
+ python -m seren_probe.regrade --load-capture /tmp/scc_capture.json
265
+ ```
266
+
267
+ ---
268
+
269
+ ## Configuration
270
+
271
+ Resolution order, later wins: **built-in defaults → `seren-probe.yaml` → `SEREN_PROBE_*` env vars.** A missing config file is fine — defaults plus env is a valid zero-config run.
272
+
273
+ Copy `serenprobe.yaml.sample` to `seren-probe.yaml` and edit, or set env vars (`SEREN_PROBE_PORT`, `SEREN_PROBE_MEMORY_URL`, `SEREN_PROBE_BEARER_TOKEN`, …). The default store layout follows the Seren family port convention:
274
+
275
+ | Service | Port |
276
+ |---|---|
277
+ | SerenMemory | 7420 |
278
+ | SerenLoci (vector) | 7421 |
279
+ | SerenLoci (no-vector) | 7422 |
280
+ | SCC (no-vector) | 7423 |
281
+ | SCC (vector) | 7424 |
282
+ | **SerenProbe dashboard** | **7430** |
283
+
284
+ ---
285
+
286
+ ## Install
287
+
288
+ ```bash
289
+ pip install seren-probe # core: eval + dashboard against live stores
290
+ pip install "seren-probe[mcp]" # + MCP surface for connected models
291
+ pip install "seren-probe[corp]" # + OS-trust-store TLS for intercepting proxies
292
+ pip install "seren-probe[dev]" # + pytest for hacking on the harness
293
+ ```
294
+
295
+ Requires Python 3.10+.
296
+
297
+ **On a corporate network** that does TLS interception (Zscaler, Netskope, and friends): install the `corp` extra and set `tls.trust_system_store: true`. That routes Python's TLS through your OS trust store so outbound calls to the store services stop failing on an unknown root CA.
298
+
299
+ ---
300
+
301
+ ## Testing
302
+
303
+ The tests are transport-injected — they prove the compiler, the compose emitter, the seeder, and the grading math without needing a live store stack, so `pytest` runs green anywhere:
304
+
305
+ ```bash
306
+ pip install -e ".[dev]"
307
+ python -m pytest tests/ -v
308
+ ```
309
+
310
+ CI runs the full matrix on every push and publishes to PyPI (tokenless Trusted Publishing) on a `v*` tag.
311
+
312
+ ---
313
+
314
+ ## Project layout
315
+
316
+ ```
317
+ seren_probe/
318
+ topology.py # ProbeConfig → validated CompiledTopology (the compiler)
319
+ topology_emit.py # CompiledTopology → docker compose + per-corpus wiring
320
+ seed_dataset.py # flat seed items + questions, ref→id capture, honest ground truth
321
+ resolve.py # ProbeConfig refs → per-store seed plan + shared questions
322
+ live_eval.py # topology-driven + legacy live-store evaluation
323
+ metrics.py # the retrieval + docket scoring math
324
+ docket.py # pairs the SCC columns → the with/without-edges delta
325
+ regrade.py # capture-once / replay-many SCC fusion sweep
326
+ runner.py # in-process per-store evaluation runner
327
+ app.py # FastAPI dashboard: /eval, /docker, /viewer, /mcp
328
+ viewer/ # the operator dashboard UI
329
+ tests/ # transport-injected, no live stack required
330
+ examples/ # a ready-to-upload seed dataset + question set
331
+ ```
332
+
333
+ ---
334
+
335
+ ## License
336
+
337
+ GPL-3.0-or-later. Part of the [Seren](https://github.com/ChadRoesler) project — a fully self-hosted, local-first AI companion stack built to run gracefully on cheap hardware. The floor is a $250 Jetson, not a data center. Take it, probe your own stores, and show us what you find.
@@ -0,0 +1,80 @@
1
+ # ═══════════════════════════════════════════════════════════════════════
2
+ # ProbeConfig — declare the stores you want SerenProbe to stand up + grade
3
+ # ═══════════════════════════════════════════════════════════════════════
4
+ #
5
+ # This is a TEMPLATE, not the shipped example. Copy it, edit it, and hand it
6
+ # to SerenProbe (POST /docker/probeconfig, or the Config tab in the viewer).
7
+ # The compiler validates it and tells you — by name — anything it doesn't like
8
+ # before a single container starts.
9
+ #
10
+ # You describe a SHAPE (how many of each store, which flavors, how the Corpus
11
+ # nodes fan) plus the DATA it runs on (seeds + questions, config-driven). One
12
+ # self-contained config: no separate dataset upload.
13
+ #
14
+ # The layout below is the canonical comparison SerenProbe was built for: the
15
+ # same corpus retrieved five ways, scored side by side — Loci with and without
16
+ # vectors, and an SCC over each — plus a negative (decoy) store as a leak test.
17
+ # ═══════════════════════════════════════════════════════════════════════
18
+
19
+ ProbeConfig:
20
+
21
+ # Every auto-assigned port starts here and counts up. Required whenever any
22
+ # node omits its Port. Nodes that DO name a Port must name one >= StartingPort.
23
+ StartingPort: 7420
24
+
25
+ # ── Data plumbing (config-driven seeding) ──────────────────────────────
26
+ # Questions are SHARED across every store — that's what keeps the comparison
27
+ # honest (apples-to-apples). Seeds default per-kind; a node's own Seed
28
+ # overrides its kind default. Every ref is a path OR inline YAML — inline
29
+ # makes the whole config self-contained for upload.
30
+ # See examples/meridian.{loci,memory,questions}.yaml.
31
+ Questions: examples/meridian.questions.yaml
32
+ DefaultLociSeed: examples/meridian.loci.yaml
33
+ DefaultMemorySeed: examples/meridian.memory.yaml
34
+
35
+ # ── The episodic "right brain" (three-tier vector store) ───────────────
36
+ Memory:
37
+ MemoryCount: 1
38
+ MemoryConfigs:
39
+ - { Name: mem, Port: 7420 }
40
+ # A node's own Seed overrides DefaultMemorySeed:
41
+ # - { Name: mem2, Port: 7425, Seed: examples/other.memory.yaml }
42
+ # Flags for Memory: [mcp] exposes the MCP surface. (Vector is always on.)
43
+
44
+ # ── The deterministic "left brain" (SQLite facts) ──────────────────────
45
+ # Two flavors of the SAME store, so you can measure what vectors add:
46
+ Loci:
47
+ LociCount: 3
48
+ LociConfigs:
49
+ - { Name: loci-v, Port: 7421, Flags: [vector] } # FTS5 + sqlite-vec hybrid
50
+ - { Name: loci-nv, Port: 7422 } # FTS5-only (the nano floor)
51
+ # A NEGATIVE (decoy) store: seeded with ONLY its own Seed (never the
52
+ # default corpus), it stands alone (don't fan it from a measured SCC), and
53
+ # the Eval tab reads it as ✓ stayed quiet / ✗ leaked — a leak test that
54
+ # proves off-topic data doesn't surface on your questions.
55
+ - { Name: decoy, Port: 7429, NegativeTest: true, Seed: examples/unrelated.yaml }
56
+ # Flags for Loci: [vector] adds sqlite-vec + an embedder (hybrid search);
57
+ # omit it for lexical-only. [mcp] exposes the MCP surface.
58
+
59
+ # ── The callosum (fuses Memory + Loci into one briefing packet) ────────
60
+ Corpus:
61
+ CorpusCount: 2
62
+ CorpusConfigs:
63
+ # ⚠ THE ONE LANDMINE: each SCC fans across its OWN Loci flavor.
64
+ # scc-v → loci-v (vector vs vector)
65
+ # scc-nv → loci-nv (lexical vs lexical)
66
+ # Pointing both SCCs at the same Loci silently poisons the comparison —
67
+ # the compiler warns if two Corpus nodes end up with identical store sets.
68
+ - { Name: scc-v, Port: 7424, Stores: [{ Store: loci-v }, { Store: mem }] }
69
+ - { Name: scc-nv, Port: 7423, Stores: [{ Store: loci-nv }, { Store: mem }] }
70
+ # Per-store fusion weight is optional and defaults to 1.0:
71
+ # Stores: [{ Store: loci-v }, { Store: mem, Weight: 0.5 }]
72
+
73
+ # ── Notes ──────────────────────────────────────────────────────────────
74
+ # • Names must be globally unique across ALL node types; ports too.
75
+ # • A Corpus can only reference Loci/Memory stores — never another Corpus.
76
+ # • Any NON-negative store you don't reference in a Corpus gets swept into an
77
+ # auto-generated catch-all Corpus, so nothing is silently stranded. A
78
+ # negative store stands alone (it's evaluated as its own column).
79
+ # • Declare a Count higher than the configs you list and the compiler fills
80
+ # the gap with auto-named, auto-ported nodes.