oct
Loading...
Searching...
No Matches
oct.docs.oct_docs Namespace Reference

Functions

bool _is_auto_generated (Path path, str marker)
list[str] _check_sphinx_deps ()
dict _load_project_info (Path root)
dict _load_docs_config (Path root)
list[str] _discover_markdown (Path docs_dir)
list[str] _order_markdown (list[str] stems)
bool _should_skip_dir (Path path, Path root)
list[tuple[str, Path]] _find_packages (Path root, dict config)
bool _generate_api_rst (Path root, Path docs_dir, dict config)
bool _render_dot_svg (str dot_src, Path target)
bool _generate_deps_rst (Path root, Path docs_dir)
None _write_index_rst (Path docs_dir, str project_name, list[str] md_stems, bool has_api, bool has_deps, *, bool include_root_readme=False)
Path setup_sphinx (Path root)
int run_sphinx_build (Path docs_dir, dict config)
int run_docs_sphinx (Path root, *, bool no_workspace=False)
bool _check_doxygen_installed ()
Path|None _find_doxyfile (Path root)
str|None _doxygen_setting (str content, str key)
Path _doxygen_html_output_dir (Path doxyfile_path)
None _clear_doxygen_pages (Path doxyfile_path, Path root)
int _prune_unreferenced_graphs (Path html_dir)
Path _create_patched_doxyfile (Path doxyfile_path, Path root)
int _sweep_stale_doxyfiles (int max_age_seconds=_STALE_DOXYFILE_MAX_AGE_SECONDS)
int run_docs_doxygen (Path root, *, bool no_workspace=False)
int run_docs_clean (Path root, *, bool dry_run=False, bool no_workspace=False)
int run_docs_all (Path root, *, bool no_workspace=False)

Variables

str _TEMPLATES_DIR = Path(__file__).parent.parent / "templates" / "sphinx"
str _AUTO_MARKER_RST = ".. Auto-generated by oct docs --sphinx"
str _AUTO_MARKER_MD = "<!-- Auto-generated by oct docs --sphinx -->"
list _REQUIRED_PACKAGES
dict _DOCS_EXCLUDE_DIRS
list _PRIORITY_DOCS
int _MAX_PACKAGE_DEPTH = 10
Path _DEPS_ASSET_REL = Path("_build") / "deps"
tuple _DOXYGEN_HTML_MARKERS
frozenset _DOT_CACHE_SUFFIXES = frozenset({".svg", ".md5", ".map"})
int _STALE_DOXYFILE_MAX_AGE_SECONDS = 24 * 60 * 60
list _DOCS_CLEAN_DIRS = ["html", "doctrees", "_api", "doxygen_output", "typedoc"]
list _DOCS_CLEAN_FILES = ["index.rst", "dependencies.rst"]

Detailed Description

Purpose
-------
Provide documentation generation pipelines for Option C projects:
``oct docs --sphinx``, ``oct docs --doxygen``, ``oct docs --all``, and ``oct docs --clean``.

Responsibilities
----------------
- Set up Sphinx configuration in the project's ``docs/`` directory (if not already present).
- Discover markdown documentation files already in ``docs/``.
- Generate API reference pages from Python module docstrings via autodoc.
- Generate dependency graph pages via ``oct deps``.
- Populate ``index.rst`` with all discovered content.
- Run ``sphinx-build`` to generate HTML output.
- Check for required Sphinx dependencies and emit a clear error if missing.
- Run Doxygen with a patched Doxyfile (correcting stale INPUT paths).
- Clean generated documentation artifacts while preserving user-authored files.

Diagnostics
-----------
Domain: OCT-DOCS
Levels:
    L2 - lifecycle
    L3 - semantic details
    L4 - deep tracing

Contracts
---------
- Must not overwrite existing ``oct/docs/_sphinx/conf.py`` if already present.
- ``oct/docs/_sphinx/index.rst`` is regenerated on every build (auto-generated content).
- Must exit 1 with a helpful message if Sphinx dependencies are not installed.
- Template files are bundled in ``oct/templates/sphinx/``.
- Doxygen runs on a temporary copy of the Doxyfile; the original is never modified.
- ``--clean`` preserves ``_sphinx/``, ``_static/``, ``_templates/``, ``doxygen_work_dir/``, and user-authored markdown files.

