spec-047 Phase 4 — V1 migration close-out (V1 is the production path)#455
Conversation
…nt bucketing) CR item #1 (GridView/ListBox deferred-echo) — revert SelectedIndex to the causal counter (ShouldSuppress/WriteSuppressed; drop the §8 value-diff arm), restoring main's mechanism. Empirically probed on this box: post-realization SelectionChanged is synchronous (deferral is mount-only), the counter and the value-diff arm behave identically in every production-reachable path, and the guarded -1 no-op leaves the echo counter at 0 (no strand) so a later genuine deselect still fires. The counter gate suppresses the whole trampoline fire, so it also governs the multi-select snapshot OnSelectionChanged (CR #3) and the guarded-no-op self-clear concern is moot (CR #4). ComboBox/toggles/pickers keep the value-diff arm (synchronous by construction). Descriptor + fixture narration updated to match. CR item #2 (empty ElementExtras bucket) — normalize the 14 bucketed init shims via ElementExtras.IsEmpty + Element.NormalizeExtras so writing a bucketed field to null never materializes/keeps a non-null empty bucket. An empty bucket is not Equals to null, which otherwise broke record equality between an extras-free element and one with a field cleared to null. New ElementExtrasBucketTests. CR item #6 — make Element.Extensions setter internal to remove the initializer-ordering footgun from the public surface (no in-repo external writes; bucketed shim properties stay public). Nits — ComboBox stale ShouldSuppress doc; PerformanceBudgets §4.9 dead-code TODO; Reconciler ContinueDefaultTraversal cref namespace fix (clears the CS1574 warning); TabView pinnable-tab "not byte-identical" callout; ContentDialog OnClosed tag-routing follow-up note; §4.4 ElementExtras construction-cost note. Verified: core builds clean (ARM64 + x64), Reactor.Tests 9132 passed / 62 skip, selftest ValueDiff/Echo/ComboBox/GridView/ListBox/SelectionEvent/EventStateSplit fixtures all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CR feedback addressed (commit 3554617)Thanks for the high-bar review. Resolutions below, with empirical results for #1. 🔴 #1 — GridView/ListBox deferred-echo → reverted to the causal counter (your preferred option (a))Before reverting I built a probe fixture to settle the deferred-vs-synchronous question on this box. Findings:
Conclusion: the value-diff arm offered no real advantage here, so I moved both back to 🔴 #2 — empty
|
Closes the XAML-interop slice of the §4.0 reachable-but-deferred carve: - Register XamlPageDescriptor/XamlHostDescriptor handlers in RegisterV1BuiltInHandlers so V1 auto-registration owns the two reverse-embedding element types. - Add internal Reconciler.IsElementTypeRegistered and make XamlInterop.Register idempotent (skips already-owned types) so the public API no longer trips EnsureRegistrableElementType under V1 ON. - Update XamlInteropTests double-throw test to idempotency; add V1-ON registration + clashless-Register regression tests. Verified: xunit XamlInterop/V1OnRegistration/TypeRegistry (91) green; Hosting_XamlInteropRegister selftest green V1 ON == V1 OFF. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract Reconciler.CleanupNavigationHostNode and own navigation teardown from NavigationHostHandler.Unmount on the V1 path. The flag-independent UnmountRecursive intercept becomes a !UseV1Protocol fallback so cleanup stays byte-identical V1 ON =/= V1 OFF until the V1-OFF escape path is deleted in 4.6. NavHost selftests 16/16 green under both flags; NavigationHostTests + UseNavigationTests 30/30 pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…led descriptor entries ControlledPropEntry.Update and HandCodedControlledPropEntry.Update wrote under echo-suppression whenever the element prop changed (oldEl != newEl), even when the control already held the new value. WriteSuppressed always increments the suppress counter, consumed only by a real change event; a suppressed no-op write strands the token and silently swallows the user's next real interaction (the cross-state echo class spec 047 section 8 exists to prevent). Suppress only on real control drift (current != newValue). Add real-input echo-stranding regression fixtures across both entry types (RadioButton + ToggleSplitButton via ControlledPropEntry; TextBox + NumberBox via HandCodedControlledPropEntry). Verified red→green: all four SecondEventNotSwallowed assertions fail with the bug reintroduced and pass with the fix. Full Desc_ descriptor suite: 0 failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a 'Selftests (V1 ON)' CI job that runs the full selftest suite with REACTOR_USE_V1_PROTOCOL=true so V1-ON =/= V1-OFF parity is enforced by an automated gate rather than asserted by a one-off manual run. Runs the AppTests.Host directly (Program.cs maps the env var to the AppContext switch before any Reconciler is built), since Reactor.Tests/xunit does not map the flag. Mirrors the existing aot-selftests run-the-host pattern and uploads TAP output as an artifact. Also harden TabRenderer_TitleBarChrome/FlatChrome_BodyRendered with a second Harness.Render() pump: TabView lazy-realizes selected-pane body content on dispatcher messages scheduled by layout, which a single pump can race on contended runners (consistent with the documented Harness.Render mitigation). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Flip the Reconciler ctor default so V1 dispatch is the production path when neither the explicit ctor flag nor the AppContext switch is set. The flag now exists only as an escape hatch to force V1 OFF during the legacy-deletion window (gated to 4.5/4.6). Fix 4 tests that assumed the OFF default: - XamlInteropTests x2: assert via IsElementTypeRegistered (flag-independent) since XamlHost/Page are owned by the V1 registry under ON. - TypeRegistryTests.Override_Builtin_Type: pin to useV1Protocol:false (legacy built-in override is V1-OFF-only per 13 Q17). - RichEditBoxElementTests: tolerate TargetInvocationException-wrapped COMException from V1 new T() construction. Verified: xunit ON = 9136 passed/0 failed; full selftest ON = 0 failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ale selftest The GridViewHandler already routes through the engine's virtualizing MountGridView body (ItemsSource = Range + shared ItemTemplate + ContainerContentChanging), mirroring ListViewHandler. Add RareControl_GridViewLazy to lock that CCC lifecycle: 500 items in a 200px viewport realize only ~96 containers (< total/2), the tail item stays unrealized, the first item realizes. Identical 96/500 under V1 ON and V1 OFF - A|B parity green. Guards against a regression to the descriptor's non-virtualizing ItemsHost<> path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the stale 100/320/500 B byte-gate estimates with the measured 11.6 targets (Target = min(Direct + 100, ReactorToday x 0.4) -> 407 / 1520 / 19200) in spec 14 cleanup, 15.1 goal 1, the 15.6 hard-gate sentence, and the 15.7 Phase 4 row. Fix the 15.6 'Phase 5 cleanup' reference to 'Phase 4' (this spec has no Phase 5). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…alyzers The final descriptor API is fully strongly-typed: events wire via typed subscribe lambdas (no changeEvent string names) and Controlled<TValue,TArgs> unifies set/readBack so the compiler already rejects a read-back type mismatch. A repo sweep found zero string-form event references in production descriptors, so REACTOR1001 (string event ref) and REACTOR1003 (read-back type) had no source pattern left to match. Remove both no-op analyzers + their test fixtures, the two reserved diagnostic descriptors, and the REACTOR1001/1003 rows from AnalyzerReleases.Unshipped.md and the guide table. REACTOR1002 (CustomEventDelegateTypeAnalyzer) remains as the active Q10 compile-time check. Analyzer tests green (4 pass). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Document completed work (4.0.6 parity, 4.1 flip, 4.0.4 GridView lock, 4.4 spec-hygiene, 4.7 analyzer retirement) and the critical context for the next session: the prelude delegate handlers call legacy MountXxx bodies, so genuine 4.0.1 (modal-lifecycle decorator strategy) + 4.0.3 (TabView engine features) ports must land before 4.5 can delete the legacy switch. Includes verified build/test commands and the concurrent-selftest-build race warning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ze overlay/TabView ports Phase 4 close-out work, landing the genuine V1 ports together with the legacy dispatch-switch deletion they unblock. §4.0.1 overlay port: overlays are now owned by the new V1 OverlayLifecycle static module; the engine no longer delegates into legacy bodies. §4.0.3 TabView port: full TabViewDescriptor replaces the delegate TabViewHandler (deleted), with drag pipeline, pinnable headers and imperative-bridged strip slots. §4.5: both Mount/Update dispatch to V1 registry -> _typeRegistry -> composition-primitive-only switch (Component/Func/Memo/ErrorBoundary/ CommandHost/FormField/ValidationVisualizer/ValidationRule). Dead-body sweep removed the orphaned legacy Mount*/Update* bodies including the 14 overlay delegators and Mount/UpdateTabView. Overlay leaf helpers (Create/Update MenuFlyout/AppBar items) promoted to internal for OverlayLifecycle; BuildTabHeader/TryUpdatePinHeaderInPlace stay internal for TabViewDescriptor. Tests: removed the obsolete descriptor-vs-handler parity harness (Spec047V1ProtocolDescriptorFixtures + Desc_ registry entries; Echo_ real-input regression fixtures preserved); dropped the dead UpdateCommandBarFlyout/UpdateFlyoutElement reflection probes. Validation: build 0 err; xunit V1 ON 9136 pass/0 fail; full selftest 0 fail. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
V1 is the unconditional production path (§4.5 deleted the legacy dispatch switch), so the entire A|B testing apparatus is dead. This removes it. Reconciler: - Collapse the four ctors into a single `Reconciler(ILogger? logger = null)`; drop the `useV1Protocol` / `registerBuiltinHandlers` params, the `public bool UseV1Protocol` property, and the AppContext-switch read. - Drop the flag guard from both dispatch sites (Mount.cs:66, Update.cs:117), both unmount arms, and the NavigationHost pre-dispatch block. Tests / harness: - Remove the REACTOR_USE_V1_PROTOCOL env-var mapping (AppTests.Host/Program.cs) and the redundant `selftests-v1` CI job. - De-switch the Spec047V1Protocol / Spec047ExternalProof selftest fixtures. - Delete V1FeatureFlagTests, TypeRegistryTests.Override_Builtin, and the redundant TextBox echo-stranding fixture; migrate the echo-stranding fixtures and Ports/*PortTests to `new Reconciler()` against the built-in descriptors; reshape V1OnRegistrationTests to the always-on registration contract. Perf / tooling: - Delete the A|B perf duplicates (StressPerf.ReactorV2, BlankReactorV2, PerfBench DescriptorVariantFactory) and tools/spec047-phase1-checkpoint; rename the ReactorV2 column to Reactor in the aggregator/slnx/scripts. Validation: core build = 0 err; xunit = 9128 pass / 0 fail; Echo + V1_* + Spec047ExternalProof_* selftests = 0 fail. Perf-project consolidation measurement is deferred to the ARM64 baseline machine (§4.9). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase 2 decided the V1 protocol is the production model and §4.5 deleted the
legacy path, so the author surface is now stable/supported. Graduate it out of
[Experimental] and confirm the external-assembly proof against the locked surface.
- Remove all 157 [Experimental("REACTOR_V1_PREVIEW")] attributes across 110
src/Reactor files (public author surface — IElementHandler, MountContext/
UpdateContext, ReactorBinding(<T>), ControlDescriptor + builders,
Register{Type,Handler}, pool policy, WriteSuppressed, AddRawRoutedHandler —
and the internal handlers/descriptors that carried it).
- Drop the now-dead REACTOR_V1_PREVIEW NoWarn from all six csprojs
(Reactor, Reactor.Tests, Reactor.AppTests.Host, PerfBench.ControlModel, and
both external_proof projects). The external_proof control now consumes the
public surface with NO experimental opt-in — the strongest form of the proof.
KD-4 (external typed-event surface) was already shipped: the external
MarqueeControl wires a typed CLR event via the public MountContext.BindFor ->
ReactorBinding<TElement>.OnCustomEvent<TArgs> path with only a plain
ProjectReference (no InternalsVisibleTo). ReactorBinding<TElement>'s ctor stays
internal but is reached through public BindFor, so it is not a gap.
Validation: core build = 0 err; Reactor.External.TestControl builds clean (0 err,
no IL trim/AOT warnings, PublishTrimmed+IsAotCompatible on, no opt-in); xunit =
9128 pass / 0 fail; all six Spec047ExternalProof_Marquee_* selftests green;
PerfBench.ControlModel + AppTests.Host build clean without the NoWarn.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…bodies
§4.5 deleted the legacy reconciler dispatch switch arms but left the value-
control MountXxx/UpdateXxx handler method bodies behind as unreachable dead
code (controls now dispatch through the V1 descriptor system). Remove them:
Mount.cs: MountToggleSplitButton, MountPasswordBox, MountNumberBox,
MountAutoSuggestBox, MountRadioButton, MountRadioButtons,
MountComboBox, MountSlider, MountRatingControl, MountColorPicker,
MountCalendarDatePicker, MountDatePicker, MountTimePicker, plus
the now-dead helpers EnsureToggleSwitchWiring and (Reconciler's
own) EnsureTextBoxWiring.
Update.cs: UpdateToggleSplitButton, UpdateTextBox, UpdatePasswordBox,
UpdateNumberBox, UpdateAutoSuggestBox, UpdateRadioButton,
UpdateToggleSwitch, UpdateRatingControl, UpdateColorPicker,
UpdateCalendarDatePicker, UpdateDatePicker, UpdateTimePicker,
UpdateRadioButtons, UpdateComboBox, UpdateCalendarView.
Preserved live islands interleaved among these: MountCheckBox/UpdateCheckBox
(Path-B via CheckBoxHandler), the NumberBox immediate-mode chain
(NumberBoxImmediateTextChanged/...LoadedEnsureImmediateTextBox/
EnsureNumberBoxImmediateTextBoxWiring/HandleNumberBoxImmediateTextChanged plus
the CanSynchronize/AreNumberBoxValuesEquivalent helpers, all live via
NumberBoxDescriptor.Immediate), and SyncSelectedDates (reflection-probed).
This finishes the §4.5 dead-code sweep and cuts ChangeEchoSuppressor references
from ~55 to 16 (the remaining live descriptor paths are migrated in §4.2 part B).
No behavior change (deleted code was unreachable).
Validation: core build 0 err; xunit 9128 pass / 0 fail / 62 skip; selftests
PrivMount/Echo/NumberBox/CheckBox all 0 failures.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Phase-0 begin-suppress-audit.csv is stale (cites §4.5/§4.2A-deleted legacy line numbers). Add a refreshed, accurate live call-site inventory for the ChangeEchoSuppressor elimination surface and record the go/no-go decision. - New: docs/specs/047/audits/echo-suppressor-phase4-live-sites.md — the ~30 live echo sites (post-§4.2A), classified (general-controlled / coercion / float-precision / collection-batch / setter-scope / public-API), with the rationale for why full elimination (part B) is a dedicated, spec-author-involved effort rather than an autopilot pass: the counter is a causal token, value-compare is causally weaker (coincidental real-value events get swallowed), ApplySetters' EchoSuppressScopeDepth has no value to compare, and public WriteSuppressed carries no value/readback for external authors. New regression fixtures required first. - Task doc: progress log records §4.2 part A done (commit 8a67e34) and a "Deferred" block for §4.2B / §4.3 / §4.4 / §4.8 / §4.10 / §4.9 with reasons; §4.2 section gets a status note pointing at the refreshed inventory. Also corrects the §4.5 log's "0 orphaned private members remain" over-claim (§4.2A removed ~28 it missed). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ry path Per product decision, prove out the spec §8 "diff/value-compare" echo suppression on the descriptor controlled fast path, keeping the causal counter as the retained fallback everywhere else (so reverting this one path is trivial if the diff approach causes issues in practice). ControlledPropEntry (src/Reactor/Core/V1Protocol/Descriptor/PropEntry.cs) — the generic two-way `.Controlled(...)` entry used by ~10 controls (RadioButton, ToggleSplitButton, ToggleSwitch, RatingControl, ColorPicker, the date/time pickers, etc.) — now suppresses its own programmatic-write echo by VALUE: - Update arms a per-control ExpectedEcho on the DescriptorControlledPayload (value + comparer) instead of bumping ChangeEchoSuppressor's counter, then writes. Arming is gated on a non-null callback AND an existing payload (no subscription => no echo to suppress), via the new non-allocating Reconciler.TryGetControlEventPayload<T>. - The change-event trampoline drops the single event whose readback equals the armed value (one-shot), then clears the arm. A mismatch means a real user change superseded the pending write -> clear and fire the callback. - ChangeEchoSuppressor.ShouldSuppress is still honored first (external WriteSuppressed tokens + the ApplySetters setter scope, which carry no value); a matching pending arm is drained on that branch too so it cannot strand and swallow a later real interaction (rubber-duck + code-review blocking finding). - Mount clears any stale arm left on a pooled payload (KD-3: payload survives rent/return). The counter (ChangeEchoSuppressor / EchoSuppressCount / EchoSuppressScopeDepth / public ReactorBinding.WriteSuppressed) is unchanged and remains the live mechanism for the hand-coded / coercing / collection entries, the Slider/TextBox/ToggleSwitch handlers, CheckBox, NumberBox-immediate, the CalendarView shim, the setter scope, and external authors. The PoC's synchronous-change-event assumption (and the migrate-back path) is documented in-code on DescriptorControlledPayload. New selftest fixtures (Spec047ValueDiffEchoFixtures) lock the value-diff DRIFT path the existing Echo_*_RealInput stranding fixtures don't cover: a real programmatic update lands on the control, does NOT echo into the callback, and a subsequent real interaction still fires. Validation (x64): core build 0 err; xunit 9128 pass / 0 fail; selftests --filter Echo (no-echo + stranding), ValueDiff (new drift), SettersScope (setter scope), Spec047ExternalProof (public WriteSuppressed counter) all 0 failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extends the §8 value-diff echo-suppression proof-of-concept from the ControlledPropEntry fast path to the live TextBox path (TextBoxHandler, the registered handler — the descriptor is an unregistered proof-point). The legacy counter (ChangeEchoSuppressor.WriteSuppressed / ShouldSuppress) is replaced for TextBoxHandler's controlled `Text` writes by arming an ExpectedEchoText on the per-control TextBoxEventPayload: the TextChanged trampoline drops the single event whose readback matches (then clears the one-shot arm). The arm is left pending after the write — like the counter, which leaves its suppression elevated for a possibly-deferred TextChanged. Stale arms are cleared on the next Mount (pool reuse). The counter is retained for the paths it still owns: external public ReactorBinding.WriteSuppressed and the ApplySetters setter-scope. The trampoline checks ShouldSuppress first (and drains a matching arm on that branch so a counter-suppressed echo can't strand the arm), then value-diff. Arming is gated on a TextChanged trampoline being wired now OR about to be wired this Update (newEl.OnChanged != null) — EnsureTextBoxWiring runs at the end of Update, so a null->non-null OnChanged transition combined with a value change still arms (creating the payload if needed) and the trampoline goes live before any deferred echo. This preserves the legacy counter's subscription-timing-independent suppression and avoids stranding a SelectionChanged-only TextBox. Accepted PoC tradeoff: value-diff only drops on an exact readback match, so a write WinUI coerces (e.g. single-line stripping \r\n) surfaces as a real change rather than being dropped. Documented in-code with a migrate-back path to the counter. Selftest fixtures added (Spec047ValueDiffEchoFixtures): ValueDiff_TextBox_Drift, _SnapBack, _Transition. Validated (x64): core build clean; xunit 9128/0; selftests Echo + ValueDiff + SettersScope 0 failures (incl. EchoSuppress_TextBox_*); DataGrid E2E (click cell -> type -> commit) + EventHandler TextBox keydown E2E passed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Migrate the synchronous, exact-comparable, single-controlled-value round-trips off the causal ChangeEchoSuppressor counter onto a value-diff arm, while RETAINING the counter as the documented fallback for paths value-comparison cannot model. End state is an intentional hybrid (no ReactorState byte win — adds one ref field — chosen for correctness/self-healing on migrated paths). Shared infra: - ReactorState.PendingEchoMatch (one-shot Func<object?,bool>?), reset at the same 3 sites as the counter (ClearCurrentEventHandlers, DetachReactorState, pool-return). - ChangeEchoSuppressor.ArmExpectedEcho / ClearExpectedEcho / ShouldSuppressEcho. Counter/scope wins first (external WriteSuppressed + ApplySetters carry no expected value); else the one-shot value predicate is consumed. - HandCodedControlledPropEntry opt-in `valueDiffEcho` + ControlDescriptor builder param: Update arms the expected echo then writes bare; the per- descriptor trampoline calls ShouldSuppressEcho(ctrl, readback). Migrated (valueDiffEcho / ShouldSuppressEcho): ComboBox, FlipView, GridView, ListBox, Pivot, PipsPager, RadioButtons, SelectorBar, TabView, TemplatedFlipView descriptors + ToggleSwitchHandler. (TextBoxHandler / ControlledPropEntry were already value-diff via their own payload arm.) Retained on the counter (rationale in spec 8.3): Slider/NumberBox doubles, NumberBox coercion, CalendarView collection, AutoSuggest/Password/RichEdit strings, Expander, CheckBox path-B, ApplySetters scope, public WriteSuppressed. Key insight: WinUI selection (SelectionChanged) and Toggled fire SYNCHRONOUSLY inside the property write (unlike deferred TextBox.TextChanged), so no arm-ahead-of-wiring is needed; a null->non-null transition simply produces no echo before the trampoline subscribes. Code-review-driven strand fixes: - ShouldSuppressEcho clears PendingEchoMatch UNCONDITIONALLY in the counter branch (safe: only synchronous controls use this arm; deferred TextBox uses a separate HasExpectedEcho payload arm), preventing a stranded arm from swallowing a later coincidental real event. - HandCodedControlledPropEntry.Update clears the arm after a guarded/coerced no-op write (post-write readback != nv) so the never-fired echo cannot strand. Docs: spec §8.3 (new) documents the hybrid + retained-counter rationale and supersedes the wholesale-deletion plan; task-doc progress log records part B'. Tests: new ValueDiff_ComboBox_Drift, ValueDiff_ToggleSwitch_Drift, and ValueDiff_GridView_GuardedNoOpStrand selftest fixtures. Validated x64: core build 0 err; xunit 9128/0; ValueDiff + Echo + migrated-control selftests 0 fail; DataGrid E2E pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sk doc Update the §4.2 section of the phase-4 task doc to reflect the value-diff echo migration that landed in c5c1399: - New "Part B'" status block + checked-off checklist for the shared value-diff arm, the migrated descriptors/handler, the strand-safety fixes, and the new regression fixtures. - Mark the original "delete + tolerance metadata + ColorPicker shim" plan (now "Part B") as DEFERRED and SUPERSEDED by spec §8.3, with the counter-retained hybrid rationale; its full-elimination boxes stay unchecked. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tHandlerState Carve the monolithic per-element EventHandlerState into the §9.2 shape: the WinUI true-routed-input family (pointer/key/tap/focus/sizechanged/ accesskey — 21 Current* + 20 trampolines) is renamed in place to ModifierEventHandlerState (lazy on ReactorState.Modifiers); control-intrinsic events move fully onto the already-shipped per-control ControlEventStateBox payloads. - Delete dead legacy Image/ScrollViewer/ScrollView Mount/Update/Ensure bodies (the registered descriptors own their event wiring) + their 5 EHS trampoline fields. Delete 3 orphaned EHS fields (ToggleSwitchToggled/TextBoxTextChanged/ TextBoxSelectionChanged — referenced only in the struct def). - Migrate the 2 live control-intrinsic paths off the shared struct onto ControlEventStateBox payloads: Button.Click → ButtonEventPayload.ClickTrampoline and NumberBox immediate-mode wiring flag → NumberBoxEventPayload.ImmediateInnerWired. Both resolve the same native-DO-keyed ReactorState, so the issue #114/#86 shared-trampoline dedup invariant is preserved; trampolines keep reading the live element via GetElementTag (CurrentClick untouched — it is preserved across pool return and must not stash a per-owner delegate). - Rename EventHandlerState→ModifierEventHandlerState, ReactorState.Events→Modifiers, GetOrCreateEventState→GetOrCreateModifierState across all callers; refresh the 5 <see cref> doc references. - Correct the stale ControlEventStateBox/payload comments that claimed pool reset clears ControlEventState on return — it is PRESERVED across rent/return (#114), dropped only on full detach. Validation (x64): core build 0 err; xunit 9128 pass / 0 fail; Button / NumberBox / Image / Scroll / Pool / EventHandler selftests 0 fail. M10/M11 byte measurement deferred to the ARM64 baseline machine (§4.9); the §9.2 pool-lifecycle hazard fixtures land next. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Lock the post-split contract with AppTests.Host self-test fixtures (Spec047EventStateSplitFixtures.cs, registered in SelfTestFixtureRegistry): - NoDuplicateSubscriptionAcrossPoolReuse — issue #114 guard: rent→fire→return→ re-rent→fire, the preserved ControlEventState trampoline fires the LIVE element exactly once (never double-subscribes). - HandlerTypeMismatchResetsBox — the ControlEventStateBox HandlerType discriminator deterministically replaces the box on type mismatch (no InvalidCastException); doubles as the hot-reload type-identity proxy. - DualReturnIdempotent — returning the same control twice is idempotent; re-rent still fires exactly once. - ModifierStateLazyForIntrinsicOnly — §9.4 alloc-shape proxy for the ARM64-blocked M10/M11 byte measurement: an intrinsic-only control leaves ReactorState.Modifiers null while ControlEventState is allocated; a routed-modifier control allocates Modifiers. - AddRawRoutedHandler_HandledEventsToo — the §9.5/Q11 escape hatch is intact on Mount/UpdateContext and independent of the split. The live "Handled child → parent still fires" leg is a documented TAP SKIP (WinUI 3 cannot synthesize a KeyRoutedEventArgs / RaiseEvent an input event headlessly) — covered by the Appium E2E KeyDownTest. Validation (x64): host build 0 err; EventStateSplit / Pool / EventHandler selftests 0 failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rogress log Mark all §4.3 task-doc checkboxes done (the monolith is gone; control-intrinsic events fully on ControlEventStateBox payloads; routed family renamed to ModifierEventHandlerState; §9.2 hazard fixtures landed). M10/M11 byte/frequency measurement noted ARM64-deferred to §4.9; alloc-shape asserted via selftest in this x64 env. Add the §4.3 progress-log entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…6 byte-gate constants Mirror the proven spec-034 ElementModifiers shim-bucket pattern: move the 14 cross-cutting nullable Element base fields (Attached, ImplicitTransitions, ThemeTransitions, ThemeBindings, LayoutAnimation, AnimationConfig, ElementTransition, InteractionStates, StaggerConfig, KeyframeAnimations, ScrollAnimation, ConnectedAnimationKey, ResourceOverrides, ContextValues) into a single nullable value-equality sub-record `ElementExtras`, exposed via one `Element.Extensions` slot. Each field name survives as a public get/init shim (get => Extensions?.X; init => copy-on-write into Extensions), so in the lean case (Extensions == null) the base carries only Key/Modifiers/Extensions — the §11.7 byte win — while all ~180 existing readers and `with`-expression writers (including the read-then-write composites in ElementExtensions.cs) compile and behave unchanged. Named ElementExtras to avoid the existing ElementExtensions static class. - Add `Extensions is null && b.Extensions is null` fast-path in ShallowEquals so the common no-Extras element skips the three structural (Attached/ThemeBindings/ ContextValues) compares in the hot reconcile diff path; update the CanSkipUpdate/ShallowEquals invariant doc. Record value-equality is preserved (the 14 move into the value-equality ElementExtras; the reconciler diffs via the structural helpers, not Element.Equals). - Land the §11.6 hard byte-gate TARGET constants (PerformanceBudgets.cs): M1 ≤407 / M2 ≤1520 / M3 ≤19200 (measured baselines 1018/~3800/~48000 ×0.4). The gate MEASUREMENT/enforcement is ARM64-baseline-blocked → deferred to §4.9. Validation (x64): core build 0 err; xunit 9128 pass / 0 fail (incl. ElementModifiersBucketTests); Animation/Transition/Theme/Context/Attached/Stagger/ Keyframe/Scroll/ConnectedAnimation/Resource selftests 0 fail. Only Element.cs + the new PerformanceBudgets.cs changed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…gress log Mark the bucketing + reader/writer-migration boxes done (ElementExtras shim bucket; zero call-site edits; PerformanceBudgets §11.6 target constants landed). The byte-gate enforcement/measurement boxes stay open — ARM64-baseline-blocked, deferred to §4.9. Add the §4.4 progress-log entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-split) Promote the V1 control-authoring guide out of "preview/provisional" and bring it in line with the post-Phase-4 reality (hand-maintained pages — extensibility- preview.md has no .md.dt template; filename kept to preserve inbound spec links). docs/guide/extensibility-preview.md: - Drop the [Experimental]/REACTOR_V1_PREVIEW/breaking-change banner; mark the surface stable. Replace "Enabling the V1 path / off by default" with a "Dispatch order" section (V1 registry → _typeRegistry → composition-primitive switch; no flag, no legacy MountXxx fallthrough). - Correct the pool-reset enumeration: ControlEventState is PRESERVED across rent/return (#114), not cleared — trampolines stay subscribed and read the live tag; dropped only on full detach / replaced on HandlerType mismatch. The shared modifier state is ModifierEventHandlerState (ReactorState.Modifiers, lazy), cleared via ClearCurrentHandlers. - Correct the per-control event-state section: EventHandlerState was split into ModifierEventHandlerState + per-control ControlEventStateBox (§9.2) — done, not "Phase 3/4". - Rewrite WriteSuppressed to the §8.3 hybrid (value-diff for synchronous single-value controls + retained suppress-counter fallback; signature stable). - Replace the children-strategy table with the final 10-strategy set; drop the Phase-status column. Drop the "descriptor authoring = Phase 2 gate" item and the NoWarn REACTOR_V1_PREVIEW checklist step (plain ProjectReference, no IVT/opt-in). - Add a "Choosing an authoring shape (decision tree)" section (spec §6.1.1): descriptor prop/engine shapes vs hand-coded IElementHandler, the children-strategy picker, echo + pool-policy notes. AGENTS.md: - Rewrite "Adding a new WinUI control" to the V1 descriptor path (Element record → ControlDescriptor/IElementHandler → RegisterV1BuiltInHandlers → selftest). - Rewrite "Echo suppression for value controls" to the §8.3 hybrid (ChangeEchoSuppressor retained as the counter fallback; authors use WriteSuppressed/.Controlled/valueDiffEcho). - Update the per-element-state line to ModifierEventHandlerState + ControlEventStateBox. Docs-only; mur not available in this env so no generated pages were recompiled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ess log Mark all §4.8 boxes done (extensibility guide promoted to stable; §6.1.1 decision tree added; AGENTS.md updated to the V1 descriptor model + §8.3 echo hybrid + ModifierEventHandlerState/ControlEventStateBox). The generated-pages box is N/A in this env (mur unavailable; the edited page is hand-maintained). Note: the task's "ChangeEchoSuppressor is deleted" premise was corrected to the settled hybrid (retained). Add the §4.8 progress-log entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-blocked) §4.9 is entirely a measurement/ratification gate on LAPTOP-4MEP83VI and is not executable in the x64 dev environment. Annotate the section with a clear handoff: all code the gates measure is already landed (§11.6 target constants §4.4, single-Reactor perf-project consolidation §4.6, EHS split §4.3, bucketed Element base §4.4, AOT-clean external proof + CI AOT job §4.7); the KD-3 M1 binder-check fold is deliberately NOT applied (measurement-gated, and prior related micro-opts were net-negative). Boxes stay unchecked until the baseline capture lands; added the baseline-operator runbook + progress-log entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Final close-out of the Phase 4 V1-migration cleanup. - Status: marked the main tracker header + spec §14 "Phase 4 — cleanup" as code-complete — migration closed; V1 is the unconditional production path. Reconciled the exit-gate/cleanup wording against the settled outcomes: the "delete ChangeEchoSuppressor" item is superseded by the §8.3 value-diff/counter HYBRID (suppressor intentionally retained; WriteSuppressed signature unchanged), and the §11.6 byte-gate + ARM64 ratification are carved out as the only outstanding baseline-machine (LAPTOP-4MEP83VI) items. - Dead-code sweep: grep-clean across src + tests — no live UseV1Protocol / REACTOR_USE_V1_PROTOCOL / ReactorV2 / registerBuiltinHandlers / EventHandlerState (monolith); only historical comments remain. ChangeEchoSuppressor is retained by design (hybrid), so it is removed from the sweep list. - Tidied a stale PoolPolicyTests TODO: the real FrameworkElement rent/return reset contract is now covered by the §4.3 self-test fixtures; corrected its "ControlEventState cleared" wording to "preserved across rent/return (#114)". Validation (x64): full solution build (Reactor.slnx -p:Platform=x64) 0 err; full xunit 9128 pass / 0 fail; full selftest 0 fail modulo the known docking-family flakiness (DockHooks_* / SidePopup_*) that fails intermittently under full-suite headless load and passes deterministically when filtered (confirmed: both filters 0 failures; full-run count varied 7→6 across runs) — pre-existing, not a regression. Outstanding (handed off, baseline-machine-only): §4.9 ARM64 stable-AC perf ratification + §4.4 §11.6 hard byte-gate measurement/enforcement. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nt bucketing) CR item #1 (GridView/ListBox deferred-echo) — revert SelectedIndex to the causal counter (ShouldSuppress/WriteSuppressed; drop the §8 value-diff arm), restoring main's mechanism. Empirically probed on this box: post-realization SelectionChanged is synchronous (deferral is mount-only), the counter and the value-diff arm behave identically in every production-reachable path, and the guarded -1 no-op leaves the echo counter at 0 (no strand) so a later genuine deselect still fires. The counter gate suppresses the whole trampoline fire, so it also governs the multi-select snapshot OnSelectionChanged (CR #3) and the guarded-no-op self-clear concern is moot (CR #4). ComboBox/toggles/pickers keep the value-diff arm (synchronous by construction). Descriptor + fixture narration updated to match. CR item #2 (empty ElementExtras bucket) — normalize the 14 bucketed init shims via ElementExtras.IsEmpty + Element.NormalizeExtras so writing a bucketed field to null never materializes/keeps a non-null empty bucket. An empty bucket is not Equals to null, which otherwise broke record equality between an extras-free element and one with a field cleared to null. New ElementExtrasBucketTests. CR item #6 — make Element.Extensions setter internal to remove the initializer-ordering footgun from the public surface (no in-repo external writes; bucketed shim properties stay public). Nits — ComboBox stale ShouldSuppress doc; PerformanceBudgets §4.9 dead-code TODO; Reconciler ContinueDefaultTraversal cref namespace fix (clears the CS1574 warning); TabView pinnable-tab "not byte-identical" callout; ContentDialog OnClosed tag-routing follow-up note; §4.4 ElementExtras construction-cost note. Verified: core builds clean (ARM64 + x64), Reactor.Tests 9132 passed / 62 skip, selftest ValueDiff/Echo/ComboBox/GridView/ListBox/SelectionEvent/EventStateSplit fixtures all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3554617 to
0f134d3
Compare
Rebased onto
|
ARM64 baseline-machine capture landed — partially addresses the two "Outstanding" itemsRan the post-Phase-4 micro suite (M1–M13) on On Outstanding #1 (§4.9 ARM64 perf ratification) and #2 (§11.6 byte-gate measurement):
The allocation axis is valid (deterministic;
The headline: the §4.4 bucketing made the leanest leaf (M1) ~+235 B/render heavier, not lighter — deterministic across all reps. This confirms the spec's own KD-3 trigger ("fold the M1 binder check if M1 is still over budget") and adds a new follow-up: investigate why bucketing regressed M1 (candidates: the added Net: the two outstanding items are measured but not closed — M1/M2 miss the byte gates, and a thermally-clean stable-AC re-capture (+ the macro suite rebuilt against the single |
Spec-047 Phase 4 — V1 migration close-out
Closes out the spec-047 "Fully Extensible Control Model" migration. V1 is now the unconditional production path; the migration is closed. This PR is the full Phase 4 set (27 commits, §4.0.2 → §4.10) on top of
main(#443, which still defaulted V1 OFF).Net: 181 files, +4,825 / −12,371 (the large deletion is dead-code removal — legacy dispatch switch, A|B harness, flag plumbing).
What landed
UseV1ProtocolON by default (then removed entirely, below).MountXxx/UpdateXxxdispatch switch + ~60 orphaned handler bodies; finalized the overlay/TabView genuine ports. Dispatch is now V1 registry → external_typeRegistry→ composition-primitive switch (no legacy fallthrough).UseV1Protocoldead code: the flag, AppContext switch, env var, dual-flag selftest harness,ReactorV2perf-project duplicates, checkpoint runner. There is onepublic Reconciler(ILogger? logger = null)ctor; V1 is unconditional.[Experimental("REACTOR_V1_PREVIEW")](157 attrs / 110 files) and locked it; closed KD-4 (external typed-event surface); retired the obsolete REACTOR1001/1003 analyzers (REACTOR1002 stays).PendingEchoMatch+ArmExpectedEcho/ShouldSuppressEcho, opt-invalueDiffEcho); the suppress-counter (ChangeEchoSuppressor) is intentionally retained as the fallback for doubles/coercion/collection/deferred-strings/Expander/CheckBox-path-B/ApplySetters-scope/publicWriteSuppressed.WriteSuppressedkeeps its public signature.EventHandlerState: the WinUI routed-input family →ModifierEventHandlerState(lazy onReactorState.Modifiers); control-intrinsic events → per-controlControlEventStateBoxpayloads. Migrated the last live holdouts (Button.Click, NumberBox immediate), deleted dead Image/ScrollViewer/ScrollView wiring + orphaned fields, added §9.2 pool-lifecycle hazard fixtures (no-double-subscribe across pool reuse, HandlerType-mismatch reset, dual-return idempotency, intrinsic-only alloc-shape,AddRawRoutedHandlersurvival). The monolith is gone.Elementbase fields into a value-equalityElementExtrasrecord behind oneElement.Extensionsslot, via the provenElementModifiersshim pattern (zero call-site edits — onlyElement.cschanged). Landed the §11.6 byte-gate target constants (PerformanceBudgets.cs: 407 / 1520 / 19200).AGENTS.mdsections (new-control authoring → V1 descriptor model; echo → §8.3 hybrid; per-element-state →ModifierEventHandlerState+ControlEventStateBox).UseV1Protocol/REACTOR_USE_V1_PROTOCOL/ReactorV2/registerBuiltinHandlers/EventHandlerState-monolith); tracker + spec §14 status updated.Validation (x64 dev machine)
Reactor.slnx -p:Platform=x64): 0 errors.Reactor.Tests): 9128 passed / 0 failed.DockHooks_*/SidePopup_*) — these fail intermittently under full-suite headless load (count varied 7→6 across runs) and pass deterministically green when filtered. Pre-existing, not a regression from this PR.Outstanding — baseline-machine-only (the reason for cross-machine validation)
Both are code-complete; only the measurement/ratification is blocked on the ARM64 baseline machine
LAPTOP-4MEP83VI:ReactorTodaybaseline, confirm/close KD-3 (apply the M1 binder-check fold only if M1 is over budget). Runbook is in the §4.9 section of the close-out task doc.Note for the spec author
The original exit-gate/§14 wording said "delete
ChangeEchoSuppressor," but that was settled as the §8.3 hybrid (suppressor retained). The close-out docs are reconciled to the hybrid; the literal exit-gate wording should be formally ratified.🤖 Generated with Claude Code