Skip to content

Write the public surface down, and check it every build - #14

Merged
tamnd merged 1 commit into
mainfrom
surface
Aug 22, 2026
Merged

Write the public surface down, and check it every build#14
tamnd merged 1 commit into
mainfrom
surface

Conversation

@tamnd

@tamnd tamnd commented Aug 22, 2026

Copy link
Copy Markdown
Owner

This is the stability item on the zu-java scorecard, which the clients spec words as: an API stability gate wired in, the tool that tells a reviewer the public surface moved before a user finds out.

api/surface.txt has one line for every exported type and every member a caller outside the module can name, across dev.zudb and dev.zudb.spi. SurfaceTest regenerates it from the compiled classes and compares. A change to the API is then a change to that file, in the same commit, at the top of the diff, rather than an implementation-looking line somebody waves through.

The file name and the place are zu-go's, deliberately. That repository already keeps api/surface.txt in the shape of the api/go1.N.txt files the language holds itself to, and a reviewer moving between the clients should not have to learn a second mechanism. Write it with mvn -pl zudb test -Dzu.surface.write=true, which is the one command to reach for when the change was meant, and running it is how you say so.

There is no japicmp and no revapi here, and that is on purpose rather than for want of trying. Both compare against a published baseline, and nothing is published yet, so on this repository today they would either compare against nothing or hold the client to a version that does not exist. A checked-in file compares against the last commit instead, which is the thing a reviewer is actually looking at, and it goes on working the same way after the first release.

One thing worth reading in the test. Nothing in the generated file comes from Class.toGenericString, because that method is not the same on every JDK the api job builds for. JDK 21 leaves the sealed marker out of it and JDK 25 puts it in, so the first version of this passed locally and would have gone red on two rows of the matrix for a change nobody made. A gate that cries wolf on the version matrix is a gate people turn off. Everything is spelled out off the class file instead: modifiers masked to what a caller can see, the kind, the type parameters, the permitted subclasses, the supertype and the interfaces, and the same for members. Checked on both JDK 21 and JDK 25 and the output is byte for byte identical.

The failure prints only the lines that moved:

the public surface moved and api/surface.txt did not:
  + method public long dev.zudb.Result.rows()
  - method public long dev.zudb.Result.rowsRenamed()

A name that arrived is a minor release. A name that went or changed shape is a major one, or a mistake.

There is a second test asserting the generator found something, because a generator that quietly found nothing would make every future change pass, and that is the one failure this test cannot report on itself.

No CI change is needed. The api job already runs mvn -pl zudb -am test on 17, 21, 25 and 26-ea, so the gate runs four times per push and reads nothing but the compiled classes, which means it answers on a clone with no library staged and no Rust installed.

Checked on a Linux box: the gate passes, and it bites when the file and the code disagree. The full reactor is green, API, TCK, Panama, JNI, Arrow and benchmarks.

The README gains the inside-the-repository table the other clients have, with the paragraph that says what the file is and how to regenerate it.

api/surface.txt has one line for every exported type and every member a
caller outside the module can name, and SurfaceTest regenerates it and
compares. A change to the API is now a change to that file, in the same
commit, where it is the first thing in a diff rather than something a user
finds after the release.

This is the same mechanism zu-go uses, held to the same file name and the
same place, because a reviewer moving between the clients should not have
to learn a second one. Write the file with mvn -pl zudb test
-Dzu.surface.write=true, which is the one command to reach for when the
change was meant, and running it is how you say so.

Nothing here comes from Class.toGenericString. That method is not the same
on every JDK the api job builds for: 21 leaves the sealed marker out and 25
puts it in, so the first version of this reported a change nobody made as
soon as it ran on the matrix. Everything is spelled out off the class file
instead, and the output is byte for byte identical on 21 and 25.

The failure prints the lines that moved rather than the file twice. Five
hundred lines against five hundred lines is a report nobody reads, and the
two lines that changed are the report.
@tamnd
tamnd merged commit 2426fcd into main Aug 22, 2026
9 of 17 checks passed
@tamnd
tamnd deleted the surface branch August 22, 2026 09:51
tamnd added a commit to tamnd/zu that referenced this pull request Aug 22, 2026
api/surface.txt has a line for every exported type and every member a
caller outside the module can name, and a test regenerates it and compares,
so a change to the API is a change to that file in the same commit. Same
file name and same place as zu-go, because a reviewer moving between the
clients should not have to learn a second mechanism.

No japicmp and no revapi: both compare against a published baseline and
nothing is published yet. A checked-in file compares against the last
commit, which is what a reviewer is looking at anyway.

tamnd/zu-java#14. That takes the practice score to 70, and what is left is
leaks and install, which are apparatus, and api-map and perf, which are
reports the release collects.
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