Skip to content

Say which parts of the Kotlin API may move, and retract the flag nobody needed - #13

Merged
aoreshkov merged 1 commit into
mainfrom
say-which-parts-of-the-api-may-move
Aug 10, 2026
Merged

Say which parts of the Kotlin API may move, and retract the flag nobody needed#13
aoreshkov merged 1 commit into
mainfrom
say-which-parts-of-the-api-may-move

Conversation

@aoreshkov

Copy link
Copy Markdown
Owner

Three changes every embedding of this engine needs, and none of them touches the engine. No format
change, no new dependency, no answer any query returns.

Say which declarations are allowed to move — STABILITY.md + @RaboshExperimental

"Major version zero: any signature may change in any release" was honest and unactionable. A
consumer could not tell whether Key.of was as volatile as IndexCatalog.readColumn, so the only
rational responses were to wrap all of the API or none of it. There are now two tiers: a small stable
core that moves only under a deprecation cycle, and everything else, marked with an opt-in
requirement.

It is deliberately a substitute for 1.0 rather than a step towards one, and STABILITY.md says
so in as many words.

What is marked is the way in, not every member. Holding a ColumnReader means having passed
Rabosh.indexCatalog or IndexCatalog.readColumn, so both of those are marked and the reader's own
methods carry nothing. Marking every member instead is ~150 annotations and, worse, forces every
stable signature naming an experimental type to be marked too — a cascade that ends with the stable
core inside the experimental tier.

SegmentObserver is that cascade caught at one step: RaboshOptions' constructor names it, so
marking the interface would have put RaboshOptions(...) behind an opt-in. It is stable, and it is
in the supported-seam list for the right reason rather than by omission.

The marker lives in rabosh-variant because it has to — everything marked is below rabosh-api in
the chain, and a marker declared there could not be applied in rabosh-index without an upward edge
this project does not have.

The ABI dumps are not the gate

Worth knowing rather than assuming. The JVM dump format writes signature lines and never
annotations
, so a declaration changing tier is invisible to checkKotlinAbi. Confirmed rather than
argued: updateKotlinAbi changed the committed dumps by exactly one entry, the annotation class
itself.

What catches it is rabosh-samples not opting in:rabosh-api and nothing else,
allWarningsAsErrors, part of build, and the one module the opt-in is deliberately withheld from.
It is a real consumer compiling against the stable core. Please don't tidy that asymmetry away by
giving every module the same compiler options.

Write the runtime contract down — INTEGRATION.md

Four rules an embedding application must obey were discoverable only by reading KDoc on classes a
caller may never open, and three of them fail silently: a row is valid only until the next
next(); a leaked Snapshot pins disk indefinitely; a second writing thread gets contention rather
than an error; a directory copied under a live writer is not defined to be recoverable.

One page, and every claim in it names the type, option or test that enforces it.

Declare a module name in every published jar

Without Automatic-Module-Name the seven jars resolve on the module path under names derived from
their filenames — unstable by construction, and where a jlink/jpackage build stops. Derived from
the project name rather than listed, for the reason PublishedModules gives.

Held by :rabosh-samples:runThreeStepsOnModulePath, which asks the JVM for app.oreshkov.rabosh.api
by name: delete the attribute and it fails at boot-layer initialisation rather than quietly
resolving something else.

Retract the native-access requirement, which was never real

FileChannel.map(mode, offset, size, Arena) is not a restricted method — no @Restricted, no
IllegalCallerException in JDK 25 — and neither are Arena.ofShared, Arena.allocate or
MemorySegment.ofArray. The restricted set is MemorySegment::reinterpret, the Linker and
SymbolLookup entry points and the load/loadLibrary family, and nothing here calls one. No
module needs --enable-native-access.
The comment in rabosh-samples/build.gradle.kts asserting
otherwise is corrected in place.

Settled by running the engine under --illegal-native-access=deny with no grant and watching it
pass, and then — because a check nobody has watched fail proves nothing — confirming the same flag
does kill a two-line program calling MemorySegment.reinterpret. The module-path sample is where
the claim now lives, and the module path is the only place it can: ALL-UNNAMED, which the two
classpath samples pass, would cover a restricted call and hide the answer.

The existing flags are kept as harmless future-proofing; only the reasoning is withdrawn.

Verification

build, -p build-logic check, publishToMavenLocal, dokkaGenerate (zero warnings), both
classpath samples and the new module-path sample all green. ABI drift: three lines, the annotation
class.

Four falsifications run and observed, each reverted:

Break Result
Delete the Automatic-Module-Name line FindException: Module app.oreshkov.rabosh.api not found
--illegal-native-access=deny vs a genuinely restricted call IllegalCallerException, and it passes once granted — so the guard is live
Add db.store.flush() to a sample build fails with the opt-in error naming RaboshExperimental
Comment out the opt-in in rabosh.kotlin-library published modules fail to compile

