diff --git a/src/docproof/cli.py b/src/docproof/cli.py index f972005..a1c6c53 100644 --- a/src/docproof/cli.py +++ b/src/docproof/cli.py @@ -18,7 +18,13 @@ superseded_lines, suppressed_lines, ) -from .docs import by_directory, find_docs, read, unread_documents +from .docs import ( + by_directory, + find_docs, + likeliest_docs_directory, + read, + unread_documents, +) from .history import classify, vanished_documents from .project import Project, find_root from .report import Report @@ -149,10 +155,21 @@ def report_coverage(project: Project, unread: list[Path]) -> None: f"default scope is top-level files plus doc/ and docs/" ) print(f" {where}") - print( - f" read them too with --docs '{groups[0][0]}/**/*.md' or " - f'[tool.docproof] docs = ["{groups[0][0]}/**/*.md"]' - ) + # The directory to widen TO is not the biggest one. Measured over sweep batch 10: + # the largest unread segments across twenty-three repositories are `skills/`, `tools/`, + # `src/`, `crates/` and `.changeset/`, and one entry in the top nine is documentation. + # See `likeliest_docs_directory`. + widen = likeliest_docs_directory(groups) + if widen: + print( + f" read them too with --docs '{widen}/**/*.md' or " + f'[tool.docproof] docs = ["{widen}/**/*.md"]' + ) + else: + print( + " none of them is named like a documentation tree; if one is, widen with " + "--docs 'DIR/**/*.md' or [tool.docproof] docs = [\"DIR/**/*.md\"]" + ) def report_set_aside(historical: list[str], disclaimed: dict[tuple[str, str], list[str]]) -> None: diff --git a/src/docproof/docs.py b/src/docproof/docs.py index ecb6fe2..a938a61 100644 --- a/src/docproof/docs.py +++ b/src/docproof/docs.py @@ -194,6 +194,42 @@ def by_directory(root: Path, paths: Iterable[Path]) -> list[tuple[str, int]]: return sorted(counts.items(), key=lambda item: (-item[1], item[0])) +# A directory whose NAME says it holds documentation, for the one line that tells the reader +# what to widen to. Everything here is a naming convention rather than a guess about contents. +DOCS_DIRECTORY = re.compile( + r"(?i)^(docs?([.\-_].*)?|.*\.wiki|.*wiki.*|handbooks?|guides?|manuals?|books?|" + r"readme_i18n|website|content|reference)$" +) + + +def likeliest_docs_directory(groups: list[tuple[str, int]]) -> str | None: + """Which unread directory to SUGGEST widening to, given (name, count) biggest first. + + **Measured across the twenty-three repositories of sweep batch 10, 2026-08-19.** They hold + 4,819 unread documentation files, and ranking the suggestion by count points at the wrong + directory most of the time, because the biggest unread directory in a monorepo is almost + never its documentation. The top segments were `skills/` 429, `tools/` 176, + `docs.feldera.com/` 131, `datafusion/` 123, `.changeset/` 108, `.claude/` 108, `src/` 97, + `backends/` 77, `crates/` 69. + + One of those nine is documentation. The rest are source-tree READMEs, agent skill + definitions and changelog fragments - which is to say `find_docs` is RIGHT to leave them + out, and its docstring already argues so. The defect was never the scope. It was that the + line telling a reader how to widen said `--docs 'ts/**/*.md'` at a project whose real + documentation site sat two entries below. + + Returns None when nothing in the unread tree is named like documentation, and the caller + says so rather than naming the biggest. Suggesting `--docs 'apps/**/*.md'` at a monorepo, + which is what ranking by count did to `superset-sh/superset`, tells the reader to widen + into exactly the package-internal READMEs `find_docs` excludes on purpose. Advice that + produces findings nobody wants is worse than no advice. + """ + for name, _ in groups: + if DOCS_DIRECTORY.match(name): + return name + return None + + def read(path: Path) -> str: """UTF-8 with universal newlines, so a CRLF checkout reads the same as a LF one.""" return path.read_text(encoding="utf-8", errors="replace").replace("\r\n", "\n") diff --git a/tests/test_coverage.py b/tests/test_coverage.py index 1e56e77..a8b867f 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -205,3 +205,44 @@ def test_full_coverage_adds_no_sentence(make_repo: Callable[..., Path], capsys) main([str(repo)]) out = capsys.readouterr().out assert "were never read" not in out + + +def test_the_widen_suggestion_names_the_documentation_tree(make_repo: Callable[..., Path], capsys) -> None: + """**Measured over sweep batch 10, twenty-three repositories, 4,819 unread files.** The + biggest unread directory in a real project is almost never its documentation: the top + segments were `skills/` 429, `tools/` 176, `docs.feldera.com/` 131, `datafusion/` 123, + `.changeset/` 108, `.claude/` 108, `src/` 97. One of the top nine is documentation. + + So ranking the suggestion by count told `immich` to read `mobile/`, `executorch` to read + `examples/` and `deepagents` to read `libs/`, while each of them keeps a wiki or an i18n + README tree the line never mentioned. + """ + files = {"README.md": README, "src/thing.py": "x = 1\n", "project.wiki/page.md": "# W\n"} + for n in range(6): + files[f"crates/pkg{n}/README.md"] = "# Crate\n" + repo = make_repo(files) + main([str(repo)]) + out = capsys.readouterr().out + assert "crates/ 6" in out, "the LIST still ranks by count, which shows the shape" + assert "--docs 'project.wiki/**/*.md'" in out + assert "--docs 'crates/**/*.md'" not in out + + +def test_no_documentation_tree_means_no_confident_suggestion(make_repo: Callable[..., Path], capsys) -> None: + """`superset-sh/superset` has 276 unread files and not one directory named like + documentation - `apps/ 116`, `plans/ 100`, `packages/ 21`. Naming the biggest would tell + the reader to widen into exactly the package-internal READMEs `find_docs` excludes on + purpose, so the advice would manufacture the findings the scope exists to avoid. + + Both widening routes are still printed, because a reader whose documentation genuinely + lives in an oddly named directory still needs to know how to say so. + """ + files = {"README.md": README, "src/thing.py": "x = 1\n"} + for n in range(4): + files[f"apps/app{n}/README.md"] = "# App\n" + repo = make_repo(files) + main([str(repo)]) + out = capsys.readouterr().out + assert "none of them is named like a documentation tree" in out + assert "--docs 'apps/**/*.md'" not in out + assert "--docs" in out and "[tool.docproof] docs" in out