From 59fbe934f7bd47480d7cd9f53371be0b3aae7289 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 5 Aug 2026 09:54:56 -0500 Subject: [PATCH 1/5] docs: enforce heading title case with Vale Heading capitalization has only been caught by human review. Add a Vale prose-lint hook so it is checked mechanically. The rule uses Chicago style, which keeps short prepositions and conjunctions lowercase ('Where to Find Examples'), matching how these docs already read. Fixes the 22 existing headings that did not comply; all are case-only edits, so RST underline lengths and HTML anchors are unchanged. Note that Vale exceptions match whole words and a single match skips the entire heading, so the exceptions list must stay narrow. This is documented in the rule file and docs/cuopt/README.md. Signed-off-by: Ramakrishna Prabhu --- .pre-commit-config.yaml | 5 ++ .vale.ini | 5 ++ ci/vale/styles/cuOpt/Headings.yml | 47 +++++++++++++++++++ .../all_cuda-129_arch-aarch64.yaml | 1 + .../all_cuda-129_arch-x86_64.yaml | 1 + .../all_cuda-133_arch-aarch64.yaml | 1 + .../all_cuda-133_arch-x86_64.yaml | 1 + dependencies.yaml | 1 + docs/cuopt/README.md | 25 ++++++++++ .../source/cuopt-c/convex/convex-examples.rst | 2 +- .../cuopt/source/cuopt-c/mip/mip-examples.rst | 4 +- docs/cuopt/source/cuopt-grpc/advanced.rst | 2 +- .../cuopt-grpc/grpc-server-architecture.md | 12 ++--- .../source/cuopt-python/mip/mip-examples.rst | 2 +- .../cuopt-python/routing/routing-examples.rst | 2 +- .../cuopt-server/examples/lp-examples.rst | 8 ++-- .../examples/routing-examples.rst | 2 +- docs/cuopt/source/faq.rst | 2 +- docs/cuopt/source/hidden/mps-api.rst | 2 +- docs/cuopt/source/hidden/parser_example.rst | 2 +- docs/cuopt/source/resources.rst | 2 +- docs/cuopt/source/system-requirements.rst | 2 +- 22 files changed, 109 insertions(+), 22 deletions(-) create mode 100644 .vale.ini create mode 100644 ci/vale/styles/cuOpt/Headings.yml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ba5636bab6..ade9f591b4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -135,6 +135,11 @@ repos: language: system pass_filenames: false files: ^(VERSION|\.claude-plugin/marketplace\.json|\.cursor-plugin/plugin\.json|gemini-extension\.json)$ + - id: vale + name: Vale docs prose lint + entry: vale + language: system + files: ^docs/cuopt/source/.*\.(rst|md)$ - id: validate-skills name: Validate agent skills entry: ci/utils/validate_skills.sh diff --git a/.vale.ini b/.vale.ini new file mode 100644 index 0000000000..7d4b11968d --- /dev/null +++ b/.vale.ini @@ -0,0 +1,5 @@ +StylesPath = ci/vale/styles +MinAlertLevel = error + +[docs/cuopt/source/**/*.{rst,md}] +BasedOnStyles = cuOpt diff --git a/ci/vale/styles/cuOpt/Headings.yml b/ci/vale/styles/cuOpt/Headings.yml new file mode 100644 index 0000000000..090b75ce55 --- /dev/null +++ b/ci/vale/styles/cuOpt/Headings.yml @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Section headings use title case. Product names, acronyms, and API +# identifiers keep their own casing and are listed under `exceptions`. +extends: capitalization +message: "Heading '%s' should use title case." +level: error +scope: heading +match: $title +style: Chicago +exceptions: + # Product and library names + - cuOpt + - cuDF + - cuDSS + - cuML + - cuSolver + - cuSparse + - cuopt_cli + - cuopt_grpc_server + - cuopt_sh + - cuopt_sh_client + - gRPC + - mTLS + - nvidia + - pip + - conda + - kubeadm + - Docker + - Helm + - Kubernetes + - PyPI + - NGC + # Package / distribution names as they appear on PyPI, conda, and NGC. + # + # Keep this list narrow. Exceptions match whole words, case-sensitively, and + # a single match makes Vale skip the ENTIRE heading. A broad entry therefore + # disables the check for every heading that mentions it -- a bare "cuopt", + # for example, also silences "Contact us - cuopt@nvidia.com". Re-run + # `vale docs/cuopt/source` after editing this list. + - libcuopt + - (cuopt) + - cuopt-server + - cuopt-cli + # API identifiers that must keep their source spelling + - solver_configs diff --git a/conda/environments/all_cuda-129_arch-aarch64.yaml b/conda/environments/all_cuda-129_arch-aarch64.yaml index 82ad4010c5..0cb4ce50c9 100644 --- a/conda/environments/all_cuda-129_arch-aarch64.yaml +++ b/conda/environments/all_cuda-129_arch-aarch64.yaml @@ -81,6 +81,7 @@ dependencies: - sysroot_linux-aarch64==2.28 - tbb-devel - uvicorn==0.34.* +- vale=3.17.0 - zlib - pip: - nvidia-sphinx-theme diff --git a/conda/environments/all_cuda-129_arch-x86_64.yaml b/conda/environments/all_cuda-129_arch-x86_64.yaml index b293e2d8c0..eea8cc2b7f 100644 --- a/conda/environments/all_cuda-129_arch-x86_64.yaml +++ b/conda/environments/all_cuda-129_arch-x86_64.yaml @@ -81,6 +81,7 @@ dependencies: - sysroot_linux-64==2.28 - tbb-devel - uvicorn==0.34.* +- vale=3.17.0 - zlib - pip: - nvidia-sphinx-theme diff --git a/conda/environments/all_cuda-133_arch-aarch64.yaml b/conda/environments/all_cuda-133_arch-aarch64.yaml index 1cb8efbe75..a8e85d0771 100644 --- a/conda/environments/all_cuda-133_arch-aarch64.yaml +++ b/conda/environments/all_cuda-133_arch-aarch64.yaml @@ -81,6 +81,7 @@ dependencies: - sysroot_linux-aarch64==2.28 - tbb-devel - uvicorn==0.34.* +- vale=3.17.0 - zlib - pip: - nvidia-sphinx-theme diff --git a/conda/environments/all_cuda-133_arch-x86_64.yaml b/conda/environments/all_cuda-133_arch-x86_64.yaml index c49b656860..c232592cfb 100644 --- a/conda/environments/all_cuda-133_arch-x86_64.yaml +++ b/conda/environments/all_cuda-133_arch-x86_64.yaml @@ -81,6 +81,7 @@ dependencies: - sysroot_linux-64==2.28 - tbb-devel - uvicorn==0.34.* +- vale=3.17.0 - zlib - pip: - nvidia-sphinx-theme diff --git a/dependencies.yaml b/dependencies.yaml index fe16140fd9..e72f572fb8 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -787,6 +787,7 @@ dependencies: - clang==20.1.8 - clang-tools=20.1.8 - &doxygen doxygen=1.9.1 # pre-commit hook needs a specific version. + - vale=3.17.0 # docs prose linter, run by the vale pre-commit hook. docs: common: - output_types: [conda] diff --git a/docs/cuopt/README.md b/docs/cuopt/README.md index a6570eb1a0..064ea51330 100644 --- a/docs/cuopt/README.md +++ b/docs/cuopt/README.md @@ -31,3 +31,28 @@ Then, navigate a web browser to the IP address or hostname of the host machine a http://:8000 ``` Now you can check if your docs edits formatted correctly, and read well. + +## Prose Style Checks + +Headings under `docs/cuopt/source/` use **title case**, enforced by +[Vale](https://vale.sh/) through the `vale` pre-commit hook. The rule lives in +`ci/vale/styles/cuOpt/Headings.yml` and follows Chicago style, so short +prepositions and conjunctions stay lowercase: + +- `Connect and Solve`, `Where to Find Examples`, `Working with Incumbent Solutions` +- not `Connect and solve`, `Where To Find Examples` + +Run it directly with: + +```bash +vale docs/cuopt/source +``` + +Product names, acronyms, and API identifiers that must keep their own casing +(`cuOpt`, `gRPC`, `mTLS`, `solver_configs`) are listed under `exceptions` in +that file. + +**Keep that list narrow.** Exceptions match whole words, case-sensitively, and +a single match makes Vale skip the entire heading — so a broad entry silently +disables the check for every heading that mentions it. Always re-run `vale` +after editing the exceptions. diff --git a/docs/cuopt/source/cuopt-c/convex/convex-examples.rst b/docs/cuopt/source/cuopt-c/convex/convex-examples.rst index a57cd67b8a..70211660a5 100644 --- a/docs/cuopt/source/cuopt-c/convex/convex-examples.rst +++ b/docs/cuopt/source/cuopt-c/convex/convex-examples.rst @@ -3,7 +3,7 @@ Convex Optimization C API Examples ================================== -LP Example With Data +LP Example with Data -------------------- This example demonstrates how to use the LP solver in C. More details on the API can be found in :doc:`C API `. diff --git a/docs/cuopt/source/cuopt-c/mip/mip-examples.rst b/docs/cuopt/source/cuopt-c/mip/mip-examples.rst index d5b9fb8869..624e4b001a 100644 --- a/docs/cuopt/source/cuopt-c/mip/mip-examples.rst +++ b/docs/cuopt/source/cuopt-c/mip/mip-examples.rst @@ -2,7 +2,7 @@ MIP C API Examples =================== -Example With Data +Example with Data ----------------- This example demonstrates how to use the MIP solver in C. More details on the API can be found in :doc:`MIP C API `. @@ -75,7 +75,7 @@ You should see the following output: Test completed successfully! -Example With MPS File +Example with MPS File --------------------- This example demonstrates how to use the cuOpt solver in C to solve an MPS file. diff --git a/docs/cuopt/source/cuopt-grpc/advanced.rst b/docs/cuopt/source/cuopt-grpc/advanced.rst index a092f026c3..0dc289a4b5 100644 --- a/docs/cuopt/source/cuopt-grpc/advanced.rst +++ b/docs/cuopt/source/cuopt-grpc/advanced.rst @@ -13,7 +13,7 @@ For RPC summaries and server behavior, see :doc:`api` and :doc:`grpc-server-arch Configuration Parameters ======================== -``cuopt_grpc_server`` (host or explicit container command) +``cuopt_grpc_server`` (Host or Explicit Container Command) ------------------------------------------------------------ Run ``cuopt_grpc_server --help`` for the full list. Typical flags (also passable inside ``CUOPT_GRPC_ARGS`` when using the container entrypoint): diff --git a/docs/cuopt/source/cuopt-grpc/grpc-server-architecture.md b/docs/cuopt/source/cuopt-grpc/grpc-server-architecture.md index 450947de6c..57dc471c48 100644 --- a/docs/cuopt/source/cuopt-grpc/grpc-server-architecture.md +++ b/docs/cuopt/source/cuopt-grpc/grpc-server-architecture.md @@ -4,19 +4,19 @@ NVIDIA cuOpt's **`cuopt_grpc_server`** uses one **main process** (gRPC front end Implementation details (IPC layout, C++ source map, chunked transfer internals) live in the contributor reference: **`cpp/docs/grpc-server-architecture.md`** in the NVIDIA cuOpt repository. -## Process model +## Process Model ![gRPC Server Process Model](images/grpc-process-model.png) -## Job lifecycle (summary) +## Job Lifecycle (Summary) **Submit** → the server assigns a job id and queues work. **Process** → a worker pulls the problem, solves on the GPU, and streams the result back. **Retrieve** → the client uses status and result RPCs (including chunked download when needed). See [gRPC API (reference)](api.rst) for RPC names. -## Job states +## Job States ![gRPC Server Job States](images/grpc-job-states.png) -## Logs, capacity, and workers +## Logs, Capacity, and Workers | Topic | Detail | |-------|--------| @@ -24,14 +24,14 @@ Implementation details (IPC layout, C++ source map, chunked transfer internals) | Default caps | Up to **100** queued jobs and **100** stored results (server compile-time limits). | | Workers | Recommended: **1 worker process per GPU**. Higher values are possible depending on the problems being solved but there is no specific guidance at this time. | -## Fault tolerance and cancellation +## Fault Tolerance and Cancellation - If a **worker process crashes**, jobs it was running are marked **FAILED**; the server can spawn replacement workers (see contributor doc for details). - **`CancelJob`** cancels **queued** jobs immediately (the worker skips them). If the solver has already started, the **worker process is killed** and the job is marked **CANCELLED**; a replacement worker is spawned automatically. - **Ctrl-C / SIGTERM** cancels active jobs, kills worker processes, and shuts the server down without waiting for an in-flight solve to finish. - **`DeleteResult`** also cancels a queued or running job (same kill/skip behavior as ``CancelJob``), then removes all server-side state for that ``job_id``. -## Further reading +## Further Reading - [Advanced configuration](advanced.rst) — `cuopt_grpc_server` **command-line flags**, TLS, Docker (`CUOPT_SERVER_TYPE`, `CUOPT_GRPC_ARGS`), and **client** environment variables (authoritative for operators). - [gRPC API (reference)](api.rst) — `CuOptRemoteService` RPC overview. diff --git a/docs/cuopt/source/cuopt-python/mip/mip-examples.rst b/docs/cuopt/source/cuopt-python/mip/mip-examples.rst index 1c76598be1..e44695bdcf 100644 --- a/docs/cuopt/source/cuopt-python/mip/mip-examples.rst +++ b/docs/cuopt/source/cuopt-python/mip/mip-examples.rst @@ -28,7 +28,7 @@ The response is as follows: Objective value = 303.0 -Semi-continuous Variable Example +Semi-Continuous Variable Example -------------------------------- :download:`semi_continuous_example.py ` diff --git a/docs/cuopt/source/cuopt-python/routing/routing-examples.rst b/docs/cuopt/source/cuopt-python/routing/routing-examples.rst index 3bc5400c4b..c506312678 100644 --- a/docs/cuopt/source/cuopt-python/routing/routing-examples.rst +++ b/docs/cuopt/source/cuopt-python/routing/routing-examples.rst @@ -4,7 +4,7 @@ Routing Examples This section contains examples for the cuOpt routing Python API. -Intra-factory Transport +Intra-Factory Transport ----------------------- A capacitated pickup-and-delivery problem with time windows (PDPTW) for a fleet diff --git a/docs/cuopt/source/cuopt-server/examples/lp-examples.rst b/docs/cuopt/source/cuopt-server/examples/lp-examples.rst index 3ec25a79f9..f42da06930 100644 --- a/docs/cuopt/source/cuopt-server/examples/lp-examples.rst +++ b/docs/cuopt/source/cuopt-server/examples/lp-examples.rst @@ -208,7 +208,7 @@ The response would be as follows: } -Using MPS or LP file directly +Using MPS or LP File Directly ----------------------------- The self-hosted client accepts both MPS and LP format files — the client @@ -276,7 +276,7 @@ the client dispatches on the file extension (``.mps`` / ``.qps`` vs ``.lp``, including ``.gz`` / ``.bz2`` compressed variants). For solver settings see :doc:`convex optimization parameters <../../convex-settings>` and :doc:`MIP parameters <../../mip-settings>`. -MPS format +MPS Format ~~~~~~~~~~ :download:`mps_datamodel_example.py ` @@ -285,7 +285,7 @@ MPS format :language: python :linenos: -LP format +LP Format ~~~~~~~~~ :download:`lp_datamodel_example.py ` @@ -294,7 +294,7 @@ LP format :language: python :linenos: -Expected output (either example, same problem instance) +Expected Output (Either Example, Same Problem Instance) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text diff --git a/docs/cuopt/source/cuopt-server/examples/routing-examples.rst b/docs/cuopt/source/cuopt-server/examples/routing-examples.rst index 763dc6fc59..fdef42f318 100644 --- a/docs/cuopt/source/cuopt-server/examples/routing-examples.rst +++ b/docs/cuopt/source/cuopt-server/examples/routing-examples.rst @@ -282,7 +282,7 @@ Please refer to the :ref:`aborting-cli` in the MIP examples for more details. .. note:: Please use solver settings while using .mps files. -To enable HTTPS +To Enable HTTPS ---------------- * In the case of the server using public certificates, simply enable https. diff --git a/docs/cuopt/source/faq.rst b/docs/cuopt/source/faq.rst index 90b45d5a24..eb04ff6c1b 100644 --- a/docs/cuopt/source/faq.rst +++ b/docs/cuopt/source/faq.rst @@ -177,7 +177,7 @@ General FAQ while openssl x509 -noout -text; do :; done < test.pem.txt -gRPC remote execution (``cuopt_grpc_server``) +gRPC Remote Execution (``cuopt_grpc_server``) ----------------------------------------------- .. dropdown:: Where are log files for the gRPC server / StreamLogs? diff --git a/docs/cuopt/source/hidden/mps-api.rst b/docs/cuopt/source/hidden/mps-api.rst index ec2be8df66..6590bf785c 100644 --- a/docs/cuopt/source/hidden/mps-api.rst +++ b/docs/cuopt/source/hidden/mps-api.rst @@ -2,7 +2,7 @@ cuOpt MPS/LP Parser API Reference =============================== -MPS/QPS/LP parser +MPS/QPS/LP Parser ------------------- .. autofunction:: cuopt.linear_programming.io.Read diff --git a/docs/cuopt/source/hidden/parser_example.rst b/docs/cuopt/source/hidden/parser_example.rst index 9f79545e80..7b0e215ee8 100644 --- a/docs/cuopt/source/hidden/parser_example.rst +++ b/docs/cuopt/source/hidden/parser_example.rst @@ -1,5 +1,5 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -cuOpt problem file parser example +cuOpt Problem File Parser Example ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/cuopt/source/resources.rst b/docs/cuopt/source/resources.rst index 145e750864..c121fa9f61 100644 --- a/docs/cuopt/source/resources.rst +++ b/docs/cuopt/source/resources.rst @@ -33,5 +33,5 @@ Please note that you need to choose a `Runtime` as `GPU` in order to run the not `Blogs `_ ---------------------------------------------------------------------------- -Contact us - cuopt@nvidia.com +Contact Us - cuopt@nvidia.com ----------------------------- diff --git a/docs/cuopt/source/system-requirements.rst b/docs/cuopt/source/system-requirements.rst index 26dcf84b22..d6ea01ef19 100644 --- a/docs/cuopt/source/system-requirements.rst +++ b/docs/cuopt/source/system-requirements.rst @@ -95,7 +95,7 @@ Container * `nvidia-container-toolkit `_ needs to be installed -Thin-client for Self-Hosted +Thin-Client for Self-Hosted ---------------------------- * OS: Linux From 6b2af2e49bab542ff554883eb987886174422c26 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 5 Aug 2026 10:18:29 -0500 Subject: [PATCH 2/5] docs: prune Vale exceptions and add a rule self-test The exceptions list carried 19 speculative entries. Each one is a liability rather than dead weight: Vale skips an entire heading when an exception matches it, so an unnecessary term silently stops the rule from checking every heading that mentions it. A leave-one-out check showed only 7 of the 26 entries were load-bearing; drop the rest. Add ci/utils/check_vale_rule.sh to make that failure mode loud. It asserts the rule still flags known violations and still accepts correct headings, and runs as a pre-commit hook whenever ci/vale/ changes, so a weakened rule fails CI instead of passing quietly. Signed-off-by: Ramakrishna Prabhu --- .pre-commit-config.yaml | 6 ++++ ci/utils/check_vale_rule.sh | 56 +++++++++++++++++++++++++++++++ ci/vale/styles/cuOpt/Headings.yml | 41 ++++++---------------- ci/vale/tests/.vale.ini | 5 +++ ci/vale/tests/must-flag.md | 14 ++++++++ ci/vale/tests/must-pass.md | 15 +++++++++ docs/cuopt/README.md | 20 ++++++++--- 7 files changed, 123 insertions(+), 34 deletions(-) create mode 100755 ci/utils/check_vale_rule.sh create mode 100644 ci/vale/tests/.vale.ini create mode 100644 ci/vale/tests/must-flag.md create mode 100644 ci/vale/tests/must-pass.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ade9f591b4..d6c4458854 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -140,6 +140,12 @@ repos: entry: vale language: system files: ^docs/cuopt/source/.*\.(rst|md)$ + - id: vale-selftest + name: Vale headings rule self-test + entry: ci/utils/check_vale_rule.sh + language: system + pass_filenames: false + files: ^ci/vale/ - id: validate-skills name: Validate agent skills entry: ci/utils/validate_skills.sh diff --git a/ci/utils/check_vale_rule.sh b/ci/utils/check_vale_rule.sh new file mode 100755 index 0000000000..1f2feaaacd --- /dev/null +++ b/ci/utils/check_vale_rule.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Self-test for the cuOpt.Headings Vale rule. +# +# Vale skips an entire heading when an `exceptions` entry matches it, so a +# careless entry can silently stop the rule from checking anything. This +# asserts the rule still flags what it should, and still accepts what it +# should, independently of what the real docs happen to contain. + +set -euo pipefail + +TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../vale/tests" && pwd)" +CONFIG="${TESTS_DIR}/.vale.ini" +STATUS=0 + +# Each heading in must-flag.md is a violation; every one must be reported. +missing=$(python3 - "$TESTS_DIR" "$CONFIG" <<'PY' +import re +import subprocess +import sys + +tests_dir, config = sys.argv[1], sys.argv[2] +path = f"{tests_dir}/must-flag.md" + +expected = [m.group(1) for m in re.finditer(r"^## (.+)$", open(path).read(), re.M)] +proc = subprocess.run( + ["vale", f"--config={config}", "--output=line", path], + capture_output=True, text=True, +) +reported = proc.stdout +print("\n".join(h for h in expected if h not in reported)) +PY +) + +if [ -n "$missing" ]; then + echo "ERROR: the Headings rule no longer flags these violations:" + echo "$missing" | sed 's/^/ - /' + echo "An over-broad 'exceptions' entry in Headings.yml is the usual cause." + STATUS=1 +fi + +# Nothing in must-pass.md may be reported. +if ! output=$(vale --config="$CONFIG" --output=line "${TESTS_DIR}/must-pass.md" 2>&1); then + echo "ERROR: the Headings rule flags headings that are already correct:" + echo "$output" | sed 's/^/ /' + STATUS=1 +fi + +if [ "$STATUS" -eq 0 ]; then + echo "Headings rule self-test passed." +fi + +exit "$STATUS" diff --git a/ci/vale/styles/cuOpt/Headings.yml b/ci/vale/styles/cuOpt/Headings.yml index 090b75ce55..9d80fcdbe8 100644 --- a/ci/vale/styles/cuOpt/Headings.yml +++ b/ci/vale/styles/cuOpt/Headings.yml @@ -1,47 +1,28 @@ # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # -# Section headings use title case. Product names, acronyms, and API -# identifiers keep their own casing and are listed under `exceptions`. +# Section headings under docs/cuopt/source use title case (Chicago style, so +# short prepositions and conjunctions stay lowercase). extends: capitalization message: "Heading '%s' should use title case." level: error scope: heading match: $title style: Chicago + +# Terms that keep their own casing. +# +# Every entry here is load-bearing: removing any one of them makes a real +# heading fail. Do not add a term speculatively. Exceptions match whole words +# and a single match makes Vale skip the ENTIRE heading, so a needless entry +# silently disables the check for every heading that mentions it. +# +# `ci/utils/check_vale_rule.sh` guards against that; run it after editing. exceptions: - # Product and library names - cuOpt - - cuDF - cuDSS - - cuML - - cuSolver - - cuSparse - - cuopt_cli - - cuopt_grpc_server - - cuopt_sh - - cuopt_sh_client - gRPC - mTLS - - nvidia - - pip - - conda - - kubeadm - - Docker - - Helm - - Kubernetes - - PyPI - - NGC - # Package / distribution names as they appear on PyPI, conda, and NGC. - # - # Keep this list narrow. Exceptions match whole words, case-sensitively, and - # a single match makes Vale skip the ENTIRE heading. A broad entry therefore - # disables the check for every heading that mentions it -- a bare "cuopt", - # for example, also silences "Contact us - cuopt@nvidia.com". Re-run - # `vale docs/cuopt/source` after editing this list. - - libcuopt - (cuopt) - cuopt-server - - cuopt-cli - # API identifiers that must keep their source spelling - solver_configs diff --git a/ci/vale/tests/.vale.ini b/ci/vale/tests/.vale.ini new file mode 100644 index 0000000000..2230005169 --- /dev/null +++ b/ci/vale/tests/.vale.ini @@ -0,0 +1,5 @@ +StylesPath = ../styles +MinAlertLevel = error + +[*.md] +BasedOnStyles = cuOpt diff --git a/ci/vale/tests/must-flag.md b/ci/vale/tests/must-flag.md new file mode 100644 index 0000000000..defc51d0ef --- /dev/null +++ b/ci/vale/tests/must-flag.md @@ -0,0 +1,14 @@ +# Headings That Must Be Flagged + +Every heading below violates the title-case rule. `check_vale_rule.sh` asserts +Vale reports each one. If an edit to `Headings.yml` makes any of them pass, the +rule has been silently weakened -- most likely by an over-broad `exceptions` +entry, which makes Vale skip the whole heading. + +## Process model + +## cuOpt process model + +## gRPC job states + +## Using MPS or LP file directly diff --git a/ci/vale/tests/must-pass.md b/ci/vale/tests/must-pass.md new file mode 100644 index 0000000000..47439ca957 --- /dev/null +++ b/ci/vale/tests/must-pass.md @@ -0,0 +1,15 @@ +# Headings That Must Pass + +Every heading below is correct title case. `check_vale_rule.sh` asserts Vale +reports none of them, so the rule cannot drift into demanding capitalized +prepositions ("Where To Find Examples") or mangling product names. + +## Where to Find Examples + +## Connect and Solve + +## How mTLS Works + +## Start the Server with TLS + +## Job States diff --git a/docs/cuopt/README.md b/docs/cuopt/README.md index 064ea51330..ef8650a8de 100644 --- a/docs/cuopt/README.md +++ b/docs/cuopt/README.md @@ -52,7 +52,19 @@ Product names, acronyms, and API identifiers that must keep their own casing (`cuOpt`, `gRPC`, `mTLS`, `solver_configs`) are listed under `exceptions` in that file. -**Keep that list narrow.** Exceptions match whole words, case-sensitively, and -a single match makes Vale skip the entire heading — so a broad entry silently -disables the check for every heading that mentions it. Always re-run `vale` -after editing the exceptions. +**Keep that list narrow, and only add a term once a real heading needs it.** +Exceptions match whole words, and a single match makes Vale skip the *entire* +heading — so a needless entry silently disables the check for every heading +that mentions it. + +That failure mode is guarded by a self-test: + +```bash +./ci/utils/check_vale_rule.sh +``` + +It asserts the rule still flags the violations in `ci/vale/tests/must-flag.md` +and still accepts the correct headings in `ci/vale/tests/must-pass.md`. It runs +as a pre-commit hook whenever `ci/vale/` changes, so weakening the rule fails +CI instead of passing quietly. Add a case to those fixtures when you add an +exception. From 18bdb7805c164ef8e79cdd522d2212ece4ef5cad Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 5 Aug 2026 10:22:29 -0500 Subject: [PATCH 3/5] docs: generate Vale self-test fixtures instead of checking them in The fixtures were only ever input to the self-test; Vale needs a file to lint, so the test has to hand it a document. Build them in a temp dir from two lists at the top of the script rather than keeping three files under ci/vale/tests, which keeps the expected headings next to the assertions that use them. Signed-off-by: Ramakrishna Prabhu --- .pre-commit-config.yaml | 2 +- ci/utils/check_vale_rule.sh | 86 ++++++++++++++++++++++++------------- ci/vale/tests/.vale.ini | 5 --- ci/vale/tests/must-flag.md | 14 ------ ci/vale/tests/must-pass.md | 15 ------- docs/cuopt/README.md | 9 ++-- 6 files changed, 62 insertions(+), 69 deletions(-) delete mode 100644 ci/vale/tests/.vale.ini delete mode 100644 ci/vale/tests/must-flag.md delete mode 100644 ci/vale/tests/must-pass.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d6c4458854..aa2f5d2161 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -145,7 +145,7 @@ repos: entry: ci/utils/check_vale_rule.sh language: system pass_filenames: false - files: ^ci/vale/ + files: ^ci/(vale/|utils/check_vale_rule\.sh) - id: validate-skills name: Validate agent skills entry: ci/utils/validate_skills.sh diff --git a/ci/utils/check_vale_rule.sh b/ci/utils/check_vale_rule.sh index 1f2feaaacd..b5181824db 100755 --- a/ci/utils/check_vale_rule.sh +++ b/ci/utils/check_vale_rule.sh @@ -7,50 +7,78 @@ # # Vale skips an entire heading when an `exceptions` entry matches it, so a # careless entry can silently stop the rule from checking anything. This -# asserts the rule still flags what it should, and still accepts what it +# asserts the rule still flags what it should and still accepts what it # should, independently of what the real docs happen to contain. +# +# Add a case below when you add an exception. set -euo pipefail -TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../vale/tests" && pwd)" -CONFIG="${TESTS_DIR}/.vale.ini" -STATUS=0 +# Headings that violate title case. Every one must be reported. +MUST_FLAG=( + "Process model" + "cuOpt process model" + "gRPC job states" + "Using MPS or LP file directly" +) -# Each heading in must-flag.md is a violation; every one must be reported. -missing=$(python3 - "$TESTS_DIR" "$CONFIG" <<'PY' -import re -import subprocess -import sys +# Correct headings. None may be reported. +MUST_PASS=( + "Where to Find Examples" + "Connect and Solve" + "How mTLS Works" + "Start the Server with TLS" + "Job States" +) -tests_dir, config = sys.argv[1], sys.argv[2] -path = f"{tests_dir}/must-flag.md" +STYLES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../vale/styles" && pwd)" +WORK_DIR="$(mktemp -d)" +trap 'rm -rf "${WORK_DIR}"' EXIT -expected = [m.group(1) for m in re.finditer(r"^## (.+)$", open(path).read(), re.M)] -proc = subprocess.run( - ["vale", f"--config={config}", "--output=line", path], - capture_output=True, text=True, -) -reported = proc.stdout -print("\n".join(h for h in expected if h not in reported)) -PY -) +cat > "${WORK_DIR}/.vale.ini" < "${path}" + printf '## %s\n\n' "$@" >> "${path}" +} + +write_doc "${WORK_DIR}/must-flag.md" "${MUST_FLAG[@]}" +write_doc "${WORK_DIR}/must-pass.md" "${MUST_PASS[@]}" + +STATUS=0 + +reported=$(vale --config="${WORK_DIR}/.vale.ini" --output=line \ + "${WORK_DIR}/must-flag.md" 2>&1 || true) +for heading in "${MUST_FLAG[@]}"; do + if [[ "${reported}" != *"${heading}"* ]]; then + if [ "${STATUS}" -eq 0 ]; then + echo "ERROR: the Headings rule no longer flags these violations:" + fi + echo " - ${heading}" + STATUS=1 + fi +done +if [ "${STATUS}" -ne 0 ]; then echo "An over-broad 'exceptions' entry in Headings.yml is the usual cause." - STATUS=1 fi -# Nothing in must-pass.md may be reported. -if ! output=$(vale --config="$CONFIG" --output=line "${TESTS_DIR}/must-pass.md" 2>&1); then +if ! output=$(vale --config="${WORK_DIR}/.vale.ini" --output=line \ + "${WORK_DIR}/must-pass.md" 2>&1); then echo "ERROR: the Headings rule flags headings that are already correct:" - echo "$output" | sed 's/^/ /' + echo "${output}" | sed 's/^/ /' STATUS=1 fi -if [ "$STATUS" -eq 0 ]; then +if [ "${STATUS}" -eq 0 ]; then echo "Headings rule self-test passed." fi -exit "$STATUS" +exit "${STATUS}" diff --git a/ci/vale/tests/.vale.ini b/ci/vale/tests/.vale.ini deleted file mode 100644 index 2230005169..0000000000 --- a/ci/vale/tests/.vale.ini +++ /dev/null @@ -1,5 +0,0 @@ -StylesPath = ../styles -MinAlertLevel = error - -[*.md] -BasedOnStyles = cuOpt diff --git a/ci/vale/tests/must-flag.md b/ci/vale/tests/must-flag.md deleted file mode 100644 index defc51d0ef..0000000000 --- a/ci/vale/tests/must-flag.md +++ /dev/null @@ -1,14 +0,0 @@ -# Headings That Must Be Flagged - -Every heading below violates the title-case rule. `check_vale_rule.sh` asserts -Vale reports each one. If an edit to `Headings.yml` makes any of them pass, the -rule has been silently weakened -- most likely by an over-broad `exceptions` -entry, which makes Vale skip the whole heading. - -## Process model - -## cuOpt process model - -## gRPC job states - -## Using MPS or LP file directly diff --git a/ci/vale/tests/must-pass.md b/ci/vale/tests/must-pass.md deleted file mode 100644 index 47439ca957..0000000000 --- a/ci/vale/tests/must-pass.md +++ /dev/null @@ -1,15 +0,0 @@ -# Headings That Must Pass - -Every heading below is correct title case. `check_vale_rule.sh` asserts Vale -reports none of them, so the rule cannot drift into demanding capitalized -prepositions ("Where To Find Examples") or mangling product names. - -## Where to Find Examples - -## Connect and Solve - -## How mTLS Works - -## Start the Server with TLS - -## Job States diff --git a/docs/cuopt/README.md b/docs/cuopt/README.md index ef8650a8de..ad859509ab 100644 --- a/docs/cuopt/README.md +++ b/docs/cuopt/README.md @@ -63,8 +63,7 @@ That failure mode is guarded by a self-test: ./ci/utils/check_vale_rule.sh ``` -It asserts the rule still flags the violations in `ci/vale/tests/must-flag.md` -and still accepts the correct headings in `ci/vale/tests/must-pass.md`. It runs -as a pre-commit hook whenever `ci/vale/` changes, so weakening the rule fails -CI instead of passing quietly. Add a case to those fixtures when you add an -exception. +It checks the rule against two lists of headings at the top of that script — +one that must be flagged, one that must pass — so weakening the rule fails +loudly. It runs as a pre-commit hook whenever `ci/vale/` changes. Add a case to +those lists when you add an exception. From b6d932ee150f54e9bc09b1acbe9516ceb55dc1f6 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 5 Aug 2026 10:28:22 -0500 Subject: [PATCH 4/5] docs: drop the Vale rule self-test Keep the surface minimal. The README now shows the one-line manual check to run after editing the exceptions list instead. Signed-off-by: Ramakrishna Prabhu --- .pre-commit-config.yaml | 6 --- ci/utils/check_vale_rule.sh | 84 ------------------------------------- docs/cuopt/README.md | 13 ++---- 3 files changed, 4 insertions(+), 99 deletions(-) delete mode 100755 ci/utils/check_vale_rule.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aa2f5d2161..ade9f591b4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -140,12 +140,6 @@ repos: entry: vale language: system files: ^docs/cuopt/source/.*\.(rst|md)$ - - id: vale-selftest - name: Vale headings rule self-test - entry: ci/utils/check_vale_rule.sh - language: system - pass_filenames: false - files: ^ci/(vale/|utils/check_vale_rule\.sh) - id: validate-skills name: Validate agent skills entry: ci/utils/validate_skills.sh diff --git a/ci/utils/check_vale_rule.sh b/ci/utils/check_vale_rule.sh deleted file mode 100755 index b5181824db..0000000000 --- a/ci/utils/check_vale_rule.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash - -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -# Self-test for the cuOpt.Headings Vale rule. -# -# Vale skips an entire heading when an `exceptions` entry matches it, so a -# careless entry can silently stop the rule from checking anything. This -# asserts the rule still flags what it should and still accepts what it -# should, independently of what the real docs happen to contain. -# -# Add a case below when you add an exception. - -set -euo pipefail - -# Headings that violate title case. Every one must be reported. -MUST_FLAG=( - "Process model" - "cuOpt process model" - "gRPC job states" - "Using MPS or LP file directly" -) - -# Correct headings. None may be reported. -MUST_PASS=( - "Where to Find Examples" - "Connect and Solve" - "How mTLS Works" - "Start the Server with TLS" - "Job States" -) - -STYLES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../vale/styles" && pwd)" -WORK_DIR="$(mktemp -d)" -trap 'rm -rf "${WORK_DIR}"' EXIT - -cat > "${WORK_DIR}/.vale.ini" < "${path}" - printf '## %s\n\n' "$@" >> "${path}" -} - -write_doc "${WORK_DIR}/must-flag.md" "${MUST_FLAG[@]}" -write_doc "${WORK_DIR}/must-pass.md" "${MUST_PASS[@]}" - -STATUS=0 - -reported=$(vale --config="${WORK_DIR}/.vale.ini" --output=line \ - "${WORK_DIR}/must-flag.md" 2>&1 || true) -for heading in "${MUST_FLAG[@]}"; do - if [[ "${reported}" != *"${heading}"* ]]; then - if [ "${STATUS}" -eq 0 ]; then - echo "ERROR: the Headings rule no longer flags these violations:" - fi - echo " - ${heading}" - STATUS=1 - fi -done -if [ "${STATUS}" -ne 0 ]; then - echo "An over-broad 'exceptions' entry in Headings.yml is the usual cause." -fi - -if ! output=$(vale --config="${WORK_DIR}/.vale.ini" --output=line \ - "${WORK_DIR}/must-pass.md" 2>&1); then - echo "ERROR: the Headings rule flags headings that are already correct:" - echo "${output}" | sed 's/^/ /' - STATUS=1 -fi - -if [ "${STATUS}" -eq 0 ]; then - echo "Headings rule self-test passed." -fi - -exit "${STATUS}" diff --git a/docs/cuopt/README.md b/docs/cuopt/README.md index ad859509ab..5593d881a3 100644 --- a/docs/cuopt/README.md +++ b/docs/cuopt/README.md @@ -55,15 +55,10 @@ that file. **Keep that list narrow, and only add a term once a real heading needs it.** Exceptions match whole words, and a single match makes Vale skip the *entire* heading — so a needless entry silently disables the check for every heading -that mentions it. - -That failure mode is guarded by a self-test: +that mentions it, with no visible symptom. After editing the list, confirm the +rule still catches a violation: ```bash -./ci/utils/check_vale_rule.sh +printf '# T\n\n## Process model\n' > /tmp/vale-check.md +vale --config=.vale.ini /tmp/vale-check.md # must report an error ``` - -It checks the rule against two lists of headings at the top of that script — -one that must be flagged, one that must pass — so weakening the rule fails -loudly. It runs as a pre-commit hook whenever `ci/vale/` changes. Add a case to -those lists when you add an exception. From 750b93c055ef913de64b06a71aa6428c3ea2c541 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 5 Aug 2026 11:25:06 -0500 Subject: [PATCH 5/5] ci: add docutils to the checks environment for Vale Vale parses .rst by shelling out to docutils' rst2html. The checks environment does not otherwise pull in docutils, so the vale hook failed in CI with 'rst2html not found' while passing locally, where Sphinx (and a system python3-docutils) already provide the binary. Signed-off-by: Ramakrishna Prabhu --- conda/environments/all_cuda-129_arch-aarch64.yaml | 1 + conda/environments/all_cuda-129_arch-x86_64.yaml | 1 + conda/environments/all_cuda-133_arch-aarch64.yaml | 1 + conda/environments/all_cuda-133_arch-x86_64.yaml | 1 + dependencies.yaml | 3 +++ 5 files changed, 7 insertions(+) diff --git a/conda/environments/all_cuda-129_arch-aarch64.yaml b/conda/environments/all_cuda-129_arch-aarch64.yaml index 0cb4ce50c9..80a7d6554a 100644 --- a/conda/environments/all_cuda-129_arch-aarch64.yaml +++ b/conda/environments/all_cuda-129_arch-aarch64.yaml @@ -22,6 +22,7 @@ dependencies: - cupy>=14.0.1,!=14.1.0 - cxx-compiler - cython>=3.0.3 +- docutils>=0.21 - doxygen=1.9.1 - fastapi - gcc_linux-aarch64=14.* diff --git a/conda/environments/all_cuda-129_arch-x86_64.yaml b/conda/environments/all_cuda-129_arch-x86_64.yaml index eea8cc2b7f..d7629c1b1e 100644 --- a/conda/environments/all_cuda-129_arch-x86_64.yaml +++ b/conda/environments/all_cuda-129_arch-x86_64.yaml @@ -22,6 +22,7 @@ dependencies: - cupy>=14.0.1,!=14.1.0 - cxx-compiler - cython>=3.0.3 +- docutils>=0.21 - doxygen=1.9.1 - fastapi - gcc_linux-64=14.* diff --git a/conda/environments/all_cuda-133_arch-aarch64.yaml b/conda/environments/all_cuda-133_arch-aarch64.yaml index a8e85d0771..d3072b8be1 100644 --- a/conda/environments/all_cuda-133_arch-aarch64.yaml +++ b/conda/environments/all_cuda-133_arch-aarch64.yaml @@ -22,6 +22,7 @@ dependencies: - cupy>=14.0.1,!=14.1.0 - cxx-compiler - cython>=3.0.3 +- docutils>=0.21 - doxygen=1.9.1 - fastapi - gcc_linux-aarch64=14.* diff --git a/conda/environments/all_cuda-133_arch-x86_64.yaml b/conda/environments/all_cuda-133_arch-x86_64.yaml index c232592cfb..c227032e4c 100644 --- a/conda/environments/all_cuda-133_arch-x86_64.yaml +++ b/conda/environments/all_cuda-133_arch-x86_64.yaml @@ -22,6 +22,7 @@ dependencies: - cupy>=14.0.1,!=14.1.0 - cxx-compiler - cython>=3.0.3 +- docutils>=0.21 - doxygen=1.9.1 - fastapi - gcc_linux-64=14.* diff --git a/dependencies.yaml b/dependencies.yaml index e72f572fb8..ee873f1294 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -788,6 +788,9 @@ dependencies: - clang-tools=20.1.8 - &doxygen doxygen=1.9.1 # pre-commit hook needs a specific version. - vale=3.17.0 # docs prose linter, run by the vale pre-commit hook. + # Vale parses .rst by shelling out to docutils' rst2html; without it + # the vale hook fails with "rst2html not found". + - docutils>=0.21 docs: common: - output_types: [conda]