My project's docs had rules.

"One document, one responsibility." "Split anything over 45 lines."
I wrote that. The day before yesterday.

Here's how that was going.

  • Folders with no README (no index): 25 out of 37
  • The folder holding our engineering rules: 11 files, zero index
  • Documents breaking the 45-line rule: 47
  • Largest offender: 1,203 lines

Writing a rule down does not make it a rule.

Obvious, I know. But there is something about being handed a list of 47 documents where you personally broke your own rule that stops being funny halfway down.

(The me of two days ago fully intended to follow it.)

I rewrote the rules themselves, and the rules file hit 150 lines

The plan was already clear: let a machine enforce this. Fail CI.

Which meant writing the rules properly first. README required, folder layout, update obligations, what CI actually checks. By the time it was all in there, the rules file was over 150 lines.

The rules file was breaking the 45-line rule.

Now, if you say "well, the rules file is special, it gets an exemption" — what have you just done?

You have created the precedent "the rules are exempt," and it is permanent from that day.

From then on, every time someone crosses 45 lines, they get to say "the rules file does it too." And they're right.

So I split it. Eight files, all under 45 lines.

If I can't follow my own rule, the rule was never worth writing.

The moment CI landed, 63 existing violations bared their teeth

On to the real work: write the checker, wire it into CI.

Run it, and of course:

63 violations

Enter fullscreen mode Exit fullscreen mode

Everything fails. All red. Files I'm about to touch and files nobody has opened in months, equally red.

Humanity is offered two choices here.

  1. Fix all 63 first, then turn on CI (including the 1,203-line monster)
  2. Add an ignore list, silence the 63, move on

Tempting, isn't it? Option 2. It was to me. A .lintignore with 63 lines in it and a comment saying "remove later."

So when exactly are you removing that list?

Think about what that file actually is. It's a second source of truth.

The rule lives in the rules doc. What is actually exempt lives only in the ignore list. Two files, growing apart.

And ignore lists never shrink. Nobody is incentivised to delete a line from one. If it shrinks, that was an accident.

(I have never seen a project where the "later" in "remove later" arrived.)

So what about option 1, fix everything first? Also a trap: while you're fixing, there is no CI. The period when you are touching docs the most is exactly the period with no gate.

Should someone fixing a typo in a 1,200-line file be forced to split it?

This is where I stopped.

Say you open a PR fixing a single typo in that 1,203-line document. Implement the boy-scout rule naively and that PR fails with "this document exceeds 45 lines."

You just demanded a 1,203-line refactor from someone fixing one character.

What happens to a team with that CI?

Nobody fixes typos any more.

Walking past the broken window becomes the rational move. The rule turns into a machine that punishes improvement.

So what should it get angry about? Only what this PR brought with it.

  • New file over 45 lines → fail (don't add debt from here on)
  • Touched the existing 1,203 lines → pass (not your fault)
  • But grew the existing 1,203 lines to 1,250 → fail (don't make it worse)

Not "new or existing" but "did it grow."

And that needs no ignore list, because git already knows. Compare the file against the merge base. Did it get longer? That's the whole judgement. No second source of truth is born.

My own rules doc got failed by my own lint

I was writing this design into the rules doc when CI went red.

LONG  docs/rules/doc-ci.md: 46 lines > 45

Enter fullscreen mode Exit fullscreen mode

It's me. I'm the culprit.

The person who implemented "adding lines to an existing file fails" added lines to a file, explaining that very feature, and failed.

I laughed. Then I fixed it (down to 44 lines).

I did not expect the proof that my design worked to come from my own neck.

And honestly, I was impressed. It stops the person who should be stopped. It does not go easy on that person just because he wrote it. I got to verify that I'm not an exception, using myself.

Moving a file counts as adding a new one

One more.

Reorganising folders means a lot of git mv. At the new path, the file does not exist in the merge base.

To CI, that is a newly added file. New files fail without mercy, so 1,203 lines that had merely moved were told to split. Ten of them at once.

Fixed by teaching the check to follow renames: for a moved file, compare against its line count at the old path. Didn't grow, doesn't fail.

Fixing your structure forces unrelated refactors. I had no idea until I ran it.

I wanted to exclude secrets by path (and that was wrong)

There was a second gate: does this commit contain secrets — API keys, tokens.

It failed on a docs PR. Two hits.

  • Authorization: Bearer <TOKEN_NAME> — angle brackets, obviously a placeholder
  • A sample id ending in a fixed dummy value, obviously an example

False positives. And not even lines I had just written: lines that had been there for months, re-detected as "newly added" because I split the file.

Cue the temptation.

"Just exclude the whole docs folder from the scan."

One line. One line fixes it.

I'm very glad I didn't.

Because somewhere in this same session I had found a production signing secret sitting in plain text inside the docs. Written by me. Forgotten by me.

What went through my head when I found it was: thank god I didn't take the one-line escape.

If I had excluded the folder, that secret would have been placed outside the scan, quietly, permanently.

Exclude the shape, not the place.

Only the angle-bracket placeholder shape is exempt. A real key has a different shape, so it always trips.

I tested both directions, to be sure.

  • Placeholder → not detected (as intended)
  • Dropped in something that looks like a real key → three rules fired at once, failed correctly

That second check matters more than the green one.

A gate you haven't watched fail is not a gate.

The day the debt hit zero, promotion took three lines

From there I split the 47. 63 → 50 → 36 → 27 → 17 → 0.

The docs ended up at 488 files across 99 folders. Every folder has an index, every file is under 45 lines (the copy-paste-whole scripts are documented exemptions — 15 of them).

Debt at zero, so I promoted CI to "every file, always, no mercy."

The diff:

  • Add STRICT: '1' to the environment
  • Delete the migration-era "only check what you touched"

Three lines.

Because I never created the ignore list, there was no second source of truth to dismantle. The staged rollout ran entirely on facts already in git, so folding it up cost nothing.

If I had written those 63 lines that day, I'd be going through them one at a time right now asking "do we still need this one?" It would not have been three lines.

What a machine can hold, and what it can't

An honest note to end on.

This CI guarantees structure. No missing index. Nothing over 45 lines. No broken links. No new secrets.

Whether the words match the implementation is not something a machine can hold.

You can build a gate that demands a doc update whenever code changes. But that's guesswork: it either fails legitimate PRs, or it manufactures the comfortable feeling of coverage. Both are worse than nothing.

So freshness went into the rules instead. If you read a doc and spot a discrepancy: fix only what you can verify on the real system. If you can't verify it, don't touch the text — open an issue and mark it suspicious.

Rewriting on a guess is the worst outcome, because then the mistake gets to wear a "reviewed" badge.

That rule fired twice during the work. Once when a folder-layout description had drifted from reality (filed, not fixed). Once when, mid-split, I noticed the reasons we had rejected past proposals were about to disappear, and restored them.

You cannot reconstruct why something was rejected. And once it's gone, the same proposal comes back in six months.

Closing

Conversations about machine-enforced rules are usually about detection: how do we catch it.

What actually mattered was designing what to let through.

Get that wrong and a gate dies one of two deaths: red on everything until people ignore it, or an ignore list so fat it protects nothing.

An ignore list annotated "remove later" does not get removed later.

So don't create one in the first place.