29 Commits

Author SHA1 Message Date
c0d8a252ff docs(wiki): added copyable prompt to generate wiki in doc-forge wiki 2026-09-14 23:09:58 +05:30
358510ebf0 (refactor): moved WIKI.md to wiki for doc-forge 2026-09-14 22:57:52 +05:30
68c51d722e fixes for compliance with existing poster wiki at mongo-ops 2026-09-14 22:49:10 +05:30
3448f55e18 added WIKI.md for instructions on how to write wiki 2026-09-14 22:44:38 +05:30
de427dd350 feat: use file-wins mkdocs configs with --refresh; wiki template in mongo-ops style 2026-09-13 18:11:42 +05:30
b86b5b0e85 fix: resolve mkdocstrings search path against repo root in lib builds 2026-09-12 14:57:34 +05:30
2ae96f58de feat: build each doc kind with its own MkDocs config and site 2026-09-12 14:23:22 +05:30
582b6809a0 docs: bring docforge docstrings and wiki to GSDFC standard
- fix GSDFC spec contradictions in __init__ docstring (parenthesized types, fenced-block rule) and sync generated README
- rewrite docstrings across loaders, models, nav, servers, renderers, cli; sync .pyi stubs
- add pydoclint (google style) gate to dev extras and pyproject config
- fix mcp nav resources doc:// -> docs://
- refresh docs/lib and docs/mcp, drop stale docforge/ duplicate group
- update wiki pages and add GSDFC + MCP guides under 05_development
2026-09-12 13:12:51 +05:30
8c6c46caf2 feat: add wiki build kind with file-structure-derived navigation
- add build_wiki_nav deriving MkDocs nav from docs/wiki file structure
  (index.md -> Home, numeric prefixes stripped and title-cased, nested
  dirs become groups, natural ordering)
- add --wiki / --wiki-dir to build; wiki-only builds need no --module
- merge wiki nav before generated lib/api nav; wiki Home replaces the
  nav spec Home entry
- add mkdocs.wiki.yml template fragment and nav/cli tests
- dogfood doc-forge's own docs/wiki and regenerate site output
2026-09-11 23:38:23 +05:30
bacf17b930 feat: add --api OpenAPI build mode with docs/api scheme and lib-prefixed nav 2026-09-11 05:37:56 +05:30
17e8628197 docs: nest generated docs under docs/lib and docs/mcp 2026-09-10 21:15:11 +05:30
a1c5f046d4 docs: output generated sources under docs/lib and docs/mcp 2026-09-10 21:11:30 +05:30
d4b79cf95a ignore coverage.xml test artifact 2026-09-10 20:21:23 +05:30
0129268cd3 standardize packaging, tooling, docs, CI, and licensing 2026-09-10 18:49:04 +05:30
8253c25928 updated docs strings and added README.md 2026-03-08 17:59:55 +05:30
e8e16f3996 code blocks with backticks for more control on formatting 2026-03-08 15:32:46 +05:30
f73282b997 formatting for examples and header sections 2026-03-08 15:05:38 +05:30
3dc6ac9427 Improve documentation look & feel via MkDocs Material template enhancements
All checks were successful
continuous-integration/drone/tag Build is passing
2026-03-07 16:22:00 +05:30
b6306baafc Improve documentation look & feel via MkDocs Material template enhancements (#5)
# Improve documentation look & feel via MkDocs Material template enhancements

## Summary

This MR improves the overall **documentation experience and visual presentation** of the doc-forge docs by enhancing the MkDocs Material template configuration.

The changes focus on **navigation usability, code readability, and richer Markdown rendering**, resulting in a cleaner and more professional documentation site.

Docstring changes were made across the codebase for consistency, but this MR description focuses on the **template and presentation improvements**.

---

## Navigation Improvements

The navigation system has been enhanced to provide a clearer structure and better discoverability.

Key improvements include:

* Section-aware navigation in the sidebar
* Automatic expansion of module/package hierarchy
* Scroll tracking within the sidebar
* Clickable package index pages

Material navigation features added:

* `navigation.sections`
* `navigation.expand`
* `navigation.tracking`
* `navigation.indexes`

This results in a **single cohesive navigation tree** that exposes the entire documentation hierarchy from the sidebar.

---

## Code Block Improvements

Code blocks previously appeared relatively plain. The template now enables richer syntax highlighting and improved readability.

Enhancements include:

* Syntax highlighting using `pymdownx.highlight`
* Line numbers for code blocks
* Anchored line numbers for deep linking
* Improved fenced code block rendering

Additional Material features:

* `content.code.copy` — copy button for code blocks
* `content.code.annotate` — support for code annotations

These changes significantly improve the readability of examples and API snippets throughout the documentation.

---

## Markdown Rendering Enhancements

Additional Markdown extensions were enabled to support richer documentation features:

* `pymdownx.superfences` for advanced fenced blocks
* `pymdownx.inlinehilite` for inline code highlighting
* `pymdownx.snippets` for reusable snippets
* `admonition` and `pymdownx.details` for callouts and collapsible sections
* `pymdownx.tabbed` for tabbed content blocks
* `pymdownx.tasklist` for checklist-style items
* `tables`, `footnotes`, and advanced formatting extensions

These extensions make it easier to write expressive and structured documentation.

---

## Search Experience

The documentation search experience has been improved using Material search features:

* `search.highlight`
* `search.share`
* `search.suggest`

These enhancements provide:

* highlighted search matches
* sharable search URLs
* auto-suggestions while typing

---

## mkdocstrings Improvements

The mkdocstrings configuration has been expanded to produce clearer API documentation.

Notable improvements include:

* grouping objects by category
* explicit category headings
* improved symbol headings
* cleaner object path display

This results in more structured API documentation pages.

---

## Result

Overall, these changes provide:

* cleaner and more intuitive navigation
* significantly improved code presentation
* richer Markdown capabilities
* better search usability

The documentation now has a **more polished, modern appearance** and improved usability for both readers and contributors.

Reviewed-on: #5
Co-authored-by: Vishesh 'ironeagle' Bangotra <aetoskia@gmail.com>
Co-committed-by: Vishesh 'ironeagle' Bangotra <aetoskia@gmail.com>
2026-03-07 10:50:18 +00:00
f8ca6075fb google styled doc string 2026-02-28 19:32:17 +05:30
3f68e51e58 google styled doc string 2026-02-28 19:19:20 +05:30
87a7f4eee1 google styled doc string 2026-02-28 18:53:02 +05:30
b5379cd82e added hierarchy rules 2026-02-27 21:27:47 +05:30
caeda0ad5c minor formatting changes 2026-02-27 21:16:05 +05:30
7643e27358 added tab feature 2026-02-27 21:12:50 +05:30
04db9f44d8 fixes for broken Examples and other formatting issues 2026-02-27 21:05:15 +05:30
6c8da4b43b fixes for bland looking doc strings 2026-02-27 20:47:23 +05:30
066e710dee doc string changes with sample to generate doc strings for packages/modules 2026-02-27 14:10:57 +05:30
ed0fac8b3d doc string changes with sample to generate doc strings for packages/modules 2026-02-27 14:10:15 +05:30
178 changed files with 11986 additions and 6600 deletions

View File

@@ -32,6 +32,26 @@ steps:
echo "🆕 New version detected: $PACKAGE_NAME==$VERSION" echo "🆕 New version detected: $PACKAGE_NAME==$VERSION"
fi fi
- name: quality-gate
image: python:3.13-slim
environment:
PIP_REPO_URL:
from_secret: PIP_REPO_URL
PIP_USERNAME:
from_secret: PIP_USERNAME
PIP_PASSWORD:
from_secret: PIP_PASSWORD
commands:
- pip install --upgrade pip build
- |
AUTH_URL="https://${PIP_USERNAME}:${PIP_PASSWORD}@$(echo "${PIP_REPO_URL#*://}" | sed 's:/*$::')/simple"
pip install --index-url "$AUTH_URL" --extra-index-url https://pypi.org/simple/ -U ".[dev]"
- echo "🛡️ Running quality gate..."
- python -m black --check .
- python -m ruff check .
- python -m mypy
- python -m pytest
- name: build-package - name: build-package
image: python:3.13-slim image: python:3.13-slim
commands: commands:
@@ -126,4 +146,4 @@ steps:
trigger: trigger:
event: event:
- custom - custom

6
.gitignore vendored
View File

@@ -53,6 +53,7 @@ test-results/
site/ site/
docs/_site/ docs/_site/
docs/build/ docs/build/
mkdocs.yml
# ========================= # =========================
# MkDocs / Sphinx output # MkDocs / Sphinx output
@@ -87,3 +88,8 @@ Thumbs.db
# ========================= # =========================
.env .env
.env.* .env.*
# =========================
# Testing artifacts
# =========================
coverage.xml

29
CHANGELOG.md Normal file
View File

@@ -0,0 +1,29 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- `py.typed` marker for PEP 561 type information.
- `.drone.yml` CI with a quality-gate step (black, ruff, mypy, pytest).
- MIT `LICENSE`.
- `build --refresh` flag to rebaseline an existing `docs/mkdocs.{kind}.yml`
from the packaged templates.
### Changed
- Standardized `pyproject.toml` (canonical packaging, lint tool config, extras).
- Pinned the `mcp` extra to `mcp>=1.0.0,<2.0.0` to restore compatibility with
the current MCP server API used by doc-forge.
- Per-kind MkDocs configs (`docs/mkdocs.{lib,api,wiki}.yml`) are now generated
only when absent; an existing config is used verbatim (the file wins).
Pass `--refresh` to regenerate it from the templates.
- The wiki template now uses the mongo-ops look: horizontal `navigation.tabs`
header, blue grey/teal palette, Roboto font, and additional material
feature flags. The lib and api templates are unchanged.
### Fixed
- Stub fixes for typed API surfaces.

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Aetoskia Platform
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

738
README.md
View File

@@ -1,244 +1,572 @@
# doc-forge # docforge
A renderer-agnostic Python documentation compiler that converts Python source code and docstrings into a structured, semantic documentation model and emits multiple downstream representations. # Summary
## Features Renderer-agnostic Python documentation compiler that converts Python docstrings
into structured documentation for both humans (MkDocs) and machines (MCP / AI agents).
- **Single Source of Truth**: Python source code and docstrings are the only authoritative input `doc-forge` statically analyzes source code, builds a semantic model of modules,
- **Renderer Agnosticism**: MkDocs, Sphinx, MCP, or future renderers don't influence the core model classes, functions, and attributes, and renders that model into documentation
- **Deterministic Output**: Given the same codebase, outputs are reproducible outputs without executing user code.
- **AI-Native Documentation**: Structured, queryable, and machine-consumable
- **Library-First Design**: All functionality accessible as a Python API
## Installation ---
# Installation
Install using pip:
```bash ```bash
pip install doc-forge pip install doc-forge
``` ```
### Optional Dependencies ---
# CLI usage
Each site kind (`lib`, `api`, `wiki`) is built independently into `site/{kind}`.
## Build the library reference from a Python package:
```bash ```bash
# For MkDocs rendering doc-forge build --mkdocs --module my_package
pip install doc-forge[mkdocs]
# For Sphinx rendering
pip install doc-forge[sphinx]
# For MCP support
pip install doc-forge[mcp]
# For development
pip install doc-forge[dev]
``` ```
## Quick Start ## Build the API reference from an OpenAPI spec:
### Command Line Interface
```bash ```bash
# Generate MkDocs documentation doc-forge build --api --openapi-spec spec.json
doc-forge generate --renderer mkdocs mypackage
# Build final HTML documentation
doc-forge build --renderer mkdocs mypackage
# Serve documentation locally
doc-forge serve --renderer mkdocs mypackage
# Export to MCP format
doc-forge export mypackage
# Start live MCP server
doc-forge server mypackage
``` ```
### Python API ## Build the hand-written wiki:
```python
from docforge.loaders import GriffeLoader
from docforge.renderers import MkDocsRenderer
from pathlib import Path
# Load your project
loader = GriffeLoader()
project = loader.load_project(["mypackage", "mypackage.utils"])
# Generate MkDocs sources
renderer = MkDocsRenderer()
renderer.generate_sources(project, Path("docs"))
# Build final documentation
from docforge.renderers.base import RendererConfig
config = RendererConfig(Path("docs"), project)
renderer.build(config)
```
## Architecture
doc-forge follows this 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)
```
## Core Components
### Documentation Model
- **Project**: Root container for all documentation
- **Module**: Represents Python modules
- **DocObject**: Base class for classes, functions, variables, etc.
- **Navigation**: Hierarchical structure for browsing
### Renderers
- **MkDocs Renderer**: Generates Markdown with mkdocstrings directives
- **Sphinx Renderer**: Generates reStructuredText with autodoc directives
### Exporters
- **MCP Exporter**: Creates static JSON bundles for machine consumption
- **MCP Server**: Live server for real-time documentation access
## CLI Commands
### `generate`
Generate renderer-specific source files without building final artifacts.
```bash ```bash
doc-forge generate --renderer mkdocs --out-dir docs mypackage doc-forge build --wiki --site-name my_package
``` ```
### `build` ## Generate MCP JSON documentation:
Build final documentation artifacts (HTML, etc.).
```bash ```bash
doc-forge build --renderer sphinx mypackage doc-forge build --mcp --module my_package
``` ```
### `serve` ## Build several kinds in one pass:
Start a local development server.
```bash ```bash
doc-forge serve --renderer mkdocs --port 9000 mypackage doc-forge build --mcp --mkdocs --wiki --module my_package
``` ```
### `export` Each enabled kind gets its own MkDocs config (`docs/mkdocs.{lib,api,wiki}.yml`)
Export to MCP format for machine consumption. and its own site under `site/`.
## Serve a site locally:
```bash ```bash
doc-forge export --out-dir mcp mypackage doc-forge serve --wiki # preview from docs/mkdocs.wiki.yml
doc-forge serve --lib
doc-forge serve --api
# or any config directly:
doc-forge serve --mkdocs --mkdocs-yml docs/mkdocs.wiki.yml
``` ```
### `server` ## Serve MCP locally:
Start live MCP server for real-time access.
```bash ```bash
doc-forge server --host 0.0.0.0 --port 8080 mypackage doc-forge serve --mcp --module my_package
``` ```
## Configuration
doc-forge is designed to work with minimal configuration. Most settings are derived automatically from your Python code structure.
### MkDocs Configuration
The MkDocs renderer automatically generates `mkdocs.yml` with sensible defaults:
```yaml
site_name: Your Project
plugins:
- mkdocstrings
theme:
name: material
```
### Sphinx Configuration
The Sphinx renderer automatically generates `conf.py` with standard extensions:
```python
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.viewcode',
'sphinx.ext.napoleon',
]
```
## MCP Integration
doc-forge provides two ways to integrate with MCP (Model Context Protocol):
### Static Export
```bash
doc-forge export mypackage
```
Creates a static JSON bundle in `mcp/` directory that can be loaded by MCP clients.
### Live Server
```bash
doc-forge server mypackage
```
Starts a live MCP server providing real-time access to documentation resources:
- `docs://index` - Project metadata
- `docs://nav` - Navigation structure
- `docs://module/{module}` - Individual module data
## Development
### Setup
```bash
git clone https://github.com/doc-forge/doc-forge
cd doc-forge
pip install -e ".[dev]"
```
### Running Tests
```bash
pytest
```
### Code Quality
```bash
black docforge/
ruff check docforge/
mypy docforge/
```
## License
MIT License - see LICENSE file for details.
## Contributing
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
## Philosophy
doc-forge is built on these core principles:
1. **Single Source of Truth**: Python source code and docstrings are the only authoritative input
2. **Renderer Agnosticism**: The core model contains no renderer-specific logic
3. **Deterministic Output**: Same input always produces same output
4. **AI-Native Documentation**: Documentation must be structured, queryable, and machine-consumable
5. **Library-First**: All functionality must be accessible as a Python API
--- ---
*doc-forge turns Python code into structured knowledge and emits it through multiple human and machine interfaces.* # Core concepts
## Loader
Extracts symbols, signatures, and docstrings using static analysis.
## Semantic model
Structured, renderer-agnostic representation of the API.
## Renderer
Converts the semantic model into output formats such as MkDocs or MCP JSON.
## Symbol
Any documentable object
- module
- class
- function
- method
- property
- attribute
---
# Architecture
`doc-forge` follows a compiler architecture:
## Front-end:
Static analysis of modules, classes, functions, type hints, and docstrings.
## Middle-end:
Builds a semantic model describing symbols and relationships.
## Back-end:
Renders documentation using interchangeable renderers.
This architecture ensures deterministic documentation generation.
---
# Rendering pipeline
Typical flow:
Python package
|
Loader (static analysis)
|
Semantic model
|
Renderer
|
MkDocs site or MCP JSON
---
# Google-Styled Doc-Forge Convention (GSDFC)
GSDFC defines how docstrings must be written so they render correctly in MkDocs and remain machine-parsable by doc-forge and AI tooling.
- Docstrings are the single source of truth.
- `doc-forge` compiles docstrings but does not generate documentation content.
- Documentation follows the Python import hierarchy.
- Every public symbol should have a complete and accurate docstring.
---
## General rules
- Use **Markdown headings** at package and module level.
- Use **Google-style structured sections** at class, function, and method level.
- Use type hints in signatures.
- Use parenthesized types in prose entries (`name (Type):`) that match the
signature types. This keeps docstrings self-contained and machine-parseable.
- Write summaries in imperative form.
- Sections are separated by `---`
---
# Notes subsection grouping
Group related information using labeled subsections.
Example:
Notes:
**Guarantees:**
- deterministic behavior
**Lifecycle:**
- created during initialization
- reused across executions
**Thread safety:**
- safe for concurrent reads
---
# Example formatting
- Use indentation for examples.
- Indent section contents using four spaces.
- Use code blocks for example code.
Example:
Single example:
Example:
```python
foo = Foo("example")
process(foo, multiplier=2)
```
Multiple examples:
Example:
Create foo:
```python
foo = Foo("example")
```
Run engine:
```python
engine = BarEngine([foo])
engine.run()
```
Avoid fenced code blocks inside argument descriptions and other prose lines.
Inside `Example:` sections, fenced `python` code blocks are allowed and must be
indented four spaces, matching the examples below.
---
# Separator rules
Use horizontal separators only at docstring root level to separate sections:
```markdown
---
```
Allowed locations:
- package docstrings
- module docstrings
- major documentation sections
Do not use separators inside code sections.
---
# Package docstrings
Package docstrings act as the documentation home page.
Recommended sections:
# Summary
# Installation
# Quick start
# CLI usage
# Core concepts
# Architecture
# Rendering pipeline
# Examples
# Notes
Example:
Package Doc String:
'''
# Summary
Foo-bar processing framework.
Provides tools for defining Foo objects and executing Bar pipelines.
---
# Installation
```bash
pip install foo-bar
```
---
# Quick start
```python
from foobar import Foo, BarEngine
foo = Foo("example")
engine = BarEngine([foo])
result = engine.run()
```
---
'''
---
# Module docstrings
Module docstrings describe a subsystem.
Recommended sections:
# Summary
# Examples
# Notes
Example:
Module Doc String:
'''
# Summary
Foo execution subsystem.
Provides utilities for executing Foo objects through Bar stages.
---
Example:
```python
from foobar.engine import BarEngine
from foobar.foo import Foo
foo = Foo("example")
engine = BarEngine([foo])
engine.run()
```
---
'''
---
# Class docstrings
Class docstrings define object responsibility, lifecycle, and attributes.
Recommended sections:
Attributes:
Notes:
Example:
Raises:
Example:
Simple Foo:
```python
class Foo:
'''
Represents a unit of work.
Attributes:
name (str):
Identifier of the foo instance.
value (int):
Numeric value associated with foo.
Notes:
Guarantees:
- instances are immutable after creation
Lifecycle:
- create instance
- pass to processing engine
Example:
Create and inspect a Foo:
```python
foo = Foo("example", value=42)
print(foo.name)
```
'''
```
Complex Bar:
```python
class BarEngine:
'''
Executes Foo objects through Bar stages.
Attributes:
foos (tuple[Foo, ...]):
Foo instances managed by the engine.
Notes:
Guarantees:
- deterministic execution order
Example:
Run engine:
```python
foo1 = Foo("a")
foo2 = Foo("b")
engine = BarEngine([foo1, foo2])
engine.run()
```
'''
```
---
# Function and method docstrings
Function docstrings define API contracts.
Recommended sections:
Args:
Returns:
Raises:
Yields:
Notes:
Example:
Example:
Simple process method:
```python
def process(foo: Foo, multiplier: int) -> int:
'''
Process a Foo instance.
Args:
foo (Foo):
Foo instance to process.
multiplier (int):
Value used to scale foo.
Returns:
int:
Processed result.
Raises:
ValueError:
If multiplier is negative.
Notes:
Guarantees:
- foo is not modified
Example:
Process foo:
```python
foo = Foo("example", value=10)
result = process(foo, multiplier=2)
print(result)
```
'''
```
Multiple Examples:
```python
def combine(foo_a: Foo, foo_b: Foo) -> Foo:
'''
Combine two Foo instances.
Args:
foo_a (Foo):
First foo.
foo_b (Foo):
Second foo.
Returns:
Foo:
Combined foo.
Example:
Basic usage:
```python
foo1 = Foo("a")
foo2 = Foo("b")
combined = combine(foo1, foo2)
```
Pipeline usage:
```python
engine = BarEngine([foo1, foo2])
engine.run()
```
'''
```
---
# Property docstrings
Properties must document return values.
Example:
Property Doc String:
```python
@property
def foos(self) -> tuple[Foo, ...]:
'''
Return contained Foo instances.
Returns:
tuple[Foo, ...]:
Stored foo objects.
Example:
```python
container = FooContainer()
foos = container.foos
```
'''
```
---
# Attribute documentation
Document attributes in class docstrings using `Attributes:`.
Example:
Attribute Doc String:
```python
'''
Represents a processing stage.
Attributes:
id (str):
Unique identifier.
enabled (bool):
Whether the stage is active.
'''
```
---
# Parsing guarantees
GSDFC ensures doc-forge can deterministically extract:
- symbol kind (module, class, function, property, attribute)
- symbol name
- parameters
- return values
- attributes
- examples
- structured Notes subsections
This enables:
- reliable MkDocs rendering
- deterministic MCP export
- accurate AI semantic interpretation
---
Notes:
- doc-forge never executes analyzed modules.
- Documentation is generated entirely through static analysis.

