diff --git a/CHANGELOG.md b/CHANGELOG.md index d09e7f699..9eb529625 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Registry plugins built before 0.49.0 install and load again instead of failing with an invalid plugin bundle error. + ## [0.49.0] - 2026-06-06 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index db6bf2a64..e10e1ca06 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -104,6 +104,8 @@ When adding a new method to the driver protocol: add to `PluginDatabaseDriver` ( **Additive changes are binary-compatible and need NO version bump**: adding a requirement to `DriverPlugin` / `PluginDatabaseDriver` that has a default implementation, reordering requirements, adding a field to a non-`@frozen` transfer struct, or removing a requirement that defaulted to `nil`. +**Adding a field to a transfer struct is additive ONLY if every existing public initializer keeps its exact signature.** Adding a parameter to an existing public init or function, even with a default value, replaces its mangled symbol and breaks every already-built plugin (this shipped in 0.49.0: `PluginQueryResult` gained `columnMeta:` on its init and every registry plugin failed to load with "Bundle failed to load executable"). Add a NEW overload for the new field and keep the old signature; mark the old overload `@_disfavoredOverload` so new code resolves to the full init while old binaries keep their symbol. A red PluginKit ABI Gate is a merge blocker, never something to merge over: either the diff is additive (verify no symbol disappeared, then add the `abi-additive` label) or it is breaking (bump and re-release). + **Bump `currentPluginKitVersion` (in `PluginManager.swift`) and `TableProPluginKitVersion` in every plugin `Info.plist` ONLY for a breaking change**: changing or removing an existing requirement's signature, adding a requirement without a default, adding a case to a `@frozen` enum, or changing a frozen type's layout. Mark a public enum `@frozen` only when an exhaustive switch over it forces it (the compiler flags the switch) and its case set is genuinely closed; leave the rest non-frozen so they can gain cases. `PluginCapability` stays non-frozen with `@unknown default` because it is a growing capability set, not a closed vocabulary. The driver protocols and transfer structs stay non-frozen so they can grow. The strict version gate in `validateBundleVersions` still rejects a stale plugin cleanly after a breaking bump (no `EXC_BAD_INSTRUCTION`). **ABI gate**: `scripts/check-pluginkit-abi.sh [base-ref]` builds TableProPluginKit at the current tree and at the base ref with the same toolchain, then diffs their public interfaces. There is no committed baseline, so a Swift version difference between a dev machine and CI never produces a false diff. CI (`.github/workflows/pluginkit-abi.yml`) runs it on every PR that touches `Plugins/TableProPluginKit/**`, comparing against the PR base. A reported diff is a real ABI change: additive needs no bump; breaking needs the version bump above plus `release-all-plugins.sh`. After reviewing a diff as additive, add the `abi-additive` label to the PR; the gate then passes and records the decision. Remove the label if later commits add a breaking change. (Until Library Evolution is on the base too, the base emits no interface and the gate passes as a bootstrap.) diff --git a/Plugins/TableProPluginKit/PluginQueryResult.swift b/Plugins/TableProPluginKit/PluginQueryResult.swift index 474a16629..d2d69ad1a 100644 --- a/Plugins/TableProPluginKit/PluginQueryResult.swift +++ b/Plugins/TableProPluginKit/PluginQueryResult.swift @@ -30,6 +30,28 @@ public struct PluginQueryResult: Codable, Sendable { self.columnMeta = columnMeta } + @_disfavoredOverload + public init( + columns: [String], + columnTypeNames: [String], + rows: [[PluginCellValue]], + rowsAffected: Int, + executionTime: TimeInterval, + isTruncated: Bool, + statusMessage: String? + ) { + self.init( + columns: columns, + columnTypeNames: columnTypeNames, + rows: rows, + rowsAffected: rowsAffected, + executionTime: executionTime, + isTruncated: isTruncated, + statusMessage: statusMessage, + columnMeta: nil + ) + } + public static let empty = PluginQueryResult( columns: [], columnTypeNames: [],