From 8c6c46caf2a3c97d6fd36ac543886005c5678a15 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Fri, 11 Sep 2026 23:38:23 +0530 Subject: [PATCH] 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 --- docforge/cli/commands.py | 31 +++- docforge/cli/commands.pyi | 2 + docforge/cli/mkdocs_utils.py | 49 ++++-- docforge/cli/mkdocs_utils.pyi | 1 + docforge/nav/__init__.py | 2 + docforge/nav/__init__.pyi | 2 + docforge/nav/wiki.py | 161 ++++++++++++++++++ docforge/nav/wiki.pyi | 15 ++ docforge/templates/mkdocs.wiki.yml | 2 + docs/lib/docforge/cli/api_utils.md | 3 + docs/lib/docforge/cli/commands.md | 3 + docs/lib/docforge/cli/index.md | 8 + docs/lib/docforge/cli/main.md | 3 + docs/lib/docforge/cli/mcp_utils.md | 3 + docs/lib/docforge/cli/mkdocs_utils.md | 3 + docs/lib/docforge/index.md | 9 + docs/lib/docforge/loaders/griffe_loader.md | 3 + docs/lib/docforge/loaders/index.md | 4 + docs/lib/docforge/models/index.md | 6 + docs/lib/docforge/models/module.md | 3 + docs/lib/docforge/models/object.md | 3 + docs/lib/docforge/models/project.md | 3 + docs/lib/docforge/nav/index.md | 7 + docs/lib/docforge/nav/mkdocs.md | 3 + docs/lib/docforge/nav/resolver.md | 3 + docs/lib/docforge/nav/spec.md | 3 + docs/lib/docforge/nav/wiki.md | 3 + docs/lib/docforge/renderers/base.md | 3 + docs/lib/docforge/renderers/index.md | 6 + docs/lib/docforge/renderers/mcp_renderer.md | 3 + .../lib/docforge/renderers/mkdocs_renderer.md | 3 + docs/lib/docforge/servers/index.md | 4 + docs/lib/docforge/servers/mcp_server.md | 3 + docs/lib/index.md | 1 + docs/wiki/01_overview.md | 25 +++ docs/wiki/02_architecture.md | 34 ++++ docs/wiki/03_conventions.md | 29 ++++ docs/wiki/04_iterative_workflow.md | 33 ++++ docs/wiki/05_development/01_environment.md | 20 +++ docs/wiki/05_development/02_quality_gates.md | 22 +++ docs/wiki/index.md | 39 +++++ mkdocs.yml | 9 +- tests/cli/test_build_wiki.py | 137 +++++++++++++++ tests/nav/test_wiki.py | 106 ++++++++++++ 44 files changed, 798 insertions(+), 17 deletions(-) create mode 100644 docforge/nav/wiki.py create mode 100644 docforge/nav/wiki.pyi create mode 100644 docforge/templates/mkdocs.wiki.yml create mode 100644 docs/lib/docforge/cli/api_utils.md create mode 100644 docs/lib/docforge/cli/commands.md create mode 100644 docs/lib/docforge/cli/index.md create mode 100644 docs/lib/docforge/cli/main.md create mode 100644 docs/lib/docforge/cli/mcp_utils.md create mode 100644 docs/lib/docforge/cli/mkdocs_utils.md create mode 100644 docs/lib/docforge/index.md create mode 100644 docs/lib/docforge/loaders/griffe_loader.md create mode 100644 docs/lib/docforge/loaders/index.md create mode 100644 docs/lib/docforge/models/index.md create mode 100644 docs/lib/docforge/models/module.md create mode 100644 docs/lib/docforge/models/object.md create mode 100644 docs/lib/docforge/models/project.md create mode 100644 docs/lib/docforge/nav/index.md create mode 100644 docs/lib/docforge/nav/mkdocs.md create mode 100644 docs/lib/docforge/nav/resolver.md create mode 100644 docs/lib/docforge/nav/spec.md create mode 100644 docs/lib/docforge/nav/wiki.md create mode 100644 docs/lib/docforge/renderers/base.md create mode 100644 docs/lib/docforge/renderers/index.md create mode 100644 docs/lib/docforge/renderers/mcp_renderer.md create mode 100644 docs/lib/docforge/renderers/mkdocs_renderer.md create mode 100644 docs/lib/docforge/servers/index.md create mode 100644 docs/lib/docforge/servers/mcp_server.md create mode 100644 docs/wiki/01_overview.md create mode 100644 docs/wiki/02_architecture.md create mode 100644 docs/wiki/03_conventions.md create mode 100644 docs/wiki/04_iterative_workflow.md create mode 100644 docs/wiki/05_development/01_environment.md create mode 100644 docs/wiki/05_development/02_quality_gates.md create mode 100644 docs/wiki/index.md create mode 100644 tests/cli/test_build_wiki.py create mode 100644 tests/nav/test_wiki.py diff --git a/docforge/cli/commands.py b/docforge/cli/commands.py index 05ca0b6..7a5e845 100644 --- a/docforge/cli/commands.py +++ b/docforge/cli/commands.py @@ -29,6 +29,9 @@ def cli() -> None: @click.option("--mcp", is_flag=True, help="Build MCP resources") @click.option("--mkdocs", is_flag=True, help="Build MkDocs site") @click.option("--api", is_flag=True, help="Build API docs from an OpenAPI spec") +@click.option( + "--wiki", is_flag=True, help="Include a hand-written wiki in the MkDocs site" +) @click.option( "--module-is-source", is_flag=True, @@ -48,6 +51,12 @@ def cli() -> None: default=Path("docs"), help="MkDocs documentation root", ) +@click.option( + "--wiki-dir", + type=click.Path(path_type=Path), + default=Path("docs/wiki"), + help="Hand-written wiki directory included in the MkDocs site", +) @click.option( "--nav", "nav_file", @@ -74,12 +83,14 @@ def build( mcp: bool, mkdocs: bool, api: bool, + wiki: bool, module_is_source: bool, module: str | None, openapi_spec: Path | None, project_name: str | None, site_name: str | None, docs_dir: Path, + wiki_dir: Path, nav_file: Path, template: Path | None, mkdocs_yml: Path, @@ -96,6 +107,7 @@ def build( - MkDocs static documentation sites for library reference docs - Swagger-enabled API docs generated from an OpenAPI spec + - Hand-written wiki pages included in the MkDocs site - MCP structured documentation resources Args: @@ -108,6 +120,9 @@ def build( api (bool): Enable API documentation generation from an OpenAPI spec. + wiki (bool): + Include a hand-written wiki directory in the MkDocs site. + module_is_source (bool): Treat the specified module directory as the project root. @@ -126,6 +141,9 @@ def build( docs_dir (Path): Shared documentation root used as the MkDocs ``docs_dir``. + wiki_dir (Path): + Directory containing hand-written wiki markdown files. + nav_file (Path): Path to the navigation specification file. @@ -142,13 +160,13 @@ def build( click.UsageError: If required options are missing or conflicting. """ - if not mcp and not mkdocs and not api: - raise click.UsageError("Must specify either --mcp, --mkdocs, or --api") + if not mcp and not mkdocs and not api and not wiki: + 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: + 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" @@ -181,16 +199,18 @@ def build( click.echo(f"Generating API sources in {api_dir}...") api_utils.generate_api_sources(spec, api_dir) - if mkdocs or api: + if mkdocs or api or wiki: modes: list[str] = [] if mkdocs: modes.append("lib") if api: modes.append("api") + if wiki: + modes.append("wiki") site_description: str | None = None site_author: str | None = None - effective_site_name = site_name or module + effective_site_name = site_name or module or Path.cwd().name if api: metadata = api_utils.derive_metadata(spec) @@ -208,6 +228,7 @@ def build( modes=modes, site_description=site_description, site_author=site_author, + wiki_dir=wiki_dir if wiki else None, ) click.echo("Running MkDocs build...") diff --git a/docforge/cli/commands.pyi b/docforge/cli/commands.pyi index 1638a22..92c3167 100644 --- a/docforge/cli/commands.pyi +++ b/docforge/cli/commands.pyi @@ -9,12 +9,14 @@ def build( mcp: bool, mkdocs: bool, api: bool, + wiki: bool, module_is_source: bool, module: str | None, openapi_spec: Path | None, project_name: str | None, site_name: str | None, docs_dir: Path, + wiki_dir: Path, nav_file: Path, template: Path | None, mkdocs_yml: Path, diff --git a/docforge/cli/mkdocs_utils.py b/docforge/cli/mkdocs_utils.py index da0d63e..949133e 100644 --- a/docforge/cli/mkdocs_utils.py +++ b/docforge/cli/mkdocs_utils.py @@ -13,7 +13,12 @@ import click import yaml from docforge.loaders import GriffeLoader, discover_module_paths -from docforge.nav import MkDocsNavEmitter, load_nav_spec, resolve_nav +from docforge.nav import ( + MkDocsNavEmitter, + build_wiki_nav, + load_nav_spec, + resolve_nav, +) from docforge.renderers import MkDocsRenderer @@ -78,17 +83,20 @@ def generate_config( modes: Iterable[str] | None = None, site_description: str | None = None, site_author: str | None = None, + wiki_dir: Path | None = None, ) -> None: """ Generate an `mkdocs.yml` configuration file. The configuration is created by combining a template configuration with a navigation structure derived from the docforge navigation - specification. + specification (and, when a wiki directory is provided, from the wiki + file structure). The ``docs_dir`` is always written relative to the MkDocs root and is expected to be the shared documentation parent (for example ``docs``), - with generated sources nested under ``lib/`` or ``api/`` subdirectories. + with generated sources nested under ``lib/`` or ``api/`` subdirectories + and hand-written wiki content under a ``wiki/`` subdirectory. Args: docs_dir (Path): @@ -110,8 +118,9 @@ def generate_config( modes (Optional[Iterable[str]]): Documentation modes to enable. Each mode contributes its own - built-in template fragment (for example ``lib`` or ``api``), - merged on top of the shared ``mkdocs.common.yml`` template. + built-in template fragment (for example ``lib``, ``api``, or + ``wiki``), merged on top of the shared ``mkdocs.common.yml`` + template. site_description (Optional[str]): Optional site description written into the configuration. @@ -119,16 +128,34 @@ def generate_config( site_author (Optional[str]): Optional site author written into the configuration. + wiki_dir (Optional[Path]): + Optional path to a hand-written wiki directory (for example + ``docs/wiki``). When provided, the site navigation is derived + from the wiki file structure and placed before the navigation + groups defined in ``nav_file``. + Raises: click.FileError: - If the navigation specification or template file cannot be found. + If the navigation specification, template, or wiki directory + cannot be found. """ - if not nav_file.exists(): + if not nav_file.exists() and wiki_dir is None: raise click.FileError(str(nav_file), hint="Nav spec not found") - spec = load_nav_spec(nav_file) - resolved = resolve_nav(spec, docs_dir) - nav_block = MkDocsNavEmitter().emit(resolved) + nav_block: list[dict] = [] + if nav_file.exists(): + spec = load_nav_spec(nav_file) + resolved = resolve_nav(spec, docs_dir) + nav_block = MkDocsNavEmitter().emit(resolved) + else: + spec = None + + if wiki_dir is not None: + if not wiki_dir.exists(): + raise click.FileError(str(wiki_dir), hint="Wiki dir not found") + wiki_nav = build_wiki_nav(wiki_dir) + if wiki_nav: + nav_block = wiki_nav + [entry for entry in nav_block if "Home" not in entry] data = _load_template(template, modes) @@ -140,7 +167,7 @@ def generate_config( data["docs_dir"] = Path(os.path.relpath(docs_dir, out.parent)).as_posix() data["nav"] = nav_block - if spec.icon: + if spec is not None and spec.icon: theme = data.setdefault("theme", {}) if not isinstance(theme, dict): theme = {} diff --git a/docforge/cli/mkdocs_utils.pyi b/docforge/cli/mkdocs_utils.pyi index a7e4613..988a857 100644 --- a/docforge/cli/mkdocs_utils.pyi +++ b/docforge/cli/mkdocs_utils.pyi @@ -17,6 +17,7 @@ def generate_config( modes: Iterable[str] | None = None, site_description: str | None = None, site_author: str | None = None, + wiki_dir: Path | None = None, ) -> None: ... def build(mkdocs_yml: Path) -> None: ... def serve(mkdocs_yml: Path) -> None: ... diff --git a/docforge/nav/__init__.py b/docforge/nav/__init__.py index f9fd853..645f2c7 100644 --- a/docforge/nav/__init__.py +++ b/docforge/nav/__init__.py @@ -26,11 +26,13 @@ independent of module hierarchy. from .spec import NavSpec, load_nav_spec from .resolver import ResolvedNav, resolve_nav from .mkdocs import MkDocsNavEmitter +from .wiki import build_wiki_nav __all__ = [ "NavSpec", "ResolvedNav", "MkDocsNavEmitter", + "build_wiki_nav", "resolve_nav", "load_nav_spec", ] diff --git a/docforge/nav/__init__.pyi b/docforge/nav/__init__.pyi index 2a3580c..02624fe 100644 --- a/docforge/nav/__init__.pyi +++ b/docforge/nav/__init__.pyi @@ -1,11 +1,13 @@ from .mkdocs import MkDocsNavEmitter from .resolver import ResolvedNav, resolve_nav from .spec import NavSpec, load_nav_spec +from .wiki import build_wiki_nav __all__ = [ "NavSpec", "ResolvedNav", "MkDocsNavEmitter", + "build_wiki_nav", "resolve_nav", "load_nav_spec", ] diff --git a/docforge/nav/wiki.py b/docforge/nav/wiki.py new file mode 100644 index 0000000..ecde65a --- /dev/null +++ b/docforge/nav/wiki.py @@ -0,0 +1,161 @@ +""" +# 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 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) -> 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 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) + ] diff --git a/docforge/nav/wiki.pyi b/docforge/nav/wiki.pyi new file mode 100644 index 0000000..fe62725 --- /dev/null +++ b/docforge/nav/wiki.pyi @@ -0,0 +1,15 @@ +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. + + Returns: + Wiki navigation entries compatible with the MkDocs + `nav` configuration. + + Raises: + FileNotFoundError: if the wiki directory does not exist + """ + ... diff --git a/docforge/templates/mkdocs.wiki.yml b/docforge/templates/mkdocs.wiki.yml new file mode 100644 index 0000000..a035a5b --- /dev/null +++ b/docforge/templates/mkdocs.wiki.yml @@ -0,0 +1,2 @@ +plugins: + - search \ No newline at end of file diff --git a/docs/lib/docforge/cli/api_utils.md b/docs/lib/docforge/cli/api_utils.md new file mode 100644 index 0000000..62040b8 --- /dev/null +++ b/docs/lib/docforge/cli/api_utils.md @@ -0,0 +1,3 @@ +# Api Utils + +::: docforge.cli.api_utils diff --git a/docs/lib/docforge/cli/commands.md b/docs/lib/docforge/cli/commands.md new file mode 100644 index 0000000..75f0d75 --- /dev/null +++ b/docs/lib/docforge/cli/commands.md @@ -0,0 +1,3 @@ +# Commands + +::: docforge.cli.commands diff --git a/docs/lib/docforge/cli/index.md b/docs/lib/docforge/cli/index.md new file mode 100644 index 0000000..314c649 --- /dev/null +++ b/docs/lib/docforge/cli/index.md @@ -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) diff --git a/docs/lib/docforge/cli/main.md b/docs/lib/docforge/cli/main.md new file mode 100644 index 0000000..b22fa07 --- /dev/null +++ b/docs/lib/docforge/cli/main.md @@ -0,0 +1,3 @@ +# Main + +::: docforge.cli.main diff --git a/docs/lib/docforge/cli/mcp_utils.md b/docs/lib/docforge/cli/mcp_utils.md new file mode 100644 index 0000000..7987ffd --- /dev/null +++ b/docs/lib/docforge/cli/mcp_utils.md @@ -0,0 +1,3 @@ +# Mcp Utils + +::: docforge.cli.mcp_utils diff --git a/docs/lib/docforge/cli/mkdocs_utils.md b/docs/lib/docforge/cli/mkdocs_utils.md new file mode 100644 index 0000000..2f2d3d9 --- /dev/null +++ b/docs/lib/docforge/cli/mkdocs_utils.md @@ -0,0 +1,3 @@ +# Mkdocs Utils + +::: docforge.cli.mkdocs_utils diff --git a/docs/lib/docforge/index.md b/docs/lib/docforge/index.md new file mode 100644 index 0000000..d6800dc --- /dev/null +++ b/docs/lib/docforge/index.md @@ -0,0 +1,9 @@ +# Docforge + +::: docforge +- [Cli](cli/) +- [Loaders](loaders/) +- [Models](models/) +- [Nav](nav/) +- [Renderers](renderers/) +- [Servers](servers/) diff --git a/docs/lib/docforge/loaders/griffe_loader.md b/docs/lib/docforge/loaders/griffe_loader.md new file mode 100644 index 0000000..21d8766 --- /dev/null +++ b/docs/lib/docforge/loaders/griffe_loader.md @@ -0,0 +1,3 @@ +# Griffe Loader + +::: docforge.loaders.griffe_loader diff --git a/docs/lib/docforge/loaders/index.md b/docs/lib/docforge/loaders/index.md new file mode 100644 index 0000000..2cbb874 --- /dev/null +++ b/docs/lib/docforge/loaders/index.md @@ -0,0 +1,4 @@ +# Loaders + +::: docforge.loaders +- [Griffe Loader](griffe_loader.md) diff --git a/docs/lib/docforge/models/index.md b/docs/lib/docforge/models/index.md new file mode 100644 index 0000000..f16cc48 --- /dev/null +++ b/docs/lib/docforge/models/index.md @@ -0,0 +1,6 @@ +# Models + +::: docforge.models +- [Module](module.md) +- [Object](object.md) +- [Project](project.md) diff --git a/docs/lib/docforge/models/module.md b/docs/lib/docforge/models/module.md new file mode 100644 index 0000000..945994c --- /dev/null +++ b/docs/lib/docforge/models/module.md @@ -0,0 +1,3 @@ +# Module + +::: docforge.models.module diff --git a/docs/lib/docforge/models/object.md b/docs/lib/docforge/models/object.md new file mode 100644 index 0000000..8e9a603 --- /dev/null +++ b/docs/lib/docforge/models/object.md @@ -0,0 +1,3 @@ +# Object + +::: docforge.models.object diff --git a/docs/lib/docforge/models/project.md b/docs/lib/docforge/models/project.md new file mode 100644 index 0000000..64db98e --- /dev/null +++ b/docs/lib/docforge/models/project.md @@ -0,0 +1,3 @@ +# Project + +::: docforge.models.project diff --git a/docs/lib/docforge/nav/index.md b/docs/lib/docforge/nav/index.md new file mode 100644 index 0000000..1740108 --- /dev/null +++ b/docs/lib/docforge/nav/index.md @@ -0,0 +1,7 @@ +# Nav + +::: docforge.nav +- [Mkdocs](mkdocs.md) +- [Resolver](resolver.md) +- [Spec](spec.md) +- [Wiki](wiki.md) diff --git a/docs/lib/docforge/nav/mkdocs.md b/docs/lib/docforge/nav/mkdocs.md new file mode 100644 index 0000000..b783fec --- /dev/null +++ b/docs/lib/docforge/nav/mkdocs.md @@ -0,0 +1,3 @@ +# Mkdocs + +::: docforge.nav.mkdocs diff --git a/docs/lib/docforge/nav/resolver.md b/docs/lib/docforge/nav/resolver.md new file mode 100644 index 0000000..4f30dc2 --- /dev/null +++ b/docs/lib/docforge/nav/resolver.md @@ -0,0 +1,3 @@ +# Resolver + +::: docforge.nav.resolver diff --git a/docs/lib/docforge/nav/spec.md b/docs/lib/docforge/nav/spec.md new file mode 100644 index 0000000..924864b --- /dev/null +++ b/docs/lib/docforge/nav/spec.md @@ -0,0 +1,3 @@ +# Spec + +::: docforge.nav.spec diff --git a/docs/lib/docforge/nav/wiki.md b/docs/lib/docforge/nav/wiki.md new file mode 100644 index 0000000..c60c750 --- /dev/null +++ b/docs/lib/docforge/nav/wiki.md @@ -0,0 +1,3 @@ +# Wiki + +::: docforge.nav.wiki diff --git a/docs/lib/docforge/renderers/base.md b/docs/lib/docforge/renderers/base.md new file mode 100644 index 0000000..196d183 --- /dev/null +++ b/docs/lib/docforge/renderers/base.md @@ -0,0 +1,3 @@ +# Base + +::: docforge.renderers.base diff --git a/docs/lib/docforge/renderers/index.md b/docs/lib/docforge/renderers/index.md new file mode 100644 index 0000000..28d61c4 --- /dev/null +++ b/docs/lib/docforge/renderers/index.md @@ -0,0 +1,6 @@ +# Renderers + +::: docforge.renderers +- [Base](base.md) +- [Mcp Renderer](mcp_renderer.md) +- [Mkdocs Renderer](mkdocs_renderer.md) diff --git a/docs/lib/docforge/renderers/mcp_renderer.md b/docs/lib/docforge/renderers/mcp_renderer.md new file mode 100644 index 0000000..02f97d1 --- /dev/null +++ b/docs/lib/docforge/renderers/mcp_renderer.md @@ -0,0 +1,3 @@ +# Mcp Renderer + +::: docforge.renderers.mcp_renderer diff --git a/docs/lib/docforge/renderers/mkdocs_renderer.md b/docs/lib/docforge/renderers/mkdocs_renderer.md new file mode 100644 index 0000000..f60486e --- /dev/null +++ b/docs/lib/docforge/renderers/mkdocs_renderer.md @@ -0,0 +1,3 @@ +# Mkdocs Renderer + +::: docforge.renderers.mkdocs_renderer diff --git a/docs/lib/docforge/servers/index.md b/docs/lib/docforge/servers/index.md new file mode 100644 index 0000000..22e4b69 --- /dev/null +++ b/docs/lib/docforge/servers/index.md @@ -0,0 +1,4 @@ +# Servers + +::: docforge.servers +- [Mcp Server](mcp_server.md) diff --git a/docs/lib/docforge/servers/mcp_server.md b/docs/lib/docforge/servers/mcp_server.md new file mode 100644 index 0000000..0f20e6b --- /dev/null +++ b/docs/lib/docforge/servers/mcp_server.md @@ -0,0 +1,3 @@ +# Mcp Server + +::: docforge.servers.mcp_server diff --git a/docs/lib/index.md b/docs/lib/index.md index 96e8919..fd38149 100644 --- a/docs/lib/index.md +++ b/docs/lib/index.md @@ -1,3 +1,4 @@ # docforge ::: docforge +- [Docforge](docforge/) diff --git a/docs/wiki/01_overview.md b/docs/wiki/01_overview.md new file mode 100644 index 0000000..70cf1e2 --- /dev/null +++ b/docs/wiki/01_overview.md @@ -0,0 +1,25 @@ +# Overview + +`docforge` turns GSDFC-compliant Python docstrings into maintainable reference +documentation. It never edits source docstrings; it reads them, renders them, +and assembles a single MkDocs site from all available material. + +## What it generates + +| Kind | Source | Output | +|-----------|----------------------------|---------------------------------| +| `lib` | GSDFC docstrings | `docs/lib/**` rendered markdown | +| `api` | OpenAPI JSON spec | `docs/api/**` rendered markdown | +| `wiki` | Hand-written markdown | `docs/wiki/**` (unchanged) | +| `mcp` | Griffe + renderers | `docs/mcp/**` structured files | + +## Combined build + +One `mkdocs.yml` and one MkDocs build serve all kinds: + +1. Wiki navigation is derived from the `docs/wiki/` file structure. +2. Generated library/API navigation is appended after it. +3. The wiki `index.md` becomes the site `Home`. + +Hand-written wiki content is never overwritten or regenerated — only its +navigation is derived automatically. \ No newline at end of file diff --git a/docs/wiki/02_architecture.md b/docs/wiki/02_architecture.md new file mode 100644 index 0000000..48b4f50 --- /dev/null +++ b/docs/wiki/02_architecture.md @@ -0,0 +1,34 @@ +# Architecture + +`docforge` is split into four horizontal layers. Everything flows top to +bottom through the CLI. + +## Loaders → Models + +The `docforge/loaders` package wraps `griffe` to extract modules, functions, +classes, and Google-style docstring sections. Loaded data is normalized into +the object model under `docforge/models` (`Module`, `Object`, `Project`, +`Field`). + +## Navigation + +`docforge/nav` parses `docforge.nav.yml` specs (`NavSpec`, `Resolver`, and the +MkDocs nav emitter) and, since the wiki kind, derives wiki navigation from the +file structure via `build_wiki_nav`. + +## Renderers + +`docforge/renderers` turn model data into artifacts: + +- `MkDocsRenderer` → `docs/lib/**` reference markdown +- `MCPRenderer` → `docs/mcp/**` structured documentation + +## CLI + +`docforge/cli` wires it all together: + +- `commands.py` — the `build` command and its `--mkdocs` / `--api` / `--wiki` + / `--mcp` modes +- `mkdocs_utils.py` — config generation (`mkdocs.yml`) including merged + wiki + lib + api navigation +- `api_utils.py` — OpenAPI loading and API docs generation \ No newline at end of file diff --git a/docs/wiki/03_conventions.md b/docs/wiki/03_conventions.md new file mode 100644 index 0000000..de07f98 --- /dev/null +++ b/docs/wiki/03_conventions.md @@ -0,0 +1,29 @@ +# Conventions + +## GSDFC docstrings + +All documented source uses the Google-Styled Doc-Forge Convention (GSDFC): +`Args:`, `Returns:`, `Raises:`, and `Attributes:` sections with properly typed +signatures. + +## `.pyi` stubs + +Every module ships a matching `.pyi` stub kept in sync with the `.py` +implementation. When signatures change, update both files. + +## Wiki pages + +- Hand-written markdown lives in `docs/wiki/**` and is never generated. +- File names use a numeric prefix: `01_overview.md`, `02_components.md`. +- Nested directories become nested navigation groups: + `05_development/01_environment.md` → group *Development*. +- `index.md` is the site `Home` at the root, and a section root inside a + directory. +- Navigation labels are derived by stripping the numeric prefix and applying + title case on the remaining words. + +## Templates + +MkDocs config fragments live in `docforge/templates`. The `mkdocs.wiki.yml` +fragment carries only the `search` plugin, since wiki pages contain no +mkdocstrings directives. \ No newline at end of file diff --git a/docs/wiki/04_iterative_workflow.md b/docs/wiki/04_iterative_workflow.md new file mode 100644 index 0000000..f0f1d46 --- /dev/null +++ b/docs/wiki/04_iterative_workflow.md @@ -0,0 +1,33 @@ +# Iterative Workflow + +A docs build runs through the CLI in a single pass. + +## Build commands + +```bash +# Library reference only +doc-forge build --mkdocs --module docforge + +# Wik + library combined (single MkDocs build) +doc-forge build --wiki --mkdocs --module docforge + +# Wiki only — no module required +doc-forge build --wiki --site-name docforge +``` + +## What a combined build does + +1. Validates the requested modes (`--mkdocs`, `--api`, `--wiki`, `--mcp`). +2. Generates library sources under `docs/lib/**` with `MkDocsRenderer`. +3. Generates API sources under `docs/api/**` when `--api` is given. +4. Derives wiki navigation from `docs/wiki/**`. +5. Writes `mkdocs.yml` with merged navigation — wiki first, generated groups + appended, and the wiki `Home` replacing any spec `Home` entry. +6. Runs `mkdocs build` once and emits the site. + +## Explore the site + +```bash +doc-forge build --wiki --mkdocs --module docforge +doc-forge serve --mkdocs-yml mkdocs.yml +``` \ No newline at end of file diff --git a/docs/wiki/05_development/01_environment.md b/docs/wiki/05_development/01_environment.md new file mode 100644 index 0000000..2926182 --- /dev/null +++ b/docs/wiki/05_development/01_environment.md @@ -0,0 +1,20 @@ +# Environment Setup + +## Create the environment + +```bash +python -m venv .venv +.venv\Scripts\activate +pip install -e . +``` + +Requires Python 3.11+. + +## Dependencies + +Development extras include: + +- `pytest`, `pytest-cov` — test suite and coverage +- `ruff` — lint and format checking +- `black` — auto-formatting +- `mypy` — strict typing checks \ No newline at end of file diff --git a/docs/wiki/05_development/02_quality_gates.md b/docs/wiki/05_development/02_quality_gates.md new file mode 100644 index 0000000..d0d2716 --- /dev/null +++ b/docs/wiki/05_development/02_quality_gates.md @@ -0,0 +1,22 @@ +# Quality Gates + +Run all checks before pushing: + +```bash +.venv\Scripts\python.exe -m pytest +.venv\Scripts\python.exe -m ruff check docforge tests +.venv\Scripts\python.exe -m black --check docforge tests +.venv\Scripts\python.exe -m mypy docforge +``` + +## Test layout + +| Path | Covers | +|--------------------------|------------------------------------| +| `tests/nav/` | Nav spec, resolver, wiki nav | +| `tests/cli/` | Build command flows and modes | +| `tests/renderers/` | MkDocs / MCP rendering | + +CLI tests use the `cli_runner` fixture with `mock_mkdocs_build` and +`mock_mkdocs_load_config` so they exercise the full flow without invoking a +real MkDocs build. \ No newline at end of file diff --git a/docs/wiki/index.md b/docs/wiki/index.md new file mode 100644 index 0000000..d581212 --- /dev/null +++ b/docs/wiki/index.md @@ -0,0 +1,39 @@ +# 🔨 docforge — Docstring-Driven Documentation Forge + +`docforge` is an internal documentation tool that generates reference +documentation from Google-Styled Doc-Forge Convention (GSDFC) docstrings and +assembles it into a single MkDocs site, alongside hand-written wiki pages and +OpenAPI-based API docs. + +--- + +## 🚀 Key Features + +- 🧪 GSDFC docstring extraction via Griffe +- 🧭 Auto-derived navigation for hand-written wiki pages +- 🗂 Combined MkDocs build: wiki first, then generated library/API reference +- 🔌 Material MkDocs theming out of the box +- 📦 MCP structured documentation resources + +--- + +## 📁 Documentation Structure + +| Section | Description | +|----------------------------------------------| -------------------------------------------------- | +| [Overview](01_overview.md) | What docforge is and how it fits the pipeline | +| [Architecture](02_architecture.md) | Loaders, models, nav, renderers, CLI | +| [Conventions](03_conventions.md) | GSDFC, `.pyi` stubs, and template conventions | +| [Iterative Workflow](04_iterative_workflow.md) | How a docs build runs end to end | +| [Development](05_development/01_environment.md) | Environment setup and quality gates | + +--- + +## 🔗 Related Resources + +- **Source Code:** `C:\Users\vishe\WorkSpace\code\aetos\doc-forge` +- **Wiki Kind:** hand-written content lives in `docs/wiki/` + +--- + +© Aetoskia Internal \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 87ef16b..917808d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -69,7 +69,14 @@ plugins: site_name: docforge docs_dir: docs nav: -- Home: lib/index.md +- Home: wiki/index.md +- Overview: wiki/01_overview.md +- Architecture: wiki/02_architecture.md +- Conventions: wiki/03_conventions.md +- Iterative Workflow: wiki/04_iterative_workflow.md +- Development: + - Environment: wiki/05_development/01_environment.md + - Quality Gates: wiki/05_development/02_quality_gates.md - Loaders: - lib/loaders/index.md - lib/loaders/griffe_loader.md diff --git a/tests/cli/test_build_wiki.py b/tests/cli/test_build_wiki.py new file mode 100644 index 0000000..5b0827a --- /dev/null +++ b/tests/cli/test_build_wiki.py @@ -0,0 +1,137 @@ +from pathlib import Path + +from docforge.cli.main import cli + + +def test_wiki_only_build_requires_no_module( + cli_runner, + mock_mkdocs_build, + mock_mkdocs_load_config, +): + with cli_runner.isolated_filesystem(): + cwd = Path.cwd() + + wiki = cwd / "docs" / "wiki" + wiki.mkdir(parents=True) + (wiki / "index.md").write_text("# Home", encoding="utf-8") + (wiki / "01_overview.md").write_text("# Overview", encoding="utf-8") + + result = cli_runner.invoke( + cli, + [ + "build", + "--wiki", + "--site-name", + "Wiki Site", + "--mkdocs-yml", + "mkdocs.yml", + ], + ) + + assert result.exit_code == 0, result.output + assert mock_mkdocs_build() is True + assert (cwd / "mkdocs.yml").exists() + + content = (cwd / "mkdocs.yml").read_text(encoding="utf-8") + assert "docs_dir: docs" in content + assert "Home: wiki/index.md" in content + assert "Overview: wiki/01_overview.md" in content + + +def test_wiki_combined_with_mkdocs_keeps_lib_nav( + cli_runner, + mock_mkdocs_build, + mock_mkdocs_load_config, +): + with cli_runner.isolated_filesystem(): + cwd = Path.cwd() + + pkg = cwd / "testpkg" + pkg.mkdir() + (pkg / "__init__.py").write_text("") + (pkg / "mod.py").write_text("def f(): ...\n") + + nav_file = cwd / "docforge.nav.yml" + nav_file.write_text( + "home: lib/testpkg/index.md\ngroups: {}\n", + encoding="utf-8", + ) + + wiki = cwd / "docs" / "wiki" + wiki.mkdir(parents=True) + (wiki / "index.md").write_text("# Home", encoding="utf-8") + + result = cli_runner.invoke( + cli, + [ + "build", + "--mkdocs", + "--wiki", + "--module", + "testpkg", + "--site-name", + "Combined Site", + "--mkdocs-yml", + "mkdocs.yml", + ], + ) + + assert result.exit_code == 0, result.output + assert mock_mkdocs_build() is True + assert (cwd / "docs" / "lib" / "testpkg" / "mod.md").exists() + + content = (cwd / "mkdocs.yml").read_text(encoding="utf-8") + assert "docs_dir: docs" in content + assert "Home: wiki/index.md" in content + # Wiki home replaces the lib home entry + assert "Home: lib/testpkg/index.md" not in content + + +def test_wiki_without_module_flag_ok( + cli_runner, + mock_mkdocs_build, + mock_mkdocs_load_config, +): + with cli_runner.isolated_filesystem(): + cwd = Path.cwd() + + wiki = cwd / "docs" / "wiki" + wiki.mkdir(parents=True) + (wiki / "index.md").write_text("# Home", encoding="utf-8") + + result = cli_runner.invoke( + cli, + ["build", "--wiki", "--site-name", "S"], + ) + + assert result.exit_code == 0, result.output + assert mock_mkdocs_build() is True + + +def test_missing_wiki_dir_errors(cli_runner, mock_mkdocs_build): + with cli_runner.isolated_filesystem(): + cwd = Path.cwd() + (cwd / "docs").mkdir() + + result = cli_runner.invoke( + cli, + [ + "build", + "--wiki", + "--site-name", + "S", + "--wiki-dir", + "docs/wiki", + "--mkdocs-yml", + "mkdocs.yml", + ], + ) + + assert result.exit_code != 0 + assert "Wiki dir not found" in result.output + + +def test_no_flag_raises_usage_error(cli_runner): + result = cli_runner.invoke(cli, ["build"]) + assert result.exit_code != 0 + assert "--mcp, --mkdocs, --wiki, or --api" in result.output diff --git a/tests/nav/test_wiki.py b/tests/nav/test_wiki.py new file mode 100644 index 0000000..5d0fe0c --- /dev/null +++ b/tests/nav/test_wiki.py @@ -0,0 +1,106 @@ +from pathlib import Path + +import pytest + +from docforge.nav import build_wiki_nav + + +def _write_wiki(root: Path, files: list[str]) -> None: + for rel in files: + full = root / rel + full.parent.mkdir(parents=True, exist_ok=True) + full.write_text(f"# {rel}", encoding="utf-8") + + +def test_missing_wiki_dir_raises(tmp_path: Path): + with pytest.raises(FileNotFoundError): + build_wiki_nav(tmp_path / "nope") + + +def test_empty_wiki_dir_returns_empty(tmp_path: Path): + wiki = tmp_path / "wiki" + wiki.mkdir() + + assert build_wiki_nav(wiki) == [] + + +def test_index_becomes_home(tmp_path: Path): + wiki = tmp_path / "docs" / "wiki" + _write_wiki(wiki, ["index.md", "01_overview.md"]) + + nav = build_wiki_nav(wiki) + + assert nav[0] == {"Home": "wiki/index.md"} + assert {"Overview": "wiki/01_overview.md"} in nav + + +def test_number_prefix_stripped_and_prettified(tmp_path: Path): + wiki = tmp_path / "docs" / "wiki" + _write_wiki(wiki, ["01_basic_crud.md", "02_custom_repo.md", "10_transactions.md"]) + + nav = build_wiki_nav(wiki) + + labels = [list(entry)[0] for entry in nav] + assert labels == ["Basic Crud", "Custom Repo", "Transactions"] + + +def test_directory_becomes_nested_group(tmp_path: Path): + wiki = tmp_path / "docs" / "wiki" + _write_wiki( + wiki, + [ + "index.md", + "03_use_cases/01_basic_crud.md", + "03_use_cases/02_custom_repo.md", + ], + ) + + nav = build_wiki_nav(wiki) + + assert {"Home": "wiki/index.md"} in nav + assert { + "Use Cases": [ + {"Basic Crud": "wiki/03_use_cases/01_basic_crud.md"}, + {"Custom Repo": "wiki/03_use_cases/02_custom_repo.md"}, + ] + } in nav + + +def test_nested_index_is_section_root(tmp_path: Path): + wiki = tmp_path / "docs" / "wiki" + _write_wiki( + wiki, + [ + "guide/index.md", + "guide/01_setup.md", + ], + ) + + nav = build_wiki_nav(wiki) + + assert { + "Guide": [ + {"Guide": "wiki/guide/index.md"}, + {"Setup": "wiki/guide/01_setup.md"}, + ] + } in nav + + +def test_hidden_entries_ignored(tmp_path: Path): + wiki = tmp_path / "docs" / "wiki" + _write_wiki(wiki, ["index.md", ".draft.md"]) + (wiki / ".hidden").mkdir() + + nav = build_wiki_nav(wiki) + + assert nav == [{"Home": "wiki/index.md"}] + + +def test_natural_order_for_zero_padded_prefixes(tmp_path: Path): + wiki = tmp_path / "docs" / "wiki" + _write_wiki(wiki, ["02_b.md", "10_j.md", "01_a.md"]) + + nav = build_wiki_nav(wiki) + + labels = [list(entry)[0] for entry in nav if list(entry)[0] in {"A", "B", "J"}] + assert labels == ["A", "B", "J"]