docs+tests: fix CI after README slim (#875)

The regression-guard tests added in #835 were pinned to the old
README shape (tool table + file-reference table). When #897 slimmed
the README and moved that content to the website, three tests
started failing:

  TestReadmeToolsExistInCode.test_every_readme_tool_exists_in_tools_dict
  TestNoUnlistedTools.test_no_undocumented_tools
  TestReadmeDialectNotLossless.test_readme_dialect_line_not_lossless

Changes in this commit:

1. Update the 3 tests to track the new canonical docs surfaces
   - Tool list -> website/reference/mcp-tools.md
     (tests parse `### \`mempalace_xxx\`` headings instead of
     markdown table rows).
   - dialect.py lossless disclaimer -> website/reference/modules.md
     (any line mentioning dialect.py must not also say "lossless").

2. Fix the website to make "no undocumented tools" true
   Add the 10 tools that existed in TOOLS but were missing from
   website/reference/mcp-tools.md (create_tunnel, delete_tunnel,
   follow_tunnels, list_tunnels, get_drawer, list_drawers,
   update_drawer, hook_settings, memories_filed_away, reconnect).
   Page header now correctly says "all 29 MCP tools".

3. Align pre-commit ruff pin to match CI (0.4.x)
   .pre-commit-config.yaml was pinning ruff v0.9.0, while
   .github/workflows/ci.yml installs ruff>=0.4.0,<0.5. The two
   formatters produce incompatible output (e.g. v0.9.0 reformats
   `assert (x), msg` -> `assert x, (msg)` in a way v0.4.x rejects),
   which would cause the pre-commit hook to modify files that CI
   then flags as unformatted. Pinning the hook to v0.4.10 keeps
   the dev loop and CI in lock-step.

Full suite: 887 passed, 0 failed.
This commit is contained in:
Igor Lins e Silva
2026-04-14 21:59:55 -03:00
parent bf3b9c5979
commit 107685930d
3 changed files with 191 additions and 32 deletions
+4 -1
View File
@@ -1,6 +1,9 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.0 # Keep in lock-step with the ruff version pinned in .github/workflows/ci.yml
# (>=0.4.0,<0.5). Using a newer rev here produces a different formatter
# output than CI and breaks `ruff format --check` in the lint job.
rev: v0.4.10
hooks: hooks:
- id: ruff - id: ruff
args: [--fix] args: [--fix]
+54 -30
View File
@@ -22,6 +22,8 @@ import pytest
REPO_ROOT = Path(__file__).resolve().parent.parent REPO_ROOT = Path(__file__).resolve().parent.parent
MEMPALACE_PKG = REPO_ROOT / "mempalace" MEMPALACE_PKG = REPO_ROOT / "mempalace"
README_PATH = REPO_ROOT / "README.md" README_PATH = REPO_ROOT / "README.md"
MCP_TOOLS_DOC_PATH = REPO_ROOT / "website" / "reference" / "mcp-tools.md"
MODULES_DOC_PATH = REPO_ROOT / "website" / "reference" / "modules.md"
def _read(path: Path) -> str: def _read(path: Path) -> str:
@@ -40,10 +42,15 @@ def _tools_dict_keys() -> list:
return re.findall(r'"(mempalace_\w+)":\s*\{', src) return re.findall(r'"(mempalace_\w+)":\s*\{', src)
def _readme_tool_table_names() -> list: def _doc_tool_names() -> list:
"""Return tool names listed in the README's MCP tool table.""" """Return the list of tool names documented in the MCP tools reference.
readme = _readme()
return re.findall(r"^\| `(mempalace_\w+)`", readme, re.MULTILINE) The MCP tool table lived in README.md prior to the #875 rewrite; it now
lives in website/reference/mcp-tools.md (linked from README). Each tool
is introduced by a level-3 heading `### \\`mempalace_xxx\\``.
"""
doc = _read(MCP_TOOLS_DOC_PATH)
return re.findall(r"^###\s+`(mempalace_\w+)`", doc, re.MULTILINE)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -77,19 +84,28 @@ class TestToolCount:
class TestReadmeToolsExistInCode: class TestReadmeToolsExistInCode:
"""Every tool name in the README tool table must be a key in TOOLS.""" """Every tool name documented in the MCP tools reference must be a key in TOOLS."""
def test_every_readme_tool_exists_in_tools_dict(self): def test_every_readme_tool_exists_in_tools_dict(self):
"""Claim: README lists tools like mempalace_get_aaak_spec. """Claim: the MCP tools reference (website/reference/mcp-tools.md)
Each one must actually be registered in the TOOLS dict.""" lists tools like mempalace_get_aaak_spec. Each one must actually be
code_tools = set(_tools_dict_keys()) registered in the TOOLS dict in mempalace/mcp_server.py.
readme_tools = _readme_tool_table_names()
assert len(readme_tools) > 0, "Could not parse any tools from README table"
missing = [t for t in readme_tools if t not in code_tools] Pre-#875 this parsed the tool table that lived in README.md; that
table has moved to the website docs and README now links out.
"""
code_tools = set(_tools_dict_keys())
doc_tools = _doc_tool_names()
assert len(doc_tools) > 0, (
f"Could not parse any tools from {MCP_TOOLS_DOC_PATH.relative_to(REPO_ROOT)} "
f"— expected `### \\`mempalace_xxx\\`` headings."
)
missing = [t for t in doc_tools if t not in code_tools]
assert missing == [], ( assert missing == [], (
f"README lists tools that don't exist in TOOLS dict: {missing}. " f"Docs list tools that don't exist in TOOLS dict: {missing}. "
f"Either add them to mcp_server.py or remove them from README." f"Either add them to mcp_server.py or remove them from "
f"{MCP_TOOLS_DOC_PATH.relative_to(REPO_ROOT)}."
) )
@@ -99,18 +115,20 @@ class TestReadmeToolsExistInCode:
class TestNoUnlistedTools: class TestNoUnlistedTools:
"""Every tool in the TOOLS dict should be documented in the README.""" """Every tool in the TOOLS dict should be documented in the MCP tools reference."""
def test_no_undocumented_tools(self): def test_no_undocumented_tools(self):
"""Claim: README's tool table is complete. """Claim: the MCP tools reference
Any tool in TOOLS but not in README is undocumented.""" (website/reference/mcp-tools.md) is complete. Any tool in TOOLS
but not documented there is undocumented on the public surface."""
code_tools = set(_tools_dict_keys()) code_tools = set(_tools_dict_keys())
readme_tools = set(_readme_tool_table_names()) doc_tools = set(_doc_tool_names())
undocumented = sorted(code_tools - readme_tools) undocumented = sorted(code_tools - doc_tools)
assert undocumented == [], ( assert undocumented == [], (
f"Tools in TOOLS dict but missing from README: {undocumented}. " f"Tools in TOOLS dict but missing from docs: {undocumented}. "
f"Add rows for these to the tool table in README.md." f"Add sections for these to "
f"{MCP_TOOLS_DOC_PATH.relative_to(REPO_ROOT)}."
) )
@@ -485,21 +503,27 @@ class TestDialectNotLossless:
class TestReadmeDialectNotLossless: class TestReadmeDialectNotLossless:
"""README's file reference table must not say dialect.py is lossless.""" """The file-reference documentation must not say dialect.py is lossless.
Pre-#875 this lived in a README.md file table; it now lives in
website/reference/modules.md. The April 7 correction established that
AAAK is a lossy abbreviation system, not lossless compression, and
every docs surface that describes dialect.py must respect that.
"""
def test_readme_dialect_line_not_lossless(self): def test_readme_dialect_line_not_lossless(self):
"""Claim: April 7 correction applied to README file table. doc = _read(MODULES_DOC_PATH)
The dialect.py row must not say 'lossless'.""" # Any line mentioning dialect.py (narrative or table) must not call it lossless
readme = _readme() dialect_lines = [line for line in doc.splitlines() if "dialect.py" in line]
# Find the line with dialect.py in the file reference table assert len(dialect_lines) > 0, (
dialect_lines = [ f"Could not find dialect.py in "
line for line in readme.splitlines() if "dialect.py" in line and "|" in line f"{MODULES_DOC_PATH.relative_to(REPO_ROOT)}. "
] f"Expected at least one reference."
assert len(dialect_lines) > 0, "Could not find dialect.py in README file table" )
for line in dialect_lines: for line in dialect_lines:
assert "lossless" not in line.lower(), ( assert "lossless" not in line.lower(), (
f"README file table still says dialect.py is lossless: {line.strip()!r}. " f"Docs still call dialect.py lossless: {line.strip()!r}. "
f"After April 7 correction, this must say 'lossy' or remove the lossless claim." f"After April 7 correction, this must say 'lossy' or remove the lossless claim."
) )
+133 -1
View File
@@ -1,6 +1,6 @@
# MCP Tools Reference # MCP Tools Reference
Detailed parameter schemas for all 19 MCP tools. Detailed parameter schemas for all 29 MCP tools.
## Palace — Read Tools ## Palace — Read Tools
@@ -114,6 +114,48 @@ Delete a drawer by ID. Irreversible.
--- ---
### `mempalace_get_drawer`
Fetch a single drawer by ID — returns full content and metadata.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `drawer_id` | string | **Yes** | ID of the drawer to fetch |
**Returns:** `{ drawer: { id, wing, room, content, ... } }`
---
### `mempalace_list_drawers`
List drawers with pagination. Optional wing/room filter. Returns IDs, wings, rooms, and content previews.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `wing` | string | No | Filter by wing |
| `room` | string | No | Filter by room |
| `limit` | integer | No | Max results per page (default 20, max 100) |
| `offset` | integer | No | Offset for pagination (default 0) |
**Returns:** `{ drawers: [...], total, limit, offset }`
---
### `mempalace_update_drawer`
Update an existing drawer's content and/or metadata (wing, room). Fetches the existing drawer first; returns an error if not found.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `drawer_id` | string | **Yes** | ID of the drawer to update |
| `content` | string | No | New content (omit to keep existing) |
| `wing` | string | No | New wing (omit to keep existing) |
| `room` | string | No | New room (omit to keep existing) |
**Returns:** `{ success, drawer_id, updated_fields }`
---
## Knowledge Graph Tools ## Knowledge Graph Tools
### `mempalace_kg_query` ### `mempalace_kg_query`
@@ -221,6 +263,61 @@ Palace graph overview: nodes, tunnels, edges, connectivity.
--- ---
### `mempalace_create_tunnel`
Create a cross-wing tunnel linking two palace locations. Use when content in one project relates to another — e.g., an API design in `project_api` connects to a database schema in `project_database`.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `source_wing` | string | **Yes** | Wing of the source |
| `source_room` | string | **Yes** | Room in the source wing |
| `target_wing` | string | **Yes** | Wing of the target |
| `target_room` | string | **Yes** | Room in the target wing |
| `label` | string | No | Description of the connection |
| `source_drawer_id` | string | No | Specific source drawer ID |
| `target_drawer_id` | string | No | Specific target drawer ID |
**Returns:** `{ success, tunnel_id, source, target }`
---
### `mempalace_list_tunnels`
List all explicit cross-wing tunnels. Optionally filter by wing.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `wing` | string | No | Filter tunnels by wing (source or target) |
**Returns:** `{ tunnels: [...], count }`
---
### `mempalace_delete_tunnel`
Delete an explicit tunnel by its ID.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `tunnel_id` | string | **Yes** | Tunnel ID to delete |
**Returns:** `{ success, tunnel_id }`
---
### `mempalace_follow_tunnels`
Follow tunnels from a room to see what it connects to in other wings. Returns connected rooms with drawer previews.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `wing` | string | **Yes** | Wing to start from |
| `room` | string | **Yes** | Room to follow tunnels from |
**Returns:** `[{ wing, room, label, previews }]`
---
## Agent Diary Tools ## Agent Diary Tools
### `mempalace_diary_write` ### `mempalace_diary_write`
@@ -247,3 +344,38 @@ Read recent diary entries.
| `last_n` | integer | No | Number of recent entries (default: 10) | | `last_n` | integer | No | Number of recent entries (default: 10) |
**Returns:** `{ agent, entries: [{ date, timestamp, topic, content }], total, showing }` **Returns:** `{ agent, entries: [{ date, timestamp, topic, content }], total, showing }`
---
## System Tools
### `mempalace_hook_settings`
Get or set auto-save hook behaviour. `silent_save=true` saves directly without MCP-level clutter; `silent_save=false` uses the legacy blocking path. `desktop_toast=true` surfaces a desktop notification when a save completes. Call with no arguments to view the current settings.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `silent_save` | boolean | No | `true` = silent direct save, `false` = blocking MCP calls |
| `desktop_toast` | boolean | No | `true` = show desktop toast via `notify-send` |
**Returns:** `{ silent_save, desktop_toast }`
---
### `mempalace_memories_filed_away`
Check whether a recent palace checkpoint was saved. Returns message count and timestamp of the last save.
**Parameters:** None
**Returns:** `{ filed, message_count, timestamp }`
---
### `mempalace_reconnect`
Force a reconnect to the palace database. Use this after external scripts or CLI commands modified the palace directly, which can leave the in-memory HNSW index stale.
**Parameters:** None
**Returns:** `{ success, palace_path }`