Fixes a critical bug where KeyPath overrides were silently mis-keyed in symbol-stripped (TestFlight/App Store) builds, crashing consumer apps at launch. All public API signatures are unchanged, but override behavior changes in documented ways — released as a minor bump per this project's 0.x policy.
Fixed
- Stripped-build override mis-keying (launch crash).
override(_:with:),removeOverride(for:), and thewithOverridesbuilder derived their storage key by parsing the KeyPath's description, which degrades to<computed 0x… (Type)>when archive builds strip symbols — Xcode Debug and Release runs were unaffected, which is why the bug only appeared in TestFlight. Overrides were stored under a key that could never match, resolution fell through to the real factory, andunimplemented()cross-module proxies trapped at launch. Overrides are now keyed by a task-scoped, type-matched capture: registering evaluates the property once and the matchingprovidecall reports the exact key used at resolution, independent of build settings. On ≤ 0.5.1 without upgrading: setSTRIP_STYLE = debuggingon the app target (verified workaround). override(\.x)on a property registered with an explicitkey:argument now works (previously mis-keyed silently for the same reason).
Behavior changes
- First registration per KeyPath runs the override factory once (key-discovery probe; value discarded — overrides remain uncached). Composition-root wiring closures therefore execute at wiring time: prefer capturing an already-resolved real (
let x = owner.xthenoverride(\.x) { x }), and wire cross-dependent proxies in dependency order. Test spies should expect 1 + resolutions invocations. - Overriding a non-provide-backed property traps in all build configurations with a descriptive message naming the container (previously a silent dead override). A same-type alias (
var alias: any P { backing }) does not trap — it is discovered as its backing registration, so overriding the alias overridesbackingcontainer-wide. removeOverride(for:)matches by KeyPath identity (including root type); removal for a never-registered KeyPath is a documented no-op.
Internals
- Key discovery runs no user code while the container lock is held, resolution's hot path stays at a single lock acquisition, and nested registration (an override factory registering another override) is supported.
Sources/Forge/Internal/KeyPathName.swift(description parsing) is deleted, along with its format-stability CI safeguard.
CI / Infrastructure
- New stripped-binary regression job: builds a release executable, strips it the way an archive would, and asserts the KeyPath override APIs still resolve — regular test bundles are never stripped, so
swift testcannot cover this failure mode. Verified to fail against the pre-fix implementation.
Full details in CHANGELOG.md and PR #2.
🤖 Generated with Claude Code