spec(047): Phase 3 close-out + finish — 100% V1 dispatch for typed-items hosts#437
Conversation
…ass shape Add optional Action<TControl, IReadOnlyList<(UIElement, Element)>>? callback to Panel<TElement,TControl> that the engine fires once after every child has been mounted (Mount path) or reconciled (Update path), receiving the full ordered (mounted, childElement) pair list. Existing PerChildAttached fires per-child mid-pass and cannot see siblings that haven't mounted yet — too late for descriptors that need to write attached DPs by sibling name (RelativePanel.SetRightOf etc.). The after-all shape carries the populated map across the existing Panel strategy without a dedicated NamedRelativePanel type. V1HandlerAdapter Panel arms in DispatchChildrenMount and DispatchChildrenUpdate now buffer pairs lazily — list is allocated only when the consumer registered the callback, so Grid / Canvas / FlexPanel / WrapGrid pay no overhead. Selftest baseline preserved: V1 ON Desc_: 534 ok / 0 failures V1 OFF Desc_: 534 ok / 0 failures Unblocks Port (4) RelativePanelDescriptor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
….BindKeyedItemsSource
Adds the descriptor-side keyed templated-items shape so the upcoming
G2 ports (ListView<T>, GridView<T>, LazyVStack<T>, LazyHStack<T>,
ItemsRepeater<T>) can declare their data without re-implementing the
spec-042 realization machinery.
ChildrenStrategy.cs — new sealed record TemplatedItems<TItem,TElement,
TControl> + internal non-generic ITemplatedItemsStrategy marker. The
marker is how the closed-(TElement,TControl) adapter dispatches into an
open-TItem strategy.
Reconciler.KeyedItemsBinding.cs (new partial) — internal void
BindKeyedItemsSource<TItem>(control, items, keySelector, buildItemView,
requestRerender, isMount). MVP supports WinUI.ListViewBase (ListView +
GridView):
Mount: BuildListStateForItems → SetListState → ItemsSource = state.Source;
shared HandleTemplatedContainerContentChanging hook attached.
Update: KeyedListDiff.Apply over an ItemsKeyAdapter projecting through
the strategy's KeySelector; AmbientAnimation survivor-move pass
mirrors the legacy ApplyKeyedDiffOrFallback shape; tail call to
RefreshRealizedContainers.
Other control types (ItemsRepeater, Lazy*Stack) throw a descriptive
InvalidOperationException at the dispatch switch so the gap surfaces at
port time. Adding a new arm is purely additive.
V1HandlerAdapter — dispatch arms in DispatchChildrenMount /
DispatchChildrenUpdate route any ITemplatedItemsStrategy via .Bind(...)
before the closed pattern switch.
Realization machinery share-out — new internal IItemViewSource interface
(ItemCount + BuildItemView(int)). TemplatedListElementBase now implements
it (no behavior change — methods already matched). HandleTemplatedContainer
ContentChanging prefers a stashed IItemViewSource (Reconciler.SetItemView
Source, parallel to the existing SetListState plumbing on ReactorState)
over the legacy element-tag fallback. RefreshRealizedContainers takes an
IItemViewSource instead of TemplatedListElementBase. Both existing
callers pass `n` which now implements the interface — zero call-site
churn.
ClosureItemViewSource + ItemsKeyAdapter are small internal helpers
captured by BindKeyedItemsSource; refreshed on every render so the
realization path always sees the live element's data.
xUnit synthetic tests cover the strategy record shape + the
ITemplatedItemsStrategy marker; end-to-end binding (real ListView, keyed
diff + container realization) is the AppTests.Host fixture territory
shipping with the G2 port subagent.
Baselines preserved:
V1 ON Desc_: 534 ok / 0 failures (no change — legacy CCC path takes
fallback branch because no descriptor uses the new
strategy yet)
V1 OFF Desc_: 534 ok / 0 failures
Reactor.Tests TemplatedItemsStrategyTests: 4/4 passed
Unblocks Port (5) G2 typed templated lists.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…achedAfterAll Closes the Batch E Phase-4 carve-out documented on RelativePanelDescriptor. The new Panel<>.PerChildAttachedAfterAll two-pass shape lets the descriptor build a name→control map across all mounted children, then write WinUI's RelativePanel.SetRightOf / SetBelow / SetAlignLeftWith / etc. against sibling references — the case the per-child PerChildAttached callback can't cover because later siblings aren't mounted yet at per-child invocation time. Body lifted from legacy MountRelativePanel (Reconciler.Mount.cs:3424). Same two passes: pass 1 assigns FrameworkElement.Name from RelativePanelAttached.Name and populates the map; pass 2 walks again and applies sibling-referencing DPs plus the AlignWithPanel booleans. Spec047V1ProtocolDescriptorFixtures — DescRelativePanelMountUpdate gains five assertions that exercise the new path: two named children with B.RightOf = A; verifies Mount populates FrameworkElement.Name and GetRightOf(uiB) returns the actual uiA reference. Selftest: V1 ON Desc_: 539 ok / 0 failures (+5 vs baseline 534) V1 OFF Desc_: 539 ok / 0 failures (+5 — parity preserved) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…platedGridView<T>
Lands the typed templated-list descriptor ports via T-erasure at the
abstract base — same dispatch model the legacy Reconciler.Mount switch
already uses on TemplatedListElementBase, no open-generic registration
required.
Engine extensions:
* V1HandlerRegistry.AddForDerivedTypes + base-walk in TryGet — exact
entries always win; base-derived entries catch every closed-T variant
via the runtime type's base chain. Per-type resolution cached so the
walk is O(1) in steady state.
* Reconciler.RegisterHandlerForDerivedTypes<TBase,TControl> entry point
surfaces the registry capability on the public v1 surface.
* TemplatedItemsErased<TElement,TControl> strategy +
IErasedTemplatedItemsStrategy marker. Strategy is non-generic in
TItem — items + keys read through the element's IKeyedItemSource
implementation. New public IItemViewSource / IKeyedItemSource
interfaces (REACTOR_V1_PREVIEW) document the contract; existing
TemplatedListElementBase bridges its internal abstract GetKeyAt to
the public interface via explicit interface implementation.
* Reconciler.BindErasedKeyedItemsSource — companion to the
BindKeyedItemsSource binder from Engine (2), targeting the same
spec-042 ReactorListState + KeyedListDiff pipeline. SelectionChanged
+ ItemClick wired here (once at Mount; trampolines re-fetch the live
element on each fire) so the descriptor doesn't need a new
ControlEventState payload box.
* DescriptorHandler — bind templated-items strategies BEFORE the prop
loop (same ordering rationale as ItemsHost: SelectedIndex initial
write needs a populated ItemsSource or WinUI silently clamps to -1).
* V1HandlerAdapter — dispatch arm for IErasedTemplatedItemsStrategy
parallel to the existing ITemplatedItemsStrategy arm.
Element hierarchy:
* New empty intermediate marker bases TemplatedListViewElementBase /
TemplatedGridViewElementBase / TemplatedFlipViewElementBase under
TemplatedListElementBase. No fields, no overrides except sealing
ControlKind. Record equality on the leaf TemplatedListViewElement<T>
is unchanged because the EqualityContract still ties to the leaf
type.
* TemplatedListViewElement<T> / TemplatedGridViewElement<T> /
TemplatedFlipViewElement<T> now derive from the intermediate bases.
Existing `: TemplatedListElementBase` pattern matches in legacy
Mount.cs still work (transitive base relationship preserved).
Descriptors:
* TemplatedListViewDescriptor — registers against
TemplatedListViewElementBase. Single registration catches every
closed-T variant via the base-walk. Strategy =
TemplatedItemsErased<>; props = SelectionMode / IsItemClickEnabled /
Header / SelectedIndex via the fluent OneWayConditional surface.
* TemplatedGridViewDescriptor — mirror targeting WinUI.GridView. Same
shape, same binder path.
* FlipView intentionally not ported (FlipView pre-mounts items via a
completely different shape — no ContainerContentChanging, no OC
delta channel). TemplatedFlipViewElementBase reserved in the
hierarchy for symmetry; descriptor port stays carved to Phase 4.
Selftest:
V1 ON Desc_: 556 ok / 0 failures (+17 vs 539 baseline — Desc_TemplatedListView
+ Desc_TemplatedGridView, including keyed-diff
insert/remove cycles + same-ref idempotency)
V1 OFF Desc_: 556 ok / 0 failures (parity preserved)
KLR_ legacy: 73 ok / 0 failures (refactored RefreshRealizedContainers
+ HandleTemplatedContainerContentChanging behavior
neutral for the legacy path)
Carve-outs preserved to Phase 4:
* LazyVStack<T> / LazyHStack<T> — different realization machinery
(ItemsRepeater + IElementFactory, not ListViewBase + CCC). Needs its
own BindKeyedItemsSource arm.
* ItemsRepeater<T> — same reason.
* TemplatedFlipView<T> — pre-mounts items; no realization pipeline to
plug into.
* TreeView / TabView / Pivot — heterogeneous shapes; each its own
descriptor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates spec §14 and the implementation tracker to reflect the four close-out commits on this branch: * Engine (1) Panel<>.PerChildAttachedAfterAll (5f5e0fa) * Engine (2) TemplatedItems<> + BindKeyedItemsSource (f1d9f74) * Port (4) RelativePanel two-pass (5af9a4c) * Port (5) G2 TemplatedListView<T> / TemplatedGridView<T> (42cf0c6) §14 + tracker move two carve-outs out of the carry-list (RelativePanel per-child attached; flat templated lists G2 for ListView/GridView) and re-document the remaining carve-outs with the post-close-out engine constraints: * Lazy*Stack<T> + ItemsRepeater<T> — different realization machinery (ItemsRepeater + IElementFactory rather than ListViewBase + CCC). Strategy shape unchanged; needs new BindErasedKeyedItemsSource arm. * G3 (TreeView / FlipView / TabView / Pivot) — heterogeneous shapes; none share the ListViewBase pipeline. * Expander.HeaderTemplate, TeachingTip.Target, Path.PathDataString, NumberBox coercion — Phase 4 as before. DescriptorVariantFactory registers the two new G2 descriptors against TemplatedListViewElementBase + TemplatedGridViewElementBase via RegisterHandlerForDerivedTypes so the perf bench harness exercises the post-close-out 54-control registration table. Perf re-capture (3×5 advisory on Cloud PC x64) lands under docs/specs/047/phase3-results/CPC-ander-YTZ3O-x64-advisory/ 2026-05-27-phase3-closeout-3x5/ — summary headline is a follow-up commit once the bench finishes. ARM64 stable-AC ratification on LAPTOP-4MEP83VI stays deferred for the §14 ratification gate per the original handoff. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… x64)
Median of n=15 (3 launches × 5 reps) V1 ON (post close-out descriptors)
vs V1 OFF (today), 52 registered descriptors:
Held:
M4 Dispatch_Switch_Cold −20.8% (prior −21.2%)
M5 Dispatch_Switch_Warm −23.9% (prior −24.3%)
M12 Pool_Rent_HotPath +18.5% (prior +20.9%; descriptor-rent
overhead unchanged)
Improved vs prior phase3-final-3x5:
M8 Update_OneLeafChanged +18.9% (prior +25.5%, −6.6pp)
M10 EventHandlerState_Alloc −1.7% (prior +8.7%, volatile)
Regressed vs prior phase3-final-3x5:
M1 Mount_Leaf_NoCallback +21.2% (prior +14.9%, +6.3pp)
M1 regression traced to two new `is`-checks in
V1HandlerAdapter.DispatchChildrenMount (ITemplatedItemsStrategy +
IErasedTemplatedItemsStrategy) that fire ahead of the closed-type
pattern switch on every Mount. Fold into the existing `case` switch in
a Phase 4 perf-tuning pass — not load-bearing for correctness.
M8 improvement is from the DescriptorHandler.Children switch refactor
adding inline-binding for templated-items strategies so non-ItemsHost
Update paths are shorter.
Cloud PC advisory only — does not cite in §13/§14 spec text. ARM64
stable-AC re-capture on LAPTOP-4MEP83VI stays deferred per the
ratification gate.
§14 headline updated with the held / improved / regressed breakdown.
Full per-bench table in summary.md; methodology + reproduce steps in
README.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nder Adds `case WinUI.ItemsRepeater ir:` to `BindErasedKeyedItemsSource` so a descriptor with `TemplatedItemsErased<TElement, WinUI.ItemsRepeater>` can drive realization through the same `ReactorListState` + `KeyedListDiff` pipeline the ListViewBase arm uses, but realizes via `IElementFactory` instead of `ContainerContentChanging` (ItemsRepeater has no CCC channel). New companion `IItemsRepeaterFactorySource` (internal) carries the factory + layout knobs the IR arm needs alongside the public `IKeyedItemSource` the source object also implements. Source objects must implement both; `LazyStackElementBase` will pick the contract up in Port (6). Update path mirrors the legacy Reconciler.Update.cs lazy-stack shape — TryUpdateFactory + keyed-diff + RefreshRealizedItems, with full factory replacement on type-mismatch fallback. V1 ON / V1 OFF Desc_ selftest: 556 ok / 0 failures both flags. Arm is dead code until Port (6) registers a Lazy*Stack descriptor that targets WinUI.ItemsRepeater. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gOneWay Engine (5) audit: `.CoercingOneWay` shape already matches the legacy `UpdateNumberBox` arm's coercion-suppression pattern line-for-line — `WriteSuppressed` wraps the mutate in `ChangeEchoSuppressor.BeginSuppress` exactly as the imperative arm does. No new engine code; ports the NumberBox Min/Max entries in this same commit (carve-forward 15). Drops the matching "Known gaps" doc bullet on the descriptor since the gap is closed. V1 ON / V1 OFF Desc_ selftest: 556 ok / 0 failures both flags. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a property-level Imperative entry shape — `.Imperative(mount, update)` on `ControlDescriptor<TElement, TControl>` plus the matching `ImperativePropEntry`. Mount lambda gets `(TControl, TElement)`; Update lambda gets `(TControl, TElement old, TElement new)`. The Update side exposes BOTH elements so the descriptor can express diffs the per-value get/set shapes can't — chiefly `Path.PathDataString` comparing the *string* field on old vs new while writing the *Geometry* value. Distinct from the existing `Imperative<TElement,TControl>` ChildrenStrategy (child-subtree escape hatch). The new entry only competes against `.OneWayConditional` for property-shaped scenarios. No fast-path; runs every render. Doc comment flags it as last-resort. Dead code until carve-forward (14) lands the PathDataString port. V1 ON Desc_ selftest: 556 ok / 0 failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Engine (2) — adds `.ImperativeBridged(mount, update)` PropEntry shape, the bridged superset of the Engine (4) `.Imperative` entry: lambdas receive `MountContext` / `UpdateContext` so they can call `Reconciler.ReconcileV1Child`. This is the answer to "two-strategy composition": for a secondary Element slot whose write target overlaps with a sibling property (Expander.HeaderTemplate writing into the same `Header` property as the string Header), express it as a PropEntry that reconciles imperatively + a sibling `.OneWayConditional` for the string fallback gated on the Element slot being null. The primary `Children` strategy stays unambiguous. NamedSlots was the first instinct but the prop-loop-then-children-dispatch ordering in V1HandlerAdapter would orphan the Element header on an Element→string transition (prop loop overwrites Header before NamedSlots sees the stale `existing` value). Engine (3) audit — TeachingTip.Target needs no engine extension: legacy `MountTeachingTip` doesn't set Target either; it's documented as `.Set` imperative setter in both paths. Carve-forward (13) closed in the same audit — declarative cross-element-reference resolution is post- Phase-3 polish; not blocking 100% V1 dispatch since Target was never routed through V1 dispatch in either path. V1 ON Desc_ selftest: 556 ok / 0 failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mperativeBridged Ports HeaderTemplate (Element header) through the Engine (2) .ImperativeBridged entry shape. Update lambda calls ReconcileV1Child to preserve descendant component state across re-renders. Sibling string Header entry gated on HeaderTemplate-null so the Element wins when both are set — mirrors the legacy "if (n.HeaderTemplate is not null) ReconcileChild(...) else exp.Header = n.Header" ordering in UpdateExpander. Existing Desc_Expander_* selftest fixtures (string Header path) stay green — 556 ok / 0 failures V1 ON. HeaderTemplate behavior is now covered by the engine path; an Element-header fixture can land later without engine changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…azyHStack<T>) LazyStackElementBase now implements IKeyedItemSource + IItemsRepeaterFactorySource so a single descriptor on the non-generic base catches every closed-T variant via RegisterHandlerForDerivedTypes. The ItemsRepeater arm in Reconciler.BindErasedKeyedItemsSource (Engine (1)) drives Mount/Update through the existing ReactorListState + KeyedListDiff pipeline — same realization plumbing as the legacy MountLazyStack / UpdateLazyStack bodies, no new engine surface. ConfigureLayout reuses the existing WinUI.StackLayout when orientation + spacing match, mirroring the in-place Spacing update from the hand-coded UpdateLazyStack (Reconciler.Update.cs:3109). BuildItemView forwards to the existing ViewBuilder closure. Behavior difference vs hand-coded handler: the legacy path wraps the ItemsRepeater in a ScrollViewer with orientation-appropriate scrollbars; the descriptor port returns ItemsRepeater as the single TControl, so authors who need scrolling wrap externally. ScrollViewerSetters is inert under the descriptor port — documented in LazyStackDescriptor xmldoc. Selftest: 573 / 573 (V1 ON + V1 OFF), 0 failures. Adds 17 Desc_LazyVStack / Desc_LazyHStack fixtures on top of the 556 baseline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ative PathDescriptor's Data entry was previously a .OneWayConditional gated on `e.PathDataString is null && e.Data is not null` — authors who used the SVG-string PathDataString surface stayed on V1 OFF because the engine's value-comparer fast-path couldn't replicate the legacy MountPath / UpdatePath three-strategy branching (XamlReader.Load → pre-built Geometry → PathDataParser.Parse) or the multi-source error context the hand-coded arm accumulates across both surfaces. Replaces that gated Data entry with a single Engine (4) .Imperative entry. Mount lambda calls a private static WriteData(c, e) that mirrors the legacy MountPath body verbatim — strategy 1 lifts a Geometry off a XamlReader.Load'd <Path Data="…"/>; strategy 2 assigns the pre-built e.Data with the legacy ArgumentException context (PathDataString + DataType + xamlNote); strategy 3 falls back to PathDataParser.Parse with the same multi-source rethrow. Update lambda replicates the legacy pathChanged gate (`o.PathDataString` vs `n.PathDataString` for the string surface, `n.Data is not null` for the Geometry surface) and re-invokes WriteData on diff. Lifted parsing locally — the static helper is self-contained and didn't warrant a Reconciler-internal promotion just for this descriptor. FillRule's .OneWayConditional gate drops the redundant `PathDataString is null` clause — the live `c.Data is PathGeometry` check at write time covers both surfaces (matches the legacy arm's behavior). Drops the "PathDataString is escape-hatched" bullet from the descriptor xmldoc's "Known gaps" list; adds a Behavior-parity note that PathDataString now ports via the Engine (4) .Imperative entry. Selftest: 573 / 573 (V1 ON + V1 OFF), 0 failures. Renames the Desc_Path_Data_PathDataStringGate fixture check to Desc_Path_Data_PathDataStringPorted — the assertion flips from "Data write skipped, p.Data unchanged" to "p.Data was replaced and is non-null". Same total check count. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Folds the per-strategy is-checks in DispatchChildrenMount / DispatchChildrenUpdate (V1HandlerAdapter) and Mount / Update (DescriptorHandler) into a single check against a new base interface IItemsBinderStrategy. ITemplatedItemsStrategy and IErasedTemplatedItemsStrategy now inherit from it; the explicit interface implementations on TemplatedItems<> and TemplatedItemsErased<> reference the base directly. Future strategy markers (tree / tab / pivot) that implement IItemsBinderStrategy plug into the same arm — M1 dispatch cost stays at one is-check + one interface call rather than scaling with the number of strategy variants. V1 ON / V1 OFF Desc_ selftest: 573 ok / 0 failures both flags. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a "Phase 3 finish" subsection to §14 documenting Engines (1)–(5), Port (6) Lazy*Stack G2, carve-forwards (12)+(14)+(15) (and (13) via audit), and the dispatch consolidation. Updates the Phase 3 close-out carve-out list — items that landed flip to checked with a one-line landing note; remaining items (Port (7) ItemsRepeater<T> element/DSL work + G3 Tree/FlipView/Tab/Pivot strategies) move to a "Phase 3 finish carry-forwards" list with the specific engine work each needs. ARM64 stable-AC ratification gate explicitly called out as the last open §14 item. Owner+date assignment to be appended once filed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-captures the 13-bench micro suite on `CPC-ander-YTZ3O` (x64, Cloud PC, not stable AC) with the Phase 3 finish branch tip including the dispatch consolidation. Median of n=15 (3 launches × 5 reps). Headline vs prior `phase3-closeout-3x5/`: - Held: M4 -20.2% / M5 -17.8% — dispatch wins persist with the +1 base-derived descriptor (LazyStackDescriptor). - M1 +20.7% (was +21.2%) — dispatch consolidation's structural fold reduces instruction count but didn't recover the close-out's +6.3pp on this Cloud-PC run. A genuine M1 fix likely needs Phase 4 perf tuning that lifts the binder check into the existing pattern switch's `case` arm rather than a leading `if`-block. - M8 +2.9pp / M12 +12.2pp — new regressions vs close-out. M12 has trended volatile across the last three captures (±15pp) and should be confirmed on stable AC. No bench exceeds §13 Q1 reopen threshold. The structural wins (the new IItemsBinderStrategy single-marker arm) are in place; absolute numbers track the close-out baseline within Cloud-PC noise. §14 Phase 3 finish narrative updated with the actual numbers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…abView / Pivot Lands the four G3 typed-list / heterogeneous-items descriptors using two new ChildrenStrategy variants and one strategy reuse: - **TreeChildren<TElement, TControl>** (new) — hierarchical TreeView binder. Reads a TreeViewNodeData tree from the element, builds the matching WinUI.TreeViewNode tree on RootNodes. Mounts per-node ContentElement through Reconciler when any node uses one (and picks SharedContentControlTemplate.Value as the ItemTemplate); otherwise uses the new TreeViewTextItemTemplate for text-only nodes. Update is positional rebuild — old ContentElement UI subtrees unmount before the WinUI tree clears. Implements IItemsBinderStrategy so dispatch goes through the consolidated arm landed earlier this branch. - **TabItemsHost<TElement, TControl, TItem>** (new) — heterogeneous items host shared by TabView (Port (10)) and Pivot (Port (11)). Each item declares header + Element content + a CreateContainer lambda that builds the per-control container (TabViewItem with Header/IsClosable/IconSource; PivotItem with Header/Content). Same positional rebuild + ContentControl-based unmount walk as the legacy paths. Implements IItemsBinderStrategy. - **FlipView (Port (9))** — reuses the existing ItemsHost<> strategy. FlipView.Items is a flat IList<object> sink; ItemsHost already pre-mounts each Element item and adds the mounted UIElement. No PreMountedItems strategy needed (handoff alternative (b) confirmed). Descriptors cover: - TreeView: Nodes via TreeChildren; SelectionMode / CanDragItems / AllowDrop / CanReorderItems; OnItemInvoked + OnExpanding. - FlipView: Items via ItemsHost; SelectedIndex round-trip. - TabView: Tabs via TabItemsHost; SelectedIndex round-trip; OnTabCloseRequested + OnAddTabButtonClick. TabStripHeader/Footer and §2.4 docking drag pipeline stay carved (documented in xmldoc). - Pivot: Items via TabItemsHost; Title via .OneWayConditional; SelectedIndex. 29 new fixtures across the four descriptors. Full Desc_ selftest: V1 ON 602 ok / 0 failures; V1 OFF 602 ok / 0 failures (baseline 573 + 29 new). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves Ports (8) TreeView, (9) FlipView, (10) TabView, (11) Pivot from the carry-forwards list into the landed-engine section. Documents the two new ChildrenStrategy variants (TreeChildren, TabItemsHost), the FlipView reuse of ItemsHost, and the carved scope on TabView (strip header/footer, docking drag, pinnable headers). After this commit only Port (7) ItemsRepeater<T> remains as a Phase 3 finish carry-forward — that one is blocked on a missing ItemsRepeaterElement<T> element + DSL surface. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…orward) Closes the last Phase 3 finish carry-forward — the missing ItemsRepeater<T> element + DSL surface that blocked 100% V1 dispatch coverage. Three new pieces ship in this commit: 1. **ItemsRepeaterElementBase + ItemsRepeaterElement<T>** (Element.cs) — non-generic base + typed peer modeled on LazyStackElementBase. The base implements IKeyedItemSource + IItemsRepeaterFactorySource so it plugs into Engine (1)'s ItemsRepeater arm without any new engine work. Distinct from LazyStackElementBase: no hard-coded StackLayout (the element exposes a nullable Layout property — author supplies any WinUI.Layout instance, default = WinUI's own ItemsRepeater default) and no ScrollViewer wrap (the rendered control is the bare ItemsRepeater; authors who need scrolling host it externally). 2. **Legacy MountItemsRepeater / UpdateItemsRepeater arms** in Reconciler.Mount.cs / Reconciler.Update.cs — there was no legacy arm before this port (the element type is new). The legacy arms mirror the existing MountLazyStack / UpdateLazyStack TryUpdateFactory + keyed-diff + RefreshRealizedItems pipeline so V1 OFF parity holds. 3. **ItemsRepeaterDescriptor** — base-derived single descriptor on ItemsRepeaterElementBase. Children = TemplatedItemsErased<> targeting WinUI.ItemsRepeater; reuses every engine partial Port (6) exercised. No event surface (ItemsRepeater itself doesn't raise selection / item-click events). DSL surface added in Dsl.cs — `ItemsRepeater<T>(items, keySelector, viewBuilder)` factory plus the IReactorKeyed-typed overload, matching the LazyVStack / LazyHStack surface. 11 new fixtures (Desc_ItemsRepeater_*). Full Desc_ selftest: V1 ON 613 ok / 0 failures; V1 OFF 613 ok / 0 failures (prior baseline 602 + 11 new). 100% V1 dispatch coverage now reached for every typed items host — only the engine arms that were carry-forwards in close-out remain in production legacy code (those flip in Phase 4 cleanup). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Flips Port (7) ItemsRepeater<T> from carry-forward to landed. §14's "Phase 3 finish carry-forwards" list now reads "none remaining" — every typed-items host has a V1 descriptor and the engine surface is complete. The production swap (RegisterV1BuiltInHandlers wiring + legacy switch deletion) is Phase 4 cleanup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…empted) Replaces the "100% V1 dispatch coverage now reached" claim in the tracker with the precise statement: every typed-items host family scoped by Phase 3's batch list has a V1 descriptor. Adds an explicit "Phase 3 deferred / not-attempted" enumeration to both §14 and the tracker covering the long tail that was never on the Phase 3 batch list — ContentDialog / Flyout / Popup family, navigation / title-bar / media family, ItemsView / ItemContainer / plain GridViewElement, interop + a11y wrappers, and the Reactor composition primitives that likely should stay out of the V1 handler protocol entirely. Also flags TemplatedFlipViewElement<T> as the one genuine engine gap still carried from Phase 3 close-out (FlipView lacks ContainerContentChanging; would need a PreMountedItems ChildrenStrategy). The intermediate base TemplatedFlipViewElementBase is reserved in the element hierarchy for that future port. No code changes — docs accuracy only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Spec 047 §14 Phase 3 close-out + finish. Bundles two staged spec-047 branches into one reviewable unit that completes the typed-items host descriptor table and consolidates dispatch. Adds five engine shapes (ImperativeBridged, Imperative, ItemsRepeater binder arm; TeachingTip / NumberBox audits), ports six controls plus a new ItemsRepeater<T> element type, and folds the per-strategy is-check dispatch in V1HandlerAdapter / DescriptorHandler behind a new IItemsBinderStrategy marker. Closes carry-forwards from PR #436 (Expander.HeaderTemplate, Path.PathDataString, NumberBox coercion, Lazy*Stack / ItemsRepeater / G3 typed lists).
Changes:
- Engine shapes (
.Imperative,.ImperativeBridged, ItemsRepeater dispatch arm) + dispatch consolidation around newIItemsBinderStrategy. - Descriptor ports for WrapGrid, Frame, RichTextBlock, NumberBox, CalendarView, Image events, Path.Data, InfoBar.ActionButton, ListBox/ComboBox/RadioButtons items, TemplatedListView/GridView, Lazy*Stack, ItemsRepeater, TreeView, FlipView, TabView, Pivot.
- New
ItemsRepeaterElement<T>+ DSL factory; spec §14 narrative + Phase-3 deferred enumeration; advisory Cloud-PC x64 perf captures.
Show a summary per file
| File | Description |
|---|---|
| src/Reactor/Core/Element.cs | New intermediate TemplatedXxxElementBase records and ItemsRepeaterElement<T>; LazyStackElementBase implements binder/source interfaces. |
| src/Reactor/Core/Reconciler.cs | Adds RegisterHandlerForDerivedTypes, CreateFlyoutForDescriptor, item-view-source storage, TreeView item template. |
| src/Reactor/Core/Reconciler.Mount.cs / Update.cs | Legacy mount/update arms for ItemsRepeater; sharing helpers widened to internal for descriptor reuse. |
| src/Reactor/Core/V1HandlerRegistry.cs | Base-derived registration + cached base-walk in TryGet. |
| src/Reactor/Core/V1Protocol/V1HandlerAdapter.cs | Consolidated dispatch arm via IItemsBinderStrategy; Panel<> per-child attached + after-all callbacks; flat ItemsHost<> rebuild. |
| src/Reactor/Core/V1Protocol/Descriptor/ControlDescriptor.cs | New entry shape APIs: .Imperative, .ImperativeBridged, .OneWayBridged, .Immediate, .CollectionDiffControlled. |
| src/Reactor/Core/V1Protocol/Descriptor/DescriptorHandler.cs | Inlines ItemsHost / items-binder dispatch before the prop loop. |
| src/Reactor/Core/V1Protocol/Descriptor/Descriptors/*.cs | New + updated descriptors across batches B/C/E/F/G1, close-out, finish. |
| src/Reactor/Core/V1Protocol/ControlEventPayloads.cs | New payload boxes for TreeView/FlipView/TabView/Frame/CalendarView and NumberBox immediate-mode fields. |
| src/Reactor/Core/V1Protocol/Handlers/ListViewHandler.cs | Drops the marker ItemsHost strategy — delegate body fully owns children dispatch. |
| src/Reactor/Core/IItemsRepeaterFactorySource.cs (new) | Internal companion to IKeyedItemSource for ItemsRepeater hosts. |
| src/Reactor/Core/Internal/IItemViewSource.cs (new) | Public uniform view-source contract consumed by templated-items realization. |
| src/Reactor/Elements/Dsl.cs | DSL factory for ItemsRepeater<T>. |
| tests/perf_bench/PerfBench.ControlModel/Variants/DescriptorVariantFactory.cs | Registrations for new descriptors incl. base-derived templated-list registrations. |
| tests/Reactor.AppTests.Host/SelfTest/SelfTestFixtureRegistry.cs | Adds entries for ~25 new Desc_ fixtures. |
| tests/Reactor.Tests/Spec047/V1Protocol/*.cs | Shape tests for new strategies; ListView handler children-strategy assertion flipped to null. |
| docs/specs/tasks/047-extensible-control-model-implementation.md | §14 Phase 3 finish narrative + deferred enumeration. |
| docs/specs/047/phase3-results/...-advisory/ (new) | Advisory Cloud-PC x64 3x5 perf captures, summaries, aggregate script. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 56/63 changed files
- Comments generated: 2
| .OneWayConditional( | ||
| get: static e => e.FillRule, | ||
| set: static (c, v) => | ||
| { | ||
| if (c.Data is PathGeometry pg && pg.FillRule != v) pg.FillRule = v; | ||
| }, | ||
| shouldWrite: static e => e.Data is PathGeometry) |
| private sealed class ElementReferenceComparer : IEqualityComparer<Element?> | ||
| { | ||
| public static readonly ElementReferenceComparer Instance = new(); | ||
| public bool Equals(Element? x, Element? y) => ReferenceEquals(x, y); | ||
| public int GetHashCode(Element obj) => global::System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); | ||
| } |
b67b1c7 to
3e28c36
Compare
| if (mountElements && data.ContentElement is not null) | ||
| node.Content = reconciler.Mount(data.ContentElement, requestRerender); | ||
| else | ||
| node.Content = data; |
Five findings cleared: 1. **PathDescriptor FillRule live-control gate (Copilot, correctness).** The `.OneWayConditional` gate was `shouldWrite: e => e.Data is PathGeometry`, which is FALSE for the PathDataString surface (where `e.Data` is null but `XamlReader.Load` / `PathDataParser.Parse` produces a `PathGeometry` on `c.Data`). Diverged from legacy `UpdatePath`'s `p.Data is PathGeometry pg => pg.FillRule = n.FillRule` which inspects the LIVE control's resolved Data. Switched to `.OneWay` so the entry runs every Mount + on every change to `e.FillRule`; the set lambda's inner `c.Data is PathGeometry` check is now the actual gate. Matches legacy exactly. 2. **V1HandlerAdapter redundant `pairs is not null` check (CodeQL, x2).** `pairs` is non-null exactly when `afterAll` is non-null (conditional allocation upstream). Dropped the redundant subcondition on both Mount and Update Panel<> arms; kept the `afterAll` guard with `pairs!`. 3. **`ElementReferenceComparer` duplicated in 3 button descriptors (Copilot).** Promoted to a single shared internal type `V1Protocol.Descriptor.Descriptors.ElementReferenceComparer`; deleted the three copies in `DropDownButtonDescriptor`, `SplitButtonDescriptor`, `ToggleSplitButtonDescriptor`. 4. **Element.cs `ConfigureLayout` float `!=` (CodeQL).** Replaced `existing.Spacing != Spacing` with `Math.Abs(...) > 1e-9` per the established spec-047 fixture convention (b091001). 5. **Fixture float `==` checks (CodeQL, x4 sites).** `l2.Spacing == 12`, `l3.Spacing == 16`, `ug.MinRowSpacing == 4`, `sl.Spacing == 4` → `Math.Abs(... - literal) < 1e-9` per b091001. Selftest after: Desc_ V1 ON 613 ok / 0 failures (baseline preserved). Path-specific Desc_Path_*: 19 ok / 0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| catch (global::System.Exception ex) | ||
| { | ||
| xamlReaderError = ex; | ||
| } |
| catch (global::System.Exception ex) | ||
| { | ||
| var xamlNote = xamlReaderError is not null | ||
| ? $" XamlReader.Load also failed: {xamlReaderError.GetType().Name}: {xamlReaderError.Message}. Attempted XAML: {attemptedXaml}" | ||
| : " (XamlReader.Load returned non-Path or wasn't attempted)"; | ||
| throw new global::System.ArgumentException( | ||
| $"Path.Data rejected by WinUI. PathDataString={e.PathDataString ?? "(null)"}; " | ||
| + $"DataType={e.Data.GetType().Name}; inner={ex.Message}.{xamlNote}", ex); | ||
| } |
| { | ||
| global::System.Exception? parserError = null; | ||
| try { c.Data = global::Microsoft.UI.Reactor.Charting.PathDataParser.Parse(pdsFallback); } | ||
| catch (global::System.Exception ex) { parserError = ex; } |
| tv.TabItems[0] is WinUI.TabViewItem tvi0 && (tvi0.Header as string) == "tab-a"); | ||
| H.Check("Desc_TabView_SecondTabClosable", | ||
| tv.TabItems[1] is WinUI.TabViewItem tvi1 && tvi1.IsClosable == false); | ||
| H.Check("Desc_TabView_AddButtonVisible", tv.IsAddTabButtonVisible == true); |
| H.Check("Desc_TabView_AfterUpdate_Count1", tv.TabItems.Count == 1); | ||
| H.Check("Desc_TabView_AfterUpdate_HeaderX", | ||
| tv.TabItems[0] is WinUI.TabViewItem tvi2 && (tvi2.Header as string) == "tab-x"); | ||
| H.Check("Desc_TabView_AfterUpdate_AddButtonHidden", tv.IsAddTabButtonVisible == false); |
| H.Check("Desc_TabView_FirstTabHeader", | ||
| tv.TabItems[0] is WinUI.TabViewItem tvi0 && (tvi0.Header as string) == "tab-a"); | ||
| H.Check("Desc_TabView_SecondTabClosable", | ||
| tv.TabItems[1] is WinUI.TabViewItem tvi1 && tvi1.IsClosable == false); |
…ization.Generator file-lock race Root cause of the recurring CI "Unit Tests" failure on PR #437 and sporadically on main: `src/Reactor.Cli/Reactor.Cli.csproj` declares a `BeforeBuild` Exec target that shells out to a nested `dotnet build Reactor.SignaturesGen.csproj`. SignaturesGen ProjectReferences `Reactor.csproj`, which ProjectReferences `Reactor.Localization.Generator`. The OUTER build (`dotnet test tests/Reactor.Tests`) already produces `obj/x64/Debug/netstandard2.0/Reactor.Localization.Generator.dll` because `Reactor.Tests.csproj` directly references that project too. The outer and nested `dotnet build` processes have their own VBCSCompiler instances and don't coordinate — they race on the same output dll. The second writer hits CSC error CS2012 ("Cannot open ... for writing -- file may be locked by 'VBCSCompiler'"). The race window scales with project size, which is why PR #437's increased compile load triggers it consistently. The committed `skills/reactor.api.txt` is what pack (`Reactor.csproj:134`) and embed (`Reactor.Cli.csproj:84`) consume, both gated `Condition="Exists(...)"`. CI never actually needs to regen api.txt — it's a dev-machine convenience step. Fix: add `and '$(CI)' != 'true'` to the RunSignaturesGen Target's Condition. GitHub Actions sets `CI=true` as an env var; MSBuild picks it up as `$(CI)`. Verified locally: - `unset CI; dotnet build src/Reactor.Cli` — RunSignaturesGen fires (nested build runs, fast-up-to-date-checks skip everything since outputs are current). - `CI=true dotnet build src/Reactor.Cli` — RunSignaturesGen fully skipped (zero "RunSignaturesGen" / "SignaturesGen.csproj" mentions in verbose log). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Closes Phase 3 close-out's named carve-outs (Expander.HeaderTemplate, TeachingTip.Target, Path.PathDataString, NumberBox coercion, Lazy*Stack G2, G3 typed lists) and ships the missing
ItemsRepeater<T>element + descriptor, reaching the precise scope goal: every typed-items host family scoped by §14's Phase 3 batch list now has a V1 descriptor, with the engine surface complete.Rebased onto current
mainafter PR #436 merged. 21 commits (6 close-out + 15 finish) on top of522c9407(Phase 3-final squash-merge). Rebase verified by V1 ON + V1 OFFDesc_selftest passing 613/613 ok / 0 failures both flags post-rebase.Engine work (5 shapes added or audited)
Reconciler.BindErasedKeyedItemsSourcegains acase WinUI.ItemsRepeaterarm. New internalIItemsRepeaterFactorySourcecompanion interface to the publicIKeyedItemSource(factory + layout knobs the ItemsRepeater realization path needs)..ImperativeBridged(mount, update)PropEntry. Bridged superset of.Imperativeso a property-level entry can callReconciler.ReconcileV1Child— answer to "two-strategy composition" at the property level (Expander.HeaderTemplate's host slot stays unambiguous; the secondary Element slot reconciles imperatively).MountTeachingTipdoesn't setTargeteither; setter escape is the contract in both paths..Imperative(mount, update)PropEntry. Property-level escape hatch with old + new TElement (Update lambda compares string fields while writing Geometry — the motivating case isPath.PathDataString)..CoercingOneWayalready matchedUpdateNumberBox's suppression pattern line-for-line.Dispatch consolidation
ITemplatedItemsStrategyandIErasedTemplatedItemsStrategyinherit from a new baseIItemsBinderStrategy.V1HandlerAdapter+DescriptorHandlercollapse their per-strategyis-checks into one base-interface check. New G3 strategies plug into the same arm — M1 cost stays at oneis-check + one interface call as new strategies arrive.Ports (6 controls + new element type)
LazyStackElementBasecatches every closed-TLazyVStackElement<T>/LazyHStackElement<T>variant.ItemsRepeaterElementBase+ItemsRepeaterElement<T>records modeled onLazyStackElementBase; legacyMountItemsRepeater/UpdateItemsRepeaterarms added for V1 OFF parity; DSL factory inDsl.cs.TreeChildren<TElement, TControl>ChildrenStrategy (hierarchical, positional rebuild, recursiveContentElementmount).ItemsHost<>strategy (FlipView.Items is a flatIList<object>sink). Note: typedTemplatedFlipViewElement<T>stays carved per close-out (FlipView lacksContainerContentChanging).TabItemsHost<TElement, TControl, TItem>strategy. Per-descriptorCreateContainerlambda (TabViewItem/PivotItem). Common-case TabView ports;TabStripHeader/TabStripFooterand spec 045 §2.4 docking drag pipeline / §2.2 pinnable headers stay on the legacy arm.Carve-forward ports
.ImperativeBridged. Sibling stringHeader.OneWayConditionalgated onHeaderTemplate is nullso Element header wins..Imperative. Single entry drives all three legacy strategies (XamlReader.Load → pre-built Geometry → PathDataParser.Parse) end-to-end with the same multi-sourceArgumentExceptionrethrow path..CoercingOneWay.Tests
V1 ON / V1 OFF baseline progression (Desc_ filter, both flags must match):
Full V1 ON selftest: 4239 ok / 0 failures. Full V1 OFF: 4238 ok / 1 known flake (
NativeDocking_Reliability_UseEffectCleanup_BodyRemovedOnPaneClose→Reliability_Effect_BodyRendered; isolated re-run 7/7 ok).Advisory perf (Cloud-PC x64, NOT authoritative)
Re-capture under
docs/specs/047/phase3-results/CPC-ander-YTZ3O-x64-advisory/2026-05-28-phase3-finish-3x5/. Headline vs prior2026-05-27-phase3-closeout-3x5/:ARM64 stable-AC ratification capture on
LAPTOP-4MEP83VIremains the §14 ratification gate — owner/date assignment pending.§14 + tracker updates
TemplatedFlipViewElement<T>as the one engine-gap close-out carve).Test plan
src/Reactor,tests/Reactor.AppTests.Host,tests/Reactor.Tests) at-c Release -p:Platform=x64.dotnet test tests/Reactor.Tests— full xunit suite (failed at build stage under file-lock contention in the close-out session; re-run in a clean shell).docs/specs/047-extensible-control-model.md.🤖 Generated with Claude Code