TL;DR: Documentation rot is normally blamed on neglect. We measured it on a repo that was 19 hours old and shipping hard. One README claim was false 29 minutes after it was written — by someone actively trying to keep it accurate. Rot rate isn't a function of neglect. It's a function of productivity.

The setup

pc-ai is our newest project — agent card intelligence (Declare · Enforce · Optimize), with a public validate API at validate.platinumcard.ai and brand at platinumcard.ai. It is not a FAF product. It's the thing we pointed FAF at.

At the measurement window it had every advantage: a growing WJTTC suite, a documented review, gates on the rules themselves. Nineteen hours old. Forty-four commits. One person, paying attention.

And it was already lying about itself.

Time-to-rot, measured

Two documented facts, with the commit that wrote them and the commit that made them false:

ClaimWrittenFalse byLifespan
README.md — tests 16/1619:3420:0329 minutes
PHASE-1-STATUS.md12/12, WASM open14:4819:224h 34m

The 29-minute claim wasn't neglected. It was written, correctly, by someone maintaining that document — and invalidated half an hour later by their own next commit.

The compounding part

Counting markdown files against files restating one fact — the schema version:

Time.md filesRestating 0.1.1
Aug 3, 14:483814
Aug 3, 22:314417
Aug 4, 09:494518

+7 documents produced +4 new copies. Roughly 57% of every new document added another place that fact could go stale — and the proportion was drifting up.

The uncomfortable bit

Rot rate scales with output. Shipping faster makes it worse. Writing more docs makes it worse. Being thorough makes it worse.

It's the same bug we'd fixed that morning

Earlier the same day, pc-ai had a real defect. Its CLI carried its own inline copy of the validation rules instead of importing the canonical ones. The copy drifted — a denylist missing entries, a version check absent — and documents that should have been rejected were passing. It's fixed, gated, and can't silently return.

Then we found six markdown files each holding their own copy of the project's status. A forked validator and a forked status table are the same bug: a copy of canon that stops matching canon, with nothing checking.

We'd gated the rules. We had not gated the prose. There was no reason for that except habit.

What actually fixed it

Two halves, and the honest version is that neither works alone.

The canonical file makes the problem finite. Without one, the claim surface grows with every document you write — audit 55 files, then 60, then 100. That work never converges. A .faf turns it into one enumerable file, and gives new documents somewhere to point instead of somewhere to copy.

The gate makes it enforced. A canonical source nobody checks is just the next copy. So every claim the file makes that can be computed, is — key files exist, package counts match, versions match the code, hosts match the deploy config. Checked against the filesystem and the code, never against another document. Two documents agreeing only proves they were written together.

And .fafm holds the half that isn't facts — why the decisions were made. That reasoning had been living in commit messages, which is exactly where nobody looks when deciding whether to change something.

It happened again while we wrote this

Adding the gate moved the test count from 25 to 26. The README had been corrected to 25 roughly one commit earlier. So the claim went stale inside a single commit, while we were building the mechanism meant to stop it.

FAIL README.md states the bar as 26/26
→ claims 25 but tests/wjttc/cases has 26

Nobody staged that. It's just what the failure mode looks like from the inside.

The transferable finding

  • Rot begins immediately — 29 minutes, on day two.
  • Discipline doesn't prevent it — every stale claim was written by someone trying to be accurate.
  • The surface compounds — ~57% of new documents added a fresh copy.
  • A canonical file is the load-bearing half — the gate then keeps that one file honest.

The rule

If a fact in your prose can be computed, either gate it or delete it. A number in a README that no test reads isn't documentation. It's a claim with a half-life.

Where it stands (as of 2026-08-04)

The measurement window was real. What happened after it is the receipt that the fix held:

  • project.faf + project.fafm — facts and decisions in one place each; FAF score at Trophy on the context file.
  • WJTTC FAF-01 — claims checked against filesystem and code, not against other docs. Live bar: 26/26.
  • Phase 1 closed — pure validate, true WASM from core, public validate API, brand site. Rank stays private (product decision, not a docs bug).
  • ~50 commits on the subject repo as of the last edit of this post — still one bar, still one canonical facts file. The surface compounds; the check does not.

Division of labour, stated plainly: FAF defines. MD instructs. AI codes.

Try it

faf init
faf score

.faf is context — what the project is. .fafm is memory — why it's like that. The gate is yours to write; it's usually a few dozen lines, and it only has to check the things a computer can already answer.