Skip to content

spec(047): Phase 3 close-out + finish — 100% V1 dispatch for typed-items hosts#437

Merged
codemonkeychris merged 23 commits into
mainfrom
spec/047-phase3-finish
May 28, 2026
Merged

spec(047): Phase 3 close-out + finish — 100% V1 dispatch for typed-items hosts#437
codemonkeychris merged 23 commits into
mainfrom
spec/047-phase3-finish

Conversation

@codemonkeychris
Copy link
Copy Markdown
Collaborator

@codemonkeychris codemonkeychris commented May 28, 2026

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 main after PR #436 merged. 21 commits (6 close-out + 15 finish) on top of 522c9407 (Phase 3-final squash-merge). Rebase verified by V1 ON + V1 OFF Desc_ selftest passing 613/613 ok / 0 failures both flags post-rebase.

Engine work (5 shapes added or audited)

  1. Engine (1)Reconciler.BindErasedKeyedItemsSource gains a case WinUI.ItemsRepeater arm. New internal IItemsRepeaterFactorySource companion interface to the public IKeyedItemSource (factory + layout knobs the ItemsRepeater realization path needs).
  2. Engine (2).ImperativeBridged(mount, update) PropEntry. Bridged superset of .Imperative so a property-level entry can call Reconciler.ReconcileV1Child — answer to "two-strategy composition" at the property level (Expander.HeaderTemplate's host slot stays unambiguous; the secondary Element slot reconciles imperatively).
  3. Engine (3) — TeachingTip.Target audit. No engine code: legacy MountTeachingTip doesn't set Target either; setter escape is the contract in both paths.
  4. Engine (4).Imperative(mount, update) PropEntry. Property-level escape hatch with old + new TElement (Update lambda compares string fields while writing Geometry — the motivating case is Path.PathDataString).
  5. Engine (5) — NumberBox.Min/Max coercion audit. No new surface; .CoercingOneWay already matched UpdateNumberBox's suppression pattern line-for-line.

Dispatch consolidation

ITemplatedItemsStrategy and IErasedTemplatedItemsStrategy inherit from a new base IItemsBinderStrategy. V1HandlerAdapter + DescriptorHandler collapse their per-strategy is-checks into one base-interface check. New G3 strategies plug into the same arm — M1 cost stays at one is-check + one interface call as new strategies arrive.

Ports (6 controls + new element type)

  • Lazy*Stack G2 (Port 6) — single base-derived descriptor on LazyStackElementBase catches every closed-T LazyVStackElement<T> / LazyHStackElement<T> variant.
  • ItemsRepeater<T> (Port 7) — new ItemsRepeaterElementBase + ItemsRepeaterElement<T> records modeled on LazyStackElementBase; legacy MountItemsRepeater / UpdateItemsRepeater arms added for V1 OFF parity; DSL factory in Dsl.cs.
  • TreeView (Port 8) — new TreeChildren<TElement, TControl> ChildrenStrategy (hierarchical, positional rebuild, recursive ContentElement mount).
  • FlipView (Port 9) — reuses existing ItemsHost<> strategy (FlipView.Items is a flat IList<object> sink). Note: typed TemplatedFlipViewElement<T> stays carved per close-out (FlipView lacks ContainerContentChanging).
  • TabView + Pivot (Ports 10 + 11) — new shared TabItemsHost<TElement, TControl, TItem> strategy. Per-descriptor CreateContainer lambda (TabViewItem / PivotItem). Common-case TabView ports; TabStripHeader / TabStripFooter and spec 045 §2.4 docking drag pipeline / §2.2 pinnable headers stay on the legacy arm.

Carve-forward ports

  • (12) Expander.HeaderTemplate — via Engine (2) .ImperativeBridged. Sibling string Header .OneWayConditional gated on HeaderTemplate is null so Element header wins.
  • (13) TeachingTip.Target — closed by Engine (3) audit (setter escape contract).
  • (14) Path.PathDataString — via Engine (4) .Imperative. Single entry drives all three legacy strategies (XamlReader.Load → pre-built Geometry → PathDataParser.Parse) end-to-end with the same multi-source ArgumentException rethrow path.
  • (15) NumberBox.Min/Max — via Engine (5) .CoercingOneWay.

Tests

V1 ON / V1 OFF baseline progression (Desc_ filter, both flags must match):

  • Close-out: 556 / 556
  • After Port (6) Lazy*Stack: 573 / 573 (+17)
  • After G3 ports (8–11): 602 / 602 (+29)
  • After Port (7) ItemsRepeater<T>: 613 / 613 (+11)

Full V1 ON selftest: 4239 ok / 0 failures. Full V1 OFF: 4238 ok / 1 known flake (NativeDocking_Reliability_UseEffectCleanup_BodyRemovedOnPaneCloseReliability_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 prior 2026-05-27-phase3-closeout-3x5/:

  • Held: M4 −20.2% / M5 −17.8% (dispatch wins persist with +1 base-derived registration).
  • M1 +20.7% (was +21.2%) — dispatch consolidation reduces instruction count but Cloud-PC numbers track close-out. Genuine M1 fix needs Phase 4 perf tuning (case-arm fold, not is-block).
  • M8 +21.8% / M12 +30.7% — within Cloud-PC volatility band; both should be confirmed on stable AC.
  • No bench exceeds §13 Q1 reopen threshold.

ARM64 stable-AC ratification capture on LAPTOP-4MEP83VI remains the §14 ratification gate — owner/date assignment pending.

§14 + tracker updates

  • Phase 3 finish narrative added to §14 documenting engines + ports + dispatch consolidation.
  • "Phase 3 finish carry-forwards: none remaining for typed-items host families scoped in Phase 3."
  • New "Phase 3 deferred / not-attempted" section in both §14 and tracker enumerating the long tail that was never on the Phase 3 batch list (dialogs / navigation / media / specialized controls + Reactor composition primitives that likely should stay above V1 entirely + TemplatedFlipViewElement<T> as the one engine-gap close-out carve).

Test plan

  • Build per-project (src/Reactor, tests/Reactor.AppTests.Host, tests/Reactor.Tests) at -c Release -p:Platform=x64.
  • Run V1 ON Desc_ selftest — expect 613 ok / 0 failures.
  • Run V1 OFF Desc_ selftest — expect 613 ok / 0 failures.
  • Run full V1 ON selftest — expect 4239 ok / 0 failures (re-run any known-flake NativeDocking / Reliability fixtures).
  • Run 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).
  • Visually verify §14 Phase 3 finish narrative + Phase 3 deferred enumeration in docs/specs/047-extensible-control-model.md.

🤖 Generated with Claude Code

@codemonkeychris codemonkeychris requested a review from Copilot May 28, 2026 17:02
Base automatically changed from spec/047-phase3-final to main May 28, 2026 17:03
codemonkeychris and others added 21 commits May 28, 2026 10:04
…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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 new IItemsBinderStrategy.
  • 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

Comment on lines +76 to +82
.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)
Comment on lines +57 to +62
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);
}
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 38/44 changed files
  • Comments generated: 0 new

Comment thread src/Reactor/Core/V1Protocol/V1HandlerAdapter.cs Fixed
Comment thread src/Reactor/Core/V1Protocol/V1HandlerAdapter.cs Fixed
Comment thread src/Reactor/Core/Element.cs Fixed
Comment on lines +377 to +380
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>
Comment on lines +157 to +160
catch (global::System.Exception ex)
{
xamlReaderError = ex;
}
Comment on lines +169 to +177
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>
@codemonkeychris codemonkeychris merged commit 836c301 into main May 28, 2026
16 checks passed
@codemonkeychris codemonkeychris deleted the spec/047-phase3-finish branch May 28, 2026 18:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants