A spec that other people build against is an interface, and interfaces have versions. Code learned this decades ago and settled on a convention: major for breaking, minor for additive, patch for everything else. Documents never did. Most specs are versioned by feel, if at all, which is how you end up with a folder containing v2, v2 final, and v2 final revised, none of which tells a consumer whether their pipeline still works. One of my quality gates is a version-bump classifier: it reads two versions of a document and says what kind of change happened, with reasons.
The Rules
| Change | Bump |
|---|---|
| A section removed or renamed | Major |
| A field removed from a schema table | Major |
| A field's type changed | Major |
| A field tightened from nullable to not-null | Major |
| A section added | Minor |
| A field added | Minor |
| A field loosened from not-null to nullable | Minor |
| Anything else: wording, examples, ordering | Patch |
Priority order, first match wins. A change that adds two fields and removes one is a major, because the consumer who depended on the removed field does not care about the two new ones.
Why a Rule Beats Judgment Here
Three reasons. The number is consistent across writers, so a major from one product manager means the same as a major from another. The number becomes a signal consumers can act on without reading the diff: a patch can be taken on faith, a minor can be taken at leisure, a major means stop and check. And the classification connects to the change policy in the data contract, where a breaking change carries a notice period and a sign-off. If the class is decided by mood, the policy attached to it is decorative.
What It Catches That People Miss
The two rows that surprise people are both in the major list. Tightening a field from nullable to not-null feels like cleanup, since the data was always populated anyway. It is breaking: a consumer that handled the null case now has dead code, and a producer that ever sends a null is now in violation of a promise it did not know it made. And renaming a heading feels cosmetic. It breaks every anchor link and every reader, human or machine, that finds a section by its name. Both are the kind of change that ships on a Friday because nobody thought it was a change.
Take It
The classifier is bump-classifier.js, no dependencies. Give it the old file and the new file and it prints the bump and the reasons. It treats any markdown table whose first column is named Field as a schema table, which matches the contract template; change one constant to match yours.
| Run | Output |
|---|---|
| Tightened a field to not-null | MAJOR, field tightened to not-null: price |
| Added a field | MINOR, field added: spread |
| Reworded the purpose | PATCH, text changed with no section or field changes |
| Deleted the examples section | MAJOR, section removed or renamed: examples |
Where it lives in my setup is a path-scoped rule: it runs only on versioned spec files, on commit, and the bump it reports is the bump the document gets. The writer can still argue, but they argue with a printed reason, and that is a different conversation from arguing with a feeling.
This is part of a track on agent tooling and spec-driven development that goes deeper than a field report, with something you can download and run in each one. Free for now while the track grows; the deepest installments become a paid tier later.