diff --git a/generated/badges.readme.md b/generated/badges.readme.md
index 0db6930..ae02d9c 100644
--- a/generated/badges.readme.md
+++ b/generated/badges.readme.md
@@ -3,12 +3,12 @@
-
+
-
-
+
+
diff --git a/tests/test_home_and_readme_agree.py b/tests/test_home_and_readme_agree.py
new file mode 100644
index 0000000..0f59280
--- /dev/null
+++ b/tests/test_home_and_readme_agree.py
@@ -0,0 +1,81 @@
+"""The README's first screen says what the homepage says.
+
+ctrlrun.dev's homepage is the marketing surface and the library's README is the GitHub and PyPI
+one. Until 2026-09-14 they led with different sentences: the site said *stops AI agents from
+taking wrong, restricted, or malicious actions in your workflows* and the README said
+*Execution safety for AI agents*, so a stranger arriving from one to the other met a second
+pitch. The README now opens with the homepage's H1 and lede, verbatim, and walks the seven
+steps of its diagram in the same order; nothing commercial crosses over. This file is what keeps the
+two from drifting apart again: it reads the sentences out of `index.mdx` and the diagram, so a
+rewrite of the homepage fails here until the README follows. The library pins the same strings
+on its side, in `tests/test_readme_assets.py`, so a rewrite of the README fails there first.
+"""
+
+from __future__ import annotations
+
+import re
+from pathlib import Path
+
+from _core import CORE_ROOT
+
+REPO_ROOT = Path(__file__).resolve().parents[1]
+HOME = REPO_ROOT / "index.mdx"
+DIAGRAM = REPO_ROOT / "snippets" / "how-diagram.jsx"
+README = CORE_ROOT / "README.md"
+
+
+def _prose(markup: str) -> str:
+ """Tags removed and whitespace collapsed. A `
` is a space, so it reads as a line break
+ does; every other tag is nothing, so `workflows.` keeps its full stop."""
+ text = re.sub(r"
", " ", markup)
+ return " ".join(re.sub(r"<[^>]+>", "", text).split())
+
+
+def _homepage(pattern: str) -> str:
+ match = re.search(pattern, HOME.read_text(encoding="utf-8"), re.S)
+ assert match, f"index.mdx no longer carries {pattern!r}"
+ return _prose(match.group(1))
+
+
+def _readme() -> str:
+ return README.read_text(encoding="utf-8")
+
+
+def test_the_readme_opens_with_the_homepage_h1_and_lede():
+ """In sequence, not merely present: the README's prose opens with the H1 and the lede
+ follows it directly. Prose before the H1, or the lede ahead of it, fails here."""
+ head = _prose(_readme().split("\n## ", 1)[0])
+ h1 = _homepage(r'
(.*?)
') + + assert h1.startswith("CTRLRun ") and h1.endswith("."), h1 + assert head.startswith(h1), f"the README does not open with the homepage H1: {head[:120]!r}" + after_h1 = head[len(h1) :].lstrip() + assert after_h1.startswith(lede), ( + f"the homepage lede does not follow the H1 directly: {after_h1[:120]!r}" + ) + + +def _diagram_steps() -> list[str]: + """The step names as the diagram draws them, in source order. The file carries the diagram + twice, wide and narrow, so the two sequences have to agree and one of them is the answer.""" + names = re.findall( + r'className="cr-dia-name"[^>]*>([^<]+)<', DIAGRAM.read_text(encoding="utf-8") + ) + assert names and len(names) % 2 == 0, names + wide, narrow = names[: len(names) // 2], names[len(names) // 2 :] + assert wide == narrow, f"the wide and narrow diagrams name different steps: {wide} vs {narrow}" + return wide + + +def test_the_readme_walks_the_homepage_seven_steps_in_order(): + """The diagram names seven steps, normalize to record. The README's *How it works* is the + same walk in prose, and a step the diagram gained or lost is a step the README follows.""" + steps = _diagram_steps() + assert len(steps) == 7, steps + assert steps[0] == "Normalize" and steps[-1] == "Record", steps + + section = _readme().split("## How it works", 1)[1].split("\n## ", 1)[0] + positions = [section.find(f"**{step}:") for step in steps] + assert all(p >= 0 for p in positions), dict(zip(steps, positions, strict=True)) + assert positions == sorted(positions), "the README walks the steps in the diagram's order" diff --git a/tools/docs_audit/render_badges.py b/tools/docs_audit/render_badges.py index e61fb36..70649a4 100644 --- a/tools/docs_audit/render_badges.py +++ b/tools/docs_audit/render_badges.py @@ -105,8 +105,8 @@ class Badge: ), Badge( "Docs", - "https://img.shields.io/badge/docs-ctrlrun.dev-B8730A", - "https://ctrlrun.dev", + "https://img.shields.io/badge/docs-docs.ctrlrun.dev-B8730A", + "https://docs.ctrlrun.dev/", ), Badge( "CI", @@ -126,12 +126,12 @@ class Badge: Badge( "Tests", f"https://img.shields.io/endpoint?url={BADGES_BRANCH}/tests-badge.json", - "https://ctrlrun.dev/docs/how-this-is-built", + "https://docs.ctrlrun.dev/how-this-is-built", ), Badge( "CTRLRun verified", f"https://img.shields.io/endpoint?url={BADGES_BRANCH}/verify-badge.json", - "https://ctrlrun.dev/docs/security/verify-guarantees", + "https://docs.ctrlrun.dev/security/verify-guarantees", ), Badge( "OpenSSF Scorecard",