From f0d28b4a1ed80f113adb6aff92c17b3cd8064c05 Mon Sep 17 00:00:00 2001 From: Danilo Reyes Date: Sun, 1 Feb 2026 11:31:48 -0600 Subject: [PATCH] sync-docs --- docs/reference/mcp-server.md | 32 ++++++++-- .../mcp-server/src/mcp_server/docs_sync.py | 63 ++++++++++++++----- scripts/mcp-server/src/mcp_server/tools.py | 8 +-- specs/001-ai-docs/research.md | 5 ++ 4 files changed, 83 insertions(+), 25 deletions(-) diff --git a/docs/reference/mcp-server.md b/docs/reference/mcp-server.md index b3a2e19..b5a40c7 100644 --- a/docs/reference/mcp-server.md +++ b/docs/reference/mcp-server.md @@ -5,13 +5,33 @@ - Transport: MCP protocol over stdio via the official Python SDK; no network listeners; enforced local-only guard. - Source: `scripts/mcp-server/`; connect via the `nixos-mcp` wrapper package. +## Sources of Truth +- Constitution: `docs/constitution.md` is authoritative for AI rules and workflows. +- Conflict handling: when human docs differ, update both and record the resolution in `specs/001-ai-docs/research.md`. +- Reference map: `docs/reference/index.md` is the navigation index for paths and responsibilities. +- Playbooks: `docs/playbooks/*.md` are the repeatable workflow guides; follow the template structure. +- Planning artifacts: `specs/001-ai-docs/` captures decisions, plans, and change history. + ## Tool Catalog -- `show-constitution`: Display `docs/constitution.md` to confirm authoritative rules. -- `list-playbooks`: List available playbooks under `docs/playbooks/` for common tasks. -- `show-reference`: Show `docs/reference/index.md` to navigate repo guidance. -- `search-docs`: Search the docs set for a query (param: `query`). -- `list-mcp-tasks`: Show MCP feature task list from `specs/001-mcp-server/tasks.md`. -- `sync-docs`: Compare tool catalog against documented anchors for drift reporting. +### show-constitution +- Purpose: Display `docs/constitution.md` to confirm authoritative rules. +- Docs anchor: `docs/constitution.md` → `#ai-constitution-for-the-nixos-repository`. +### list-playbooks +- Purpose: List available playbooks under `docs/playbooks/` for common tasks. +- Docs anchor: `docs/playbooks/template.md` → `#playbook-template`. +### show-reference +- Purpose: Show `docs/reference/index.md` to navigate repo guidance. +- Docs anchor: `docs/reference/index.md` → `#reference-map`. +### search-docs +- Purpose: Search the docs set for a query. +- Inputs: `query` (string). +- Docs anchor: `docs/reference/mcp-server.md` → `#search-docs`. +### list-mcp-tasks +- Purpose: Show MCP feature task list from `specs/001-mcp-server/tasks.md`. +- Docs anchor: `specs/001-mcp-server/tasks.md` → `#tasks-mcp-server-for-repo-maintenance`. +### sync-docs +- Purpose: Compare tool catalog against documented anchors for drift reporting. +- Docs anchor: `docs/reference/mcp-server.md` → `#sync-docs`. ## Invocation - Start server: `nixos-mcp` (stdio mode). diff --git a/scripts/mcp-server/src/mcp_server/docs_sync.py b/scripts/mcp-server/src/mcp_server/docs_sync.py index 2010584..c3be9ef 100644 --- a/scripts/mcp-server/src/mcp_server/docs_sync.py +++ b/scripts/mcp-server/src/mcp_server/docs_sync.py @@ -2,10 +2,13 @@ from __future__ import annotations +import re from pathlib import Path from mcp_server.tools import DocsPath, tool_catalog +_REFERENCE_DOC = DocsPath / "reference" / "mcp-server.md" + def check_catalog_parity() -> dict[str, object]: """Compare documented anchors with the live tool catalog and report drift.""" @@ -13,14 +16,14 @@ def check_catalog_parity() -> dict[str, object]: missing_in_catalog: list[str] = [] mismatches: list[dict[str, str]] = [] - docs_tools = _doc_anchors() + docs_tools = _doc_tool_anchors() catalog = tool_catalog() + catalog_names = {tool.name for tool in catalog} for tool in catalog: - anchor_key = _anchor_key(tool.docs_anchor.path, tool.docs_anchor.anchor) - if anchor_key not in docs_tools: + if not _anchor_exists(tool.docs_anchor.path, tool.docs_anchor.anchor): missing_in_docs.append(tool.name) for anchor_key, tool_name in docs_tools.items(): - if tool_name not in {t.name for t in catalog}: + if tool_name not in catalog_names: missing_in_catalog.append(anchor_key) return { "status": ( @@ -34,21 +37,51 @@ def check_catalog_parity() -> dict[str, object]: } -def _doc_anchors() -> dict[str, str]: - """Derive anchors from docs files to detect missing catalog entries.""" +def _doc_tool_anchors() -> dict[str, str]: + """Derive documented tool anchors from the MCP reference page.""" anchors: dict[str, str] = {} - files = list(DocsPath.rglob("*.md")) - for path in files: - tool_name = _derive_tool_name(path) - anchor_id = path.stem - anchors[_anchor_key(path, anchor_id)] = tool_name + for heading in _heading_texts(_REFERENCE_DOC, prefix="### "): + tool_name = heading + anchor_id = _slugify_heading(heading) + anchors[_anchor_key(_REFERENCE_DOC, anchor_id)] = tool_name return anchors -def _derive_tool_name(path: Path) -> str: - """Create a best-effort tool name from a documentation path.""" - parts = path.parts[-3:] - return "-".join(filter(None, parts)).replace(".md", "") +def _anchor_exists(path: Path, anchor: str) -> bool: + """Check whether a heading anchor exists in the target doc.""" + if not path.exists() or path.is_dir(): + return False + return anchor in _heading_anchors(path) + + +def _heading_anchors(path: Path) -> set[str]: + """Collect slugified heading anchors from a markdown file.""" + anchors: set[str] = set() + for heading in _heading_texts(path, prefix="#"): + anchors.add(_slugify_heading(heading)) + return anchors + + +def _heading_texts(path: Path, prefix: str) -> list[str]: + """Return heading text lines matching a prefix.""" + if not path.exists(): + return [] + headings: list[str] = [] + for line in path.read_text().splitlines(): + if not line.startswith(prefix): + continue + heading = line.lstrip("#").strip() + if heading: + headings.append(heading) + return headings + + +def _slugify_heading(text: str) -> str: + """Normalize heading text to a GitHub-style anchor.""" + slug = text.strip().lower() + slug = re.sub(r"[^a-z0-9\s-]", " ", slug) + slug = re.sub(r"\s+", "-", slug) + return re.sub(r"-+", "-", slug).strip("-") def _anchor_key(path: Path, anchor: str) -> str: diff --git a/scripts/mcp-server/src/mcp_server/tools.py b/scripts/mcp-server/src/mcp_server/tools.py index 3f8480c..0c16782 100644 --- a/scripts/mcp-server/src/mcp_server/tools.py +++ b/scripts/mcp-server/src/mcp_server/tools.py @@ -168,17 +168,17 @@ def tool_catalog() -> tuple[Tool, ...]: ) anchor_reference = DocsAnchor( path=DocsPath / "reference" / "index.md", - anchor="reference-index", + anchor="reference-map", summary="Navigation map for repository docs", ) anchor_search = DocsAnchor( - path=DocsPath, - anchor="docs-search", + path=DocsPath / "reference" / "mcp-server.md", + anchor="search-docs", summary="Search across docs for maintenance topics", ) anchor_tasks = DocsAnchor( path=RepoPath / "specs" / "001-mcp-server" / "tasks.md", - anchor="tasks", + anchor="tasks-mcp-server-for-repo-maintenance", summary="Implementation tasks for MCP feature", ) anchor_sync = DocsAnchor( diff --git a/specs/001-ai-docs/research.md b/specs/001-ai-docs/research.md index db7f625..b64fd38 100644 --- a/specs/001-ai-docs/research.md +++ b/specs/001-ai-docs/research.md @@ -19,3 +19,8 @@ - **Decision**: Anchor discoverability through `docs/constitution.md` with links to `docs/reference/index.md` and `docs/playbooks/`; record any conflict resolutions in `specs/001-ai-docs/research.md` with date and source references. - **Rationale**: Keeps navigation within two steps for AI and centralizes historical decisions for reconciliation. - **Alternatives considered**: (a) Distribute links per playbook only (rejected: harder to enforce two-step navigation); (b) Log conflicts in ad-hoc comments (rejected: loses traceability for AI tools). + +## Decision 5 (2026-02-01): MCP docs sync alignment +- **Decision**: Treat the constitution as authoritative, update MCP docs to include explicit tool anchors, and align the tool catalog anchors to actual markdown headings; scope sync checks to MCP tool headings in `docs/reference/mcp-server.md`. +- **Rationale**: Prevents false drift from unrelated docs while ensuring tool anchors remain accurate and navigable. +- **Alternatives considered**: (a) Force every doc to map to a tool (rejected: inflates catalog and adds noise); (b) Keep loose anchors without validation (rejected: undermines navigation and sync intent).