Semantic Versioning
Implements: REL-01, REL-05, REL-06, GATE-03 · requirements stated in CHARTER.md Part 8
How to decide whether a change to @lgriffin/esi.ts is a major, minor or patch release, and how to say so in a commit so the published version is right. RELEASE.md covers what happens after the commits land: release-please, the changelog, publishing and signing.
The version number is a promise to every consumer. ^9.9.0 in someone's package.json means "install any 9.x.y release without my code breaking". A breaking change released as a minor breaks every one of those installs on their next npm install. A harmless change released as a major makes consumers read migration notes that do not exist. Both are bugs.
The three numbers
MAJOR.MINOR.PATCH, following Semantic Versioning 2.0.0.
| Bump | When | Commit |
|---|---|---|
| Major | Code that compiled and ran against the previous release can fail against this one | type!: subject and a BREAKING CHANGE: footer |
| Minor | New capability; everything that worked before still works the same way | feat: |
| Patch | A fix or internal change with no new capability and nothing removed | fix:, perf:, refactor:, chore:, docs:, test: |
The package is past 1.0, and release-please runs with bump-minor-pre-major: false, so these rules apply exactly as written. There is no "it's only a minor break".
What the public contract is
Everything a consumer can reach without importing from dist/ by path:
- Every entry point in
package.jsonexports:.,./schemas,./errors,./testing,./client,./sde,./sde/memory, and./package.json../testing(TestDataFactoryand friends) is public; consumers use it in their own tests. - The types those entry points export. That includes interfaces consumers implement, such as
ILogger,ICircuitBreaker,IDeduplicatorandIRetryStrategy, as well as ones they only call. - Runtime behaviour documented in the guides. This covers which error class is thrown and its
retryableflag (ERRORS.md), what a response schema accepts (RUNTIME-VALIDATION.md), pagination results (PAGINATION.md), default configuration values, and log events other code may key on (LOGGING.md). - The environment.
engines.node, the oldest supported TypeScript (5.4, checked by the consumer contract),dependencieswhose types appear in the public API (zod), andpeerDependencies(js-yaml,adm-zip), including their ranges.
Not public: anything not reachable from an entry point, file layout under src/, test files, CI, generated files' internal shape where no exported type depends on it, and log message wording.
Deciding
Ask: could code that works against the last release stop compiling, throw, or return something different? If yes, the change is major. The tables below cover the cases that come up in this repository.
Exports and signatures
| Change | Bump | Why |
|---|---|---|
| Add an export, a client method, a sub-path export or an endpoint | minor | Nothing existing changes |
| Remove or rename an export, method, sub-path or endpoint | major | Importers stop compiling |
| Add an optional parameter or an optional config field | minor | Existing calls still type-check and behave the same |
| Add a required parameter, or make an optional one required | major | Existing calls stop compiling |
Narrow a parameter type (number | string → number) | major | Callers passing the removed type stop compiling |
Widen a parameter type (number → number | string) | minor | Existing calls still compile |
Widen a return type (T → T | undefined) | major | Callers must handle a case they never saw |
Narrow a return type (T | undefined → T) | minor | Existing handling still compiles |
Add a required member to an interface consumers implement (ILogger) | major | Their implementations stop compiling. The API report shows only an added line, so the gate cannot catch this one |
| Add an optional member to such an interface | minor | Existing implementations still satisfy it |
| Add a required member to a type the library only returns | minor | Consumers read it; they never construct it |
Runtime behaviour
| Change | Bump | Why |
|---|---|---|
Throw a different error class, or flip retryable, for an existing case | major | instanceof checks and retry logic in consumer code change behaviour |
| Tighten a Zod response schema (a field becomes required, an enum loses a member) | major | Responses that validated before now throw EsiValidationError at runtime |
| Make a response field optional in a Zod schema | major | The inferred type widens to T | undefined, so consumer code that reads the field can stop compiling. This holds even when ESI already omits the field |
| Add a member to an ESI enum union in a response schema | minor | ESI enums are open by design (CHARTER principle 3): consumers must not switch on them exhaustively. z.looseObject keeps unknown fields regardless |
| Change a default that alters results: retry count, cache on or off, validation on or off, datasource | major | Same code, different outcome |
| Change a default that only alters timing or resource use, within documented bounds | patch | Results are the same |
| Fix a bug so the documented behaviour now happens | patch | The contract was already stated; the code now meets it |
| Fix a bug that consumers could reasonably depend on (an error that was swallowed now throws) | major, or patch with a changelog warning | Decide in the pull request and write the reason down; when unsure, ask the maintainer |
| Performance improvement with identical results | patch | perf: |
Environment and dependencies
| Change | Bump | Why |
|---|---|---|
Drop a Node line (raise engines.node) | major | REL-05; installs on that line stop working |
Raise the oldest supported TypeScript (OLDEST_TYPESCRIPT) | major | Consumers on that version stop compiling |
Major version of a dependency whose types are public (zod 4 → 5) | major | Consumers' schemas and inferred types change |
Raise the minimum of a peer dependency (js-yaml) | major | Existing installs no longer satisfy it |
| Add a new required runtime dependency or peer dependency | major | Existing installs may break or warn |
| Remove an optional peer dependency that no public export needs | minor | Existing installs keep working; nothing public loads it |
| Patch or minor dependency update with no public type change | patch | chore(deps): |
Changes that come from ESI itself
CCP changes the ESI spec on its own schedule, and npm run generate:types follows. Classify the resulting library change by the tables above, not by what CCP called it:
- ESI adds a field or an endpoint: minor.
- ESI removes an endpoint: deprecate it first (below). Removing the client method is a major release, even though the endpoint no longer works, because removal is what stops consumers' code compiling.
- ESI stops always sending a field the schema requires: make it optional, as
fix(schemas)!:. It fixes validation failures consumers already see, but the inferred type widens, so it is still a major release. Say both in theBREAKING CHANGE:footer.
Deprecate before you remove
A removal should never be the first a consumer hears of it.
- In a minor release: mark the export
@deprecatedin JSDoc, naming the replacement. For an endpoint whose ESI operation is deprecated upstream, addDeprecationInfo(message,replacedBy,sunsetDate) to its definition, socreateClientlogs a warning on each call. A hand-written method logs throughlogWarn(this._client, …)(DESIGN-RULES.md §3.6). Record it in the changelog under Deprecated. - Keep it working for at least one minor release.
- In the next major release: remove it, with a
BREAKING CHANGE:footer that names the replacement.
When ESI has already removed the endpoint and every call fails, steps 1 and 3 can land in consecutive releases. The method still stays until a major release.
Adding @deprecated changes no signature and no behaviour, but it is a change to the contract consumers read, so it goes in as feat: (11.0.0's feat(client): deprecate EsiApiFactory's named methods … is the precedent) and reaches a minor release and the changelog. Lifting a deprecation is the same kind of change, also feat: in a minor release (12.2.0 lifted 12.1.0's). The API report rewrites each declaration's comment line to // @public @deprecated and back, which the api-semver gate reads as a changed line, so the commit carries an API-Compatible: trailer saying only tags changed.
The custom clients are never deprecated. EsiClient, getDefaultClient, EsiClientBuilder, CustomEsiClient, EsiApiFactory with createClient(type), BaseEsiClient, ApiClientType and every domain client are the product (charter posture 1). ARCH-14 fails a change that marks one @deprecated, removes it or replaces it with the generated surface (tests/tdd/scripts/custom-client-identity.test.ts); changing that starts with a charter amendment, not with this process. CLIENT-ARCHITECTURE.md records the decision.
Saying it in the commit
release-please reads commit messages on master and nothing else. The only way to get the version right is to write the commit right. commitlint checks the format; it cannot check honesty.
A breaking change
Put ! after the type and add a BREAKING CHANGE: footer that tells a consumer what to do:
feat(market)!: require the order type in getMarketOrders
Fetching both sides of the book by default doubled the payload for the
common case of reading one side.
BREAKING CHANGE: getMarketOrders(regionId) is now
getMarketOrders(regionId, orderType), where orderType is 'buy', 'sell' or
'all'. Pass 'all' to keep the previous behaviour.The ! triggers the major bump. The footer is the migration note that release-please copies into the changelog. Either one alone bumps the version, but a ! without a footer ships a major release with no explanation.
A compatible change the API report flags
Some compatible changes still remove or rewrite a line in etc/esi.ts.api.md, such as an optional parameter added to an existing signature. Say why it is compatible with an API-Compatible: trailer:
feat(market): accept an optional type filter in getMarketOrders
API-Compatible: the new parameter is optional, so existing calls still type-check and return the same orders.Which commit carries the marker
- A pull request that contains the break carries the marker on the commit that introduces it.
- Never mark a change breaking "to be safe". An unnecessary major release is also a wrong version.
- Never hide a break inside a
chore:,refactor:ortest:commit. The type describes the change to consumers, not the kind of work.
Merging the pull request
GitHub offers three ways to merge a pull request, and they give release-please different commits to read:
| Button | What lands on master | What release-please reads |
|---|---|---|
| Merge commit | Every commit from the branch, unchanged, plus one extra "Merge pull request #N" commit that joins the branch to master | Each branch commit; the merge commit is ignored |
| Squash and merge | One new commit containing all the branch's changes. With several commits, its title is the pull request title; with one commit, it is that commit's message | That single commit |
| Rebase and merge | Every commit from the branch, replayed on top of master, with no merge commit | Each branch commit |
The consequences:
- Merge commit or rebase: each commit's type and markers reach the changelog as written. Use one of these when a pull request mixes types, for example a
fix:alongsidetest:commits, so each gets its own changelog entry. - Squash: the pull request title becomes the only commit title. It must be a conventional commit, and it must carry
!when any commit in the pull request is breaking. Write anyBREAKING CHANGE:footer into the squash dialog's message box too. Squash also collapses the changelog to one entry.
What checks it
| Check | What it catches | Where |
|---|---|---|
| commitlint | A commit message that is not a conventional commit | commit-msg hook |
| API surface diff (GATE-03) | An exported shape changed without etc/esi.ts.api.md being regenerated (npm run api-report) | ci.yml API Surface Check |
publicApiSurface.test.ts | A root export renamed or removed | npm test |
api-semver gate (REL-06), added by #310 | The API report lost or changed a line, but no commit is type!:, has a BREAKING CHANGE: footer or an API-Compatible: trailer; or a declared break spans several commits and the pull request title is not type!: | ci.yml, npm run api-report:semver |
| Review | Everything else in Deciding: runtime behaviour, schema tightening, defaults, required members on implemented interfaces, sub-paths the report does not cover, dependency changes | AGENTS.md Reviewer Checklist |
The API report covers only the root entry point, and it cannot see runtime behaviour. The gate is a tripwire for the common case, not a proof. The author is responsible for the classification, and the reviewer checks it.
Checklist for every pull request
- Does the change touch anything in the public contract? If not, it is a patch-level type (
fix:,refactor:,chore:and so on) and you are done. - Classify it with the tables in Deciding.
- If an exported shape changed, run
npm run api-reportand commitetc/esi.ts.api.md. - Breaking:
type!:plus aBREAKING CHANGE:footer with the migration. If it removes something, was it deprecated in an earlier minor release? If not, deprecate first unless ESI has already removed it. - Compatible but flagged by the report: an
API-Compatible:trailer with the reason. - Squash-merging several commits: pull request title in conventional form, with
!if anything is breaking.