Dependencies
------------
- oct.core.exclusions (directory exclusion lists)
- oct.core.diagnostics (_dbg structured logger)
- oct.tools.fs_remove (remove_tree_or_warn - removes generated output, warning on a tree that refuses to go)
- oct.core.option_c_dir (lazy; resolve_octrc - finds the octrc that holds the docs settings)
- oct.deps.import_graph (lazy; build_import_graph and its renderers - the dependencies page)
- oct.docs.doxyfile_init (lazy; DOXYFILE_REL - where a project's Doxyfile lives)
- oct.docs.typedoc (lazy; has_typedoc, run_docs_typedoc - the TypeDoc step of --all)
- oct.docs.workspace_docs (lazy; is_workspace_root, run_workspace_docs - the workspace fan-out)
- sphinx.ext.apidoc (lazy; main - writes the per-module API pages)
- tomli (lazy; reads pyproject.toml on Python 3.10, where tomllib is absent)

Function Documentation

◆ _check_doxygen_installed()

bool oct.docs.oct_docs._check_doxygen_installed ( )
protected
Check if doxygen is on PATH via shutil.which().

Definition at line 818 of file oct_docs.py.

Here is the caller graph for this function:

◆ _check_sphinx_deps()

list[str] oct.docs.oct_docs._check_sphinx_deps ( )
protected
Check which Sphinx dependencies are missing.

Definition at line 129 of file oct_docs.py.

Here is the caller graph for this function:

◆ _clear_doxygen_pages()

None oct.docs.oct_docs._clear_doxygen_pages ( Path doxyfile_path,
Path root )
protected
Delete the previous run's pages but keep the graph cache.

Doxygen regenerates every page it still has an input for, but it
never deletes the page for an input it no longer sees. Excluding a
file therefore leaves its generated page on disk indefinitely: the
navigation stops linking it, yet it keeps being copied into the
published site and keeps answering at its old URL. Clearing the pages
first is what makes the output mean "what the current configuration
produces".

The graphs are deliberately NOT cleared. Doxygen keys every dot graph
on an md5 of its source and skips ``dot`` when the rendered file
already matches, and that cache is most of the build: oc_pricing
rebuilds in 0.8 s with 0 dot invocations when the cache is present
and in 79 s with 304 when it is not (measured 2026-09-27). The
2026-09-26 version of this function removed the whole directory and
so threw the cache away on every build - oct's 3,185 graphs were
being re-rendered from scratch each time. Graphs whose page no
longer exists are pruned after the run by
:func:`_prune_unreferenced_graphs`, which is what keeps the original
orphan problem solved.

Two guards, because this deletes inside a directory whose location
comes from a config file: the target must resolve inside *root*, and
it must look like Doxygen's own output. Anything else is left alone
with a warning - a stale page is a much smaller problem than a
deleted source tree.

Failure is never fatal. Whatever cannot be removed is reported, and
the build still runs.

Definition at line 886 of file oct_docs.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _create_patched_doxyfile()

Path oct.docs.oct_docs._create_patched_doxyfile ( Path doxyfile_path,
Path root )
protected
Create a temporary copy of the Doxyfile with INPUT patched to actual root.

The existing Doxyfiles have hardcoded stale paths.  This creates a temp
copy with the correct INPUT path.  Does NOT modify the original.

Returns the path to the temp Doxyfile.

Definition at line 1007 of file oct_docs.py.

Here is the caller graph for this function:

◆ _discover_markdown()

list[str] oct.docs.oct_docs._discover_markdown ( Path docs_dir)
protected
Discover markdown files in the docs directory, recursing into
subdirectories that are not in :data:`_DOCS_EXCLUDE_DIRS`.

Returns a sorted list of file identifiers relative to ``docs_dir`` and
stripped of the ``.md`` extension. For files in the docs/ root these
identifiers are bare stems (e.g. ``"ARCHITECTURE"``); for files in
subdirectories they are POSIX-form relative paths
(e.g. ``"option_c_specs/00_Master_Option_C_Specs_Intro_V3_5"``).
Sphinx-MyST consumes both forms directly in the toctree.

Definition at line 229 of file oct_docs.py.

Here is the caller graph for this function:

◆ _doxygen_html_output_dir()

Path oct.docs.oct_docs._doxygen_html_output_dir ( Path doxyfile_path)
protected
Resolve the HTML output directory *doxyfile_path* writes to.

Doxygen resolves a relative ``OUTPUT_DIRECTORY`` against its working
directory, and this module runs it with ``cwd=doxyfile.parent``, so
that is the base used here. ``HTML_OUTPUT`` is then resolved against
``OUTPUT_DIRECTORY``. Both default the way Doxygen defaults them.

Definition at line 863 of file oct_docs.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _doxygen_setting()

str | None oct.docs.oct_docs._doxygen_setting ( str content,
str key )
protected
Return the value of Doxyfile tag *key*, or ``None`` when unset.

Only single-line assignments are read, which is what both settings
this module cares about (``OUTPUT_DIRECTORY``, ``HTML_OUTPUT``) are
in every Doxyfile Doxygen itself generates. Surrounding quotes are
stripped; a tag present but empty reads as ``None``.

Definition at line 846 of file oct_docs.py.

Here is the caller graph for this function:

◆ _find_doxyfile()

Path | None oct.docs.oct_docs._find_doxyfile ( Path root)
protected
Locate the project's Doxyfile if it exists.

The path is :data:`oct.docs.doxyfile_init.DOXYFILE_REL`, shared with the
workspace fan-out, the hub card filter and the scaffold so that "has a
Doxyfile" means the same thing everywhere.

Definition at line 823 of file oct_docs.py.

Here is the caller graph for this function:

◆ _find_packages()

list[tuple[str, Path]] oct.docs.oct_docs._find_packages ( Path root,
dict config )
protected
Discover Python packages to document.

Walks the project tree recursively, stopping at each package (a
directory containing ``__init__.py``) rather than descending into
its subpackages - Sphinx autodoc handles subpackages automatically.
Respects ``LINTER_EXCLUDE_DIRS`` and ``WILDCARD_EXCLUDE_DIRS`` via
:func:`_should_skip_dir`.

Bounded by :data:`_MAX_PACKAGE_DEPTH` to prevent pathological walks
on misconfigured projects (OI-426).

Returns list of ``(package_name, package_path)`` tuples.

Definition at line 308 of file oct_docs.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _generate_api_rst()

bool oct.docs.oct_docs._generate_api_rst ( Path root,
Path docs_dir,
dict config )
protected
Generate per-module API documentation using sphinx-apidoc.

Uses sphinx-apidoc to create comprehensive .rst files for every
module, class, and function - similar to Doxygen's EXTRACT_ALL output.

Returns True if at least one package was documented.

Definition at line 354 of file oct_docs.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _generate_deps_rst()

bool oct.docs.oct_docs._generate_deps_rst ( Path root,
Path docs_dir )
protected
Generate the ``dependencies`` page: the subproject's dependency overview.

Two levels on one page (plan A, docs/DOCS_OVERVIEW_CHARTS_PLAN_2026-09-27.md):
a package-level Mermaid chart a person can read in one look, and the
full module-level graph rendered by Graphviz to a zoomable SVG. Both are
derived from the ``import`` statements, not the ``Dependencies``
docstring declarations that ``oct deps`` reports, because a structural
picture that can drift from the code is worse than none.

Returns True when a page was written (the graph has at least one edge),
so the caller can add it to the toctree.

Definition at line 448 of file oct_docs.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _is_auto_generated()

bool oct.docs.oct_docs._is_auto_generated ( Path path,
str marker )
protected
Return True if *path* is absent or was written by ``oct docs``.

OI-518: used as a write-safety guard around regeneration of
``docs/README.md``, ``docs/index.rst``, ``docs/dependencies.rst``,
and ``docs/_api/modules.rst``. A user-authored file (no marker) is
left untouched and a warning is emitted instead.

Definition at line 79 of file oct_docs.py.

Here is the caller graph for this function:

◆ _load_docs_config()

dict oct.docs.oct_docs._load_docs_config ( Path root)
protected
Load docs configuration from the project's octrc with defaults (Appendix H).

Resolved through :func:`oct.core.option_c_dir.resolve_octrc`, which prefers
``.option_c/octrc.json`` and falls back to the legacy ``<root>/.octrc.json``.
This function used to hardcode the legacy path, so on a MIGRATED project --
which has no root ``.octrc.json`` -- the whole ``docs`` block was silently
ignored and the defaults below were always used. ``oct`` itself is migrated,
so its own configured ``autodoc_modules`` was being dropped: discovery fell
back to walking the tree, which picked up the stale ``build/lib/oct`` copy
alongside the real package and ran ``sphinx-apidoc`` over both.

Definition at line 187 of file oct_docs.py.

Here is the caller graph for this function:

◆ _load_project_info()

dict oct.docs.oct_docs._load_project_info ( Path root)
protected
Load project name, version, and author from pyproject.toml or defaults.

Definition at line 146 of file oct_docs.py.

Here is the caller graph for this function:

◆ _order_markdown()

list[str] oct.docs.oct_docs._order_markdown ( list[str] stems)
protected
Order markdown identifiers with priority docs first, then alphabetical.

Identifiers may be bare stems (root-level docs) or
``subdir/.../stem`` paths (subdirectory docs). Priority matching
compares against the basename so ``_PRIORITY_DOCS`` entries can stay
leaf-name-based even as files move into subdirectories.

Definition at line 253 of file oct_docs.py.

Here is the caller graph for this function:

◆ _prune_unreferenced_graphs()

int oct.docs.oct_docs._prune_unreferenced_graphs ( Path html_dir)
protected
Remove graph files no page in *html_dir* embeds; return the count.

The complement of :func:`_clear_doxygen_pages`: pages are always
rebuilt fresh, so after a run the set of ``.svg`` files the pages
reference is exactly the set of live graphs. Everything else in the
dot cache belongs to a function or class Doxygen no longer sees and
is removed - the rendered ``.svg``, the ``_org.svg`` original that
``INTERACTIVE_SVG`` keeps beside it, and the ``.map`` and ``.md5``
that would otherwise keep it "cached" forever.

Doxygen's own non-graph SVGs (``doxygen.svg``, the legend and the
theme's icons) are referenced from the pages like any graph and so
survive this on the same rule, with no special-casing.

Definition at line 966 of file oct_docs.py.

Here is the caller graph for this function:

◆ _render_dot_svg()

bool oct.docs.oct_docs._render_dot_svg ( str dot_src,
Path target )
protected
Render *dot_src* to *target* with Graphviz; False when dot is absent or fails.

Definition at line 426 of file oct_docs.py.

Here is the caller graph for this function:

◆ _should_skip_dir()

bool oct.docs.oct_docs._should_skip_dir ( Path path,
Path root )
protected
Check if a directory should be excluded from module discovery.

Definition at line 286 of file oct_docs.py.

Here is the caller graph for this function:

◆ _sweep_stale_doxyfiles()

int oct.docs.oct_docs._sweep_stale_doxyfiles ( int max_age_seconds = _STALE_DOXYFILE_MAX_AGE_SECONDS)
protected
Remove orphaned ``oct-doxyfile-*.Doxyfile`` temp files.

The ``finally`` block in :func:`run_docs_doxygen` cleans up on normal
exit, exceptions, and ``FileNotFoundError`` - but it is skipped on
``SIGKILL``, power loss, or OOM-kill. Without this sweep the system
temp directory would slowly accumulate orphaned Doxyfiles (OI-423).

Best-effort: any OS error (per-file or directory-wide) is silently
swallowed so that a failed sweep never blocks a legitimate doxygen
build.

Returns the number of files removed.

Definition at line 1046 of file oct_docs.py.

Here is the caller graph for this function:

◆ _write_index_rst()

None oct.docs.oct_docs._write_index_rst ( Path docs_dir,
str project_name,
list[str] md_stems,
bool has_api,
bool has_deps,
* ,
bool include_root_readme = False )
protected
Write a populated index.rst with discovered content.

With *include_root_readme* the project's root ``README.md`` is rendered
in place at the top of the index through docutils ``include`` with the
MyST parser, so the landing page opens with the project's own
introduction and no copy of the README is written into ``docs/``.

Definition at line 545 of file oct_docs.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ run_docs_all()

int oct.docs.oct_docs.run_docs_all ( Path root,
* ,
bool no_workspace = False )
Run both Sphinx and Doxygen pipelines.

Returns 0 if both succeed, 1 if either fails.

Workspace mode (FS-WS-DOCS): from a workspace root, dispatches to
:func:`oct.docs.workspace_docs.run_workspace_docs` with
``mode="all"``.

Definition at line 1253 of file oct_docs.py.

Here is the call graph for this function:

◆ run_docs_clean()

int oct.docs.oct_docs.run_docs_clean ( Path root,
* ,
bool dry_run = False,
bool no_workspace = False )
Remove generated documentation artifacts from docs/.

Cleans: html/, doctrees/, _api/, doxygen_output/, typedoc/,
        index.rst, dependencies.rst

Preserves: _sphinx/ (user conf.py), _static/, _templates/,
           user-authored .md files, doxygen_work_dir/Doxyfile

Returns 0 when the artifacts are gone (or when there were none, or under
*dry_run*), and 1 when one or more could not be removed - the count it
prints is of directories verified absent afterwards, never of removal
calls made, so a refused delete cannot report as a clean.

Workspace mode (FS-WS-DOCS): from a workspace root, dispatches to
:func:`oct.docs.workspace_docs.run_workspace_docs` with
``mode="clean"`` (Phase F implementation). ``--no-workspace`` falls
back to single-project clean.

Definition at line 1168 of file oct_docs.py.

Here is the call graph for this function:

◆ run_docs_doxygen()

int oct.docs.oct_docs.run_docs_doxygen ( Path root,
* ,
bool no_workspace = False )
Full oct docs --doxygen pipeline.

1. Check doxygen is installed (shutil.which)
2. Find existing Doxyfile in docs/doxygen_work_dir/
3. Create patched temp copy with correct INPUT
4. Run doxygen on the temp Doxyfile
5. Clean up temp Doxyfile
6. Report output location (docs/doxygen_output/)

Returns exit code: 0 on success, 1 on failure.

Workspace mode (FS-WS-DOCS): when ``root`` is a workspace root and
*no_workspace* is ``False``, dispatches to
:func:`oct.docs.workspace_docs.run_workspace_docs`.

Definition at line 1078 of file oct_docs.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ run_docs_sphinx()

int oct.docs.oct_docs.run_docs_sphinx ( Path root,
* ,
bool no_workspace = False )
Full oct docs --sphinx pipeline.

1. Check dependencies
2. Set up Sphinx config (one-time)
3. Load .octrc.json docs configuration
4. Discover markdown files
5. Generate API reference (autodoc)
6. Generate dependency graph
7. Write populated index.rst
8. Run sphinx-build

Returns exit code.

Workspace mode (FS-WS-DOCS): when ``root`` is a workspace root
(``.option_c_workspace.json`` present) and *no_workspace* is
``False``, dispatches to
:func:`oct.docs.workspace_docs.run_workspace_docs` for fan-out
+ aggregation. Subproject invocations and ``--no-workspace``
keep the existing single-project body.

Definition at line 727 of file oct_docs.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ run_sphinx_build()

int oct.docs.oct_docs.run_sphinx_build ( Path docs_dir,
dict config )
Run sphinx-build to generate HTML output.

Returns exit code: 0 on success, 1 on failure.

Definition at line 686 of file oct_docs.py.

Here is the caller graph for this function:

◆ setup_sphinx()

Path oct.docs.oct_docs.setup_sphinx ( Path root)
Set up Sphinx configuration in the project's docs/ directory.

Uses docs/ as the Sphinx source directory directly - markdown files
already present in docs/ are included without copying.

Sphinx config files (conf.py, Makefile, make.bat) live in docs/_sphinx/
to keep the docs/ root clean.  The ``-c`` flag in ``run_sphinx_build``
points Sphinx at this configuration directory.

Returns the path to the docs directory.

Definition at line 621 of file oct_docs.py.

Here is the call graph for this function:
Here is the caller graph for this function:

Variable Documentation

◆ _AUTO_MARKER_MD

str oct.docs.oct_docs._AUTO_MARKER_MD = "<!-- Auto-generated by oct docs --sphinx -->"
protected

Definition at line 76 of file oct_docs.py.

◆ _AUTO_MARKER_RST

str oct.docs.oct_docs._AUTO_MARKER_RST = ".. Auto-generated by oct docs --sphinx"
protected

Definition at line 75 of file oct_docs.py.

◆ _DEPS_ASSET_REL

Path oct.docs.oct_docs._DEPS_ASSET_REL = Path("_build") / "deps"
protected

Definition at line 423 of file oct_docs.py.

◆ _DOCS_CLEAN_DIRS

list oct.docs.oct_docs._DOCS_CLEAN_DIRS = ["html", "doctrees", "_api", "doxygen_output", "typedoc"]
protected

Definition at line 1162 of file oct_docs.py.

◆ _DOCS_CLEAN_FILES

list oct.docs.oct_docs._DOCS_CLEAN_FILES = ["index.rst", "dependencies.rst"]
protected

Definition at line 1165 of file oct_docs.py.

◆ _DOCS_EXCLUDE_DIRS

dict oct.docs.oct_docs._DOCS_EXCLUDE_DIRS
protected
Initial value:
1= {"old", "code_reviews", "sphinx_docs", "_build",
2 "_static", "_templates", "_autosummary", "_sphinx",
3 "_api", "html", "doctrees", "doxygen_output",
4 "doxygen_work_dir"}

Definition at line 105 of file oct_docs.py.

◆ _DOT_CACHE_SUFFIXES

frozenset oct.docs.oct_docs._DOT_CACHE_SUFFIXES = frozenset({".svg", ".md5", ".map"})
protected

Definition at line 883 of file oct_docs.py.

◆ _DOXYGEN_HTML_MARKERS

tuple oct.docs.oct_docs._DOXYGEN_HTML_MARKERS
protected
Initial value:
1= (
2 "index.html", "doxygen.css", "doxygen.svg", "doxygen_crawl.html",
3)

Definition at line 841 of file oct_docs.py.

◆ _MAX_PACKAGE_DEPTH

int oct.docs.oct_docs._MAX_PACKAGE_DEPTH = 10
protected

Definition at line 305 of file oct_docs.py.

◆ _PRIORITY_DOCS

list oct.docs.oct_docs._PRIORITY_DOCS
protected
Initial value:
1= [
2 "README",
3 "00_Master_Option_C_Specs_Intro_",
4 "01_Master_Option_C_Specs_Core_",
5 "02_Master_Option_C_Specs_Tooling_",
6 "03_Master_Option_C_Specs_Diagnostics_",
7 "04_Master_Option_C_Specs_AI_Protocol_",
8 "05_Master_Option_C_Specs_Changelog_",
9 "ARCHITECTURE",
10 "REFERENCE",
11 "ROADMAP",
12]

Definition at line 113 of file oct_docs.py.

◆ _REQUIRED_PACKAGES

list oct.docs.oct_docs._REQUIRED_PACKAGES
protected
Initial value:
1= [
2 "sphinx",
3 "sphinx_book_theme",
4 "myst_parser",
5 "sphinxcontrib.mermaid",
6 "sphinx_copybutton",
7]

Definition at line 96 of file oct_docs.py.

◆ _STALE_DOXYFILE_MAX_AGE_SECONDS

int oct.docs.oct_docs._STALE_DOXYFILE_MAX_AGE_SECONDS = 24 * 60 * 60
protected

Definition at line 1043 of file oct_docs.py.

◆ _TEMPLATES_DIR

str oct.docs.oct_docs._TEMPLATES_DIR = Path(__file__).parent.parent / "templates" / "sphinx"
protected

Definition at line 69 of file oct_docs.py.