Publish a stable iw-cli core API so downstream tools don't break on every change #398

Open
opened 2026-06-30 05:26:43 +00:00 by mph · 0 comments
Owner

Problem

Downstream tools compile directly against iw-cli's internal core source tree
and break, at the consumer's compile time, whenever any core signature
changes. There is no published, versioned, stable API surface and no contract
about what is safe to depend on.

Concretely, kanon's iw-plugin Scala commands (batch-implement, implement,
autonomous-bootstrap) and hooks wildcard-import three entire internal
packages:

import iw.core.model.*
import iw.core.adapters.*
import iw.core.output.*

They are built against the local core sources (via using-directives), so the
de-facto public API is "everything in those packages" — undocumented, unversioned,
and free to change.

Concrete incident that triggered this

IW-389 (Forgejo support) made a correct, intentional change to
core/model/ForgeType.scala:

  • def cliTool: Stringdef cliTool: Option[String]
  • def installUrl: Stringdef installUrl: Option[String]
  • added a Forgejo case to the ForgeType enum

That is the right model — Forgejo has no CLI binary. But it silently broke
kanon's batch-implement.scala:

  • compile error Found: Option[String], Required: String at the forge-CLI
    pre-flight check;
  • a latent non-exhaustive match (the new Forgejo enum case) in the
    merged-phase catch-up path → would have been a runtime MatchError.

Fixed downstream in kanon, but only after it broke in the field. Every future
core change carries the same risk for every consumer.

Root cause

No stability contract between core and its consumers:

  • no curated/public API surface — internals are wildcard-imported;
  • no semantic versioning of that surface, no changelog, no deprecation cycle;
  • no automated binary/source-compatibility checking in CI;
  • consumers build against local source rather than a released, pinned artifact.

What we want

A stable, published API that downstream tools can pin, so internal refactors
do not break them with every change.

Options to explore (not prescribing a solution)

  1. Curated facade / public module. Define a narrow, documented public
    surface (the symbols plugins actually use — e.g. ForgeType, IssueId,
    IssueTrackerType, ProjectContext, Output, ProcessAdapter,
    GitAdapter, ReviewStateAdapter, MarkdownTaskParser, the BatchImplement
    helpers, PhaseMerge duration parsing, …). Treat everything else as
    private/internal.
  2. Publish a versioned artifact to our Maven repo and have consumers depend
    on a released version instead of local core source via using-directives.
  3. Semver policy + changelog for that surface; enforce with a binary/source
    compatibility check (e.g. MiMa) in CI so breaking changes are caught before
    release.
  4. Enum-evolution guidance. Adding an enum case is source-breaking for
    exhaustive match in consumers. Document the additive-change policy and/or
    provide helper combinators so consumers don't pattern-match raw enums.

Acceptance criteria

  • A documented public API surface for iw-cli that downstream tools are
    expected to depend on, with everything else marked internal.
  • The surface is versioned (semver) and published so consumers can pin a
    version.
  • CI fails on an unintended breaking change to the public surface.
  • Documented policy/process for evolving the public API (deprecation,
    enum additions, etc.).
  • kanon's iw-plugin commands migrated to depend on the published surface
    instead of local core source.

References

  • IW-389 (Forgejo support) — the change that surfaced this.
  • kanon iw-plugin/commands/batch-implement.scala — the broken consumer.
## Problem Downstream tools compile directly against iw-cli's internal `core` source tree and break, at the consumer's compile time, whenever any `core` signature changes. There is no published, versioned, stable API surface and no contract about what is safe to depend on. Concretely, kanon's `iw-plugin` Scala commands (`batch-implement`, `implement`, `autonomous-bootstrap`) and hooks wildcard-import three entire internal packages: ``` import iw.core.model.* import iw.core.adapters.* import iw.core.output.* ``` They are built against the local `core` sources (via using-directives), so the de-facto public API is "everything in those packages" — undocumented, unversioned, and free to change. ## Concrete incident that triggered this IW-389 (Forgejo support) made a correct, intentional change to `core/model/ForgeType.scala`: - `def cliTool: String` → `def cliTool: Option[String]` - `def installUrl: String` → `def installUrl: Option[String]` - added a `Forgejo` case to the `ForgeType` enum That is the right model — Forgejo has no CLI binary. But it silently broke kanon's `batch-implement.scala`: - compile error `Found: Option[String], Required: String` at the forge-CLI pre-flight check; - a latent non-exhaustive `match` (the new `Forgejo` enum case) in the merged-phase catch-up path → would have been a runtime `MatchError`. Fixed downstream in kanon, but only after it broke in the field. Every future `core` change carries the same risk for every consumer. ## Root cause No stability contract between `core` and its consumers: - no curated/public API surface — internals are wildcard-imported; - no semantic versioning of that surface, no changelog, no deprecation cycle; - no automated binary/source-compatibility checking in CI; - consumers build against local source rather than a released, pinned artifact. ## What we want A stable, published API that downstream tools can pin, so internal refactors do not break them with every change. ## Options to explore (not prescribing a solution) 1. **Curated facade / public module.** Define a narrow, documented public surface (the symbols plugins actually use — e.g. `ForgeType`, `IssueId`, `IssueTrackerType`, `ProjectContext`, `Output`, `ProcessAdapter`, `GitAdapter`, `ReviewStateAdapter`, `MarkdownTaskParser`, the `BatchImplement` helpers, `PhaseMerge` duration parsing, …). Treat everything else as `private`/internal. 2. **Publish a versioned artifact** to our Maven repo and have consumers depend on a released version instead of local `core` source via using-directives. 3. **Semver policy + changelog** for that surface; enforce with a binary/source compatibility check (e.g. MiMa) in CI so breaking changes are caught before release. 4. **Enum-evolution guidance.** Adding an enum case is source-breaking for exhaustive `match` in consumers. Document the additive-change policy and/or provide helper combinators so consumers don't pattern-match raw enums. ## Acceptance criteria - A documented public API surface for iw-cli that downstream tools are expected to depend on, with everything else marked internal. - The surface is versioned (semver) and published so consumers can pin a version. - CI fails on an unintended breaking change to the public surface. - Documented policy/process for evolving the public API (deprecation, enum additions, etc.). - kanon's `iw-plugin` commands migrated to depend on the published surface instead of local `core` source. ## References - IW-389 (Forgejo support) — the change that surfaced this. - kanon `iw-plugin/commands/batch-implement.scala` — the broken consumer.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
iterative-works/iw-cli#398
No description provided.