ebony-enriching 0.1.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 (31) hide show
  1. ebony_enriching-0.1.0/.github/workflows/release.yml +97 -0
  2. ebony_enriching-0.1.0/.gitignore +36 -0
  3. ebony_enriching-0.1.0/Dockerfile +26 -0
  4. ebony_enriching-0.1.0/LICENSE +21 -0
  5. ebony_enriching-0.1.0/PKG-INFO +353 -0
  6. ebony_enriching-0.1.0/README.md +336 -0
  7. ebony_enriching-0.1.0/docker-compose.yml +30 -0
  8. ebony_enriching-0.1.0/pyproject.toml +70 -0
  9. ebony_enriching-0.1.0/src/ebony_enriching/__init__.py +6 -0
  10. ebony_enriching-0.1.0/src/ebony_enriching/__main__.py +16 -0
  11. ebony_enriching-0.1.0/src/ebony_enriching/app.py +33 -0
  12. ebony_enriching-0.1.0/src/ebony_enriching/config.py +51 -0
  13. ebony_enriching-0.1.0/src/ebony_enriching/mutex.py +66 -0
  14. ebony_enriching-0.1.0/src/ebony_enriching/permissions.py +69 -0
  15. ebony_enriching-0.1.0/src/ebony_enriching/schema.py +261 -0
  16. ebony_enriching-0.1.0/src/ebony_enriching/server.py +202 -0
  17. ebony_enriching-0.1.0/src/ebony_enriching/storage/__init__.py +0 -0
  18. ebony_enriching-0.1.0/src/ebony_enriching/storage/gaps.py +163 -0
  19. ebony_enriching-0.1.0/src/ebony_enriching/storage/markdown.py +83 -0
  20. ebony_enriching-0.1.0/src/ebony_enriching/storage/paths.py +73 -0
  21. ebony_enriching-0.1.0/src/ebony_enriching/tools.py +1145 -0
  22. ebony_enriching-0.1.0/tests/__init__.py +0 -0
  23. ebony_enriching-0.1.0/tests/_mcp_helpers.py +91 -0
  24. ebony_enriching-0.1.0/tests/conftest.py +46 -0
  25. ebony_enriching-0.1.0/tests/test_cross_server.py +311 -0
  26. ebony_enriching-0.1.0/tests/test_experiments.py +260 -0
  27. ebony_enriching-0.1.0/tests/test_gaps.py +230 -0
  28. ebony_enriching-0.1.0/tests/test_proposals.py +432 -0
  29. ebony_enriching-0.1.0/tests/test_schema.py +189 -0
  30. ebony_enriching-0.1.0/tests/test_server.py +221 -0
  31. ebony_enriching-0.1.0/uv.lock +937 -0
