feat: mirror tag-vocabulary summary and note-type validation

Ports two changes from the knowledge-base vault this skeleton is extracted
from, keeping the shared scripts byte-identical across the pair:

- `graph_summary` reports `by_tag` and `tag_count`, sized by `topTags`, and
  drops the `by_type`/`by_status` breakdowns. An MCP-only agent could not
  otherwise discover the tag spellings that `graph_search_nodes` matches
  verbatim.
- `generate-graph.py --validate` now enforces a controlled vocabulary for a
  note's `type`, which is where the drift signal `by_type` used to give now
  lives. `AGENT_QUERY_GUIDE.md`, both skills and the-knowing-one quote the
  enforced enum.

`tests/test_generate_graph.py` is new; `make test` is 50 tests. Regenerating
the graph also drops a stale `GLOBAL_AGENTS` node for a file this skeleton
does not carry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Manuel Ganter
2026-09-14 11:37:02 +02:00
co-authored by Claude Opus 5
parent 638cbe6b39
commit 1cb6369282
10 changed files with 316 additions and 52 deletions
+92 -6
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env python3
"""Tests for the knowledge-base MCP server's write and git tools.
"""Tests for the knowledge-base MCP server's query, write and git tools.
Covers the tools an ingesting agent needs when the MCP server is its only way
into the vault: creating notes, editing notes, capturing raw bytes (local files
and downloads), committing staged paths, and pushing. Each test drives the
server through `_call_tool` against a throwaway repository so no test touches
the real vault.
Covers the tools an agent needs when the MCP server is its only way into the
vault: orienting with graph_summary, creating notes, editing notes, capturing
raw bytes (local files and downloads), committing staged paths, and pushing.
Each test drives the server through `_call_tool` against a throwaway repository
so no test touches the real vault.
"""
from __future__ import annotations
@@ -430,3 +430,89 @@ class TestReadNoteBinary(ToolTestCase):
if __name__ == "__main__":
unittest.main()
class TestGraphSummary(ToolTestCase):
"""graph_summary is the agent's vocabulary lookup, not a stats dump.
Tags are the vault's only high-cardinality facet and the `tag` filter of
graph_search_nodes matches them verbatim, so the summary has to name them.
"""
def write_graph(self, nodes: list) -> None:
self.state.graph_path.parent.mkdir(parents=True, exist_ok=True)
self.state.graph_path.write_text(
json.dumps(
{
"version": 1,
"generated_at": "2026-09-14T00:00:00Z",
"nodes": nodes,
"edges": [],
}
),
encoding="utf-8",
)
def node(self, node_id: str, **fields) -> dict:
base = {
"id": node_id,
"title": node_id,
"path": f"03_resources/{node_id}.md",
"repo": "global",
"type": "note",
"status": "active",
"tags": [],
"inbound_count": 0,
}
base.update(fields)
return base
def test_counts_tags_and_orders_them_by_frequency(self) -> None:
self.write_graph(
[
self.node("a", tags=["kcp", "flux"]),
self.node("b", tags=["kcp"]),
self.node("c", tags=["kcp", "helm"]),
]
)
payload = self.structured("graph_summary")
self.assertEqual(
payload["by_tag"],
[
{"tag": "kcp", "count": 3},
{"tag": "flux", "count": 1},
{"tag": "helm", "count": 1},
],
)
def test_reports_the_distinct_tag_total_even_when_truncated(self) -> None:
self.write_graph(
[self.node(f"n{i}", tags=[f"tag-{i}"]) for i in range(10)]
)
payload = self.structured("graph_summary", topTags=3)
self.assertEqual(len(payload["by_tag"]), 3)
self.assertEqual(payload["tag_count"], 10)
def test_drops_the_type_and_status_facets(self) -> None:
self.write_graph([self.node("a"), self.node("b", status="deprecated")])
payload = self.structured("graph_summary")
self.assertNotIn("by_type", payload)
self.assertNotIn("by_status", payload)
def test_keeps_repo_counts_and_most_linked_notes(self) -> None:
self.write_graph(
[
self.node("hub", repo="global", inbound_count=9),
self.node("leaf", repo="kunnus", inbound_count=1),
]
)
payload = self.structured("graph_summary", topLinked=1)
self.assertEqual(payload["by_repo"], {"global": 1, "kunnus": 1})
self.assertEqual([n["id"] for n in payload["top_linked"]], ["hub"])
self.assertEqual(payload["node_count"], 2)
def test_advertises_the_top_tags_argument(self) -> None:
tool = next(
t for t in mcp._tool_definitions() if t["name"] == "graph_summary"
)
self.assertIn("topTags", tool["inputSchema"]["properties"])