docs: update documentation files and fix linting issues

This commit is contained in:
2026-07-07 13:32:48 +03:00
parent 9bf1e1333e
commit be3e288e0d
27 changed files with 423 additions and 250 deletions
+152 -68
View File
@@ -12,25 +12,23 @@ Exit codes:
"""
import argparse
import json
import os
import re
import subprocess
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
# ---------------------------------------------------------------------------
# Types
# ---------------------------------------------------------------------------
@dataclass
class Finding:
severity: str # "ERROR" | "WARN" | "INFO"
source: str # which doc file
section: str # which section
source: str # which doc file
section: str # which section
message: str
@@ -82,9 +80,20 @@ def all_source_files() -> list[Path]:
"""Walk the source tree and return all .py files."""
files = []
for root, dirs, filenames in os.walk(ROOT):
dirs[:] = [d for d in dirs if not d.startswith(".")
and d not in ("__pycache__", ".pytest_cache", ".mypy_cache",
".ruff_cache", ".hypothesis", ".ipynb_checkpoints")]
dirs[:] = [
d
for d in dirs
if not d.startswith(".")
and d
not in (
"__pycache__",
".pytest_cache",
".mypy_cache",
".ruff_cache",
".hypothesis",
".ipynb_checkpoints",
)
]
if "egg-info" in root:
continue
for fn in filenames:
@@ -97,6 +106,7 @@ def all_source_files() -> list[Path]:
# Checks
# ---------------------------------------------------------------------------
def check_version_consistency(report: Report):
"""Check that __version__ matches pyproject.toml version."""
pyproject = ROOT / "pyproject.toml"
@@ -116,8 +126,12 @@ def check_version_consistency(report: Report):
ver = ver_match.group(1)
if ver != py_ver:
rel = pyfile.relative_to(ROOT)
report.add("ERROR", rel.name, "version",
f"__version__ = '{ver}' but pyproject.toml says '{py_ver}'")
report.add(
"ERROR",
rel.name,
"version",
f"__version__ = '{ver}' but pyproject.toml says '{py_ver}'",
)
def check_version_in_code(report: Report):
@@ -134,16 +148,19 @@ def check_version_in_code(report: Report):
try:
result = subprocess.run(
["bs_pnpi", "--version"],
capture_output=True, text=True, timeout=10
["bs_pnpi", "--version"], capture_output=True, text=True, timeout=10
)
if result.returncode == 0:
cli_ver_match = re.search(r'(\d+\.\d+\.\d+)', result.stdout)
cli_ver_match = re.search(r"(\d+\.\d+\.\d+)", result.stdout)
if cli_ver_match:
cli_ver = cli_ver_match.group(1)
if cli_ver != py_ver:
report.add("ERROR", "cli", "version",
f"CLI reports version '{cli_ver}' but pyproject.toml says '{py_ver}'")
report.add(
"ERROR",
"cli",
"version",
f"CLI reports version '{cli_ver}' but pyproject.toml says '{py_ver}'",
)
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
@@ -168,8 +185,10 @@ def check_file_references(report: Report):
# Skip tree diagrams and multi-line code blocks.
for line in content.split("\n"):
# Find single-line inline code that looks like a file path
refs = re.findall(r'`([^`\n]+\.py[^`]*)`', line)
refs.extend(re.findall(r'`([^`\n]+\\.(csv|json|toml|md|yaml|png|log))`', line))
refs = re.findall(r"`([^`\n]+\.py[^`]*)`", line)
refs.extend(
re.findall(r"`([^`\n]+\\.(csv|json|toml|md|yaml|png|log))`", line)
)
for ref in refs:
if isinstance(ref, tuple):
ref = ref[0]
@@ -183,20 +202,40 @@ def check_file_references(report: Report):
if target.exists():
continue
# Filter out tree-diagram artifacts
if any(c in ref for c in ('\u2190', '\u2192', '\u2713', '\u2717',
'[', ']', '|', '\u2502', '\u2514', '\u251c',
'\u2500', '\u2502', '\u2560', '\u256c')):
if any(
c in ref
for c in (
"\u2190",
"\u2192",
"\u2713",
"\u2717",
"[",
"]",
"|",
"\u2502",
"\u2514",
"\u251c",
"\u2500",
"\u2502",
"\u2560",
"\u256c",
)
):
continue
# Only warn if it looks like a real file path
if re.search(r'[a-zA-Z0-9_\-]+\.[a-zA-Z0-9]+', ref):
if re.search(r"[a-zA-Z0-9_\-]+\.[a-zA-Z0-9]+", ref):
basename = ref.split("/")[-1]
if basename in known_removed or basename in known_placeholders:
continue
# Skip bare __init__.py (common in tree diagrams, not actionable)
if basename == "__init__.py":
continue
report.add("WARN", doc_name, "file-reference",
f"Referenced file not found: `{ref}`")
report.add(
"WARN",
doc_name,
"file-reference",
f"Referenced file not found: `{ref}`",
)
def check_cli_commands(report: Report):
@@ -207,12 +246,15 @@ def check_cli_commands(report: Report):
if "bs_pnpi" in agents_md:
try:
result = subprocess.run(
["bs_pnpi", "--help"],
capture_output=True, text=True, timeout=10
["bs_pnpi", "--help"], capture_output=True, text=True, timeout=10
)
if result.returncode != 0:
report.add("WARN", "AGENTS.md", "cli",
f"`bs_pnpi --help` failed: {result.stderr.strip()[:100]}")
report.add(
"WARN",
"AGENTS.md",
"cli",
f"`bs_pnpi --help` failed: {result.stderr.strip()[:100]}",
)
except (subprocess.TimeoutExpired, FileNotFoundError):
report.add("WARN", "AGENTS.md", "cli", "bs_pnpi not found in PATH")
@@ -220,12 +262,15 @@ def check_cli_commands(report: Report):
if "bs_exp" in agents_md:
try:
result = subprocess.run(
["bs_exp", "--help"],
capture_output=True, text=True, timeout=10
["bs_exp", "--help"], capture_output=True, text=True, timeout=10
)
if result.returncode != 0:
report.add("WARN", "AGENTS.md", "cli",
f"`bs_exp --help` failed: {result.stderr.strip()[:100]}")
report.add(
"WARN",
"AGENTS.md",
"cli",
f"`bs_exp --help` failed: {result.stderr.strip()[:100]}",
)
except (subprocess.TimeoutExpired, FileNotFoundError):
report.add("WARN", "AGENTS.md", "cli", "bs_exp not found in PATH")
@@ -234,15 +279,15 @@ def check_imports_in_examples(report: Report):
"""Check that import statements in README examples are valid."""
readme = read_doc("README.md")
code_blocks = re.findall(r'```[^\n]*\n(.*?)```', readme, re.DOTALL)
code_blocks = re.findall(r"```[^\n]*\n(.*?)```", readme, re.DOTALL)
for block in code_blocks:
for line in block.strip().split("\n"):
line = line.strip()
if not line.startswith("from ") and not line.startswith("import "):
continue
match = re.match(r'from\s+(\S+)\s+import', line)
match = re.match(r"from\s+(\S+)\s+import", line)
if not match:
match = re.match(r'import\s+(\S+)', line)
match = re.match(r"import\s+(\S+)", line)
if not match:
continue
@@ -250,8 +295,12 @@ def check_imports_in_examples(report: Report):
try:
__import__(module)
except ImportError:
report.add("WARN", "README.md", "imports",
f"Cannot import module in example: `{module}`")
report.add(
"WARN",
"README.md",
"imports",
f"Cannot import module in example: `{module}`",
)
def check_directory_structure(report: Report):
@@ -268,12 +317,14 @@ def check_directory_structure(report: Report):
def _check_tree_in_doc(content: str, doc_name: str, report: Report):
"""Extract file references from a markdown tree diagram and check existence."""
# Find fenced code blocks that look like directory trees
trees = re.findall(r'```\w*\n(.*?)```', content, re.DOTALL)
trees = re.findall(r"```\w*\n(.*?)```", content, re.DOTALL)
for tree in trees:
lines = tree.split("\n")
for i, line in enumerate(lines):
# Match lines like: `├── filename.py` or `│ ├── filename.py`
match = re.search(r'[\u251c\u2514\u2502\s]*[\u2500\u2501]+\s+([a-zA-Z0-9_\-]+\.\w+)', line)
match = re.search(
r"[\u251c\u2514\u2502\s]*[\u2500\u2501]+\s+([a-zA-Z0-9_\-]+\.\w+)", line
)
if match:
filename = match.group(1)
# Determine depth by counting leading tree chars
@@ -288,20 +339,27 @@ def _check_tree_in_doc(content: str, doc_name: str, report: Report):
seen_dir = False
for j in range(i - 1, max(i - 10, -1), -1):
bl = lines[j]
dir_match = re.search(r'[\u251c\u2514\u2502\s]*[\u2500\u2501]+\s+([a-zA-Z0-9_\-]+)/', bl)
dir_match = re.search(
r"[\u251c\u2514\u2502\s]*[\u2500\u2501]+\s+([a-zA-Z0-9_\-]+)/",
bl,
)
if dir_match:
dir_depth = len(bl) - len(bl.lstrip())
if dir_depth < depth:
dirs.append(dir_match.group(1))
seen_dir = True
elif re.search(r'\w', bl): # non-empty, non-matching line
elif re.search(r"\w", bl): # non-empty, non-matching line
if seen_dir:
break # stop looking after first non-matching content line
if dirs and len(dirs) <= 3: # cap at 3 levels
target = ROOT / "/".join(reversed(dirs)) / filename
if not target.exists():
report.add("WARN", doc_name, "structure",
f"File in structure diagram not found: `{'/'.join(reversed(dirs))}/{filename}`")
report.add(
"WARN",
doc_name,
"structure",
f"File in structure diagram not found: `{'/'.join(reversed(dirs))}/{filename}`",
)
def check_cli_args_consistency(report: Report):
@@ -311,30 +369,44 @@ def check_cli_args_consistency(report: Report):
try:
result = subprocess.run(
["bs_pnpi", "--help"],
capture_output=True, text=True, timeout=10
["bs_pnpi", "--help"], capture_output=True, text=True, timeout=10
)
if result.returncode == 0:
actual_flags = set()
for line in result.stdout.split("\n"):
flag_match = re.match(r'\s{0,2}(-\S+)', line)
flag_match = re.match(r"\s{0,2}(-\S+)", line)
if flag_match:
actual_flags.add(flag_match.group(1))
known_flags = {"-v", "-vv", "-q", "--nuclide", "--output",
"--input", "--dry-run", "--version", "--plot",
"--log-file", "--intensity-cutoff", "--help",
"-h"}
known_flags = {
"-v",
"-vv",
"-q",
"--nuclide",
"--output",
"--input",
"--dry-run",
"--version",
"--plot",
"--log-file",
"--intensity-cutoff",
"--help",
"-h",
}
mentioned = set()
# Only match actual CLI flag patterns (--flag-name, not random words)
for flag in re.findall(r'--[a-z][a-z0-9-]{2,}', agents_md):
for flag in re.findall(r"--[a-z][a-z0-9-]{2,}", agents_md):
mentioned.add(flag)
for flag in re.findall(r'-[a-z][a-z0-9-]{2,}', readme):
for flag in re.findall(r"-[a-z][a-z0-9-]{2,}", readme):
mentioned.add(flag)
unknown = mentioned - known_flags
for flag in sorted(unknown):
report.add("INFO", "AGENTS.md", "cli-args",
f"Flag `{flag}` mentioned but not in --help (may be custom)")
report.add(
"INFO",
"AGENTS.md",
"cli-args",
f"Flag `{flag}` mentioned but not in --help (may be custom)",
)
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
@@ -350,8 +422,9 @@ def check_markdown_formatting(report: Report):
content = path.read_text()
for i, line in enumerate(content.split("\n"), 1):
if line != line.rstrip():
report.add("INFO", doc_name, f"line {i}",
"Trailing whitespace detected")
report.add(
"INFO", doc_name, f"line {i}", "Trailing whitespace detected"
)
break # one per file is enough
@@ -361,18 +434,24 @@ def check_todo_completions(report: Report):
# Find completed items that claim specific files/methods exist
# Pattern: "- [x] ... implemented: `SomeClass` or `method_name`"
completed = re.findall(r'- \[x\].*?(?=- \[|$)', todo, re.DOTALL)
completed = re.findall(r"- \[x\].*?(?=- \[|$)", todo, re.DOTALL)
for block in completed:
# Skip historical references (items about cleanup, moves, removals)
if re.search(r'(?:remove|moved|replaced|deleted|clean up|cleanup)', block, re.IGNORECASE):
if re.search(
r"(?:remove|moved|replaced|deleted|clean up|cleanup)", block, re.IGNORECASE
):
continue
# Look for specific file claims
file_claims = re.findall(r'`([^`]+\.py)`', block)
file_claims = re.findall(r"`([^`]+\.py)`", block)
for fc in file_claims:
target = ROOT / fc
if not target.exists():
report.add("WARN", "TODO.md", "completions",
f"Completed item references non-existent file: `{fc}`")
report.add(
"WARN",
"TODO.md",
"completions",
f"Completed item references non-existent file: `{fc}`",
)
# ---------------------------------------------------------------------------
@@ -395,12 +474,10 @@ CHECKS = [
def main():
parser = argparse.ArgumentParser(description="Check documentation consistency")
parser.add_argument(
"--strict", action="store_true",
help="Treat warnings as errors (exit 1)"
"--strict", action="store_true", help="Treat warnings as errors (exit 1)"
)
parser.add_argument(
"--json", action="store_true", dest="as_json",
help="Output findings as JSON"
"--json", action="store_true", dest="as_json", help="Output findings as JSON"
)
args = parser.parse_args()
@@ -414,15 +491,22 @@ def main():
# Output
if args.as_json:
import json as _json
output = [
{"severity": f.severity, "source": f.source,
"section": f.section, "message": f.message}
{
"severity": f.severity,
"source": f.source,
"section": f.section,
"message": f.message,
}
for f in report.findings
]
print(_json.dumps(output, indent=2))
else:
print(f"docs_check: {len(report.findings)} findings "
f"({len(report.errors)} errors, {len(report.warnings)} warnings)")
print(
f"docs_check: {len(report.findings)} findings "
f"({len(report.errors)} errors, {len(report.warnings)} warnings)"
)
print(report.summary())
# Exit
-1
View File
@@ -10,7 +10,6 @@ Each plot gets a descriptive filename based on notebook name and cell index.
"""
import json
import os
import sys
from pathlib import Path
-1
View File
@@ -14,7 +14,6 @@ Usage:
"""
import json
import os
import sys
import base64
from pathlib import Path