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
30 changes: 30 additions & 0 deletions .github/workflows/api-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Public API check

# Guards the committed public-API dumps (sharingan/api, sharingan-noop/api).
# apiCheck fails the build when the public surface drifts from the dumps, so an
# accidental signature change cannot slip in between releases — the "swap
# sharingan-noop in release" safety story depends on the API staying stable.
# Regenerate intentional changes locally with `./gradlew apiDump` and commit.
on:
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
api-check:
# macOS so the Kotlin/Native (iOS) klib ABI targets build, matching the
# committed *.klib.api dumps; the Android/JVM dumps are checked here too.
runs-on: macos-latest
steps:
- uses: actions/checkout@v6

- name: Set up JDK 17
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "17"

- name: Check public API against committed dumps
run: ./gradlew apiCheck
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,13 @@ presentSharingan(animated: Boolean = true) // presents over topmost VC, any thr
- Min targets: Android API 24, iOS arm64 + simulator arm64. Requires Ktor 3.x for the plugin.
- KMP/iOS: dependency must be `api(...)` in `iosMain` (not just `commonMain`) and the framework block must `export(...)` it — without both, Kotlin/Native emits an empty header.
- XCFramework build tasks: `./gradlew :sharingan:assembleSharinganReleaseXCFramework` (debug tool) and `./gradlew :sharingan-noop:assembleSharinganReleaseXCFramework` (inert twin); outputs at `<module>/build/XCFrameworks/release/Sharingan.xcframework`.

## Public API stability (BCV)

The public API of both `:sharingan` and `:sharingan-noop` is guarded by Kotlin's
binary-compatibility-validator. The golden dumps are committed at
`<module>/api/<module>.api` (Android/JVM) and `<module>/api/<module>.klib.api`
(iOS ABI) and cover the full surface, including the iOS-only entry points.

- `./gradlew apiCheck` — fails if the public API drifts from the committed dumps. Runs on every PR (`.github/workflows/api-check.yml`, macOS so the iOS klib targets build).
- `./gradlew apiDump` — regenerate the dumps after an *intentional* API change, then commit the updated `api/*.api` files in the same PR.
19 changes: 19 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,23 @@ plugins {
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.composeMultiplatform) apply false
alias(libs.plugins.composeCompiler) apply false
// Applied to the root project: BCV injects apiDump/apiCheck into every
// subproject and guards the committed public-API dumps (issue #11).
alias(libs.plugins.binaryCompatibilityValidator)
}

// Public-API stability gate. The "swap sharingan-noop in release" safety story
// depends on the public API staying stable across versions; apiCheck (run in CI)
// fails the build when the surface drifts from the committed api/*.api dumps.
apiValidation {
// The sample app is not a published library — nothing to protect.
ignoredProjects += "composeApp"

// KMP: also dump/verify the Kotlin/Native (iOS) ABI, not just the JVM/Android
// surface, so iosMain-only declarations (SharinganViewController,
// presentSharingan) are covered too.
@OptIn(kotlinx.validation.ExperimentalBCVApi::class)
klib {
enabled = true
}
}
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ kotlin = "2.4.0"
agp = "8.13.2"
composeMultiplatform = "1.11.1"
vanniktechMavenPublish = "0.36.0"
binaryCompatibilityValidator = "0.18.1"
ktor = "3.5.0"
kotlinx-coroutines = "1.11.0"