View File

@@ -1,26 +1,30 @@
home: index.md home: lib/index.md
groups: groups:
Loaders: Loaders:
- loaders/index.md - lib/loaders/index.md
- loaders/griffe_loader.md - lib/loaders/griffe_loader.md
Models: Models:
- models/index.md - lib/models/index.md
- models/module.md - lib/models/module.md
- models/object.md - lib/models/object.md
- models/project.md - lib/models/project.md
Navigation: Navigation:
- nav/index.md - lib/nav/index.md
- nav/spec.md - lib/nav/spec.md
- nav/resolver.md - lib/nav/resolver.md
- nav/mkdocs.md - lib/nav/mkdocs.md
Renderers: Renderers:
- renderers/index.md - lib/renderers/index.md
- renderers/base.md - lib/renderers/base.md
- renderers/mkdocs_renderer.md - lib/renderers/mkdocs_renderer.md
- renderers/mcp_renderer.md - lib/renderers/mcp_renderer.md
CLI: CLI:
- cli/index.md - lib/cli/index.md
- cli/main.md - lib/cli/main.md
- cli/commands.md - lib/cli/commands.md
- cli/mcp_utils.md - lib/cli/api_utils.md
- cli/mkdocs_utils.md - lib/cli/mcp_utils.md
- lib/cli/mkdocs_utils.md
icon:
logo: material/file-document-multiple
repo: fontawesome/brands/github

View File

