Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 69 additions & 13 deletions scripts/check_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,15 @@
]

# Section 2, "Habits to avoid".
#
# `robust` and `powerful` are deliberately absent. Both are technical terms in
# this corpus — a robust estimator, statistical power — and a rule that flags
# them reported twelve non-defects in the research docs and one quotation of
# Hansen's SPA test. A word that is marketing in a landing page and a term of
# art in a research note cannot be decided by a word list, so the list keeps
# only the words that have no technical sense here.
MARKETING_WORDS = [
"powerful", "seamless", "robust", "cutting-edge", "comprehensive",
"seamless", "cutting-edge", "comprehensive",
"world-class", "next-generation", "state-of-the-art", "best-in-class",
]
FILLER_PHRASES = [
Expand All @@ -92,10 +99,12 @@
# Section 13 says "non-placeholder buckets" and section 7 line 104 says the
# placeholder form is `<bucket>`, `<project-id>`, `$PROJECTS_ROOT`. So a
# `gs://` URI whose bucket segment is a placeholder is the correct form, not a
# finding.
# finding. `synthetic-` is on the list because the organisation's fixtures are
# named that way; without it every fixture a doc quotes becomes a finding the
# day the doc is written.
PLACEHOLDER_BUCKET = re.compile(
r"^[<{$]|[<>{}]|\.\.\."
r"|^(?:your|my|example|sample|placeholder|shared)[-_]"
r"|^(?:your|my|example|sample|placeholder|shared|synthetic)[-_]"
r"|^bucket(?:-|$)"
)

Expand All @@ -108,6 +117,18 @@ class FileContext(NamedTuple):

url_spans: list[tuple[int, int]]
prose: str # the text with fenced blocks blanked out, offsets preserved
masked: str # `prose` with inline code blanked out too


def phrase_is_prose(text: str, match: re.Match, ctx: FileContext) -> bool:
"""A rule about phrasing describes prose; backticks name the phrase.

The style guide writes `this round` when it means the words, and a value
rule such as `gs://` is the opposite case: a bucket inside backticks is
still a bucket. That is why this is a per-rule predicate and not a blanket
skip on code spans.
"""
return bool(ctx.masked[match.start():match.end()].strip())


def bucket_is_real(text: str, match: re.Match, ctx: FileContext) -> bool:
Expand Down Expand Up @@ -167,12 +188,15 @@ def hex_is_prose(text: str, match: re.Match, ctx: FileContext) -> bool:
"content", "personal subdomain (section 13)", None),
(re.compile(r"\b[0-9a-f]{40}\b"),
"content", "40-character hex string in prose (section 5)", hex_is_prose),
(re.compile(r"\|\s*(PR\b|#\d+)"),
# `| PR |` is a table header; `| PR #12 |` and `| #12 |` are the rows this
# rule is about. Matching the header too reported the guide's own row
# describing the rule.
(re.compile(r"\|\s*(?:PR\s*#?\d+|#\d+)"),
"content", "PR-number status table (section 5)", None),
(re.compile(r"\b(this round|this task|in this task)\b", re.IGNORECASE),
"content", "task-scope wording (section 5)", None),
"content", "task-scope wording (section 5)", phrase_is_prose),
(re.compile(r"\b(the owner approved|approved by)\b", re.IGNORECASE),
"content", "owner approval note (section 5)", None),
"content", "owner approval note (section 5)", phrase_is_prose),
]


Expand Down Expand Up @@ -328,8 +352,12 @@ def check_links(root: Path, report: Report) -> None:
# cannot verify. Reporting it as broken makes the check unusable in
# CI while passing locally, which is the worst of both.
if not resolved.is_relative_to(root):
# `xref` is not in PHASE_BLOCKING, so this stays a note in
# both phases. `tools/check_cross_repo_links.py` resolves these
# against the sibling's default branch on a schedule; a check
# that says "not checked" has no standing to block.
report.add(
"content",
"xref",
f"{path.relative_to(root).as_posix()}: cross-repository reference, not checked -> {target}",
)
continue
Expand Down Expand Up @@ -381,17 +409,38 @@ def check_language(root: Path, report: Report) -> None:
if not is_markdown(path):
continue
rel = path.relative_to(root).as_posix()
for number, line in enumerate(
path.read_text(encoding="utf-8", errors="ignore").splitlines(), 1
):
# A word inside a fenced block or a code span is being named, not used.
# The guide lists the words it forbids, and without this the guide is
# the largest offender in the organisation.
text = blank_code_spans(blank_fenced_blocks(path.read_text(encoding="utf-8", errors="ignore")))
first_heading = True
for number, line in enumerate(text.splitlines(), 1):
for match in marketing.finditer(line):
report.add("language", f"{rel}:{number}: marketing word '{match.group(0)}' (section 2)")
for match in filler.finditer(line):
report.add("language", f"{rel}:{number}: filler phrase '{match.group(0)}' (section 2)")
if line.startswith("#") and has_emoji(line):
report.add("language", f"{rel}:{number}: emoji in heading (section 2)")
if line.startswith("#") and DATED_TITLE.search(line):
report.add("language", f"{rel}:{number}: dated title in a reader-facing doc (section 5)")
if line.startswith("#"):
# Section 5 is about the title a reader sees in a list of
# documents. A dated section inside a research log is a
# chronology, not a title.
if first_heading and DATED_TITLE.search(line):
report.add("language", f"{rel}:{number}: dated title in a reader-facing doc (section 5)")
first_heading = False


