This commit is contained in:
@@ -5,13 +5,33 @@
|
|||||||
- Transport: MCP protocol over stdio via the official Python SDK; no network listeners; enforced local-only guard.
|
- 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.
|
- 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
|
## Tool Catalog
|
||||||
- `show-constitution`: Display `docs/constitution.md` to confirm authoritative rules.
|
### show-constitution
|
||||||
- `list-playbooks`: List available playbooks under `docs/playbooks/` for common tasks.
|
- Purpose: Display `docs/constitution.md` to confirm authoritative rules.
|
||||||
- `show-reference`: Show `docs/reference/index.md` to navigate repo guidance.
|
- Docs anchor: `docs/constitution.md` → `#ai-constitution-for-the-nixos-repository`.
|
||||||
- `search-docs`: Search the docs set for a query (param: `query`).
|
### list-playbooks
|
||||||
- `list-mcp-tasks`: Show MCP feature task list from `specs/001-mcp-server/tasks.md`.
|
- Purpose: List available playbooks under `docs/playbooks/` for common tasks.
|
||||||
- `sync-docs`: Compare tool catalog against documented anchors for drift reporting.
|
- 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
|
## Invocation
|
||||||
- Start server: `nixos-mcp` (stdio mode).
|
- Start server: `nixos-mcp` (stdio mode).
|
||||||
|
|||||||
@@ -2,10 +2,13 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from mcp_server.tools import DocsPath, tool_catalog
|
from mcp_server.tools import DocsPath, tool_catalog
|
||||||
|
|
||||||
|
_REFERENCE_DOC = DocsPath / "reference" / "mcp-server.md"
|
||||||
|
|
||||||
|
|
||||||
def check_catalog_parity() -> dict[str, object]:
|
def check_catalog_parity() -> dict[str, object]:
|
||||||
"""Compare documented anchors with the live tool catalog and report drift."""
|
"""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] = []
|
missing_in_catalog: list[str] = []
|
||||||
mismatches: list[dict[str, str]] = []
|
mismatches: list[dict[str, str]] = []
|
||||||
|
|
||||||
docs_tools = _doc_anchors()
|
docs_tools = _doc_tool_anchors()
|
||||||
catalog = tool_catalog()
|
catalog = tool_catalog()
|
||||||
|
catalog_names = {tool.name for tool in catalog}
|
||||||
for tool in catalog:
|
for tool in catalog:
|
||||||
anchor_key = _anchor_key(tool.docs_anchor.path, tool.docs_anchor.anchor)
|
if not _anchor_exists(tool.docs_anchor.path, tool.docs_anchor.anchor):
|
||||||
if anchor_key not in docs_tools:
|
|
||||||
missing_in_docs.append(tool.name)
|
missing_in_docs.append(tool.name)
|
||||||
for anchor_key, tool_name in docs_tools.items():
|
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)
|
missing_in_catalog.append(anchor_key)
|
||||||
return {
|
return {
|
||||||
"status": (
|
"status": (
|
||||||
@@ -34,21 +37,51 @@ def check_catalog_parity() -> dict[str, object]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _doc_anchors() -> dict[str, str]:
|
def _doc_tool_anchors() -> dict[str, str]:
|
||||||
"""Derive anchors from docs files to detect missing catalog entries."""
|
"""Derive documented tool anchors from the MCP reference page."""
|
||||||
anchors: dict[str, str] = {}
|
anchors: dict[str, str] = {}
|
||||||
files = list(DocsPath.rglob("*.md"))
|
for heading in _heading_texts(_REFERENCE_DOC, prefix="### "):
|
||||||
for path in files:
|
tool_name = heading
|
||||||
tool_name = _derive_tool_name(path)
|
anchor_id = _slugify_heading(heading)
|
||||||
anchor_id = path.stem
|
anchors[_anchor_key(_REFERENCE_DOC, anchor_id)] = tool_name
|
||||||
anchors[_anchor_key(path, anchor_id)] = tool_name
|
|
||||||
return anchors
|
return anchors
|
||||||
|
|
||||||
|
|
||||||
def _derive_tool_name(path: Path) -> str:
|
def _anchor_exists(path: Path, anchor: str) -> bool:
|
||||||
"""Create a best-effort tool name from a documentation path."""
|
"""Check whether a heading anchor exists in the target doc."""
|
||||||
parts = path.parts[-3:]
|
if not path.exists() or path.is_dir():
|
||||||
return "-".join(filter(None, parts)).replace(".md", "")
|
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:
|
def _anchor_key(path: Path, anchor: str) -> str:
|
||||||
|
|||||||
@@ -168,17 +168,17 @@ def tool_catalog() -> tuple[Tool, ...]:
|
|||||||
)
|
)
|
||||||
anchor_reference = DocsAnchor(
|
anchor_reference = DocsAnchor(
|
||||||
path=DocsPath / "reference" / "index.md",
|
path=DocsPath / "reference" / "index.md",
|
||||||
anchor="reference-index",
|
anchor="reference-map",
|
||||||
summary="Navigation map for repository docs",
|
summary="Navigation map for repository docs",
|
||||||
)
|
)
|
||||||
anchor_search = DocsAnchor(
|
anchor_search = DocsAnchor(
|
||||||
path=DocsPath,
|
path=DocsPath / "reference" / "mcp-server.md",
|
||||||
anchor="docs-search",
|
anchor="search-docs",
|
||||||
summary="Search across docs for maintenance topics",
|
summary="Search across docs for maintenance topics",
|
||||||
)
|
)
|
||||||
anchor_tasks = DocsAnchor(
|
anchor_tasks = DocsAnchor(
|
||||||
path=RepoPath / "specs" / "001-mcp-server" / "tasks.md",
|
path=RepoPath / "specs" / "001-mcp-server" / "tasks.md",
|
||||||
anchor="tasks",
|
anchor="tasks-mcp-server-for-repo-maintenance",
|
||||||
summary="Implementation tasks for MCP feature",
|
summary="Implementation tasks for MCP feature",
|
||||||
)
|
)
|
||||||
anchor_sync = DocsAnchor(
|
anchor_sync = DocsAnchor(
|
||||||
|
|||||||
@@ -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.
|
- **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.
|
- **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).
|
- **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).
|
||||||
|
|||||||
Reference in New Issue
Block a user