Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 60 additions & 9 deletions .claude/rules/build-and-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,63 @@ claim: `release.yml` derives the release version from the git tag and nowhere el
development one by construction. Do not "fix" it to a release number — that would put the version in
two places and make the tag advisory.

## The format claim

**The format claim lives in `COMPATIBILITY.md` and nowhere else.** It used to live in the README's
status blockquote, coupled to the version in `gradle.properties`, and the coupling was what kept it:
each artefact cited another and none cited the format. The two guarantees are now separate and stated
at the strength of their own evidence — the **on-disk format is declared and stable**, held by the
golden stores; the **Kotlin API is major-version zero** and free to move, held by nothing, which is
exactly why it is not claimed. A change to either belongs in `COMPATIBILITY.md` first; the README
links it rather than restating it, so the two cannot drift.
## The format claim, the API claim and the runtime contract

**Each lives in exactly one file, and the README links all three rather than restating any.**
`COMPATIBILITY.md` holds the on-disk format, `STABILITY.md` the Kotlin API, `INTEGRATION.md` the
runtime contract. The format claim used to live in the README's status blockquote, coupled to the
version in `gradle.properties`, and the coupling was what kept it: each artefact cited another and
none cited the format.

The two guarantees are stated at the strength of their own evidence. The **on-disk format is declared
and stable**, held by the golden stores. The **Kotlin API is tiered** — a stable core that moves only
under a deprecation cycle, and an explicit `@RaboshExperimental` for the rest. It used to be
"major-version zero, held by nothing", which was honest and unactionable: a consumer could not tell
whether `Key.of` was as volatile as `IndexCatalog.readColumn`, so the rational response was to wrap
all of the API or none of it. Phase 23 replaced it with the smaller, truer claim, and **that is a
substitute for 1.0 rather than a step towards one** — say so wherever it is described.

Three things about the marker that a change must not quietly undo.

**It lives in `rabosh-variant`, package `app.oreshkov.rabosh`, and it has to.** Everything marked is
below `rabosh-api` in the chain, so a marker declared there could not be applied in `rabosh-index`
without the upward edge this project does not have. The package is the project's rather than the
codec's for the same reason.

**What is marked is the way *in*, not every member.** `Rabosh.store`/`catalog`/`indexCatalog`,
`DocumentStore.open`, the `SchemaCatalog`/`IndexCatalog`/`QueryEngine` constructors,
`IndexCatalog.read`/`readColumn`, `SchemaCatalog.sketchOf`, `InferredField.sketch`, and the bitmap,
column and sketch *types*. 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
marking the interface would have put `RaboshOptions(...)` behind an opt-in. It is stable, deliberately.

**The gate is `rabosh-samples` not opting in, and the ABI dumps are not the gate.** The JVM dump
format writes signature lines and never annotations — verified in the dumper, and confirmed by the
markers changing the committed dumps by exactly one entry, the annotation class itself — so a
declaration changing tier is invisible to `checkKotlinAbi`. What catches it is the samples module:
`:rabosh-api` and nothing else, `allWarningsAsErrors`, part of `build`, and the one module the
opt-in is deliberately withheld from. Do not "tidy" that asymmetry by giving every module the same
compiler options; `rabosh.kotlin-library`, `rabosh-testkit` and `rabosh-bench` opt in, samples do not.

## Native access: the flag nobody needs

**No module requires `--enable-native-access`, and the reason is not that the engine avoids the FFM
API.** It maps every segment through `FileChannel.map(mode, offset, size, Arena)` — which is simply
**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.

This was believed otherwise for several phases and written into a build comment as fact, so it is
worth stating how it was settled: not by reading the JEP, but by running the engine under
`--illegal-native-access=deny` with no grant and watching it pass, and then confirming that the same
flag *does* fail a two-line program that calls `MemorySegment.reinterpret`. A check that has not been
seen fail proves nothing, and that applies to a check on the JVM's behaviour as much as to one in the
suite.

`:rabosh-samples:runThreeStepsOnModulePath` is where the claim now lives, and the module path is the
only place it can: `ALL-UNNAMED`, which the other two samples pass, would cover a restricted call from
the classpath and hide the answer. The `--enable-native-access=ALL-UNNAMED` on `Test` tasks and on the
two classpath samples is retained as harmless future-proofing; the *reasoning* attached to it is not
load-bearing and should not be repeated as though it were.
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ jobs:
if: matrix.os == 'ubuntu-latest'
run: ./gradlew publishToMavenLocal --stacktrace

# The published jars carry `Automatic-Module-Name`, and the only thing that checks it is a run
# with the library on the module path — asked for by module name, so a missing attribute fails
# at startup rather than silently resolving the jar under a filename-derived name. It is also
# the one place the *module-name* spelling of `--enable-native-access` is exercised, which is
# what INTEGRATION.md tells a jlink/jpackage consumer to use.
- name: Run a sample on the module path
run: ./gradlew :rabosh-samples:runThreeStepsOnModulePath --stacktrace