@@ -0,0 +1,97 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ['v*']
6
+
7
+ permissions:
8
+ contents: read
9
+ packages: write # GHCR push
10
+ id-token: write # PyPI trusted publishing
11
+
12
+ jobs:
13
+ # Pre-flight checks. Both publish jobs depend on this, so a failed gate
14
+ # prevents *any* artifact from shipping (no half-shipped state).
15
+ gate:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ with:
20
+ fetch-depth: 0 # full history needed for the ancestor check below
21
+
22
+ - name: Install uv
23
+ uses: astral-sh/setup-uv@v3
24
+
25
+ - name: Verify tag matches pyproject version
26
+ run: |
27
+ TAG="${GITHUB_REF#refs/tags/v}"
28
+ PROJECT_VERSION=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
29
+ if [ "$TAG" != "$PROJECT_VERSION" ]; then
30
+ echo "tag $TAG != pyproject $PROJECT_VERSION"
31
+ exit 1
32
+ fi
33
+
34
+ - name: Verify tag is reachable from origin/main
35
+ run: |
36
+ git fetch --no-tags origin main
37
+ if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
38
+ echo "tagged commit $GITHUB_SHA is not on origin/main"
39
+ echo "release tags must come from main — back-merge through, then re-tag from main"
40
+ exit 1
41
+ fi
42
+
43
+ docker:
44
+ needs: gate
45
+ runs-on: ubuntu-latest
46
+ steps:
47
+ - uses: actions/checkout@v4
48
+
49
+ - name: Set up QEMU (arm64 emulation)
50
+ uses: docker/setup-qemu-action@v3
51
+
52
+ - name: Set up Docker Buildx
53
+ uses: docker/setup-buildx-action@v3
54
+
55
+ - name: Log in to GHCR
56
+ uses: docker/login-action@v3
57
+ with:
58
+ registry: ghcr.io
59
+ username: ${{ github.actor }}
60
+ password: ${{ secrets.GITHUB_TOKEN }}
61
+
62
+ - name: Extract metadata (tags + labels)
63
+ id: meta
64
+ uses: docker/metadata-action@v5
65
+ with:
66
+ images: ghcr.io/${{ github.repository }}
67
+ tags: |
68
+ type=semver,pattern={{version}}
69
+ type=semver,pattern={{major}}.{{minor}}
70
+ type=raw,value=latest
71
+
72
+ - name: Build and push (linux/amd64 + linux/arm64)
73
+ uses: docker/build-push-action@v5
74
+ with:
75
+ context: .
76
+ platforms: linux/amd64,linux/arm64
77
+ push: true
78
+ tags: ${{ steps.meta.outputs.tags }}
79
+ labels: ${{ steps.meta.outputs.labels }}
80
+ cache-from: type=gha
81
+ cache-to: type=gha,mode=max
82
+
83
+ pypi:
84
+ needs: gate
85
+ runs-on: ubuntu-latest
86
+ environment: pypi # matches the trusted-publisher config on PyPI
87
+ steps:
88
+ - uses: actions/checkout@v4
89
+
90
+ - name: Install uv
91
+ uses: astral-sh/setup-uv@v3
92
+
93
+ - name: Build wheel + sdist
94
+ run: uv build
95
+
96
+ - name: Publish to PyPI
97
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,36 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ *.egg-info/
8
+ *.egg
9
+ .pytest_cache/
10
+ .ruff_cache/
11
+ .ty_cache/
12
+ .mypy_cache/
13
+
14
+ # Build
15
+ build/
16
+ dist/
17
+
18
+ # venv / uv
19
+ .venv/
20
+ .python-version
21
+
22
+ # IDE
23
+ .vscode/
24
+ .idea/
25
+
26
+ # OS
27
+ .DS_Store
28
+ Thumbs.db
29
+
30
+ # Local test artifacts
31
+ /tmp-ebony/
32
+ /output/
33
+
34
+ # Secrets
35
+ .env
36
+ .env.*
@@ -0,0 +1,26 @@
1
+ FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Resolve deps first so they cache across source changes.
6
+ COPY pyproject.toml uv.lock ./
7
+ RUN --mount=type=cache,target=/root/.cache/uv \
8
+ uv sync --no-dev --no-install-project
9
+
10
+ # README.md is part of the package metadata (pyproject.toml -> readme).
11
+ # uv sync --no-install-project skipped reading it; the second sync
12
+ # (which installs the project itself) does, so it must be present.
13
+ COPY README.md ./
14
+ COPY src/ src/
15
+ RUN --mount=type=cache,target=/root/.cache/uv \
16
+ uv sync --no-dev
17
+
18
+ ENV PYTHONUNBUFFERED=1 \
19
+ EBONY_ENRICHING_DIR=/data \
20
+ PORT=35834 \
21
+ HOST=0.0.0.0
22
+
23
+ EXPOSE 35834
24
+ VOLUME ["/data"]
25
+
26
+ CMD ["uv", "run", "python", "-m", "ebony_enriching"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Parkview Lab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,353 @@
1
+ Metadata-Version: 2.4
2
+ Name: ebony-enriching
3
+ Version: 0.1.0
4
+ Summary: MCP server: the lab notebook substrate (proposals + experiments + gap signals) for ParkviewLab's CoGrind project.
5
+ Author-email: Gary <garycoding@gmail.com>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.13
9
+ Requires-Dist: fastapi
10
+ Requires-Dist: mcp[cli]>=1.27.0
11
+ Requires-Dist: pydantic
12
+ Requires-Dist: python-frontmatter
13
+ Requires-Dist: pyyaml
14
+ Requires-Dist: starlette
15
+ Requires-Dist: uvicorn[standard]
16
+ Description-Content-Type: text/markdown
17
+
18
+ # ebony-enriching
19
+
20
+ MCP server: **the lab notebook substrate** (proposals + experiments + gap signals) for ParkviewLab's [CoGrind](https://github.com/ParkviewLab/cobalt-grinding) project.
21
+
22
+ Sister to [`smalt-mcp`](https://github.com/ParkviewLab/smalt-mcp): smalt-mcp is the **library** (canonical knowledge); ebony-enriching is the **lab notebook** (research-in-flight). Both substrates have zero outbound dependencies — cobalt-grinding's cognitive agents orchestrate any cross-substrate flow.
23
+
24
+ ## Status
25
+
26
+ **v0.1 surface complete.** 13 tools across 2 permission tiers, covering the full proposal / experiment / gap lifecycle. End-to-end orchestration with smalt-mcp is exercised by the integration tests. Awaiting the v0.1.0 release tag.
27
+
28
+ - **READ_ONLY (6):** `status`, `read_proposal`, `list_proposals`, `read_experiment`, `list_experiments`, `list_gaps`
29
+ - **READ_WRITE (7):** `bootstrap`, `write_proposal`, `update_proposal_status`, `supersede_proposal`, `write_experiment`, `add_gap`, `remove_gap`
30
+
31
+ No `REMOVE_DESTRUCTIVE` tier in v0 — lab-notebook semantics are append-only with status transitions (don't delete proposals, transition to `rejected`; don't delete experiments, they're the historical record). Gaps are the one exception: `remove_gap` exists because a gap is a transient signal that gets resolved when the answering work lands.
32
+
33
+ ## Lab notebook
34
+
35
+ A scientist keeps two artifacts: a **library** of established knowledge (textbooks, published papers, vetted references) and a **lab notebook** where research-in-flight lives (observations, hypotheses, tested predictions, unanswered questions). The two have different rules — the library is canonical and citable, the notebook is messy and dated. cobalt-grinding mirrors this split across two MCP servers:
36
+
37
+ | Concept | Library | Lab notebook |
38
+ |---|---|---|
39
+ | **Server** | `smalt-mcp` | `ebony-enriching` |
40
+ | **Substrate dir** | `~/Documents/Smalt/` | `~/Documents/EbonyEnriching/` |
41
+ | **Storage** | LanceDB + markdown (hybrid FTS + vector + alias search) | Filesystem + markdown only (filesystem walks) |
42
+ | **Lifecycle** | pages stabilize toward canonical state; old versions superseded by new | proposals flow through `proposed → under_test → validated → applied \| rejected`; experiments accrue; gaps queue and drain |
43
+ | **Tier of truth** | one-and-only canonical store | working memory that publishes to the library when validated |
44
+
45
+ **ebony-enriching records; it doesn't decide.** Lifecycle policy (when to mark a proposal `rejected`, when to auto-test vs. defer to user review, what counts as falsifiability) lives in cobalt-grinding's cognitive agents reading the substrate's `POLICY.md`. The MCP tools enforce **storage** correctness (path safety, atomicity, schema validation) and nothing else.
46
+
47
+ ## Run
48
+
49
+ Same five-mode pattern as [`smalt-mcp`](https://github.com/ParkviewLab/smalt-mcp) and [`deco-assaying`](https://github.com/ParkviewLab/deco-assaying). Pick whichever fits.
50
+
51
+ | Mode | When to use |
52
+ |---|---|
53
+ | 1. `uvx` (one-off) | Try it once, no install. |
54
+ | 2. `uv tool install` (pinned daemon) | Run it occasionally, want it on `$PATH`. |
55
+ | 3. macOS LaunchAgent | Persistent daemon on a Mac. |
56
+ | 4. Linux systemd user unit | Persistent daemon on Linux. |
57
+ | 5. Docker / docker compose | Container deployment. |
58
+
59
+ ### Prereqs
60
+
61
+ - **uv-based modes (1–4)** need [`uv`](https://docs.astral.sh/uv/) and `git`. uv ships a portable Python 3.13, so no system Python install required.
62
+
63
+ ```bash
64
+ curl -LsSf https://astral.sh/uv/install.sh | sh
65
+ ```
66
+
67
+ - **Docker mode (5)** needs `docker` (or compatible). The image bundles Python 3.13; nothing else on the host.
68
+
69
+ In every mode the server listens on `PORT` (default `35834` — one above smalt-mcp's `35833`). Sanity-check it's up:
70
+
71
+ ```bash
72
+ curl http://127.0.0.1:35834/health
73
+ ```
74
+
75
+ ---
76
+
77
+ ### 1. One-off — `uvx`
78
+
79
+ `uvx` resolves the package into a temporary venv and runs it once. Nothing persists between runs.
80
+
81
+ ```bash
82
+ uvx ebony-enriching # latest release
83
+ uvx ebony-enriching@0.1.0 # pin a specific version
84
+
85
+ # With env vars (custom data dir, restricted scope):
86
+ EBONY_ENRICHING_DIR=$HOME/EbonyEnriching \
87
+ EBONY_SCOPE=read_only \
88
+ uvx ebony-enriching
89
+ ```
90
+
91
+ Good for kicking the tires or running on a CI box where you don't want to leave anything on disk.
92
+
93
+ ### 2. Pinned daemon — `uv tool install`
94
+
95
+ Installs the `ebony-enriching` command on your `$PATH`, isolated in its own venv that uv manages. Faster startup than `uvx` (no resolve on each run).
96
+
97
+ ```bash
98
+ uv tool install ebony-enriching
99
+ ebony-enriching # foreground server
100
+ ```
101
+
102
+ To upgrade: `uv tool upgrade ebony-enriching`. To remove: `uv tool uninstall ebony-enriching`.
103
+
104
+ For a real "always running" setup, see the launchd / systemd recipes below.
105
+
106
+ ### 3. macOS persistent daemon (launchd)
107
+
108
+ After `uv tool install ebony-enriching`, register a LaunchAgent so the daemon starts at login and restarts if it crashes.
109
+
110
+ Save this as `~/Library/LaunchAgents/com.garycoding.ebony-enriching.plist` (replace `CHANGE-ME` with your username):
111
+
112
+ ```xml
113
+ <?xml version="1.0" encoding="UTF-8"?>
114
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
115
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
116
+ <plist version="1.0">
117
+ <dict>
118
+ <key>Label</key>
119
+ <string>com.garycoding.ebony-enriching</string>
120
+
121
+ <key>ProgramArguments</key>
122
+ <array>
123
+ <string>/Users/CHANGE-ME/.local/bin/ebony-enriching</string>
124
+ </array>
125
+
126
+ <key>EnvironmentVariables</key>
127
+ <dict>
128
+ <key>EBONY_ENRICHING_DIR</key>
129
+ <string>/Users/CHANGE-ME/Documents/EbonyEnriching</string>
130
+ <key>EBONY_SCOPE</key>
131
+ <string>read_write</string>
132
+ </dict>
133
+
134
+ <key>RunAtLoad</key><true/>
135
+ <key>KeepAlive</key><true/>
136
+
137
+ <key>StandardOutPath</key>
138
+ <string>/Users/CHANGE-ME/Library/Logs/ebony-enriching.out.log</string>
139
+ <key>StandardErrorPath</key>
140
+ <string>/Users/CHANGE-ME/Library/Logs/ebony-enriching.err.log</string>
141
+ </dict>
142
+ </plist>
143
+ ```
144
+
145
+ Load and start it:
146
+
147
+ ```bash
148
+ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.garycoding.ebony-enriching.plist
149
+ launchctl kickstart -k gui/$(id -u)/com.garycoding.ebony-enriching
150
+
151
+ # Check status:
152
+ launchctl print gui/$(id -u)/com.garycoding.ebony-enriching | head -30
153
+
154
+ # Tail logs:
155
+ tail -f ~/Library/Logs/ebony-enriching.{out,err}.log
156
+
157
+ # Stop / unload:
158
+ launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.garycoding.ebony-enriching.plist
159
+ ```
160
+
161
+ ### 4. Linux persistent daemon (systemd)
162
+
163
+ After `uv tool install ebony-enriching`, register a user-scope systemd unit so no root is required.
164
+
165
+ Save this as `~/.config/systemd/user/ebony-enriching.service`:
166
+
167
+ ```ini
168
+ [Unit]
169
+ Description=ebony-enriching MCP server (lab notebook substrate)
170
+ After=network-online.target
171
+
172
+ [Service]
173
+ Type=simple
174
+ ExecStart=%h/.local/bin/ebony-enriching
175
+ Restart=on-failure
176
+ RestartSec=5
177
+ Environment=EBONY_ENRICHING_DIR=%h/Documents/EbonyEnriching
178
+ Environment=EBONY_SCOPE=read_write
179
+
180
+ [Install]
181
+ WantedBy=default.target
182
+ ```
183
+
184
+ Enable and start:
185
+
186
+ ```bash
187
+ systemctl --user daemon-reload
188
+ systemctl --user enable --now ebony-enriching
189
+
190
+ # Check status:
191
+ systemctl --user status ebony-enriching
192
+
193
+ # Tail logs:
194
+ journalctl --user -u ebony-enriching -f
195
+
196
+ # Stop:
197
+ systemctl --user disable --now ebony-enriching
198
+ ```
199
+
200
+ To keep the daemon running when the user is logged out, enable lingering:
201
+
202
+ ```bash
203
+ loginctl enable-linger "$USER"
204
+ ```
205
+
206
+ ### 5. Docker / GHCR
207
+
208
+ Pull the published multi-arch image (linux/amd64 + linux/arm64) and run it directly:
209
+
210
+ ```bash
211
+ docker pull ghcr.io/parkviewlab/ebony-enriching:latest
212
+
213
+ docker run --rm \
214
+ -p 35834:35834 \
215
+ -e EBONY_SCOPE=read_write \
216
+ -v ebony-data:/data \
217
+ ghcr.io/parkviewlab/ebony-enriching:latest
218
+ ```
219
+
220
+ Pin a specific version with a tag — `:0.1.0`, `:0.1`, or `:latest`. See the [container registry](https://github.com/ParkviewLab/ebony-enriching/pkgs/container/ebony-enriching) for available tags.
221
+
222
+ For a real deployment, copy [`docker-compose.yml`](docker-compose.yml), edit env vars if needed, then:
223
+
224
+ ```bash
225
+ docker compose up -d # start in background
226
+ docker compose logs -f # tail logs
227
+ docker compose pull && docker compose up -d # upgrade
228
+ docker compose down # stop, keep volume
229
+ docker compose down -v # stop and drop the volume
230
+ ```
231
+
232
+ ### From source (for development)
233
+
234
+ ```bash
235
+ git clone https://github.com/ParkviewLab/ebony-enriching.git
236
+ cd ebony-enriching
237
+ uv sync
238
+ EBONY_ENRICHING_DIR=~/Documents/EbonyEnriching uv run python -m ebony_enriching
239
+ ```
240
+
241
+ ## Endpoints
242
+
243
+ - `POST /sse` — MCP Streamable HTTP transport. Tools.
244
+ - `GET /health` — liveness probe (`{ok, version, uptime_seconds}`).
245
+ - `GET /admin/version` — server identity + scope + configured EbonyEnriching path.
246
+ - `GET /docs` — OpenAPI / Swagger UI for the HTTP routes.
247
+
248
+ HTTP responses are gzipped when the client sends `Accept-Encoding: gzip`.
249
+
250
+ ## MCP tools
251
+
252
+ Two permission tiers controlled by `EBONY_SCOPE`. A caller at tier N sees and may call any tool whose required scope is ≤ N.
253
+
254
+ **`read_only` (6 tools):**
255
+
256
+ - `status` — EbonyEnriching path, existence, single-writer mutex state. Always safe to call.
257
+ - `read_proposal` — read a single proposal by id. Returns full frontmatter + body.
258
+ - `list_proposals` — list proposals, optionally filtered by `system` (subdir), `status` (lifecycle state), or `kind` (`proposal_kind`). Malformed proposals appear with `valid: false` rather than being silently dropped.
259
+ - `read_experiment` — read one experiment record by `(proposal_id, run_timestamp)`. Returns full input + result.
260
+ - `list_experiments` — list experiments. With `proposal_id`, only that proposal's runs; without, all experiments. Returns summary metadata.
261
+ - `list_gaps` — parse `gaps.md` and return all gap entries (id, query, created_at, optional why / source).
262
+
263
+ **`read_write` (+7 tools):**
264
+
265
+ - `bootstrap` — initialize the canonical directory layout at `EBONY_ENRICHING_DIR`; drop in `gaps.md` / `schema/SCHEMA.md` / `schema/POLICY.md` / `config.toml` placeholders. Idempotent — reports only what was newly created.
266
+ - `write_proposal` — write a proposal to `proposals/<subdir>/<id>.md`. Schema-related kinds (`schema_addition` / `schema_drift` / `schema_removal`) route to `proposals/schema/`; others to `proposals/<proposed_by>/`. Atomic write.
267
+ - `update_proposal_status` — update a proposal's lifecycle fields (`status`, optional `test_status`, `test_cost`) in-place. RMW under the single-writer mutex. Validates values against their StrEnum but does NOT enforce transition rules — that policy lives in cobalt-grinding's agents.
268
+ - `supersede_proposal` — link two proposals: sets `superseded_by: new_id` on `old_id` and `supersedes: old_id` on `new_id`. Both must already exist; does not transition statuses.
269
+ - `write_experiment` — record one run of a proposal's prediction test at `experiments/<proposal_id>/<run_timestamp>.md`. `run_timestamp` defaults to now (UTC). Doesn't check that the referenced proposal exists.
270
+ - `add_gap` — record an unanswered query in `gaps.md`. `gap_id` is derived from the query (SHA-256 hex, truncated to 8 chars; lowercase + collapsed whitespace), so adding the same query twice is idempotent (returns `already_present: true`).
271
+ - `remove_gap` — drop a gap bullet by id. Idempotent — unknown id returns `removed: 0`.
272
+
273
+ For the canonical-page storage surface (writing `EntityPage` / `ConceptPage` / `SourcePage` / `SynthesisPage`, hybrid search, link / claim management), use [`smalt-mcp`](https://github.com/ParkviewLab/smalt-mcp) — the library substrate. cobalt-grinding's cognitive agents orchestrate any cross-substrate flow.
274
+
275
+ ## On-disk layout
276
+
277
+ ```
278
+ $EBONY_ENRICHING_DIR/
279
+ ├── proposals/
280
+ │ ├── schema/ # schema_addition / schema_drift / schema_removal kinds
281
+ │ ├── cogitate/ # written by the Cogitate cognitive system
282
+ │ ├── curate/ # written by Curate
283
+ │ ├── research/ # written by Research
284
+ │ ├── toolsmith/ # written by Toolsmith
285
+ │ └── converse/ # written by Converse (novelty detector)
286
+ ├── experiments/
287
+ │ └── <proposal-id>/
288
+ │ └── <run-timestamp>.md
289
+ ├── gaps.md # one bullet per open gap (managed by add_gap / remove_gap)
290
+ ├── schema/
291
+ │ ├── SCHEMA.md # human-readable narrative of proposal / experiment / gap shape
292
+ │ └── POLICY.md # human-readable falsifiability + cost-tier policy
293
+ └── config.toml # reserved (empty in v0)
294
+ ```
295
+
296
+ `bootstrap` materializes this layout. Proposal subdirs route by `proposal_kind` (schema-related kinds land in `proposals/schema/`; everything else lands in `proposals/<proposed_by>/`).
297
+
298
+ ## Configuration
299
+
300
+ | Env var | Default | Purpose |
301
+ |---|---|---|
302
+ | `PORT` | `35834` | HTTP listen port. |
303
+ | `HOST` | `0.0.0.0` | HTTP bind address. |
304
+ | `EBONY_ENRICHING_DIR` | `~/Documents/EbonyEnriching` | Path to the lab notebook this server wraps. Call `bootstrap` once to materialize the canonical layout. `EBONY_DIR` is accepted as a shorter alias. |
305
+ | `EBONY_SCOPE` | `read_write` | `read_only`, `read_write`, or `remove_destructive`. Tiered: caller at tier N sees every tool whose required scope is ≤ N. (`remove_destructive` is reserved — no v0 tool requires it.) |
306
+ | `EBONY_INTERNAL_TOKEN` | *(unset)* | Reserved for future per-client scope routing; not yet enforced. |
307
+
308
+ ## Why a separate MCP server (not part of smalt-mcp)
309
+
310
+ The two storage substrates have different shapes:
311
+
312
+ - **Smalt** is LanceDB-backed (hybrid FTS + vector + alias search over thousands of pages); ships an embedder; the `smalt-mcp` package carries hundreds of MB of deps.
313
+ - **Lab notebook** is filesystem-text-only (filesystem walks over hundreds of proposals/experiments/gaps); no embedder, no LanceDB; the `ebony-enriching` package is small.
314
+
315
+ Bundling them produced a server that paid the search-stack cost for a workload that didn't need it, and made the two surfaces' release cadences coupled when they shouldn't be. smalt-mcp's storage tools stabilize toward 1.0; ebony-enriching's schema will iterate as cobalt-grinding's cognitive systems land. Splitting them into two MCP children — both supervised by `cogrindd` — gives each substrate its own lifecycle.
316
+
317
+ See [`cobalt-grinding/docs/plan.md`](https://github.com/ParkviewLab/cobalt-grinding/blob/main/docs/plan.md) → *Decisions made* for the full rationale.
318
+
319
+ ## Tests
320
+
321
+ Default (fast — ~0.3s, the full v0.1 tool surface in-process):
322
+
323
+ ```sh
324
+ uv run pytest
325
+ ```
326
+
327
+ **Integration tests** exercise both ebony-enriching AND a real smalt-mcp subprocess to verify the cobalt-grinding orchestration pattern (write proposal → validate → cross-substrate publish → mark applied). Default `pytest` skips them; run explicitly:
328
+
329
+ ```sh
330
+ uv run pytest -m integration
331
+ ```
332
+
333
+ The integration fixture resolves smalt-mcp's project directory in this order:
334
+ 1. `SMALT_MCP_PROJECT` env var (explicit override)
335
+ 2. `../../smalt-mcp/worktrees/main` relative to this repo (the [ParkviewLab worktree convention](https://github.com/ParkviewLab/dev-tools))
336
+
337
+ Skipped with a clear message if neither resolves.
338
+
339
+ ## Releasing
340
+
341
+ Tag-driven via the release workflow on push of a `v*` tag. Use the [`ParkviewLab/dev-tools`](https://github.com/ParkviewLab/dev-tools) helpers — they enforce the SSOT-tag-CI loop (`pyproject.toml` is the only place the version lives; CI verifies the pushed tag matches before publishing).
342
+
343
+ ```sh
344
+ git bump patch # 0.1.0 → 0.1.1, committed
345
+ git release # annotated tag v0.1.1 from pyproject.toml
346
+ git push --follow-tags # CI fires
347
+ ```
348
+
349
+ Don't have the helpers? Install once: `git clone https://github.com/ParkviewLab/dev-tools.git ~/dev-tools && cd ~/dev-tools && ./install.sh`.
350
+
351
+ ## License
352
+
353
+ MIT. See `LICENSE`.