A version number should tell you what changed before you read the changelog.
BFIP splits Semantic Versioning's minor and patch fields into four: Breaking, Feature, Internal, and Patch. So "you get something new" and "nothing changed for you, we just moved things around" are never the same digit.
Summary
Given a version number B.F.I.P, increment the:
- B (breaking) version when you change the public API in a way that breaks existing user code
- F (feature) version when you add new functionality to the public API without breaking existing user code
- I (internal) version when you change how something works internally, without touching the public API in any way
- P (patch) version when you fix a known bug without touching the public API in any way
A probation tag is available as an extension to the B.F.I.P format, for trialing a new B before committing to it. Chronological constraints (=, >, >=, <, <=) describe where a version falls in publication order, independent of compatibility.
Introduction
Semantic Versioning asks a maintainer to fold two different questions into one number. The minor version is supposed to mean "a feature was added," but it is also where "the internals were reworked, nothing user-facing changed" ends up, because there is nowhere else for it to go. The patch version has the same problem: it is meant for bug fixes, but any internal change that isn't a new feature and isn't quite a bug fix tends to get patched in there too. Once two different kinds of change share one number, that number stops telling you which kind of change actually happened, and you're back to reading the changelog to find out.
BFIP starts from the same goal as Semantic Versioning: a version number should tell you, without reading a changelog, what you are getting relative to what you had. It answers that goal with four fields instead of three, so that "the public API got bigger" and "the internals moved but nothing you can see changed" are never the same digit.
For BFIP to mean anything, you have to declare a public API first. What counts as a public API is not the same for a library, a language, a file format, or a service, so BFIP defines a default for each and lets a project state its own boundary if the default doesn't fit. Once that boundary is drawn, every change to the project falls into exactly one of four buckets, and each bucket has its own field.
The four fields
Every change to a project's public API falls into exactly one of these four buckets.
The public API changed in a way that would stop existing user code from working.
Something new was added to the public API. Existing user code still works, unmodified.
The implementation changed — performance, structure, dependencies — but the public API is untouched.
A known bug was fixed. Behavior was wrong and is now correct; the public API didn't change.
Public API
In the absence of a project's own declaration, the public API is:
- Library or module — its exported symbols and their signatures and observable behavior.
- Language, compiler, or interpreter — its accepted syntax and the semantics of that syntax.
- File format or protocol — the structure of the data it reads or writes, or the messages it sends or receives, and their meaning.
- Service or command-line tool — its externally visible interface: endpoints, commands, flags, and their observable effects.
A project may narrow or widen this default with its own published declaration. In its absence, the default applies.
Specification
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL are to be interpreted as described in RFC 2119.
Software using BFIP Versioning MUST declare a public API, either directly or by adopting the default definition (see Public API, above).
A normal version number MUST take the form B.F.I.P where B, F, I, and P are non-negative integers, and MUST NOT contain leading zeroes. Each element MUST increase numerically. For instance: 1.9.0.0 → 1.10.0.0 → 1.11.0.0.
Once a versioned release has been published, its contents MUST NOT be modified. Any modification MUST be published as a new version.
Public API. Covered above — see Public API.
BFIP's own public API. This specification is itself versioned under BFIP, and declares its own public API rather than relying on the default: the grammar for a valid version string (including the probation tag); the increment triggers for B, F, I, and P; the Field Reset, Monotonicity, Development Phase, Initial Public Release, and No Re-entry rules; the Compatibility and Precedence relations; and the Chronological and Dependency constraint languages built on them. Editorial wording, examples, the Introduction, the "Why Use BFIP" section, and the FAQ are not part of this public API, and may be revised without a version increment, provided no rule's normative meaning changes as a result.
Breaking version zero (0.f.i.p) is for initial development. No public API is defined, and none is promised, at any F.I.P within it. B, F, I, and P still increment according to the rules below throughout this phase, but none of those increments carries any compatibility meaning until the project leaves it.
The transition out of breaking version zero MUST land on exactly 1.0.0.0, and MUST occur as a deliberate act of declaring the public API for the first time. It is not an ordinary B increment, since an ordinary B increment presupposes a public API already existed to break. Once B has reached 1 or higher, it MUST NOT return to 0.
Patch version P (b.f.i.P | b > 0) MUST be incremented if a known bug is fixed without any change to the public API. A bug fix is defined as a change that corrects incorrect behavior without adding to, removing from, or otherwise altering the public API.
Internal version I (b.f.I.p | b > 0) MUST be incremented if the implementation changes in a way that does not add to, remove from, or otherwise alter the public API — including changes made for performance, clarity, or maintainability. It MUST NOT be incremented for a change a user of the public API could observe. P MUST be reset to 0 when I is incremented.
Feature version F (b.F.i.p | b > 0) MUST be incremented if new functionality is added to the public API without altering or removing any existing part of it. Existing user code MUST continue to work unmodified after this increment. I and P MUST be reset to 0 when F is incremented.
Breaking version B (B.f.i.p | B > 0) MUST be incremented if any change to the public API would cause existing user code to stop working. F, I, and P MUST be reset to 0 when B is incremented.
Monotonicity. B MUST NOT decrease across the life of a project. For a fixed B, F MUST NOT decrease. For a fixed B.F, I MUST NOT decrease across successive releases of that B.F. For a fixed B.F.I, P MUST NOT decrease across successive releases of that B.F.I.
Different B.F pairs may carry on independent, concurrently maintained I.P histories, published in any order relative to each other — BFIP does not require a single global publication order across separate B.F lines.
Probation
A version of the form b.0.0.0 immediately following a B increment may be tagged as being on probation, by appending a number sign and a positive integer immediately following the patch version: b.0.0.0#n. This tag MUST only be applied to a version with F, I, and P all equal to 0 immediately following a B increment. It MUST NOT be applied to any other version.
- n MUST NOT decrease across successive probation releases of the same b.0.0.0.
- A probation-tagged version has strictly lower precedence than the plain version it decorates: b.0.0.0#n < b.0.0.0.
- Between two probation-tagged releases of the same b.0.0.0, the one with the larger n has higher precedence: b.0.0.0#n < b.0.0.0#m whenever n < m.
- A probation-tagged version MUST NOT be considered compatible with any other version, including the plain b.0.0.0 it is on probation for and any version of a different breaking generation.
- Probation ends by publishing the plain b.0.0.0, as a deliberate release act — not an automatic consequence of any increment rule. Once probation ends, the patch version begins its own, separate count from 0, independent of how high n climbed during probation.
Probation exists for a narrower reason than the rest of the spec: moving to a new B declares a new, non-fungible generation of the public API — one that by definition cannot be compatible with what came before. A project that wants to trial that new generation before standing behind it now has an explicit way to say so.
Compatibility & Precedence
Compatibility
Given a required version and an actual version, the actual version satisfies the required version if and only if:
- actual.breaking is equal to required.breaking, and
- actual.feature is greater than or equal to required.feature.
The internal and patch fields carry no compatibility meaning and MUST NOT affect this determination. A probation-tagged actual or required version MUST NOT be considered to satisfy, or be satisfied by, any version under this rule.
Precedence
- Precedence MUST be calculated by separating the version into breaking, feature, internal, patch, and probation number, in that order.
- Precedence is determined by the first difference when comparing breaking, feature, internal, and patch from left to right, each compared numerically. Example: 1.0.0.0 < 2.0.0.0 < 2.1.0.0 < 2.1.1.0 < 2.1.1.1.
- When breaking, feature, internal, and patch are all equal, a probation-tagged version has lower precedence than the plain version. Example: 2.0.0.0#1 < 2.0.0.0.
- When breaking, feature, internal, and patch are all equal and both versions are on probation, the one with the smaller probation number has lower precedence. Example: 2.0.0.0#1 < 2.0.0.0#2.
Constraints
Chronological constraints
Chronological constraints describe where a version falls relative to one or more fixed points in Precedence. A chronological constraint makes no claim about compatibility; it answers only where a version sits on the ordered line defined by Precedence — a version on probation is bound by the same constraint as any other version, with no exception.
| Form | Meaning |
|---|---|
| = V | satisfied only by V itself |
| > V / >= V | strictly greater / greater-or-equal precedence than V |
| < V / <= V | strictly lesser / lesser-or-equal precedence than V |
A bounded interval is the conjunction of one lower-bound and one upper-bound constraint. Example: >= 1.2.0.0, < 2.0.0.0 is satisfied by 1.2.0.0, 1.9.0.0, and 1.9.0.0#4 — and is not satisfied by 1.2.0.0#1 or 2.0.0.0.
Chronological constraints MUST NOT be used as a substitute for the Compatibility relation (Rule 14). A version satisfying a chronological constraint carries no guarantee of compatibility with any other version, including other versions satisfying the same constraint.
Dependency constraints
Dependency constraints describe what a project may safely build against. Unlike a chronological constraint, a dependency constraint MUST reflect the Compatibility relation (Rule 14) wherever it makes a safety claim. No dependency constraint of any kind may be satisfied by a probation-tagged version.
- Compatible-with (~ V) — satisfied by any actual version that satisfies V under Compatibility. This is the sole floor-and-ceiling primitive in BFIP: the ceiling is the next breaking version, implied automatically by Rule 14; the floor is V's feature version. It MUST NOT be given a second, artificial ceiling — intersect it with a chronological upper bound instead.
- Exact-version (= V) — the chronological exact constraint applied in a dependency context. Satisfied only by V itself; V must not be probation-tagged.
- A bare minimum-version request is a compatible-with constraint. A bare maximum-version request, with no safety claim, is a chronological upper bound; one that must also preserve safety is the intersection of both.
- Intersection (X AND Y) — satisfied only by a version satisfying both. Intersecting two compatible-with constraints with different required breaking versions is always unsatisfiable — a legitimate outcome to detect at resolution time, not a malformed constraint.
- Union (X OR Y) — satisfied by a version satisfying X, Y, or both. The only combinator capable of expressing support for more than one breaking version at once.
- Negation (NOT X) — satisfied by any non-probation version not satisfying X.
- Exclusion (X BUT NOT Y) — shorthand for X AND (NOT Y). Not an independent combinator.
Grammar
Backus–Naur form for a valid BFIP version string.
<valid bfip> ::= <version core>
| <version core> "#" <probation number>
<version core> ::= <breaking> "." <feature> "." <internal> "." <patch>
<breaking> ::= <numeric identifier>
<feature> ::= <numeric identifier>
<internal> ::= <numeric identifier>
<patch> ::= <numeric identifier>
<probation number> ::= <positive digit>
| <positive digit> <digits>
<numeric identifier> ::= "0"
| <positive digit>
| <positive digit> <digits>
<digits> ::= <digit>
| <digit> <digits>
<digit> ::= "0"
| <positive digit>
<positive digit> ::= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
FAQ
How should I handle revisions during breaking version zero?
There is no wrong way, since nothing during this phase is a compatibility promise. A common approach is to start at 0.1.0.0 and bump F for each subsequent round of development, using I and P as you would after 1.0.0.0, purely for your own bookkeeping.
How do I know when to release 1.0.0.0?
If people are depending on your public API and expect it to keep working, you are already past the point where 1.0.0.0 should have shipped. Staying in 0.f.i.p past that point just means BFIP isn't yet telling your users anything about compatibility, even though they're relying on it as if it were.
Won't projects with fast-moving public APIs blow through B quickly?
Only if the public API is genuinely breaking that often, in which case that's useful information, not noise. Projects going through rapid, still-unstable churn have breaking version zero for exactly that reason, and probation exists for the case where a single upcoming B needs a trial period before it's trusted.
What is the difference between an I bump and a P bump, concretely?
A P bump corrects behavior that was wrong. An I bump changes how correct behavior is achieved — performance work, restructuring, dependency swaps, anything that leaves the public API's inputs and outputs exactly as they were. If nothing was broken to begin with, it isn't a patch; it's internal.
What if I add something to the public API but also happen to fix a bug in the same release?
Release it as an F bump. F increments already permit any amount of I- and P-level change riding along, since F is the higher field of the two — the reset rule takes care of I and P automatically.
What if I accidentally ship a breaking change under F instead of B?
Fix it and release the correction under a proper B bump as soon as you notice. Don't edit the offending release after the fact — publish the correction as a new version, and document the mistake if people may already depend on the broken F release.
Can two different B.F lines really both keep releasing at once?
Yes. Once a B.F pair exists, BFIP allows it to keep accumulating its own I and P history indefinitely, in parallel with newer B.F pairs, with no requirement that releases across different lines interleave in any particular order.
If I write < 2.0.0.0, why does that also match 1.9.0.0's probation builds?
Because a chronological constraint only asks where a version sits in publication order, and every probation build of 1.9.0.0 or earlier sorts below 2.0.0.0 by definition. It isn't asking whether that version is safe to depend on — for that, use the Compatibility relation, or a dependency constraint built on it.
When would I reach for a chronological constraint instead of compatibility?
Whenever the question is about the timeline rather than about safety: which versions have shipped since a given point, sorting a release history, deciding what "latest" means, or enforcing Monotonicity during publishing. The moment the question becomes "can I depend on this instead of that," you want Compatibility.
Why doesn't a compatible-with constraint let me set my own upper bound?
Because Rule 14 already gives it one for free: the next breaking version. Letting a dependency constraint override that would mean "compatible with" no longer always means the same thing. If you want a tighter ceiling, intersect a compatible-with constraint with a chronological upper bound instead — the guarantee stays intact, and the extra caution is visible in the constraint.
Why can't I depend on an exact probation build?
Because a dependency constraint is a promise about safety, and a probation build carries no such promise for anyone — Rule 13.4 already says it satisfies no compatibility relation at all. Testing against a specific probation build is legitimate, but it belongs to a project's own test tooling, not to a dependency declaration other projects might inherit unknowingly.
Does BFIP have build metadata like Semantic Versioning?
No. BFIP has exactly one extension to the plain four-field format — the probation tag — reserved for trialing an unreleased B. There's no mechanism for embedding build or provenance information in a BFIP version string.