From aeeb4a5bb24ba179b8c5a64eb3a8fdf75de3f84c Mon Sep 17 00:00:00 2001 From: melbinjp Date: Wed, 19 Aug 2026 17:50:42 +0530 Subject: [PATCH 1/2] The argparse flag set is not complete for a command built with typer This file's docstring promises the one thing that just happened cannot happen: when it says no, a flag it has not seen is unjudged, and the reason given is the incompleteness itself It said yes, from the wrong parsers. `unslothai/unsloth` declares `unsloth = unsloth_cli:app`, a typer application, and its README shows `unsloth start claude --as-subagent`. That option is declared at `unsloth_cli/commands/start.py:340` as `typer.Option(False, "--as-subagent", ...)`. docproof reported it BROKEN with "no parser in this project defines it", because the backend, tests and scripts elsewhere in the monorepo contain enough argparse for 153 flags and a complete verdict. The datasette rule above catches a project with NO argparse. It cannot catch a monorepo that has plenty, none of it behind the command being documented. So completeness is now asked of the COMMAND rather than of the repository: the console script names its module, and if that package imports typer, click, cleo, docopt or fire, the argparse set cannot describe it. Reads the package's own files only. A framework imported by a sibling tool in the same repository says nothing about this command, and widening it to the whole tree would silence the verifier on every repository that vendors an example. docproof itself stays complete at 6 flags. rigout stays incomplete for the reason it already was, a parser handed to helpers. 201 tests. --- src/docproof/parsers.py | 61 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/docproof/parsers.py b/src/docproof/parsers.py index a190c24..40f093b 100644 --- a/src/docproof/parsers.py +++ b/src/docproof/parsers.py @@ -23,6 +23,7 @@ from __future__ import annotations import ast +import re from dataclasses import dataclass, field from pathlib import Path @@ -191,9 +192,69 @@ def argparse_flags(project: Project) -> FlagSet: "no argparse parser was found in this project, so its command line is built " "with something this cannot read — click, typer, cleo or a hand-rolled parser" ) + for command, package in _console_script_packages(project): + elsewhere = _built_with_something_else(project, package) + if elsewhere: + flags.incomplete( + f"`{command}` is `{package}`, which imports {elsewhere}, so its options are " + f"declared where this cannot read them however much argparse the rest of the " + f"repository contains" + ) return flags +# **`seen_a_parser` is repository-wide, and a monorepo is not one program.** The datasette +# rule above catches a project with NO argparse. It does not catch `unslothai/unsloth`, whose +# console script is a typer app in `unsloth_cli/` and whose backend, tests and scripts contain +# enough argparse for 153 flags and a `complete: True`. Its README shows +# `unsloth start claude --as-subagent`, declared at `unsloth_cli/commands/start.py:340` as +# `typer.Option(False, "--as-subagent", ...)`, and the verifier reported it BROKEN with the +# sentence "no parser in this project defines it". +# +# That is the one thing this file's docstring promises cannot happen: *"when it says no, a flag +# it has not seen is unjudged."* It said yes, from the wrong parsers. +# +# So completeness is asked of the COMMAND, not of the repository. The console script names its +# module; if that package reaches for click or typer, the argparse set cannot describe it. +CLI_FRAMEWORKS = ("typer", "click", "cleo", "docopt", "fire") + + +def _console_script_packages(project: Project) -> list[tuple[str, str]]: + """(command, top-level module) for each declared console script, deduplicated.""" + out, seen = [], set() + for command, target in project.console_scripts.items(): + package = target.split(":", 1)[0].split(".", 1)[0].strip() + if package and (command, package) not in seen: + seen.add((command, package)) + out.append((command, package)) + return out + + +def _built_with_something_else(project: Project, package: str) -> str: + """Which CLI framework this package imports, or empty. + + Reads the package's own files only. A framework imported by a sibling tool in the same + repository says nothing about this command, and widening it to the whole tree would + silence the verifier on every repository that vendors an example. + """ + directory = project.root / package + if not directory.is_dir(): + single = project.root / (package + ".py") + files = [single] if single.is_file() else [] + else: + files = [p for p in directory.rglob("*.py") if p.is_file()] + found: set[str] = set() + for path in files[:400]: # a package with more files than this has been found already + try: + text = path.read_text(encoding="utf-8", errors="replace") + except OSError: + continue + for name in CLI_FRAMEWORKS: + if re.search(r"^\s*(?:import|from)\s+" + name + r"\b", text, re.MULTILINE): + found.add(name) + return " and ".join(sorted(found)) + + def wrapper_flags(project: Project) -> set[str]: """Options named in shell or PowerShell wrappers shipped beside the program. From fa796a7329c5db9c375e3070b80ade14b3c51893 Mon Sep 17 00:00:00 2001 From: melbinjp Date: Wed, 19 Aug 2026 18:04:14 +0530 Subject: [PATCH 2/2] Both sides of the typer rule, and the measurement that says what it costs Measured over 44 clones that declare a console script: 12 newly abstain, and SEVEN of those were already incomplete under the datasette rule with zero argparse found, so the new reason only makes theirs specific. Five genuinely change: openmed, opensre, nanobot, unsloth, mitmproxy. In those five the "complete" verdict was false, so this is not a recall cost. HKUDS/nanobot alone produced 93 findings, 90 of them cli-flag, and its console script is `nanobot.cli.entry:main` with every option declared as `typer.Option`. Spot-checked two: `--refresh` and `--wizard`, both at `nanobot/cli/commands.py:119-120`, both reported BROKEN. With the fix nanobot goes from 93 findings to ZERO. 93 of the 455 findings on disk across every corpus were this one defect. Two tests, one per direction: a typer console script beside plenty of argparse abstains, and an argparse console script keeps judging. 203 tests. --- tests/test_cli_flags.py | 45 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/test_cli_flags.py b/tests/test_cli_flags.py index 4ee92e0..b83e3be 100644 --- a/tests/test_cli_flags.py +++ b/tests/test_cli_flags.py @@ -206,3 +206,48 @@ def test_a_project_whose_cli_is_not_argparse_is_never_judged(make_repo: Callable verdict, detail = check(make_repo(files))["--serve"] assert verdict is Verdict.SKIPPED assert "no argparse parser" in detail + + +def test_a_typer_command_is_not_judged_by_argparse_found_elsewhere(make_repo: Callable[..., Path]): + """`unslothai/unsloth` declares `unsloth = unsloth_cli:app`, a typer application, and its + README shows `unsloth start claude --as-subagent`. That option is declared at + `unsloth_cli/commands/start.py:340` as `typer.Option(False, "--as-subagent", ...)`. + + docproof reported it BROKEN with "no parser in this project defines it", because the + backend, tests and scripts elsewhere in that monorepo hold enough argparse for 153 flags + and a complete verdict. `parsers.py` promises in its own docstring that this cannot + happen: *"when it says no, a flag it has not seen is unjudged."* It said yes, from the + wrong parsers. + + The datasette rule catches a project with NO argparse. It cannot catch a monorepo that has + plenty, none of it behind the command being documented. + """ + from docproof.parsers import argparse_flags + + repo = make_repo( + { + "pyproject.toml": PYPROJECT, + "README.md": "Run `toolkit --as-subagent` to attach it.\n", + "toolkit/cli.py": ( + "import typer\n\napp = typer.Typer()\n\nOPT = typer.Option(False, '--as-subagent')\n" + ), + # Plenty of argparse, none of it behind the console script. + "scripts/bench.py": PARSER, + } + ) + flags = argparse_flags(Project(root=repo)) + assert not flags.complete + assert any("which imports typer" in reason for reason in flags.reasons), flags.reasons + assert check(repo)["--as-subagent"][0] is Verdict.SKIPPED + + +def test_an_argparse_command_still_judges(make_repo: Callable[..., Path]): + """The recall side of the same rule. A project whose console script really is argparse + keeps its complete verdict, or the fix above would have silenced the verifier everywhere. + docproof itself is this case: 6 flags, complete.""" + from docproof.parsers import argparse_flags + + repo = make_repo(toolkit("Run `toolkit --dryrun` first.\n")) + flags = argparse_flags(Project(root=repo)) + assert flags.complete, flags.reasons + assert check(repo)["--dryrun"][0] is Verdict.BROKEN