diff --git a/scripts/check_style.py b/scripts/check_style.py index 0ced611..b95d919 100755 --- a/scripts/check_style.py +++ b/scripts/check_style.py @@ -234,7 +234,7 @@ def hex_is_prose(text: str, match: re.Match, ctx: FileContext) -> bool: # section 3 is a default and section 4 says so. PHASE_BLOCKING = { 1: {"paths", "links", "agents", "hygiene"}, - 2: {"paths", "links", "agents", "hygiene", "content", "naming", "language"}, + 2: {"paths", "links", "agents", "hygiene", "content", "naming", "language", "docs"}, } @@ -398,6 +398,30 @@ def check_agent_files(root: Path, report: Report) -> None: ) +def check_docs_baseline(root: Path, report: Report) -> None: + """The file set a reader expects, split by what the repository is. + + A LICENSE is the signal: a repository that grants rights to outside readers + owes them a contribution path, a way to report a vulnerability, and a + conduct policy. A repository that does not grant them is internal, and an + internal repository needs one thing from this list — where credentials live + and what to do when one leaks — not a public disclosure process it will + never run. + """ + if (root / "LICENSE").exists(): + required = ["CONTRIBUTING.md", "SECURITY.md", "CODE_OF_CONDUCT.md"] + kind = "public" + else: + required = ["SECURITY.md"] + kind = "private" + missing = [name for name in required if not (root / name).exists()] + if missing: + report.add( + "docs", + f"{kind} repository is missing {', '.join(missing)} (section 14)", + ) + + def check_language(root: Path, report: Report) -> None: marketing = re.compile( r"\b(" + "|".join(re.escape(w) for w in MARKETING_WORDS) + r")\b", re.IGNORECASE @@ -522,6 +546,7 @@ def main() -> int: check_links(root, report) check_agent_files(root, report) check_language(root, report) + check_docs_baseline(root, report) check_leaks(root, report) check_internal_paths(root, report) diff --git a/scripts/test_style.py b/scripts/test_style.py index 45369d9..f2e8b5c 100644 --- a/scripts/test_style.py +++ b/scripts/test_style.py @@ -281,6 +281,37 @@ def test_cross_repository_reference_never_blocks(self): self.assertEqual(report.blocking_findings(), []) +class DocsBaselineTest(CheckerTestCase): + def test_public_repository_needs_the_reader_file_set(self): + self.write("LICENSE", "MIT\n") + report = self.report() + check_style.check_docs_baseline(self.root, report) + found = self.messages(report, "docs") + self.assertTrue(any("CONTRIBUTING.md" in m for m in found)) + self.assertTrue(any("CODE_OF_CONDUCT.md" in m for m in found)) + + def test_public_repository_with_the_file_set_passes(self): + self.write("LICENSE", "MIT\n") + for name in ("CONTRIBUTING.md", "SECURITY.md", "CODE_OF_CONDUCT.md"): + self.write(name, "# " + name + "\n") + report = self.report() + check_style.check_docs_baseline(self.root, report) + self.assertEqual(self.messages(report, "docs"), []) + + def test_private_repository_needs_only_the_credential_doc(self): + # No LICENSE means the repository grants nothing to outside readers, so + # a public disclosure process would be a document nobody runs. + self.write("SECURITY.md", "# Credentials\n") + report = self.report() + check_style.check_docs_baseline(self.root, report) + self.assertEqual(self.messages(report, "docs"), []) + + def test_private_repository_without_the_credential_doc_is_reported(self): + report = self.report() + check_style.check_docs_baseline(self.root, report) + self.assertTrue(any("SECURITY.md" in m for m in self.messages(report, "docs"))) + + class PhaseTest(CheckerTestCase): def test_phase_one_does_not_block_naming(self): self.write("docs/hk_equity_runtime.md", "# x\n")