From fdc794608086dc7de64b2c0bf59b7a0bdf32f5af Mon Sep 17 00:00:00 2001 From: Tarik Moody Date: Sat, 22 Aug 2026 18:58:11 -0500 Subject: [PATCH] docs(readme): make its claims true, and pin them to the code (3.6.2) Running /readme found the README promising something the code did not do. - "It stops with a clear message on anything older" than Python 3.9 was false. No version guard existed anywhere. On 3.8 the reader got "TypeError: 'type' object is not subscriptable" from an import. The audience here is often on the macOS system python3. There is now a guard, above the imports that would crash first, naming the version you have, the version you need, and two ways to fix it. Exits 2, the documented code for "could not run". - Four tests pin the README to the code: the guard must sit above the imports or it can never fire; the version the README states must be the version the code enforces; the skill and command counts must match disk; every relative link must resolve. - Exit codes and the CI gating command are now in the README, not only in --help and SKILL.md. - docs/decisions is linked and appears in the repository layout. Nine decisions were written and nothing pointed at them. 122 tests. --- .claude-plugin/marketplace.json | 4 +-- .claude-plugin/plugin.json | 2 +- CHANGELOG.md | 14 ++++++++ README.md | 23 ++++++++++-- .../scripts/prod_audit.py | 14 ++++++++ .../tests/test_prod_audit.py | 35 +++++++++++++++++++ 6 files changed, 87 insertions(+), 5 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 02b3647..565e7dc 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-marketplace.json", "name": "clean-code-toolkit", - "version": "3.6.1", + "version": "3.6.2", "description": "Clean-code and product-handoff tools for AI-assisted builders.", "owner": { "name": "Tarik Moody" @@ -10,7 +10,7 @@ { "name": "clean-code-toolkit", "description": "Review code, assess product readiness, refactor safely, and prepare a developer handoff.", - "version": "3.6.1", + "version": "3.6.2", "author": { "name": "Tarik Moody" }, diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 6451336..8441e0e 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "clean-code-toolkit", - "version": "3.6.1", + "version": "3.6.2", "description": "Practical clean-code, product-readiness, and developer-handoff workflows for AI-assisted projects.", "author": { "name": "Tarik Moody" diff --git a/CHANGELOG.md b/CHANGELOG.md index 048cadb..0c8a5d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 3.6.2 + +A documentation pass that found the README making a promise the code did not keep. + +**The Python version guard now exists.** The README said the script "stops with a clear message on anything older" than Python 3.9. No such check was anywhere in the codebase. On 3.8 the reader got `TypeError: 'type' object is not subscriptable` from an import, which is not a message, and the audience for this toolkit is often running the macOS system python3. There is now a guard, it sits above the imports that would crash first, and it names the version you have, the version you need, and two ways to fix it. It exits 2, which is the documented code for "could not run". + +**The README and the code are pinned to each other.** Four new tests: the guard must sit above the imports or it can never fire; the version the README states must be the version the code enforces; the skill and command counts in the README must match what is on disk; and every relative link in the README must point at a file that exists. A promise in a document that the software does not keep is the same defect this toolkit exists to find. + +**Exit codes are in the README.** 3.6.1 documented them in `--help` and SKILL.md but not where someone wiring a pipeline would look, along with the command to gate CI on the audit. + +**The decision log is linked.** Nine decisions are written up in `docs/decisions`, in plain English, with what was given up and how we will know if each was right. Nothing pointed at them, and the repository layout in the README predated the folder. + +Tests: 122. + ## 3.6.1 A second developer review, run under the stop rule from 3.6.0: look for a new class of defect, not another instance of a closed one. Nine adversarial probes found every closed class still closed. A filename carrying `|` cannot forge a table column, a filename starting `#` cannot forge a heading, a symlink to `~/.ssh/id_ed25519` leaks nothing, binary and invalid-UTF-8 files do not crash it, a 400,000-line file hits the byte cap, a symlink loop does not hang it, 10,000 files take about ten seconds with no blowup, and two runs against the same commit are byte-identical in both markdown and JSON. diff --git a/README.md b/README.md index 4487e5a..ee135be 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,23 @@ To add the standards to a project without the plugin: It creates or appends to `CLAUDE.md` and refuses to overwrite a section it cannot migrate safely. +To gate a pipeline on the audit, run the script directly and read the exit code: + +```bash +python3 skills/prod-readiness-coach/scripts/prod_audit.py \ + --repo . --output /dev/null --fail-on critical +``` + +The exit codes are a contract you can wire CI against: + +| Code | Meaning | +|---|---| +| `0` | The audit ran and nothing at or above `--fail-on` failed. | +| `1` | The audit ran and something at or above `--fail-on` failed. | +| `2` | The audit could not run: the path is missing, is a file, or holds no files. | + +Exit `2` is never a verdict about your code. It means nothing was scanned, so check the path. + Repository layout: ```text @@ -163,10 +180,12 @@ Repository layout: ├── skills/ # the six skills and their reference files ├── scripts/ # installer and validator ├── templates/CLAUDE.md # the always-on standards -└── docs/ # guides, including the release checklist +└── docs/ + ├── decisions/ # why each non-obvious call was made + └── ... # guides, including the release checklist ``` -The root `CLAUDE.md` holds contributor instructions for this repository. User-facing behavior lives in the skills, commands, and the installable template. Maintainers follow the [release checklist](docs/release-checklist.md) before tagging. +The root `CLAUDE.md` holds contributor instructions for this repository. User-facing behavior lives in the skills, commands, and the installable template. Maintainers follow the [release checklist](docs/release-checklist.md) before tagging. Every call that a reasonable person could have made differently is written up in [docs/decisions](docs/decisions), in plain English, with what was given up and how we will know if it was right. **One of the six tools is a script, and that script is tested.** `prod-readiness-coach` keeps a coverage grid: every check has to prove it fires when a control is missing and stays quiet when the control is there. It sits at 83 of 83 and CI fails if it slips. Run `python3 skills/prod-readiness-coach/scripts/coverage_grid.py` to see it. diff --git a/skills/prod-readiness-coach/scripts/prod_audit.py b/skills/prod-readiness-coach/scripts/prod_audit.py index 6a73a09..731db86 100644 --- a/skills/prod-readiness-coach/scripts/prod_audit.py +++ b/skills/prod-readiness-coach/scripts/prod_audit.py @@ -22,6 +22,20 @@ from datetime import datetime, timedelta, timezone from pathlib import Path +# The audit modules use syntax that 3.8 cannot parse, so this has to sit ABOVE +# the imports below. Moved under them, it never runs and the reader gets +# "TypeError: 'type' object is not subscriptable" instead of a sentence. The +# audience here is often on the macOS system python3, which is old. +MIN_PYTHON = (3, 9) +if sys.version_info < MIN_PYTHON: + sys.stderr.write( + f"error: this script needs Python {MIN_PYTHON[0]}.{MIN_PYTHON[1]} or newer, " + f"and you are running {sys.version.split()[0]}.\n" + "On macOS the built-in python3 is usually older than the one you installed. " + "Try `python3.12` or `python3.11` in place of `python3`, or install a current " + "Python from python.org or with `brew install python`.\n") + raise SystemExit(2) + from audit.model import ( # noqa: F401 (re-exported for callers and tests) CHECK_SKIPS_BY_PROFILE, CONTRADICTIONS, MIN_DOC_WORDS, PROFILES, SEVERITY_LABEL, SEVERITY_PENALTY, SEVERITY_PENALTY_WARN, WAIVER_FIELDS, WAIVER_FILE, WAIVER_MAX_AGE_DAYS, diff --git a/skills/prod-readiness-coach/tests/test_prod_audit.py b/skills/prod-readiness-coach/tests/test_prod_audit.py index ab95f06..f08286f 100644 --- a/skills/prod-readiness-coach/tests/test_prod_audit.py +++ b/skills/prod-readiness-coach/tests/test_prod_audit.py @@ -803,5 +803,40 @@ def test_product_context_survives_as_readable_text(self): self.assertIn("Losing a day of mail would be bad.", md) +class TheReadmeMustMatchTheCode(unittest.TestCase): + """The README promised a clear message on an old Python and no guard existed. + A promise in a document that the software does not keep is the same defect + this toolkit exists to find, so the two are pinned to each other here.""" + + ROOT = Path(__file__).resolve().parents[3] + SCRIPT = Path(__file__).resolve().parent.parent / "scripts" / "prod_audit.py" + + def test_the_guard_sits_above_the_imports_that_would_crash_first(self): + src = self.SCRIPT.read_text() + self.assertLess(src.index("sys.version_info"), src.index("from audit."), + "the version guard is below the imports, so it can never run") + + def test_the_readme_states_the_version_the_code_enforces(self): + readme = (self.ROOT / "README.md").read_text() + major, minor = prod_audit.MIN_PYTHON + self.assertIn(f"Python {major}.{minor} or newer", readme, + f"README does not state the enforced floor of {major}.{minor}") + + def test_the_readme_names_the_right_number_of_skills_and_commands(self): + readme = (self.ROOT / "README.md").read_text() + skills = len([d for d in (self.ROOT / "skills").iterdir() if d.is_dir()]) + commands = len(list((self.ROOT / "commands").glob("*.md"))) + words = {6: "six", 3: "three", 5: "five", 7: "seven", 4: "four"} + self.assertIn(f"{words[skills]} skills and {words[commands]} commands", readme, + f"README count is stale: there are {skills} skills and {commands} commands") + + def test_every_link_in_the_readme_points_at_something_real(self): + import re + readme = (self.ROOT / "README.md").read_text() + dead = [t for t in re.findall(r"\]\((?!https?:)([^)#]+)", readme) + if not (self.ROOT / t).exists()] + self.assertEqual(dead, [], f"dead links in README: {dead}") + + if __name__ == "__main__": unittest.main()