Expand Down Expand Up @@ -37,3 +38,4 @@ androidLibrary = { id = "com.android.library", version.ref = "agp" }
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktechMavenPublish" }
binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidator" }
246 changes: 246 additions & 0 deletions sharingan-noop/api/sharingan-noop.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
public final class dev/sharingan/BleEvent : dev/sharingan/SharinganEvent {
public fun <init> (Ljava/lang/String;JLdev/sharingan/BleOperation;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/lang/String;JLdev/sharingan/BleOperation;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()J
public final fun component3 ()Ldev/sharingan/BleOperation;
public final fun component4 ()Ljava/lang/String;
public final fun component5 ()Ljava/lang/String;
public final fun component6 ()Ljava/lang/String;
public final fun component7 ()Ljava/lang/String;
public final fun component8 ()Ljava/lang/Long;
public final fun component9 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;JLdev/sharingan/BleOperation;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;)Ldev/sharingan/BleEvent;
public static synthetic fun copy$default (Ldev/sharingan/BleEvent;Ljava/lang/String;JLdev/sharingan/BleOperation;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;ILjava/lang/Object;)Ldev/sharingan/BleEvent;
public fun equals (Ljava/lang/Object;)Z
public final fun getCharacteristic ()Ljava/lang/String;
public final fun getDevice ()Ljava/lang/String;
public fun getError ()Ljava/lang/String;
public fun getId ()Ljava/lang/String;
public final fun getOperation ()Ldev/sharingan/BleOperation;
public final fun getPayload ()Ljava/lang/String;
public final fun getSizeBytes ()Ljava/lang/Long;
public fun getTimestampMillis ()J
public final fun getUuid ()Ljava/lang/String;
public fun hashCode ()I
public fun isFailure ()Z
public fun toString ()Ljava/lang/String;
}

