Skip to content

0.6.0

Latest

Choose a tag to compare

@tatejennings tatejennings released this 12 Jul 06:09

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 the withOverrides builder 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, and unimplemented() cross-module proxies trapped at launch. Overrides are now keyed by a task-scoped, type-matched capture: registering evaluates the property once and the matching provide call reports the exact key used at resolution, independent of build settings. On ≤ 0.5.1 without upgrading: set STRIP_STYLE = debugging on the app target (verified workaround).
  • override(\.x) on a property registered with an explicit key: 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.x then override(\.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 overrides backing container-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 test cannot cover this failure mode. Verified to fail against the pre-fix implementation.

Full details in CHANGELOG.md and PR #2.

🤖 Generated with Claude Code