Skip to content

chore: release as 3.0.0 and write down the BC break #18 shipped - #28

Merged
karlwaldman merged 3 commits into
mainfrom
chore/version-3-bc-notes
Sep 13, 2026
Merged

karlwaldman merged 3 commits into
mainfrom
chore/version-3-bc-notes

Conversation

@karlwaldman

Copy link
Copy Markdown
Member

The problem

Client::VERSION reads '2.1.2' and the changelog entry is headed
## Unreleased with no version at all — while Price::fromArray(), which is
public, documented and callable directly, went from total to partial in
#18. It now throws ApiException for payloads the 2.x line accepted. That is a
major, not a patch, and an integrator reading either signal would conclude
nothing in their code could break.

What this PR does

  • Client::VERSION → 3.0.0. The change already shipped to main
    labelled as a patch; this corrects the label rather than re-releasing the
    code. No 2.2.0 in between, stated explicitly in the changelog.

  • CHANGELOG gets a version heading and a ### Breaking changes section —
    a table of exactly what throws now that did not before, what is still
    preserved, and the code a caller writes to adapt:

    Row 2.x behaviour 3.0 behaviour
    no code, or a blank or non-string code code became '' ApiException
    no price, or a non-numeric price price became 0.0 ApiException
    an unparseable created_at/updated_at a date was invented ApiException

    Still preserved: a legitimate zero or negative price, a genuinely empty
    prices list, and a row with no timestamp field (updatedAt stays null).

    What callers do: a caller using Client methods and already catching
    ApiException needs no change — those methods surfaced malformed data as
    ApiException before. A caller invoking Price::fromArray() on its own
    payloads must wrap it. Anyone who relied on the manufactured values (reading
    $price->price === 0.0 as "no data") must switch to catching. There is no
    opt-out by design: the manufactured values were indistinguishable from real
    quotes once they left the SDK.

  • README gets an "Upgrading to 3.0" section making the same point where a
    caller will actually see it.

  • tests/PublicClaimsTest.php updated to match.

PHPStan: filed, not wired — and why

I confirmed the reviewer's count. On origin/main @ 21339d43e,
phpstan analyse src --level 9: [ERROR] Found 17 errors.

src/Client.php:225  Method Client::dataOrFail() should return array<string, mixed> but returns array<mixed, mixed>.
src/Client.php:265  Parameter #1 $data of Price::fromArray() expects array<string, mixed>, array<mixed, mixed> given.
src/Client.php:314  Strict comparison using !== between string and null will always evaluate to true.
src/Client.php:340  Call to function assert() with true will always evaluate to true.
src/Client.php:340  Instanceof between HttpResponse and HttpResponse will always evaluate to true.
src/Client.php:414  Cannot cast mixed to string.
src/Client.php:415  Cannot cast mixed to string.
src/Client.php:421  Cannot cast mixed to string.
src/Client.php:579  Cannot access offset 'message' on mixed.
src/Client.php:589  Cannot access offset 'message' on mixed.
src/Http/CurlTransport.php:27  Parameter #2 $options of curl_setopt_array expects ... given.
src/Price.php:96   Cannot cast mixed to string.
src/Price.php:99   Cannot cast mixed to string.
src/Price.php:100  Cannot cast mixed to string.
src/Price.php:101  Cannot cast mixed to string.
src/Price.php:102  Cannot cast mixed to string.
src/Price.php:103  Cannot cast mixed to string.

Wiring it in here is not cheap, for a specific reason: the six
src/Price.php errors are precisely the casts being removed in #27, and
src/Http/CurlTransport.php:27 is in the file being rewritten in #26. Adding
PHPStan now means either a baseline whose line references conflict with both
open PRs, or fixing Client.php's ten mixed errors inside a PR about version
numbers. Either bundles work this PR should not carry.

Inventory posted to #24 with the ordering: land it after #26 and #27, when six
of the seventeen are already gone and it can go in at level 9 with no
baseline
.

TDD evidence

Test written first, against unmodified origin/main.

RED (./vendor/bin/phpunit --filter VersioningTest, source at origin/main):

.FFFF.                                                              6 / 6 (100%)

There were 4 failures:

