From 16b8cbb11a821f381f1b934f11e3ba7c6fc1c22f Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Tue, 14 Apr 2026 12:19:33 +0100 Subject: [PATCH 01/11] docs: add CLI reference page and remove CLI from API docs (#704) Remove cli/ from the generated API reference (it's a user tool, not a public Python API) and replace it with an auto-generated CLI Reference page under docs/docs/reference/cli.md. The new generate_cli_reference.py script introspects the Typer app at build time, extracting flags, types, defaults, help strings, and structured docstring sections (Prerequisites, Output, Examples, See Also). Strict mode (--strict) fails the build if any command has incomplete documentation. - Remove "cli" from PACKAGES in generate-ast.py - Remove cli from audit_coverage.py discovery and quality scope - Add generate_cli_reference.py with strict validation - Integrate into build.py (Step 4, --strict) and CI workflow - Add 21 unit tests for the generator (test_cli_reference.py) - Wire docs-autogen tests into docs-publish CI workflow - Enrich all CLI command docstrings with Prerequisites, Output, Examples, and See Also sections - Add cross-links from 5 guide pages back to CLI reference - Add 12 Mintlify redirects for old /api/cli/* URLs - Remove dead m decompose glossary entry - Add CLI docstring convention to CONTRIBUTING.md and AGENTS.md - Add clidocs/clidocs-clean poe tasks --- .github/workflows/docs-publish.yml | 5 + .gitignore | 3 +- AGENTS.md | 1 + cli/alora/commands.py | 56 ++- cli/decompose/decompose.py | 26 +- cli/eval/commands.py | 48 +- cli/fix/commands.py | 31 +- cli/m.py | 11 +- cli/serve/app.py | 22 +- docs/docs/advanced/lora-and-alora-adapters.md | 3 +- docs/docs/docs.json | 51 +- .../evaluate-with-llm-as-a-judge.md | 3 +- docs/docs/guide/CONTRIBUTING.md | 64 +++ docs/docs/guide/glossary.md | 16 - docs/docs/guide/m-decompose.md | 2 +- docs/docs/how-to/refactor-prompts-with-cli.md | 2 + docs/docs/integrations/m-serve.md | 3 +- pyproject.toml | 8 + tooling/docs-autogen/audit_coverage.py | 17 +- tooling/docs-autogen/build.py | 25 + tooling/docs-autogen/generate-ast.py | 6 +- .../docs-autogen/generate_cli_reference.py | 471 ++++++++++++++++++ tooling/docs-autogen/test_cli_reference.py | 201 ++++++++ 23 files changed, 1021 insertions(+), 54 deletions(-) create mode 100644 tooling/docs-autogen/generate_cli_reference.py create mode 100644 tooling/docs-autogen/test_cli_reference.py diff --git a/.github/workflows/docs-publish.yml b/.github/workflows/docs-publish.yml index f8694702f..81a8f24ce 100644 --- a/.github/workflows/docs-publish.yml +++ b/.github/workflows/docs-publish.yml @@ -81,6 +81,11 @@ jobs: - name: Generate API documentation run: uv run python tooling/docs-autogen/build.py + # -- Run docs-autogen unit tests ------------------------------------------ + + - name: Run docs-autogen tests + run: uv run pytest tooling/docs-autogen/ -v --tb=short + # -- Validate static docs ------------------------------------------------ - name: Lint static docs (markdownlint) diff --git a/.gitignore b/.gitignore index 448955a09..86a08bc82 100644 --- a/.gitignore +++ b/.gitignore @@ -455,7 +455,8 @@ pyrightconfig.json .claude/* !.claude/settings.json -# Generated API documentation (built by tooling/docs-autogen/) +# Generated documentation (built by tooling/docs-autogen/) docs/docs/api/ docs/docs/api-reference.mdx +docs/docs/reference/cli.md .venv-docs-autogen/ diff --git a/AGENTS.md b/AGENTS.md index 7e6933783..fabbdd2a9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -88,6 +88,7 @@ mkdir -p .bob && ln -s ../.agents/skills .bob/skills - Use `...` in `@generative` function bodies - Prefer primitives over classes - **Friendly Dependency Errors**: Wraps optional backend imports in `try/except ImportError` with a helpful message (e.g., "Please pip install mellea[hf]"). See `mellea/stdlib/session.py` for examples. +- **CLI command docstrings**: Typer command functions in `cli/` follow an enriched convention with `Prerequisites:` and `See Also:` sections — these feed the auto-generated CLI reference page. See [`docs/docs/guide/CONTRIBUTING.md`](docs/docs/guide/CONTRIBUTING.md) for the full pattern. Regenerate after changes: `uv run poe clidocs`. - **Backend telemetry fields**: All backends must populate `mot.usage` (dict with `prompt_tokens`, `completion_tokens`, `total_tokens`), `mot.model` (str), and `mot.provider` (str) in their `post_processing()` method. `mot.streaming` (bool) and `mot.ttfb_ms` (float | None) are set automatically in `astream()` — backends do not need to set them. Metrics are automatically recorded by `TokenMetricsPlugin` and `LatencyMetricsPlugin` — don't add manual `record_token_usage_metrics()` or `record_request_duration()` calls. ## 6. Commits & Hooks diff --git a/cli/alora/commands.py b/cli/alora/commands.py index 4b2f537e3..fecc396d2 100644 --- a/cli/alora/commands.py +++ b/cli/alora/commands.py @@ -31,7 +31,25 @@ def alora_train( max_length: int = typer.Option(1024, help="Max sequence length"), grad_accum: int = typer.Option(4, help="Gradient accumulation steps"), ): - """Train an aLoRA or LoRA model on your dataset. + """Train an aLoRA or LoRA adapter on a labelled dataset. + + Fine-tunes a base causal language model using a JSONL dataset of item/label + pairs. Supports both aLoRA (asymmetric LoRA) and standard LoRA adapters. + + Prerequisites: + Mellea installed with adapter extras (``uv add mellea[adapters]``). + A CUDA, MPS, or CPU device available for training. + + Output: + Saves adapter weights to the path specified by ``--outfile``. The output + directory contains an ``adapter_config.json`` and the trained weight + files, ready for upload or local inference. + + Examples: + m alora train data.jsonl --basemodel ibm-granite/granite-3.3-2b-instruct --outfile ./adapter + + See Also: + guide: advanced/lora-and-alora-adapters Args: datafile: JSONL file with item/label pairs for training. @@ -79,7 +97,23 @@ def alora_upload( "processing if the model is invoked as an intrinsic.", ), ): - """Upload trained adapter to remote model registry. + """Upload a trained adapter to a remote model registry. + + Pushes adapter weights to Hugging Face Hub, optionally packaging the adapter + as an intrinsic with an ``io.yaml`` configuration file. + + Prerequisites: + Hugging Face CLI authenticated (``huggingface-cli login``). + + Output: + Creates or updates a Hugging Face Hub repository at the name specified + by ``--name`` and uploads the adapter weight files. + + Examples: + m alora upload ./adapter --name acme/my-alora + + See Also: + guide: advanced/lora-and-alora-adapters Args: weight_path: Path to saved adapter weights directory. @@ -140,6 +174,24 @@ def alora_add_readme( ): """Generate and upload an INTRINSIC_README.md for a trained adapter. + Uses an LLM to auto-generate documentation for a trained adapter based on + the training data and model configuration, then uploads it to the Hugging + Face Hub repository. + + Prerequisites: + Hugging Face CLI authenticated (``huggingface-cli login``). + An LLM backend available for README generation. + + Output: + Generates a README.md file, displays it for confirmation, and uploads + it to the Hugging Face Hub repository specified by ``--name``. + + Examples: + m alora add-readme data.jsonl --basemodel ibm-granite/granite-3.3-2b-instruct --name acme/my-alora + + See Also: + guide: advanced/lora-and-alora-adapters + Args: datafile: JSONL file with item/label pairs used to train the adapter. basemodel: Base model ID or path. diff --git a/cli/decompose/decompose.py b/cli/decompose/decompose.py index 9d2572a28..69b1594d2 100644 --- a/cli/decompose/decompose.py +++ b/cli/decompose/decompose.py @@ -236,12 +236,28 @@ def run( ), ] = False, ) -> None: - """Runs the ``m decompose`` CLI workflow and writes generated outputs. + """Break a complex task into ordered, executable subtasks. - Reads user queries from a file or interactive input, runs the decomposition - pipeline for each task job, and writes one JSON file, one rendered Python - program, and any generated validation modules under a per-job output - directory. + Reads user queries from a file or interactive input, runs the LLM-driven + decomposition pipeline for each task job, and writes one JSON file, one + rendered Python script, and any generated validation modules under a per-job + output directory. + + Prerequisites: + Mellea installed (``uv add mellea``). An Ollama instance running locally, + or an OpenAI-compatible endpoint configured via ``--backend-endpoint``. + + Output: + Creates a directory ``//`` containing a JSON + decomposition result file, a ready-to-run Python script, and any + generated validation modules. One directory per task job. + + Examples: + m decompose run --out-dir ./output --input-file tasks.txt + + See Also: + guide: guide/m-decompose + guide: how-to/refactor-prompts-with-cli Args: out_dir: Existing directory under which per-job output directories are diff --git a/cli/eval/commands.py b/cli/eval/commands.py index ec778a234..8525c6989 100644 --- a/cli/eval/commands.py +++ b/cli/eval/commands.py @@ -4,22 +4,38 @@ import typer -eval_app = typer.Typer(name="eval") +eval_app = typer.Typer(name="eval", help="LLM-as-a-judge evaluation pipelines.") def eval_run( test_files: list[str] = typer.Argument( ..., help="List of paths to json/jsonl files containing test cases" ), - backend: str = typer.Option("ollama", "--backend", "-b", help="Generation backend"), - model: str = typer.Option(None, "--model", help="Generation model name"), + backend: str = typer.Option( + "ollama", + "--backend", + "-b", + help="Inference backend for generating candidate responses (e.g. ollama, openai)", + ), + model: str = typer.Option( + None, + "--model", + help="Model name/id for the generation backend; uses backend default if omitted", + ), max_gen_tokens: int = typer.Option( 256, "--max-gen-tokens", help="Max tokens to generate for responses" ), judge_backend: str = typer.Option( - None, "--judge-backend", "-jb", help="Judge backend" + None, + "--judge-backend", + "-jb", + help="Inference backend for the judge model; reuses --backend if omitted", + ), + judge_model: str = typer.Option( + None, + "--judge-model", + help="Model name/id for the judge; uses judge backend default if omitted", ), - judge_model: str = typer.Option(None, "--judge-model", help="Judge model name"), max_judge_tokens: int = typer.Option( 256, "--max-judge-tokens", help="Max tokens for the judge model's judgement." ), @@ -29,7 +45,11 @@ def eval_run( output_format: str = typer.Option( "json", "--output-format", help="Either json or jsonl format for results" ), - continue_on_error: bool = typer.Option(True, "--continue-on-error"), + continue_on_error: bool = typer.Option( + True, + "--continue-on-error", + help="Skip failed test cases instead of aborting the entire run", + ), ): """Run LLM-as-a-judge evaluation on one or more test files. @@ -37,6 +57,22 @@ def eval_run( the specified generation backend, scores them with a judge model, and writes aggregated results to a file. + Prerequisites: + Mellea installed (``uv add mellea``). At least one inference backend + available (Ollama by default). A separate judge backend/model is + recommended but optional (defaults to the generation backend). + + Output: + Writes evaluation results to ``.`` (default + ``eval_results.json``). The file contains per-test-case scores, judge + verdicts, and aggregate statistics. + + Examples: + m eval run tests.jsonl --backend ollama --model granite3.3:2b + + See Also: + guide: evaluation-and-observability/evaluate-with-llm-as-a-judge + Args: test_files: Paths to JSON/JSONL files containing test cases. backend: Generation backend name. diff --git a/cli/fix/commands.py b/cli/fix/commands.py index c2155efa6..233a4e458 100644 --- a/cli/fix/commands.py +++ b/cli/fix/commands.py @@ -16,7 +16,21 @@ def fix_async( False, "--dry-run", help="Report locations without modifying files" ), ): - """Fix async calls (aact, ainstruct, aquery) for the await_result default change. + """Fix async calls for the await_result default change. + + Scans Python source files for ``aact``, ``ainstruct``, and ``aquery`` calls + and applies an automated migration to restore blocking behaviour after the + ``await_result`` default changed from ``True`` to ``False``. + + Prerequisites: + Mellea installed (``uv add mellea``). + + Output: + Modifies Python source files in place (unless ``--dry-run``). Prints a + summary of fixed call sites with file paths and line numbers. + + Examples: + m fix async src/ --dry-run Args: path: File or directory to scan. @@ -83,7 +97,20 @@ def fix_genslots( False, "--dry-run", help="Report locations without modifying files" ), ): - """Rewrite old genslot imports and class names to genstub equivalents. + """Rewrite genslot imports and class names to genstub equivalents. + + Scans Python source files and replaces deprecated ``GenerativeSlot`` imports + and class references with their ``GenerativeStub`` replacements. + + Prerequisites: + Mellea installed (``uv add mellea``). + + Output: + Modifies Python source files in place (unless ``--dry-run``). Prints a + summary of rewritten references with file paths and line numbers. + + Examples: + m fix genslots src/ --dry-run Args: path: File or directory to scan. diff --git a/cli/m.py b/cli/m.py index 504c2dbf9..8d0f0acab 100644 --- a/cli/m.py +++ b/cli/m.py @@ -23,8 +23,15 @@ def callback() -> None: """Mellea command-line tool for LLM-powered workflows. Provides sub-commands for serving models (``m serve``), training and uploading - adapters (``m alora``), decomposing tasks into subtasks (``m decompose``), and - running test-based evaluation pipelines (``m eval``). + adapters (``m alora``), decomposing tasks into subtasks (``m decompose``), + running test-based evaluation pipelines (``m eval``), and applying automated + code migrations (``m fix``). + + Prerequisites: + Mellea installed (``uv add mellea``). + + See Also: + guide: getting-started/quickstart """ diff --git a/cli/serve/app.py b/cli/serve/app.py index b4613b9cf..37cc359ca 100644 --- a/cli/serve/app.py +++ b/cli/serve/app.py @@ -221,7 +221,27 @@ def serve( host: str = typer.Option("0.0.0.0", help="Host to bind to"), port: int = typer.Option(8080, help="Port to bind to"), ): - """Serve a FastAPI endpoint for a given script.""" + """Serve a Mellea program as an OpenAI-compatible HTTP endpoint. + + Loads a Python script containing a Mellea generative function and exposes it + via a FastAPI server implementing the OpenAI chat completions API. The server + accepts ``POST /v1/chat/completions`` requests. + + Prerequisites: + Mellea installed (``uv add mellea``). The target script must define at + least one generative function. + + Output: + Starts a long-running HTTP server on the specified host and port. + The ``/v1/chat/completions`` endpoint accepts OpenAI-format chat + completion requests and returns ``ChatCompletion`` JSON responses. + + Examples: + m serve my_app.py --port 9000 + + See Also: + guide: integrations/m-serve + """ module = load_module_from_path(script_path) route_path = "/v1/chat/completions" diff --git a/docs/docs/advanced/lora-and-alora-adapters.md b/docs/docs/advanced/lora-and-alora-adapters.md index d32e2c395..efc51b0f9 100644 --- a/docs/docs/advanced/lora-and-alora-adapters.md +++ b/docs/docs/advanced/lora-and-alora-adapters.md @@ -158,4 +158,5 @@ affect other sessions. **See also:** [Intrinsics](./intrinsics) | [The Requirements System](../concepts/requirements-system) | -[Write Custom Verifiers](../how-to/write-custom-verifiers) +[Write Custom Verifiers](../how-to/write-custom-verifiers) | +[CLI Reference](../reference/cli) diff --git a/docs/docs/docs.json b/docs/docs/docs.json index e196e8df5..7edee8982 100644 --- a/docs/docs/docs.json +++ b/docs/docs/docs.json @@ -136,7 +136,8 @@ { "group": "Reference", "pages": [ - "guide/glossary" + "guide/glossary", + "reference/cli" ] }, { @@ -547,6 +548,54 @@ { "source": "/dev/tool-calling", "destination": "/guide/tools-and-agents" + }, + { + "source": "/api/cli/m", + "destination": "/reference/cli" + }, + { + "source": "/api/cli/alora/commands", + "destination": "/reference/cli" + }, + { + "source": "/api/cli/alora/intrinsic_uploader", + "destination": "/reference/cli" + }, + { + "source": "/api/cli/alora/readme_generator", + "destination": "/reference/cli" + }, + { + "source": "/api/cli/alora/train", + "destination": "/reference/cli" + }, + { + "source": "/api/cli/alora/upload", + "destination": "/reference/cli" + }, + { + "source": "/api/cli/decompose/decompose", + "destination": "/reference/cli" + }, + { + "source": "/api/cli/decompose/pipeline", + "destination": "/reference/cli" + }, + { + "source": "/api/cli/decompose/utils", + "destination": "/reference/cli" + }, + { + "source": "/api/cli/eval/commands", + "destination": "/reference/cli" + }, + { + "source": "/api/cli/eval/eval", + "destination": "/reference/cli" + }, + { + "source": "/api/cli/eval/runner", + "destination": "/reference/cli" } ] } diff --git a/docs/docs/evaluation-and-observability/evaluate-with-llm-as-a-judge.md b/docs/docs/evaluation-and-observability/evaluate-with-llm-as-a-judge.md index f8064ca58..2a18317ea 100644 --- a/docs/docs/evaluation-and-observability/evaluate-with-llm-as-a-judge.md +++ b/docs/docs/evaluation-and-observability/evaluate-with-llm-as-a-judge.md @@ -202,4 +202,5 @@ requirements. `sample_generations` lists every attempt made. **See also:** [The Requirements System](../concepts/requirements-system) | [Write Custom Verifiers](../how-to/write-custom-verifiers) | -[Handling Exceptions and Failures](../how-to/handling-exceptions) +[Handling Exceptions and Failures](../how-to/handling-exceptions) | +[CLI Reference](../reference/cli) diff --git a/docs/docs/guide/CONTRIBUTING.md b/docs/docs/guide/CONTRIBUTING.md index de2110b21..1b86b6f6f 100644 --- a/docs/docs/guide/CONTRIBUTING.md +++ b/docs/docs/guide/CONTRIBUTING.md @@ -411,6 +411,70 @@ in the table below, follow the fix instructions, and re-push. --- +## CLI command docstrings + +Typer command functions in `cli/` feed the auto-generated **CLI Reference** page +(`docs/docs/reference/cli.md`). The generator script +(`tooling/docs-autogen/generate_cli_reference.py`) extracts content from both +Typer metadata (`help=` strings on `typer.Option`/`typer.Argument`) and the +command function's docstring. + +Follow this convention for CLI command functions: + +```python +def my_command( + path: str = typer.Argument(..., help="File or directory to process"), + verbose: bool = typer.Option(False, help="Enable verbose output"), +): + """One-line summary of what the command does. + + Extended description with more detail about the command's behaviour. + + Prerequisites: + Mellea installed (``uv add mellea``). Any other requirements + (running services, authentication, etc.). + + Output: + Describe what the command produces — files written, services started, + or side effects applied. + + See Also: + guide: how-to/some-guide-page + guide: advanced/another-page + """ +``` + +**Rules:** + +- **First line** — imperative summary; becomes the command description in the reference. +- **Body** — expanded description; rendered as a paragraph below the summary. +- **`Prerequisites:`** — what must be installed or running. Rendered as a callout. +- **`Output:`** — what the command produces (files, services, side effects). Rendered + as an "Output" paragraph. +- **`See Also:`** — cross-links to guide pages. Each line is `guide: ` + (no `.md` extension). Rendered as "See also" links. +- **`help=` strings** on `typer.Option()` / `typer.Argument()` become the flag + descriptions in the options table. Every option **must** have a `help=` string. +- Regenerate after changes: `uv run poe clidocs` + +### CI enforcement + +The build pipeline runs `generate_cli_reference.py --strict` which fails if any +command is missing a summary, `Prerequisites:`, `Output:` section, or has options +without `help=` text. The `docs-publish` workflow also runs the docs-autogen unit +tests which verify that all expected commands appear in the generated output. + +### Cross-linking convention + +Guide pages that document CLI commands should include a link to the CLI Reference +in their **See also** footer: + +```markdown +**See also:** [Other Page](../path/to-page) | [CLI Reference](../reference/cli) +``` + +--- + ## Local preview ```bash diff --git a/docs/docs/guide/glossary.md b/docs/docs/guide/glossary.md index 05bc9d06c..c86f6d464 100644 --- a/docs/docs/guide/glossary.md +++ b/docs/docs/guide/glossary.md @@ -399,22 +399,6 @@ See: [Instruct, Validate, Repair](../concepts/instruct-validate-repair) --- -## m decompose - -`m decompose` is a CLI tool that takes a complex task description and uses an LLM -to break it into ordered subtasks, extract constraints, and generate a ready-to-run -Python script. - -```bash -m decompose run --prompt-file task.txt --out-dir ./output/ -``` - -The output includes a JSON breakdown of subtasks and a `result.py` you can run -immediately. Also available programmatically via -`cli.decompose.pipeline.decompose()`. - ---- - ## MelleaSession The primary entry point for Mellea. A `MelleaSession` wraps a backend and provides diff --git a/docs/docs/guide/m-decompose.md b/docs/docs/guide/m-decompose.md index daaa55425..1134d1c0a 100644 --- a/docs/docs/guide/m-decompose.md +++ b/docs/docs/guide/m-decompose.md @@ -123,4 +123,4 @@ For tasks that fit comfortably in a single prompt, use `m.instruct()` directly. --- -**See also:** [Tools and Agents](../guide/tools-and-agents) | [Refactor Prompts with CLI](../how-to/refactor-prompts-with-cli) +**See also:** [Tools and Agents](../guide/tools-and-agents) | [Refactor Prompts with CLI](../how-to/refactor-prompts-with-cli) | [CLI Reference](../reference/cli) diff --git a/docs/docs/how-to/refactor-prompts-with-cli.md b/docs/docs/how-to/refactor-prompts-with-cli.md index b5ae8615f..896c96b72 100644 --- a/docs/docs/how-to/refactor-prompts-with-cli.md +++ b/docs/docs/how-to/refactor-prompts-with-cli.md @@ -339,3 +339,5 @@ and `"llm"` for quality checks that require LLM-as-a-judge evaluation. typed returns, and context steering to the generated pipeline - [Enforce Structured Output](../how-to/enforce-structured-output) — constrain subtask outputs to Pydantic models or `Literal` values +- [CLI Reference](../reference/cli) — complete flag and option reference for + all `m` subcommands diff --git a/docs/docs/integrations/m-serve.md b/docs/docs/integrations/m-serve.md index f96e8fedf..9f970f57a 100644 --- a/docs/docs/integrations/m-serve.md +++ b/docs/docs/integrations/m-serve.md @@ -112,4 +112,5 @@ print(response.choices[0].message.content) --- **See also:** [Context and Sessions](../concepts/context-and-sessions) | -[Backends and Configuration](../guide/backends-and-configuration) +[Backends and Configuration](../guide/backends-and-configuration) | +[CLI Reference](../reference/cli) diff --git a/pyproject.toml b/pyproject.toml index 003d38717..08e99f113 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -215,6 +215,14 @@ python tooling/docs-autogen/audit_coverage.py --quality \ """ help = "Generate fresh API docs to /tmp/mellea-preview and audit quality." +[tool.poe.tasks.clidocs] +cmd = "python tooling/docs-autogen/generate_cli_reference.py" +help = "Generate the CLI reference page (docs/docs/reference/cli.md) from Typer metadata." + +[tool.poe.tasks.clidocs-clean] +shell = "rm -f docs/docs/reference/cli.md" +help = "Remove the generated CLI reference page." + # ----------------------------- # Ruff - Linting and Formatting # ----------------------------- diff --git a/tooling/docs-autogen/audit_coverage.py b/tooling/docs-autogen/audit_coverage.py index d53adb034..8cd21cfe3 100755 --- a/tooling/docs-autogen/audit_coverage.py +++ b/tooling/docs-autogen/audit_coverage.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Audit API documentation coverage and docstring quality. -Discovers all public classes and functions in mellea/ and cli/ using Griffe, +Discovers all public classes and functions in mellea/ using Griffe, then checks which ones have generated MDX documentation. Constants and module attributes are excluded from the count — they are not expected to have standalone documentation. @@ -96,7 +96,7 @@ def discover_public_symbols( Args: source_dir: Root directory to scan (e.g., mellea/ or cli/) - package_name: Package name to prepend (e.g., "mellea" or "cli") + package_name: Package name to prepend (e.g., "mellea") Returns: Dict mapping full symbol paths to empty lists (for compatibility) @@ -615,8 +615,8 @@ def audit_docstring_quality( actually surfaced in the API reference. Args: - source_dir: Root directory to scan (e.g., mellea/ or cli/) - package_name: Package name (e.g., "mellea" or "cli") + source_dir: Root directory to scan (e.g., mellea/) + package_name: Package name (e.g., "mellea") short_threshold: Word count below which a docstring is flagged as short include_methods: Whether to audit public methods on classes in addition to top-level functions and classes @@ -1119,17 +1119,12 @@ def main(): print("🔍 Discovering public symbols...") mellea_symbols = discover_public_symbols(source_dir / "mellea", "mellea") - cli_symbols = discover_public_symbols(source_dir / "cli", "cli") - - print("🔍 Discovering CLI commands...") - cli_commands = discover_cli_commands(source_dir / "cli") print("📚 Finding documented symbols...") documented = find_documented_symbols(docs_dir) print("📊 Generating coverage report...") - all_symbols = {**mellea_symbols, **cli_symbols} - report = generate_coverage_report(all_symbols, documented, cli_commands) + report = generate_coverage_report(mellea_symbols, documented, cli_commands=[]) # Print coverage report print(f"\n{'=' * 60}") @@ -1175,7 +1170,7 @@ def main(): if args.quality: print("\n🔬 Running docstring quality audit (documented symbols only)...") include_methods = not args.no_methods - for pkg, pkg_name in [("mellea", "mellea"), ("cli", "cli")]: + for pkg, pkg_name in [("mellea", "mellea")]: pkg_dir = source_dir / pkg if pkg_dir.exists(): quality_issues.extend( diff --git a/tooling/docs-autogen/build.py b/tooling/docs-autogen/build.py index 5a4853634..470f7d3e8 100755 --- a/tooling/docs-autogen/build.py +++ b/tooling/docs-autogen/build.py @@ -54,6 +54,11 @@ def main(): parser.add_argument( "--skip-decoration", action="store_true", help="Skip MDX decoration" ) + parser.add_argument( + "--skip-cli-reference", + action="store_true", + help="Skip CLI reference page generation", + ) args = parser.parse_args() script_dir = Path(__file__).parent @@ -133,6 +138,26 @@ def main(): ) sys.exit(result.returncode) + # Step 4: Generate CLI reference page + if not args.skip_cli_reference: + cmd = [ + sys.executable, + str(script_dir / "generate_cli_reference.py"), + "--docs-root", + str(output_dir.parent), + "--source-dir", + str(repo_root), + "--strict", + ] + print(f"[build.py] Running: {' '.join(cmd)}") + result = subprocess.run(cmd, check=False) + if result.returncode != 0: + print( + f"[build.py] ERROR: generate_cli_reference.py failed with code {result.returncode}", + file=sys.stderr, + ) + sys.exit(result.returncode) + print( f"[build.py] ✅ Documentation build complete (version={version}, normalized={normalized_version})" ) diff --git a/tooling/docs-autogen/generate-ast.py b/tooling/docs-autogen/generate-ast.py index 7384936b2..448d77e8e 100644 --- a/tooling/docs-autogen/generate-ast.py +++ b/tooling/docs-autogen/generate-ast.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """generate-ast.py — mdxify + postprocess docs pipeline. -Runs mdxify against the project's mellea and cli packages then postprocesses +Runs mdxify against the project's mellea package then postprocesses the generated MDX files into the Mintlify docs tree. Requires mdxify to be installed in the current Python environment. Run via:: @@ -9,7 +9,7 @@ uv run python tooling/docs-autogen/generate-ast.py Pipeline: - 1) Run mdxify --all for root modules: mellea, cli into STAGING: /docs/api/ + 1) Run mdxify --all for root modules: mellea into STAGING: /docs/api/ 2) Reorganize flat mdxify output into nested folders 3) Rename __init__.mdx -> .mdx (dedupe if identical) 4) Update frontmatter (title/sidebarTitle/description) from H1 + first paragraph @@ -32,7 +32,7 @@ from typing import Any NAV_TAB = "API Reference" -PACKAGES = ["mellea", "cli"] +PACKAGES = ["mellea"] # Script is in tooling/docs-autogen/generate-ast.py -> repo root is 2 parents up REPO_ROOT = Path(__file__).resolve().parents[2] diff --git a/tooling/docs-autogen/generate_cli_reference.py b/tooling/docs-autogen/generate_cli_reference.py new file mode 100644 index 000000000..8c2c9ead1 --- /dev/null +++ b/tooling/docs-autogen/generate_cli_reference.py @@ -0,0 +1,471 @@ +#!/usr/bin/env python3 +"""Generate a CLI reference page from Typer command metadata. + +Imports the ``cli.m`` Typer application, introspects its Click command tree, +and emits a single Markdown reference page (``reference/cli.md``) documenting +every command, its flags, defaults, and descriptions. + +Structured docstring sections (``Prerequisites:``, ``See Also:``) in command +functions are extracted and rendered as admonitions and cross-links. + +Run via:: + + uv run python tooling/docs-autogen/generate-cli-reference.py +""" + +from __future__ import annotations + +import argparse +import re +import sys +import textwrap +from pathlib import Path +from typing import Any + +import click + +# --------------------------------------------------------------------------- +# Docstring parsing +# --------------------------------------------------------------------------- + +_SECTION_RE = re.compile( + r"^(Prerequisites|See Also|Output|Examples|Args|Raises|Returns|Attributes|Yields):\s*$", + re.MULTILINE, +) + +# RST-style double-backtick → markdown single-backtick +_RST_BACKTICK_RE = re.compile(r"``([^`]+)``") + + +def _parse_docstring_sections(docstring: str | None) -> dict[str, str]: + """Parse a Google-style docstring into named sections. + + Returns a dict with keys ``"summary"``, ``"body"``, and any structured + section names found (e.g. ``"Prerequisites"``, ``"See Also"``). + """ + if not docstring: + return {"summary": "", "body": ""} + + lines = textwrap.dedent(docstring).strip().splitlines() + + # First non-empty line is the summary + summary = lines[0].strip() if lines else "" + + # Collect body lines until we hit a structured section + body_lines: list[str] = [] + sections: dict[str, list[str]] = {} + current_section: str | None = None + i = 1 + + while i < len(lines): + line = lines[i] + match = _SECTION_RE.match(line.strip()) + if match: + section_name: str = match.group(1) + current_section = section_name + sections[section_name] = [] + elif current_section is not None: + sections[current_section].append(line) + else: + body_lines.append(line) + i += 1 + + result: dict[str, str] = {"summary": summary, "body": "\n".join(body_lines).strip()} + for name, section_lines in sections.items(): + result[name] = textwrap.dedent("\n".join(section_lines)).strip() + return result + + +def _parse_see_also(see_also_text: str) -> list[tuple[str, str]]: + """Parse ``See Also`` entries into ``(kind, path)`` tuples. + + Expected format:: + + guide: getting-started/quickstart + guide: how-to/refactor-prompts-with-cli + """ + links: list[tuple[str, str]] = [] + for line in see_also_text.strip().splitlines(): + line = line.strip() + if ":" in line: + kind, _, path = line.partition(":") + links.append((kind.strip(), path.strip())) + return links + + +def _rst_to_md(text: str) -> str: + """Convert RST-style double-backticks to Markdown single-backticks.""" + return _RST_BACKTICK_RE.sub(r"`\1`", text) + + +# --------------------------------------------------------------------------- +# Click model traversal +# --------------------------------------------------------------------------- + + +def _get_click_app(): + """Import and return the Click command tree for the ``m`` CLI.""" + import typer.main + + from cli.m import cli + + return typer.main.get_command(cli) + + +def _format_default(value: Any) -> str: + """Format a parameter default for display.""" + if value is None: + return "" + if isinstance(value, bool): + return str(value).lower() + # Handle enum defaults (e.g. DecompBackend.ollama) + if hasattr(value, "value"): + return str(value.value) + return str(value) + + +def _format_type(param: click.Parameter) -> str: + """Format a Click parameter type for display.""" + type_name = param.type.name + if hasattr(param.type, "choices"): + return " \\| ".join(param.type.choices) + return type_name + + +def _format_flags(param: click.Parameter) -> str: + """Format parameter flags (e.g. ``--backend, -b``).""" + is_arg = isinstance(param, click.Argument) + if is_arg: + name = param.name or "" + return f"``{name.upper()}``" + opts = list(getattr(param, "opts", [])) + secondary = list(getattr(param, "secondary_opts", [])) + # Filter out --no-* boolean counterparts + all_opts = [o for o in opts + secondary if not o.startswith("--no-")] + return ", ".join(f"``{o}``" for o in all_opts) + + +def _build_synopsis(full_name: str, cmd: click.BaseCommand) -> str: + """Build a usage synopsis line for a command.""" + parts = [full_name] + for param in cmd.params: + is_arg = isinstance(param, click.Argument) + if is_arg: + name = (param.name or "").upper() + if param.required: + parts.append(f"<{name}>") + else: + parts.append(f"[{name}]") + else: + opts = list(getattr(param, "opts", [])) + long_opt = next( + (o for o in opts if o.startswith("--")), opts[0] if opts else "" + ) + if param.required: + parts.append(f"{long_opt} ") + else: + parts.append(f"[{long_opt}]") + return " ".join(parts) + + +def _render_command( + full_name: str, cmd: click.BaseCommand, heading_level: int +) -> list[str]: + """Render a single command as Markdown lines.""" + lines: list[str] = [] + heading = "#" * heading_level + lines.append(f"{heading} `{full_name}`") + lines.append("") + + # Parse docstring + sections = _parse_docstring_sections(cmd.help) + + # Summary + summary = _rst_to_md(sections.get("summary", "")) + if summary: + lines.append(summary) + lines.append("") + + # Extended description + body = _rst_to_md(sections.get("body", "")) + if body: + # Strip Click's \b formatting markers + body = body.replace("\b", "").strip() + if body: + lines.append(body) + lines.append("") + + # Prerequisites — render as bulleted blockquote list + prereqs = _rst_to_md(sections.get("Prerequisites", "")) + if prereqs: + # Join continuation lines into single string, then split on sentences + prereqs_joined = " ".join(prereqs.split()) + items = re.split(r"\.\s+(?=[A-Z])", prereqs_joined.strip()) + items = [item.rstrip(".").strip() for item in items if item.strip()] + if len(items) == 1: + lines.append(f"> **Prerequisites:** {items[0]}.") + else: + lines.append("> **Prerequisites:**") + lines.append(">") + for item in items: + lines.append(f"> - {item}.") + lines.append("") + + # Synopsis + lines.append("```bash") + lines.append(_build_synopsis(full_name, cmd)) + lines.append("```") + lines.append("") + + # Options table — split into arguments and options + arguments = [p for p in cmd.params if isinstance(p, click.Argument)] + options = [p for p in cmd.params if not isinstance(p, click.Argument)] + + if arguments: + lines.append("**Arguments:**") + lines.append("") + lines.append("| Name | Type | Required | Description |") + lines.append("| ---- | ---- | -------- | ----------- |") + for p in arguments: + flags = _format_flags(p) + ptype = _format_type(p) + required = "yes" if p.required else "no" + help_text = _rst_to_md(getattr(p, "help", "") or "") + lines.append(f"| {flags} | {ptype} | {required} | {help_text} |") + lines.append("") + + if options: + lines.append("**Options:**") + lines.append("") + lines.append("| Flag | Type | Default | Description |") + lines.append("| ---- | ---- | ------- | ----------- |") + for p in options: + flags = _format_flags(p) + ptype = _format_type(p) + default_str = _format_default(p.default) + if p.required: + default = "*required*" + elif default_str: + default = f"`{default_str}`" + else: + default = "—" + help_text = _rst_to_md(getattr(p, "help", "") or "—") + lines.append(f"| {flags} | {ptype} | {default} | {help_text} |") + lines.append("") + + # Output + output = _rst_to_md(sections.get("Output", "")) + if output: + # Join continuation lines + output_joined = " ".join(output.split()) + lines.append(f"**Output:** {output_joined}") + lines.append("") + + # Examples + examples = sections.get("Examples", "") + if examples: + lines.append("**Example:**") + lines.append("") + lines.append("```bash") + for ex_line in examples.strip().splitlines(): + lines.append(ex_line.strip()) + lines.append("```") + lines.append("") + + # See Also + see_also = sections.get("See Also", "") + if see_also: + links = _parse_see_also(see_also) + if links: + see_parts = [] + for kind, path in links: + if kind == "guide": + see_parts.append(f"[{path.split('/')[-1]}](../{path})") + if see_parts: + lines.append(f"**See also:** {', '.join(see_parts)}") + lines.append("") + + return lines + + +# --------------------------------------------------------------------------- +# Full page generation +# --------------------------------------------------------------------------- + +FRONTMATTER = """\ +--- +title: "CLI Reference" +sidebarTitle: "CLI Reference" +description: "Complete reference for the m command-line tool — all subcommands, flags, and defaults." +--- +""" + + +def generate_cli_reference(click_app: click.BaseCommand) -> str: + """Generate the full CLI reference page as a Markdown string.""" + lines: list[str] = [FRONTMATTER] + + # Intro from root callback docstring + root_sections = _parse_docstring_sections(click_app.help) + root_summary = root_sections.get("summary", "") + if root_summary: + lines.append(root_summary) + lines.append("") + root_body = root_sections.get("body", "") + if root_body: + lines.append(root_body) + lines.append("") + + # Iterate commands + if not hasattr(click_app, "commands"): + return "\n".join(lines) + + for cmd_name in sorted(click_app.commands): + cmd = click_app.commands[cmd_name] + + if hasattr(cmd, "commands") and cmd.commands: + # Command group — render group heading then subcommands + group_summary = "" + if cmd.help: + group_summary = cmd.help.split("\n")[0].strip() + + lines.append(f"## `m {cmd_name}`") + lines.append("") + if group_summary: + lines.append(group_summary) + lines.append("") + + for sub_name in sorted(cmd.commands): + sub_cmd = cmd.commands[sub_name] + lines.extend( + _render_command( + f"m {cmd_name} {sub_name}", sub_cmd, heading_level=3 + ) + ) + else: + # Top-level command + lines.extend(_render_command(f"m {cmd_name}", cmd, heading_level=2)) + + return "\n".join(lines) + + +# --------------------------------------------------------------------------- +# Strict validation +# --------------------------------------------------------------------------- + + +def validate_cli_reference(click_app: click.BaseCommand) -> list[str]: + """Validate CLI command docstrings for completeness. + + Returns a list of error messages. Empty list means all checks pass. + """ + errors: list[str] = [] + + if not hasattr(click_app, "commands"): + errors.append("Root CLI app has no commands") + return errors + + def _check_command(full_name: str, cmd: click.BaseCommand) -> None: + sections = _parse_docstring_sections(cmd.help) + summary = sections.get("summary", "").strip() + + if not summary: + errors.append(f"{full_name}: missing docstring summary") + + if not sections.get("Prerequisites", "").strip(): + errors.append(f"{full_name}: missing Prerequisites section") + + if not sections.get("Output", "").strip(): + errors.append(f"{full_name}: missing Output section") + + # Check all options have help text + for param in cmd.params: + help_text = getattr(param, "help", None) + if not help_text: + param_name = param.name or "(unnamed)" + errors.append(f"{full_name}: option --{param_name} has no help text") + + for cmd_name in sorted(click_app.commands): + cmd = click_app.commands[cmd_name] + if hasattr(cmd, "commands") and cmd.commands: + for sub_name in sorted(cmd.commands): + sub_cmd = cmd.commands[sub_name] + _check_command(f"m {cmd_name} {sub_name}", sub_cmd) + else: + _check_command(f"m {cmd_name}", cmd) + + return errors + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Generate CLI reference documentation from Typer metadata." + ) + parser.add_argument( + "--docs-root", + default=None, + help="Docs root directory (defaults to docs/docs relative to repo root).", + ) + parser.add_argument( + "--source-dir", + default=None, + help="Repository root (for sys.path setup). Defaults to two parents up.", + ) + parser.add_argument( + "--stdout", + action="store_true", + help="Print to stdout instead of writing a file.", + ) + parser.add_argument( + "--strict", + action="store_true", + help="Fail with non-zero exit if any CLI docstring is incomplete.", + ) + args = parser.parse_args() + + script_dir = Path(__file__).resolve().parent + repo_root = ( + Path(args.source_dir).resolve() if args.source_dir else script_dir.parents[1] + ) + + # Ensure repo root is on sys.path so cli/ can be imported + repo_root_str = str(repo_root) + if repo_root_str not in sys.path: + sys.path.insert(0, repo_root_str) + + docs_root = Path(args.docs_root) if args.docs_root else repo_root / "docs" / "docs" + output_path = docs_root / "reference" / "cli.md" + + print("🔧 Importing CLI application...", flush=True) + click_app = _get_click_app() + + print("📝 Generating CLI reference...", flush=True) + content = generate_cli_reference(click_app) + + if args.stdout: + print(content) + else: + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(content) + print(f"✅ CLI reference written to {output_path}") + + # Strict validation — always run, fail only with --strict + errors = validate_cli_reference(click_app) + if errors: + print(f"\n⚠️ CLI docstring validation: {len(errors)} issue(s):", flush=True) + for err in errors: + print(f" • {err}") + if args.strict: + print("\n❌ Strict mode: failing due to incomplete CLI docstrings.") + sys.exit(1) + else: + print("✅ CLI docstring validation passed.") + + +if __name__ == "__main__": + main() diff --git a/tooling/docs-autogen/test_cli_reference.py b/tooling/docs-autogen/test_cli_reference.py new file mode 100644 index 000000000..a7f281022 --- /dev/null +++ b/tooling/docs-autogen/test_cli_reference.py @@ -0,0 +1,201 @@ +"""Tests for generate-cli-reference.py.""" + +from __future__ import annotations + +import re + +import pytest + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def click_app(): + """Import and return the Click command tree for the ``m`` CLI.""" + import typer.main + + from cli.m import cli + + return typer.main.get_command(cli) + + +@pytest.fixture(scope="module") +def generated_md(click_app): + """Generate the full CLI reference Markdown string.""" + from generate_cli_reference import generate_cli_reference + + return generate_cli_reference(click_app) + + +# --------------------------------------------------------------------------- +# Import / introspection tests +# --------------------------------------------------------------------------- + + +def test_click_app_is_group(click_app): + """Root app should be a Click Group with commands.""" + assert hasattr(click_app, "commands") + assert len(click_app.commands) > 0 + + +EXPECTED_TOP_LEVEL = {"serve", "alora", "decompose", "eval", "fix"} + + +def test_all_top_level_commands_present(click_app): + """All expected top-level commands must be discovered.""" + assert EXPECTED_TOP_LEVEL <= set(click_app.commands.keys()) + + +EXPECTED_SUBCOMMANDS = { + "alora": {"train", "upload", "add-readme"}, + "decompose": {"run"}, + "eval": {"run"}, + "fix": {"async", "genslots"}, +} + + +@pytest.mark.parametrize( + "group,expected_subs", list(EXPECTED_SUBCOMMANDS.items()), ids=EXPECTED_SUBCOMMANDS +) +def test_subcommands_present(click_app, group, expected_subs): + """Each command group must contain its expected subcommands.""" + cmd = click_app.commands[group] + assert hasattr(cmd, "commands"), f"{group} should be a command group" + assert expected_subs <= set(cmd.commands.keys()) + + +def test_serve_is_not_a_group(click_app): + """serve is a direct command, not a group.""" + serve = click_app.commands["serve"] + assert not hasattr(serve, "commands") + + +# --------------------------------------------------------------------------- +# Docstring parsing tests +# --------------------------------------------------------------------------- + + +def test_parse_docstring_sections_basic(): + from generate_cli_reference import _parse_docstring_sections + + result = _parse_docstring_sections( + """One-line summary. + + Extended body here. + + Prerequisites: + Some prereq. + + See Also: + guide: foo/bar + """ + ) + assert result["summary"] == "One-line summary." + assert "Extended body" in result["body"] + assert "Some prereq" in result["Prerequisites"] + assert "guide: foo/bar" in result["See Also"] + + +def test_parse_docstring_sections_empty(): + from generate_cli_reference import _parse_docstring_sections + + result = _parse_docstring_sections(None) + assert result["summary"] == "" + assert result["body"] == "" + + +def test_parse_see_also(): + from generate_cli_reference import _parse_see_also + + links = _parse_see_also("guide: how-to/my-page\nguide: advanced/other") + assert links == [("guide", "how-to/my-page"), ("guide", "advanced/other")] + + +# --------------------------------------------------------------------------- +# Generated output tests +# --------------------------------------------------------------------------- + + +def test_frontmatter_present(generated_md): + assert generated_md.startswith("---\n") + assert 'title: "CLI Reference"' in generated_md + + +def test_all_commands_in_output(generated_md): + """Every expected command should appear as a heading in the output.""" + assert "## `m serve`" in generated_md + assert "## `m alora`" in generated_md + assert "## `m decompose`" in generated_md + assert "## `m eval`" in generated_md + assert "## `m fix`" in generated_md + + +def test_subcommands_in_output(generated_md): + """Subcommands should appear as H3 headings.""" + assert "### `m alora train`" in generated_md + assert "### `m alora upload`" in generated_md + assert "### `m decompose run`" in generated_md + assert "### `m eval run`" in generated_md + assert "### `m fix async`" in generated_md + assert "### `m fix genslots`" in generated_md + + +def test_options_tables_present(generated_md): + """Options tables should be present for commands with flags.""" + assert "| Flag | Type | Default | Description |" in generated_md + + +def test_prerequisites_rendered(generated_md): + """Prerequisites should be rendered as blockquote callouts.""" + assert "> **Prerequisites:**" in generated_md + + +def test_see_also_links_rendered(generated_md): + """See also links should be rendered as markdown links.""" + assert "**See also:**" in generated_md + # Should contain at least one relative link + assert re.search(r"\[.*?\]\(\.\./.*?\)", generated_md) + + +def test_synopsis_present(generated_md): + """Each command should have a code-fenced synopsis.""" + assert "```bash\nm serve" in generated_md + assert "```bash\nm alora train" in generated_md + + +def test_output_sections_rendered(generated_md): + """Output sections should be rendered for commands that define them.""" + assert "**Output:**" in generated_md + + +def test_rst_backticks_converted(generated_md): + """RST-style double-backticks from docstrings should be converted.""" + # The description column text should not contain rst-style ``backticks`` + # (flag names in the Name column use markdown ``code`` which is fine) + # Check that description text in tables doesn't have rst backticks + for line in generated_md.splitlines(): + if line.startswith("|") and "| ``--" not in line and "| ``" not in line: + continue + # Check descriptions (last column) for rst backticks + if line.startswith("|"): + cols = line.split("|") + if len(cols) >= 5: + desc = cols[-2] # Description is second-to-last (before trailing |) + assert "``" not in desc, f"RST backticks in description: {desc}" + + +def test_strict_validation_passes(click_app): + """Strict validation should pass with no errors for all current commands.""" + from generate_cli_reference import validate_cli_reference + + errors = validate_cli_reference(click_app) + assert errors == [], f"Strict validation errors: {errors}" + + +def test_no_mdx_or_framework_specific_syntax(generated_md): + """Output should be standard Markdown, no MDX components.""" + assert " Date: Tue, 14 Apr 2026 12:29:26 +0100 Subject: [PATCH 02/11] fix(ci): scope docs-publish test step to CLI reference tests only The full tooling/docs-autogen/ test suite has 13 pre-existing failures in test_validate.py, test_escape_mdx.py, and test_anchor_collisions.py. Scope the CI step to test_cli_reference.py to avoid blocking on unrelated failures. --- .github/workflows/docs-publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs-publish.yml b/.github/workflows/docs-publish.yml index 81a8f24ce..e79bdf0bb 100644 --- a/.github/workflows/docs-publish.yml +++ b/.github/workflows/docs-publish.yml @@ -83,8 +83,8 @@ jobs: # -- Run docs-autogen unit tests ------------------------------------------ - - name: Run docs-autogen tests - run: uv run pytest tooling/docs-autogen/ -v --tb=short + - name: Run CLI reference tests + run: uv run pytest tooling/docs-autogen/test_cli_reference.py -v --tb=short # -- Validate static docs ------------------------------------------------ From d605f4d62fce25a974c5cb0e966408ec4aba0280 Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Tue, 14 Apr 2026 12:40:47 +0100 Subject: [PATCH 03/11] fix: address code review findings - Fix wrong filename in module docstring (hyphens -> underscores) - Validate before writing file (fail early with --strict) - Add missing redirects for serve/ and fix/ old API pages - Add Examples: section to CONTRIBUTING.md template and rules --- docs/docs/docs.json | 20 ++++++++++++++++ docs/docs/guide/CONTRIBUTING.md | 5 ++++ .../docs-autogen/generate_cli_reference.py | 24 +++++++++---------- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/docs/docs/docs.json b/docs/docs/docs.json index 7edee8982..66af87e01 100644 --- a/docs/docs/docs.json +++ b/docs/docs/docs.json @@ -596,6 +596,26 @@ { "source": "/api/cli/eval/runner", "destination": "/reference/cli" + }, + { + "source": "/api/cli/serve/app", + "destination": "/reference/cli" + }, + { + "source": "/api/cli/serve/models", + "destination": "/reference/cli" + }, + { + "source": "/api/cli/fix/commands", + "destination": "/reference/cli" + }, + { + "source": "/api/cli/fix/async_fixer", + "destination": "/reference/cli" + }, + { + "source": "/api/cli/fix/genstub_fixer", + "destination": "/reference/cli" } ] } diff --git a/docs/docs/guide/CONTRIBUTING.md b/docs/docs/guide/CONTRIBUTING.md index 1b86b6f6f..d538e53ca 100644 --- a/docs/docs/guide/CONTRIBUTING.md +++ b/docs/docs/guide/CONTRIBUTING.md @@ -438,6 +438,9 @@ def my_command( Describe what the command produces — files written, services started, or side effects applied. + Examples: + m my-command path/to/input --flag value + See Also: guide: how-to/some-guide-page guide: advanced/another-page @@ -451,6 +454,8 @@ def my_command( - **`Prerequisites:`** — what must be installed or running. Rendered as a callout. - **`Output:`** — what the command produces (files, services, side effects). Rendered as an "Output" paragraph. +- **`Examples:`** — a minimal one-liner invocation showing the most common flags. + Rendered as a fenced code block. - **`See Also:`** — cross-links to guide pages. Each line is `guide: ` (no `.md` extension). Rendered as "See also" links. - **`help=` strings** on `typer.Option()` / `typer.Argument()` become the flag diff --git a/tooling/docs-autogen/generate_cli_reference.py b/tooling/docs-autogen/generate_cli_reference.py index 8c2c9ead1..10c9cbc2d 100644 --- a/tooling/docs-autogen/generate_cli_reference.py +++ b/tooling/docs-autogen/generate_cli_reference.py @@ -10,7 +10,7 @@ Run via:: - uv run python tooling/docs-autogen/generate-cli-reference.py + uv run python tooling/docs-autogen/generate_cli_reference.py """ from __future__ import annotations @@ -444,17 +444,7 @@ def main() -> None: print("🔧 Importing CLI application...", flush=True) click_app = _get_click_app() - print("📝 Generating CLI reference...", flush=True) - content = generate_cli_reference(click_app) - - if args.stdout: - print(content) - else: - output_path.parent.mkdir(parents=True, exist_ok=True) - output_path.write_text(content) - print(f"✅ CLI reference written to {output_path}") - - # Strict validation — always run, fail only with --strict + # Validate before writing — fail early with --strict errors = validate_cli_reference(click_app) if errors: print(f"\n⚠️ CLI docstring validation: {len(errors)} issue(s):", flush=True) @@ -466,6 +456,16 @@ def main() -> None: else: print("✅ CLI docstring validation passed.") + print("📝 Generating CLI reference...", flush=True) + content = generate_cli_reference(click_app) + + if args.stdout: + print(content) + else: + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(content) + print(f"✅ CLI reference written to {output_path}") + if __name__ == "__main__": main() From 58feb1a53dc69d556efc1546a9b4eff285b8fae3 Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Tue, 14 Apr 2026 12:48:24 +0100 Subject: [PATCH 04/11] fix: address remaining code review findings - Convert See Also link text from URL slugs to human-readable titles with proper capitalisation (3/3 reviewer consensus) - Remove dead discover_cli_commands() from audit_coverage.py - Switch flag formatting from double to single backticks - Filter --help param from synopsis, options tables, and strict validator - Apply _rst_to_md to root intro text - Simplify test_rst_backticks test now double backticks are gone --- tooling/docs-autogen/audit_coverage.py | 30 --------- .../docs-autogen/generate_cli_reference.py | 61 ++++++++++++++++--- tooling/docs-autogen/test_cli_reference.py | 20 ++---- 3 files changed, 59 insertions(+), 52 deletions(-) diff --git a/tooling/docs-autogen/audit_coverage.py b/tooling/docs-autogen/audit_coverage.py index 8cd21cfe3..c00cb29cc 100755 --- a/tooling/docs-autogen/audit_coverage.py +++ b/tooling/docs-autogen/audit_coverage.py @@ -959,36 +959,6 @@ def _extract(obj: object) -> None: return sorted(mdx_files - nav_refs) -def discover_cli_commands(cli_dir: Path) -> list[str]: - """Discover CLI commands from Typer applications. - - Args: - cli_dir: Path to cli/ directory - - Returns: - List of command names (e.g., ["m serve", "m alora", "m decompose"]) - """ - commands = [] - - # Look for Typer app definitions - # This is a simplified version - full implementation would parse the CLI structure - main_file = cli_dir / "m.py" - if main_file.exists(): - content = main_file.read_text() - - # Simple heuristic: look for @app.command() decorators or add_typer() calls - - # Find command decorators - command_pattern = r'@app\.command\(["\']([^"\']+)["\']\)' - commands.extend(re.findall(command_pattern, content)) - - # Find subcommand additions - typer_pattern = r'app\.add_typer\([^,]+,\s*name=["\']([^"\']+)["\']\)' - commands.extend(re.findall(typer_pattern, content)) - - return sorted(set(commands)) - - def find_documented_symbols(docs_dir: Path) -> set[str]: """Find which symbols have MDX documentation. diff --git a/tooling/docs-autogen/generate_cli_reference.py b/tooling/docs-autogen/generate_cli_reference.py index 10c9cbc2d..1f6eebebd 100644 --- a/tooling/docs-autogen/generate_cli_reference.py +++ b/tooling/docs-autogen/generate_cli_reference.py @@ -98,6 +98,37 @@ def _rst_to_md(text: str) -> str: return _RST_BACKTICK_RE.sub(r"`\1`", text) +_TITLE_LOWERCASE = { + "a", + "an", + "and", + "as", + "at", + "by", + "for", + "in", + "of", + "on", + "or", + "the", + "to", + "vs", + "with", +} + + +def _slug_to_title(slug: str) -> str: + """Convert a URL slug to a human-readable title with proper capitalisation.""" + words = slug.replace("-", " ").split() + result = [] + for i, word in enumerate(words): + if i == 0 or word.lower() not in _TITLE_LOWERCASE: + result.append(word.capitalize()) + else: + result.append(word.lower()) + return " ".join(result) + + # --------------------------------------------------------------------------- # Click model traversal # --------------------------------------------------------------------------- @@ -137,18 +168,25 @@ def _format_flags(param: click.Parameter) -> str: is_arg = isinstance(param, click.Argument) if is_arg: name = param.name or "" - return f"``{name.upper()}``" + return f"`{name.upper()}`" opts = list(getattr(param, "opts", [])) secondary = list(getattr(param, "secondary_opts", [])) # Filter out --no-* boolean counterparts all_opts = [o for o in opts + secondary if not o.startswith("--no-")] - return ", ".join(f"``{o}``" for o in all_opts) + return ", ".join(f"`{o}`" for o in all_opts) + + +def _is_help_param(param: click.Parameter) -> bool: + """Return True for Click's auto-generated --help parameter.""" + return param.name == "help" and not isinstance(param, click.Argument) def _build_synopsis(full_name: str, cmd: click.BaseCommand) -> str: """Build a usage synopsis line for a command.""" parts = [full_name] for param in cmd.params: + if _is_help_param(param): + continue is_arg = isinstance(param, click.Argument) if is_arg: name = (param.name or "").upper() @@ -217,9 +255,13 @@ def _render_command( lines.append("```") lines.append("") - # Options table — split into arguments and options + # Options table — split into arguments and options, exclude --help arguments = [p for p in cmd.params if isinstance(p, click.Argument)] - options = [p for p in cmd.params if not isinstance(p, click.Argument)] + options = [ + p + for p in cmd.params + if not isinstance(p, click.Argument) and not _is_help_param(p) + ] if arguments: lines.append("**Arguments:**") @@ -280,7 +322,8 @@ def _render_command( see_parts = [] for kind, path in links: if kind == "guide": - see_parts.append(f"[{path.split('/')[-1]}](../{path})") + title = _slug_to_title(path.split("/")[-1]) + see_parts.append(f"[{title}](../{path})") if see_parts: lines.append(f"**See also:** {', '.join(see_parts)}") lines.append("") @@ -307,11 +350,11 @@ def generate_cli_reference(click_app: click.BaseCommand) -> str: # Intro from root callback docstring root_sections = _parse_docstring_sections(click_app.help) - root_summary = root_sections.get("summary", "") + root_summary = _rst_to_md(root_sections.get("summary", "")) if root_summary: lines.append(root_summary) lines.append("") - root_body = root_sections.get("body", "") + root_body = _rst_to_md(root_sections.get("body", "")) if root_body: lines.append(root_body) lines.append("") @@ -378,8 +421,10 @@ def _check_command(full_name: str, cmd: click.BaseCommand) -> None: if not sections.get("Output", "").strip(): errors.append(f"{full_name}: missing Output section") - # Check all options have help text + # Check all options have help text (skip auto-generated --help) for param in cmd.params: + if _is_help_param(param): + continue help_text = getattr(param, "help", None) if not help_text: param_name = param.name or "(unnamed)" diff --git a/tooling/docs-autogen/test_cli_reference.py b/tooling/docs-autogen/test_cli_reference.py index a7f281022..78a48e92d 100644 --- a/tooling/docs-autogen/test_cli_reference.py +++ b/tooling/docs-autogen/test_cli_reference.py @@ -170,20 +170,12 @@ def test_output_sections_rendered(generated_md): assert "**Output:**" in generated_md -def test_rst_backticks_converted(generated_md): - """RST-style double-backticks from docstrings should be converted.""" - # The description column text should not contain rst-style ``backticks`` - # (flag names in the Name column use markdown ``code`` which is fine) - # Check that description text in tables doesn't have rst backticks - for line in generated_md.splitlines(): - if line.startswith("|") and "| ``--" not in line and "| ``" not in line: - continue - # Check descriptions (last column) for rst backticks - if line.startswith("|"): - cols = line.split("|") - if len(cols) >= 5: - desc = cols[-2] # Description is second-to-last (before trailing |) - assert "``" not in desc, f"RST backticks in description: {desc}" +def test_no_double_backticks_in_output(generated_md): + """No RST-style double-backticks should appear in the generated output.""" + # After frontmatter, strip code blocks, then check for `` + content = generated_md.split("---", 2)[-1] + non_code = re.sub(r"```.*?```", "", content, flags=re.DOTALL) + assert "``" not in non_code, "Double backticks found outside code blocks" def test_strict_validation_passes(click_app): From 5a90a70831ac8ed9c6bacefac8a0a3d7fc0d9868 Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Tue, 14 Apr 2026 12:55:10 +0100 Subject: [PATCH 05/11] docs: update docs-autogen README for CLI reference pipeline - Add clidocs/clidocs-clean to quick start - Add Step 4 (generate_cli_reference.py) to pipeline overview - Add generate_cli_reference.py and test file to file structure - Remove cli/ from generated docs tree, add reference/cli.md - Remove stale Makefile shim reference - Fix make -> poe reference in validate step --- tooling/docs-autogen/README.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/tooling/docs-autogen/README.md b/tooling/docs-autogen/README.md index c5bb4c3d8..99c704693 100644 --- a/tooling/docs-autogen/README.md +++ b/tooling/docs-autogen/README.md @@ -12,10 +12,10 @@ uv run poe apidocs-quality # Audit docstring quality (public symbols, no metho uv run poe apidocs-orphans # Find MDX files not referenced in docs.json navigation uv run poe apidocs-validate # Verify coverage + MDX syntax uv run poe apidocs-clean # Remove generated artefacts +uv run poe clidocs # Generate CLI reference page from Typer metadata +uv run poe clidocs-clean # Remove generated CLI reference page ``` -Makefile shims are also available if you prefer `make apidocs` etc. - The `apidocs` task runs `build.py`, which calls `generate-ast.py` then `decorate_api_mdx.py` in sequence. Both the `docs/docs/api/` directory and the `docs/docs/api-reference.mdx` landing page are **fully generated artefacts** — @@ -47,7 +47,7 @@ docs/docs/api/ (fresh copy, replaces previous entirely) - Add CLASS/FUNC pills and visual dividers to headings │ ▼ -[3] validate.py (optional, run via make docs-validate) +[3] validate.py (optional, run via uv run poe apidocs-validate) - GitHub source links correct? - API coverage ≥ threshold? - MDX syntax valid (no unescaped braces)? @@ -55,7 +55,15 @@ docs/docs/api/ (fresh copy, replaces previous entirely) - No duplicate heading anchors? │ ▼ -[4] Mintlify dev server (make docs-serve) +[4] generate_cli_reference.py + - Imports Typer app, walks Click command tree + - Extracts flags, types, defaults, help strings + - Parses docstring sections (Prerequisites, Output, Examples, See Also) + - Emits docs/docs/reference/cli.md (standard Markdown) + - --strict fails on incomplete docstrings + │ + ▼ +[5] Mintlify dev server (mintlify dev from docs/docs/) http://localhost:3000 ``` @@ -69,6 +77,8 @@ tooling/docs-autogen/ ├── decorate_api_mdx.py # Step 2: decoration, escaping, cross-references ├── validate.py # Step 3: quality validation ├── audit_coverage.py # Symbol coverage + quality audit +├── generate_cli_reference.py # CLI reference page generator +├── test_cli_reference.py # Tests for CLI reference generation ├── test_escape_mdx.py # Tests for MDX brace escaping ├── test_cross_references.py ├── test_mintlify_anchors.py @@ -79,8 +89,9 @@ docs/docs/ ├── docs.json # Mintlify config — API Reference tab auto-generated ├── api-reference.mdx # Auto-generated landing page ├── api/ # Fully generated — do not edit -│ ├── mellea/ -│ └── cli/ +│ └── mellea/ +├── reference/ +│ └── cli.md # Auto-generated CLI reference — do not edit └── snippets/ └── SidebarFix.mdx # Mintlify sidebar component (hand-maintained) ``` From 8a886f16328013a844beaf4dc82081a962e99690 Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Tue, 14 Apr 2026 12:59:33 +0100 Subject: [PATCH 06/11] docs: add prerequisites section to docs-autogen README Document the install command (matching CI) and Node.js requirement for local doc builds and previews. --- tooling/docs-autogen/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tooling/docs-autogen/README.md b/tooling/docs-autogen/README.md index 99c704693..ab1114e17 100644 --- a/tooling/docs-autogen/README.md +++ b/tooling/docs-autogen/README.md @@ -3,6 +3,14 @@ Automated system for generating, decorating, and validating Mellea API documentation using Mintlify. +## Prerequisites + +```bash +uv sync --all-extras --group dev # Same as CI — installs all extras + dev tools +``` + +Node.js (LTS, v22 or earlier) is required for `mintlify dev` previews. + ## Quick Start ```bash From b5aca2a8caba94491c191c81d10c3c347eb390bd Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Tue, 14 Apr 2026 13:00:16 +0100 Subject: [PATCH 07/11] fix: add friendly error when CLI extras are missing generate_cli_reference.py now catches ImportError during CLI app import and prints the required install command instead of a raw traceback. --- tooling/docs-autogen/generate_cli_reference.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tooling/docs-autogen/generate_cli_reference.py b/tooling/docs-autogen/generate_cli_reference.py index 1f6eebebd..6d87b1e9f 100644 --- a/tooling/docs-autogen/generate_cli_reference.py +++ b/tooling/docs-autogen/generate_cli_reference.py @@ -136,9 +136,15 @@ def _slug_to_title(slug: str) -> str: def _get_click_app(): """Import and return the Click command tree for the ``m`` CLI.""" - import typer.main - - from cli.m import cli + try: + import typer.main + + from cli.m import cli + except ImportError as e: + raise SystemExit( + f"❌ Failed to import CLI application: {e}\n" + " Install all extras: uv sync --all-extras --group dev" + ) from e return typer.main.get_command(cli) From 439dfe08572a023a354dd8a0ab15fe0a836e8974 Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Tue, 14 Apr 2026 18:34:56 +0100 Subject: [PATCH 08/11] docs: fix docs-autogen README gaps for CLI reference pipeline - Correct apidocs intro: build.py now runs 4 steps, not 2 - Document --strict distinction: clidocs is lenient, apidocs pipeline is always strict; add standalone --strict invocation example - Add CLI reference generator configuration section (--strict, --docs-root, --source-dir, --skip-cli-reference) - Document that full clean requires apidocs-clean + clidocs-clean - Add test_cli_reference.py to Development examples - Replace stale `make docs` references with `uv run poe apidocs` --- tooling/docs-autogen/README.md | 54 +++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/tooling/docs-autogen/README.md b/tooling/docs-autogen/README.md index ab1114e17..ea9f5325e 100644 --- a/tooling/docs-autogen/README.md +++ b/tooling/docs-autogen/README.md @@ -24,10 +24,21 @@ uv run poe clidocs # Generate CLI reference page from Typer metadata uv run poe clidocs-clean # Remove generated CLI reference page ``` -The `apidocs` task runs `build.py`, which calls `generate-ast.py` then -`decorate_api_mdx.py` in sequence. Both the `docs/docs/api/` directory and the -`docs/docs/api-reference.mdx` landing page are **fully generated artefacts** — -do not commit or edit them by hand. +The `apidocs` task runs `build.py`, which orchestrates four steps: AST generation +(`generate-ast.py`), MDX decoration (`decorate_api_mdx.py`), nav rebuild, and CLI +reference generation (`generate_cli_reference.py`). Both the `docs/docs/api/` +directory and `docs/docs/api-reference.mdx` are **fully generated artefacts** — +do not commit or edit them by hand. `docs/docs/reference/cli.md` is also generated +— edit the CLI command docstrings in `cli/` instead. + +`clidocs` runs `generate_cli_reference.py` without validation flags — useful for +quick local iteration. The `apidocs` pipeline always passes `--strict`, which fails +the build if any command is missing required docstring sections or has options +without `help=` text. To test strict validation locally: + +```bash +uv run python tooling/docs-autogen/generate_cli_reference.py --strict +``` ## Pipeline Overview @@ -190,7 +201,35 @@ cross-repo audits. **Navigation and landing page** are auto-generated by `generate-ast.py`. Do not edit the API Reference tab in `docs.json` or `api-reference.mdx` by hand -— both are overwritten on every `make docs` run. +— both are overwritten on every `uv run poe apidocs` run. + +**CLI reference generator** (`generate_cli_reference.py`): + +```bash +uv run poe clidocs # generate (lenient — for local iteration) +uv run python tooling/docs-autogen/generate_cli_reference.py --strict # strict mode (same as CI) +uv run python tooling/docs-autogen/generate_cli_reference.py --docs-root /tmp/preview # custom output location +uv run python tooling/docs-autogen/generate_cli_reference.py --source-dir ../mellea-b # cross-repo +``` + +`--strict` exits non-zero if any command lacks a summary, `Prerequisites:`, or +`Output:` section, or has options without `help=` text. The `apidocs` pipeline +always runs with `--strict`; `clidocs` does not. + +**Skipping pipeline steps** (`build.py`): + +```bash +uv run python tooling/docs-autogen/build.py --skip-cli-reference # API docs only, no CLI reference +uv run python tooling/docs-autogen/build.py --skip-generation # decoration + CLI reference only +uv run python tooling/docs-autogen/build.py --skip-decoration # generation + CLI reference only +``` + +**Full clean** — `apidocs-clean` removes only the API artefacts; `clidocs-clean` +removes only the CLI reference. To remove everything: + +```bash +uv run poe apidocs-clean && uv run poe clidocs-clean +``` ## Cross-References @@ -226,6 +265,7 @@ uv run poe apidocs-test # Run a specific test file uv run pytest tooling/docs-autogen/test_escape_mdx.py -v +uv run pytest tooling/docs-autogen/test_cli_reference.py -v # Find MDX files not in docs.json navigation uv run poe apidocs-orphans @@ -252,8 +292,8 @@ most common cases but has not been fully verified end-to-end. | Symptom | Fix | | --- | --- | | `No module named 'mdxify'` | `uv add --dev mdxify griffe` | -| `Could not parse expression with acorn` | Unescaped `{}` — run `make docs` to regenerate | +| `Could not parse expression with acorn` | Unescaped `{}` — run `uv run poe apidocs` to regenerate | | `VIRTUAL_ENV … does not match` warning | Harmless — `uv run` uses the project venv regardless | | Port 3000 in use | `lsof -ti:3000 \| xargs kill -9` | -| Duplicate preamble / double dividers in MDX | Files were decorated twice — run `make docs` (which starts from fresh generation) | +| Duplicate preamble / double dividers in MDX | Files were decorated twice — run `uv run poe apidocs` (starts from fresh generation) | | Griffe loading wrong package when using `--source-dir` | Expected — Griffe uses `try_relative_path=False` to avoid loading same-named packages from CWD | From 558b5997b6c6878e6938290b551e5672f9e23509 Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Tue, 14 Apr 2026 18:38:49 +0100 Subject: [PATCH 09/11] docs: fix pipeline diagram and AGENTS.md traceability for CLI reference - Pipeline Overview: replace validate.py (step 3) with the actual build.py step 3 (generate-ast.py --nav-only); move validate.py to an "optional tools" section below the diagram - AGENTS.md: add test command and link to docs-autogen README so the full local testing workflow is traceable from the agent guide --- AGENTS.md | 2 +- tooling/docs-autogen/README.md | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fabbdd2a9..1dc299113 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -88,7 +88,7 @@ mkdir -p .bob && ln -s ../.agents/skills .bob/skills - Use `...` in `@generative` function bodies - Prefer primitives over classes - **Friendly Dependency Errors**: Wraps optional backend imports in `try/except ImportError` with a helpful message (e.g., "Please pip install mellea[hf]"). See `mellea/stdlib/session.py` for examples. -- **CLI command docstrings**: Typer command functions in `cli/` follow an enriched convention with `Prerequisites:` and `See Also:` sections — these feed the auto-generated CLI reference page. See [`docs/docs/guide/CONTRIBUTING.md`](docs/docs/guide/CONTRIBUTING.md) for the full pattern. Regenerate after changes: `uv run poe clidocs`. +- **CLI command docstrings**: Typer command functions in `cli/` follow an enriched convention with `Prerequisites:` and `See Also:` sections — these feed the auto-generated CLI reference page. See [`docs/docs/guide/CONTRIBUTING.md`](docs/docs/guide/CONTRIBUTING.md) for the full pattern. Regenerate after changes: `uv run poe clidocs`. Test the generator: `uv run pytest tooling/docs-autogen/test_cli_reference.py -v`. Full pipeline docs: [`tooling/docs-autogen/README.md`](tooling/docs-autogen/README.md). - **Backend telemetry fields**: All backends must populate `mot.usage` (dict with `prompt_tokens`, `completion_tokens`, `total_tokens`), `mot.model` (str), and `mot.provider` (str) in their `post_processing()` method. `mot.streaming` (bool) and `mot.ttfb_ms` (float | None) are set automatically in `astream()` — backends do not need to set them. Metrics are automatically recorded by `TokenMetricsPlugin` and `LatencyMetricsPlugin` — don't add manual `record_token_usage_metrics()` or `record_request_duration()` calls. ## 6. Commits & Hooks diff --git a/tooling/docs-autogen/README.md b/tooling/docs-autogen/README.md index ea9f5325e..913fc320a 100644 --- a/tooling/docs-autogen/README.md +++ b/tooling/docs-autogen/README.md @@ -66,24 +66,29 @@ docs/docs/api/ (fresh copy, replaces previous entirely) - Add CLASS/FUNC pills and visual dividers to headings │ ▼ -[3] validate.py (optional, run via uv run poe apidocs-validate) - - GitHub source links correct? - - API coverage ≥ threshold? - - MDX syntax valid (no unescaped braces)? - - Internal cross-reference links resolve? - - No duplicate heading anchors? +[3] generate-ast.py --nav-only + - Re-reads decorated files to rebuild the landing page and nav + - Ensures API Reference tab cards show full module descriptions │ ▼ -[4] generate_cli_reference.py +[4] generate_cli_reference.py (--strict in CI / apidocs; lenient via clidocs) - Imports Typer app, walks Click command tree - Extracts flags, types, defaults, help strings - Parses docstring sections (Prerequisites, Output, Examples, See Also) - Emits docs/docs/reference/cli.md (standard Markdown) - - --strict fails on incomplete docstrings │ ▼ [5] Mintlify dev server (mintlify dev from docs/docs/) http://localhost:3000 + +── Optional tools (not part of the build pipeline) ────────────────────────── + +validate.py (uv run poe apidocs-validate) + - GitHub source links correct? + - API coverage ≥ threshold? + - MDX syntax valid (no unescaped braces)? + - Internal cross-reference links resolve? + - No duplicate heading anchors? ``` ## File Structure From 9110fb53e32bed5a1de96c558bedb71143757af6 Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Tue, 14 Apr 2026 18:47:30 +0100 Subject: [PATCH 10/11] docs: add node version workaround and API 404 to troubleshooting - Prerequisites: document node@22 PATH override for systems running node v25+ (mintlify dev rejects non-LTS versions) - Troubleshooting: add entries for node version error and API Reference tab 404 (generated artefacts must be built first) --- tooling/docs-autogen/README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tooling/docs-autogen/README.md b/tooling/docs-autogen/README.md index 913fc320a..31b8546e1 100644 --- a/tooling/docs-autogen/README.md +++ b/tooling/docs-autogen/README.md @@ -9,7 +9,14 @@ documentation using Mintlify. uv sync --all-extras --group dev # Same as CI — installs all extras + dev tools ``` -Node.js (LTS, v22 or earlier) is required for `mintlify dev` previews. +Node.js (LTS, v22 or earlier) is required for `mintlify dev` previews. If your +default node is newer (e.g. v25), prefix the command with the v22 bin path: + +```bash +env PATH="/opt/homebrew/opt/node@22/bin:$PATH" mintlify dev +``` + +Install node@22 via Homebrew if needed: `brew install node@22`. ## Quick Start @@ -300,5 +307,7 @@ most common cases but has not been fully verified end-to-end. | `Could not parse expression with acorn` | Unescaped `{}` — run `uv run poe apidocs` to regenerate | | `VIRTUAL_ENV … does not match` warning | Harmless — `uv run` uses the project venv regardless | | Port 3000 in use | `lsof -ti:3000 \| xargs kill -9` | +| `mint dev is not supported on node versions 25+` | Run `env PATH="/opt/homebrew/opt/node@22/bin:$PATH" mintlify dev` | +| API Reference tab shows 404 locally | Generated artefacts are gitignored — run `uv run poe apidocs` first | | Duplicate preamble / double dividers in MDX | Files were decorated twice — run `uv run poe apidocs` (starts from fresh generation) | | Griffe loading wrong package when using `--source-dir` | Expected — Griffe uses `try_relative_path=False` to avoid loading same-named packages from CWD | From 708a193f70ee5d57805c41f0e8b942c55cfce48a Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Tue, 14 Apr 2026 18:47:58 +0100 Subject: [PATCH 11/11] docs: simplify node version requirement and troubleshooting entries --- tooling/docs-autogen/README.md | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tooling/docs-autogen/README.md b/tooling/docs-autogen/README.md index 31b8546e1..6811a9650 100644 --- a/tooling/docs-autogen/README.md +++ b/tooling/docs-autogen/README.md @@ -9,14 +9,7 @@ documentation using Mintlify. uv sync --all-extras --group dev # Same as CI — installs all extras + dev tools ``` -Node.js (LTS, v22 or earlier) is required for `mintlify dev` previews. If your -default node is newer (e.g. v25), prefix the command with the v22 bin path: - -```bash -env PATH="/opt/homebrew/opt/node@22/bin:$PATH" mintlify dev -``` - -Install node@22 via Homebrew if needed: `brew install node@22`. +Node.js LTS (v22 or earlier) is required for `mintlify dev` previews. ## Quick Start @@ -307,7 +300,7 @@ most common cases but has not been fully verified end-to-end. | `Could not parse expression with acorn` | Unescaped `{}` — run `uv run poe apidocs` to regenerate | | `VIRTUAL_ENV … does not match` warning | Harmless — `uv run` uses the project venv regardless | | Port 3000 in use | `lsof -ti:3000 \| xargs kill -9` | -| `mint dev is not supported on node versions 25+` | Run `env PATH="/opt/homebrew/opt/node@22/bin:$PATH" mintlify dev` | +| `mint dev is not supported on node versions 25+` | Switch to Node.js LTS v22 via your version manager (nvm, volta, fnm) | | API Reference tab shows 404 locally | Generated artefacts are gitignored — run `uv run poe apidocs` first | | Duplicate preamble / double dividers in MDX | Files were decorated twice — run `uv run poe apidocs` (starts from fresh generation) | | Griffe loading wrong package when using `--source-dir` | Expected — Griffe uses `try_relative_path=False` to avoid loading same-named packages from CWD |