kdiff 1.0.0
The version where the promises stop being provisional. docs/api-stability.md was written before the
tag on purpose; this is the tag it was written for, and from here the recorded public API of the three
published modules, the closed Change vocabulary and the three declared exception types only change in
a major version.
Two things had to happen first. One is a retraction: PatchFailure.Reason was declared closed in
0.7.0 and is not — a minor version may declare a further reason, and freezing the set for the length
of 1.x would have made the next supported shape choose between a major version and a reason that
misdescribes itself. Better to loosen it before the tag than to discover it after.
The other is that the promises are now checked by the build rather than by a maintainer remembering.
A consumer build resolving only the three published coordinates compiles and runs inside
./gradlew check, the regeneration claim has a test that spans two builds, and every message
docs/errors.md quotes is compared to the source that emits it.
No behaviour of the library changes in this release. If you are on 0.7.0 and do not match
PatchFailure.Reason exhaustively without an else, the upgrade is the version number.
Added
-
The release is checked, not inspected. A new unpublished
kdiff-integrationmodule drives a
Gradle TestKit build that declares onlykdiff-annotations,kdiff-runtimeandkdiff-processor,
resolved as artifacts from a repository underbuild/, then annotates a class, compiles it and runs
the generated differ. It is the only thing in the repository that reads a generated POM — the sample
and the tutorial consume the project, so a descriptor missing a dependency was invisible to them.
It also asserts that neither KotlinPoet nor the KSP API reaches a consumer's compile classpath.
Nothing is published to~/.m2and nothing needs the network or a credential. -
The incremental-regeneration promise has a test. The same harness builds a consumer twice with an
edit between the runs, and checks both halves: that editing a declaration a differ read regenerates
that differ, and that an unrelated annotated class's generated file is left exactly as the first
build wrote it.0.7.0fixed a real bug here, found by reasoning, because nothing could catch it. -
Every message
docs/errors.mdquotes is checked against the source that emits it.
DocumentationMessagesSpeccuts each source message into the runs of text the code states literally
and requires the page's quotation to contain them in order, so rewording a diagnostic without editing
the page failscheck.CONTRIBUTING.mdloses the paragraph that admitted this hole. -
The toolchain versions the README names are pinned to the build. The
kotlin("jvm")and KSP
plugin versions in the install snippet, and the Kotlin badge, now fail the build when they name
something the build does not use — the same treatment the published coordinates already had, applied
to the other half of the block a reader copies. -
The recorded API and the promised API are stated as two different things.
docs/api-stability.md§1 now names the entries the ABI dump records
without promising: a declaration published only so that aninlinefunction can reach it
(ChangeRoutes.register,ChangeRoutes.dispatch,ElementRoutes.dispatchand the routing classes'
constructors), and the accessors an inlinevalue classlowers to (FieldPath.box-impl,
constructor-impl, and the mangledgetPath-…on eachChangevariant). They stay in the dump, so
their removal still fails the build — that is what a mechanical dump is for. What they are not is
yours to call, and the mangled accessors are the concrete reason kdiff is not designed for Java
consumers:change.getPath()is not a method that exists. -
The three additions a minor version may make are written down, each with what code written before
it still does: a furtherPatchFailure.Reasoncase, a member onDiffer,PatcherorTracked
carrying its own implementation, and a further capability interface on a generated object. A
hand-writtenobject : Patcher<T>inherits a defaulted member, andDiffer<T> { … }keeps
converting, because afun interfaceadmits non-abstract members. A member without an
implementation stays breaking. -
A Kotlin and KSP compatibility policy, which the repository previously stated nowhere. The
runtime and annotations require a Kotlin compiler at least the version they were built with, because
an older one cannot read their metadata; the processor expects the KSP plugin matching your Kotlin
version, which is the one you apply anyway. A Kotlin minor is tracked by a kdiff minor, and only
the declared pair is tested. → Kotlin and KSP -
Two questions in the FAQ: Which Kotlin version do I need? and What may change in a
kdiff minor? -
An experimental opt-in tier is recorded as declined, with the reasoning and its cost — an
addition in1.xis permanent from the release that makes it, which is a reason to add slowly rather
than a reason for a marker every call site would carry.
Changed
-
BREAKING —
PatchFailure.Reasonis no longer a closed vocabulary.0.7.0said it was sealed
and closed, on the same terms asChange: fourteen cases, and adding one breaking. That is
retracted. It stays sealed — nothing outside kdiff declares a case, and every case still carries the
facts it knows as properties — but a minor version may now declare a further reason. A reason
accompanies a newly supported shape, and both releases since the vocabulary was declared wanted new
ones; freezing the set for the length of1.xwould make the next supported shape choose between a
major version and a reason that misdescribes itself.Changeis unaffected and stays closed. The two differ in what a caller does with them: a change is
what routing dispatches on, so exhaustive handling is the reason the type is sealed at all; a reason
is what a caller reports.Nothing about this release's behaviour changes — there are still fourteen cases and none has moved.
What changes is what a later release may do, which is why the loosening lands before1.0.0rather
than after it.Migration: if you match reasons exhaustively with no
else, add one. Such awhencompiles against
this release and against every release that adds nothing; the release that adds a case makes it a
compile error on recompilation, and — because Kotlin lowers an exhaustivewhento a throw on the
branch it believes unreachable — aNoWhenBranchMatchedExceptionif the code is not recompiled. A
caller that only logs or renders failures needs no change at all:PatchFailure.toString()covers
every case, including one declared after your code was written. -
BREAKING —
DiffProcessorisinternal.kdiff-processor's recorded API is now the single
declaration its artifact exists to provide,DiffProcessorProvider, which the compiler loads through
the module's service registration. A dump of exactly one entry is what fails the build the day a
helper class becomes public by accident.Marked breaking because a recorded public declaration disappears, which this project treats as
breaking without exception. Migration: none — a consumer that namedDiffProcessorwas constructing
a symbol processor outside a compiler. -
JVM 21 is stated as the platform kdiff targets, rather than as fallout from
inline. No
requirement moved and no code changed: a consuming module still needs a JDK 21 or later and a JVM
target of 21. What changed is that the README and the FAQ derived that from the builder entry points
beinginline, which read as an accident with a workaround behind it. It is a choice, and two things
follow from it — class files a JVM 21 loads, andinlineentry points a consumer must compile at
target 21 to use. Those entry points arediffer { },trackScope { },tracker { },Diff.route
andChangeRoutes.onEach, so the requirement reaches a consumer who only routes a generated differ's
diff, not just one who writes a differ by hand. -
Every promise in
docs/api-stability.mdnow says when it takes effect: at1.0.0. Until then a
minor version may break anything on that page, including a promise on it — which is what writing it
before the tag is for.
Coordinates
implementation("io.github.rcapraro:kdiff-annotations:1.0.0")
implementation("io.github.rcapraro:kdiff-runtime:1.0.0")
ksp("io.github.rcapraro:kdiff-processor:1.0.0")