Public hygiene: the tree carries no imprint, held by a ratchet
A repository that grew inside one company is full of that company: its name in URLs and workflow files, its people in comments, its private issue numbers as provenance, its planning vocabulary (KTD16, U3) in docstrings, its Slack and account ids in fixtures, and dated retellings of incidents where a timeless rule should stand. None of it helps a reader who is not that company, and some of it is a leak. The policy is that every comment, docstring, fixture and prose line in the public tree is answerable from the repo alone; the mechanism is a check, because a policy held by discipline lasts one busy week.
The check is a ratchet, the shape the clock allowlist already uses (tracing.md item 8): every hit in the tree is counted per file and class in a committed list that can only shrink. The tree grows imprint nowhere from the day the check lands, and a scrub of any directory is measurable — its files leave the list. When the list is empty the check is the ordinary "none" gate and stays.
- Code:
scripts/public-hygiene.mjs(the classes, the scope, the scan, the ratchet; plain JS with a.d.mtstwin — nothing undersrc/imports it),scripts/public-hygiene.allowlist.json(the recorded imprint, path → class → count),scripts/public-hygiene.allow(lines allowed by name, each with its reason). - Tests:
src/publicHygiene.test.ts. - Docs: AGENTS.md ("Comments are for the stranger"), decision record 0022.
Behavior
- Five classes, one regex each, tested per line.
names(the company, its sibling products, people, the private Slack channel — case-insensitive; a 1Passwordop://reference is an integration's own syntax, not imprint, and a vault path that names the company is caught by the company),trackers(#NNNissue references and the private org's GitHub URLs; a line anchor, an HTML entity, a heading or a hex colour is not a tracker),planIds(KTD…,KD…,OQ…, and the bareU1–U9/R1–R99markers; a Slack user id and Cloudflare'sR2are not),ids(Slack channel/user/DM ids and 32-hex account ids),dates(a full ISO date). A line can hit several classes; each is counted. - The scope is the public tree. Tracked files under
src/,deploy/,web/,scripts/,config/,docs/,.github/, the root markdown andpackage.json. Not in scope:docs/plans/(working records, not published),CHANGELOG.mdand generated notices (provenance lives there), lockfiles, binaries, and the ratchet's own three files and test. Decision records, and the index generated from them, carry dates and may cite PRs for provenance, sotrackersanddatesare not counted there. - The list only shrinks.
npm run hygiene:check(incheck:consistency) scans the tree and compares it with the recorded list: a file whose count in any class grew, or a file with hits that is not listed, is new imprint and fails with the line's remedy; a file whose count fell, or that vanished, fails asking fornpm run hygiene:genso the progress is recorded. Equal passes.hygiene:gen(infix) writes the list from the tree — but never records growth: a class that grew, or a file with hits that is not listed, makesgenrefuse (exit 1, the same message, nothing written) unless-- --forcesays the growth is deliberate; a stale allow entry likewise stops the write. Sonpm run fixis safe to run blindly and cannot absorb new imprint into the list. CI checks a PR merged into its base, so a branch whose base has moved fails until it rebases and re-records; growth that arrived from the base is the one legitimate use of--force. - A legitimate line is allowed by name.
scripts/public-hygiene.allowholdspath<TAB>trimmed lineentries with a comment saying why (the English word "nominal", a colour that looks like an issue number, an integration's own identifier). An allowed line is skipped whole. An entry is stale — and fails the check — when its line no longer exists or no longer matches any class, so the file carries neither ghosts nor dead weight. - A scrub sees what is left.
npm run hygiene:check -- --list <prefix…>prints every remaining hit under the given paths aspath:line, class, text, and a count.
Validation criteria
| Criterion | Proof |
|---|---|
Each class matches its imprint and not its look-alikes: names incl. the case-insensitive company and people, not nominally/justinian/the op:// scheme; trackers not #L12, ', ## 2026, hex colours; planIds not Slack ids or R2; ids at word boundaries; dates only full ISO (item 1) | [unit] src/publicHygiene.test.ts::the classes::* |
Scope: the public prefixes, root markdown and package.json are in; plans, changelog, notices, lockfiles, binaries, skills and the ratchet's own files are out; decision records and their generated index skip trackers and dates (item 2) | [unit] src/publicHygiene.test.ts::scope::* |
| One file's scan counts per class, names each hit with its line, skips an allowed line whole and reports it used, and counts nothing for an exempt class (items 1, 4) | [unit] src/publicHygiene.test.ts::scanText::* |
The allow file parses path<TAB>line entries past comments and blanks; a stale entry is named (item 4) | [unit] src/publicHygiene.test.ts::the allow file::* |
| The ratchet: equal passes; shrinkage or a vanished file asks to regenerate; growth or a new file names the class, the delta and the remedy (item 3) | [unit] src/publicHygiene.test.ts::ratchetProblems::* |
hygiene:gen refuses growth: only grown classes and new files are named, shrinkage is recorded (item 3) | [unit] src/publicHygiene.test.ts::growthProblems — what \hygiene:gen` refuses to record::*` |
hygiene:gen with growth in the tree exits 1 and leaves the list unchanged; -- --force records it (item 3) | [agent] add // see #123 to a source file; npm run hygiene:gen → names the file, 0 → 1, exit 1, scripts/public-hygiene.allowlist.json byte-identical (shasum before and after); npm run hygiene:gen -- --force → records it, exit 0; revert the line and hygiene:gen again. |
| The tree equals the committed list and no allow entry is stale (item 3) | [agent] npm run hygiene:check → public-hygiene ok — N file(s), M hit(s) still listed, K line(s) allowed, exit 0. Red step: add // see #123 to any source file and re-run — it must name the file, the class, 0 → 1, and exit 1. |