"""Generate or verify the deterministic DocForge CLI and MCP command reference.""" from __future__ import annotations import argparse import asyncio import contextlib import json import os import secrets import shutil import stat import sys import tempfile from collections.abc import Sequence from dataclasses import dataclass from pathlib import Path from typing import Literal from mcp.shared.memory import create_connected_server_and_client_session from docforge.command_reference import ( CliCommandReference, McpToolReference, cli_command_references, mcp_tool_references, render_command_reference_markdown, ) from docforge.index import ProjectIndex from docforge.mcp_server import ALL_TOOLS, APPLICATION_TOOLS, create_server from docforge.project import Project REPOSITORY_ROOT = Path(__file__).resolve().parents[1] DEFAULT_PROJECT_ROOT = REPOSITORY_ROOT / "tests" / "fixtures" / "alpha" EXPECTED_CLI_ROWS = 28 EXPECTED_MCP_ROWS = 36 MAX_REFERENCE_BYTES = 5_000_000 class CommandReferenceToolError(RuntimeError): """One safe repository-tool validation or publication failure.""" class CommandReferenceDrift(CommandReferenceToolError): """The checked output does not match current registered command metadata.""" @dataclass(frozen=True) class _FileIdentity: device: int inode: int mode: int size: int modified_ns: int changed_ns: int async def collect_command_reference_rows( project_root: Path, *, proposal_writer: str, canonical_applier: str, ) -> tuple[tuple[CliCommandReference, ...], tuple[McpToolReference, ...]]: """Collect the real CLI parser and full project-bound MCP registration surface.""" source = _safe_project_root(project_root) with tempfile.TemporaryDirectory(prefix="docforge-command-reference-") as directory: copied = Path(directory) / "project" shutil.copytree(source, copied, symlinks=True) ProjectIndex(Project.open(copied)).build() server = create_server( copied, proposal_writer, canonical_applier_id=canonical_applier, ) async with create_connected_server_and_client_session( server, raise_exceptions=True, ) as session: tools = tuple((await session.list_tools()).tools) cli_rows = cli_command_references() expected_mcp_names = (*ALL_TOOLS, *APPLICATION_TOOLS) mcp_rows = mcp_tool_references(tools, expected_names=expected_mcp_names) if len(cli_rows) != EXPECTED_CLI_ROWS: raise CommandReferenceToolError( f"Expected {EXPECTED_CLI_ROWS} CLI rows, found {len(cli_rows)}" ) if len(mcp_rows) != EXPECTED_MCP_ROWS: raise CommandReferenceToolError( f"Expected {EXPECTED_MCP_ROWS} MCP rows, found {len(mcp_rows)}" ) return cli_rows, mcp_rows def generate_command_reference_bytes( project_root: Path, *, proposal_writer: str = "alpha-editor", canonical_applier: str = "alpha-editor", ) -> bytes: """Render one stable Markdown document from an isolated project copy.""" cli_rows, mcp_rows = asyncio.run( collect_command_reference_rows( project_root, proposal_writer=proposal_writer, canonical_applier=canonical_applier, ) ) rendered = render_command_reference_markdown(cli_rows, mcp_rows).encode("utf-8") if len(rendered) > MAX_REFERENCE_BYTES: raise CommandReferenceToolError("Generated command reference exceeds its byte limit") return rendered def publish_or_check_command_reference( content: bytes, *, repository_root: Path, project_root: Path, output: Path, check: bool, ) -> Literal["current", "unchanged", "written"]: """Safely check or atomically publish one repository-confined Markdown file.""" if len(content) > MAX_REFERENCE_BYTES: raise CommandReferenceToolError("Command reference exceeds its byte limit") root = _safe_repository_root(repository_root) source = _safe_project_root(project_root) relative = _safe_output_relative(root, source, output) parent_parts = relative.parts[:-1] target_name = relative.name parent_fd = _open_relative_directory(root, parent_parts) try: initial = _file_identity(parent_fd, target_name) existing = ( _read_regular_file(parent_fd, target_name, limit=MAX_REFERENCE_BYTES) if initial is not None else None ) if _file_identity(parent_fd, target_name) != initial: raise CommandReferenceToolError("Output target changed during inspection") if check: if existing != content: raise CommandReferenceDrift( f"Command reference is missing or stale: {relative.as_posix()}" ) return "current" if existing == content: return "unchanged" _atomic_replace(parent_fd, target_name, content, expected=initial) return "written" finally: os.close(parent_fd) def _safe_repository_root(path: Path) -> Path: if path.is_symlink(): raise CommandReferenceToolError("Repository root must not be a symbolic link") try: resolved = path.resolve(strict=True) except OSError as error: raise CommandReferenceToolError("Repository root does not exist") from error if not resolved.is_dir(): raise CommandReferenceToolError("Repository root must be a directory") return resolved def _safe_project_root(path: Path) -> Path: if path.is_symlink(): raise CommandReferenceToolError("Project fixture root must not be a symbolic link") try: resolved = path.resolve(strict=True) except OSError as error: raise CommandReferenceToolError("Project fixture root does not exist") from error if not resolved.is_dir(): raise CommandReferenceToolError("Project fixture root must be a directory") return resolved def _safe_output_relative(root: Path, project_root: Path, output: Path) -> Path: candidate = output if output.is_absolute() else root / output absolute = Path(os.path.abspath(candidate)) if absolute == root or not absolute.is_relative_to(root): raise CommandReferenceToolError("Output must remain inside the repository root") if absolute == project_root or absolute.is_relative_to(project_root): raise CommandReferenceToolError("Output must not modify the source project fixture") relative = absolute.relative_to(root) if relative.suffix != ".md": raise CommandReferenceToolError("Output must be one Markdown file") if not relative.name or len(relative.parts) < 2: raise CommandReferenceToolError("Output must be below an existing repository directory") return relative def _open_relative_directory(root: Path, parts: tuple[str, ...]) -> int: flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW descriptor = os.open(root, flags) try: for part in parts: if part in {"", ".", ".."}: raise CommandReferenceToolError("Output directory is unsafe") try: next_descriptor = os.open(part, flags, dir_fd=descriptor) except OSError as error: raise CommandReferenceToolError( "Output parent must be an existing non-symlink directory" ) from error os.close(descriptor) descriptor = next_descriptor return descriptor except Exception: os.close(descriptor) raise def _file_identity(directory_fd: int, name: str) -> _FileIdentity | None: try: status = os.stat(name, dir_fd=directory_fd, follow_symlinks=False) except FileNotFoundError: return None if not stat.S_ISREG(status.st_mode): raise CommandReferenceToolError("Output target must be a regular file") return _FileIdentity( device=status.st_dev, inode=status.st_ino, mode=status.st_mode, size=status.st_size, modified_ns=status.st_mtime_ns, changed_ns=status.st_ctime_ns, ) def _read_regular_file(directory_fd: int, name: str, *, limit: int) -> bytes: try: descriptor = os.open(name, os.O_RDONLY | os.O_NOFOLLOW, dir_fd=directory_fd) except OSError as error: raise CommandReferenceToolError("Output target could not be opened safely") from error try: before = os.fstat(descriptor) if not stat.S_ISREG(before.st_mode) or before.st_size > limit: raise CommandReferenceToolError("Output target is not a bounded regular file") chunks: list[bytes] = [] remaining = limit + 1 while remaining: chunk = os.read(descriptor, min(remaining, 64 * 1024)) if not chunk: break chunks.append(chunk) remaining -= len(chunk) content = b"".join(chunks) after = os.fstat(descriptor) if len(content) > limit or _identity_from_stat(before) != _identity_from_stat(after): raise CommandReferenceToolError("Output target changed during inspection") return content finally: os.close(descriptor) def _identity_from_stat(status: os.stat_result) -> _FileIdentity: return _FileIdentity( device=status.st_dev, inode=status.st_ino, mode=status.st_mode, size=status.st_size, modified_ns=status.st_mtime_ns, changed_ns=status.st_ctime_ns, ) def _atomic_replace( directory_fd: int, name: str, content: bytes, *, expected: _FileIdentity | None, ) -> None: temporary_name = f".{name}.docforge-command-reference-{secrets.token_hex(12)}" flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW try: descriptor = os.open(temporary_name, flags, 0o644, dir_fd=directory_fd) except OSError as error: raise CommandReferenceToolError("Could not create atomic output temporary") from error try: view = memoryview(content) while view: written = os.write(descriptor, view) if written <= 0: raise CommandReferenceToolError("Could not write command reference output") view = view[written:] os.fsync(descriptor) except Exception: os.close(descriptor) _unlink_at(directory_fd, temporary_name) raise else: os.close(descriptor) try: if _file_identity(directory_fd, name) != expected: raise CommandReferenceToolError("Output target changed before atomic publication") os.replace( temporary_name, name, src_dir_fd=directory_fd, dst_dir_fd=directory_fd, ) os.fsync(directory_fd) except Exception: _unlink_at(directory_fd, temporary_name) raise def _unlink_at(directory_fd: int, name: str) -> None: with contextlib.suppress(FileNotFoundError): os.unlink(name, dir_fd=directory_fd) def _parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="Generate or check deterministic DocForge command-reference Markdown." ) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--check", action="store_true") parser.add_argument("--repository-root", type=Path, default=REPOSITORY_ROOT) parser.add_argument("--project-root", type=Path, default=DEFAULT_PROJECT_ROOT) parser.add_argument("--proposal-writer", default="alpha-editor") parser.add_argument("--canonical-applier", default="alpha-editor") return parser def main(arguments: Sequence[str] | None = None) -> int: parsed = _parser().parse_args(arguments) try: content = generate_command_reference_bytes( parsed.project_root, proposal_writer=parsed.proposal_writer, canonical_applier=parsed.canonical_applier, ) state = publish_or_check_command_reference( content, repository_root=parsed.repository_root, project_root=parsed.project_root, output=parsed.output, check=parsed.check, ) except CommandReferenceDrift as error: print(str(error), file=sys.stderr) return 1 except Exception as error: print(f"command-reference generation failed: {error}", file=sys.stderr) return 2 print( json.dumps( { "status": state, "output": str(parsed.output), "cli_rows": EXPECTED_CLI_ROWS, "mcp_rows": EXPECTED_MCP_ROWS, }, sort_keys=True, separators=(",", ":"), ) ) return 0 if __name__ == "__main__": raise SystemExit(main())