Skip to content

Enforce :sharingan/:sharingan-noop public API parity in CI (#12)#29

Merged
mibrahimdev merged 1 commit into
mainfrom
feat/12-api-parity
Jun 14, 2026
Merged

Enforce :sharingan/:sharingan-noop public API parity in CI (#12)#29
mibrahimdev merged 1 commit into
mainfrom
feat/12-api-parity

Conversation

@mibrahimdev

Copy link
Copy Markdown
Owner

Closes #12.

What & why

The release-swap safety story — consumer compiles against :sharingan in debug, swaps :sharingan-noop in release — depends on the two independently-compiled modules exposing a signature-for-signature identical public contract. Until now nothing enforced that; a drifted default or a forgotten method would surface only as a consumer's release-build compile error, the worst place to find it.

This adds a checkApiParity Gradle task that compares the four committed BCV dumps (JVM .api + native .klib.api, both modules), applies an exclusion filter, and fails the build on any bidirectional divergence. It is pure text processing over already-committed files — no compilation, no iOS link, no Android/iOS SDK — so it runs in seconds.

Approach (not a whole-file diff)

:sharingan legitimately carries symbols the noop must not expose. These are filtered out before comparing (full rationale in docs/api-parity.md):

  1. Debug-only UI/platform classesdev/sharingan/ui/**, dev/sharingan/internal/**, SharinganActivity, any ComposableSingletons$*, and the native top-level dev.sharingan.ui/SharinganScreen composable.
  2. Compose $stable / $stableprop synthetics — the compiler stability metadata that ships only in the Compose-carrying real module. This is the subtle one: it lives inside otherwise-shared classes (e.g. HttpEvent), so a naive diff false-fails on every event type.
  3. The klib // Library unique name: header, which differs by module name by construction.

After filtering, both modules' 19 JVM contract classes (+ native SharinganViewController/presentSharingan) must match exactly. Comparison is bidirectional: it fails if the noop is missing a real symbol and if it over-exposes one.

Acceptance criteria

  • Automated check asserts an identical public surface (the filtered contract). checkApiParity in the root build.gradle.kts; compares both the JVM .api pair and the native .klib.api pair as sets of class-qualified signatures.
  • Runs in CI, fails the build on divergence. New api-parity job in .github/workflows/api-check.yml (ubuntu — needs no macOS host). Also hooked into the root check lifecycle and runnable locally via ./gradlew checkApiParity.
  • Intentional divergence makes the check FAIL (demonstrated). See below — shown on both surfaces, then reverted.
  • Approach + exclusions documented. docs/api-parity.md (contract, why parity matters, every exclusion, and the fix-it workflow) + an extensively-commented task + a README contributing pointer.

Verification (light checks only — no build/assemble/iOS-native run)

Green (parity holds):

> Task :checkApiParity
API parity OK: :sharingan and :sharingan-noop expose an identical public contract (JVM + native).
BUILD SUCCESSFUL

Red — drop clear ()V from the noop JVM dump → FAILS:

> API PARITY MISMATCH on the Android/JVM (.api) surface
    In :sharingan but MISSING from :sharingan-noop
    (consumer code compiled against debug would FAIL to compile after a release swap):
      - dev/sharingan/Sharingan :: public final fun clear ()V
BUILD FAILED

Red — drop presentSharingan from the noop native dump → FAILS:

> API PARITY MISMATCH on the native/iOS (.klib.api) surface
    In :sharingan but MISSING from :sharingan-noop ...
      - final fun dev.sharingan/presentSharingan(kotlin/Boolean = ...) // ...

Both divergences were reverted; the check returns to green.

./gradlew apiCheckBUILD SUCCESSFUL, committed dumps are current.

Implementation note

The comparison helpers are local functions inside the task action rather than script-level functions: Gradle Kotlin DSL silently drops top-level statements placed after top-level fun declarations (and statements can't forward-reference functions declared later), so a top-level helper layout left the task unregistered. Keeping the logic inside doLast sidesteps both.

Unrelated pre-existing site/assets/*.css working-tree edits were intentionally left out of this PR.

🤖 Generated with Claude Code

The release-swap safety story (consumer compiles against :sharingan in debug,
swaps :sharingan-noop in release) depends on the two independently-compiled
modules exposing a signature-for-signature identical public contract. Until now
nothing enforced that — a drifted default or a forgotten method would surface
only as a consumer's release-build compile error.

Add a `checkApiParity` Gradle task (root build) that compares the four committed
BCV dumps (JVM .api + native .klib.api, both modules) after filtering out the
symbols that legitimately exist in only the real module, and fails on any
bidirectional divergence:

  - debug-only UI/platform classes the consumer never references directly
    (dev/sharingan/ui/**, internal/**, SharinganActivity, ComposableSingletons$*,
    and the native ui SharinganScreen composable);
  - the Compose-compiler $stable field / $stableprop synthetics that only the
    Compose-carrying real module emits — the gotcha that would false-fail a
    naive diff since they live inside otherwise-shared classes;
  - the klib `// Library unique name` header line, which differs by construction.

After filtering, both modules' 19 JVM contract classes (+ native
SharinganViewController/presentSharingan) must match exactly. The check is pure
text processing over committed files — no compilation, no iOS link — so it runs
in seconds with no Android/iOS SDK.

Wiring:
  - registered in the `verification` group; runnable as `./gradlew checkApiParity`
  - hooked into the root `check` lifecycle
  - new `api-parity` CI job in api-check.yml on ubuntu (no macOS host needed)

Verified: red→green demonstrated on both surfaces (dropping a contract symbol
from a noop dump fails the check with a precise per-symbol diff; reverting
passes). `./gradlew apiCheck` confirms the committed dumps are current.

Docs: docs/api-parity.md explains the contract, the why, and every exclusion so
contributors keep the modules in lockstep; README contributing section points to
it.

Implementation note: the comparison helpers are local functions inside the task
action because Gradle Kotlin DSL silently drops top-level statements placed after
top-level `fun` declarations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mibrahimdev mibrahimdev merged commit 3473dda into main Jun 14, 2026
4 checks passed
@mibrahimdev mibrahimdev deleted the feat/12-api-parity branch June 14, 2026 14:58
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.

Cross-module API-parity check: assert :sharingan and :sharingan-noop expose identical public surface

1 participant