@@ -1,86 +1,574 @@
""" """
# doc-forge # Summary
`doc-forge` is a renderer-agnostic Python documentation compiler designed for Renderer-agnostic Python documentation compiler that converts Python docstrings
speed, flexibility, and beautiful output. It decouples the introspection of into structured documentation for both humans (MkDocs) and machines (MCP / AI agents).
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` statically analyzes source code, builds a semantic model of modules,
classes, functions, and attributes, and renders that model into documentation
outputs without executing user code.
`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. # Installation
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 Install using pip:
`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).
- **serve**: Serve documentation (MkDocs or MCP).
- **tree**: Visualize the introspected project structure.
## Installation
Install using `pip` with the optional `mkdocs` dependencies for a complete setup:
```bash ```bash
pip install doc-forge pip install doc-forge
``` ```
## Quick Start ---
1. **Build Documentation**: # CLI usage
Introspect your package and generate documentation in one step:
```bash
# Build MkDocs site
doc-forge build --mkdocs --module my_package --site-name "My Docs"
# Build MCP resources Each site kind (`lib`, `api`, `wiki`) is built independently into `site/{kind}`.
doc-forge build --mcp --module my_package
## Build the library reference from a Python package:
```bash
doc-forge build --mkdocs --module my_package
```
## Build the API reference from an OpenAPI spec:
```bash
doc-forge build --api --openapi-spec spec.json
```
## Build the hand-written wiki:
```bash
doc-forge build --wiki --site-name my_package
```
## Generate MCP JSON documentation:
```bash
doc-forge build --mcp --module my_package
```
## Build several kinds in one pass:
```bash
doc-forge build --mcp --mkdocs --wiki --module my_package
```
Each enabled kind gets its own MkDocs config (`docs/mkdocs.{lib,api,wiki}.yml`)
and its own site under `site/`.
## Serve a site locally:
```bash
doc-forge serve --wiki # preview from docs/mkdocs.wiki.yml
doc-forge serve --lib
doc-forge serve --api
# or any config directly:
doc-forge serve --mkdocs --mkdocs-yml docs/mkdocs.wiki.yml
```
## Serve MCP locally:
```bash
doc-forge serve --mcp --module my_package
```
---
# Core concepts
## Loader
Extracts symbols, signatures, and docstrings using static analysis.
## Semantic model
Structured, renderer-agnostic representation of the API.
## Renderer
Converts the semantic model into output formats such as MkDocs or MCP JSON.
## Symbol
Any documentable object
- module
- class
- function
- method
- property
- attribute
---
# Architecture
`doc-forge` follows a compiler architecture:
## Front-end:
Static analysis of modules, classes, functions, type hints, and docstrings.
## Middle-end:
Builds a semantic model describing symbols and relationships.
## Back-end:
Renders documentation using interchangeable renderers.
This architecture ensures deterministic documentation generation.
---
# Rendering pipeline
Typical flow:
Python package
|
Loader (static analysis)
|
Semantic model
|
Renderer
|
MkDocs site or MCP JSON
---
# Google-Styled Doc-Forge Convention (GSDFC)
GSDFC defines how docstrings must be written so they render correctly in MkDocs and remain machine-parsable by doc-forge and AI tooling.
- Docstrings are the single source of truth.
- `doc-forge` compiles docstrings but does not generate documentation content.
- Documentation follows the Python import hierarchy.
- Every public symbol should have a complete and accurate docstring.
---
## General rules
- Use **Markdown headings** at package and module level.
- Use **Google-style structured sections** at class, function, and method level.
- Use type hints in signatures.
- Use parenthesized types in prose entries (`name (Type):`) that match the
signature types. This keeps docstrings self-contained and machine-parseable.
- Write summaries in imperative form.
- Sections are separated by `---`
---
# Notes subsection grouping
Group related information using labeled subsections.
Example:
Notes:
**Guarantees:**
- deterministic behavior
**Lifecycle:**
- created during initialization
- reused across executions
**Thread safety:**
- safe for concurrent reads
---
# Example formatting
- Use indentation for examples.
- Indent section contents using four spaces.
- Use code blocks for example code.
Example:
Single example:
Example:
```python
foo = Foo("example")
process(foo, multiplier=2)
```
Multiple examples:
Example:
Create foo:
```python
foo = Foo("example")
```
Run engine:
```python
engine = BarEngine([foo])
engine.run()
```
Avoid fenced code blocks inside argument descriptions and other prose lines.
Inside `Example:` sections, fenced `python` code blocks are allowed and must be
indented four spaces, matching the examples below.
---
# Separator rules
Use horizontal separators only at docstring root level to separate sections:
```markdown
---
```
Allowed locations:
- package docstrings
- module docstrings
- major documentation sections
Do not use separators inside code sections.
---
# Package docstrings
Package docstrings act as the documentation home page.
Recommended sections:
# Summary
# Installation
# Quick start
# CLI usage
# Core concepts
# Architecture
# Rendering pipeline
# Examples
# Notes
Example:
Package Doc String:
'''
# Summary
Foo-bar processing framework.
Provides tools for defining Foo objects and executing Bar pipelines.
---
# Installation
```bash
pip install foo-bar
```
---
# Quick start
```python
from foobar import Foo, BarEngine
foo = Foo("example")
engine = BarEngine([foo])
result = engine.run()
```
---
'''
---
# Module docstrings
Module docstrings describe a subsystem.
Recommended sections:
# Summary
# Examples
# Notes
Example:
Module Doc String:
'''
# Summary
Foo execution subsystem.
Provides utilities for executing Foo objects through Bar stages.
---
Example:
```python
from foobar.engine import BarEngine
from foobar.foo import Foo
foo = Foo("example")
engine = BarEngine([foo])
engine.run()
```
---
'''
---
# Class docstrings
Class docstrings define object responsibility, lifecycle, and attributes.
Recommended sections:
Attributes:
Notes:
Example:
Raises:
Example:
Simple Foo:
```python
class Foo:
'''
Represents a unit of work.
Attributes:
name (str):
Identifier of the foo instance.
value (int):
Numeric value associated with foo.
Notes:
Guarantees:
- instances are immutable after creation
Lifecycle:
- create instance
- pass to processing engine
Example:
Create and inspect a Foo:
```python
foo = Foo("example", value=42)
print(foo.name)
```
'''
``` ```
2. **Define Navigation**: Complex Bar:
Create a `docforge.nav.yml` to organize your documentation:
```yaml ```python
home: my_package/index.md class BarEngine:
groups: '''
Core API: Executes Foo objects through Bar stages.
- my_package/core/*.md
Utilities: Attributes:
- my_package/utils.md foos (tuple[Foo, ...]):
Foo instances managed by the engine.
Notes:
Guarantees:
- deterministic execution order
Example:
Run engine:
```python
foo1 = Foo("a")
foo2 = Foo("b")
engine = BarEngine([foo1, foo2])
engine.run()
```
'''
``` ```
3. **Preview**: ---
```bash
# Serve MkDocs site
doc-forge serve --mkdocs
# Serve MCP documentation # Function and method docstrings
doc-forge serve --mcp
Function docstrings define API contracts.
Recommended sections:
Args:
Returns:
Raises:
Yields:
Notes:
Example:
Example:
Simple process method:
```python
def process(foo: Foo, multiplier: int) -> int:
'''
Process a Foo instance.
Args:
foo (Foo):
Foo instance to process.
multiplier (int):
Value used to scale foo.
Returns:
int:
Processed result.
Raises:
ValueError:
If multiplier is negative.
Notes:
Guarantees:
- foo is not modified
Example:
Process foo:
```python
foo = Foo("example", value=10)
result = process(foo, multiplier=2)
print(result)
```
'''
``` ```
## Project Structure Multiple Examples:
- `docforge.loaders`: Introspects source code using static analysis (`griffe`). ```python
- `docforge.models`: The internal representation of your project, modules, and objects. def combine(foo_a: Foo, foo_b: Foo) -> Foo:
- `docforge.renderers`: Converters that turn the models into physical files. '''
- `docforge.nav`: Managers for logical-to-physical path mapping and navigation. Combine two Foo instances.
Args:
foo_a (Foo):
First foo.
foo_b (Foo):
Second foo.
Returns:
Foo:
Combined foo.
Example:
Basic usage:
```python
foo1 = Foo("a")
foo2 = Foo("b")
combined = combine(foo1, foo2)
```
Pipeline usage:
```python
engine = BarEngine([foo1, foo2])
engine.run()
```
'''
```
---
# Property docstrings
Properties must document return values.
Example:
Property Doc String:
```python
@property
def foos(self) -> tuple[Foo, ...]:
'''
Return contained Foo instances.
Returns:
tuple[Foo, ...]:
Stored foo objects.
Example:
```python
container = FooContainer()
foos = container.foos
```
'''
```
---
# Attribute documentation
Document attributes in class docstrings using `Attributes:`.
Example:
Attribute Doc String:
```python
'''
Represents a processing stage.
Attributes:
id (str):
Unique identifier.
enabled (bool):
Whether the stage is active.
'''
```
---
# Parsing guarantees
GSDFC ensures doc-forge can deterministically extract:
- symbol kind (module, class, function, property, attribute)
- symbol name
- parameters
- return values
- attributes
- examples
- structured Notes subsections
This enables:
- reliable MkDocs rendering
- deterministic MCP export
- accurate AI semantic interpretation
---
Notes:
- doc-forge never executes analyzed modules.
- Documentation is generated entirely through static analysis.
""" """
from .loaders import GriffeLoader, discover_module_paths from .loaders import GriffeLoader, discover_module_paths

View File

@@ -1,7 +1,7 @@
from .loaders import GriffeLoader, discover_module_paths
from .renderers import MkDocsRenderer, MCPRenderer
from .cli import main
from . import models from . import models
from .cli import main
from .loaders import GriffeLoader, discover_module_paths
from .renderers import MCPRenderer, MkDocsRenderer
__all__ = [ __all__ = [
"GriffeLoader", "GriffeLoader",

View File

@@ -1,18 +1,40 @@
""" """
# CLI Layer # Summary
The `docforge.cli` package provides the command-line interface for interacting Command line interface entry point for doc-forge.
with doc-forge.
## Available Commands This module exposes the primary CLI entry function used by the
`doc-forge` command. The actual command implementation resides in
`docforge.cli.main`, while this module provides a stable import path
for external tools and the package entry point configuration.
- **build**: Build documentation (MkDocs site or MCP resources). The CLI is responsible for orchestrating documentation workflows such as
- **serve**: Serve documentation (MkDocs or MCP). generating renderer sources, building documentation sites, exporting
- **tree**: Visualize the introspected project structure. machine-readable documentation bundles, and starting development or MCP
servers.
---
# Typical usage
The CLI is normally invoked through the installed command:
```bash
doc-forge <command> [options]
```
Programmatic invocation is also possible:
Example:
```python
from docforge.cli import main
main()
```
---
""" """
from .main import main from .main import main
__all__ = [ __all__ = ["main"]
"main"
]

View File

@@ -1,5 +1,3 @@
from .main import main from .main import main
__all__ = [ __all__ = ["main"]
"main"
]

118
docforge/cli/api_utils.py Normal file
View File

@@ -0,0 +1,118 @@
"""
# Summary
Utilities for building API documentation from an OpenAPI specification.
"""
import json
from dataclasses import dataclass
from pathlib import Path
import click
SWAGGER_SPEC_FILENAME = "openapi.json"
@dataclass
class OpenAPIMetadata:
"""
Metadata derived from the ``info`` block of an OpenAPI specification.
Attributes:
site_name: Spec title, used as the MkDocs site name.
site_description: Spec description, used as the site description.
site_author: Contact name (fallback: contact email), used as the
site author.
"""
site_name: str
site_description: str | None
site_author: str | None
def load_openapi_spec(spec_path: Path) -> dict:
"""
Load and validate an OpenAPI specification from a JSON file.
Args:
spec_path (Path):
Path to the OpenAPI JSON specification file.
Returns:
dict:
The parsed OpenAPI specification.
Raises:
click.ClickException:
If the file cannot be read or the ``info`` block is invalid.
"""
if not spec_path.exists():
raise click.ClickException(f"OpenAPI spec not found: {spec_path}")
try:
data = json.loads(spec_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError) as exc:
raise click.ClickException(f"Unable to read OpenAPI spec: {exc}") from exc
if not isinstance(data, dict):
raise click.ClickException("OpenAPI spec must be a JSON mapping")
info = data.get("info")
if not isinstance(info, dict) or not isinstance(info.get("title"), str):
raise click.ClickException("OpenAPI spec missing 'info.title'")
return data
def derive_metadata(spec: dict) -> OpenAPIMetadata:
"""
Derive MkDocs site metadata from an OpenAPI spec ``info`` block.
Args:
spec (dict):
Parsed OpenAPI specification.
Returns:
OpenAPIMetadata:
Site name, description, and author derived from the spec.
"""
info = spec["info"]
contact = info.get("contact")
if isinstance(contact, dict):
author = contact.get("name") or contact.get("email")
else:
author = None
return OpenAPIMetadata(
site_name=info.get("title", ""),
site_description=info.get("description"),
site_author=author,
)
def generate_api_sources(spec: dict, docs_dir: Path) -> None:
"""
Generate swagger-enabled Markdown sources and the spec copy.
The specification is written as ``openapi.json`` inside ``docs_dir`` and
an ``index.md`` embedding the swagger UI is generated alongside it.
Args:
spec (dict):
Parsed OpenAPI specification.
docs_dir (Path):
Directory (for example ``docs/api``) where the swagger
sources are written.
"""
docs_dir.mkdir(parents=True, exist_ok=True)
spec_path = docs_dir / SWAGGER_SPEC_FILENAME
spec_json = json.dumps(spec, indent=2)
if not spec_path.exists() or spec_path.read_text(encoding="utf-8") != spec_json:
spec_path.write_text(spec_json, encoding="utf-8")
index_path = docs_dir / "index.md"
content = "# API Reference\n\n" f'<swagger-ui src="{SWAGGER_SPEC_FILENAME}"/>\n'
if not index_path.exists() or index_path.read_text(encoding="utf-8") != content:
index_path.write_text(content, encoding="utf-8")

View File

@@ -0,0 +1,13 @@
from dataclasses import dataclass
from pathlib import Path
from typing import Any
@dataclass
class OpenAPIMetadata:
site_name: str
site_description: str | None
site_author: str | None
def load_openapi_spec(spec_path: Path) -> dict[Any, Any]: ...
def derive_metadata(spec: dict[Any, Any]) -> OpenAPIMetadata: ...
def generate_api_sources(spec: dict[Any, Any], docs_dir: Path) -> None: ...

View File

@@ -1,97 +1,284 @@
import click """
# Summary
Command definitions for the doc-forge CLI.
Provides the CLI structure using Click, including build, serve, and tree commands.
---
Notes:
- The `build` command validates requested modes before generating anything.
- `--mkdocs`, `--api`, and `--wiki` each emit their own MkDocs config and
build (`docs/mkdocs.{kind}.yml` into `site/{kind}`); `--mcp` generates a
machine-readable bundle independently.
---
"""
import os
from pathlib import Path from pathlib import Path
from typing import Sequence, Optional
import click
from docforge.cli import api_utils, mcp_utils, mkdocs_utils
from docforge.loaders import GriffeLoader from docforge.loaders import GriffeLoader
from docforge.cli import mkdocs_utils from docforge.models import DocObject
from docforge.cli import mcp_utils
@click.group() @click.group()
def cli() -> None: def cli() -> None:
""" """
doc-forge CLI: A tool for introspecting Python projects and generating Root command group for the doc-forge CLI.
documentation.
Provides commands for building, serving, and inspecting
documentation generated from Python source code.
""" """
pass pass
@cli.command() @cli.command()
@click.option("--mcp", is_flag=True, help="Build MCP resources") @click.option("--mcp", is_flag=True, help="Build MCP resources")
@click.option("--mkdocs", is_flag=True, help="Build MkDocs site") @click.option("--mkdocs", is_flag=True, help="Build the lib MkDocs site")
@click.option("--module-is-source", is_flag=True, help="Module is source folder and to be treated as root folder") @click.option("--api", is_flag=True, help="Build API docs from an OpenAPI spec")
@click.option(
"--wiki", is_flag=True, help="Build a hand-written wiki as its own MkDocs site"
)
@click.option(
"--refresh",
is_flag=True,
help="Regenerate existing docs/mkdocs.{kind}.yml configs from templates",
)
@click.option(
"--module-is-source",
is_flag=True,
help="Module is source folder and to be treated as root folder",
)
@click.option("--module", help="Python module to document") @click.option("--module", help="Python module to document")
@click.option(
"--openapi-spec",
type=click.Path(path_type=Path),
help="Path to the OpenAPI JSON specification",
)
@click.option("--project-name", help="Project name override") @click.option("--project-name", help="Project name override")
# MkDocs specific @click.option("--site-name", help="MkDocs site name for lib and wiki sites")
@click.option("--site-name", help="MkDocs site name") @click.option(
@click.option("--docs-dir", type=click.Path(path_type=Path), default=Path("docs"), help="Directory for MD sources") "--docs-dir",
@click.option("--nav", "nav_file", type=click.Path(path_type=Path), default=Path("docforge.nav.yml"), type=click.Path(path_type=Path),
help="Nav spec path") default=Path("docs"),
@click.option("--template", type=click.Path(path_type=Path), help="MkDocs template path") help="MkDocs documentation root",
@click.option("--mkdocs-yml", type=click.Path(path_type=Path), default=Path("mkdocs.yml"), help="Output config path") )
# MCP specific @click.option(
@click.option("--out-dir", type=click.Path(path_type=Path), default=Path("mcp_docs"), help="MCP output directory") "--wiki-dir",
type=click.Path(path_type=Path),
default=Path("docs/wiki"),
help="Hand-written wiki directory built as its own MkDocs site",
)
@click.option(
"--nav",
"nav_file",
type=click.Path(path_type=Path),
default=Path("docforge.nav.yml"),
help="Nav spec path",
)
@click.option(
"--template", type=click.Path(path_type=Path), help="MkDocs template path"
)
@click.option(
"--out-dir",
type=click.Path(path_type=Path),
default=Path("docs/mcp"),
help="MCP output directory",
)
def build( def build(
mcp: bool, mcp: bool,
mkdocs: bool, mkdocs: bool,
api: bool,
wiki: bool,
refresh: bool,
module_is_source: bool, module_is_source: bool,
module: Optional[str], module: str | None,
project_name: Optional[str], openapi_spec: Path | None,
site_name: Optional[str], project_name: str | None,
site_name: str | None,
docs_dir: Path, docs_dir: Path,
wiki_dir: Path,
nav_file: Path, nav_file: Path,
template: Optional[Path], template: Path | None,
mkdocs_yml: Path,
out_dir: Path, out_dir: Path,
) -> None: ) -> None:
""" """
Build documentation (MkDocs site or MCP resources). Build documentation artifacts.
This command orchestrates the full build process: This command runs the full documentation pipeline: it loads Python
1. Introspects the code (Griffe) modules, generates renderer-specific documentation sources, and
2. Renders sources (MkDocs Markdown or MCP JSON) optionally builds the final output.
3. (MkDocs only) Generates config and runs the final site build.
Depending on the selected options, the build can target:
- A lib MkDocs site (`--mkdocs`) for library reference docs
- A swagger-enabled API MkDocs site (`--api`) built from an OpenAPI spec
- A wiki MkDocs site (`--wiki`) built from hand-written markdown
- MCP structured documentation resources (`--mcp`)
Each enabled site kind produces its own MkDocs configuration
(`docs/mkdocs.{kind}.yml`) and its own build (`site/{kind}`).
Notes:
- At least one of `--mcp`, `--mkdocs`, `--wiki`, or `--api` must be
provided.
- `--mkdocs`, `--api`, and `--wiki` emit independent MkDocs builds,
while `--mcp` emits a machine-readable bundle.
- Configuration files are generated only when absent; an existing
`docs/mkdocs.{kind}.yml` is used as-is. Pass `--refresh` to
rebaseline it from the templates.
Args: Args:
mcp: Use the MCP documentation builder. mcp (bool):
mkdocs: Use the MkDocs documentation builder. Enable MCP documentation generation.
module_is_source: Module is the source folder and to be treated as the root folder.
module: The dotted path of the module to the document. mkdocs (bool):
project_name: Optional override for the project name. Enable the lib MkDocs documentation generation.
site_name: (MkDocs) The site display name. Defaults to module name.
docs_dir: (MkDocs) Target directory for Markdown sources. api (bool):
nav_file: (MkDocs) Path to the docforge.nav.yml specification. Enable API documentation generation from an OpenAPI spec.
template: (MkDocs) Optional custom mkdocs.yml template.
mkdocs_yml: (MkDocs) Target path for the generated mkdocs.yml. wiki (bool):
out_dir: (MCP) Target directory for MCP JSON resources. Build a hand-written wiki directory as its own MkDocs site.
refresh (bool):
Regenerate ``docs/mkdocs.{kind}.yml`` from templates even when
it already exists. By default, existing configs are used as-is.
module_is_source (bool):
Treat the specified module directory as the project root.
module (str | None):
Python module import path to document.
openapi_spec (Path | None):
Path to the OpenAPI JSON specification used for API docs.
project_name (str | None):
Optional override for the project name.
site_name (str | None):
Display name for the lib and wiki MkDocs sites.
docs_dir (Path):
Shared documentation root used for generated sources.
wiki_dir (Path):
Directory containing hand-written wiki markdown files.
nav_file (Path):
Path to the navigation specification file.
template (Path | None):
Optional custom MkDocs configuration template.
out_dir (Path):
Output directory for generated MCP resources.
Raises:
click.UsageError:
If required options are missing or conflicting.
""" """
if not mcp and not mkdocs: if not mcp and not mkdocs and not api and not wiki:
raise click.UsageError("Must specify either --mcp or --mkdocs") raise click.UsageError("Must specify either --mcp, --mkdocs, --wiki, or --api")
if api:
if not openapi_spec:
raise click.UsageError("--openapi-spec is required for API build")
if site_name and not mkdocs and not wiki:
raise click.UsageError(
"--site-name cannot be overridden for API build; "
"the OpenAPI spec provides the site name"
)
if mkdocs and not module:
raise click.UsageError("--module is required for MkDocs build")
if mcp and not module:
raise click.UsageError("--module is required for MCP build")
spec: dict | None = None
if api:
spec = api_utils.load_openapi_spec(openapi_spec)
kinds: list[str] = []
if mkdocs: if mkdocs:
if not module: kinds.append("lib")
raise click.UsageError("--module is required for MkDocs build") lib_dir = docs_dir / "lib"
if not site_name: click.echo(f"Generating MkDocs sources in {lib_dir}...")
site_name = module
click.echo(f"Generating MkDocs sources in {docs_dir}...")
mkdocs_utils.generate_sources( mkdocs_utils.generate_sources(
module, module,
docs_dir, lib_dir,
project_name, project_name,
module_is_source, module_is_source,
readme_dir=Path(".").resolve(),
) )
click.echo(f"Generating MkDocs config {mkdocs_yml}...") if api:
mkdocs_utils.generate_config(docs_dir, nav_file, template, mkdocs_yml, site_name) kinds.append("api")
api_dir = docs_dir / "api"
click.echo(f"Generating API sources in {api_dir}...")
api_utils.generate_api_sources(spec, api_dir)
click.echo("Running MkDocs build...") if wiki:
mkdocs_utils.build(mkdocs_yml) kinds.append("wiki")
click.echo("MkDocs build completed.")
if kinds:
api_metadata: api_utils.OpenAPIMetadata | None = None
if api:
api_metadata = api_utils.derive_metadata(spec)
theme_icon = mkdocs_utils.load_spec_icon(nav_file)
config_paths: list[Path] = []
for kind in kinds:
out = docs_dir / f"mkdocs.{kind}.yml"
if out.exists() and not refresh:
click.echo(
f"Using existing MkDocs config {out} (run --refresh to rebaseline)..."
)
config_paths.append(out)
continue
kind_root = wiki_dir if kind == "wiki" else docs_dir / kind
site_name_kind = site_name or module or Path.cwd().name
site_description: str | None = None
site_author: str | None = None
if kind == "api" and api_metadata:
site_name_kind = api_metadata.site_name
site_description = api_metadata.site_description
site_author = api_metadata.site_author
if kind == "lib":
nav_block, _kind_icon = mkdocs_utils.build_lib_nav(nav_file, docs_dir)
elif kind == "wiki":
nav_block = mkdocs_utils.build_wiki_nav_block(wiki_dir)
else:
nav_block = [{"API Reference": "index.md"}]
click.echo(f"Generating MkDocs config {out}...")
mkdocs_utils.generate_site_config(
kind,
kind_root,
nav_block,
out,
site_name_kind,
Path(os.path.relpath(kind_root, docs_dir)).as_posix(),
Path(os.path.relpath(Path.cwd() / "site" / kind, docs_dir)).as_posix(),
template=template,
site_description=site_description,
site_author=site_author,
theme_icon=theme_icon,
)
config_paths.append(out)
click.echo("Running MkDocs builds...")
mkdocs_utils.build_configs(config_paths)
if mcp: if mcp:
if not module:
raise click.UsageError("--module is required for MCP build")
click.echo(f"Generating MCP resources in {out_dir}...") click.echo(f"Generating MCP resources in {out_dir}...")
mcp_utils.generate_resources(module, project_name, out_dir) mcp_utils.generate_resources(module, project_name, out_dir)
click.echo("MCP build completed.") click.echo("MCP build completed.")
@@ -99,38 +286,105 @@ def build(
@cli.command() @cli.command()
@click.option("--mcp", is_flag=True, help="Serve MCP documentation") @click.option("--mcp", is_flag=True, help="Serve MCP documentation")
@click.option("--mkdocs", is_flag=True, help="Serve MkDocs site") @click.option("--mkdocs", is_flag=True, help="Serve an MkDocs site from --mkdocs-yml")
@click.option("--lib", is_flag=True, help="Serve the lib MkDocs site")
@click.option("--api", is_flag=True, help="Serve the API MkDocs site")
@click.option("--wiki", is_flag=True, help="Serve the wiki MkDocs site")
@click.option("--module", help="Python module to serve") @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(
@click.option("--out-dir", type=click.Path(path_type=Path), default=Path("mcp_docs"), help="MCP root directory") "--mkdocs-yml",
type=click.Path(path_type=Path),
default=Path("docs/mkdocs.wiki.yml"),
help="MkDocs config path",
)
@click.option(
"--out-dir",
type=click.Path(path_type=Path),
default=Path("docs/mcp"),
help="MCP root directory",
)
def serve( def serve(
mcp: bool, mcp: bool,
mkdocs: bool, mkdocs: bool,
module: Optional[str], lib: bool,
api: bool,
wiki: bool,
module: str | None,
mkdocs_yml: Path, mkdocs_yml: Path,
out_dir: Path, out_dir: Path,
) -> None: ) -> None:
""" """
Serve documentation (MkDocs or MCP). Serve generated documentation locally.
Depending on the selected mode, this command starts either:
- A MkDocs development server for browsing a site, or
- An MCP server exposing structured documentation resources
The kind flags (`--lib`, `--api`, `--wiki`) select the generated
per-kind config (`docs/mkdocs.{kind}.yml`); `--mkdocs` serves the config
passed via `--mkdocs-yml`.
Args: Args:
mcp: Serve MCP resources via an MCP server. mcp (bool):
mkdocs: Serve the MkDocs site using the built-in development server. Serve documentation using the MCP 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.
"""
if mcp and mkdocs:
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 (bool):
mkdocs_utils.serve(mkdocs_yml) Serve the MkDocs development site from ``--mkdocs-yml``.
elif mcp:
lib (bool):
Serve the lib MkDocs site.
api (bool):
Serve the API MkDocs site.
wiki (bool):
Serve the wiki MkDocs site.
module (str | None):
Python module import path to serve via MCP.
mkdocs_yml (Path):
Path to the MkDocs configuration file.
out_dir (Path):
Root directory containing MCP documentation resources.
Raises:
click.UsageError:
If invalid or conflicting options are provided.
"""
selected = [
name
for name, enabled in (
("mcp", mcp),
("mkdocs", mkdocs),
("lib", lib),
("api", api),
("wiki", wiki),
)
if enabled
]
if len(selected) != 1:
raise click.UsageError(
"Must specify exactly one of --mcp, --mkdocs, --lib, --api, --wiki"
)
if mcp:
if not module:
raise click.UsageError("--module is required for MCP serve")
mcp_utils.serve(module, out_dir) mcp_utils.serve(module, out_dir)
return
config = mkdocs_yml
if lib:
config = Path("docs/mkdocs.lib.yml")
elif api:
config = Path("docs/mkdocs.api.yml")
elif wiki:
config = Path("docs/mkdocs.wiki.yml")
mkdocs_utils.serve(config)
@cli.command() @cli.command()
@@ -145,14 +399,21 @@ def serve(
) )
def tree( def tree(
module: str, module: str,
project_name: Optional[str], project_name: str | None,
) -> None: ) -> None:
""" """
Visualize the project structure in the terminal. Display the documentation object tree for a module.
This command introspects the specified module and prints a
hierarchical representation of the discovered documentation
objects, including modules, classes, functions, and members.
Args: Args:
module: The module import path to recursively introspect. module (str):
project_name: Optional override for the project name shown at the root. Python module import path to introspect.
project_name (str | None):
Optional name to display as the project root.
""" """
loader = GriffeLoader() loader = GriffeLoader()
project = loader.load_project([module], project_name) project = loader.load_project([module], project_name)
@@ -165,13 +426,19 @@ def tree(
_print_object(obj, indent="│ ") _print_object(obj, indent="│ ")
def _print_object(obj, indent: str) -> None: def _print_object(obj: DocObject, indent: str) -> None:
""" """
Recursive helper to print doc objects and their members to the console. Recursively print a documentation object and its members.
This helper function traverses the documentation object graph
and prints each object with indentation to represent hierarchy.
Args: Args:
obj: The DocObject instance to print. obj (DocObject):
indent: Current line indentation (e.g., '│ '). Documentation object to print.
indent (str):
Current indentation prefix used for nested members.
""" """
click.echo(f"{indent}├── {obj.name}") click.echo(f"{indent}├── {obj.name}")
for member in obj.get_all_members(): for member in obj.get_all_members():

View File

@@ -1,34 +1,39 @@
from click.core import Group
from pathlib import Path from pathlib import Path
from typing import Sequence, Optional, Any
from click.core import Group
from docforge.models import DocObject
cli: Group cli: Group
def build( def build(
mcp: bool, mcp: bool,
mkdocs: bool, mkdocs: bool,
api: bool,
wiki: bool,
module_is_source: bool, module_is_source: bool,
module: Optional[str], module: str | None,
project_name: Optional[str], openapi_spec: Path | None,
site_name: Optional[str], project_name: str | None,
site_name: str | None,
docs_dir: Path, docs_dir: Path,
wiki_dir: Path,
nav_file: Path, nav_file: Path,
template: Optional[Path], template: Path | None,
mkdocs_yml: Path,
out_dir: Path, out_dir: Path,
) -> None: ... ) -> None: ...
def serve( def serve(
mcp: bool, mcp: bool,
mkdocs: bool, mkdocs: bool,
module: Optional[str], lib: bool,
api: bool,
wiki: bool,
module: str | None,
mkdocs_yml: Path, mkdocs_yml: Path,
out_dir: Path, out_dir: Path,
) -> None: ... ) -> None: ...
def tree( def tree(
module: str, module: str,
project_name: Optional[str], project_name: str | None,
) -> None: ... ) -> None: ...
def _print_object(obj: DocObject, indent: str) -> None: ...
def _print_object(obj: Any, indent: str) -> None: ...

View File

@@ -1,14 +1,25 @@
""" """
Main entry point for the doc-forge CLI. This module delegates all command # Summary
execution to docforge.cli.commands.
Command-line entry point for the doc-forge CLI.
This module exposes the executable entry point that initializes the
Click command group defined in `docforge.cli.commands`.
""" """
from docforge.cli.commands import cli from docforge.cli.commands import cli
def main() -> None: def main() -> None:
""" """
CLI Entry point. Boots the click application. Run the doc-forge command-line interface.
This function initializes and executes the Click CLI application.
It is used as the console entry point when invoking `doc-forge`
from the command line.
""" """
cli() cli()
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -1,17 +1,48 @@
"""
# Summary
Utilities for working with MCP in the doc-forge CLI.
---
Notes:
- `generate_resources` produces the bundle consumed by `MCPServer`:
`index.json`, `nav.json`, and per-module resources under `modules/`.
- Resource URIs use the `docs://` scheme: `docs://index`, `docs://nav`,
and `docs://modules/{module}`.
---
"""
from pathlib import Path from pathlib import Path
import click import click
from docforge.loaders import GriffeLoader, discover_module_paths from docforge.loaders import GriffeLoader, discover_module_paths
from docforge.renderers import MCPRenderer from docforge.renderers import MCPRenderer
from docforge.servers import MCPServer from docforge.servers import MCPServer
def generate_resources(module: str, project_name: str | None, out_dir: Path) -> None: def generate_resources(module: str, project_name: str | None, out_dir: Path) -> None:
""" """
Generate MCP-compatible documentation resources. Generate MCP documentation resources from a Python module.
The function performs project introspection, builds the internal
documentation model, and renders MCP-compatible JSON resources
to the specified output directory.
Args: Args:
module: The dotted path of the primary module to document. module (str):
project_name: Optional override for the project name. Python module import path used as the entry point for
out_dir: Directory where the MCP JSON resources and nav will be written. documentation generation.
project_name (str | None):
Optional override for the project name used in generated
documentation metadata.
out_dir (Path):
Directory where MCP resources (index.json, nav.json, and module data)
will be written.
""" """
loader = GriffeLoader() loader = GriffeLoader()
discovered_paths = discover_module_paths(module) discovered_paths = discover_module_paths(module)
@@ -20,16 +51,29 @@ def generate_resources(module: str, project_name: str | None, out_dir: Path) ->
renderer = MCPRenderer() renderer = MCPRenderer()
renderer.generate_sources(project, out_dir) renderer.generate_sources(project, out_dir)
def serve(module: str, mcp_root: Path) -> None: def serve(module: str, mcp_root: Path) -> None:
""" """
Serve MCP documentation from a pre-built bundle. Start an MCP server for a pre-generated documentation bundle.
The server exposes documentation resources such as project metadata,
navigation structure, and module documentation through MCP endpoints.
Args: Args:
module: The dotted path of the primary module to serve. module (str):
mcp_root: Path to the directory containing index.json, nav.json, and modules/. Python module import path used to identify the served
documentation instance.
mcp_root (Path):
Path to the directory containing the MCP documentation
bundle (index.json, nav.json, and modules/).
Raises:
click.ClickException:
If the MCP documentation bundle is missing required files or directories.
""" """
if not mcp_root.exists(): if not mcp_root.exists():
raise click.ClickException(f"mcp_docs directory not found: {mcp_root}") raise click.ClickException(f"MCP docs directory not found: {mcp_root}")
required = [ required = [
mcp_root / "index.json", mcp_root / "index.json",

View File

@@ -1,4 +1,6 @@
from pathlib import Path from pathlib import Path
def generate_resources(module: str, project_name: str | None, out_dir: Path) -> None: ... def generate_resources(
module: str, project_name: str | None, out_dir: Path
) -> None: ...
def serve(module: str, mcp_root: Path) -> None: ... def serve(module: str, mcp_root: Path) -> None: ...

View File

@@ -1,25 +1,70 @@
from pathlib import Path """
# Summary
Utilities for working with MkDocs in the doc-forge CLI.
---
Notes:
- A separate `mkdocs.{kind}.yml` configuration and build is emitted per
enabled kind (lib, api, wiki), each scoped to its own `docs_dir` and
written into its own `site_dir` (`site/lib`, `site/api`, `site/wiki`).
- Navigation blocks are re-rooted per kind: the wiki navigation drops its
leading `wiki/` scope and the resolved nav spec drops its `lib/` scope.
---
"""
from collections.abc import Iterable
from importlib import resources from importlib import resources
from pathlib import Path
from typing import Any, cast
import click import click
import yaml import yaml
from docforge.loaders import GriffeLoader, discover_module_paths from docforge.loaders import GriffeLoader, discover_module_paths
from docforge.nav import (
MkDocsNavEmitter,
build_wiki_nav,
load_nav_spec,
resolve_nav,
)
from docforge.renderers import MkDocsRenderer from docforge.renderers import MkDocsRenderer
from docforge.nav import load_nav_spec, resolve_nav, MkDocsNavEmitter
def generate_sources( def generate_sources(
module: str, module: str,
docs_dir: Path, docs_dir: Path,
project_name: str | None = None, project_name: str | None = None,
module_is_source: bool | None = None, module_is_source: bool | None = None,
readme_dir: Path | None = None,
) -> None: ) -> None:
""" """
Generate Markdown source files for the specified module. Generate MkDocs Markdown sources for a Python module.
This function introspects the specified module, builds the internal
documentation model, and renders Markdown documentation files for
use with MkDocs.
Args: Args:
module: The dotted path of the primary module to document. module (str):
project_name: Optional override for the project name. Python module import path used as the entry point for
docs_dir: Directory where the generated Markdown files will be written. documentation generation.
module_is_source: Module is the source folder and to be treated as the root folder.
docs_dir (Path):
Directory where the generated Markdown files will be written.
project_name (str | None):
Optional override for the project name used in documentation metadata.
module_is_source (bool | None):
If True, treat the specified module directory as the project root
rather than a nested module.
readme_dir (Path | None):
Directory where the generated README.md should be written. If not
provided, defaults to the parent of ``docs_dir``.
""" """
loader = GriffeLoader() loader = GriffeLoader()
discovered_paths = discover_module_paths(module) discovered_paths = discover_module_paths(module)
@@ -32,66 +77,377 @@ def generate_sources(
module_is_source, module_is_source,
) )
def generate_config(docs_dir: Path, nav_file: Path, template: Path | None, out: Path, site_name: str) -> None: renderer.generate_readme(
project,
docs_dir,
module_is_source,
readme_dir,
)
def build_lib_nav(
nav_file: Path,
docs_root: Path,
) -> tuple[list[dict[str, Any]], dict[str, str] | None]:
""" """
Generate an mkdocs.yml configuration file. Build the re-rooted navigation block for a lib site.
The navigation specification is resolved against the shared documentation
root and every resulting path is re-rooted relative to the ``lib``
subdirectory by stripping its leading ``lib/`` scope component.
Args: Args:
docs_dir: Path to the directory containing documentation Markdown files. nav_file (Path):
nav_file: Path to the docforge.nav.yml specification. Path to the `docforge.nav.yml` navigation specification.
template: Optional path to an mkdocs.yml template (overrides built-in).
out: Path where the final mkdocs.yml will be written. docs_root (Path):
site_name: The display name for the documentation site. Shared documentation root containing the ``lib`` sources.
Returns:
tuple[list[dict[str, Any]], dict[str, str] | None]:
The re-rooted navigation block and the optional theme icon
mapping from the specification.
Raises:
click.FileError:
If the navigation specification cannot be found.
""" """
if not nav_file.exists(): if not nav_file.exists():
raise click.FileError(str(nav_file), hint="Nav spec not found") raise click.FileError(str(nav_file), hint="Nav spec not found")
spec = load_nav_spec(nav_file) spec = load_nav_spec(nav_file)
resolved = resolve_nav(spec, docs_dir) resolved = resolve_nav(spec, docs_root)
nav_block = MkDocsNavEmitter().emit(resolved) block = MkDocsNavEmitter().emit(resolved)
return cast(list[dict[str, Any]], _strip_scope(block, "lib")), spec.icon
# Load template
if template is not None: def build_wiki_nav_block(wiki_dir: Path) -> list[dict[str, Any]]:
if not template.exists(): """
raise click.FileError(str(template), hint="Template not found") Build the re-rooted navigation block for a wiki site.
data = yaml.safe_load(template.read_text(encoding="utf-8"))
else: The wiki navigation derived from the wiki file structure is re-rooted
text = ( relative to the wiki directory itself by stripping the leading ``wiki/``
resources.files("docforge.templates") scope component.
.joinpath("mkdocs.sample.yml")
.read_text(encoding="utf-8") Args:
) wiki_dir (Path):
data = yaml.safe_load(text) Path to the hand-written wiki directory, for example ``docs/wiki``.
Returns:
list[dict[str, Any]]:
Navigation entries relative to the wiki directory.
Raises:
click.FileError:
If the wiki directory does not exist.
"""
if not wiki_dir.exists():
raise click.FileError(str(wiki_dir), hint="Wiki dir not found")
return cast(list[dict[str, Any]], _strip_scope(build_wiki_nav(wiki_dir), "wiki"))
def load_spec_icon(nav_file: Path) -> dict[str, str] | None:
"""
Load the theme icon mapping from a navigation specification.
Args:
nav_file (Path):
Path to the navigation specification file.
Returns:
dict[str, str] | None:
The icon mapping, or ``None`` when the specification file is
absent or cannot be parsed.
"""
if not nav_file.exists():
return None
try:
return load_nav_spec(nav_file).icon
except (ValueError, yaml.YAMLError):
return None
def _strip_scope(value: object, scope: str) -> object:
"""
Re-root navigation paths by removing a leading scope component.
The navigation block is walked recursively and every relative path
string that starts with ``{scope}/`` has that prefix removed, so a lib
or wiki site scoped to its own ``docs_dir`` can reuse paths that were
originally written relative to the shared documentation root.
Args:
value (object):
Navigation block, group list, path string, or scalar value.
scope (str):
Leading path component to strip, for example ``lib`` or ``wiki``.
Returns:
object:
The navigation structure with re-rooted paths.
"""
if isinstance(value, str):
prefix = f"{scope}/"
return value[len(prefix) :] if value.startswith(prefix) else value
if isinstance(value, list):
return [_strip_scope(item, scope) for item in value]
if isinstance(value, dict):
return {key: _strip_scope(val, scope) for key, val in value.items()}
return value
def generate_site_config(
kind: str,
kind_root: Path,
nav_block: list[dict[str, Any]],
out: Path,
site_name: str,
docs_dir: str,
site_dir: str,
template: Path | None = None,
site_description: str | None = None,
site_author: str | None = None,
theme_icon: dict[str, str] | None = None,
) -> None:
"""
Generate a per-kind `mkdocs.{kind}.yml` configuration file.
The configuration is created by merging the shared ``mkdocs.common.yml``
template with the fragment contributed by the kind (``lib``, ``api``, or
``wiki``). Both ``docs_dir`` and ``site_dir`` are written relative to the
configuration file's directory: the kind's sources when expressed as a
sibling path (for example ``lib``) and the per-kind site output (for
example ``../site/lib``).
Args:
kind (str):
Documentation kind, one of ``lib``, ``api``, or ``wiki``.
kind_root (Path):
Directory scoped to the kind (for example ``docs/lib``) that
serves as the MkDocs ``docs_dir``.
nav_block (list[dict[str, Any]]):
Re-rooted navigation entries for the kind's site.
out (Path):
Destination path where the generated ``mkdocs.{kind}.yml`` file
is written.
site_name (str):
Display name for the generated documentation site.
docs_dir (str):
MkDocs ``docs_dir`` value, relative to the configuration
file's directory.
site_dir (str):
MkDocs ``site_dir`` value, relative to the configuration
file's directory.
template (Path | None):
Optional path to a fully custom MkDocs configuration template
that replaces the built-in templates entirely.
site_description (str | None):
Optional site description written into the configuration.
site_author (str | None):
Optional site author written into the configuration.
theme_icon (dict[str, str] | None):
Optional mapping of theme icon entries injected as
``theme.icon``.
"""
data = _load_template(template, [kind])
data["site_name"] = site_name data["site_name"] = site_name
if site_description:
data["site_description"] = site_description
if site_author:
data["site_author"] = site_author
data["docs_dir"] = docs_dir
data["site_dir"] = site_dir
data["nav"] = nav_block data["nav"] = nav_block
if theme_icon:
theme = data.setdefault("theme", {})
if not isinstance(theme, dict):
theme = {}
theme["icon"] = theme_icon
data["theme"] = theme
out.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8") out.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8")
def build(mkdocs_yml: Path) -> None:
def _load_template(
template: Path | None,
modes: Iterable[str] | None,
) -> dict:
""" """
Build the documentation site using MkDocs. Load the MkDocs configuration template.
When a custom template path is provided, it is used as-is. Otherwise the
shared ``mkdocs.common.yml`` template is deep-merged with the fragments
contributed by each enabled mode (``lib``, ``api``, or ``wiki``).
Args: Args:
mkdocs_yml: Path to the mkdocs.yml configuration file. template (Path | None):
Optional fully custom template that replaces the built-ins.
modes (Iterable[str] | None):
Documentation modes whose template fragments should be merged.
Returns:
dict:
Merged MkDocs configuration mapping.
Raises:
click.FileError:
If a referenced template file cannot be found.
""" """
if not mkdocs_yml.exists(): if template is not None:
raise click.ClickException(f"mkdocs.yml not found: {mkdocs_yml}") if not template.exists():
raise click.FileError(str(template), hint="Template not found")
return yaml.safe_load(template.read_text(encoding="utf-8"))
from mkdocs.config import load_config data: dict = {}
active_modes = list(modes) if modes else ["lib"]
parts = ["common", *active_modes]
seen: set[str] = set()
for part_name in parts:
if part_name in seen:
continue
seen.add(part_name)
text = (
resources.files("docforge.templates")
.joinpath(f"mkdocs.{part_name}.yml")
.read_text(encoding="utf-8")
)
part: dict = yaml.safe_load(text)
data = _deep_merge(data, part)
return data
def _item_name(item: object) -> str:
"""
Return the identifying name of a list entry.
String entries identify as themselves; mapping entries identify by their
first key. This is used to deduplicate plugin and extension lists.
Args:
item (object):
List entry, either a string or a single-key mapping.
Returns:
str:
The identifying name of the entry.
"""
if isinstance(item, dict):
return next(iter(item.keys()), "")
return str(item)
def _merge_list(base: list, added: list) -> list:
"""
Merge two lists, preserving order and dropping duplicates by name.
Args:
base (list):
Existing list entries.
added (list):
Entries to append when not already present.
Returns:
list:
Merged list with duplicates removed.
"""
result = list(base)
names = {_item_name(item) for item in result}
for item in added:
name = _item_name(item)
if name not in names:
result.append(item)
names.add(name)
return result
def _deep_merge(base: dict, part: dict) -> dict:
"""
Deep merge a template fragment into a base configuration.
Mappings are merged recursively, while lists are combined by
deduplicating entries by their identifying name. Non-container values in
the fragment override the base.
Args:
base (dict):
Configuration being built up.
part (dict):
Template fragment to merge into the base.
Returns:
dict:
The merged configuration.
"""
for key, value in part.items():
if isinstance(value, dict) and isinstance(base.get(key), dict):
base[key] = _deep_merge(base[key], value)
elif isinstance(value, list) and isinstance(base.get(key), list):
base[key] = _merge_list(base[key], value)
else:
base[key] = value
return base
def build_configs(yml_paths: Iterable[Path]) -> None:
"""
Build the MkDocs documentation site for every given configuration.
Each configuration file is loaded and built in turn, producing the
per-kind static sites (``site/lib``, ``site/api``, ``site/wiki``).
Args:
yml_paths (Iterable[Path]):
Configuration files to build, in order.
Raises:
click.ClickException:
If a configuration file does not exist.
"""
from mkdocs.commands.build import build as mkdocs_build from mkdocs.commands.build import build as mkdocs_build
from mkdocs.config import load_config
for yml in yml_paths:
if not yml.exists():
raise click.ClickException(f"mkdocs.yml not found: {yml}")
mkdocs_build(load_config(str(yml)))
mkdocs_build(load_config(str(mkdocs_yml)))
def serve(mkdocs_yml: Path) -> None: def serve(mkdocs_yml: Path) -> None:
""" """
Serve the documentation site with live-reload using MkDocs. Start an MkDocs development server with live reload.
The server watches documentation files and automatically reloads
the site when changes are detected.
Args: Args:
mkdocs_yml: Path to the mkdocs.yml configuration file. mkdocs_yml (Path):
Path to the `mkdocs.yml` configuration file.
Raises:
click.ClickException:
If the configuration file does not exist.
""" """
if not mkdocs_yml.exists(): if not mkdocs_yml.exists():
raise click.ClickException(f"mkdocs.yml not found: {mkdocs_yml}") raise click.ClickException(f"mkdocs.yml not found: {mkdocs_yml}")
from mkdocs.commands.serve import serve as mkdocs_serve from mkdocs.commands.serve import serve as mkdocs_serve
mkdocs_serve(config_file=str(mkdocs_yml)) mkdocs_serve(config_file=str(mkdocs_yml))

View File

@@ -1,11 +1,33 @@
from collections.abc import Iterable
from pathlib import Path from pathlib import Path
from typing import Any
def generate_sources( def generate_sources(
module: str, module: str,
docs_dir: Path, docs_dir: Path,
project_name: str | None = None, project_name: str | None = None,
module_is_source: bool | None = None, module_is_source: bool | None = None,
readme_dir: Path | None = None,
) -> None: ... ) -> None: ...
def generate_config(docs_dir: Path, nav_file: Path, template: Path | None, out: Path, site_name: str) -> None: ... def build_lib_nav(
def build(mkdocs_yml: Path) -> None: ... nav_file: Path,
docs_root: Path,
) -> tuple[list[dict[str, Any]], dict[str, str] | None]: ...
def build_wiki_nav_block(wiki_dir: Path) -> list[dict[str, Any]]: ...
def load_spec_icon(nav_file: Path) -> dict[str, str] | None: ...
def _strip_scope(value: object, scope: str) -> object: ...
def generate_site_config(
kind: str,
kind_root: Path,
nav_block: list[dict[str, Any]],
out: Path,
site_name: str,
docs_dir: str,
site_dir: str,
template: Path | None = None,
site_description: str | None = None,
site_author: str | None = None,
theme_icon: dict[str, str] | None = None,
) -> None: ...
def build_configs(yml_paths: Iterable[Path]) -> None: ...
def serve(mkdocs_yml: Path) -> None: ... def serve(mkdocs_yml: Path) -> None: ...

View File

@@ -1,17 +1,28 @@
""" """
# Loader Layer # Summary
The `docforge.loaders` package is responsible for discovering Python source files Loader layer for doc-forge.
and extracting their documentation using static analysis.
## Core Features The `docforge.loaders` package is responsible for discovering Python modules
and extracting documentation data using static analysis.
- **Discovery**: Automatically find all modules and packages in a project ---
directory.
- **Introspection**: Uses `griffe` to parse docstrings, signatures, and # Overview
hierarchical relationships without executing the code.
- **Filtering**: Automatically excludes private members (prefixed with `_`) to This layer converts Python source code into an intermediate documentation
ensure clean public documentation. model used by doc-forge. It performs module discovery, introspection, and
initial filtering before the data is passed to the core documentation models.
Core capabilities include:
- **Module discovery** – Locate Python modules and packages within a project.
- **Static introspection** – Parse docstrings, signatures, and object
hierarchies using the `griffe` library without executing the code.
- **Public API filtering** – Exclude private members (names prefixed with
`_`) to produce clean public documentation structures.
---
""" """
from .griffe_loader import GriffeLoader, discover_module_paths from .griffe_loader import GriffeLoader, discover_module_paths

View File

@@ -1,21 +1,35 @@
""" """
This module provides the GriffeLoader, which uses the 'griffe' library to # Summary
introspect Python source code and populate the doc-forge Project models.
Utilities for loading and introspecting Python modules using Griffe.
This module provides the `GriffeLoader` class and helper utilities used to
discover Python modules, introspect their structure, and convert the results
into doc-forge documentation models.
---
Notes:
- All analysis is static; analyzed modules are never executed.
- Private members (names starting with `_`) are skipped during conversion.
---
""" """
import logging import logging
from pathlib import Path from pathlib import Path
from typing import List, Optional
from griffe import ( from griffe import (
GriffeLoader as _GriffeLoader,
ModulesCollection,
LinesCollection,
Object,
AliasResolutionError, AliasResolutionError,
LinesCollection,
ModulesCollection,
Object,
)
from griffe import (
GriffeLoader as _GriffeLoader,
) )
from docforge.models import Module, Project, DocObject from docforge.models import DocObject, Module, Project
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -23,21 +37,34 @@ logger = logging.getLogger(__name__)
def discover_module_paths( def discover_module_paths(
module_name: str, module_name: str,
project_root: Path | None = None, project_root: Path | None = None,
) -> List[str]: ) -> list[str]:
""" """
Discover all Python modules under a package via filesystem traversal. Discover Python modules within a package directory.
Rules: The function scans the filesystem for `.py` files inside the specified
- Directory with __init__.py is treated as a package. package and converts them into dotted module import paths.
- Any .py file is treated as a module.
- All paths are converted to dotted module paths. Discovery rules:
- Directories containing `__init__.py` are treated as packages.
- Each `.py` file is treated as a module.
- Results are returned as dotted import paths.
Args: Args:
module_name: The name of the package to discover. module_name (str):
project_root: The root directory of the project. Defaults to current working directory. Top-level package name to discover modules from.
project_root (Path | None):
Root directory used to resolve module paths. If not provided, the
current working directory is used.
Returns: Returns:
A sorted list of dotted module paths. list[str]:
A sorted list of unique dotted module import paths.
Raises:
FileNotFoundError:
If the specified package directory does not exist.
""" """
if project_root is None: if project_root is None:
@@ -47,7 +74,7 @@ def discover_module_paths(
if not pkg_dir.exists(): if not pkg_dir.exists():
raise FileNotFoundError(f"Package not found: {pkg_dir}") raise FileNotFoundError(f"Package not found: {pkg_dir}")
module_paths: List[str] = [] module_paths: list[str] = []
for path in pkg_dir.rglob("*.py"): for path in pkg_dir.rglob("*.py"):
if path.name == "__init__.py": if path.name == "__init__.py":
@@ -64,12 +91,23 @@ def discover_module_paths(
class GriffeLoader: class GriffeLoader:
""" """
Loads Python modules and extracts documentation using the Griffe introspection engine. Load Python modules using Griffe and convert them into doc-forge models.
This loader uses the Griffe introspection engine to analyze Python source
code and transform the extracted information into `Project`, `Module`,
and `DocObject` instances used by doc-forge.
Attributes:
_loader (_GriffeLoader):
Internal Griffe loader with dedicated module and line collections.
""" """
def __init__(self) -> None: def __init__(self) -> None:
""" """
Initialize the GriffeLoader. Initialize the Griffe-backed loader.
Creates an internal Griffe loader instance with dedicated collections
for modules and source lines.
""" """
self._loader = _GriffeLoader( self._loader = _GriffeLoader(
modules_collection=ModulesCollection(), modules_collection=ModulesCollection(),
@@ -78,20 +116,38 @@ class GriffeLoader:
def load_project( def load_project(
self, self,
module_paths: List[str], module_paths: list[str],
project_name: Optional[str] = None, project_name: str | None = None,
skip_import_errors: bool = None, skip_import_errors: bool | None = None,
) -> Project: ) -> Project:
""" """
Load multiple modules and combine them into a single Project models. Load multiple modules and assemble them into a Project model.
Each module path is introspected and converted into a `Module`
instance. All modules are then aggregated into a single `Project`
object.
Args: Args:
module_paths: A list of dotted paths to the modules to load. module_paths (list[str]):
project_name: Optional name for the project. Defaults to the first module name. List of dotted module import paths to load.
skip_import_errors: If True, modules that fail to import will be skipped.
project_name (str | None):
Optional override for the project name. Defaults to the top-level
name of the first module.
skip_import_errors (bool | None):
If True, modules that fail to load will be skipped instead of raising an error.
Returns: Returns:
A Project instance containing the loaded modules. Project:
A populated `Project` instance containing the loaded modules.
Raises:
ValueError:
If no module paths are provided.
ImportError:
If a module fails to load and `skip_import_errors` is False.
""" """
if not module_paths: if not module_paths:
raise ValueError("At least one module path must be provided") raise ValueError("At least one module path must be provided")
@@ -116,13 +172,33 @@ class GriffeLoader:
def load_module(self, path: str) -> Module: def load_module(self, path: str) -> Module:
""" """
Load a single module and convert its introspection data into the docforge models. Load and convert a single Python module.
The module is introspected using Griffe and then transformed into
a doc-forge `Module` model.
Args: Args:
path: The dotted path of the module to load. path (str):
Dotted import path of the module.
Returns: Returns:
A Module instance. Module:
A populated `Module` instance.
Raises:
ImportError:
If the module cannot be loaded by Griffe.
KeyError:
If the loaded module is missing from the module collection.
Example:
Load a single module:
```python
loader = GriffeLoader()
module = loader.load_module("mypackage.submodule")
```
""" """
self._loader.load(path) self._loader.load(path)
griffe_module = self._loader.modules_collection[path] griffe_module = self._loader.modules_collection[path]
@@ -135,13 +211,18 @@ class GriffeLoader:
def _convert_module(self, obj: Object) -> Module: def _convert_module(self, obj: Object) -> Module:
""" """
Convert a Griffe Object (module) into a docforge Module. Convert a Griffe module object into a doc-forge Module.
All public members of the module are recursively converted into
`DocObject` instances.
Args: Args:
obj: The Griffe Object representing the module. obj (Object):
Griffe object representing the module.
Returns: Returns:
A populated Module instance. Module:
A populated `Module` model.
""" """
module = Module( module = Module(
path=obj.path, path=obj.path,
@@ -158,13 +239,19 @@ class GriffeLoader:
def _convert_object(self, obj: Object) -> DocObject: def _convert_object(self, obj: Object) -> DocObject:
""" """
Recursively convert a Griffe Object into a DocObject hierarchy. Convert a Griffe object into a doc-forge DocObject.
The conversion preserves the object's metadata such as name,
kind, path, signature, and docstring. Child members are processed
recursively.
Args: Args:
obj: The Griffe Object to convert. obj (Object):
Griffe object representing a documented Python object.
Returns: Returns:
A DocObject instance. DocObject:
A `DocObject` instance representing the converted object.
""" """
kind = obj.kind.value kind = obj.kind.value
signature = self._safe_signature(obj) signature = self._safe_signature(obj)
@@ -191,30 +278,34 @@ class GriffeLoader:
# Safe extractors # Safe extractors
# ------------------------- # -------------------------
def _safe_docstring(self, obj: Object) -> Optional[str]: def _safe_docstring(self, obj: Object) -> str | None:
""" """
Safely retrieve the docstring value from a Griffe object. Safely extract a docstring from a Griffe object.
Args: Args:
obj: The Griffe Object to inspect. obj (Object):
Griffe object to inspect.
Returns: Returns:
The raw docstring string, or None if missing or unresolvable. str | None:
The raw docstring text if available, otherwise `None`.
""" """
try: try:
return obj.docstring.value if obj.docstring else None return obj.docstring.value if obj.docstring else None
except AliasResolutionError: except AliasResolutionError:
return None return None
def _safe_signature(self, obj: Object) -> Optional[str]: def _safe_signature(self, obj: Object) -> str | None:
""" """
Safely retrieve the signature string from a Griffe object. Safely extract the signature of a Griffe object.
Args: Args:
obj: The Griffe Object to inspect. obj (Object):
Griffe object to inspect.
Returns: Returns:
The string representation of the signature, or None. str | None:
String representation of the object's signature if available, otherwise `None`.
""" """
try: try:
if hasattr(obj, "signature") and obj.signature: if hasattr(obj, "signature") and obj.signature:

View File

@@ -1,29 +1,25 @@
from typing import List, Optional
from pathlib import Path from pathlib import Path
from docforge.models import Module, Project from docforge.models import Module, Project
def discover_module_paths( def discover_module_paths(
module_name: str, module_name: str,
project_root: Path | None = None, project_root: Path | None = None,
) -> List[str]: ) -> list[str]: ...
...
class GriffeLoader: class GriffeLoader:
"""Griffe-based introspection loaders. """Griffe-based introspection loaders.
This is the only supported introspection backend in doc-forge. This is the only supported introspection backend in doc-forge.
Converts Griffe results into `Project`, `Module`, and `DocObject` models.
""" """
def __init__(self) -> None: ... def __init__(self) -> None: ...
def load_project( def load_project(
self, self,
module_paths: List[str], module_paths: list[str],
project_name: Optional[str] = ..., project_name: str | None = ...,
skip_import_errors: bool = ..., skip_import_errors: bool | None = ...,
) -> Project: ) -> Project:
"""Load a documentation project from Python modules.""" """Load a documentation project from Python modules."""

View File

@@ -1,17 +1,33 @@
""" """
# Model Layer # Summary
The `docforge.models` package provides the core data structures used to represent Model layer for doc-forge.
Python source code in a documentation-focused hierarchy.
## Key Components The `docforge.models` package defines the core data structures used to
represent Python source code as a structured documentation model.
- **Project**: The root container for all documented modules. ---
- **Module**: Represents a Python module or package, containing members.
- **DocObject**: A recursive structure for classes, functions, and attributes.
These classes are designed to be renderer-agnostic, allowing the same internal # Overview
representation to be transformed into various output formats (currently MkDocs).
The model layer forms the central intermediate representation used throughout
doc-forge. Python modules and objects discovered during introspection are
converted into a hierarchy of documentation models that can later be rendered
into different documentation formats.
Key components:
- **Project** – Root container representing an entire documented codebase.
- **Module** – Representation of a Python module or package containing
documented members.
- **DocObject** – Recursive structure representing Python objects such as
classes, functions, methods, and attributes.
These models are intentionally **renderer-agnostic**, allowing the same
documentation structure to be transformed into multiple output formats
(e.g., MkDocs, MCP, or other renderers).
---
""" """
from .project import Project from .project import Project

View File

@@ -1,6 +1,6 @@
from .project import Project
from .module import Module from .module import Module
from .object import DocObject from .object import DocObject
from .project import Project
__all__ = [ __all__ = [
"Project", "Project",

View File

@@ -1,66 +1,98 @@
""" """
This module defines the Module class, which represents a Python module or package # Summary
in the doc-forge documentation models. It acts as a container for top-level
documented objects. Documentation model representing a Python module or package.
This module defines the `Module` class used in the doc-forge documentation
model. A `Module` acts as a container for top-level documented objects
(classes, functions, variables, and other members) discovered during
introspection.
---
Notes:
- Only public members are stored; private names are filtered by the loader.
---
""" """
from typing import Dict, Iterable, Optional from collections.abc import Iterable
from docforge.models.object import DocObject from docforge.models.object import DocObject
class Module: class Module:
""" """
Represents a documented Python module or package. Representation of a documented Python module or package.
A `Module` stores metadata about the module itself and maintains a
collection of top-level documentation objects discovered during
introspection.
Attributes: Attributes:
path: Dotted import path of the module. path (str):
docstring: Module-level docstring content. Dotted import path of the module.
members: Dictionary mapping object names to their DocObject representations.
docstring (str | None):
Module-level documentation string, if present.
members (dict[str, DocObject]):
Mapping of object names to their corresponding `DocObject` representations.
""" """
def __init__( def __init__(
self, self,
path: str, path: str,
docstring: Optional[str] = None, docstring: str | None = None,
) -> None: ) -> None:
""" """
Initialize a new Module. Initialize a Module instance.
Args: Args:
path: The dotted path of the module. path (str):
docstring: The module's docstring, if any. Dotted import path identifying the module.
docstring (str | None):
Module-level documentation text, if available.
""" """
self.path = path self.path = path
self.docstring = docstring self.docstring = docstring
self.members: Dict[str, DocObject] = {} self.members: dict[str, DocObject] = {}
def add_object(self, obj: DocObject) -> None: def add_object(self, obj: DocObject) -> None:
""" """
Add a documented object to the module. Add a documented object to the module.
Args: Args:
obj: The object to add. obj (DocObject):
Documentation object to register as a top-level member of the module.
""" """
self.members[obj.name] = obj self.members[obj.name] = obj
def get_object(self, name: str) -> DocObject: def get_object(self, name: str) -> DocObject:
""" """
Retrieve a member object by name. Retrieve a documented object by name.
Args: Args:
name: The name of the object. name (str):
Name of the object to retrieve.
Returns: Returns:
The requested DocObject. DocObject:
The corresponding `DocObject` instance.
Raises:
KeyError:
If no object with the given name exists.
""" """
return self.members[name] return self.members[name]
def get_all_objects(self) -> Iterable[DocObject]: def get_all_objects(self) -> Iterable[DocObject]:
""" """
Get all top-level objects in the module. Return all top-level documentation objects in the module.
Returns: Returns:
An iterable of DocObject instances. Iterable[DocObject]:
An iterable of `DocObject` instances representing the module's public members.
""" """
return self.members.values() return self.members.values()

View File

@@ -1,23 +1,19 @@
from typing import Dict, Iterable, Optional from collections.abc import Iterable
from docforge.models.object import DocObject from docforge.models.object import DocObject
class Module: class Module:
"""Represents a documented Python module.""" """Represents a documented Python module."""
path: str path: str
docstring: Optional[str] docstring: str | None
members: Dict[str, DocObject] members: dict[str, DocObject]
def __init__( def __init__(
self, self,
path: str, path: str,
docstring: Optional[str] = ..., docstring: str | None = ...,
) -> None: ... ) -> None: ...
def add_object(self, obj: DocObject) -> None: ... def add_object(self, obj: DocObject) -> None: ...
def get_object(self, name: str) -> DocObject: ... def get_object(self, name: str) -> DocObject: ...
def get_all_objects(self) -> Iterable[DocObject]: ... def get_all_objects(self) -> Iterable[DocObject]: ...

View File

@@ -1,23 +1,51 @@
""" """
This module defines the DocObject class, the fundamental recursive unit of the # Summary
doc-forge documentation models. A DocObject represents a single Python entity
(class, function, method, or attribute) and its nested members. Documentation model representing individual Python objects.
This module defines the `DocObject` class, the fundamental recursive unit of
the doc-forge documentation model. Each `DocObject` represents a Python
entity such as a class, function, method, or attribute, and may contain nested
members that form a hierarchical documentation structure.
---
Notes:
- `DocObject` instances form a tree mirroring the Python import hierarchy.
- Objects are renderer-agnostic and may be consumed by any renderer.
---
""" """
from typing import Dict, Iterable, Optional from collections.abc import Iterable
class DocObject: class DocObject:
""" """
Represents a documented Python object (class, function, method, etc.). Representation of a documented Python object.
A `DocObject` models a single Python entity discovered during
introspection. Objects may contain nested members, allowing the structure
of modules, classes, and other containers to be represented recursively.
Attributes: Attributes:
name: Local name of the object. name (str):
kind: Type of object (e.g., 'class', 'function', 'attribute'). Local name of the object.
path: Full dotted import path to the object.
signature: Callable signature, if applicable. kind (str):
docstring: Raw docstring content extracted from the source. Type of object (for example `class`, `function`, `method`, or `attribute`).
members: Dictionary mapping member names to their DocObject representations.
path (str):
Fully qualified dotted path to the object.
signature (str | None):
Callable signature if the object represents a callable.
docstring (str | None):
Raw docstring text extracted from the source code.
members (dict[str, DocObject]):
Mapping of member names to child `DocObject` instances.
""" """
def __init__( def __init__(
@@ -25,52 +53,72 @@ class DocObject:
name: str, name: str,
kind: str, kind: str,
path: str, path: str,
signature: Optional[str] = None, signature: str | None = None,
docstring: Optional[str] = None, docstring: str | None = None,
) -> None: ) -> None:
""" """
Initialize a new DocObject. Initialize a DocObject instance.
Args: Args:
name: The local name of the object. name (str):
kind: The kind of object (e.g., 'class', 'function'). Local name of the object.
path: The full dotted path to the object.
signature: The object's signature (for callable objects). kind (str):
docstring: The object's docstring, if any. Object type identifier (for example `class` or `function`).
path (str):
Fully qualified dotted path of the object.
signature (str | None):
Callable signature if applicable.
docstring (str | None):
Documentation string associated with the object.
""" """
self.name = name self.name = name
self.kind = kind self.kind = kind
self.path = path self.path = path
self.signature = signature self.signature = signature
self.docstring = docstring self.docstring = docstring
self.members: Dict[str, 'DocObject'] = {} self.members: dict[str, DocObject] = {}
def add_member(self, obj: 'DocObject') -> None: def add_member(self, obj: "DocObject") -> None:
""" """
Add a child member to this object (e.g., a method to a class). Add a child documentation object.
This is typically used when attaching methods to classes or
nested objects to their parent containers.
Args: Args:
obj: The child DocObject to add. obj (DocObject):
Documentation object to add as a member.
""" """
self.members[obj.name] = obj self.members[obj.name] = obj
def get_member(self, name: str) -> 'DocObject': def get_member(self, name: str) -> "DocObject":
""" """
Retrieve a child member by name. Retrieve a member object by name.
Args: Args:
name: The name of the member. name (str):
Name of the member to retrieve.
Returns: Returns:
The requested DocObject. DocObject:
The corresponding `DocObject` instance.
Raises:
KeyError:
If the member does not exist.
""" """
return self.members[name] return self.members[name]
def get_all_members(self) -> Iterable['DocObject']: def get_all_members(self) -> Iterable["DocObject"]:
""" """
Get all members of this object. Return all child members of the object.
Returns: Returns:
An iterable of child DocObject instances. Iterable[DocObject]:
An iterable of `DocObject` instances representing nested members.
""" """
return self.members.values() return self.members.values()

View File

@@ -1,5 +1,4 @@
from typing import Dict, Iterable, Optional from collections.abc import Iterable
class DocObject: class DocObject:
"""Represents a documented Python object.""" """Represents a documented Python object."""
@@ -7,21 +6,18 @@ class DocObject:
name: str name: str
kind: str kind: str
path: str path: str
signature: Optional[str] signature: str | None
docstring: Optional[str] docstring: str | None
members: Dict[str, "DocObject"] members: dict[str, DocObject]
def __init__( def __init__(
self, self,
name: str, name: str,
kind: str, kind: str,
path: str, path: str,
signature: Optional[str] = ..., signature: str | None = ...,
docstring: Optional[str] = ..., docstring: str | None = ...,
) -> None: ... ) -> None: ...
def add_member(self, obj: DocObject) -> None: ...
def add_member(self, obj: "DocObject") -> None: ... def get_member(self, name: str) -> DocObject: ...
def get_all_members(self) -> Iterable[DocObject]: ...
def get_member(self, name: str) -> "DocObject": ...
def get_all_members(self) -> Iterable["DocObject"]: ...

View File

@@ -1,38 +1,67 @@
""" """
This module defines the Project class, the top-level container for a documented # Summary
project. It aggregates multiple Module instances into a single named entity.
Documentation model representing a project.
This module defines the `Project` class, the top-level container used by
doc-forge to represent a documented codebase. A `Project` aggregates multiple
modules and provides access to them through a unified interface.
---
Notes:
- Modules are keyed by their dotted import path.
- Objects are renderer-agnostic; the same model feeds every renderer.
---
""" """
from typing import Dict, Iterable from collections.abc import Iterable
from docforge.models.module import Module from docforge.models.module import Module
class Project: class Project:
""" """
Represents a documentation project, serving as a container for modules. Representation of a documentation project.
A `Project` serves as the root container for all modules discovered during
introspection. Each module is stored by its dotted import path.
Attributes: Attributes:
name: Name of the project. name (str):
modules: Dictionary mapping module paths to Module instances. Name of the project.
modules (dict[str, Module]):
Mapping of module paths to `Module` instances.
""" """
def __init__(self, name: str) -> None: def __init__(self, name: str) -> None:
""" """
Initialize a new Project. Initialize a Project instance.
Args: Args:
name: The name of the project. name (str):
Name used to identify the documentation project.
Example:
Create a project and register a module:
```python
project = Project("mypackage")
project.add_module(module)
```
""" """
self.name = name self.name = name
self.modules: Dict[str, Module] = {} self.modules: dict[str, Module] = {}
def add_module(self, module: Module) -> None: def add_module(self, module: Module) -> None:
""" """
Add a module to the project. Register a module in the project.
Args: Args:
module: The module to add. module (Module):
Module instance to add to the project.
""" """
self.modules[module.path] = module self.modules[module.path] = module
@@ -41,27 +70,35 @@ class Project:
Retrieve a module by its dotted path. Retrieve a module by its dotted path.
Args: Args:
path: The dotted path of the module (e.g., 'pkg.mod'). path (str):
Fully qualified dotted module path (for example `pkg.module`).
Returns: Returns:
The requested Module. Module:
The corresponding `Module` instance.
Raises:
KeyError:
If the module does not exist in the project.
""" """
return self.modules[path] return self.modules[path]
def get_all_modules(self) -> Iterable[Module]: def get_all_modules(self) -> Iterable[Module]:
""" """
Get all modules in the project. Return all modules contained in the project.
Returns: Returns:
An iterable of Module objects. Iterable[Module]:
An iterable of `Module` instances.
""" """
return self.modules.values() return self.modules.values()
def get_module_list(self) -> list[str]: def get_module_list(self) -> list[str]:
""" """
Get the list of all module dotted paths. Return the list of module import paths.
Returns: Returns:
A list of module paths. list[str]:
A list containing the dotted paths of all modules in the project.
""" """
return list(self.modules.keys()) return list(self.modules.keys())

View File

@@ -1,20 +1,15 @@
from typing import Dict, Iterable from collections.abc import Iterable
from docforge.models.module import Module from docforge.models.module import Module
class Project: class Project:
"""Represents a documentation project.""" """Represents a documentation project."""
name: str name: str
modules: Dict[str, Module] modules: dict[str, Module]
def __init__(self, name: str) -> None: ... def __init__(self, name: str) -> None: ...
def add_module(self, module: Module) -> None: ... def add_module(self, module: Module) -> None: ...
def get_module(self, path: str) -> Module: ... def get_module(self, path: str) -> Module: ...
def get_all_modules(self) -> Iterable[Module]: ... def get_all_modules(self) -> Iterable[Module]: ...
def get_module_list(self) -> list[str]: ... def get_module_list(self) -> list[str]: ...

View File

@@ -1,27 +1,38 @@
""" """
# Navigation Layer Navigation layer for doc-forge.
The `docforge.nav` package manages the mapping between the logical documentation The ``docforge.nav`` package manages the relationship between the logical
structure and the physical files on disk. documentation structure defined by the user and the physical documentation
files generated on disk.
## Workflow ---
1. **Spec Definition**: Users define navigation intent in `docforge.nav.yml`. Workflow
2. **Resolution**: `resolve_nav` matches patterns in the spec to generated `.md` files. --------
3. **Emission**: `MkDocsNavEmitter` produces the final YAML structure for `mkdocs.yml`.
This abstraction allows doc-forge to support complex grouping and ordering 1. **Specification** – Users define navigation intent in ``docforge.nav.yml``.
independently of the source code's physical layout. 2. **Resolution** – ``resolve_nav`` expands patterns and matches them against
generated Markdown files.
3. **Emission** – ``MkDocsNavEmitter`` converts the resolved structure into
the YAML navigation format required by ``mkdocs.yml``.
This layer separates documentation organization from the underlying source
code layout, enabling flexible grouping, ordering, and navigation structures
independent of module hierarchy.
---
""" """
from .spec import NavSpec, load_nav_spec from .spec import NavSpec, load_nav_spec
from .resolver import ResolvedNav, resolve_nav from .resolver import ResolvedNav, resolve_nav
from .mkdocs import MkDocsNavEmitter from .mkdocs import MkDocsNavEmitter
from .wiki import build_wiki_nav
__all__ = [ __all__ = [
"NavSpec", "NavSpec",
"ResolvedNav", "ResolvedNav",
"MkDocsNavEmitter", "MkDocsNavEmitter",
"build_wiki_nav",
"resolve_nav", "resolve_nav",
"load_nav_spec", "load_nav_spec",
] ]

View File

@@ -1,11 +1,13 @@
from .spec import NavSpec, load_nav_spec
from .resolver import ResolvedNav, resolve_nav
from .mkdocs import MkDocsNavEmitter from .mkdocs import MkDocsNavEmitter
from .resolver import ResolvedNav, resolve_nav
from .spec import NavSpec, load_nav_spec
from .wiki import build_wiki_nav
__all__ = [ __all__ = [
"NavSpec", "NavSpec",
"ResolvedNav", "ResolvedNav",
"MkDocsNavEmitter", "MkDocsNavEmitter",
"build_wiki_nav",
"resolve_nav", "resolve_nav",
"load_nav_spec", "load_nav_spec",
] ]

View File

@@ -1,32 +1,49 @@
""" """
This module provides the MkDocsNavEmitter, which converts a ResolvedNav instance MkDocs navigation emitter.
into the specific YAML-ready list structure expected by the MkDocs 'nav'
configuration. This module provides the ``MkDocsNavEmitter`` class, which converts a
``ResolvedNav`` instance into the navigation structure required by the
MkDocs ``nav`` configuration.
---
Notes:
- The emitted structure is a list of dictionaries, one per top-level nav
entry, matching the MkDocs ``nav`` YAML format.
---
""" """
from pathlib import Path from pathlib import Path
from typing import List, Dict, Any from typing import Any
from docforge.nav.resolver import ResolvedNav from docforge.nav.resolver import ResolvedNav
class MkDocsNavEmitter: class MkDocsNavEmitter:
""" """
Emitter responsible for transforming a ResolvedNav into an MkDocs-compatible Emit MkDocs navigation structures from resolved navigation data.
navigation structure.
The emitter transforms a ``ResolvedNav`` object into the YAML-compatible
list structure expected by the MkDocs ``nav`` configuration field.
""" """
def emit(self, nav: ResolvedNav) -> List[Dict[str, Any]]: def emit(self, nav: ResolvedNav) -> list[dict[str, Any]]:
""" """
Generate a list of navigation entries for mkdocs.yml. Generate a navigation structure for ``mkdocs.yml``.
Args: Args:
nav: The resolved navigation data. nav (ResolvedNav):
Resolved navigation data describing documentation groups
and their associated Markdown files.
Returns: Returns:
A list of dictionary mappings representing the MkDocs navigation. list[dict[str, Any]]:
A list of dictionaries representing the MkDocs navigation layout.
Each dictionary maps a navigation label to a page or a list of
pages.
""" """
result: List[Dict[str, Any]] = [] result: list[dict[str, Any]] = []
# Home entry (semantic path) # Home entry (semantic path)
if nav.home: if nav.home:
@@ -34,7 +51,7 @@ class MkDocsNavEmitter:
# Group entries # Group entries
for group, paths in nav.groups.items(): for group, paths in nav.groups.items():
entries: List[str] = [] entries: list[str] = []
for p in paths: for p in paths:
# Convert filesystem path back to docs-relative path # Convert filesystem path back to docs-relative path
rel_path = self._to_relative(p, nav._docs_root) rel_path = self._to_relative(p, nav._docs_root)
@@ -45,16 +62,21 @@ class MkDocsNavEmitter:
def _to_relative(self, path: Path, docs_root: Path | None) -> str: def _to_relative(self, path: Path, docs_root: Path | None) -> str:
""" """
Convert a filesystem path to a path relative to the documentation root. Convert a filesystem path into a documentation-relative path.
This handles both absolute and relative filesystem paths, ensuring the
output is compatible with MkDocs navigation requirements. This method normalizes paths so they can be used in MkDocs navigation.
It handles both absolute and relative filesystem paths and ensures the
resulting path is relative to the documentation root.
Args: Args:
path: The path to convert. path (Path):
docs_root: The root directory for documentation. Filesystem path to convert.
docs_root (Path | None):
Root directory of the documentation sources.
Returns: Returns:
A string representing the relative POSIX-style path. str:
POSIX-style path relative to the documentation root.
""" """
if docs_root and path.is_absolute(): if docs_root and path.is_absolute():
try: try:
@@ -66,7 +88,7 @@ class MkDocsNavEmitter:
path_str = path.as_posix() path_str = path.as_posix()
docs_root_str = docs_root.as_posix() docs_root_str = docs_root.as_posix()
if path_str.startswith(docs_root_str + "/"): if path_str.startswith(docs_root_str + "/"):
return path_str[len(docs_root_str) + 1:] return path_str[len(docs_root_str) + 1 :]
# Fallback for other cases # Fallback for other cases
return path.as_posix().split("/docs/", 1)[-1] return path.as_posix().split("/docs/", 1)[-1]

View File

@@ -1,15 +1,14 @@
from typing import Dict, List, Any
from pathlib import Path from pathlib import Path
from typing import Any
from docforge.nav.resolver import ResolvedNav from docforge.nav.resolver import ResolvedNav
class MkDocsNavEmitter: class MkDocsNavEmitter:
""" """
Converts a ResolvedNav into MkDocs-compatible `nav` data. Converts a ResolvedNav into MkDocs-compatible `nav` data.
""" """
def emit(self, nav: ResolvedNav) -> List[Dict[str, Any]]: def emit(self, nav: ResolvedNav) -> list[dict[str, Any]]:
""" """
Emit a structure suitable for insertion into mkdocs.yml. Emit a structure suitable for insertion into mkdocs.yml.

View File

@@ -1,40 +1,54 @@
""" """
This module contains the logic for resolving a NavSpec against the physical Navigation resolution utilities.
filesystem. It expands globs and validates that all referenced documents
actually exist on disk.
"""
from pathlib import Path This module resolves a ``NavSpec`` against the filesystem by expanding glob
from typing import Dict, Iterable, List patterns and validating that referenced documentation files exist.
---
Notes:
- Glob resolution is recursive and returns paths in sorted order.
- Unmatched patterns raise ``FileNotFoundError`` to fail fast on typos.
---
"""
import glob import glob
from collections.abc import Iterable
from pathlib import Path
from docforge.nav.spec import NavSpec from docforge.nav.spec import NavSpec
class ResolvedNav: class ResolvedNav:
""" """
Represents a navigation structure where all patterns and paths have been Resolved navigation structure.
resolved against the actual filesystem contents.
A ``ResolvedNav`` represents navigation data after glob patterns have been
expanded and paths validated against the filesystem.
Attributes: Attributes:
home: Resolved relative path to the home page. home: Relative path to the documentation home page.
groups: Mapping of group titles to lists of absolute or relative Path objects. groups: Mapping of navigation group titles to lists of resolved
documentation file paths.
""" """
def __init__( def __init__(
self, self,
home: str | None, home: str | None,
groups: Dict[str, List[Path]], groups: dict[str, list[Path]],
docs_root: Path | None = None, docs_root: Path | None = None,
) -> None: ) -> None:
""" """
Initialize a ResolvedNav instance. Initialize a ResolvedNav instance.
Args: Args:
home: The relative path to the project home page. home (str | None):
groups: A mapping of group names to their resolved filesystem paths. Relative path to the home page within the documentation root.
docs_root: The root documentation directory. groups (dict[str, list[Path]]):
Mapping of group titles to resolved documentation file paths.
docs_root (Path | None):
Root directory of the documentation source files.
""" """
self.home = home self.home = home
self.groups = groups self.groups = groups
@@ -42,18 +56,24 @@ class ResolvedNav:
def all_files(self) -> Iterable[Path]: def all_files(self) -> Iterable[Path]:
""" """
Get an iterable of all resolved files in the navigation structure. Iterate over all files referenced by the navigation structure.
Returns: Yields:
An iterable of Path objects. Path:
A documentation file referenced by the navigation, including
the home page when defined.
Raises:
RuntimeError: If the home page is defined but the documentation
root is not available for resolution.
""" """
if self.home: if self.home:
if self._docs_root is None: if self._docs_root is None:
raise RuntimeError("docs_root is required to resolve home path") raise RuntimeError("docs_root is required to resolve home path")
yield self._docs_root / self.home yield self._docs_root / self.home
for paths in self.groups.values(): for paths in self.groups.values():
for p in paths: yield from paths
yield p
def resolve_nav( def resolve_nav(
@@ -61,46 +81,52 @@ def resolve_nav(
docs_root: Path, docs_root: Path,
) -> ResolvedNav: ) -> ResolvedNav:
""" """
Create a ResolvedNav by processing a NavSpec against the filesystem. Resolve a navigation specification against the filesystem.
This expands globs and validates the existence of referenced files.
The function expands glob patterns defined in a ``NavSpec`` and verifies
that referenced documentation files exist within the documentation root.
Args: Args:
spec: The navigation specification to resolve. spec (NavSpec):
docs_root: The root directory for documentation files. Navigation specification describing documentation layout.
docs_root (Path):
Root directory containing documentation Markdown files.
Returns: Returns:
A ResolvedNav instance. ResolvedNav:
A `ResolvedNav` instance containing validated navigation paths.
Raises: Raises:
FileNotFoundError: If a pattern doesn't match any files or the docs_root doesn't exist. FileNotFoundError: If the documentation root does not exist or a
navigation pattern does not match any files.
""" """
if not docs_root.exists(): if not docs_root.exists():
raise FileNotFoundError(docs_root) raise FileNotFoundError(docs_root)
def resolve_pattern(pattern: str) -> List[Path]: def resolve_pattern(pattern: str) -> list[Path]:
""" """
Resolve a single glob pattern relative to the docs_root. Resolve a glob pattern relative to the documentation root.
Args: Args:
pattern: The glob pattern to resolve. pattern (str):
Glob pattern used to match documentation files.
Returns: Returns:
A sorted list of matching Path objects. list[Path]:
A sorted list of matching `Path` objects.
Raises: Raises:
FileNotFoundError: If the pattern doesn't match any files. FileNotFoundError: If the pattern does not match any files.
""" """
full = docs_root / pattern full = docs_root / pattern
matches = sorted( matches = sorted(Path(p) for p in glob.glob(str(full), recursive=True))
Path(p) for p in glob.glob(str(full), recursive=True)
)
if not matches: if not matches:
raise FileNotFoundError(pattern) raise FileNotFoundError(pattern)
return matches return matches
# Resolve home # Resolve home page
home: str | None = None home: str | None = None
if spec.home: if spec.home:
home_path = docs_root / spec.home home_path = docs_root / spec.home
@@ -108,11 +134,11 @@ def resolve_nav(
raise FileNotFoundError(spec.home) raise FileNotFoundError(spec.home)
home = spec.home home = spec.home
# Resolve groups # Resolve navigation groups
resolved_groups: Dict[str, List[Path]] = {} resolved_groups: dict[str, list[Path]] = {}
for group, patterns in spec.groups.items(): for group, patterns in spec.groups.items():
files: List[Path] = [] files: list[Path] = []
for pattern in patterns: for pattern in patterns:
files.extend(resolve_pattern(pattern)) files.extend(resolve_pattern(pattern))
resolved_groups[group] = files resolved_groups[group] = files

View File

@@ -1,9 +1,8 @@
from collections.abc import Iterable
from pathlib import Path from pathlib import Path
from typing import Dict, List, Iterable, Optional
from docforge.nav.spec import NavSpec from docforge.nav.spec import NavSpec
class ResolvedNav: class ResolvedNav:
""" """
Fully-resolved navigation tree. Fully-resolved navigation tree.
@@ -13,17 +12,16 @@ class ResolvedNav:
- Order is preserved - Order is preserved
""" """
home: Optional[str] home: str | None
groups: Dict[str, List[Path]] groups: dict[str, list[Path]]
_docs_root: Optional[Path] _docs_root: Path | None
def __init__( def __init__(
self, self,
home: str | None, home: str | None,
groups: Dict[str, List[Path]], groups: dict[str, list[Path]],
docs_root: Path | None = ..., docs_root: Path | None = ...,
) -> None: ... ) -> None: ...
def all_files(self) -> Iterable[Path]: def all_files(self) -> Iterable[Path]:
""" """
Return all resolved documentation files in nav order. Return all resolved documentation files in nav order.
@@ -33,7 +31,6 @@ class ResolvedNav:
""" """
... ...
def resolve_nav( def resolve_nav(
spec: NavSpec, spec: NavSpec,
docs_root: Path, docs_root: Path,

View File

@@ -1,53 +1,84 @@
""" """
This module defines the NavSpec class, which represents the user's intent for Navigation specification model.
documentation navigation as defined in a YAML specification (usually
docforge.nav.yml). This module defines the ``NavSpec`` class, which represents the navigation
structure defined by the user in the doc-forge navigation specification
(typically ``docforge.nav.yml``).
---
Notes:
- The spec file supports an optional ``icon`` mapping for MkDocs theme
customization.
- All file references in ``groups`` are relative to the documentation root.
---
""" """
from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional
import yaml import yaml
class NavSpec: class NavSpec:
""" """
Parsed representation of the docforge navigation specification file. Parsed representation of a navigation specification.
A ``NavSpec`` describes the intended documentation navigation layout before
it is resolved against the filesystem.
Attributes: Attributes:
home: Path to the home document (e.g., 'index.md'). home: Relative path to the documentation home page (for example
groups: Mapping of group titles to lists of path patterns/globs. ``index.md``).
groups: Mapping of navigation group titles to lists of file patterns
or glob expressions.
icon: Optional mapping of theme icon entries (for example
``{"logo": "material/code-tags"}``) injected into the MkDocs
theme as ``theme.icon``.
""" """
def __init__( def __init__(
self, self,
home: Optional[str], home: str | None,
groups: Dict[str, List[str]], groups: dict[str, list[str]],
icon: dict[str, str] | None = None,
) -> None: ) -> None:
""" """
Initialize a NavSpec. Initialize a NavSpec instance.
Args: Args:
home: The path to the home document. home (str | None):
groups: A mapping of group names to lists of path patterns (globs). Relative path to the home document.
groups (dict[str, list[str]]):
Mapping of group names to lists of path patterns
(glob expressions).
icon (dict[str, str] | None):
Optional mapping of theme icon entries applied to the
generated MkDocs configuration.
""" """
self.home = home self.home = home
self.groups = groups self.groups = groups
self.icon = icon
@classmethod @classmethod
def load(cls, path: Path) -> "NavSpec": def load(cls, path: Path) -> NavSpec:
""" """
Load a NavSpec from a YAML file. Load a navigation specification from a YAML file.
Args: Args:
path: The filesystem path to the YAML file. path (Path):
Filesystem path to the navigation specification file.
Returns: Returns:
A NavSpec instance. NavSpec:
A ``NavSpec`` instance representing the parsed configuration.
Raises: Raises:
FileNotFoundError: If the path does not exist. FileNotFoundError: If the specified file does not exist.
ValueError: If the file content is not a valid NavSpec mapping. ValueError: If the file contents are not a valid navigation
specification.
""" """
if not path.exists(): if not path.exists():
raise FileNotFoundError(path) raise FileNotFoundError(path)
@@ -59,6 +90,7 @@ class NavSpec:
home = data.get("home") home = data.get("home")
groups = data.get("groups", {}) groups = data.get("groups", {})
icon = data.get("icon")
if home is not None and not isinstance(home, str): if home is not None and not isinstance(home, str):
raise ValueError("home must be a string") raise ValueError("home must be a string")
@@ -74,41 +106,65 @@ class NavSpec:
): ):
raise ValueError(f"group '{key}' must be a list of strings") raise ValueError(f"group '{key}' must be a list of strings")
return cls(home=home, groups=groups) if icon is not None and (
not isinstance(icon, dict)
or not all(
isinstance(k, str) and isinstance(v, str) for k, v in icon.items()
)
):
raise ValueError("icon must be a mapping of strings")
def all_patterns(self) -> List[str]: return cls(home=home, groups=groups, icon=icon)
def all_patterns(self) -> list[str]:
""" """
Get all path patterns referenced in the specification. Return all path patterns referenced by the specification.
Returns: Returns:
A list of all patterns (home plus all groups). list[str]:
A list containing the home document (if defined) and all
group pattern entries.
""" """
patterns: List[str] = [] patterns: list[str] = []
if self.home: if self.home:
patterns.append(self.home) patterns.append(self.home)
for items in self.groups.values(): for items in self.groups.values():
patterns.extend(items) patterns.extend(items)
return patterns return patterns
def load_nav_spec(path: Path) -> NavSpec: def load_nav_spec(path: Path) -> NavSpec:
""" """
Utility function to load a NavSpec from a file. Load a navigation specification file.
This helper function reads a YAML navigation file and constructs a
corresponding ``NavSpec`` instance.
Args: Args:
path: Path to the navigation specification file. path (Path):
Path to the navigation specification file.
Returns: Returns:
A loaded NavSpec instance. NavSpec:
A ``NavSpec`` instance representing the parsed specification.
Raises:
FileNotFoundError: If the specification file does not exist.
ValueError: If the YAML structure is invalid.
""" """
if not path.exists(): if not path.exists():
raise FileNotFoundError(path) raise FileNotFoundError(path)
data = yaml.safe_load(path.read_text(encoding="utf-8")) data = yaml.safe_load(path.read_text(encoding="utf-8"))
if not isinstance(data, dict): if not isinstance(data, dict):
raise ValueError("Nav spec must be a YAML mapping") raise ValueError("Nav spec must be a YAML mapping")
return NavSpec( return NavSpec(
home=data.get("home"), home=data.get("home"),
groups=data.get("groups", {}), groups=data.get("groups", {}),
icon=data.get("icon"),
) )

View File

@@ -1,6 +1,4 @@
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional
class NavSpec: class NavSpec:
""" """
@@ -10,18 +8,16 @@ class NavSpec:
of filesystem structure or MkDocs specifics. of filesystem structure or MkDocs specifics.
""" """
home: Optional[str] home: str | None
groups: Dict[str, List[str]] groups: dict[str, list[str]]
def __init__( def __init__(
self, self,
home: Optional[str], home: str | None,
groups: Dict[str, List[str]], groups: dict[str, list[str]],
) -> None: ) -> None: ...
...
@classmethod @classmethod
def load(cls, path: Path) -> "NavSpec": def load(cls, path: Path) -> NavSpec:
""" """
Load and validate a nav specification from YAML. Load and validate a nav specification from YAML.
@@ -31,12 +27,11 @@ class NavSpec:
""" """
... ...
def all_patterns(self) -> List[str]: def all_patterns(self) -> list[str]:
""" """
Return all path patterns referenced by the spec Return all path patterns referenced by the spec
(including home and group entries). (including home and group entries).
""" """
... ...
def load_nav_spec(path: Path) -> NavSpec: ... def load_nav_spec(path: Path) -> NavSpec: ...

165
docforge/nav/wiki.py Normal file
View File

@@ -0,0 +1,165 @@
"""
# Summary
Wiki navigation derivation.
This module provides ``build_wiki_nav``, which derives an MkDocs-ready
navigation block from the file structure of a hand-written wiki directory
(typically ``docs/wiki``). wiki content is authored by hand and is never
modified by doc-forge; only the navigation layout is inferred.
# Notes
- ``index.md`` at the wiki root becomes the ``Home`` entry.
- Page labels are derived from filenames: numeric order prefixes such as
``01_`` or ``02-`` are stripped, separators are replaced with spaces, and
names are title-cased (``01_overview.md`` becomes ``Overview``).
- Subdirectories become nested navigation groups. A nested ``index.md`` is
rendered as the section root placed first inside the group.
- Only ``.md`` files are considered; hidden entries are ignored.
"""
import re
from collections.abc import Callable
from pathlib import Path
from typing import Any
def build_wiki_nav(wiki_dir: Path) -> list[dict[str, Any]]:
"""
Derive an MkDocs navigation block from a wiki directory.
Returned paths are relative to the parent of ``wiki_dir`` and carry the
wiki directory name as their leading component (for example
``wiki/01_overview.md`` when the wiki lives at ``docs/wiki``). This makes
the result directly usable in an MkDocs ``nav`` block with
- ``index.md`` at the wiki root becomes the ``Home`` entry.
- Page labels are derived from filenames: numeric order prefixes such as
``01_`` or ``02-`` are stripped, separators are replaced with spaces, and
names are title-cased (``01_overview.md`` becomes ``Overview``).
- Subdirectories become nested navigation groups. A nested ``index.md`` is
rendered as the section root placed first inside the group.
- Only ``.md`` files are considered; hidden entries are ignored.
Args:
wiki_dir (Path):
Path to the hand-written wiki directory, for example ``docs/wiki``.
Returns:
list[dict[str, Any]]:
Navigation entries compatible with the MkDocs ``nav`` configuration.
The list is empty if the wiki contains no Markdown files.
Raises:
FileNotFoundError:
If the wiki directory does not exist.
"""
if not wiki_dir.exists():
raise FileNotFoundError(wiki_dir)
root = wiki_dir.parent
def rel(path: Path) -> str:
try:
return path.relative_to(root).as_posix()
except ValueError:
return path.as_posix()
nav: list[dict[str, Any]] = []
if (wiki_dir / "index.md").exists():
nav.append({"Home": rel(wiki_dir / "index.md")})
nav.extend(_render_entries(wiki_dir, rel))
return nav
def _render_entries(
base_dir: Path,
rel: Callable[[Path], str],
) -> list[dict[str, Any]]:
"""
Render navigation entries for the children of a wiki directory.
Markdown pages become labeled entries, subdirectories containing
Markdown become nested groups, and a nested ``index.md`` is emitted
first as the section root.
Args:
base_dir (Path):
Directory whose children are rendered.
rel (Callable[[Path], str]):
Callable converting a wiki file path into a docs-relative path.
Returns:
list[dict[str, Any]]:
Navigation entries for ``base_dir`` in natural sort order.
"""
children = sorted(
(child for child in base_dir.iterdir() if not child.name.startswith(".")),
key=lambda child: _natural_key(child.name),
)
entries: list[dict[str, Any]] = []
for child in children:
if child.is_dir():
if not any(child.rglob("*.md")):
continue
group: list[dict[str, Any]] = []
nested_index = child / "index.md"
if nested_index.exists():
group.append({_prettify(child.name): rel(nested_index)})
group.extend(_render_entries(child, rel))
entries.append({_prettify(child.name): group})
elif child.suffix == ".md":
if child.name == "index.md":
continue
entries.append({_prettify(child.stem): rel(child)})
return entries
def _prettify(name: str) -> str:
"""
Convert a filename or directory name into a navigation label.
Numeric order prefixes (``01_``, ``02-``, ``03.``) are stripped,
separators are replaced with spaces, and the result is title-cased.
Args:
name (str):
Filename stem or directory name to prettify.
Returns:
str:
Title-cased navigation label.
"""
name = re.sub(r"^\d+[_\-. ]?", "", name)
name = re.sub(r"[_-]+", " ", name).strip()
return name.title()
def _natural_key(name: str) -> list[object]:
"""
Build a natural sort key from a filename.
The key splits ``name`` into alternating non-digit and digit parts so
that numeric prefixes sort numerically (``02`` before ``10``).
Args:
name (str):
Filename or directory name to key.
Returns:
list[object]:
Mixed list of lowercased strings and integers used for sorting.
"""
return [
int(part) if part.isdigit() else part.lower()
for part in re.split(r"(\d+)", name)
]

41
docforge/nav/wiki.pyi Normal file
View File

@@ -0,0 +1,41 @@
from collections.abc import Callable
from pathlib import Path
from typing import Any
def build_wiki_nav(wiki_dir: Path) -> list[dict[str, Any]]:
"""
Derive an MkDocs navigation block from a wiki directory.
Args:
wiki_dir (Path):
Path to the hand-written wiki directory.
Returns:
list[dict[str, Any]]:
Wiki navigation entries compatible with the MkDocs `nav`
configuration.
Raises:
FileNotFoundError:
If the wiki directory does not exist.
"""
...
def _render_entries(
base_dir: Path,
rel: Callable[[Path], str],
) -> list[dict[str, Any]]:
"""
Render navigation entries for the children of a wiki directory.
Args:
base_dir (Path):
Directory whose children are rendered.
rel (Callable[[Path], str]):
Callable converting a wiki file path into a docs-relative path.
Returns:
list[dict[str, Any]]:
Navigation entries for `base_dir` in natural sort order.
"""
...

0
docforge/py.typed Normal file
View File

View File

@@ -1,20 +1,34 @@
""" """
# Renderers Layer # Summary
The `docforge.renderers` package handles the transformation of the internal Renderers layer for doc-forge.
documentation models into physical files formatted for specific documentation
engines.
## Current Implementations The `docforge.renderers` package transforms the internal documentation
models into files formatted for specific documentation systems.
- **MkDocsRenderer**: Generates Markdown files utilizing the `mkdocstrings` ---
syntax. It automatically handles package/module hierarchy and generates
`index.md` files for packages.
## Extending # Overview
To add a new renderer, implement the `DocRenderer` protocol defined in Renderers consume the doc-forge project model and generate output suitable
`docforge.renderers.base`. for documentation tools or machine interfaces.
Current implementations:
- **MkDocsRenderer** – Produces Markdown files compatible with MkDocs and
the `mkdocstrings` plugin. It automatically handles package hierarchy
and generates `index.md` files for packages.
- **MCPRenderer** – Emits structured JSON resources designed for consumption
by Model Context Protocol (MCP) clients.
---
# Extending
New renderers can be added by implementing the `DocRenderer` protocol
defined in `docforge.renderers.base`.
---
""" """
from .mkdocs_renderer import MkDocsRenderer from .mkdocs_renderer import MkDocsRenderer

View File

@@ -1,5 +1,5 @@
from .mkdocs_renderer import MkDocsRenderer
from .mcp_renderer import MCPRenderer from .mcp_renderer import MCPRenderer
from .mkdocs_renderer import MkDocsRenderer
__all__ = [ __all__ = [
"MkDocsRenderer", "MkDocsRenderer",

View File

@@ -1,7 +1,11 @@
""" """
This module defines the base interfaces and configuration containers for # Summary
doc-forge renderers. All renderer implementations should adhere to the
DocRenderer protocol. Renderer base interfaces and configuration models.
This module defines the base protocol and configuration container used by
doc-forge renderers. Concrete renderer implementations should implement the
`DocRenderer` protocol.
""" """
from pathlib import Path from pathlib import Path
@@ -14,12 +18,28 @@ class RendererConfig:
""" """
Configuration container for documentation renderers. Configuration container for documentation renderers.
Args: A `RendererConfig` instance groups together the project model and the
out_dir: The directory where documentation files should be written. output directory used during rendering.
project: The introspected project models to be rendered.
Attributes:
out_dir (Path):
Directory where generated documentation files will be written.
project (Project):
Documentation project model to be rendered.
""" """
def __init__(self, out_dir: Path, project: Project) -> None: def __init__(self, out_dir: Path, project: Project) -> None:
"""
Initialize a RendererConfig instance.
Args:
out_dir (Path):
Target directory where documentation files should be written.
project (Project):
Introspected project model to render.
"""
self.out_dir = out_dir self.out_dir = out_dir
self.project = project self.project = project
@@ -27,6 +47,9 @@ class RendererConfig:
class DocRenderer(Protocol): class DocRenderer(Protocol):
""" """
Protocol defining the interface for documentation renderers. Protocol defining the interface for documentation renderers.
Implementations of this protocol are responsible for transforming a
`Project` model into renderer-specific documentation sources.
""" """
name: str name: str
@@ -37,10 +60,13 @@ class DocRenderer(Protocol):
out_dir: Path, out_dir: Path,
) -> None: ) -> None:
""" """
Generate renderer-specific source files for the given project. Generate renderer-specific documentation sources.
Args: Args:
project: The project models containing modules and objects. project (Project):
out_dir: Target directory for the generated files. Project model containing modules and documentation objects.
out_dir (Path):
Directory where generated documentation sources should be written.
""" """
... ...

View File

@@ -3,7 +3,6 @@ from typing import Protocol
from docforge.models import Project from docforge.models import Project
class RendererConfig: class RendererConfig:
"""Renderer configuration container.""" """Renderer configuration container."""
@@ -12,7 +11,6 @@ class RendererConfig:
def __init__(self, out_dir: Path, project: Project) -> None: ... def __init__(self, out_dir: Path, project: Project) -> None: ...
class DocRenderer(Protocol): class DocRenderer(Protocol):
"""Renderer interface.""" """Renderer interface."""

View File

@@ -1,37 +1,57 @@
"""
# Summary
MCP renderer implementation.
This module defines the `MCPRenderer` class, which generates documentation
resources compatible with the Model Context Protocol (MCP).
"""
import json import json
from pathlib import Path from pathlib import Path
from typing import Dict, List
from docforge.models import Project, Module, DocObject from docforge.models import DocObject, Module, Project
class MCPRenderer: class MCPRenderer:
""" """
Renderer that emits MCP-native JSON resources from docforge models. Renderer that generates MCP-compatible documentation resources.
This renderer converts doc-forge project models into structured JSON
resources suitable for consumption by systems implementing the Model
Context Protocol (MCP).
""" """
name = "mcp" name = "mcp"
def generate_sources(self, project: Project, out_dir: Path) -> None: def generate_sources(self, project: Project, out_dir: Path) -> None:
""" """
Generate MCP-compatible JSON resources and navigation for the project. Generate MCP documentation resources for a project.
The renderer serializes each module into a JSON resource and produces
supporting metadata files such as `nav.json` and `index.json`.
Args: Args:
project: The project model to render. project (Project):
out_dir: Target directory for the generated JSON files. Documentation project model to render.
out_dir (Path):
Directory where MCP resources will be written.
""" """
modules_dir = out_dir / "modules" modules_dir = out_dir / "modules"
modules_dir.mkdir(parents=True, exist_ok=True) modules_dir.mkdir(parents=True, exist_ok=True)
nav: List[Dict[str, str]] = [] nav: list[dict[str, str]] = []
for module in project.get_all_modules(): for module in project.get_all_modules():
self._write_module(module, modules_dir) self._write_module(module, modules_dir)
nav.append({ nav.append(
"module": module.path, {
"resource": f"doc://modules/{module.path}", "module": module.path,
}) "resource": f"docs://modules/{module.path}",
}
)
# Write nav.json # Write nav.json
(out_dir / "nav.json").write_text( (out_dir / "nav.json").write_text(
@@ -54,11 +74,14 @@ class MCPRenderer:
def _write_module(self, module: Module, modules_dir: Path) -> None: def _write_module(self, module: Module, modules_dir: Path) -> None:
""" """
Serialize a module into an MCP JSON resource on disk. Serialize a module into an MCP resource file.
Args: Args:
module: The module instance to serialize. module (Module):
modules_dir: The directory where the module JSON file should be written. Module instance to serialize.
modules_dir (Path):
Directory where module JSON files are stored.
""" """
payload = { payload = {
"module": module.path, "module": module.path,
@@ -69,17 +92,19 @@ class MCPRenderer:
out.parent.mkdir(parents=True, exist_ok=True) out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(self._json(payload), encoding="utf-8") out.write_text(self._json(payload), encoding="utf-8")
def _render_module(self, module: Module) -> Dict: def _render_module(self, module: Module) -> dict:
""" """
Render a Module into MCP-friendly structured data. Convert a Module model into MCP-compatible structured data.
Args: Args:
module: The module instance to render. module (Module):
Module instance to convert.
Returns: Returns:
A dictionary following the MCP documentation resource schema. dict:
Dictionary representing the module and its documented objects.
""" """
data: Dict = { data: dict = {
"path": module.path, "path": module.path,
"docstring": module.docstring, "docstring": module.docstring,
"objects": {}, "objects": {},
@@ -90,17 +115,19 @@ class MCPRenderer:
return data return data
def _render_object(self, obj: DocObject) -> Dict: def _render_object(self, obj: DocObject) -> dict:
""" """
Recursively render a DocObject into structured MCP data. Recursively convert a DocObject into structured MCP data.
Args: Args:
obj: The documented object (class, func, etc.) to render. obj (DocObject):
Documentation object to convert.
Returns: Returns:
A dictionary representing the object and its members. dict:
Dictionary describing the object and any nested members.
""" """
data: Dict = { data: dict = {
"name": obj.name, "name": obj.name,
"kind": obj.kind, "kind": obj.kind,
"path": obj.path, "path": obj.path,
@@ -111,12 +138,22 @@ class MCPRenderer:
members = list(obj.get_all_members()) members = list(obj.get_all_members())
if members: if members:
data["members"] = { data["members"] = {
member.name: self._render_object(member) member.name: self._render_object(member) for member in members
for member in members
} }
return data return data
@staticmethod @staticmethod
def _json(data: Dict) -> str: def _json(data: dict) -> str:
"""
Serialize data to formatted JSON.
Args:
data (dict):
Dictionary to serialize.
Returns:
str:
JSON string formatted with indentation and UTF-8 compatibility.
"""
return json.dumps(data, indent=2, ensure_ascii=False) return json.dumps(data, indent=2, ensure_ascii=False)

View File

@@ -1,8 +1,7 @@
from pathlib import Path from pathlib import Path
from typing import Dict, List from typing import Any
from docforge.models import Project, Module, DocObject
from docforge.models import DocObject, Module, Project
class MCPRenderer: class MCPRenderer:
"""Renderer that emits MCP-native JSON resources from docforge models.""" """Renderer that emits MCP-native JSON resources from docforge models."""
@@ -15,12 +14,12 @@ class MCPRenderer:
def _write_module(self, module: Module, modules_dir: Path) -> None: def _write_module(self, module: Module, modules_dir: Path) -> None:
"""Serialize a module into an MCP JSON resource.""" """Serialize a module into an MCP JSON resource."""
def _render_module(self, module: Module) -> Dict: def _render_module(self, module: Module) -> dict[str, Any]:
"""Render a Module into MCP-friendly structured data.""" """Render a Module into MCP-friendly structured data."""
def _render_object(self, obj: DocObject) -> Dict: def _render_object(self, obj: DocObject) -> dict[str, Any]:
"""Recursively render a DocObject into structured MCP data.""" """Recursively render a DocObject into structured MCP data."""
@staticmethod @staticmethod
def _json(data: Dict) -> str: def _json(data: dict[str, Any]) -> str:
"""Serialize structured data to formatted JSON.""" """Serialize structured data to formatted JSON."""

View File

@@ -1,22 +1,31 @@
""" """
MkDocsRenderer # Summary
Generates Markdown source files compatible with MkDocs Material MkDocs renderer implementation.
and mkdocstrings, ensuring:
- Root index.md always exists This module defines the `MkDocsRenderer` class, which generates Markdown
- Parent package indexes are created automatically documentation sources compatible with MkDocs Material and the mkdocstrings
- Child modules are linked in parent index files plugin.
The renderer ensures a consistent documentation structure by:
- Creating a root `index.md` if one does not exist
- Generating package index pages automatically
- Linking child modules within parent package pages
- Optionally generating `README.md` from the root package docstring
""" """
from pathlib import Path from pathlib import Path
from docforge.models import Project, Module
from docforge.models import Module, Project
class MkDocsRenderer: class MkDocsRenderer:
""" """
Renderer that generates Markdown source files formatted for the MkDocs Renderer that produces Markdown documentation for MkDocs.
'mkdocstrings' plugin.
Generated pages use mkdocstrings directives to reference Python modules,
allowing MkDocs to render API documentation dynamically.
""" """
name = "mkdocs" name = "mkdocs"
@@ -24,6 +33,7 @@ class MkDocsRenderer:
# ------------------------- # -------------------------
# Public API # Public API
# ------------------------- # -------------------------
def generate_sources( def generate_sources(
self, self,
project: Project, project: Project,
@@ -31,13 +41,22 @@ class MkDocsRenderer:
module_is_source: bool | None = None, module_is_source: bool | None = None,
) -> None: ) -> None:
""" """
Produce a set of Markdown files in the output directory based on the Generate Markdown documentation files for a project.
provided Project models.
This method renders a documentation structure from the provided
project model and writes the resulting Markdown files to the
specified output directory.
Args: Args:
project: The project models to render. project (Project):
out_dir: Target directory for documentation files. Project model containing modules to document.
module_is_source: Module is the source folder and to be treated as the root folder.
out_dir (Path):
Directory where generated Markdown files will be written.
module_is_source (bool | None):
If True, treat the specified module as the documentation root
rather than nesting it inside a folder.
""" """
out_dir.mkdir(parents=True, exist_ok=True) out_dir.mkdir(parents=True, exist_ok=True)
self._ensure_root_index(project, out_dir) self._ensure_root_index(project, out_dir)
@@ -45,10 +64,9 @@ class MkDocsRenderer:
modules = list(project.get_all_modules()) modules = list(project.get_all_modules())
paths = {m.path for m in modules} paths = {m.path for m in modules}
# Package detection (level-agnostic) # Detect packages (modules with children)
packages = { packages = {
p for p in paths p for p in paths if any(other.startswith(p + ".") for other in paths)
if any(other.startswith(p + ".") for other in paths)
} }
for module in modules: for module in modules:
@@ -59,9 +77,93 @@ class MkDocsRenderer:
module_is_source, module_is_source,
) )
def generate_readme(
self,
project: Project,
docs_dir: Path,
module_is_source: bool | None = None,
readme_dir: Path | None = None,
) -> None:
"""
Generate a `README.md` file from the root module docstring.
Notes:
- If `module_is_source` is True, `README.md` is written to the
project root directory.
- If False, README generation is currently not implemented.
Args:
project (Project):
Project model containing documentation metadata.
docs_dir (Path):
Directory containing generated documentation sources.
module_is_source (bool | None):
Whether the module is treated as the project source root.
readme_dir (Path | None):
Directory where the generated README.md should be written.
Defaults to the parent of `docs_dir`.
"""
if not module_is_source:
# Future: support README generation per module
return
readme_root = readme_dir if readme_dir is not None else docs_dir.parent
readme_path = readme_root / "README.md"
root_module = None
for module in project.get_all_modules():
if module.path == project.name:
root_module = module
break
if root_module is None:
return
doc = ""
if root_module.docstring:
doc = getattr(
root_module.docstring,
"value",
str(root_module.docstring),
)
content = f"# {project.name}\n\n" f"{doc.strip()}\n"
if (
not readme_path.exists()
or readme_path.read_text(encoding="utf-8") != content
):
readme_path.write_text(
content,
encoding="utf-8",
)
# ------------------------- # -------------------------
# Internal helpers # Internal helpers
# ------------------------- # -------------------------
def _find_root_module(self, project: Project) -> Module | None:
"""
Locate the root module corresponding to the project name.
Args:
project (Project):
Project model to inspect.
Returns:
Module | None:
The root `Module` if found, otherwise `None`.
"""
for module in project.get_all_modules():
if module.path == project.name:
return module
return None
def _write_module( def _write_module(
self, self,
module: Module, module: Module,
@@ -70,15 +172,26 @@ class MkDocsRenderer:
module_is_source: bool | None = None, module_is_source: bool | None = None,
) -> None: ) -> None:
""" """
Write a single module's documentation file. Packages are written as Write documentation for a single module.
'index.md' inside their respective directories.
Package modules are rendered as `index.md` files inside their
corresponding directories, while leaf modules are written as
standalone Markdown pages.
Args: Args:
module: The module to write. module (Module):
packages: A set of module paths that are identified as packages. Module to render.
out_dir: The base output directory.
module_is_source: Module is the source folder and to be treated as the root folder. packages (set[str]):
Set of module paths identified as packages.
out_dir (Path):
Base directory for generated documentation files.
module_is_source (bool | None):
Whether the module acts as the documentation root directory.
""" """
parts = module.path.split(".") parts = module.path.split(".")
if module_is_source: if module_is_source:
@@ -87,15 +200,15 @@ class MkDocsRenderer:
module_name, parts = parts[0], parts module_name, parts = parts[0], parts
if module.path in packages: if module.path in packages:
# Package → directory/index.md
dir_path = out_dir.joinpath(*parts) dir_path = out_dir.joinpath(*parts)
dir_path.mkdir(parents=True, exist_ok=True) dir_path.mkdir(parents=True, exist_ok=True)
md_path = dir_path / "index.md" md_path = dir_path / "index.md"
link_target = f"{parts[-1]}/" if parts else None link_target = f"{parts[-1]}/" if parts else None
else: else:
# Leaf module → parent_dir/<name>.md
dir_path = out_dir.joinpath(*parts[:-1]) dir_path = out_dir.joinpath(*parts[:-1])
dir_path.mkdir(parents=True, exist_ok=True) dir_path.mkdir(parents=True, exist_ok=True)
md_path = dir_path / f"{parts[-1]}.md" md_path = dir_path / f"{parts[-1]}.md"
link_target = f"{parts[-1]}.md" if parts else None link_target = f"{parts[-1]}.md" if parts else None
@@ -110,31 +223,41 @@ class MkDocsRenderer:
def _render_markdown(self, title: str, module_path: str) -> str: def _render_markdown(self, title: str, module_path: str) -> str:
""" """
Generate the Markdown content for a module file. Generate Markdown content for a module documentation page.
Args: Args:
title: The display title for the page. title (str):
module_path: The dotted path of the module to document. Page title displayed in the documentation.
module_path (str):
Dotted import path of the module.
Returns: Returns:
A string containing the Markdown source. str:
Markdown source containing a mkdocstrings directive.
""" """
return ( return f"# {title}\n\n" f"::: {module_path}\n"
f"# {title}\n\n"
f"::: {module_path}\n"
)
def _ensure_root_index( def _ensure_root_index(
self, self,
project: Project, project: Project,
out_dir: Path out_dir: Path,
) -> None: ) -> None:
"""
Ensure that the root `index.md` page exists.
Args:
project (Project):
Project model used for the page title.
out_dir (Path):
Documentation output directory.
"""
root_index = out_dir / "index.md" root_index = out_dir / "index.md"
if not root_index.exists(): if not root_index.exists():
root_index.write_text( root_index.write_text(
f"# {project.name}\n\n" f"# {project.name}\n\n" "## Modules\n\n",
"## Modules\n\n",
encoding="utf-8", encoding="utf-8",
) )
@@ -145,16 +268,30 @@ class MkDocsRenderer:
link_target: str, link_target: str,
title: str, title: str,
) -> None: ) -> None:
"""
Ensure that parent package index files exist and contain links to
child modules.
Args:
parts (list[str]):
Module path components.
out_dir (Path):
Documentation output directory.
link_target (str):
Link target used in the parent index.
title (str):
Display title for the link.
"""
if len(parts) == 1: if len(parts) == 1:
parent_index = out_dir / "index.md" parent_index = out_dir / "index.md"
link = f"- [{title}]({link_target})\n"
else: else:
parent_dir = out_dir.joinpath(*parts[:-1]) parent_dir = out_dir.joinpath(*parts[:-1])
parent_dir.mkdir(parents=True, exist_ok=True) parent_dir.mkdir(parents=True, exist_ok=True)
parent_index = parent_dir / "index.md" parent_index = parent_dir / "index.md"
link = f"- [{title}]({link_target})\n"
if not parent_index.exists(): if not parent_index.exists():
parent_title = parts[-2].replace("_", " ").title() parent_title = parts[-2].replace("_", " ").title()
parent_index.write_text( parent_index.write_text(
@@ -164,5 +301,6 @@ class MkDocsRenderer:
content = parent_index.read_text(encoding="utf-8") content = parent_index.read_text(encoding="utf-8")
link = f"- [{title}]({link_target})\n"
if link not in content: if link not in content:
parent_index.write_text(content + link, encoding="utf-8") parent_index.write_text(content + link, encoding="utf-8")

View File

@@ -1,6 +1,6 @@
from pathlib import Path from pathlib import Path
from docforge.models import Project, Module
from docforge.models import Module, Project
class MkDocsRenderer: class MkDocsRenderer:
name: str name: str
@@ -11,7 +11,12 @@ class MkDocsRenderer:
out_dir: Path, out_dir: Path,
module_is_source: bool | None = None, module_is_source: bool | None = None,
) -> None: ... ) -> None: ...
def generate_readme(
self,
project: Project,
docs_dir: Path,
module_is_source: bool | None = None,
) -> None: ...
def _write_module( def _write_module(
self, self,
module: Module, module: Module,
@@ -19,9 +24,8 @@ class MkDocsRenderer:
out_dir: Path, out_dir: Path,
module_is_source: bool | None = None, module_is_source: bool | None = None,
) -> None: ... ) -> None: ...
def _render_markdown(self, title: str, module_path: str) -> str: ... def _render_markdown(self, title: str, module_path: str) -> str: ...
def _ensure_root_index(self, project: Project, out_dir: Path) -> None: ...
def _ensure_root_index(self, project, out_dir) -> None: ... def _ensure_parent_index(
self, parts: list[str], out_dir: Path, link_target: str, title: str
def _ensure_parent_index(self, parts, out_dir, link_target, title) -> None: ... ) -> None: ...

View File

@@ -1,5 +1,17 @@
"""
# Summary
Server layer for doc-forge.
This module exposes server implementations used to provide live access
to generated documentation resources. Currently, it includes the MCP
documentation server.
---
"""
from .mcp_server import MCPServer from .mcp_server import MCPServer
__all__ = [ __all__ = [
"MCPServer", "MCPServer",
] ]

View File

@@ -1,3 +1,22 @@
"""
# Summary
MCP server implementation.
This module defines the `MCPServer` class, which serves pre-generated
documentation bundles through the Model Context Protocol (MCP).
---
Notes:
- The served bundle is generated offline by `MCPRenderer`.
- Missing resources are reported as structured error dictionaries rather
than raising exceptions.
- The server exposes read-only resources and a single health-check tool.
---
"""
from __future__ import annotations from __future__ import annotations
import json import json
@@ -9,16 +28,31 @@ from mcp.server.fastmcp import FastMCP
class MCPServer: class MCPServer:
""" """
MCP server for serving a pre-built MCP documentation bundle. MCP server for serving a pre-generated documentation bundle.
The server exposes documentation resources and diagnostic tools through
MCP endpoints backed by JSON files generated by the MCP renderer.
Attributes:
mcp_root (Path):
Directory containing the generated MCP documentation bundle.
app (FastMCP):
Underlying FastMCP application instance that registers resources
and tools.
""" """
def __init__(self, mcp_root: Path, name: str) -> None: def __init__(self, mcp_root: Path, name: str) -> None:
""" """
Initialize the MCPServer. Initialize the MCP server.
Args: Args:
mcp_root: Path to the directory containing pre-built MCP JSON resources. mcp_root (Path):
name: Name of the MCP server. Directory containing the generated MCP documentation bundle
(for example `index.json`, `nav.json`, and `modules/`).
name (str):
Identifier used for the MCP server instance.
""" """
self.mcp_root = mcp_root self.mcp_root = mcp_root
self.app = FastMCP(name) self.app = FastMCP(name)
@@ -32,19 +66,26 @@ class MCPServer:
def _read_json(self, path: Path) -> Any: def _read_json(self, path: Path) -> Any:
""" """
Read and parse a JSON file, returning diagnostic errors if missing. Load and parse a JSON file.
If the file does not exist, a structured error response is returned
instead of raising an exception.
Args: Args:
path: Path to the JSON file. path (Path):
Path to the JSON file to read.
Returns: Returns:
The parsed JSON data or an error dictionary. Any:
Parsed JSON data if the file exists, otherwise an error dictionary
describing the missing resource.
""" """
if not path.exists(): if not path.exists():
return { return {
"error": "not_found", "error": "not_found",
"path": str(path), "path": str(path),
} }
return json.loads(path.read_text(encoding="utf-8")) return json.loads(path.read_text(encoding="utf-8"))
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -53,8 +94,16 @@ class MCPServer:
def _register_resources(self) -> None: def _register_resources(self) -> None:
""" """
Register MCP resources for index, nav, and individual modules. Register MCP resource endpoints.
The server exposes documentation resources through the following
endpoints:
- `docs://index` – Project metadata
- `docs://nav` – Navigation structure
- `docs://modules/{module}` – Individual module documentation
""" """
@self.app.resource("docs://index") @self.app.resource("docs://index")
def index(): def index():
return self._read_json(self.mcp_root / "index.json") return self._read_json(self.mcp_root / "index.json")
@@ -65,31 +114,39 @@ class MCPServer:
@self.app.resource("docs://modules/{module}") @self.app.resource("docs://modules/{module}")
def module(module: str): def module(module: str):
return self._read_json( return self._read_json(self.mcp_root / "modules" / f"{module}.json")
self.mcp_root / "modules" / f"{module}.json"
)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# MCP tools (optional / diagnostic) # MCP tools
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def _register_tools(self) -> None: def _register_tools(self) -> None:
""" """
Register high-level MCP tools for diagnostics. Register optional MCP diagnostic tools.
These tools provide lightweight endpoints useful for verifying that
the MCP server is operational.
""" """
@self.app.tool() @self.app.tool()
def ping() -> str: def ping() -> str:
"""Return a simple health check response."""
return "pong" return "pong"
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Server lifecycle # Server lifecycle
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def run(self, transport: Literal["stdio", "sse", "streamable-http"] = "streamable-http") -> None: def run(
self,
transport: Literal["stdio", "sse", "streamable-http"] = "streamable-http",
) -> None:
""" """
Start the MCP server. Start the MCP server.
Args: Args:
transport: MCP transport (default: streamable-http) transport (Literal["stdio", "sse", "streamable-http"]):
Transport mechanism used by the MCP server. Supported options
include `stdio`, `sse`, and `streamable-http`.
""" """
self.app.run(transport=transport) self.app.run(transport=transport)

View File

@@ -1,22 +1,20 @@
from pathlib import Path from pathlib import Path
from typing import Literal, Any from typing import Any, Literal
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
class MCPServer: class MCPServer:
"""MCP server for serving documentation.""" """MCP server serving a pre-generated documentation bundle.
Exposes read-only documentation resources and a health-check tool.
"""
mcp_root: Path mcp_root: Path
app: FastMCP app: FastMCP
def __init__(self, mcp_root: Path, name: str) -> None: ... def __init__(self, mcp_root: Path, name: str) -> None: ...
def _read_json(self, path: Path) -> Any: ... def _read_json(self, path: Path) -> Any: ...
def _register_resources(self) -> None: ... def _register_resources(self) -> None: ...
def _register_tools(self) -> None: ... def _register_tools(self) -> None: ...
def run(self, transport: Literal["stdio", "sse", "streamable-http"] = ...) -> None: def run(self, transport: Literal["stdio", "sse", "streamable-http"] = ...) -> None:
"""Start the MCP server.""" """Start the MCP server."""

View File

@@ -0,0 +1,16 @@
theme:
features:
- toc.integrate
- header.autohide
- announce.dismiss
- footer.social
- content.code.select
- content.code.line_numbers
- content.tooltips
plugins:
- search
- swagger-ui-tag
- neoteroi.mkdocsoad:
use_pymdownx: true

View File

@@ -0,0 +1,60 @@
theme:
name: material
palette:
- scheme: slate
primary: deep purple
accent: cyan
font:
text: Inter
code: JetBrains Mono
features:
# Navigation
- navigation.sections
- navigation.expand
- navigation.top
- navigation.instant
- navigation.tracking
- navigation.indexes
# Content
- content.code.copy
- content.code.annotate
- content.tabs.link
- content.action.edit
# Search UX
- search.highlight
- search.share
- search.suggest
markdown_extensions:
- pymdownx.superfences
- pymdownx.inlinehilite
- pymdownx.snippets
- admonition
- pymdownx.details
- pymdownx.highlight:
linenums: true
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
- pymdownx.tabbed:
alternate_style: true
- pymdownx.tasklist:
custom_checkbox: true
- tables
- footnotes
- pymdownx.caret
- pymdownx.tilde
- pymdownx.mark
extra_css:
- https://unpkg.com/dracula-prism/dist/css/dracula-prism.css

View File

@@ -1,26 +1,9 @@
theme:
name: material
palette:
- scheme: slate
primary: deep purple
accent: cyan
font:
text: Inter
code: JetBrains Mono
features:
- navigation.tabs
- navigation.expand
- navigation.top
- navigation.instant
- content.code.copy
- content.code.annotate
plugins: plugins:
- search - search
- mkdocstrings: - mkdocstrings:
handlers: handlers:
python: python:
paths: ["."] paths: [".."]
options: options:
docstring_style: google docstring_style: google
show_source: false show_source: false
@@ -31,3 +14,6 @@ plugins:
annotations_path: brief annotations_path: brief
show_root_heading: true show_root_heading: true
group_by_category: true group_by_category: true
show_category_heading: true
show_object_full_path: false
show_symbol_type_heading: true

View File

@@ -0,0 +1,25 @@
theme:
features:
# Horizontal tab navigation (mongo-ops style)
- navigation.tabs
# Navigation UX
- toc.integrate
- header.autohide
- announce.dismiss
- footer.social
# Content & Code
- content.code.select
- content.code.line_numbers
- content.tooltips
palette:
scheme: slate
primary: blue grey
accent: teal
font:
text: Roboto
code: JetBrains Mono
icon:
logo: material/database
repo: fontawesome/brands/github
plugins:
- search

View File

@@ -0,0 +1,3 @@
# Api Utils
::: docforge.cli.api_utils

View File

@@ -0,0 +1,3 @@
# Api Utils
::: docforge.cli.api_utils

View File

@@ -0,0 +1,3 @@
# Commands
::: docforge.cli.commands

View File

@@ -0,0 +1,8 @@
# Cli
::: docforge.cli
- [Api Utils](api_utils.md)
- [Commands](commands.md)
- [Main](main.md)
- [Mcp Utils](mcp_utils.md)
- [Mkdocs Utils](mkdocs_utils.md)

View File

@@ -0,0 +1,3 @@
# Main
::: docforge.cli.main

View File

@@ -0,0 +1,3 @@
# Mcp Utils
::: docforge.cli.mcp_utils

View File

@@ -0,0 +1,3 @@
# Mkdocs Utils
::: docforge.cli.mkdocs_utils

View File

@@ -0,0 +1,9 @@
# Docforge
::: docforge
- [Cli](cli/)
- [Loaders](loaders/)
- [Models](models/)
- [Nav](nav/)
- [Renderers](renderers/)
- [Servers](servers/)

View File

@@ -0,0 +1,4 @@
# Loaders
::: docforge.loaders
- [Griffe Loader](griffe_loader.md)

View File

@@ -0,0 +1,6 @@
# Models
::: docforge.models
- [Module](module.md)
- [Object](object.md)
- [Project](project.md)

View File

@@ -0,0 +1,7 @@
# Nav
::: docforge.nav
- [Mkdocs](mkdocs.md)
- [Resolver](resolver.md)
- [Spec](spec.md)
- [Wiki](wiki.md)

View File

@@ -0,0 +1,3 @@
# Wiki
::: docforge.nav.wiki

View File

@@ -0,0 +1,6 @@
# Renderers
::: docforge.renderers
- [Base](base.md)
- [Mcp Renderer](mcp_renderer.md)
- [Mkdocs Renderer](mkdocs_renderer.md)

View File

@@ -0,0 +1,4 @@
# Servers
::: docforge.servers
- [Mcp Server](mcp_server.md)

View File

@@ -1,3 +1,4 @@
# docforge # docforge
::: docforge ::: docforge
- [Docforge](docforge/)

View File

@@ -0,0 +1,3 @@
# Griffe Loader
::: docforge.loaders.griffe_loader

View File

@@ -0,0 +1,3 @@
# Module
::: docforge.models.module

View File

@@ -0,0 +1,3 @@
# Object
::: docforge.models.object

View File

@@ -0,0 +1,3 @@
# Project
::: docforge.models.project

3
docs/lib/nav/mkdocs.md Normal file
View File

@@ -0,0 +1,3 @@
# Mkdocs
::: docforge.nav.mkdocs

3
docs/lib/nav/resolver.md Normal file
View File

@@ -0,0 +1,3 @@
# Resolver
::: docforge.nav.resolver

3
docs/lib/nav/spec.md Normal file
View File

@@ -0,0 +1,3 @@
# Spec
::: docforge.nav.spec

3
docs/lib/nav/wiki.md Normal file
View File

@@ -0,0 +1,3 @@
# Wiki
::: docforge.nav.wiki

View File

@@ -0,0 +1,3 @@
# Base
::: docforge.renderers.base

Some files were not shown because too many files have changed in this diff Show More