Compare commits
4 Commits
678f522456
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 56fb39de08 | |||
| 8a509e590a | |||
| cb68b0b93f | |||
| 2ed962d639 |
327
ADS.llm.md
327
ADS.llm.md
@@ -1,327 +0,0 @@
|
||||
# doc-forge — Architecture & Design Specification
|
||||
|
||||
**doc-forge** is a renderer-agnostic Python documentation compiler. It converts Python source code and docstrings into a structured, semantic documentation model and then emits multiple downstream representations, including:
|
||||
|
||||
* Human-facing documentation sites (MkDocs, Sphinx)
|
||||
* Machine-facing documentation bundles (MCP JSON)
|
||||
* Live documentation APIs (MCP servers)
|
||||
|
||||
This document is the **authoritative design and codebase specification** for the library. It is written to be both **LLM-friendly** and **developer-facing**, and should be treated as the canonical reference for implementation decisions.
|
||||
|
||||
---
|
||||
|
||||
## 1. Design Goals
|
||||
|
||||
1. **Single Source of Truth**
|
||||
Python source code and docstrings are the only authoritative input.
|
||||
|
||||
2. **Renderer Agnosticism**
|
||||
MkDocs, Sphinx, MCP, or future renderers must not influence the core model.
|
||||
|
||||
3. **Deterministic Output**
|
||||
Given the same codebase, outputs must be reproducible.
|
||||
|
||||
4. **AI-Native Documentation**
|
||||
Documentation must be structured, queryable, and machine-consumable.
|
||||
|
||||
5. **Library-First, CLI-Second**
|
||||
All functionality must be accessible as a Python API. The CLI is a thin wrapper.
|
||||
|
||||
---
|
||||
|
||||
## 2. Core Mental Model
|
||||
|
||||
### Fundamental Abstraction
|
||||
|
||||
> **The atomic unit of documentation is a Python import path**
|
||||
|
||||
Examples:
|
||||
|
||||
* `mail_intake`
|
||||
* `mail_intake.config`
|
||||
* `mail_intake.adapters.base`
|
||||
|
||||
Files, Markdown, HTML, and JSON are *representations*, not documentation units.
|
||||
|
||||
---
|
||||
|
||||
## 3. High-Level Architecture
|
||||
|
||||
```
|
||||
Python Source Code
|
||||
↓
|
||||
Introspection Layer (Griffe)
|
||||
↓
|
||||
Documentation Model (doc-forge core)
|
||||
↓
|
||||
Renderer / Exporter Layer
|
||||
├── MkDocs
|
||||
├── Sphinx
|
||||
├── MCP (static JSON)
|
||||
└── MCP Server (live)
|
||||
```
|
||||
|
||||
Only the **Documentation Model** is shared across all outputs.
|
||||
|
||||
---
|
||||
|
||||
## 4. Package Layout (Proposed)
|
||||
|
||||
```
|
||||
docforge/
|
||||
├── __init__.py
|
||||
├── model/
|
||||
│ ├── project.py
|
||||
│ ├── module.py
|
||||
│ ├── object.py
|
||||
│ └── nav.py
|
||||
├── loader/
|
||||
│ └── griffe_loader.py
|
||||
├── renderers/
|
||||
│ ├── base.py
|
||||
│ ├── mkdocs.py
|
||||
│ └── sphinx.py
|
||||
├── exporters/
|
||||
│ └── mcp.py
|
||||
├── server/
|
||||
│ └── mcp_server.py
|
||||
├── cli/
|
||||
│ └── main.py
|
||||
└── utils/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Documentation Model (Core)
|
||||
|
||||
The documentation model is renderer-neutral and must not contain any MkDocs-, Sphinx-, or MCP-specific logic.
|
||||
|
||||
### 5.1 Project
|
||||
|
||||
```python
|
||||
class Project:
|
||||
name: str
|
||||
version: str | None
|
||||
modules: dict[str, Module]
|
||||
nav: Navigation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.2 Module
|
||||
|
||||
```python
|
||||
class Module:
|
||||
path: str # import path
|
||||
docstring: str | None
|
||||
members: dict[str, DocObject]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.3 DocObject
|
||||
|
||||
Represents classes, functions, variables, etc.
|
||||
|
||||
```python
|
||||
class DocObject:
|
||||
name: str
|
||||
kind: str # class, function, attribute, module
|
||||
path: str
|
||||
signature: str | None
|
||||
docstring: str | None
|
||||
members: dict[str, DocObject]
|
||||
```
|
||||
|
||||
Private members (`_name`) are excluded by default.
|
||||
|
||||
---
|
||||
|
||||
### 5.4 Navigation
|
||||
|
||||
```python
|
||||
class Navigation:
|
||||
entries: list[NavEntry]
|
||||
|
||||
class NavEntry:
|
||||
title: str
|
||||
module: str
|
||||
```
|
||||
|
||||
Navigation is derived, not authored.
|
||||
|
||||
---
|
||||
|
||||
## 6. Introspection Layer
|
||||
|
||||
### 6.1 Griffe Loader
|
||||
|
||||
Griffe is the **only supported introspection backend**.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
* Load modules by import path
|
||||
* Resolve docstrings, signatures, and members
|
||||
* Tolerate alias resolution failures
|
||||
|
||||
Output: fully populated `Project` and `Module` objects.
|
||||
|
||||
---
|
||||
|
||||
## 7. Renderer Interface
|
||||
|
||||
Renderers consume the documentation model and emit renderer-specific source trees.
|
||||
|
||||
```python
|
||||
class DocRenderer(Protocol):
|
||||
name: str
|
||||
|
||||
def generate_sources(self, project: Project, out_dir: Path) -> None:
|
||||
"""Generate renderer-specific source files."""
|
||||
|
||||
def build(self, config: RendererConfig) -> None:
|
||||
"""Build final artifacts (HTML, site, etc.)."""
|
||||
|
||||
def serve(self, config: RendererConfig) -> None:
|
||||
"""Serve documentation locally (optional)."""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. MkDocs Renderer
|
||||
|
||||
### Source Generation
|
||||
|
||||
* Emits `.md` files
|
||||
* One file per module
|
||||
* Uses `mkdocstrings` directives exclusively
|
||||
|
||||
```md
|
||||
# Config
|
||||
|
||||
::: mail_intake.config
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
* Uses `mkdocs.commands.build`
|
||||
|
||||
### Serve
|
||||
|
||||
* Uses `mkdocs.commands.serve`
|
||||
|
||||
MkDocs-specific configuration lives outside the core model.
|
||||
|
||||
---
|
||||
|
||||
## 9. Sphinx Renderer
|
||||
|
||||
### Source Generation
|
||||
|
||||
* Emits `.rst` files
|
||||
* Uses `autodoc` directives
|
||||
|
||||
```rst
|
||||
mail_intake.config
|
||||
==================
|
||||
|
||||
.. automodule:: mail_intake.config
|
||||
:members:
|
||||
:undoc-members:
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
* Uses `sphinx.application.Sphinx` directly
|
||||
|
||||
### Serve
|
||||
|
||||
* Optional (static build is sufficient)
|
||||
|
||||
---
|
||||
|
||||
## 10. MCP Exporter (Static)
|
||||
|
||||
The MCP exporter bypasses renderers entirely.
|
||||
|
||||
### Output Structure
|
||||
|
||||
```
|
||||
mcp/
|
||||
├── index.json
|
||||
├── nav.json
|
||||
└── modules/
|
||||
└── package.module.json
|
||||
```
|
||||
|
||||
### Design Principles
|
||||
|
||||
* Alias-safe
|
||||
* Deterministic
|
||||
* Fully self-contained
|
||||
* No Markdown, HTML, or templates
|
||||
|
||||
---
|
||||
|
||||
## 11. MCP Server (Live)
|
||||
|
||||
The MCP server exposes documentation as queryable resources.
|
||||
|
||||
### Resources
|
||||
|
||||
* `docs://index`
|
||||
* `docs://nav`
|
||||
* `docs://module/{module}`
|
||||
|
||||
### Characteristics
|
||||
|
||||
* Read-only
|
||||
* Stateless
|
||||
* Backed by MCP JSON bundle
|
||||
|
||||
---
|
||||
|
||||
## 12. CLI Design
|
||||
|
||||
The CLI is a thin orchestration layer.
|
||||
|
||||
```bash
|
||||
doc-forge generate --renderer mkdocs
|
||||
doc-forge generate --renderer sphinx
|
||||
|
||||
doc-forge build --renderer mkdocs
|
||||
doc-forge serve --renderer mkdocs
|
||||
|
||||
doc-forge export mcp
|
||||
```
|
||||
|
||||
Renderer choice never affects the core model.
|
||||
|
||||
---
|
||||
|
||||
## 13. Explicit Non-Goals
|
||||
|
||||
* Markdown authoring
|
||||
* Theme design
|
||||
* Runtime code execution
|
||||
* Code formatting or linting
|
||||
|
||||
---
|
||||
|
||||
## 14. Invariants (Must Never Break)
|
||||
|
||||
1. Import paths are canonical identifiers
|
||||
2. Core model contains no renderer logic
|
||||
3. MCP does not depend on MkDocs or Sphinx
|
||||
4. Renderers do not introspect Python directly
|
||||
5. All outputs trace back to the same model
|
||||
|
||||
---
|
||||
|
||||
## 15. One-Line Definition
|
||||
|
||||
> **doc-forge is a documentation compiler that turns Python code into structured knowledge and emits it through multiple human and machine interfaces.**
|
||||
|
||||
---
|
||||
|
||||
*End of specification.*
|
||||
@@ -6,6 +6,29 @@ speed, flexibility, and beautiful output. It decouples the introspection of
|
||||
your code from the rendering process, allowing you to generate documentation
|
||||
for various platforms (starting with MkDocs) from a single internal models.
|
||||
|
||||
## Core Philosophy
|
||||
|
||||
`doc-forge` operates on two fundamental principles:
|
||||
|
||||
1. **The Atomic Unit is a Python Import Path**: Documentation is organized around the semantic structure of your code (e.g., `mypackage.utils`), not the filesystem.
|
||||
2. **The Documentation Compiler Paradigm**: We separate documentation into three distinct phases:
|
||||
- **Front-end (Introspection)**: Static analysis of source code and docstrings.
|
||||
- **Middle-end (Semantic Model)**: A renderer-neutral internal representation.
|
||||
- **Back-end (Renderers)**: Generation of human-facing (MkDocs) or machine-facing (MCP) outputs.
|
||||
|
||||
## Documentation Design
|
||||
|
||||
`doc-forge` is an "AI-Native" documentation compiler. To get the most out of it, design your docstrings with both humans and LLMs in mind:
|
||||
|
||||
### For Humans (Readability & Structure)
|
||||
- **`__init__.py` as Landing Pages**: Use the docstring of your package's `__init__.py` as the home page. Include overviews, installation instructions, and high-level examples here.
|
||||
- **Single Source of Truth**: Keep all technical details in docstrings. This ensures your MkDocs/Sphinx sites stay in sync with the code.
|
||||
- **Semantic Hierarchy**: Use standard Markdown headers to structure complex module documentation.
|
||||
|
||||
### For LLMs (AI-Native Knowledge)
|
||||
- **Model Context Protocol (MCP)**: `doc-forge` exports your docs as structured JSON. This allows AI agents to "understand" your API surface area without layout noise.
|
||||
- **Canonical Paths**: Use dotted import paths as primary identifiers. AI tools use these to link code usage to documentation.
|
||||
- **Type Annotations**: While not in docstrings, `doc-forge` (via Griffe) extracts signatures. Clean type hints dramatically improve an LLM's ability to generate correct code using your library.
|
||||
## Available Commands
|
||||
|
||||
- **build**: Build documentation (MkDocs site or MCP resources).
|
||||
|
||||
@@ -92,11 +92,13 @@ def build(
|
||||
@cli.command()
|
||||
@click.option("--mcp", is_flag=True, help="Serve MCP documentation")
|
||||
@click.option("--mkdocs", is_flag=True, help="Serve MkDocs site")
|
||||
@click.option("--module", help="Python module to serve")
|
||||
@click.option("--mkdocs-yml", type=click.Path(path_type=Path), default=Path("mkdocs.yml"), help="MkDocs config path")
|
||||
@click.option("--out-dir", type=click.Path(path_type=Path), default=Path("mcp_docs"), help="MCP root directory")
|
||||
def serve(
|
||||
mcp: bool,
|
||||
mkdocs: bool,
|
||||
module: Optional[str],
|
||||
mkdocs_yml: Path,
|
||||
out_dir: Path,
|
||||
) -> None:
|
||||
@@ -106,6 +108,7 @@ def serve(
|
||||
Args:
|
||||
mcp: Serve MCP resources via an MCP server.
|
||||
mkdocs: Serve the MkDocs site using the built-in development server.
|
||||
module: The dotted path of the module to serve.
|
||||
mkdocs_yml: (MkDocs) Path to the mkdocs.yml configuration.
|
||||
out_dir: (MCP) Path to the mcp_docs/ directory.
|
||||
"""
|
||||
@@ -113,37 +116,38 @@ def serve(
|
||||
raise click.UsageError("Cannot specify both --mcp and --mkdocs")
|
||||
if not mcp and not mkdocs:
|
||||
raise click.UsageError("Must specify either --mcp or --mkdocs")
|
||||
if mcp and not module:
|
||||
raise click.UsageError("--module is required for MCP serve")
|
||||
|
||||
if mkdocs:
|
||||
mkdocs_utils.serve(mkdocs_yml)
|
||||
elif mcp:
|
||||
mcp_utils.serve(out_dir)
|
||||
mcp_utils.serve(module, out_dir)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option(
|
||||
"--modules",
|
||||
multiple=True,
|
||||
"--module",
|
||||
required=True,
|
||||
help="Python module import paths to introspect",
|
||||
help="Python module import path to introspect",
|
||||
)
|
||||
@click.option(
|
||||
"--project-name",
|
||||
help="Project name (defaults to first module)",
|
||||
help="Project name (defaults to specified module)",
|
||||
)
|
||||
def tree(
|
||||
modules: Sequence[str],
|
||||
module: str,
|
||||
project_name: Optional[str],
|
||||
) -> None:
|
||||
"""
|
||||
Visualize the project structure in the terminal.
|
||||
|
||||
Args:
|
||||
modules: List of module import paths to recursively introspect.
|
||||
module: The module import path to recursively introspect.
|
||||
project_name: Optional override for the project name shown at the root.
|
||||
"""
|
||||
loader = GriffeLoader()
|
||||
project = loader.load_project(list(modules), project_name)
|
||||
project = loader.load_project([module], project_name)
|
||||
|
||||
click.echo(project.name)
|
||||
|
||||
|
||||
@@ -20,12 +20,13 @@ def build(
|
||||
def serve(
|
||||
mcp: bool,
|
||||
mkdocs: bool,
|
||||
module: Optional[str],
|
||||
mkdocs_yml: Path,
|
||||
out_dir: Path,
|
||||
) -> None: ...
|
||||
|
||||
def tree(
|
||||
modules: Sequence[str],
|
||||
module: str,
|
||||
project_name: Optional[str],
|
||||
) -> None: ...
|
||||
|
||||
|
||||
@@ -20,11 +20,12 @@ def generate_resources(module: str, project_name: str | None, out_dir: Path) ->
|
||||
renderer = MCPRenderer()
|
||||
renderer.generate_sources(project, out_dir)
|
||||
|
||||
def serve(mcp_root: Path) -> None:
|
||||
def serve(module: str, mcp_root: Path) -> None:
|
||||
"""
|
||||
Serve MCP documentation from a pre-built bundle.
|
||||
|
||||
Args:
|
||||
module: The dotted path of the primary module to serve.
|
||||
mcp_root: Path to the directory containing index.json, nav.json, and modules/.
|
||||
"""
|
||||
if not mcp_root.exists():
|
||||
@@ -42,6 +43,6 @@ def serve(mcp_root: Path) -> None:
|
||||
|
||||
server = MCPServer(
|
||||
mcp_root=mcp_root,
|
||||
name="doc-forge-mcp",
|
||||
name=f"{module}-mcp",
|
||||
)
|
||||
server.run()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from pathlib import Path
|
||||
|
||||
def generate_resources(module: str, project_name: str | None, out_dir: Path) -> None: ...
|
||||
def serve(mcp_root: Path) -> None: ...
|
||||
def serve(module: str, mcp_root: Path) -> None: ...
|
||||
|
||||
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "doc-forge"
|
||||
version = "0.0.3"
|
||||
version = "0.0.4"
|
||||
description = "A renderer-agnostic Python documentation compiler"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -10,7 +10,7 @@ def test_mcp_serve(
|
||||
|
||||
result = cli_runner.invoke(
|
||||
cli,
|
||||
["serve", "--mcp", "--out-dir", str(fake_mcp_docs)],
|
||||
["serve", "--mcp", "--module", "fake_module", "--out-dir", str(fake_mcp_docs)],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
|
||||
Reference in New Issue
Block a user