1) VersioningTest::testVersionRecordsTheBreakingChangeAsAMajor
Price::fromArray() went from total to partial, so 2.1.2 is not a valid version for this line: a caller reading it would expect no breakage.
Failed asserting that 2 is greater than 2.

2) VersioningTest::testTopmostChangelogEntryCarriesAVersionHeading
The topmost CHANGELOG entry is "## Unreleased" and names no version.
Failed asserting that 'Unreleased' matches PCRE pattern "/^\d+\.\d+\.\d+/".

3) VersioningTest::testTopmostChangelogEntryMatchesTheShippedVersion
The CHANGELOG and Client::VERSION disagree about what is shipping.
Failed asserting that 'Unreleased' starts with "2.1.2".

4) VersioningTest::testChangelogDocumentsTheBreakingChange
The major entry has no "### Breaking changes" section.

FAILURES!
Tests: 6, Assertions: 10, Failures: 4.

Re-proved red-capable after the fix by
git checkout origin/main -- src/Client.php CHANGELOG.md:
Tests: 6, Assertions: 10, Failures: 4. — then restored.

GREEN (this branch):

......                                                              6 / 6 (100%)
OK (6 tests, 12 assertions)

The test is a standing fence, not a one-off: the version must be semver, its
major must be past the last BC-compatible line, the topmost changelog entry
must name a version and match Client::VERSION, that entry must document the
break and name what it throws, and the User-Agent must carry the version it
claims.

Full suite, PHP 8.5.8 / PHPUnit 13.3.3:

  • baseline on clean main @ 21339d43e: OK (108 tests, 478 assertions)
  • this branch: OK (114 tests, 490 assertions)

Note on ordering

#25, #26 and #27 each narrow Price::fromArray() or the transport further.
They all belong in this same 3.0.0 entry; each adds its own changelog bullet
under the section headings this PR creates. No version bump is needed on top.

🤖 Generated with Claude Code

https://claude.ai/code/session_015ao5paex73xXvuM424Libo

`Client::VERSION` still read '2.1.2' and the changelog entry carried no
version heading at all, while `Price::fromArray()` - public, documented, and
callable directly - went from total to partial in #18. An integrator reading
either signal would conclude nothing in their code could break.

- `Client::VERSION` is 3.0.0. The change shipped to main labelled as a patch;
  this corrects the label rather than re-releasing the code.
- CHANGELOG gets a version heading and a `### Breaking changes` section: a
  table of exactly what throws now that did not before, what is still
  preserved (legitimate zero and negative prices, an empty `prices` list, a
  row with no timestamp), and the code a caller writes to adapt.
- README gets an "Upgrading to 3.0" section making the same point where a
  caller will actually see it.
- `VersioningTest` fences all of it: the version is semver, its major is past
  the last BC-compatible line, the topmost changelog entry names a version and
  matches `Client::VERSION`, that entry documents the break and what it
  throws, and the User-Agent carries the same version it claims.

PHPStan is deliberately NOT wired in here. Level 9 reports 17 errors on src/,
and six of them are the `(string)` casts being removed in the #21 PR while an
eleventh is in the transport being rewritten in the #22 PR. Adding it now
means either a baseline that conflicts with both, or fixing Client.php in a
PR about version numbers. Inventory posted to #24 instead, to land after
those two.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ao5paex73xXvuM424Libo
@coderabbitai

coderabbitai Bot commented Sep 13, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 4827638e-c2b4-4abf-9fcf-7203ca8a2ed5


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

karlwaldman and others added 2 commits September 13, 2026 13:15
The section documented only the #18 break. A reader deciding whether it is
safe to upgrade reads "### Breaking changes" and nothing else, so a break
recorded only under "Fixed" or "Security" is one they meet in production.

