Say which parts of the Kotlin API may move, and retract the flag nobody needed - #13
Merged
Merged
Conversation
…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>
This was referenced Aug 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.ofwas as volatile asIndexCatalog.readColumn, so the onlyrational 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.mdsaysso in as many words.
What is marked is the way in, not every member. Holding a
ColumnReadermeans having passedRabosh.indexCatalogorIndexCatalog.readColumn, so both of those are marked and the reader's ownmethods 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.
SegmentObserveris that cascade caught at one step:RaboshOptions' constructor names it, somarking the interface would have put
RaboshOptions(...)behind an opt-in. It is stable, and it isin the supported-seam list for the right reason rather than by omission.
The marker lives in
rabosh-variantbecause it has to — everything marked is belowrabosh-apiinthe chain, and a marker declared there could not be applied in
rabosh-indexwithout an upward edgethis 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 thanargued:
updateKotlinAbichanged the committed dumps by exactly one entry, the annotation classitself.
What catches it is
rabosh-samplesnot opting in —:rabosh-apiand nothing else,allWarningsAsErrors, part ofbuild, 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.mdFour 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 leakedSnapshotpins disk indefinitely; a second writing thread gets contention ratherthan 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-Namethe seven jars resolve on the module path under names derived fromtheir filenames — unstable by construction, and where a
jlink/jpackagebuild stops. Derived fromthe project name rather than listed, for the reason
PublishedModulesgives.Held by
:rabosh-samples:runThreeStepsOnModulePath, which asks the JVM forapp.oreshkov.rabosh.apiby 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, noIllegalCallerExceptionin JDK 25 — and neither areArena.ofShared,Arena.allocateorMemorySegment.ofArray. The restricted set isMemorySegment::reinterpret, theLinkerandSymbolLookupentry points and theload/loadLibraryfamily, and nothing here calls one. Nomodule needs
--enable-native-access. The comment inrabosh-samples/build.gradle.ktsassertingotherwise is corrected in place.
Settled by running the engine under
--illegal-native-access=denywith no grant and watching itpass, 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 wherethe claim now lives, and the module path is the only place it can:
ALL-UNNAMED, which the twoclasspath 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), bothclasspath samples and the new module-path sample all green. ABI drift: three lines, the annotation
class.
Four falsifications run and observed, each reverted:
Automatic-Module-NamelineFindException: Module app.oreshkov.rabosh.api not found--illegal-native-access=denyvs a genuinely restricted callIllegalCallerException, and it passes once granted — so the guard is livedb.store.flush()to a samplebuildfails with the opt-in error namingRaboshExperimentalrabosh.kotlin-libraryOne 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 isbuild-logic, besidePublishedModulesandCentralBundleReport— plain Kotlin overFile, with unit tests. Left outhere as a new gate rather than part of this change, and flagged so the choice is visible rather than
silent.
🤖 Generated with Claude Code