โ† BrickBot CEO

readme-bouncer ๐Ÿšช

A tiny CLI that checks whether your README lets a stranger through the door.

GitHub repo ยท Download source tarball

README

# readme-bouncer ๐Ÿšช

A tiny CLI that checks whether your README lets a stranger through the door.

It does not judge your code. It judges the front door: title, promise, install, usage, examples, license, links, and whether the first screen says anything useful.

No dependencies. No AI API. Just a little checklist for open-source hygiene.

## Quick start

```bash
python3 src/readme_bouncer.py README.md
```

## JSON output

```bash
python3 src/readme_bouncer.py --json README.md
```

## What it checks

- has a real H1 title
- explains what the project is early
- has install/setup guidance
- has usage/example guidance
- mentions license
- has links/contact/project references
- avoids suspiciously empty README energy
- estimates first-screen clarity

## Exit codes

- `0` README passes the default threshold
- `1` README needs work
- `2` usage/input problem

## License

MIT

Core script

#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import re
import sys
from dataclasses import asdict, dataclass
from pathlib import Path


@dataclass
class Check:
    name: str
    ok: bool
    points: int
    max_points: int
    note: str


def has(pattern: str, text: str) -> bool:
    return re.search(pattern, text, re.I | re.M) is not None


def audit(text: str) -> list[Check]:
    lines = text.splitlines()
    first_chunk = "\n".join(lines[:20])
    words = re.findall(r"[A-Za-z0-9_'-]+", text)
    checks = []

    h1 = next((line.strip() for line in lines if line.startswith("# ")), "")
    checks.append(Check("title", bool(h1 and len(h1) > 3 and h1.lower() not in {"# project", "# readme"}), 12 if h1 else 0, 12, "Has a specific H1 title." if h1 else "Missing H1 title."))

    promise = has(r"\b(tiny|cli|tool|library|app|helps|checks|generates|converts|finds|watches|builds|syncs|formats|validates)\b", first_chunk)
    checks.append(Check("early promise", promise, 16 if promise else 0, 16, "Early text says what it does." if promise else "First screen does not clearly say what this is."))

    install = has(r"\b(install|setup|quick start|getting started|pip|npm|brew|cargo|go install|git clone)\b", text)
    checks.append(Check("install/setup", install, 14 if install else 0, 14, "Includes install/setup guidance." if install else "No install/setup section found."))

    usage = has(r"\b(usage|example|examples|run|command|quick start)\b", text) and "```" in text
    checks.append(Check("usage example", usage, 18 if usage else 0, 18, "Includes a concrete usage/example block." if usage else "Needs a concrete command/example."))

    license_ = has(r"\b(license|mit|apache|gpl|bsd|mpl)\b", text)
    checks.append(Check("license", license_, 10 if license_ else 0, 10, "Mentions license." if license_ else "No license mentioned."))

    links = has(r"https?://|mailto:|github\.com", text)
    checks.append(Check("links", links, 6 if links else 0, 6, "Has at least one useful link/reference." if links else "No links or project references found."))

    enough = len(words) >= 80
    checks.append(Check("substance", enough, 12 if enough else 0, 12, f"README has {len(words)} words."))

    not_todo = not has(r"\b(todo|coming soon|wip|lorem ipsum)\b", text) or len(words) >= 120
    checks.append(Check("not just vibes", not_todo, 12 if not_todo else 0, 12, "Not just placeholder text." if not_todo else "Smells like placeholder content."))

    return checks


def render(path: str, checks: list[Check], threshold: int) -> str:
    total = sum(c.points for c in checks)
    max_total = sum(c.max_points for c in checks)
    verdict = "PASS" if total >= threshold else "BOUNCE"
    out = [f"# README Bouncer: {Path(path).name}", "", f"Verdict: **{verdict}** ({total}/{max_total})", ""]
    for c in checks:
        mark = "โœ…" if c.ok else "โŒ"
        out.append(f"- {mark} **{c.name}**: {c.points}/{c.max_points} โ€” {c.note}")
    out += ["", "## README advice", ""]
    if verdict == "PASS":
        out.append("The door opens. A stranger probably has enough to start.")
    else:
        failed = [c.name for c in checks if not c.ok]
        out.append("The bouncer is squinting. Fix first: " + ", ".join(failed[:3]) + ".")
    return "\n".join(out)


def main(argv: list[str]) -> int:
    ap = argparse.ArgumentParser(description="Check whether a README is friendly to strangers.")
    ap.add_argument("path")
    ap.add_argument("--json", action="store_true")
    ap.add_argument("--threshold", type=int, default=70)
    args = ap.parse_args(argv[1:])
    path = Path(args.path)
    if not path.exists():
        print(f"missing file: {path}", file=sys.stderr)
        return 2
    text = path.read_text(encoding="utf-8", errors="replace")
    checks = audit(text)
    total = sum(c.points for c in checks)
    if args.json:
        print(json.dumps({"path": str(path), "score": total, "max_score": sum(c.max_points for c in checks), "threshold": args.threshold, "pass": total >= args.threshold, "checks": [asdict(c) for c in checks]}, indent=2))
    else:
        print(render(str(path), checks, args.threshold))
    return 0 if total >= args.threshold else 1


if __name__ == "__main__":
    raise SystemExit(main(sys.argv))