This release rebaselines the current InnoFlow implementation, documentation, and release-readiness
gates as the 4.0.0 public contract. It does not require source migration from the current public
APIs.
Release Readiness
- Updated README, localized README, architecture contract, migration notes, release notes, releasing
checklist, and doc-parity metadata to describe the 4.0.0 contract surface. - Clarified that fixed-arity
SelectedStoreselection covers one through six explicit state slices,
select(dependingOnAll:)covers larger explicit sets, and closure selection remains the
always-refresh fallback. - Strengthened release-sync and principle gates so localized install snippets, release target docs,
localized selection guidance, and throwing.runMarkdown snippets cannot drift silently.
Fixed
- The phase totality diagnostic no longer subclasses
SyntaxVisitor. The diagnostic now walks
Syntaxrecursively, avoiding a SwiftSyntax internal visitor symbol link failure that surfaced
when the canonical sample package built the macro tool through the path dependency graph. - Phase-managed totality diagnostics now count only the
PhaseMapDSL locations that actually
declare graph coverage (From(...),On(..., to:), andOn(..., targets:)). Unrelated member
accesses inside the phase-map body no longer hide a missingPhasecase warning. - Store effect cancellation now also tracks non-awaited
.merge/.concatenatewrapper tasks.
cancelAllEffects()and scoped cancellation can tear down composite wrappers even when the
wrapper itself was not made explicitly.cancellable(...), preventing child sends from escaping
after the store boundary has been cancelled. - Nested
.cancellable(...)wrappers now preserve every active cancellation boundary internally.
Cancelling an outer id reaches already-started inner run tasks in bothStoreandTestStore,
rather than only checking the innermost id. EffectRuntime.finish(token:)is now idempotent for cancellation races. A token that is cancelled
while in flight is still counted as finished once when its task unwinds, but duplicate finish
calls no longer inflate runtime instrumentation counters.swift build -c releasenow succeeds on the current Swift 6.3 toolchain. The SILEarlyPerfInlinerpreviously segfaulted inisCallerAndCalleeLayoutConstraintsCompatiblewhile scanningStore.deinitandTestStore.deinitfor inlining candidates. Both deinits are now annotated with@_optimize(none), which sidesteps the crash without changing@MainActor isolated deinitsemantics or the public API. Deinit is not a hot path, so the localized optimization loss is negligible. Upstream tracker: swiftlang/swift#88173 (and adjacent #87077 / #87736 / #87462). A minimal in-tree reproducer lives atRepro/SILCrashRepro/with the full crash dump inRepro/SILCrashRepro/CRASH.txt; retest on toolchain bumps to retire the workaround.swift test -c releasenow passes the full InnoFlow test suite (212 tests). Five crash-contract subprocess tests (StaleScope*CrashContractTests,PhaseMap*CrashContractTests) previously failed in release becauserunStaleScopedStoreHarnessandrunPhaseMapCrashHarnesslinked against enumerated.build/**/InnoFlow.build/*.oobject files — which pulls in whichever configuration happened to be in.build/. When the outer runner was invoked with-c release, the harness picked up release-optimized objects whereassertionFailurehad been elided, so the subprocess could not abort and the crash contract could not be verified. Both harnesses now inline-compile InnoFlow sources with-Onone+-package-name InnoFlowvia the existinginnoFlowCoreSourcePathshelper, matching the pattern already used by the release-harness counterparts and removing any dependency on the outer test-runner's build configuration.- Five timing-sensitive tests (
effectContextUsesStoreClock,effectContextCheckCancellationPassesWhileActive,effectContextCheckCancellationThrowsAfterCancelEffects,storeCombinatorComposition, andmanualTestClockResumesSameDeadlineSleepersInInsertionOrder) previously asserted state after a fixed number ofawait Task.yield()calls. That count was sufficient in debug but not in release — release-mode WMO eliminates some scheduling boundaries, and the remaining actor hops insideStore.executeEffect → Task { walker.walk → driver.startRun → inner Task { gate.wait }}still need scheduler turns. The tests now poll for the observable outcome (e.g.,store.throttled == [1],await probe.started == 1,await clock.sleeperCount == N), which is the idiomatic pattern for async-effect tests and was already used for the_completedprobe flows elsewhere in the suite. TheStore.send(_:)scheduling contract is now documented inARCHITECTURE_CONTRACT.mdunder "Store.send(_:) scheduling contract".
Added
ScopedStore.select(dependingOnAll:)now mirrorsStore.select(dependingOnAll:)for child-state
projections with more than six explicit dependencies. The overload preserves selective
invalidation for large derived read models without falling back to always-refresh selection.assertPhaseMapCovers(...)inInnoFlowTestingrecords a test failure when an opt-in
PhaseMap.validationReport(expectedTriggersByPhase:)has missing triggers, while still
returning the structured report for additional assertions.docs/MIGRATION_3_1.md,docs/INSTRUMENTATION_COOKBOOK.md,
docs/PERFORMANCE_BASELINES.md, anddocs/FRAMEWORK_COMPARISON.mddocument the 3.1 adoption
path, instrumentation adapters, maintainer baseline policy, and adjacent-library positioning.ScopedStore.isAlive/ScopedStore.optionalStateand
SelectedStore.isAlive/SelectedStore.optionalValue— explicit
lifecycle accessors for the projection-lifecycle race documented in
ARCHITECTURE_CONTRACT.md.ScopedStore.statekeeps the cached snapshot
fallback for SwiftUI observer reads, whileSelectedStore.valueis removed
andSelectedStorelive reads useoptionalValue(nilwhen dead) or
requireAlive()(release-modepreconditionFailurewhen dead). The
accessors expose the same lifecycle signal in a release-tolerant form for
call sites that prefer to branch on liveness. Backed by four new@Test
cases verifying both the live-parent and released-parent paths.CONTRIBUTING.mdnow documents the intentional package layout: core lives in the rootPackage.swift, the canonical sample lives inExamples/InnoFlowSampleApp/InnoFlowSampleAppPackage, and the SIL inliner reproducer lives inRepro/SILCrashRepro. Consumers depend only on the core package, so sample- or reproducer-only changes do not invalidate consumer build caches.Tests/InnoFlowTests/PhaseMapPerfTests.swift— opt-in (INNOFLOW_PERF_BENCHMARKS=1) dispatch benchmark forPhaseMapcovering small (4 phases × 3 transitions), medium (16 × 5), large (64 × 5), and worst-case (last-of-5 match in a 64-phase ring) FSM fixtures. Establishes baseline numbers so a future per-phase transition index — which would require aHashableconstraint onActionand an opt-inPhaseMapshape — can be evaluated against measurement instead of intuition. ThePhaseMapdoc comment now records the per-action complexity (O(1)phase lookup + linear walk over the matched phase's transitions) and explains why the index work is intentionally deferred until real workloads demand it.StoreInstrumentation.signpost(signposter:name:includeActions:)— new instrumentation factory that bridges store run lifecycle toOSSignposter. EachrunStartedevent opens an Instruments interval signpost identified by the run's UUID token, the matchingrunFinishedevent closes it, and action emissions / drops / cancellations are surfaced asemitEventsignposts on the same name so they appear inline in Instruments' timeline. Token, sequence, and cancellation identifiers are included in signpost messages, while action payloads are redacted by default unlessincludeActions: trueis passed. Pairs cleanly with.osLog(logger:)through.combined(...). Backed by a new@Testcase verifying that signpost-instrumented stores preserve runtime behavior on the canonical async load path.ARCHITECTURE_CONTRACT.mdlists.signpostalongside.sink/.osLog/.combinedas official instrumentation surfaces.Store.select(dependingOnAll:)— Swift parameter-pack-based selection that declares an arbitrary number of explicit dependency key paths and projects them into a derived value. Lifts the previous six-field ceiling onselect(dependingOn:)for projections that legitimately depend on more state slices, without forcing the closure-onlyselect(_:)form's.alwaysRefreshre-evaluation on every parent action. Uses a distinctdependingOnAll:argument label rather than overloading the existingdependingOn:so the fixed-arity tuple overloads remain unambiguous at call sites. Backed by a new@Testcase driving an eight-field selection through tracked- and untracked-mutation paths and asserting that the projection only refreshes when a declared dependency changes.@InnoFlow(phaseManaged: true)— phase-managed variant of the macro that requires the type to provide a staticphaseMapdeclaration and automatically wraps the synthesizedreduce(into:action:)in.phaseMap(Self.phaseMap). Authors no longer have to remember to call.phaseMap(Self.phaseMap)insidebody; forgetting the staticphaseMapbecomes a compile-time error. A boolean marker (phaseManaged: true) is used instead of aWritableKeyPathargument because a keypath argument back into the same type would create a self-referential macro-attribute cycle; the actual phase key path lives inside the staticphaseMapvalue where it belongs. Existing@InnoFlow(no-arg) features continue to work unchanged — they keep authoring.phaseMap(map)explicitly insidebody. Backed by a new@Testcase verifying both the auto-apply path and the no-op-on-undeclared-action contract through the synthesized reducer.- Compile-time phase totality diagnostic for
@InnoFlow(phaseManaged: true). The macro now collects the case names of the nestedPhaseenum (top-level or insideState) and walks the staticphaseMapgetter body forMemberAccessExprSyntaxreferences; any Phase case that never appears in the phaseMap surfaces awarninganchored on the enum case declaration. Catches the declared-but-unwired authoring hazard at compile time without changing the runtime partial-by-default contract documented inADR-phase-map-totality-validation.md. The newdocs/adr/ADR-compile-time-phase-totality.mdrecords the analysis-vs-reachability trade-off and why graph-based reachability stays out of scope for this layer. Backed by a new@TestinInnoFlowMacrosTeststhat drives the diagnostic against a fixture with an.orphanPhase case and asserts both the warning surface and the synthesized reducer expansion. docs/adr/ADR-no-builtin-di-container.md,docs/adr/ADR-post-reduce-vs-pre-reduce-phase.md,docs/adr/ADR-reducer-sendable-policy.md— ADRs recording the trade-off analyses behind three previously-implicit framework decisions: why InnoFlow ships construction-timeDependenciesbundles instead of a runtime resolver, whyPhaseMapruns as a post-reduce decorator instead of a pre-reduce action filter, and whyState/Action/ effect payloads must beSendablewhile theReducerprotocol itself does not.EffectTimingRecorder(InnoFlowTesting) — a test-only actor that capturesStoreInstrumentationevents (run lifecycle, action emission, cancellation) with monotonic nanosecond timestamps. Passrecorder.instrumentation()toStore(reducer:..., instrumentation:)to record a run;recorder.entries()returns the captured timeline andrecorder.dumpJSONL(to:)serialises it as newline-delimited JSON for offline comparison. Timestamps are captured synchronously inside each instrumentation callback (not inside the async append hop) so measured run durations stay faithful to theStoreevent order even under scheduler contention.scripts/compare-effect-timings.sh— relative p95 / mean comparison between a baseline JSONL and a fresh recorder dump. Exit1means a real metric regression; exit2means malformed JSONL, incomplete capture, usage error, or missing dependency. Purebash+jq.scripts/report-effect-timing-trend.sh— non-blocking trend reporter that captures a fresh JSONL (or consumes an existing capture) and prints both mean and p95 deltas against the committed baseline. Metric regressions stay non-blocking, but malformed data and capture failures now fail loudly instead of being reported as ordinary slowdowns.Tests/InnoFlowTests/Fixtures/EffectTimings.baseline.jsonl— committed baseline distribution (10 matched runs) that the newEffectTimingBaselineGatesuite compares against under principle-gates' release-mode gate. The gate opts in viaINNOFLOW_CHECK_EFFECT_BASELINE=1so localswift testruns remain silent while CI blocks malformed or incomplete timing captures and reports metric regressions as non-blocking trend output.Tests/InnoFlowTests/Fixtures/EffectTimings.baseline.meta.json— maintainer metadata that records the gate's fixed workload size, metric, tolerance, and refresh posture so baseline regeneration stays decision-complete when runners or toolchains change.store.binding(_:to:)— argument-label alias forstore.binding(_:send:)that reads naturally when passing an enum case constructor, as instore.binding(\.$step, to: Feature.Action.setStep). The two labeled spellings are semantically identical and both remain supported onStoreandScopedStore. New code should use an explicitto:orsend:label; the older unlabeled closure spelling is retained only as a source-compatibility shim for already-written call sites, not as the documented authoring surface.ManualTestClock.sleeperCount— test-only observable for the number of sleepers currently suspended on the clock. Tests that need to confirm.run/.debounce/.throttleeffects have reached theirtry await clock.sleep(...)registration before callingadvance(by:)can poll this instead of relying on a fixed yield count.- Four new canonical sample demos in
Examples/InnoFlowSampleAppcovering the domains flagged as missing by the competitive analysis:AuthenticationFlowDemo— multi-step credentials + MFA flow modeled withPhaseMap(idle → credentials → submitting → mfaRequired → submittingMFA → authenticated / failed) plus a.cancellable("auth-submit", cancelInFlight: true)effect for cancel-and-retry.ListDetailPaginationDemo— paginated list + per-row child reducer + detail scope. UsesForEachReducer(state:action:reducer:)for row state,scope(collection:action:)for list rendering, andAction.articleActionPathfor the generated collection action path. Intentionally phase-light to contrastAuthenticationFlowDemo.OfflineFirstDemo— optimistic local update + debounced save + server-side rollback. Uses.cancellable("offline-save-debounce", cancelInFlight: true)to collapse consecutive edits and_saveConfirmed/_saveRolledBack(previous:reason:)actions to reconcile with repository truth.RealtimeStreamDemo— looping.runsubscription driven by the injectedtickInterval: Durationdependency andcontext.sleep. Tests swap inManualTestClockto advance time deterministically and pollsleeperCountinstead of sleeping on wall clock.
- Ten new
@Testcases inInnoFlowSampleAppFeatureTeststhat exercise each new sample throughTestStore— happy paths, failure / retry paths, collection-scoped row actions, debounced-save confirmation vs. rollback, and clock-driven tick receipts.
Changed
- The canonical
Phase-Driven FSMsample now uses@InnoFlow(phaseManaged: true), making the
sample app demonstrate the preferred phase-managed authoring path instead of the legacy explicit
.phaseMap(Self.phaseMap)wrapper. StoreandTestStorenow share a single cancellation-boundary implementation, keeping run
sequencing, cancel-in-flight, and cancel-all semantics aligned between production and test
runtimes.ForEachReducernow mutates the matched collection element in place instead of copying the whole
collection before writing the result back.- Collection-scoped store caching now prunes stale IDs by revision, avoiding the per-refresh live-ID
set and dictionary filter pass while preserving stale-scope cleanup behavior. - Tag-triggered release gates now run the principle gate with release-tag enforcement enabled, and
successful multiline contract searches stay quiet so gate output stays focused on failures. .github/workflows/ci.ymlnow splits package tests into two parallel jobs —Package Tests (Core)andPackage Tests (Sample). Sample-only failures no longer hold up core test feedback, and platform builds depend on the core test job alone, while sample-package builds depend on the sample test job. The principle-gates job depends on both since it runs the full canonical suite.scripts/principle-gates.shnow excludesRepro/and any.build-*working directory when rsyncing the project into the canonical sample test root. The reproducer is not exercised by CI, and broader.build-*exclusion keeps stray release-build caches out of the staged copy used for sample-package tests.- Sample app, DocC walkthrough,
.cursorauthoring rules, README, andCLAUDE.mdnow document the newstore.binding(_:to:)alias when forwarding an enum case constructor.store.binding(\.$step, to: Feature.Action.setStep)is the recommended form,send:remains supported indefinitely as an explicit labeled alternative, and the old unlabeled closure spelling remains a compatibility shim only. ScopedStorenow survives the SwiftUI observer / parent-store-release race without aborting release builds: stalestatereads return the last valid cached snapshot and stale sends become silent no-ops.SelectedStoreexposes the same liveness signal throughoptionalValue(nilwhen dead), whilerequireAlive()and dynamic-member reads trap withpreconditionFailurewhen the projection is dead. Programming errors that are not lifecycle races — state resolver returningnilat init, orIdentifiable.idtype mismatch — still trap. The current contract is documented inARCHITECTURE_CONTRACT.mdunder "Projection lifecycle contract".ReducerBuildernow preserves composed reducer structure through the full builder chain instead of collapsing every step into nested closure composition. Public authoring (CombineReducers { … }withReduce,Scope,IfLet,IfCaseLet,ForEachReducer) is unchanged. Construction-side benchmarks (debug build) show −29%/−34%/−40% at N=2/8/32; dispatch-side benchmarks show modest −3 to −5% gains in debug and are expected to improve further in release builds where@inlinableunlocks specialization across the builder boundary.
Internal
@InnoFlownow emits a Fix-It-backed warning when a feature'sStatedeclares@BindableField var <name>but the associatedActionenum has no matchingcase set<Name>(Value)(or anyset*case whose suffix matches case-insensitively, somfaCode↔setMFACodeis accepted). The diagnostic is strictly opt-out-safe: it never fires whenStateorActionis atypealias(for exampleSampleArticleRowFeature.Action = ...), andcase _setX(Value)serves as an explicit escape hatch for private setters. Payload type is inferred from the literal initialiser or the explicit type annotation; without that signal, the warning is emitted without a Fix-It. Backed by four newInnoFlowMacrosTestscases covering positive match, acronym casing tolerance, typealiased-Action skip, and multi-field partial coverage.docs/DEPENDENCY_PATTERNS.mdis now the canonical authoring guide for how construction-timeDependenciesbundles enter reducers. The document covers the three canonical patterns (single service / composite bundle / framework-provided clock), the three test-substitution scenarios, preview conventions, the list of anti-patterns InnoFlow explicitly rejects (singletons, property-wrapper resolvers, runtime service locators), and the charter rationale for not shipping a DI container. The English README,README.kr.md,README.jp.md,README.cn.md, andARCHITECTURE_CONTRACT.mdlink to it from their dependency / quick-link sections.scripts/principle-gates.shnow enforces that the document exists and that every README and the architecture contract link to it.scripts/principle-gates.shnow opts the release-mode test run into theEffectTimingBaselineGatesuite viaINNOFLOW_CHECK_EFFECT_BASELINE=1. The gate drives a probeStorethrough a fixed workload, captures the recorder's JSONL output to a temp file, and invokesscripts/compare-effect-timings.shto report mean regressions without blocking CI. Malformed or incomplete timing captures still fail loudly. Stricter timing observations belong toscripts/report-effect-timing-trend.sh, which preserves hard failures for malformed or incomplete data instead of flattening them into non-blocking regressions. Maintainers can regenerateTests/InnoFlowTests/Fixtures/EffectTimings.baseline.jsonldeliberately withINNOFLOW_WRITE_EFFECT_BASELINE=<path>.- Added a release-build regression guard to
scripts/principle-gates.sh. The gate runsswift build -c releasein an isolated build path so release object files cannot leak into.build/and pollute subprocess harness linking. - Added a release-test regression gate to
scripts/principle-gates.sh. The gate runsswift test -c releasein an isolated build path so tests that pass under debug but regress under release optimization (flaky timing assumptions, harness configuration leakage, SIL inliner variants) surface in CI instead of only manual runs. - Added release-mode subprocess tests that verify
ScopedStore.state,ScopedStore.send, collection-scoped projections, andSelectedStore.optionalValue/SelectedStore.requireAlive()follow the current dead-projection contract after the parent store is released. - Added
Repro/SILCrashRepro/— a minimal, standalone SwiftPM package that reproduces the Swift 6.3EarlyPerfInlinercrash on a generic@MainActorclass with anisolated deinitthat stores result-builder-composed value types. Kept in-tree so toolchain bumps can retest whether the InnoFlow@_optimize(none)workaround is still required.