CODE_SPAN = re.compile(r"`[^`\n]+`")


def blank_code_spans(text: str) -> str:
"""Replace inline code with spaces, keeping offsets.

A word in backticks is a mention: the style guide writes `robust` when it
means the word, and a research note writes a commit id as a value. Neither
is prose, and both were reported as prose.
"""
return CODE_SPAN.sub(lambda m: " " * len(m.group(0)), text)


def blank_fenced_blocks(text: str) -> str:
Expand All @@ -413,12 +462,19 @@ def check_leaks(root: Path, report: Report) -> None:
continue
rel = path.relative_to(root).as_posix()
text = path.read_text(encoding="utf-8", errors="ignore")
prose = blank_fenced_blocks(text)
ctx = FileContext(
url_spans=[(m.start(), m.end()) for m in URL_TOKEN.finditer(text)],
prose=blank_fenced_blocks(text),
prose=prose,
masked=blank_code_spans(prose),
)
for pattern, category, label, predicate in LEAK_PATTERNS:
for match in pattern.finditer(text):
# A pattern without a predicate describes prose, and a fenced
# block is not prose. The style guide shows each of these
# shapes as an example of what to avoid.
if predicate is None and not ctx.prose[match.start():match.end()].strip():
continue
if predicate is not None and not predicate(text, match, ctx):
continue
report.add(category, f"{rel}: {label}: {match.group(0)[:40]}")
Expand Down
65 changes: 65 additions & 0 deletions scripts/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,71 @@ def test_claude_must_be_the_single_line(self):
self.assertTrue(any("@AGENTS.md" in m for m in self.messages(report, "agents")))


class LanguageTest(CheckerTestCase):
"""Section 2 — the word list reports usage, not mention."""

def test_marketing_word_in_prose_is_reported(self):
self.write("docs/note.md", "# Note\n\nA seamless handover.\n")
report = self.report()
check_style.check_language(self.root, report)
self.assertTrue(any("seamless" in m for m in self.messages(report, "language")))

def test_marketing_word_in_a_code_span_is_a_mention(self):
# The guide writes the words it forbids. Reporting them there makes the
# guide the largest offender in the organisation.
self.write("docs/note.md", "# Note\n\nAvoid `seamless` and `world-class`.\n")
report = self.report()
check_style.check_language(self.root, report)
self.assertEqual(self.messages(report, "language"), [])

def test_marketing_word_in_a_fenced_block_is_a_mention(self):
self.write("docs/note.md", "# Note\n\n```markdown\nA seamless handover.\n```\n")
report = self.report()
check_style.check_language(self.root, report)
self.assertEqual(self.messages(report, "language"), [])

def test_technical_terms_are_not_marketing(self):
# A robust estimator and statistical power are terms of art; the list
# that flagged them reported twelve non-defects in the research docs.
self.write(
"docs/note.md",
"# Note\n\nThe robust plateau holds, and the test is more powerful "
"than the Reality Check.\n",
)
report = self.report()
check_style.check_language(self.root, report)
self.assertEqual(self.messages(report, "language"), [])

def test_dated_section_heading_is_not_a_dated_title(self):
self.write(
"docs/note.md",
"# Contract acceptance\n\n## Bounded run acceptance (2026-10-05)\n",
)
report = self.report()
check_style.check_language(self.root, report)
self.assertEqual(self.messages(report, "language"), [])

def test_dated_document_title_is_reported(self):
self.write("docs/note.md", "# Console audit, 2026-10-15\n")
report = self.report()
check_style.check_language(self.root, report)
self.assertTrue(any("dated title" in m for m in self.messages(report, "language")))


class CrossRepositoryTest(CheckerTestCase):
def test_cross_repository_reference_never_blocks(self):
# The scheduled link job resolves these against the sibling's default
# branch. A finding that says "not checked" cannot block a merge.
# Two levels up: one level stays inside the repository, and on macOS
# `/tmp` resolves through a symlink, so a one-level fixture passed here
# and failed on the Linux runner.
self.write("docs/note.md", "# Note\n\n[peer](../../Sibling/docs/x.md)\n")
report = self.report(phase=2)
check_style.check_links(self.root, report)
self.assertTrue(any(c == "xref" for c, _ in report.findings))
self.assertEqual(report.blocking_findings(), [])


class PhaseTest(CheckerTestCase):
def test_phase_one_does_not_block_naming(self):
self.write("docs/hk_equity_runtime.md", "# x\n")
Expand Down
Loading