From ca779f72893830f929719b7282c50de306ce62c8 Mon Sep 17 00:00:00 2001 From: Paul Ambrose Date: Sun, 27 Sep 2026 14:31:59 -0700 Subject: [PATCH 01/16] Move jev4k to Kotlin Multiplatform jev4k now builds from common code for the JVM, Apple platforms, Linux, Windows and Node.js (js and wasmJs), with a default Ktor engine per platform. On the JVM, the public API, dependencies, bytecode level and behavior are unchanged from 0.1.0. - Breaking: the Maven group is now com.pambrose.jev4k. Gradle builds depend on com.pambrose.jev4k:jev4k, Maven builds on com.pambrose.jev4k:jev4k-jvm; 0.1.0 stays at com.pambrose:jev4k. The release notes, README and docs site call this out. - ABI validation guards the JVM and klib surfaces (api/). - The common tests run on every platform; LiveProbeTest adds live checks that spend no tokens. - make docker-linux-tests runs the linuxX64 and linuxArm64 tests in Docker containers, and make platform-tests runs every platform's tests. The tvOS and watchOS simulator tests run where a simulator device exists. - CI adds macOS and Windows jobs. - Fix four empty admonitions and a broken anchor on the docs site. Co-Authored-By: Claude Opus 5.5 --- .editorconfig | 4 +- .github/workflows/ci.yml | 98 ++- .github/workflows/docs.yml | 7 + .gitignore | 5 + CHANGELOG.md | 51 +- CLAUDE.md | 209 +++++-- Makefile | 127 +++- README.md | 87 ++- RELEASE_NOTES.md | 66 ++ api/jev4k.api | 497 +++++++++++++++ api/jev4k.klib.api | 543 +++++++++++++++++ build.gradle.kts | 303 ++++++++-- codecov.yml | 4 +- config/detekt/detekt.yml | 2 + docs/packages.md | 12 +- docs/release-checklist.md | 30 +- gradle.properties | 6 +- gradle/libs.versions.toml | 11 +- kotlin-js-store/wasm/yarn.lock | 47 ++ kotlin-js-store/yarn.lock | 567 ++++++++++++++++++ .../pambrose/jev4k/internal/Engine.apple.kt | 16 + .../kotlin/com/pambrose/jev4k/Answers.kt | 0 .../kotlin/com/pambrose/jev4k/BlockingJev.kt | 12 + .../kotlin/com/pambrose/jev4k/Builders.kt | 2 + .../kotlin/com/pambrose/jev4k/Entries.kt | 1 + .../kotlin/com/pambrose/jev4k/Errors.kt | 0 .../kotlin/com/pambrose/jev4k/JevApi.kt | 1 + .../kotlin/com/pambrose/jev4k/JevClient.kt | 90 +-- .../kotlin/com/pambrose/jev4k/JevConfig.kt | 11 +- .../kotlin/com/pambrose/jev4k/JevDsl.kt | 0 .../kotlin/com/pambrose/jev4k/JevQuery.kt | 1 + .../kotlin/com/pambrose/jev4k/JevResult.kt | 1 + .../kotlin/com/pambrose/jev4k/Questions.kt | 5 +- .../kotlin/com/pambrose/jev4k/Testing.kt | 1 + .../jev4k/internal/HttpClientFactory.kt | 16 +- .../com/pambrose/jev4k/internal/Platform.kt | 32 + .../pambrose/jev4k/internal/ResponseMapper.kt | 2 +- .../com/pambrose/jev4k/internal/Retry.kt | 9 +- .../com/pambrose/jev4k/internal/Wire.kt | 0 .../kotlin/com/pambrose/jev4k/ClientTest.kt | 46 +- .../kotlin/com/pambrose/jev4k/ConfigTest.kt | 0 .../kotlin/com/pambrose/jev4k/DslTest.kt | 0 .../kotlin/com/pambrose/jev4k/EntriesTest.kt | 0 .../com/pambrose/jev4k/LiveProbeTest.kt | 37 ++ .../com/pambrose/jev4k/PlatformEngineTest.kt | 33 + .../com/pambrose/jev4k/ResponseMappingTest.kt | 0 .../kotlin/com/pambrose/jev4k/RetryTest.kt | 14 +- .../kotlin/com/pambrose/jev4k/TestSupport.kt | 77 +-- .../com/pambrose/jev4k/ValidationTest.kt | 0 .../pambrose/jev4k/WireSerializationTest.kt | 0 .../com/pambrose/jev4k/BlockingJev.jvm.kt | 60 ++ .../pambrose/jev4k/internal/Platform.jvm.kt | 23 + .../java/website/JavaInterop.java | 2 +- .../com/pambrose/jev4k/BlockingJevTest.kt | 0 .../com/pambrose/jev4k/ClientJvmTest.kt | 61 ++ .../kotlin/com/pambrose/jev4k/ConsumerTest.kt | 0 .../com/pambrose/jev4k/LiveSmokeTest.kt | 0 .../kotlin/com/pambrose/jev4k/SilentServer.kt | 58 ++ .../pambrose/jev4k/examples/TriageExample.kt | 0 .../kotlin/website/BestPracticeExamples.kt | 0 .../kotlin/website/ChoiceExamples.kt | 0 .../kotlin/website/ClassificationExamples.kt | 0 .../kotlin/website/ClientConfig.txt | 0 .../kotlin/website/ClientExamples.kt | 0 .../website/CompositeScoringExamples.kt | 0 .../kotlin/website/ConceptsExamples.kt | 0 .../kotlin/website/ConfidenceExamples.kt | 0 src/jvmTest/kotlin/website/Development.txt | 30 + .../kotlin/website/EnumChoiceExamples.kt | 0 .../kotlin/website/ErrorExamples.kt | 0 .../kotlin/website/ExtractionExamples.kt | 0 .../kotlin/website/FanOutExamples.kt | 0 .../kotlin/website/FirstTriage.kt | 0 .../kotlin/website/GettingStarted.txt | 25 +- .../kotlin/website/GuardrailExamples.kt | 0 .../kotlin/website/InlineDslExamples.kt | 0 .../kotlin/website/NoulExamples.kt | 0 .../kotlin/website/QuickStart.kt | 0 .../kotlin/website/RankingExamples.kt | 0 .../kotlin/website/ResultExamples.kt | 0 .../kotlin/website/RoutingExamples.kt | 0 .../kotlin/website/ScoreExamples.kt | 0 .../kotlin/website/Shared.kt | 0 .../kotlin/website/StateExamples.kt | 0 .../kotlin/website/StructuredExamples.kt | 0 .../kotlin/website/TypedQueryExamples.kt | 0 .../kotlin/website/VerificationExamples.kt | 0 .../kotlin/website/WireExamples.txt | 0 .../kotlin/website/WireRequest.kt | 0 .../pambrose/jev4k/internal/Engine.linux.kt | 16 + .../pambrose/jev4k/internal/Engine.mingw.kt | 16 + .../com/pambrose/jev4k/BlockingJev.native.kt | 7 + .../jev4k/internal/Platform.native.kt | 10 + src/test/kotlin/website/Development.txt | 24 - .../com/pambrose/jev4k/BlockingJev.web.kt | 7 + .../pambrose/jev4k/internal/Platform.web.kt | 30 + website/jev4k/docs/api.md | 2 +- website/jev4k/docs/client/calls.md | 6 +- website/jev4k/docs/client/configuration.md | 9 +- .../docs/getting-started/installation.md | 149 +++-- .../jev4k/docs/getting-started/quick-start.md | 2 +- website/jev4k/docs/guides/development.md | 47 +- website/jev4k/docs/index.md | 4 +- website/jev4k/docs/llms.txt | 7 +- website/jev4k/docs/patterns/guardrails.md | 7 +- website/jev4k/docs/questions/index.md | 9 +- website/jev4k/docs/questions/noul.md | 7 +- website/jev4k/docs/questions/score.md | 7 +- website/jev4k/zensical.toml | 6 +- website/uv.lock | 185 +++--- 110 files changed, 3339 insertions(+), 560 deletions(-) create mode 100644 api/jev4k.api create mode 100644 api/jev4k.klib.api create mode 100644 kotlin-js-store/wasm/yarn.lock create mode 100644 kotlin-js-store/yarn.lock create mode 100644 src/appleMain/kotlin/com/pambrose/jev4k/internal/Engine.apple.kt rename src/{main => commonMain}/kotlin/com/pambrose/jev4k/Answers.kt (100%) create mode 100644 src/commonMain/kotlin/com/pambrose/jev4k/BlockingJev.kt rename src/{main => commonMain}/kotlin/com/pambrose/jev4k/Builders.kt (98%) rename src/{main => commonMain}/kotlin/com/pambrose/jev4k/Entries.kt (99%) rename src/{main => commonMain}/kotlin/com/pambrose/jev4k/Errors.kt (100%) rename src/{main => commonMain}/kotlin/com/pambrose/jev4k/JevApi.kt (98%) rename src/{main => commonMain}/kotlin/com/pambrose/jev4k/JevClient.kt (66%) rename src/{main => commonMain}/kotlin/com/pambrose/jev4k/JevConfig.kt (92%) rename src/{main => commonMain}/kotlin/com/pambrose/jev4k/JevDsl.kt (100%) rename src/{main => commonMain}/kotlin/com/pambrose/jev4k/JevQuery.kt (99%) rename src/{main => commonMain}/kotlin/com/pambrose/jev4k/JevResult.kt (99%) rename src/{main => commonMain}/kotlin/com/pambrose/jev4k/Questions.kt (96%) rename src/{main => commonMain}/kotlin/com/pambrose/jev4k/Testing.kt (98%) rename src/{main => commonMain}/kotlin/com/pambrose/jev4k/internal/HttpClientFactory.kt (83%) create mode 100644 src/commonMain/kotlin/com/pambrose/jev4k/internal/Platform.kt rename src/{main => commonMain}/kotlin/com/pambrose/jev4k/internal/ResponseMapper.kt (98%) rename src/{main => commonMain}/kotlin/com/pambrose/jev4k/internal/Retry.kt (89%) rename src/{main => commonMain}/kotlin/com/pambrose/jev4k/internal/Wire.kt (100%) rename src/{test => commonTest}/kotlin/com/pambrose/jev4k/ClientTest.kt (91%) rename src/{test => commonTest}/kotlin/com/pambrose/jev4k/ConfigTest.kt (100%) rename src/{test => commonTest}/kotlin/com/pambrose/jev4k/DslTest.kt (100%) rename src/{test => commonTest}/kotlin/com/pambrose/jev4k/EntriesTest.kt (100%) create mode 100644 src/commonTest/kotlin/com/pambrose/jev4k/LiveProbeTest.kt create mode 100644 src/commonTest/kotlin/com/pambrose/jev4k/PlatformEngineTest.kt rename src/{test => commonTest}/kotlin/com/pambrose/jev4k/ResponseMappingTest.kt (100%) rename src/{test => commonTest}/kotlin/com/pambrose/jev4k/RetryTest.kt (89%) rename src/{test => commonTest}/kotlin/com/pambrose/jev4k/TestSupport.kt (66%) rename src/{test => commonTest}/kotlin/com/pambrose/jev4k/ValidationTest.kt (100%) rename src/{test => commonTest}/kotlin/com/pambrose/jev4k/WireSerializationTest.kt (100%) create mode 100644 src/jvmMain/kotlin/com/pambrose/jev4k/BlockingJev.jvm.kt create mode 100644 src/jvmMain/kotlin/com/pambrose/jev4k/internal/Platform.jvm.kt rename src/{test => jvmTest}/java/website/JavaInterop.java (96%) rename src/{test => jvmTest}/kotlin/com/pambrose/jev4k/BlockingJevTest.kt (100%) create mode 100644 src/jvmTest/kotlin/com/pambrose/jev4k/ClientJvmTest.kt rename src/{test => jvmTest}/kotlin/com/pambrose/jev4k/ConsumerTest.kt (100%) rename src/{test => jvmTest}/kotlin/com/pambrose/jev4k/LiveSmokeTest.kt (100%) create mode 100644 src/jvmTest/kotlin/com/pambrose/jev4k/SilentServer.kt rename src/{test => jvmTest}/kotlin/com/pambrose/jev4k/examples/TriageExample.kt (100%) rename src/{test => jvmTest}/kotlin/website/BestPracticeExamples.kt (100%) rename src/{test => jvmTest}/kotlin/website/ChoiceExamples.kt (100%) rename src/{test => jvmTest}/kotlin/website/ClassificationExamples.kt (100%) rename src/{test => jvmTest}/kotlin/website/ClientConfig.txt (100%) rename src/{test => jvmTest}/kotlin/website/ClientExamples.kt (100%) rename src/{test => jvmTest}/kotlin/website/CompositeScoringExamples.kt (100%) rename src/{test => jvmTest}/kotlin/website/ConceptsExamples.kt (100%) rename src/{test => jvmTest}/kotlin/website/ConfidenceExamples.kt (100%) create mode 100644 src/jvmTest/kotlin/website/Development.txt rename src/{test => jvmTest}/kotlin/website/EnumChoiceExamples.kt (100%) rename src/{test => jvmTest}/kotlin/website/ErrorExamples.kt (100%) rename src/{test => jvmTest}/kotlin/website/ExtractionExamples.kt (100%) rename src/{test => jvmTest}/kotlin/website/FanOutExamples.kt (100%) rename src/{test => jvmTest}/kotlin/website/FirstTriage.kt (100%) rename src/{test => jvmTest}/kotlin/website/GettingStarted.txt (68%) rename src/{test => jvmTest}/kotlin/website/GuardrailExamples.kt (100%) rename src/{test => jvmTest}/kotlin/website/InlineDslExamples.kt (100%) rename src/{test => jvmTest}/kotlin/website/NoulExamples.kt (100%) rename src/{test => jvmTest}/kotlin/website/QuickStart.kt (100%) rename src/{test => jvmTest}/kotlin/website/RankingExamples.kt (100%) rename src/{test => jvmTest}/kotlin/website/ResultExamples.kt (100%) rename src/{test => jvmTest}/kotlin/website/RoutingExamples.kt (100%) rename src/{test => jvmTest}/kotlin/website/ScoreExamples.kt (100%) rename src/{test => jvmTest}/kotlin/website/Shared.kt (100%) rename src/{test => jvmTest}/kotlin/website/StateExamples.kt (100%) rename src/{test => jvmTest}/kotlin/website/StructuredExamples.kt (100%) rename src/{test => jvmTest}/kotlin/website/TypedQueryExamples.kt (100%) rename src/{test => jvmTest}/kotlin/website/VerificationExamples.kt (100%) rename src/{test => jvmTest}/kotlin/website/WireExamples.txt (100%) rename src/{test => jvmTest}/kotlin/website/WireRequest.kt (100%) create mode 100644 src/linuxMain/kotlin/com/pambrose/jev4k/internal/Engine.linux.kt create mode 100644 src/mingwMain/kotlin/com/pambrose/jev4k/internal/Engine.mingw.kt create mode 100644 src/nativeMain/kotlin/com/pambrose/jev4k/BlockingJev.native.kt create mode 100644 src/nativeMain/kotlin/com/pambrose/jev4k/internal/Platform.native.kt delete mode 100644 src/test/kotlin/website/Development.txt create mode 100644 src/webMain/kotlin/com/pambrose/jev4k/BlockingJev.web.kt create mode 100644 src/webMain/kotlin/com/pambrose/jev4k/internal/Platform.web.kt diff --git a/.editorconfig b/.editorconfig index 6fd6be7..993d0cd 100644 --- a/.editorconfig +++ b/.editorconfig @@ -32,8 +32,8 @@ ktlint_standard_import-ordering = disabled # The documentation examples line their trailing comments up in a column, which reads better on the docs site # than ragged comments do. ktlint's no-multi-spaces rule forbids that, so it is off for these files only; the -# library sources in src/main keep it. -[src/test/kotlin/website/*.kt] +# library sources in src/commonMain and the other main source sets keep it. +[src/jvmTest/kotlin/website/*.kt] ktlint_standard_no-multi-spaces = disabled [build/generated/**/*] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a27a553..3e530d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,8 +16,11 @@ permissions: contents: read jobs: + # Linux runs the jvm, js, wasmJs and linuxX64 tests. It can't run the Apple or Windows test executables, so the + # native-apple and native-windows jobs below cover those. build: runs-on: ubuntu-latest + timeout-minutes: 60 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # JDK 25 is the toolchain in the catalog. @@ -26,15 +29,22 @@ jobs: distribution: temurin java-version: 25 - uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 + # setup-gradle caches ~/.gradle but not the Kotlin/Native toolchain that compiling the native targets + # downloads. The catalog pins the Kotlin version, so its hash is a good cache key. + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.konan + key: konan-${{ runner.os }}-${{ hashFiles('gradle/libs.versions.toml') }} + # A catalog bump that leaves Kotlin alone still restores the toolchain; Gradle downloads what + # is missing. + restore-keys: konan-${{ runner.os }}- - # build runs check without the tests, and check already includes lintKotlin and detekt. - - name: Compile, lint, and detekt (no tests) - run: ./gradlew clean build -x test - - # --continue lets the Kover report tasks run even if some tests fail, so coverage is still uploaded. - # LiveSmokeTest stays disabled: it needs JEV4K_LIVE=1 and TYPESAFE_API_KEY, neither of which is set here. - - name: Run tests and generate coverage report - run: ./gradlew --continue test koverVerify koverXmlReport koverLog + # build runs check, which compiles every target this host supports and runs lintKotlin, detekt, the ABI + # check against api/, and the jvm, js, wasmJs and linuxX64 tests. --continue lets the Kover report tasks + # run even if some tests fail, so coverage is still uploaded. The live tests stay disabled: they need + # JEV4K_LIVE=1 (and LiveSmokeTest a TYPESAFE_API_KEY), and neither is set here. + - name: Build, lint, check the ABI, run the tests, and generate the coverage report + run: ./gradlew --continue build koverVerify koverXmlReport koverLog - name: Upload test reports on failure if: failure() @@ -63,6 +73,7 @@ jobs: test: name: Tests on JDK ${{ matrix.java }} runs-on: ubuntu-latest + timeout-minutes: 30 strategy: # One JDK failing shouldn't hide the result on the others. fail-fast: false @@ -88,7 +99,7 @@ jobs: # shortcut rather than a requirement. - name: Run tests on JDK ${{ matrix.java }} run: > - ./gradlew test -PtestJavaVersion=${{ matrix.java }} + ./gradlew jvmTest -PtestJavaVersion=${{ matrix.java }} "-Porg.gradle.java.installations.fromEnv=JAVA_HOME_${{ matrix.java }}_X64,JAVA_HOME_25_X64" - name: Upload test reports on failure @@ -100,3 +111,72 @@ jobs: build/reports/tests/ build/test-results/ retention-days: 7 + + # The Apple targets' tests need a macOS host. The watchOS and tvOS simulator test tasks are disabled in the build + # (Xcode doesn't install those simulator runtimes by default), and Gradle skips iosX64Test on the arm64 runner. + native-apple: + runs-on: macos-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 + with: + distribution: temurin + java-version: 25 + - uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 + # setup-gradle caches ~/.gradle but not the Kotlin/Native toolchain that compiling the native targets + # downloads. The catalog pins the Kotlin version, so its hash is a good cache key. + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.konan + key: konan-${{ runner.os }}-${{ hashFiles('gradle/libs.versions.toml') }} + # A catalog bump that leaves Kotlin alone still restores the toolchain; Gradle downloads what + # is missing. + restore-keys: konan-${{ runner.os }}- + + - name: Run the macOS and iOS simulator tests + run: ./gradlew macosArm64Test iosSimulatorArm64Test + + - name: Upload test reports on failure + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: test-reports-apple + path: | + build/reports/tests/ + build/test-results/ + retention-days: 7 + + native-windows: + runs-on: windows-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 + with: + distribution: temurin + java-version: 25 + - uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 + # setup-gradle caches ~/.gradle but not the Kotlin/Native toolchain that compiling the native targets + # downloads. The catalog pins the Kotlin version, so its hash is a good cache key. + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.konan + key: konan-${{ runner.os }}-${{ hashFiles('gradle/libs.versions.toml') }} + # A catalog bump that leaves Kotlin alone still restores the toolchain; Gradle downloads what + # is missing. + restore-keys: konan-${{ runner.os }}- + + - name: Run the mingwX64 tests + shell: bash + run: ./gradlew mingwX64Test + + - name: Upload test reports on failure + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: test-reports-windows + path: | + build/reports/tests/ + build/test-results/ + retention-days: 7 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6439b58..0b421d7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -40,6 +40,13 @@ jobs: distribution: temurin java-version: 25 - uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 + # Dokka documents every source set, the native ones included, so it needs the Kotlin/Native toolchain; + # setup-gradle doesn't cache it. The catalog pins the Kotlin version, so its hash is a good cache key. + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.konan + key: konan-${{ runner.os }}-${{ hashFiles('gradle/libs.versions.toml') }} + restore-keys: konan-${{ runner.os }}- - run: ./gradlew dokkaGeneratePublicationHtml # Copy KDocs into the Zensical site output; the site's "KDocs" page links to /kdocs/. diff --git a/.gitignore b/.gitignore index e26ccce..4a10e5a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ secrets/ .idea/jarRepositories.xml .idea/compiler.xml .idea/libraries/ +.idea/artifacts/ *.iws *.iml *.ipr @@ -53,6 +54,10 @@ __pycache__/ ### it. This negation is what keeps it in git, because a global *.lock ignore would otherwise hide it. !website/uv.lock +### The Node.js lockfiles for the js and wasmJs test toolchains are committed too (the same global *.lock ignore +### would hide them). Regenerate them with kotlinUpgradeYarnLock and kotlinWasmUpgradeYarnLock. +!kotlin-js-store/**/yarn.lock + ### Zensical build output ### website/**/site/ website/**/.cache/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 1097fb7..ca5193c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,54 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Narrative notes for each release are in [RELEASE_NOTES.md](RELEASE_NOTES.md). -## [Unreleased] +## [0.2.0] — unreleased -Nothing yet. +jev4k is now a Kotlin Multiplatform library, published under the new group `com.pambrose.jev4k`. The JVM API is +unchanged; the other platforms are new. + +### Added + +- Targets beyond the JVM: Apple platforms (`macosArm64`, `iosArm64`, `iosX64`, `iosSimulatorArm64`, `tvosArm64`, + `tvosSimulatorArm64`, `watchosArm32`, `watchosArm64`, `watchosSimulatorArm64`, `watchosDeviceArm64`), Linux + (`linuxX64`, `linuxArm64`), Windows (`mingwX64`), and Node.js (`js`, `wasmJs`). The whole API is common code. +- A default Ktor engine per platform: CIO on the JVM, as before, Darwin on Apple platforms, Curl on Linux, WinHttp + on Windows, and the `fetch`-based Js engine on Node.js. CIO can't make HTTPS requests on Kotlin/Native, hence the + others. +- Each engine's own way of reporting a failed connection (a bare `IllegalStateException` from Curl and WinHttp, a + failed fetch from the Js engine) becomes a `JevConnectionException` and is retried like an `IOException`. +- ABI validation. `api/jev4k.api` (the JVM surface) and `api/jev4k.klib.api` (the other targets) record the public + API; `make tests` and CI fail when it changes, and `make abi-update` rewrites them after an intended change. +- `LiveProbeTest`, two opt-in calls to the real API that spend no tokens, run on every platform: an invalid key must + come back as `JevAuthenticationException` and a 1 ms timeout as `JevTimeoutException`. +- `make docker-linux-tests` runs the linuxX64 and linuxArm64 tests in Docker containers, so any host with Docker can + run them, and linuxArm64, which has no Gradle test task, is tested at all. `make all-tests` includes it when Docker + is running. +- The tvOS and watchOS simulator tests run on a Mac that has a simulator device for them, and are skipped on one + that doesn't; `make tests` and `make native-tests` include them. + +### Changed + +- **Breaking: Maven coordinates.** The group is now `com.pambrose.jev4k`, so every artifact sits under one group, as + common-utils' do. `com.pambrose.jev4k:jev4k` is the multiplatform root module: Gradle builds depend on it and get + the right artifact for each target, and Maven builds depend on `com.pambrose.jev4k:jev4k-jvm`. 0.1.0 stays at + `com.pambrose:jev4k`, and nothing newer is published there. +- The JVM API is unchanged: its ABI dump matches 0.1.0's, `JavaInterop.java` compiles as before, and the + `jev4k-jvm` POM lists the same seven dependencies. The published metadata still carries + `org.gradle.jvm.version = 17`. +- `JevClient.blocking` has its methods on the JVM only. On the other platforms `BlockingJev` has no members: + Kotlin/JS and Kotlin/Wasm can't block a thread, and Kotlin/Native callers can wrap the suspend calls in + `runBlocking` themselves. +- The `User-Agent` version is compiled in rather than read from the jar manifest, so it is correct on every + platform and in tests. The JVM jar still carries `Implementation-Version` and `Automatic-Module-Name`. +- Sources moved to `src/commonMain`, `src/jvmMain` and the per-platform source sets. Tests moved to + `src/commonTest`, which runs on every platform, and `src/jvmTest`, which keeps the MockK, blocking, live-smoke and + real-CIO tests, the runnable example, and the documentation examples. +- `make build` compiles every target without running tests; `make tests` re-runs every test task the host supports; + `jvm-tests`, `js-tests`, `native-tests`, `platform-tests` (every platform's tests, Docker Linux included), + `abi-check` and `abi-update` are new. The Maven Central publishing targets require macOS, the only host that + builds the Apple targets. +- CI runs the jvm, js, wasmJs and linuxX64 tests on Linux, and adds a macOS job (macOS and the iOS simulator) and a + Windows job (`mingwX64`). ## [0.1.0] - 2026-09-20 @@ -116,5 +161,5 @@ First release: a Kotlin DSL and client for [TypeSafe](https://docs.typesafe.ai)' - Dokka KDocs for the public API at . - `llms.txt` at , indexing the site for coding agents. -[Unreleased]: https://github.com/pambrose/jev4k/compare/0.1.0...HEAD +[0.2.0]: https://github.com/pambrose/jev4k/compare/0.1.0...0.2.0 [0.1.0]: https://github.com/pambrose/jev4k/releases/tag/0.1.0 diff --git a/CLAUDE.md b/CLAUDE.md index c7e3b9e..ac4a42f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,10 +4,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project -`jev4k` is a Kotlin DSL and client for TypeSafe's Jev "System One" model, built on the Ktor client (CIO engine) and +`jev4k` is a Kotlin Multiplatform DSL and client for TypeSafe's Jev "System One" model, built on the Ktor client and kotlinx.serialization. You describe a state and typed questions (Noul, Choice, Score), and it returns typed answers. The -library is `com.pambrose.jev4k` under `src/main/kotlin`. A runnable example is -`src/test/kotlin/com/pambrose/jev4k/examples/TriageExample.kt`. +library is `com.pambrose.jev4k`, almost all of it common code under `src/commonMain/kotlin`, for the JVM (the primary +target), Apple platforms, Linux, Windows, and Node.js (js and wasmJs). A runnable example is +`src/jvmTest/kotlin/com/pambrose/jev4k/examples/TriageExample.kt`. ## TypeSafe / Jev API docs @@ -55,10 +56,38 @@ There are two DSL layers over one core model. Both produce a validated `Question - `HttpRequestRetry` must be installed **before** `HttpTimeout`, otherwise one timeout cancels every retry. - `expectSuccess = false`: non-2xx responses map to `JevApiException` subclasses (`apiException` in `Errors.kt`) after retries run out, keeping the raw body and the `x-typesafe-request-id` header. - - `BlockingJev` (`jev.blocking`) wraps the suspend API in `runBlocking`. + - `BlockingJev` (`jev.blocking`) wraps the suspend API in `runBlocking`, on the JVM only (see Platforms). + - `JevClient.execute` classifies a failed call in one place: `SerializationException` becomes + `JevValidationException`, a `CancellationException` is rethrown untouched (checked first, because on + Kotlin/Native it is also an `IllegalStateException`), and any other `Throwable` becomes `JevTimeoutException` + or `JevConnectionException` if it is one, or is rethrown. `Throwable`, because the Js engine reports a failed + fetch as a `kotlin.Error`. `isConnectionError` in `Retry.kt` uses the same predicate, so what is reported as + a connection error is also what gets retried. - **Config** (`JevConfig.kt`). Each setting resolves as explicit value, then env var, then default; blank env values are ignored. The env vars are `TYPESAFE_API_KEY` (required), `TYPESAFE_BASE_URL` and `TYPESAFE_DEFAULT_MODEL`. Internal - hooks (`env`, `retryDelay`, `random`) make tests deterministic. + hooks (`env`, `retryDelay`, `random`) make tests deterministic; `env` defaults to `platformGetenv`. +- **Platforms** (`internal/Platform.kt` and its actuals). Everything that differs between platforms is an `internal` + expect: `platformGetenv`, `defaultHttpClient` + `DEFAULT_ENGINE_NAME`, `isPlatformConnectionError`, and + `Enum<*>.enumTypeName()` (which keeps `enumChoiceRef`'s `@PublishedApi` signature unchanged). Actual files carry a + platform suffix (`Platform.jvm.kt`, `Engine.linux.kt`) so JVM facade names never clash. + - Default engines: CIO on the JVM (`jvmMain`), Darwin (`appleMain`), Curl (`linuxMain`), WinHttp (`mingwMain`), + and the Js engine bundled in `ktor-client-core` (`webMain`, shared by js and wasmJs). CIO can't be used + natively: Ktor's native TLS fails with "TLS sessions are not supported on Native platform". + - How each engine reports a refused connection: CIO and Darwin throw an `IOException`; Curl and WinHttp a bare + `IllegalStateException` (matched by exact class, so a native `CancellationException` never counts); the Js + engine `Error("Fail to fetch")`. `PlatformEngineTest` dials a dead loopback port on every platform to pin this. + The bare-ISE rule is deliberately broad: Curl and WinHttp also throw one for local setup failures (a failed + handle or proxy setup), which are then retried and reported as connection errors with the cause kept, and the + rule follows the host's default engine rather than the engine in use. `ClientJvmTest` pins that the JVM + treats a bare ISE as an ordinary failure. + - `BlockingJev` is an `expect class`. The `jvmMain` actual is the real one; the `nativeMain` and `webMain` actuals + are empty. `JevClient` keeps `val blocking = BlockingJev(this)` in common code, so the JVM class file, and + Java's `jev.getBlocking()`, are exactly as before. `-Xexpect-actual-classes` silences the Beta warning. + - `platformGetenv` on js/wasmJs is a `js()` call that must be the whole body of a top-level function (a + Kotlin/Wasm rule) and needs `@OptIn(ExperimentalWasmJsInterop::class)`. It reads `process.env`, so it only + works on Node.js, the only JS runtime targeted. + - Common code can't use JVM-only APIs such as `Map.putIfAbsent`, and needs explicit `kotlin.jvm.JvmOverloads` / + `kotlin.jvm.JvmSynthetic` imports (only the JVM imports `kotlin.jvm.*` by default). - **Validation** (`Questions.kt`). Every problem is collected into one `JevValidationException` before anything is sent: at least one question, unique non-blank ids, non-empty instructions, 1..255 Choice options, 2..10 Score levels. @@ -89,19 +118,22 @@ There are two DSL layers over one core model. Both produce a validated `Question breaks into a one-item list, a stray rule and two loose paragraphs, each landing in its own grid cell. The page still builds cleanly, so only the rendered HTML (or a look at the page) catches it: one `