public final class dev/sharingan/BleLogger {
public fun <init> (Ldev/sharingan/SharinganStore;)V
public final fun connect (Ljava/lang/String;Ljava/lang/String;)V
public static synthetic fun connect$default (Ldev/sharingan/BleLogger;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V
public final fun disconnect (Ljava/lang/String;Ljava/lang/String;)V
public static synthetic fun disconnect$default (Ldev/sharingan/BleLogger;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V
public final fun discover (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public static synthetic fun discover$default (Ldev/sharingan/BleLogger;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V
public final fun error (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public static synthetic fun error$default (Ldev/sharingan/BleLogger;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V
public final fun notify (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public static synthetic fun notify$default (Ldev/sharingan/BleLogger;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V
public final fun read (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public static synthetic fun read$default (Ldev/sharingan/BleLogger;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V
public final fun write (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public static synthetic fun write$default (Ldev/sharingan/BleLogger;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V
}

public final class dev/sharingan/BleOperation : java/lang/Enum {
public static final field CONNECT Ldev/sharingan/BleOperation;
public static final field DISCONNECT Ldev/sharingan/BleOperation;
public static final field DISCOVER Ldev/sharingan/BleOperation;
public static final field ERROR Ldev/sharingan/BleOperation;
public static final field NOTIFY Ldev/sharingan/BleOperation;
public static final field READ Ldev/sharingan/BleOperation;
public static final field WRITE Ldev/sharingan/BleOperation;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Ldev/sharingan/BleOperation;
public static fun values ()[Ldev/sharingan/BleOperation;
}

public final class dev/sharingan/HttpEvent : dev/sharingan/SharinganEvent {
public fun <init> (Ljava/lang/String;JLjava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Long;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/util/List;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/lang/String;JLjava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Long;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/util/List;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component10 ()Ljava/lang/String;
public final fun component11 ()Ljava/lang/String;
public final fun component12 ()Ljava/lang/Long;
public final fun component13 ()Ljava/util/List;
public final fun component14 ()Ljava/lang/String;
public final fun component2 ()J
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Ljava/lang/String;
public final fun component5 ()Ljava/lang/Integer;
public final fun component6 ()Ljava/lang/Long;
public final fun component7 ()Ljava/util/List;
public final fun component8 ()Ljava/util/List;
public final fun component9 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;JLjava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Long;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/util/List;Ljava/lang/String;)Ldev/sharingan/HttpEvent;
public static synthetic fun copy$default (Ldev/sharingan/HttpEvent;Ljava/lang/String;JLjava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Long;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Ldev/sharingan/HttpEvent;
public fun equals (Ljava/lang/Object;)Z
public final fun getContentType ()Ljava/lang/String;
public final fun getDurationMillis ()Ljava/lang/Long;
public fun getError ()Ljava/lang/String;
public final fun getHost ()Ljava/lang/String;
public fun getId ()Ljava/lang/String;
public final fun getMethod ()Ljava/lang/String;
public final fun getPath ()Ljava/lang/String;
public final fun getRequestBody ()Ljava/lang/String;
public final fun getRequestHeaders ()Ljava/util/List;
public final fun getResponseBody ()Ljava/lang/String;
public final fun getResponseHeaders ()Ljava/util/List;
public final fun getResponseSizeBytes ()Ljava/lang/Long;
public final fun getStatusCode ()Ljava/lang/Integer;
public fun getTimestampMillis ()J
public final fun getTiming ()Ljava/util/List;
public final fun getUrl ()Ljava/lang/String;
public fun hashCode ()I
public fun isFailure ()Z
public fun toString ()Ljava/lang/String;
}

public final class dev/sharingan/HttpLogger {
public static final field Companion Ldev/sharingan/HttpLogger$Companion;
public static final field REDACTED_VALUE Ljava/lang/String;
public fun <init> (Ldev/sharingan/SharinganStore;Ljava/util/Set;)V
public synthetic fun <init> (Ldev/sharingan/SharinganStore;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun log (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Long;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/util/List;Ljava/lang/String;)V
public static synthetic fun log$default (Ldev/sharingan/HttpLogger;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Long;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)V
}

public final class dev/sharingan/HttpLogger$Companion {
public final fun getDEFAULT_REDACTED_HEADERS ()Ljava/util/Set;
}

public final class dev/sharingan/MqttDirection : java/lang/Enum {
public static final field PUBLISH Ldev/sharingan/MqttDirection;
public static final field RECEIVE Ldev/sharingan/MqttDirection;
public static final field SUBSCRIBE Ldev/sharingan/MqttDirection;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Ldev/sharingan/MqttDirection;
public static fun values ()[Ldev/sharingan/MqttDirection;
}

public final class dev/sharingan/MqttEvent : dev/sharingan/SharinganEvent {
public fun <init> (Ljava/lang/String;JLdev/sharingan/MqttDirection;Ljava/lang/String;IZLjava/lang/String;Ljava/lang/Long;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/lang/String;JLdev/sharingan/MqttDirection;Ljava/lang/String;IZLjava/lang/String;Ljava/lang/Long;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()J
public final fun component3 ()Ldev/sharingan/MqttDirection;
public final fun component4 ()Ljava/lang/String;
public final fun component5 ()I
public final fun component6 ()Z
public final fun component7 ()Ljava/lang/String;
public final fun component8 ()Ljava/lang/Long;
public final fun component9 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;JLdev/sharingan/MqttDirection;Ljava/lang/String;IZLjava/lang/String;Ljava/lang/Long;Ljava/lang/String;)Ldev/sharingan/MqttEvent;
public static synthetic fun copy$default (Ldev/sharingan/MqttEvent;Ljava/lang/String;JLdev/sharingan/MqttDirection;Ljava/lang/String;IZLjava/lang/String;Ljava/lang/Long;Ljava/lang/String;ILjava/lang/Object;)Ldev/sharingan/MqttEvent;
public fun equals (Ljava/lang/Object;)Z
public final fun getDirection ()Ldev/sharingan/MqttDirection;
public fun getError ()Ljava/lang/String;
public fun getId ()Ljava/lang/String;
public final fun getPayload ()Ljava/lang/String;
public final fun getPayloadSizeBytes ()Ljava/lang/Long;
public final fun getQos ()I
public final fun getRetained ()Z
public fun getTimestampMillis ()J
public final fun getTopic ()Ljava/lang/String;
public fun hashCode ()I
public fun isFailure ()Z
public fun toString ()Ljava/lang/String;
}

public final class dev/sharingan/MqttLogger {
public fun <init> (Ldev/sharingan/SharinganStore;)V
public final fun publish (Ljava/lang/String;Ljava/lang/String;IZLjava/lang/String;)V
public static synthetic fun publish$default (Ldev/sharingan/MqttLogger;Ljava/lang/String;Ljava/lang/String;IZLjava/lang/String;ILjava/lang/Object;)V
public final fun received (Ljava/lang/String;Ljava/lang/String;IZ)V
public static synthetic fun received$default (Ldev/sharingan/MqttLogger;Ljava/lang/String;Ljava/lang/String;IZILjava/lang/Object;)V
public final fun subscribed (Ljava/lang/String;I)V
public static synthetic fun subscribed$default (Ldev/sharingan/MqttLogger;Ljava/lang/String;IILjava/lang/Object;)V
}

public final class dev/sharingan/Sharingan {
public static final field INSTANCE Ldev/sharingan/Sharingan;
public final fun clear ()V
public final fun getBle ()Ldev/sharingan/BleLogger;
public final fun getEvents ()Lkotlinx/coroutines/flow/StateFlow;
public final fun getHttp ()Ldev/sharingan/HttpLogger;
public final fun getMqtt ()Ldev/sharingan/MqttLogger;
public final fun getStore ()Ldev/sharingan/SharinganStore;
public final fun isRecording ()Lkotlinx/coroutines/flow/StateFlow;
public final fun setRecording (Z)V
}

public final class dev/sharingan/SharinganAndroidKt {
public static final fun setNotificationEnabled (Ldev/sharingan/Sharingan;Z)V
public static final fun show (Ldev/sharingan/Sharingan;Landroid/content/Context;)V
}

public abstract interface class dev/sharingan/SharinganEvent {
public abstract fun getError ()Ljava/lang/String;
public abstract fun getId ()Ljava/lang/String;
public abstract fun getTimestampMillis ()J
public fun isFailure ()Z
}

public final class dev/sharingan/SharinganEvent$DefaultImpls {
public static fun isFailure (Ldev/sharingan/SharinganEvent;)Z
}

public final class dev/sharingan/SharinganExport {
public static final field INSTANCE Ldev/sharingan/SharinganExport;
public final fun agentMarkdown (Ldev/sharingan/SharinganEvent;)Ljava/lang/String;
public final fun agentMarkdown (Ljava/util/List;)Ljava/lang/String;
public final fun curl (Ldev/sharingan/HttpEvent;)Ljava/lang/String;
public final fun json (Ldev/sharingan/SharinganEvent;)Ljava/lang/String;
public final fun sessionJson (Ljava/util/List;)Ljava/lang/String;
public final fun summary (Ljava/util/List;)Ljava/lang/String;
}

public final class dev/sharingan/SharinganStore {
public static final field Companion Ldev/sharingan/SharinganStore$Companion;
public static final field DEFAULT_CAPACITY I
public fun <init> ()V
public fun <init> (I)V
public synthetic fun <init> (IILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun clear ()V
public final fun getCapacity ()I
public final fun getEvents ()Lkotlinx/coroutines/flow/StateFlow;
public final fun isRecording ()Lkotlinx/coroutines/flow/StateFlow;
public final fun record (Ldev/sharingan/SharinganEvent;)V
public final fun setRecording (Z)V
}

public final class dev/sharingan/SharinganStore$Companion {
}

public final class dev/sharingan/TimingPhase {
public fun <init> (Ljava/lang/String;J)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()J
public final fun copy (Ljava/lang/String;J)Ldev/sharingan/TimingPhase;
public static synthetic fun copy$default (Ldev/sharingan/TimingPhase;Ljava/lang/String;JILjava/lang/Object;)Ldev/sharingan/TimingPhase;
public fun equals (Ljava/lang/Object;)Z
public final fun getLabel ()Ljava/lang/String;
public final fun getMillis ()J
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class dev/sharingan/ktor/SharinganKtorConfig {
public fun <init> ()V
public final fun getCaptureBodies ()Z
public final fun getMaxBodyBytes ()I
public final fun getRedactedHeaders ()Ljava/util/Set;
public final fun getStore ()Ldev/sharingan/SharinganStore;
public final fun setCaptureBodies (Z)V
public final fun setMaxBodyBytes (I)V
public final fun setRedactedHeaders (Ljava/util/Set;)V
public final fun setStore (Ldev/sharingan/SharinganStore;)V
}

public final class dev/sharingan/ktor/SharinganKtorKt {
public static final fun getSharinganKtor ()Lio/ktor/client/plugins/api/ClientPlugin;
}

Loading