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
34 changes: 27 additions & 7 deletions .claude/rules/build-and-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,33 @@ experimental type to be marked too, and that cascade ends with the stable core i
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.
**Two gates, and neither is `checkKotlinAbi`.** 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 it.

The first gate 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.
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.

The second is **`checkApiTiers`**, and it exists because the first one only sees what a sample
happens to call. Module-wide opt-in blinds the compiler to a public signature that *names* an
experimental type without carrying the marker — a consumer meeting it gets handed an experimental
type with nothing having asked them to opt in. `ApiTierAudit` reads the marker set from the
**sources** and the surface from the **committed dumps**, both derived and neither listed, for the
`PublishedModules` reason: a hand-maintained list of experimental types would disagree with the
annotations exactly once, silently, in the direction of not reporting a leak. It is a root task,
because the leak is cross-module — a type marked in `rabosh-index` leaks through a signature in
`rabosh-query`'s dump — and it hangs off the root `check`, so `./gradlew build` runs it.

Three things about it that are decisions. **An annotation the scanner cannot attribute is a failure,
not a shorter set**: under-reporting is the only failure mode that matters, because an audit that
misses a leak passes and passing is what it is read for. **Nesting is followed by indentation rather
than by counting braces**, because Kotlin string templates put braces inside string literals and a
counter needs a lexer to be right; the comparison is *strictly* less than the declaration's column, so
a sibling `private class` declared beside a marked function is not mistaken for its enclosing scope —
which the first version did, reporting `IndexCatalog.read` as a leak. And **a missing dump is skipped
rather than failed**, because `checkKotlinAbi` already owns that and owning it twice is worse.

## Native access: the flag nobody needs

Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,23 @@ else may change in any release. That claim lives in [STABILITY.md](STABILITY.md)

### Added

- **`checkApiTiers` — the stability tiers are now held by a gate rather than by a script.** The
module-wide opt-in that keeps the engine from needing several hundred `@OptIn`s also blinds the
compiler to a public signature that *names* an experimental type without carrying the marker, and
an ABI dump writes signatures and never annotations — so between them nothing was checking that
`STABILITY.md`'s claim was still true. Run by hand it had already found four such leaks.

`ApiTierAudit` reads the marker set from the **sources** and the surface from the **committed
dumps**, both derived and neither listed: a hand-maintained list of experimental types would
disagree with the annotations exactly once, silently, in the direction of not reporting a leak. It
is a root task, because a type marked in one module leaks through another module's dump, and it
hangs off `check`, so `./gradlew build` runs it.

Promoting it found a bug the hand-written version never had to have: attributing a marked member to
the *most recent* type declaration rather than the enclosing one put `IndexCatalog.read` inside a
`private class` two hundred lines above it. Nesting now follows indentation, and the sibling case is
a test.

- **`Rabosh.checkpoint(target)` — a consistent copy, taken while you are writing.** The recipe it
replaces was *stop writing and copy the directory*, which a desktop application cannot do because it
is the writer. The database is flushed, a snapshot is pinned, and the copy is of what that snapshot
Expand Down
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ them at the latest stable release; do not adopt pre-releases (e.g. Kotlin `-Beta
dumps live at `<module>/api/<module>.api`.
- **`build-logic/` is an included build, and its tests are not part of the root `build`** — CI runs
`./gradlew -p build-logic check` as its own step.
- **`checkApiTiers` is the gate `checkKotlinAbi` cannot be**: a dump carries signatures and never
annotations, and module-wide opt-in blinds the compiler, so a public signature exposing a
`@RaboshExperimental` type without carrying the marker is invisible to both. It is a root task and
runs under `build`.
- **`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 the API claim in `STABILITY.md`, each in one
Expand Down
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ and commit the result. The tasks are `checkKotlinAbi` and `updateKotlinAbi`; the
that Kotlin still registers are deprecated shims and using them is a build that breaks on the next
upgrade for no benefit today.

**A dump says nothing about stability tiers**, so a second check runs beside it. `checkApiTiers` —
also part of `build` — fails when a public signature exposes a `@RaboshExperimental` type without
carrying the marker itself. If it names your declaration, either mark it or move the type into the
stable core and say so in [STABILITY.md](STABILITY.md); the one thing not to do is leave a consumer
holding an experimental type that nothing asked them to opt in to. It reads the marker set from the
sources, so adding a marker is all that is needed to teach it.

**2. A new dependency needs agreement first, and the runtime scope is closed.** "No runtime
dependencies at all" is a claim the README makes, so it has to stay true — the JSON parser, the
compressed bitmap, the HyperLogLog, the bloom filter and the property-test harness are all in-repo
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,7 @@ Requires JDK 25.
./gradlew build # compile, test, and check the public ABI against the committed dumps
./gradlew test # tests only
./gradlew updateKotlinAbi # after an intentional public API change
./gradlew checkApiTiers # no unmarked signature exposes an experimental type (part of build)
./gradlew dokkaGenerate # the aggregated API site, into build/dokka/html
```

Expand Down
15 changes: 12 additions & 3 deletions STABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,25 @@ they do **not** carry is the markers: the JVM dump format writes signature lines
annotations, and the synthetic method Kotlin emits for an annotated property is filtered out as
synthetic. A declaration changing tier is invisible to it.

**`rabosh-samples` is what holds the tiers.** It depends on `:rabosh-api` and nothing else, it
**`rabosh-samples` is what holds the stable core.** It depends on `:rabosh-api` and nothing else, it
compiles with `allWarningsAsErrors`, it is part of `./gradlew build`, and — unlike every other module
in the repository — it deliberately does **not** opt in to `@RaboshExperimental`. It is therefore a
real consumer compiling against the stable core with no opt-in. A stable declaration that silently
acquires the marker fails there, and so does a sample that reaches past the facade. That asymmetry is
load-bearing and should not be tidied away by giving every module the same build configuration.

Verified by breaking it, which is this repository's standing rule for a check nobody has watched
**`checkApiTiers` holds the other direction**, which a sample cannot: that no unmarked public
signature *exposes* an experimental type. Module-wide opt-in means the compiler permits exactly that
inside the library, so a consumer could be handed a `ColumnReader` by a method carrying no marker at
all — the tier statement above quietly ceasing to be true. The audit reads the marker set from the
sources and the surface from the committed dumps, both derived rather than listed, and runs at the
root because the leak is cross-module. It found four leaks the first time it was run by hand.

Verified by breaking them, which is this repository's standing rule for a check nobody has watched
fail: adding `db.store.flush()` to a sample fails `./gradlew build` with the opt-in error naming the
marker, and commenting out the opt-in in `rabosh.kotlin-library` fails the published modules.
marker; commenting out the opt-in in `rabosh.kotlin-library` fails the published modules; and removing
the marker from `IndexCatalog.read` fails `checkApiTiers` naming the method, the type it exposes and
the module.

## Reporting

Expand Down
Loading