# `publishToMavenLocal` builds each module's own Dokka HTML for the `javadoc` classifier, but
# not the aggregated site — that is the root's `rabosh.api-docs`, and only `docs.yml` on `main`
# would otherwise run it. A pull request that broke the aggregation would go green here and
Expand Down
51 changes: 46 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,55 @@ All notable changes to this project are recorded here. The format follows
[Semantic Versioning](https://semver.org/spec/v2.0.0.html) — with one qualification that matters
more here than the version number does.

**Two guarantees, at different strengths.** The Kotlin API is major-version zero: any signature may
change in any release, and `0.x` gives you no compatibility promise at all. The **on-disk format** is
declared and stable — a store written by an earlier release opens on every later one — and that
promise does not wait for `1.0`. Anything affecting it is stated in
[COMPATIBILITY.md](COMPATIBILITY.md) first and only summarised here.
**Two guarantees, at different strengths, and neither waits for `1.0`.** The **on-disk format** is
declared and stable — a store written by an earlier release opens on every later one — and anything
affecting it is stated in [COMPATIBILITY.md](COMPATIBILITY.md) first and only summarised here. The
**Kotlin API** is tiered: a small stable core moves only under a deprecation cycle, and everything
else may change in any release. That claim lives in [STABILITY.md](STABILITY.md), on the same terms.

## [Unreleased]

### Added

- **[`INTEGRATION.md`](INTEGRATION.md) — the runtime contract, in public.** The rules an embedding
application has to 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, and a second writing thread gets contention rather than an error. One page,
every claim naming the type, option or test that enforces it.

- **[`STABILITY.md`](STABILITY.md) and `@RaboshExperimental` — which parts of the Kotlin API are
allowed to move.** "Major version zero, any signature may change" 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 **not** a promise of 1.0.

What is marked is the way *in* rather than every member: `Rabosh.store`/`catalog`/`indexCatalog`,
`DocumentStore.open`, the `SchemaCatalog`/`IndexCatalog`/`QueryEngine` constructors,
`IndexCatalog.read`/`readColumn`, and the bitmap, column and sketch types themselves. Holding one
of those objects means you already opted in, so its own methods carry nothing.

`rabosh-samples` is what holds the claim, and it holds it by *not* opting in: it depends on
`:rabosh-api` alone, compiles with `allWarningsAsErrors`, and is part of `build`, so it is a real
consumer compiling against the stable core. The ABI dumps cannot do this job — the JVM dump format
writes signatures and never annotations, so a tier change is invisible to `checkKotlinAbi`.

- **`Automatic-Module-Name` in every published jar**, derived from the module name:
`app.oreshkov.rabosh.{variant,core,catalog,index,query,api,jsonpath}`. On the module path the jars
previously resolved under names derived from their filenames, which is unstable by construction and
is where a `jlink`/`jpackage` build stopped. Held by a new
`:rabosh-samples:runThreeStepsOnModulePath`, which asks the JVM for `app.oreshkov.rabosh.api` **by
name** — so a missing attribute fails at startup rather than silently resolving something else.

### Changed

- **No `--enable-native-access` flag is required, by any module**, and `INTEGRATION.md` now says so.
The engine maps segments through `FileChannel.map(mode, offset, size, Arena)`, which is *not* a
restricted method — it carries no `@Restricted` and declares no `IllegalCallerException` — and
nothing here calls one that is. The new module-path task runs the full cycle under
`--illegal-native-access=deny` with no grant of any kind, so the claim is checked rather than
asserted, and a future release that acquires a restricted call fails it.

### Compatibility

- **The composite index's on-disk shape is now pinned by committed bytes**, in a fifth golden store
Expand Down
12 changes: 9 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,15 @@ them at the latest stable release; do not adopt pre-releases (e.g. Kotlin `-Beta
`./gradlew -p build-logic check` as its own step.
- **`gradle.properties` stays `0.1.0-SNAPSHOT`**: `release.yml` derives the release version from the
git tag and nowhere else. Do not "fix" it to a release number.
- **The format claim lives in `COMPATIBILITY.md` and nowhere else**, and the README links it rather
than restating it. The on-disk format is declared and stable; the Kotlin API is major-version zero
and is deliberately not claimed.
- **The format claim lives in `COMPATIBILITY.md` and the API claim in `STABILITY.md`, each in one
place**, and the README links both rather than restating either. The on-disk format is declared and
stable; the Kotlin API is tiered, with a stable core and an explicit `@RaboshExperimental`.
- **The runtime contract lives in `INTEGRATION.md`** — JDK floor, the native-access question, one
writer, the `AutoCloseable`s, copy-before-`next()`. A sentence about bytes on disk belongs in
`COMPATIBILITY.md` and is linked, never moved.
- **No module needs `--enable-native-access`, and that is checked**:
`:rabosh-samples:runThreeStepsOnModulePath` runs under `--illegal-native-access=deny` with no grant.
`FileChannel::map` is not a restricted method; do not add the flag back on the assumption that it is.

## Module layout

Expand Down
8 changes: 7 additions & 1 deletion COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@ rather than in code; this is that declaration, and it is deliberately separate f
| | Guarantee |
|---|---|
| **On-disk format** | Stable. Governed by this document. |
| **Kotlin API** | Not stable. Major version zero: any signature may change in any release. |
| **Kotlin API** | Tiered. A small stable core moves only under a deprecation cycle; everything else may change in any release. Governed by [STABILITY.md](STABILITY.md). |

A store is data somebody owns; an API is a call somebody can rewrite. Freezing the first and not the
second says exactly what the evidence supports, and no more.

The API row used to read *not stable, major version zero, any signature may change in any release*.
That was honest and unactionable — it gave a consumer no way to tell whether `Key.of` was as volatile
as `IndexCatalog.readColumn` — so it has been replaced by the smaller, truer claim in its own
document. It is **not** a promise of 1.0 and is not a step towards one; the two files stay separate
for the same reason they always were, and each links the other rather than restating it.

## What is covered

Ten independently versioned encodings. Eight carry an eight-byte magic, legible in a hex dump; two
Expand Down
Loading