Skip to content

v0.1.0

Choose a tag to compare

@github-actions github-actions released this 08 Oct 17:54
· 9 commits to main since this release
0f9b6cc

Security

  • Secret scanning with gitleaks. A Secret Scan CI workflow scans the full commit history on every push and PR, with --redact so a matched secret never reaches public logs. A .pre-commit-config.yaml runs the same check locally, and .gitignore now blocks common credential and signing-key files (*.gpg, *.pem, *.p12, *.jks, .env, …). A scan of the full existing history found no leaked secrets.

Fixed

  • The extractor no longer aborts on a type whose serializer can't load (#190). A NoClassDefFoundError, ExceptionInInitializerError, or other Error while walking one type used to fail the whole extraction. That type is now an OPAQUE coverage gap and the rest extract normally.
  • Value classes over a nullable value keep that nullability (#199). value class Id(val raw: String?) is now recorded as kotlin.String? (also inside List/Map), so narrowing it to String is a PROPERTY_TYPE_CHANGED BREAK instead of passing silently.
  • Shape changes around a generic type parameter are now caught (#192). List<T> → T or Map<String,T> → Map<Int,T> on a root-only generic is a PROPERTY_TYPE_CHANGED BREAK; it used to be skipped whenever either side contained a type parameter. A hole↔concrete flip is still not a finding.
  • ENUM_VALUE_ADDED coercibility counts every reader in the baseline, not just in-scope ones (#194). A required field in an out-of-scope type (for example one another module owns) now keeps the forward verdict at BREAK instead of WARN.
  • The sticky PR comment no longer duplicates on busy PRs, and its icon matches the job (#189). The comment lookup now paginates past 30 comments. ❌ now means the check failed (possibly with no BREAK, such as an empty baseline), ⚠️ covers BREAKs that don't fail the build under failOnBreaking = false, and a failed run without a report shows ❌ instead of "nothing configured".
  • A custom layout.buildDirectory is honored (#195). Setting it in the build script body used to split the extract and check tasks across two directories, so a stale build/serialkompat/current.snapshot could be diffed as the current schema (a false pass).
  • serialkompatRecord reads project.version lazily (#196). A version set in the build script body is now seen, and an unversioned project no longer fails configuration; only running serialkompatRecord fails without a version.
  • serialkompatCheck works on the root project of a single-module build (#203). The nested baseline build asked for ::serialkompatExtract, which Gradle rejects.
  • The baseline worktree checks out the resolved commit SHA, not the ref (#188). A ref that moved between resolution and checkout could cache the wrong schema under the old SHA.
  • The baseline snapshot cache is keyed by serialkompat version as well as commit SHA (#202). Upgrading serialkompat re-extracts the baseline instead of reusing one an older extractor produced.
  • Multi-module builds no longer run baseline extractions concurrently (#183). Under --parallel, each module raced the others on git worktree add and started its own nested Gradle daemon. The check tasks now take turns through a shared build service, while extraction and compilation stay parallel. The plugin is tested under --parallel, the configuration cache, and Gradle Isolated Projects.
  • The extractor now runs your @Serializable classes on your project's own runtime (#180). Previously the plugin's kotlinx-serialization and Kotlin stdlib came first on the classpath, so a project on a newer version had its serializers loaded against older runtimes. Your runtime classpath now comes first; the plugin's jars only fill gaps.
  • SUBTYPE_ADDED now credits a default deserializer on sealed bases, but only for tolerant readers (#181). Adding a subtype to a sealed base with an Unknown fallback was a false forward BREAK; it is now a WARN when the old reader has ignoreUnknownKeys. Without ignoreUnknownKeys the real decode throws, so it is now a BREAK (the #128 WARN was fail-open there). Both verdicts are backed by real round-trip oracles.
  • @EncodeDefault is now read from bytecode, so a field made optional can't fail open (#158, #176). Previously a field made optional with @EncodeDefault(NEVER) under encodeDefaults=true scored SAFE, yet old readers that still require it throw MissingFieldException. Now NEVER → BREAK, ALWAYS → SAFE, no annotation → follows encodeDefaults, and an unreadable mode (a hand-written serializer, or a snapshot recorded before this fix) → WARN under encodeDefaults=true, never SAFE. API: EncodeDefaultMode gains ABSENT, Element.encodeDefault == null now means "unknown", and Change.ElementOptionalityChanged gains newEncodeDefault.
  • Unresolved @Contextual elements are now an OPAQUE coverage gap instead of passing silently (#131). Their wire shape is chosen at runtime, so the gate can't see it; it used to report no change even when the real shape changed.
  • ENUM_VALUE_ADDED forward is now sound when the old reader uses coerceInputValues (#129). The old reader coerces an unknown value only into a field's default, so the verdict is WARN only when every baseline field reading that enum has a default. A required field, a List/Map use, or a top-level decode still throws, so those stay a BREAK.
  • List values containing whitespace no longer corrupt the snapshot file (#146). A jsonNames or enum value with a space, tab, or newline used to split apart or break the file on parse. parse(serialize(s)) == s now holds for every snapshot; whitespace-free output is byte-identical and older snapshots still parse. (Follow-up to #56.)
  • The gate fails closed on an empty baseline (#78). A baseline with zero contracts used to diff as "everything added → safe", hiding removals; serialkompatCheck now fails with a clear message. Set serialkompat { failOnEmptyBaseline } to false for first-time adoption with OPT_IN/OPT_OUT discovery, when the baseline ref has no @Serializable types yet.
  • Documentation reconciled with the shipped code (#87). The design doc's Gradle DSL and rule names now match the real SerialkompatExtension and Rules constants, and a phantom serialkompatDump task was removed. Version references were updated to Kotlin 2.4.0 and kotlinx-serialization 1.11.0.
  • More rules are now proven against the real library by round-trip oracles (#86). New coverage: numeric widening (Float→Double, Byte→Int), numeric narrowing (Long→Int, BREAK both ways), and non-null→nullable. All agree with the classifier.
  • Baselines survive crashes (#83). The snapshot cache now writes atomically, so a crash can't leave a half-written baseline that a later run trusts. A leftover git worktree from a killed run is cleaned up automatically instead of wedging every later run.
  • The CLI's --direction accepts the space-separated form (#85). --direction BACKWARD used to be ignored silently (falling back to FULL) and shifted the file arguments. Both --direction=VALUE and --direction VALUE now work, and a --direction with no value exits 2 with a usage error.
  • Escaping gaps closed in the snapshot file and JSON report (#84). The classDiscriminator and namingStrategy config values now round-trip exactly, avoiding spurious or missed config diffs. JsonReporter now escapes every control character, so a control character in a @SerialName no longer produces invalid JSON. Existing snapshots are byte-identical.
  • The GitHub composite action no longer interpolates untrusted inputs into scripts (#80). ${{ inputs.ref }}, inputs.task, inputs.gradle-args, and inputs.report-path were substituted into script bodies, which allowed shell/JS injection and could bypass the gate. All inputs now pass through env:, and the required pull-requests: write permission is documented.
  • One unresolvable type no longer aborts the whole extraction (#81). A single type that failed to load used to drop every type. Each type is now resolved independently, and a failure becomes an OPAQUE coverage gap (reported as a WARN) while the rest extract normally.
  • A custom JsonNamingStrategy is now recorded by its class name (#82). A strategy without its own toString() produced a different identity hash in each JVM, which caused a phantom CONFIG_NAMING_STRATEGY BREAK on every PR. Built-in strategies are unchanged.
  • Removing or renaming a field under a tolerant reader is now a WARN, not a false SAFE (#77). With ignoreUnknownKeys=true the old payload still decodes, but the removed field's data is silently lost. PROPERTY_REMOVED backward is now WARN for a tolerant reader and BREAK for a strict one, so a field rename (remove + add) now surfaces as one backward WARN instead of no findings at all.
  • The snapshot file now round-trips names that contain its delimiters. @SerialName, @JsonNames, and @JsonClassDiscriminator values may contain spaces, newlines, tabs, or backslashes, which used to corrupt the file. These names are now escaped; names without delimiters serialize byte-for-byte as before.
  • The CLI never crashes on bad input. A missing or malformed snapshot file, an unknown flag, or an invalid --direction now exits 2 with an error: message instead of a stack trace. Added --help/-h. Exit codes: 0 ok, 1 breaking, 2 usage/input error.
  • @JvmInline value classes are now recorded as their underlying wire type. A field typed UserId(val raw: Int) is recorded as kotlin.Int, so swapping a primitive for a wire-identical value class (or back) no longer produces a false BREAK. A value class that wraps a @Serializable type still checks that type.

Added

  • Seven more Json settings are captured and classified (#198): useArrayPolymorphism, allowStructuredMapKeys, allowSpecialFloatingPointValues, isLenient, decodeEnumsCaseInsensitive, allowTrailingComma, and allowComments. New rules CONFIG_ARRAY_POLYMORPHISM (BREAK both ways), CONFIG_STRUCTURED_MAP_KEYS (BREAK for the reader that lacks it), and CONFIG_SPECIAL_FLOATS (WARN for the reader that lacks it). The four reader-only flags join CONFIG_READER_STRICTNESS (WARN when tightened).
  • CLI --allow-empty-baseline (#187). It opts out of the new empty-baseline guard (see Changed) for first-time adoption.
  • serialkompatExtract is now build-cacheable and relocatable (#182). It restores FROM-CACHE on a clean build and from a checkout at another path. That includes the baseline git worktree, so with a shared or remote cache an already-built baseline commit needs no extraction JVM. The check tasks are intentionally never cached, because their verdict depends on the live baseline ref. See CI → "Build cache & configuration cache".
  • Round-trip oracles for the last rules without real-library proof (#119). DISCRIMINATOR_CHANGED, CONFIG_READER_STRICTNESS, CONFIG_COERCE_INPUT, DISCRIMINATOR_VALUE_CHANGED, CONFIG_CHANGED, and CONTRACT_REMOVED are now each backed by an oracle or a structural guard. The DISCRIMINATOR_VALUE_CHANGED oracle confirms its BREAK is a deliberate over-prediction.
  • Per-rule worked examples on the Rules docs page (#119). Each documented rule shows the change as a diff, the wire outcome with the real exception, and a link to the oracle test that proves the verdict. A checkRulesProof build gate fails CI if a proof link points at a missing test. Pilot rules: PROPERTY_ADDED, PROPERTY_REMOVED, ENUM_VALUE_ADDED.
  • Machine-readable report formats with a versioned JSON schema (#122). The JSON report carries schemaVersion ("1.0") so your tooling can depend on it. New formats: SARIF 2.1.0 (logical locations only, so no GitHub code-scanning upload) and GitHub Actions annotations (capped at 10 errors + 10 warnings, with a notice for the rest). Select formats in Gradle with serialkompat { reports { json { … }; sarif { … } } } (JSON on, SARIF off by default) or in the CLI with --format=console|json|sarif|github. The composite action also emits the capped annotations. See the "Report formats" docs page.
  • Gradual adoption with discovery modes and the new serialkompat-annotations module (#115). serialkompat { discovery } picks EXPLICIT (default: only types), OPT_OUT (every discovered type except @SerialkompatIgnore), or OPT_IN (only @SerialkompatChecked). The annotations are Kotlin Multiplatform, so you can mark types in KMP modules.
  • Generic @Serializable root types are now checked (#139). The fields of a generic envelope such as Page<T> are gated even when the type only appears as a root; its type parameters are treated as placeholders.
  • @Serializable discovery by scanning compiled class directories (#55, re-scoped). When no types are configured, the extractor finds roots in the classpath manifest plus a scan of compiled class files, without loading any classes. Generic classes are skipped as scan roots and logged by name, and unreadable class files become OPAQUE coverage gaps. This replaces the compiler-plugin producer originally planned in #55; KSP stays rejected (#22). Unblocks discovery modes (#115).
  • DISCRIMINATOR_COLLISION rule (#132). A subtype with a property whose JSON key equals the class discriminator can't be serialized at all, so the gate now flags it as a BREAK. It is skipped when classDiscriminatorMode = NONE.
  • baselineRef defaults to your default branch (#116). If you don't set it, the plugin tries origin/HEAD, then origin/main, origin/master, main, and master, and fails with a clear message if none exists.
  • Published-history checks in Gradle (#88, #121). serialkompatRecord records each release's schema into a committed history { dir } directory, and serialkompatCheckHistory checks the current schema against every recorded version. history { sinceVersion; depth; maxAge } bounds how far back that check reaches (#121).
  • Maven Central publishing (#24). serialkompat-core, serialkompat-extractor, serialkompat-gradle, and serialkompat-annotations publish under com.chrisjenx with the Gradle plugin id com.chrisjenx.serialkompat (moved off io.github.chrisjenx, which Sonatype refused). A Release workflow (manual dispatch from main with a version) checks the ref, publishing secrets, and version, then tests, publishes, tags, creates a GitHub release, and opens a PR bumping to the next -SNAPSHOT. A Snapshot workflow publishes -SNAPSHOTs on pushes to main; credentials live only in CI secrets. Gradle Plugin Portal publishing is still a follow-up, so resolve the plugin from Maven Central.
  • serialkompat { acceptedBreaks; renames } (#57). acceptedBreaks entries ("<serialName> <RULE> [DIRECTION]") are downgraded to acknowledged, so the gate logs them but doesn't fail. renames (old → new serial name) diffs a moved contract as a move instead of a remove + add.
  • Discovery from a classpath manifest. When no types are configured, the extractor reads type names from META-INF/serialkompat/serializable-types.txt (one per line; # comments and blank lines ignored). You can write it by hand or generate it; the class-dir scan (#55, above) is the built-in producer, not KSP (#22).
  • Persisted-data checks: TransitiveCompatibility.checkAgainstHistory checks the current schema against every published version, catching a break against an old version even when the latest looks fine. PublishedHistory is an append-only per-version snapshot store that refuses overwrites.
  • Dokka 2.0 API docs for core, extractor, and gradle, published to GitHub Pages by the Docs workflow on main.
  • serialkompat-cli: a standalone serialkompat diff <baseline> <current> for non-Gradle and cross-repo use, with --direction and --no-fail. It uses the same engine and report as the plugin. Exit codes: 0 ok, 1 breaking, 2 usage.
  • GitHub composite action (action.yml). It runs the gate, posts a sticky PR comment summarizing breaking, warning, and acknowledged findings, and fails the job on incompatible changes. The Gradle task itself stays CI-agnostic.
  • serialkompatCheckAgainst task: check against any ref with -Pserialkompat.ref=<ref>, falling back to the configured baselineRef.
  • Gradle plugin, end to end. serialkompatExtract dumps the current schema from your runtime classpath, and serialkompatCheck rebuilds the baseline from baselineRef and fails on unacknowledged breaks. The serialkompat { … } extension covers types, jsonInstance, baselineRef, direction, failOnBreaking, and include/exclude. Applying the plugin without types is a safe no-op.
  • CompatibilityEngine: the single entry point that scopes, diffs, classifies, and applies renames and accepted breaks to produce a Report. The Gradle task and the CLI share it.
  • Live git-ref baselines: GitRefBaseline rebuilds the baseline from the target ref's source in a throwaway worktree, so there is no committed baseline to go stale. Results are cached by commit SHA, and an unresolvable ref fails closed.
  • Scope and Coverage: include or exclude contracts by serial-name prefix (exclude wins). Excluded contracts are not checked, and reports don't list them yet.
  • Graceful degradation with ContractKind.OPAQUE. Anything the extractor can't analyse (an unknown SerialKind, an unresolved contextual or custom serializer, an extraction failure) becomes an explicit coverage gap, never an exception or a silent omission.
  • Rename and move tracking: the differ follows a declared renames entry as a ContractMoved and diffs the contents. A plain move is safe; a polymorphic move is a discriminator BREAK. A stale rename can't silently drop a contract.
  • Round-trip oracle test harness: encodes with one schema version, decodes with the other using real kotlinx-serialization, and asserts the classifier never misses a real break. The initial corpus covers field add/remove, nullability narrowing, type changes, numeric widening, and enum-value add.
  • Report, ConsoleReporter, JsonReporter, and AcceptedBreak: console and JSON rendering of findings, accepted breaks downgraded to acknowledged, and a shouldFail(floor) gate decision.
  • Json config changes are classified. Naming-strategy and discriminator changes are BREAK; tightening ignoreUnknownKeys or disabling encodeDefaults / coerceInputValues is WARN; loosening is safe.
  • JsonConfigReader: reads the wire-relevant settings from your real Json.configuration (naming strategy, discriminator, ignoreUnknownKeys, encodeDefaults, explicitNulls, coerceInputValues), so the gate uses your actual config instead of a copy.
  • Classifier, Finding, Rules, and CompatibilityProfile: apply the rule matrix to each change, per direction, using the reader and writer Json config. Only actionable (WARN/BREAK) findings are reported.
  • SnapshotExtractor and DescriptorSnapshotExtractor: walk the runtime SerialDescriptors to record serial names, optionality, nullability, @JsonNames, enum values, sealed subtypes, and SerializersModule-registered polymorphic subtypes.
  • SnapshotDiffer and the Change taxonomy: a deterministic structural diff of two snapshots. Reordering fields produces no change.
  • Snapshot model (Contract, Element, Subtype, SnapshotConfig, ContractKind, EncodeDefaultMode): the canonical, order-independent description of a JSON wire contract.
  • SnapshotFormat: a deterministic, sorted text format for snapshots that round-trips losslessly (parse(serialize(s)) == s).
  • Project scaffold: serialkompat-core, serialkompat-extractor, serialkompat-gradle modules.
  • Runtime descriptor reference extractor (reads element names and optionality from a SerialDescriptor).
  • SerialkompatPlugin registering serialkompatCheck and wiring it into the check lifecycle.
  • Tooling: Spotless (ktlint), Kover, binary-compatibility-validator, CI.
  • Design document (docs/design).

Changed

  • Sealed and polymorphic subtypes are identified by serial name and base, and reported as Base/sub (#201, #206). A @SerialName reused under two bases (OrderEvent/created, UserEvent/created) used to collapse into one contract and hide real breaks; each is now checked against its own base. Findings, reports, and Coverage.excluded show Base/sub. renames, acceptedBreaks, and include/exclude accept Base/sub (one base) or bare sub (every base), so existing bare entries keep working. Snapshot headers gain base=; older snapshots still parse and pair by bare name. A generic used with different type arguments (Page<A>/Page<B>) is still recorded as an OPAQUE coverage gap (#204).
  • A configured jsonInstance that can't be loaded now fails the extraction (#193). It used to print a warning and fall back to the default Json, silently checking every type under the wrong config and without the module's polymorphic registrations. Unset jsonInstance still uses the default. An open polymorphic base with no visible subtypes is now an OPAQUE coverage gap instead of an empty, falsely safe hierarchy.
  • The CLI fails closed on an empty baseline (#187). A baseline with 0 contracts against a non-empty current snapshot now prints an error: and exits 2, mirroring Gradle's failOnEmptyBaseline. --no-fail doesn't skip it; pass --allow-empty-baseline to opt out.
  • A contract that was OPAQUE in the baseline and is analysed now is one COVERAGE_GAP WARN, not a CONTRACT_REMOVED BREAK plus an add (#205). Analysed → OPAQUE is unchanged and stays loud.
  • Open polymorphic bases with a default deserializer are credited (#128). Adding a subtype to such a base is a forward WARN instead of a BREAK, because old readers decode the unknown subtype to the default. (#181 later also requires ignoreUnknownKeys on the old reader.)
  • Nullable fields under explicitNulls=false (#118). Adding a nullable field without a default is now backward SAFE, because the new reader decodes it as null. Removing one is forward WARN, because old readers silently see null. The default explicitNulls=true is unchanged (BREAK).
  • SnapshotFormat rebuilt on a document tree that drives both writing and parsing (#56). Output is byte-identical for every extractor-produced snapshot. Parsing is now stricter: a line that is invalid for its contract's kind is rejected instead of silently mis-read. A classDiscriminatorMode containing whitespace now round-trips; list values with whitespace were fixed separately (#146).

What's Changed

  • Build(deps): Bump actions/upload-artifact from 4 to 7 by @dependabot[bot] in #1
  • Build(deps): Bump gradle/actions from 4 to 6 by @dependabot[bot] in #2
  • Build(deps): Bump actions/checkout from 4 to 7 by @dependabot[bot] in #3
  • Build(deps): Bump actions/setup-java from 4 to 5 by @dependabot[bot] in #4
  • Build(deps): Bump the kotlin group with 5 updates by @dependabot[bot] in #31
  • Build(deps): Bump com.diffplug.spotless from 7.0.2 to 8.8.0 by @dependabot[bot] in #32
  • feat(core): Snapshot model + canonical text (de)serialization (#5) by @chrisjenx in #35
  • feat(core): structural differ + Change taxonomy (#9) by @chrisjenx in #36
  • spike(extractor): validate SerialDescriptor fidelity — vendor the walk (#6) by @chrisjenx in #37
  • feat(extractor): vendored SerialDescriptor → Snapshot walk (#7) by @chrisjenx in #38
  • feat(core): classifier + full rule matrix (#11) by @chrisjenx in #39
  • Build(deps): Bump org.jetbrains.dokka from 2.0.0 to 2.2.0 by @dependabot[bot] in #34
  • Build(deps): Bump org.junit.jupiter:junit-jupiter-engine from 6.0.1 to 6.1.1 by @dependabot[bot] in #33
  • feat(extractor): read Json.configuration into SnapshotConfig (#8) by @chrisjenx in #40
  • feat(core): classify Json config changes (#13) by @chrisjenx in #41
  • feat(core): report rendering + accepted-breaks model (#14) by @chrisjenx in #42
  • test(extractor): round-trip oracle validates the rule matrix (#10) by @chrisjenx in #43
  • feat(core): type rename/move tracking (#12) by @chrisjenx in #44
  • feat: graceful degradation — never crash, opaque coverage gaps (#18) by @chrisjenx in #45
  • feat(core): scope + coverage invariant (#15) by @chrisjenx in #46
  • feat(gradle): git-ref-live baseline — worktree + SHA cache, fail-closed (#16) by @chrisjenx in #47
  • feat: end-to-end engine + Gradle wire-up (serialkompatExtract/Check) (#17) by @chrisjenx in #48
  • feat(gradle): serialkompatCheckAgainst ad-hoc ref check (#20) by @chrisjenx in #49
  • feat(ci): unified GitHub Action + sticky PR comment (#19) by @chrisjenx in #58
  • feat(cli): standalone snapshot-diff CLI (#23) by @chrisjenx in #51
  • docs: Dokka 2.0 multi-module API docs + GitHub Pages (#25) by @chrisjenx in #52
  • feat: append-only published history + transitive checks (#21) by @chrisjenx in #53
  • feat: producer-agnostic @serializable discovery contract; revert KSP (#22) by @chrisjenx in #54
  • fix(ci): let the Docs workflow enable GitHub Pages itself by @chrisjenx in #59
  • fix(extractor): unwrap @JvmInline value classes to underlying wire type by @chrisjenx in #61
  • fix(cli): controlled exit on bad input instead of crashing by @chrisjenx in #62
  • test(gradle): red-bar E2E for serialkompatCheck incl. configuration cache by @chrisjenx in #63
  • fix(core): escape name fields in the snapshot codec by @chrisjenx in #64
  • test(core): fill classifier + differ matrix gaps by @chrisjenx in #65
  • test(oracle): back config rules with real round-trips + data-loss detection by @chrisjenx in #66
  • test(ci): lock the JsonReporter to action.yml JSON contract by @chrisjenx in #67
  • test(core): pin transitive dedup worst-case + cross-version accepted breaks by @chrisjenx in #68
  • test(extractor): breadth — generics, custom serializers, naming, discriminator by @chrisjenx in #69
  • feat(gradle): expose acceptedBreaks + renames on the extension (closes #57) by @chrisjenx in #70
  • Build(deps): Bump actions/github-script from 7 to 9 by @dependabot[bot] in #75
  • Build(deps): Bump actions/checkout from 4 to 7 by @dependabot[bot] in #72
  • Build(deps): Bump gradle/actions from 4 to 6 by @dependabot[bot] in #74
  • Build(deps): Bump actions/deploy-pages from 4 to 5 by @dependabot[bot] in #73
  • Build(deps): Bump actions/upload-pages-artifact from 3 to 5 by @dependabot[bot] in #71
  • chore: stop tracking .claude/ local state (#79) by @chrisjenx in #89
  • fix(core): silent data-loss on field removal/rename is a WARN, not a false SAFE (#77) by @chrisjenx in #90
  • fix(extractor): record a custom JsonNamingStrategy by a stable class identity (#82) by @chrisjenx in #91
  • fix(extractor): one unresolvable type degrades to OPAQUE instead of aborting extraction (#81) by @chrisjenx in #92
  • fix(action): pass inputs via env, never interpolate them into script bodies (#80) by @chrisjenx in #93
  • fix(core): complete hand-rolled serialization escaping — codec config values + reporter control chars (#84) by @chrisjenx in #94
  • fix(cli): support space-form --direction and reject a valueless one (#85) by @chrisjenx in #95
  • fix(gradle): baseline durability — atomic cache writes + stale-worktree self-healing (#83) by @chrisjenx in #96
  • test(oracle): backfill round-trip coverage for fail-open-prone rules (#86) by @chrisjenx in #97
  • docs: reconcile design/README/CLAUDE with the shipped code; archive stale plan (#87) by @chrisjenx in #98
  • fix(gradle): fail closed on a degenerate (zero-contract) baseline (#78) by @chrisjenx in #99
  • feat(publish): Maven Central publishing via vanniktech, mirroring compose2pdf (#24) by @chrisjenx in #100
  • fix(publish): move Maven coordinates to the verified com.chrisjenx namespace (#24) by @chrisjenx in #101
  • chore(security): add gitleaks secret-scanning gate (CI + pre-commit) by @chrisjenx in #102
  • chore(ci): bump lagging workflow action versions to current majors by @chrisjenx in #103
  • docs(site): MkDocs Material skeleton + Pages assembly + strict CI by @chrisjenx in #108
  • docs(site): content — pages, animated pipeline, rule matrix, diagrams by @chrisjenx in #112
  • docs(site): sync gates — checkRulesDoc completeness gate + version templating by @chrisjenx in #111
  • docs: refresh README to point at the docs site by @chrisjenx in #113
  • docs(site): fix home pipeline layout + stale version warnings by @chrisjenx in #114
  • feat(gradle): auto-detect the default branch when baselineRef is unset by @chrisjenx in #124
  • ci(release): floating major tag + Marketplace listing docs by @chrisjenx in #125
  • feat(gradle): wire published-history transitive check (#88) by @chrisjenx in #126
  • feat(gradle): history retention — bound the transitive horizon (#121) by @chrisjenx in #127
  • test(extractor): real-world coverage audit (#123) by @chrisjenx in #130
  • fix(extractor): surface unresolved @contextual as an OPAQUE coverage gap (#131) by @chrisjenx in #133
  • feat(core): flag classDiscriminator / subtype-property collisions statically (#132) by @chrisjenx in #134
  • feat(gradle): capture open-polymorphism defaultDeserializer tolerance (#128) by @chrisjenx in #135
  • feat(core): make the enum coerce-fallback verdict sound and field-aware (#129) by @chrisjenx in #136
  • feat(core): nullability-aware PROPERTY_ADDED / PROPERTY_REMOVED verdicts (#118) by @chrisjenx in #137
  • feat(extractor): @serializable discovery via class-dir scanning (#55) by @chrisjenx in #138
  • chore: gitignore superpowers scratch (specs/plans/SDD workspace) by @chrisjenx in #140
  • refactor(extractor): name the JVMS class-file constants in the scanner by @chrisjenx in #141
  • feat: discovery modes + serialkompat-annotations + KMP support (#115) by @chrisjenx in #142
  • Build(deps): Bump com.vanniktech.maven.publish from 0.34.0 to 0.37.0 by @dependabot[bot] in #144
  • Build(deps): Bump actions/configure-pages from 5 to 6 by @dependabot[bot] in #143
  • feat(#139): generic-root shape analysis — gate envelope fields of root-only generic @serializable types by @chrisjenx in #145
  • refactor(core): FormatDoc AST codec replacing the hand-rolled SnapshotFormat (#56) by @chrisjenx in #147
  • fix(core): escape whitespace in SnapshotFormat list values (#146) by @chrisjenx in #148
  • feat(core): machine-readable report reporters — JSON schemaVersion, SARIF, GitHub annotations (#122 PR 1) by @chrisjenx in #149
  • feat: report format selection — Gradle reports {} DSL + CLI --format (#122 PR 2) by @chrisjenx in #150
  • feat: action annotations + report-formats docs (#122 PR 3) by @chrisjenx in #151
  • docs: surface report formats across the site (#122 follow-up) by @chrisjenx in #152
  • docs: buf-style show-don't-tell hero on the landing page by @chrisjenx in #153
  • docs: worked examples for every report format by @chrisjenx in #154
  • Build(deps): Bump the kotlin group with 3 updates by @dependabot[bot] in #155
  • Build(deps): Bump org.junit.jupiter:junit-jupiter-engine from 6.1.1 to 6.1.2 by @dependabot[bot] in #156
  • docs: rules-matrix proof — worked examples + oracle-test proof links (pilot) by @chrisjenx in #157
  • Build(deps): Bump actions/setup-python from 6 to 7 by @dependabot[bot] in #160
  • Build(deps): Bump org.jetbrains.kotlinx.kover from 0.9.8 to 0.9.9 in the kotlin group by @dependabot[bot] in #161
  • Build(deps): Bump com.diffplug.spotless from 8.8.0 to 8.9.0 by @dependabot[bot] in #163
  • Build(deps): Bump org.junit.jupiter:junit-jupiter-engine from 6.1.2 to 6.1.3 by @dependabot[bot] in #165
  • ci: fix Gradle wrapper updates + bump to 9.7.0 by @chrisjenx in #166
  • test(#119): backfill real-library oracles for the last 6 unproven rules (PR 2a) by @chrisjenx in #162
  • Build(deps): Bump com.diffplug.spotless from 8.9.0 to 8.10.0 by @dependabot[bot] in #168
  • Build(deps): Bump actions/setup-java from 5 to 6 by @dependabot[bot] in #170
  • Build(deps): Bump com.diffplug.spotless from 8.10.0 to 8.10.1 by @dependabot[bot] in #169
  • Build(deps): Bump com.diffplug.spotless from 8.10.1 to 8.10.2 by @dependabot[bot] in #172
  • Build(deps): Bump org.jetbrains.kotlinx.binary-compatibility-validator from 0.18.1 to 0.18.2 in the kotlin group across 1 directory by @dependabot[bot] in #171
  • Build(deps): Bump the kotlin group with 3 updates by @dependabot[bot] in #173
  • Build(deps): Bump com.diffplug.spotless from 8.10.2 to 8.10.3 by @dependabot[bot] in #174
  • ci(release): make the Release workflow safe to run for the first time by @chrisjenx in #175
  • fix(#158): recover @EncodeDefault so a NEVER field cannot read as SAFE by @chrisjenx in #176
  • build(deps): Gradle 9.7.0 -> 9.8.0, ktlint 1.5.0 -> 1.8.0 by @chrisjenx in #178
  • ci(wrapper): replace the failing wrapper action with a script; fix bot labels by @chrisjenx in #177
  • fix(gradle): let the project's runtime win on the extractor classpath by @chrisjenx in #180
  • fix(rules): credit sealed-base default deserializers; require reader tolerance by @chrisjenx in #181
  • feat(gradle): make serialkompatExtract build-cacheable and relocatable by @chrisjenx in #182
  • fix(gradle): serialize baseline extraction across modules; cover Isolated Projects by @chrisjenx in #183
  • Build(deps): Bump org.jetbrains.kotlinx.kover from 0.9.9 to 0.9.11 in the kotlin group by @dependabot[bot] in #179
  • fix(core): drop Unit-only when branches that warn UNUSED_EXPRESSION by @chrisjenx in #184
  • fix(cli): fail closed on an empty baseline (--allow-empty-baseline opt-out) by @chrisjenx in #187
  • fix(action): paginate sticky-comment lookup; comment verdict follows exit code by @chrisjenx in #189
  • fix(gradle): check out the resolved SHA for the baseline worktree by @chrisjenx in #188
  • fix(gradle): serialkompatCheck fails in every single-module project (root task path) by @chrisjenx in #203
  • fix(extractor): degrade per-type Errors to OPAQUE instead of aborting the walk by @chrisjenx in #190
  • fix(core): classify envelope changes around generic holes by @chrisjenx in #192
  • fix(core): judge enum coercibility over the unscoped baseline by @chrisjenx in #194
  • fix(extractor): open base with no visible subtypes is OPAQUE; fail on unloadable jsonInstance by @chrisjenx in #193
  • fix(extractor): keep a value class's nullable underlying value in the type ref by @chrisjenx in #199
  • fix(extractor): record a serial name with conflicting shapes as OPAQUE by @chrisjenx in #201
  • fix(gradle): resolve build-dir paths lazily so a relocated buildDirectory is honored by @chrisjenx in #195
  • fix(gradle): read the record version lazily and validate it at execution by @chrisjenx in #196
  • fix(core): capture and classify the remaining wire-relevant Json flags by @chrisjenx in #198
  • fix(gradle): key the baseline snapshot cache by serialkompat version by @chrisjenx in #202
  • fix(core): baseline-OPAQUE contract now analysed is a coverage gap, not a removal by @chrisjenx in #205
  • feat(core,extractor): base-qualified contract identity for sealed subtypes (#200) by @chrisjenx in #206
  • docs: rewrite for first-time readers and fix stale facts by @chrisjenx in #185
  • ci(release): take release notes from CHANGELOG; document the release flow by @chrisjenx in #207

New Contributors

Full Changelog: https://github.com/chrisjenx/serialkompat/commits/v0.1.0