Skip to content

docs: keep JetWhale out of release builds, and a skill that wires it in - #208

Open
kitakkun wants to merge 5 commits into
mainfrom
docs/integrating_jetwhale_without_affecting_prod_build
Open

docs: keep JetWhale out of release builds, and a skill that wires it in#208
kitakkun wants to merge 5 commits into
mainfrom
docs/integrating_jetwhale_without_affecting_prod_build

Conversation

@kitakkun

@kitakkun kitakkun commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Why

Getting Started only said "add it to debug builds". That is true right up until shared code calls startJetWhale { } — from then on the release variant needs those symbols to compile and the dependency comes straight back. Android variant source sets do not reach a KMP commonMain, and a BuildConfig.DEBUG guard still ships every class it guards.

What

A guide — docs/guide/excluding-from-release-builds.md. Production code depends on an app-owned interface; a no-op binding is contributed from an always-present module and displaced on the debug classpath by a JetWhale-backed implementation. HTTP client capture is modelled as a multibinding instead, since release simply contributes no element. Covers the Gradle wiring for both Android variants and KMP, how to verify against the release classpath, and the failure modes.

A skill — /jetwhale:integrate. The QA skill serves plugin authors working inside this repository; nothing served the larger group adding JetWhale to an app they want to debug. The skill surveys the project's targets, DI framework, HTTP client and existing debug seam before writing anything, prefers riding an existing debug-only module over inventing one, then routes to a per-framework reference: metro.md, anvil.md, dagger-hilt.md, koin.md, no-di.md.

Plugin bumped to 0.2.0 and both manifests reworded, since the plugin is no longer only about developing host plugins.

Verification

Every documented pattern was built as a four-module project (:seam, :tooling, :app-debug depending on both, :app-release depending on :seam only) and run, so each recorded result is the binding that actually resolved rather than one inferred from the annotations.

Framework Versions Result
Metro 1.3.2, Kotlin 2.4.10 release noop + empty decorator set; debug real binding; @SingleIn holder identical across both injection sites
kotlin-inject-anvil 0.1.7 / kotlin-inject 0.9.0 / KSP 2.3.10 / Kotlin 2.3.10 replaces resolves both ways; an empty Set<T> fails KSP outright
Square Anvil 2.7.0, Dagger 2.60.1, Kotlin 2.2.20 replaces resolves both ways — only after moving Dagger from KSP to kapt
Dagger 2.60.1 @BindsOptionalOfOptional.empty() in release, present in debug; @Multibinds allows empty with no parameter
Hilt + Android variants 2.60.1, AGP 9.3.0, Kotlin 2.4.10 generated component binds Optional.of(...) in debug vs Optional.empty() in release, with no release-side module
Koin 4.2.2 getOrNull null in release; getAll empty; single shares one instance
No DI + AGP variants AGP 9.3.0, Kotlin 2.4.10 debug APK dex carries the debug-only class, release APK carries zero occurrences

Isolation was checked on the built artifacts, not the wiring: the debug-only project appeared on debugRuntimeClasspath, was absent from releaseRuntimeClasspath, and its classes were absent from the release APK's dex.

Three claims from the first draft did not survive contact with a compiler and were corrected:

  • kotlin-inject has no @Multibinds(allowEmpty = true). An empty set fails with Cannot find an @Inject constructor or provider for: Set<…>, so the HTTP decorator must be an ordinary binding with a no-op default, displaced by replaces like the initializer.
  • Square Anvil requires kapt. It generates through the Kotlin compiler plugin, which runs after KSP has read source, so Dagger on KSP generates nothing and the build fails much later on an unresolved DaggerAppComponent.
  • Hilt does not need a same-name module pair. @InstallIn is discovered from the classpath, so @BindsOptionalOf in src/main plus a module in src/debug is enough. The pair is only unavoidable for plain Dagger, which lists its modules explicitly.

Metro semantics were additionally checked inside this repository: with replaces the graph merges, without it the build fails with Metro/DuplicateBinding naming both contributions. That probe was removed. @Provides follows this repository's own working idiom (a @ContributesTo interface).

VitePress builds clean, dead-link checking included.

Note

The verification projects live outside the repository. Committing them so CI keeps the skill honest as these libraries move is a reasonable follow-up, at the cost of repository weight and CI time.

kitakkun added 2 commits July 29, 2026 12:02
A debug-only dependency stops being enough as soon as shared code calls
startJetWhale: the release variant needs those symbols to compile, so the
dependency comes back. Android variant source sets do not help a KMP
commonMain, and a BuildConfig.DEBUG guard still ships every class.

Document the seam instead: an app-owned interface with a no-op binding
contributed from an always-present module, displaced on the debug classpath
by a JetWhale-backed implementation via Metro's `@ContributesBinding`
replaces. HTTP client capture is a multibinding rather than a replacement,
since release simply contributes no element.

The Metro semantics here were checked against 1.3.2 by compiling both
directions: with replaces the graph merges, without it the build fails with
Metro/DuplicateBinding naming both contributions.
The QA skill serves plugin authors inside this repository; nothing served
the far larger group adding JetWhale to an app they want to debug. That
task is decided by two things the guide cannot assume — how the project
splits debug from release, and which DI framework it uses — so the skill
surveys both before writing anything, and prefers riding an existing
debug-only module or initializer abstraction over inventing a new seam.

