diff --git a/.github/workflows/build-explainers.yml b/.github/workflows/build-explainers.yml
index 61e8659..232c857 100644
--- a/.github/workflows/build-explainers.yml
+++ b/.github/workflows/build-explainers.yml
@@ -8,7 +8,10 @@ name: Build Explainers
# explainer - not byte- or pixel-compared against this job's own fresh
# regeneration, since Pillow's bundled FreeType renders text with
# genuinely different pixels across platforms (see
-# scripts/check_generated_files_current.py's docstring).
+# scripts/check_generated_files_current.py's docstring). Also verifies the
+# plain-prose "N explainers" mentions in README.md, CONTRIBUTORS.md,
+# METRICS.md, and ROADMAP.md haven't drifted from the real count of
+# explainers/*.md (scripts/check_explainer_count.py).
#
# main requires a PR (branch protection), and github-actions[bot] isn't in
# that ruleset's bypass list, so this can no longer auto-commit a fix by
@@ -27,6 +30,11 @@ on:
- "scripts/build_explainers.py"
- "scripts/generate_og_images.py"
- "scripts/check_generated_files_current.py"
+ - "scripts/check_explainer_count.py"
+ - "README.md"
+ - "CONTRIBUTORS.md"
+ - "METRICS.md"
+ - "ROADMAP.md"
pull_request:
paths:
- "explainers/**.md"
@@ -35,6 +43,11 @@ on:
- "scripts/build_explainers.py"
- "scripts/generate_og_images.py"
- "scripts/check_generated_files_current.py"
+ - "scripts/check_explainer_count.py"
+ - "README.md"
+ - "CONTRIBUTORS.md"
+ - "METRICS.md"
+ - "ROADMAP.md"
jobs:
check:
@@ -67,3 +80,6 @@ jobs:
# existence/size/dimensions instead, which is platform-independent.
# Text-based generated files still get an exact git diff.
run: python3 scripts/check_generated_files_current.py
+
+ - name: Fail if a "current explainer count" mention has drifted
+ run: python3 scripts/check_explainer_count.py
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 439453f..59fde31 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -55,7 +55,7 @@ the [Git identity map](#git-identity-map) so `git shortlog -sne` can be reconcil
| | Who | Role |
|:--|-----|------|
-|
| **Yash Kewlani** - [@yakew7](https://github.com/yakew7) | Creator and maintainer. Author of the seven audits, the `faircode` library and benchmark harness, the Open Dataset Profiler, the website, and the bulk of the 39 explainers. Code owner for `faircode/`, `paper/`, every `audit.yaml`, and project policy (`CLAUDE.md`, `CONTRIBUTING.md`); co-owner of `explainers/`. |
+|
| **Yash Kewlani** - [@yakew7](https://github.com/yakew7) | Creator and maintainer. Author of the seven audits, the `faircode` library and benchmark harness, the Open Dataset Profiler, the website, and the bulk of the 53 explainers. Code owner for `faircode/`, `paper/`, every `audit.yaml`, and project policy (`CLAUDE.md`, `CONTRIBUTING.md`); co-owner of `explainers/`. |
Contact: [yashkewlani2020@gmail.com](mailto:yashkewlani2020@gmail.com) ยท [@thefaircodeproject](https://instagram.com/thefaircodeproject)
diff --git a/README.md b/README.md
index bb4cc04..5d6c169 100644
--- a/README.md
+++ b/README.md
@@ -1166,7 +1166,7 @@ The full public roadmap - with phases, completion status, and content schedule -
| Combined Social Reach (Instagram + LinkedIn) | 30K+ |
| Countries Reached (Website Visitors) | 18 |
| Code Audits Published | 7 |
-| Explainers Published | 47 |
+| Explainers Published | 53 |
Tracked weekly in [METRICS.md](METRICS.md).
diff --git a/scripts/check_explainer_count.py b/scripts/check_explainer_count.py
new file mode 100644
index 0000000..e393092
--- /dev/null
+++ b/scripts/check_explainer_count.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python3
+"""Fails if a "current explainer count" mention in README.md, CONTRIBUTORS.md,
+METRICS.md, or ROADMAP.md drifts from the real count of explainers/*.md.
+
+CHECKS below is a small, explicit list of exact regexes, one per file - not a
+fuzzy "N explainers" scan across the whole file. METRICS.md in particular is a
+weekly log full of legitimate historical counts (e.g. "explainer count `39 ->
+44`" from an old week); matching those against today's count would be a false
+positive worse than the drift this script is meant to catch. Each regex below
+targets the one line in its file that's meant to state the current, live
+total.
+
+Run locally: python3 scripts/check_explainer_count.py
+Exit code: 0 = all mentions match, 1 = at least one is stale or missing.
+"""
+from __future__ import annotations
+
+import re
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+EXPLAINERS_DIR = ROOT / "explainers"
+
+# (relative path, regex whose sole capture group is the claimed count)
+CHECKS = [
+ ("README.md", re.compile(r"Show all (\d+) explainers")),
+ ("CONTRIBUTORS.md", re.compile(r"the bulk of the (\d+) explainers")),
+ ("METRICS.md", re.compile(r"Explainers-(\d+)-blueviolet")),
+ ("ROADMAP.md", re.compile(r"(\d+) explainers published")),
+]
+
+
+def main() -> int:
+ actual = len(list(EXPLAINERS_DIR.glob("*.md")))
+ stale = []
+
+ for rel_path, pattern in CHECKS:
+ path = ROOT / rel_path
+ text = path.read_text(encoding="utf-8")
+ match = pattern.search(text)
+ if match is None:
+ stale.append((rel_path, f"expected pattern not found: {pattern.pattern!r}"))
+ continue
+ claimed = int(match.group(1))
+ if claimed != actual:
+ stale.append((rel_path, f"says {claimed} explainers, but explainers/*.md has {actual}"))
+
+ if stale:
+ print(f"Explainer count drift found (explainers/*.md currently has {actual} files):",
+ file=sys.stderr)
+ for rel_path, reason in stale:
+ print(f" {rel_path}: {reason}", file=sys.stderr)
+ return 1
+
+ print(f"OK: all {len(CHECKS)} explainer-count mentions match explainers/*.md ({actual} files).")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())