Added to the table: `currency` is now required, a non-string `currency`, and a
non-string `unit`/`name`/`source`/`type`/`formatted` (#21). Split the single
"unparseable timestamp" row into the two distinct cases #20 introduced - a
value PHP repaired into a plausible date, and a relative expression such as
`now` - because #18 already rejected the genuinely unparseable and conflating
them understates what changed.

Added as their own entries: redirects are no longer followed (#22), and the
new `lib-curl >= 7.58.0` platform requirement, which is an install-time break
on an old host and appears nowhere else a reader would look.

Called out the two rejections a reader would not predict: leap seconds, and
naive timestamps read as UTC rather than host-local.

Extended the "what still works" paragraph to cover absent and null label
fields, so a reader can tell the optional fields stayed optional.

VersioningTest now fences all of it: eight required breaks, two surprising
rejections, and the still-works guarantees.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ao5paex73xXvuM424Libo
#25 and #26 landed; their CHANGELOG bullets merge cleanly under the 3.0.0
heading this branch created. No conflict.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ao5paex73xXvuM424Libo
@karlwaldman

Copy link
Copy Markdown
Member Author

The 3.0.0 entry did NOT read correctly. Fixed in bc4ba10, plus a merge of 056669c.

Answering the hold directly: the heading is fine, the contents were not. ### Breaking changes documented only the #18 break. Everything else was recorded under ### Fixed, ### Security or ### Changed — which is where the detail belongs, but a reader deciding whether it is safe to upgrade reads the breaking-changes section and stops. Four breaks were reachable only by reading the whole entry, and two of them were in no section a reader would think to check.

What was missing

Break Was documented under Now also in Breaking changes
currency is required ### Changed table row
a non-string currency throws ### Fixed table row
a non-string unit/name/source/type/formatted throws ### Fixed table row
redirects are no longer followed ### Security its own entry
lib-curl >= 7.58.0 is now required nowhere its own entry

The libcurl floor is the one that would actually have bitten someone: it is an install-time break — Composer refuses on an older host — and it appeared in no section at all.

I also split the single table row an unparseable created_at/updated_at | a date was invented. That conflated two different changes: #18 already rejected the genuinely unparseable (not-a-date), so as written the row understated #20 rather than describing it. It is now two rows — a value PHP repaired (2026-13-45T99:99:99Z → 2027-02-18T04:40:39Z) and a relative expression (now, next friday, +1 week) — which is what actually changed.

Added the two rejections a reader would not predict and would otherwise meet on a row that looks fine: leap seconds (23:59:60) and naive timestamps read as UTC rather than host-local. Both are Karl's open judgement calls; naming them in the changelog is not the same as settling them.

Extended the "what still works" paragraph to say that absent and explicitly null label fields still mean "not provided", so a reader can tell the optional fields stayed optional.

Fenced, not just fixed

VersioningTest now asserts the section itself — eight required breaks, two surprising rejections, three still-works guarantees. Written red first against the pre-edit changelog:

1) ...@currency is required with data ('no `currency`')
"no `currency`" is not documented under "### Breaking changes".
...
7) ...@libcurl floor with data ('lib-curl >= 7.58.0')
"lib-curl >= 7.58.0" is not documented under "### Breaking changes".
8) ...@leap second
9) ...@naive timestamp timezone

Tests: 17, Assertions: 58, Failures: 9.

Green after: OK (17 tests, 58 assertions).

A break that lands in a future PR without a line in this section now fails CI rather than shipping quietly.

Verified against the actual final release state

I built main + #27 + #28 locally and read the entry that a 3.0.0 reader will get:

### Breaking changes   fromArray table (7 rows), redirects, lib-curl floor, version-jump note
### Security           redirects (#22), raw API paths (#17)
### Changed            Breaking: currency required (#21)
### Fixed              timestamps (#20), casts (#21), malformed rows (#18),
                       durable quota (#16), Retry-After (#16)

All four changes present. Suite on that combined state: OK (229 tests, 747 assertions).

Baselines

Tests Assertions
clean main @ 056669c (pristine clone) 160 593
this branch (now merged with 056669c) 177 651
simulated main + #27 + #28 229 747

One thing left for you, at release time

The heading reads ## 3.0.0 (unreleased). It needs its date when you tag — ## 3.0.0 (YYYY-MM-DD). VersioningTest accepts either, deliberately, so it does not block the merge; it is a release-day edit, not a blocker now.

Ordering is unchanged and still correct: #27 first, then this. Merging this first would publish a 3.0.0 whose breaking-changes table describes cast and currency behaviour that is not in the code.

🤖 Generated with Claude Code

https://claude.ai/code/session_015ao5paex73xXvuM424Libo

@karlwaldman
karlwaldman merged commit e39965c into main Sep 13, 2026
8 checks passed
@karlwaldman
karlwaldman deleted the chore/version-3-bc-notes branch September 13, 2026 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant