Commit 836c301
spec(047): Phase 3 close-out + finish — 100% V1 dispatch for typed-items hosts (#437)
* spec(047): Phase 3 close-out — Panel<>.PerChildAttachedAfterAll two-pass 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>
* spec(047): Phase 3 close-out — TemplatedItems<> strategy + Reconciler.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>
* spec(047): Phase 3 close-out Port (4) — RelativePanel via PerChildAttachedAfterAll
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>
* spec(047): Phase 3 close-out Port (5) G2 — TemplatedListView<T> / TemplatedGridView<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>
* spec(047): Phase 3 close-out — docs + perf bench registry
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>
* spec(047): Phase 3 close-out — advisory perf re-capture (3×5 Cloud PC 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>
* spec(047): Phase 3 finish — Engine (1) ItemsRepeater arm on erased binder
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>
* spec(047): Phase 3 finish — Engine (5) NumberBox.Min/Max via .CoercingOneWay
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>
* spec(047): Phase 3 finish — Engine (4) .Imperative property escape hatch
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>
* spec(047): Phase 3 finish — Engines (2)+(3): two-strategy + Target audit
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>
* spec(047): Phase 3 finish — Carve (12) Expander.HeaderTemplate via .ImperativeBridged
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>
* spec(047): Phase 3 finish — Port (6) Lazy*Stack G2 (LazyVStack<T> / LazyHStack<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>
* spec(047): Phase 3 finish — Carve (14) Path.PathDataString via .Imperative
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>
* spec(047): Phase 3 finish — dispatch consolidation (single is-check arm)
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>
* spec(047): Phase 3 finish — §14 + tracker close-out narrative
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>
* spec(047): Phase 3 finish — advisory perf re-capture (3×5 Cloud PC x64)
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>
* spec(047): Phase 3 finish — Ports (8)-(11) G3 TreeView / FlipView / TabView / 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>
* spec(047): Phase 3 finish — §14 + tracker update for G3 ports landing
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>
* spec(047): Phase 3 finish — Port (7) ItemsRepeater<T> (closes carry-forward)
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>
* spec(047): Phase 3 finish — §14 + tracker close-out (Port (7) landed)
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>
* spec(047): Phase 3 finish — honest scope accounting (deferred/not-attempted)
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>
* spec(047): Phase 3 finish — address PR #437 CR feedback
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>
* build(cli): skip SignaturesGen regen in CI to break the Reactor.Localization.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>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 522c940 commit 836c301
49 files changed
Lines changed: 5662 additions & 185 deletions
File tree
- docs/specs
- 047/phase3-results/CPC-ander-YTZ3O-x64-advisory
- 2026-05-27-phase3-closeout-3x5
- 2026-05-28-phase3-finish-3x5
- tasks
- src
- Reactor.Cli
- Reactor
- Core
- Internal
- V1Protocol
- Descriptor
- Descriptors
- Elements
- tests
- Reactor.AppTests.Host/SelfTest
- Fixtures
- Reactor.Tests/Spec047/V1Protocol
- perf_bench/PerfBench.ControlModel/Variants
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Large diffs are not rendered by default.
Lines changed: 121 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
Lines changed: 122 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
0 commit comments