Wiring is routed per framework: metro.md carries the verified detail,
with anvil.md, dagger-hilt.md, koin.md and no-di.md covering the rest.
Only Metro's semantics were compile-checked here, so the others tell the
agent to confirm annotation parameters against the project's own versions.

Verification targets the release classpath rather than the wiring, since
a graph that compiles proves nothing about what ships.
Copilot AI review requested due to automatic review settings July 29, 2026 03:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR expands JetWhale’s documentation and Claude Code plugin skill set to help consumers integrate JetWhale in a way that keeps all JetWhale symbols off release compile/runtime classpaths, even when shared code needs to call startJetWhale { }.

Changes:

  • Added a new long-form guide explaining the “DI seam” approach for excluding JetWhale from release builds (with Metro-focused examples and Gradle wiring for Android + KMP).
  • Added a new Claude Code skill (/jetwhale:integrate) plus DI-framework-specific reference pages to drive safe integrations.
  • Updated plugin manifests/README and docs-site navigation to reflect the broader scope and expose the new guide.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
plugins/jetwhale/skills/integrate/SKILL.md New /jetwhale:integrate skill instructions and routing logic (survey → choose wiring reference).
plugins/jetwhale/skills/integrate/references/no-di.md Reference wiring for projects without a DI framework (variant source sets / module-based seam).
plugins/jetwhale/skills/integrate/references/metro.md Metro reference wiring for debug/release displacement via classpath contribution merging.
plugins/jetwhale/skills/integrate/references/koin.md Koin reference wiring using variant-specific module definitions.
plugins/jetwhale/skills/integrate/references/dagger-hilt.md Dagger/Hilt reference wiring using variant source sets and (optional) multibinding for decorators.
plugins/jetwhale/skills/integrate/references/anvil.md Anvil / kotlin-inject-anvil deltas relative to the Metro-shaped approach.
plugins/jetwhale/README.md Updates plugin README to include the new integrate skill and revised positioning.
plugins/jetwhale/.claude-plugin/plugin.json Bumps plugin metadata to 0.2.0 with broadened description.
docs/guide/getting-started.md Links readers to the new “Excluding from Release Builds” guide once shared code calls JetWhale APIs.
docs/guide/excluding-from-release-builds.md New guide page describing the seam/module layout, DI bindings, and verification steps.
docs-site/.vitepress/config.mts Adds the new guide page to the VitePress sidebar navigation.
.claude-plugin/marketplace.json Updates marketplace metadata (description + version 0.2.0).
Comments suppressed due to low confidence (1)

plugins/jetwhale/skills/integrate/references/dagger-hilt.md:94

  • @Provides methods must be declared inside a @Module (and for Hilt, also in something @InstallIn(...)). As written, this top-level @Provides function won’t compile / be picked up by Dagger/Hilt.
// src/debug
@Provides
@IntoSet
fun provideJetWhaleDecorator(agent: JetWhaleNetworkAgentPlugin): HttpClientDecorator =
    HttpClientDecorator { client ->

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +12 to +14
interface DebugToolingInitializer {
fun initialize()
}
Comment on lines +29 to +31
interface DebugToolingInitializer {
fun initialize()
}
kitakkun added 3 commits July 29, 2026 13:54
Shipping a skill that tells an agent how to wire five DI frameworks, while
having compiled only one of them, put the burden of being wrong on whoever
ran it. Each pattern was rebuilt as a four-module project and run, so the
recorded result is the binding that actually resolved.

Three claims did not survive:

- kotlin-inject has no `@Multibinds(allowEmpty = true)`, so an empty set
  fails KSP outright. The HTTP decorator has to be an ordinary binding with
  a no-op default, displaced by `replaces` like the initializer.
- Square Anvil generates through the Kotlin compiler plugin, which runs
  after KSP reads source. With Dagger on KSP nothing is generated at all
  and the build fails much later on an unresolved DaggerAppComponent;
  kapt is required.
- Hilt does not need the same-name module pair this previously described.
  `@InstallIn` is discovered from the classpath, so `@BindsOptionalOf` in
  src/main plus a module in src/debug is enough. The pair is only
  unavoidable for plain Dagger, which lists its modules explicitly.

Dagger's `@Multibinds` also allows an empty set with no parameter, unlike
Metro's. Android isolation was checked on the artifacts rather than the
wiring: the debug-only project is absent from releaseRuntimeClasspath and
its classes are absent from the release APK's dex.
Nobody adopts Anvil today — 2.7.0 is pinned to Kotlin 2.2.20 and Metro is
its successor — so a how-to shaped section invited the wrong reading and
buried the one thing that costs real time. Lead with the kapt constraint,
since it has to be checked before any wiring is written and it fails
silently: no warning, nothing generated, and a build error much later at
an unresolved DaggerAppComponent.

The wiring itself collapses to a few lines now that it is stated as a
delta from Metro, and the migration exit gets its own closing section.
The file header also names the two libraries as unrelated, which they are
— they share a word and nothing else.
The reference files are prose a person reads, not instructions an agent
executes, and the Anvil section had drifted into commanding the reader:
"Do not reach for Anvil to solve this" tells someone already on Anvil not
to make a choice they are not making, and reads as a judgement of their
stack. The imperatives in SKILL.md stay — those genuinely direct an
agent's next action.

Also trims the dramatised description of the KSP failure to the fact
itself: there is no warning, nothing is generated, and the build fails
later somewhere unrelated.
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.

2 participants