One thing reviewers should push back on if they disagree

Module-wide opt-in means the compiler cannot report an unmarked public declaration whose
signature names an experimental type — it is opted in, so it compiles. That gap is real and it found
four leaks (IndexCatalog.read, IndexCatalog.readColumn, SchemaCatalog.sketchOf, IndexQuery),
all now marked, via an audit over the committed ABI dumps that was itself verified by being made to
fire.

That audit is not wired into build. Its natural home is build-logic, beside
PublishedModules and CentralBundleReport — plain Kotlin over File, with unit tests. Left out
here as a new gate rather than part of this change, and flagged so the choice is visible rather than
silent.

🤖 Generated with Claude Code

…dy needed

Three changes every embedding of this engine needs, and none of them touches
the engine.

Say which declarations are allowed to move. "Major version zero: any signature
may change in any release" was honest and unactionable — a consumer could not
tell whether `Key.of` was as volatile as `IndexCatalog.readColumn`, so the only
rational responses were to wrap all of the API or none of it. There are now two
tiers, in STABILITY.md: a small stable core that moves only under a deprecation
cycle, and everything else, marked `@RaboshExperimental`. It is deliberately a
substitute for 1.0 rather than a step towards one.

What is marked is the way *in*, not every member. Holding a `ColumnReader` means
passing `Rabosh.indexCatalog` or `IndexCatalog.readColumn`, so both of those are
marked and the reader's own methods carry nothing. Marking every member instead
forces every stable signature naming an experimental type to be marked too, and
that cascade ends with the stable core inside the experimental tier.
`SegmentObserver` is that cascade caught at one step — `RaboshOptions`'
constructor names it — so it is stable, deliberately.

The marker lives in `rabosh-variant` because it has to: everything marked is
below `rabosh-api` in the chain, and a marker declared there could not be applied
in `rabosh-index` without the upward edge this project does not have.

The ABI dumps are not the gate, and that is worth knowing rather than assuming.
The JVM dump format writes signature lines and never annotations, so a
declaration changing tier is invisible to `checkKotlinAbi` — confirmed by the
markers changing the committed dumps by exactly one entry, the annotation class
itself. What catches it is `rabosh-samples`: `:rabosh-api` and nothing else,
`allWarningsAsErrors`, part of `build`, and the one module the opt-in is
withheld from. It is a real consumer compiling against the stable core.

Write the runtime contract down. Four rules an embedding application must obey
were discoverable only by reading KDoc on classes a caller may never open, and
three of them fail silently: a row is valid only until the next `next()`, a
leaked `Snapshot` pins disk indefinitely, a second writing thread gets
contention rather than an error, and a directory copied under a live writer is
not defined to be recoverable. INTEGRATION.md is one page and every claim in it
names the type, option or test that enforces it.

Declare a module name in every published jar. Without `Automatic-Module-Name`
the seven jars resolve on the module path under names derived from their
filenames, which is unstable by construction and is where a jlink/jpackage build
stops. Derived from the project name rather than listed, for the reason
`PublishedModules` gives. Held by `:rabosh-samples:runThreeStepsOnModulePath`,
which asks the JVM for `app.oreshkov.rabosh.api` by name — delete the attribute
and it fails at boot-layer initialisation rather than resolving something else.

And retract the native-access requirement, which was never real.
`FileChannel.map(mode, offset, size, Arena)` is not a restricted method: it
carries no `@Restricted` and declares no `IllegalCallerException` in JDK 25, and
neither do `Arena.ofShared`, `Arena.allocate` or `MemorySegment.ofArray`. The
restricted set is `MemorySegment::reinterpret`, the `Linker` and `SymbolLookup`
entry points and the `load`/`loadLibrary` family, and nothing here calls one. So
no module needs `--enable-native-access`, and the comment in `rabosh-samples`
asserting otherwise is corrected in place.

Settled by running the engine under `--illegal-native-access=deny` with no grant
and watching it pass, and then — because a check nobody has watched fail proves
nothing — confirming the same flag does kill a two-line program that calls
`MemorySegment.reinterpret`. The module-path sample is where the claim now
lives, and the module path is the only place it can: `ALL-UNNAMED`, which the
two classpath samples pass, would cover a restricted call and hide the answer.
The existing flags are kept as harmless future-proofing; only the reasoning is
withdrawn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@aoreshkov
aoreshkov merged commit 8da8c70 into main Aug 10, 2026
2 checks passed
@aoreshkov
aoreshkov deleted the say-which-parts-of-the-api-may-move branch August 10, 2026 18:17
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