Skip to content

spec-047 Phase 4 — V1 migration close-out (V1 is the production path)#455

Merged
codemonkeychris merged 28 commits into
mainfrom
spec-047-phase4-v1-migration-closeout
May 30, 2026
Merged

spec-047 Phase 4 — V1 migration close-out (V1 is the production path)#455
codemonkeychris merged 28 commits into
mainfrom
spec-047-phase4-v1-migration-closeout

Conversation

@codemonkeychris
Copy link
Copy Markdown
Collaborator

Spec-047 Phase 4 — V1 migration close-out

Closes out the spec-047 "Fully Extensible Control Model" migration. V1 is now the unconditional production path; the migration is closed. This PR is the full Phase 4 set (27 commits, §4.0.2 → §4.10) on top of main (#443, which still defaulted V1 OFF).

Net: 181 files, +4,825 / −12,371 (the large deletion is dead-code removal — legacy dispatch switch, A|B harness, flag plumbing).

Opened primarily to validate on other machines — in particular the ARM64 baseline box (LAPTOP-4MEP83VI) for the two deferred perf/byte-gate measurements (see "Outstanding" below).

What landed

  • §4.0.2 / §4.0.5 — NavigationHost unmount ported to a V1 handler; XamlHost/XamlPage own their V1 registration.
  • §4.1 — flipped UseV1Protocol ON by default (then removed entirely, below).
  • §4.5 — deleted the legacy MountXxx/UpdateXxx dispatch switch + ~60 orphaned handler bodies; finalized the overlay/TabView genuine ports. Dispatch is now V1 registry → external _typeRegistry → composition-primitive switch (no legacy fallthrough).
  • §4.6 — removed all A|B / UseV1Protocol dead code: the flag, AppContext switch, env var, dual-flag selftest harness, ReactorV2 perf-project duplicates, checkpoint runner. There is one public Reconciler(ILogger? logger = null) ctor; V1 is unconditional.
  • §4.7 — graduated the public author surface out of [Experimental("REACTOR_V1_PREVIEW")] (157 attrs / 110 files) and locked it; closed KD-4 (external typed-event surface); retired the obsolete REACTOR1001/1003 analyzers (REACTOR1002 stays).
  • §4.2 / §8 — echo handling settled as a documented HYBRID (spec §8.3): synchronous single-value round-trips use a value-diff arm (PendingEchoMatch + ArmExpectedEcho/ShouldSuppressEcho, opt-in valueDiffEcho); the suppress-counter (ChangeEchoSuppressor) is intentionally retained as the fallback for doubles/coercion/collection/deferred-strings/Expander/CheckBox-path-B/ApplySetters-scope/public WriteSuppressed. WriteSuppressed keeps its public signature.
  • §4.3 — split the monolithic EventHandlerState: the WinUI routed-input family → ModifierEventHandlerState (lazy on ReactorState.Modifiers); control-intrinsic events → per-control ControlEventStateBox payloads. Migrated the last live holdouts (Button.Click, NumberBox immediate), deleted dead Image/ScrollViewer/ScrollView wiring + orphaned fields, added §9.2 pool-lifecycle hazard fixtures (no-double-subscribe across pool reuse, HandlerType-mismatch reset, dual-return idempotency, intrinsic-only alloc-shape, AddRawRoutedHandler survival). The monolith is gone.
  • §4.4 — bucketed the 14 cross-cutting Element base fields into a value-equality ElementExtras record behind one Element.Extensions slot, via the proven ElementModifiers shim pattern (zero call-site edits — only Element.cs changed). Landed the §11.6 byte-gate target constants (PerformanceBudgets.cs: 407 / 1520 / 19200).
  • §4.8 — promoted the author guide to stable; rewrote the stale AGENTS.md sections (new-control authoring → V1 descriptor model; echo → §8.3 hybrid; per-element-state → ModifierEventHandlerState + ControlEventStateBox).
  • §4.10 — dead-code sweep (grep-clean for UseV1Protocol/REACTOR_USE_V1_PROTOCOL/ReactorV2/registerBuiltinHandlers/EventHandlerState-monolith); tracker + spec §14 status updated.

Validation (x64 dev machine)

  • Full solution build (Reactor.slnx -p:Platform=x64): 0 errors.
  • xunit (Reactor.Tests): 9128 passed / 0 failed.
  • Full selftest: 0 failures modulo the known docking-family flakiness (DockHooks_* / SidePopup_*) — these fail intermittently under full-suite headless load (count varied 7→6 across runs) and pass deterministically green when filtered. Pre-existing, not a regression from this PR.

Outstanding — baseline-machine-only (the reason for cross-machine validation)

Both are code-complete; only the measurement/ratification is blocked on the ARM64 baseline machine LAPTOP-4MEP83VI:

  1. §4.9 — ARM64 stable-AC perf ratification. Run §15.3 M1–M13 (randomized/interleaved ordering, cooldowns, CPU-clock telemetry) + L2/L3/L4/L6 macros + L13/L14 AOT, check §15.6 budgets vs the ReactorToday baseline, confirm/close KD-3 (apply the M1 binder-check fold only if M1 is over budget). Runbook is in the §4.9 section of the close-out task doc.
  2. §4.4 — §11.6 hard byte-gate measurement/enforcement (M1 ≤407 / M2 ≤1520 / M3 ≤19200). The bucketed base ships and the target constants are landed; only the merge-blocking measurement remains.

Note for the spec author

The original exit-gate/§14 wording said "delete ChangeEchoSuppressor," but that was settled as the §8.3 hybrid (suppressor retained). The close-out docs are reconciled to the hybrid; the literal exit-gate wording should be formally ratified.

🤖 Generated with Claude Code

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 encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

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 encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

codemonkeychris added a commit that referenced this pull request May 30, 2026
…nt bucketing)

CR item #1 (GridView/ListBox deferred-echo) — revert SelectedIndex to the
causal counter (ShouldSuppress/WriteSuppressed; drop the §8 value-diff arm),
restoring main's mechanism. Empirically probed on this box: post-realization
SelectionChanged is synchronous (deferral is mount-only), the counter and the
value-diff arm behave identically in every production-reachable path, and the
guarded -1 no-op leaves the echo counter at 0 (no strand) so a later genuine
deselect still fires. The counter gate suppresses the whole trampoline fire,
so it also governs the multi-select snapshot OnSelectionChanged (CR #3) and the
guarded-no-op self-clear concern is moot (CR #4). ComboBox/toggles/pickers keep
the value-diff arm (synchronous by construction). Descriptor + fixture
narration updated to match.

CR item #2 (empty ElementExtras bucket) — normalize the 14 bucketed init shims
via ElementExtras.IsEmpty + Element.NormalizeExtras so writing a bucketed field
to null never materializes/keeps a non-null empty bucket. An empty bucket is
not Equals to null, which otherwise broke record equality between an extras-free
element and one with a field cleared to null. New ElementExtrasBucketTests.

CR item #6 — make Element.Extensions setter internal to remove the
initializer-ordering footgun from the public surface (no in-repo external
writes; bucketed shim properties stay public).

Nits — ComboBox stale ShouldSuppress doc; PerformanceBudgets §4.9 dead-code
TODO; Reconciler ContinueDefaultTraversal cref namespace fix (clears the CS1574
warning); TabView pinnable-tab "not byte-identical" callout; ContentDialog
OnClosed tag-routing follow-up note; §4.4 ElementExtras construction-cost note.

Verified: core builds clean (ARM64 + x64), Reactor.Tests 9132 passed / 62 skip,
selftest ValueDiff/Echo/ComboBox/GridView/ListBox/SelectionEvent/EventStateSplit
fixtures all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codemonkeychris
Copy link
Copy Markdown
Collaborator Author

CR feedback addressed (commit 3554617)

Thanks for the high-bar review. Resolutions below, with empirical results for #1.

🔴 #1 — GridView/ListBox deferred-echo → reverted to the causal counter (your preferred option (a))

Before reverting I built a probe fixture to settle the deferred-vs-synchronous question on this box. Findings:

  • Post-realization SelectionChanged is synchronous for both GridView and ListBox (single programmatic write → single inline fire). The deferral called out in the fixture is mount-only (and mount writes are bare/unarmed), so the value-diff arm's precondition actually does hold in steady state.
  • The counter and the value-diff arm behave identically in every production-reachable path. A contrived "two UpdateChilds with no render/dispatcher drain between them" leaks one spurious callback under both mechanisms — but the render loop never does that (one reconcile pass visits each element once), so it isn't reachable.
  • For the guarded -1 no-op, I read the echo counter directly: it stays 0 through the dropped write (nothing is armed), so a later genuine deselect fires correctly — i.e. no strand under the counter.

Conclusion: the value-diff arm offered no real advantage here, so I moved both back to ShouldSuppress/WriteSuppressed, restoring main's mechanism for the default flip. ComboBox/toggles/pickers keep the value-diff arm (synchronous by construction). The probe was investigation-only and has been removed; descriptor + fixture narration updated to match.

🔴 #2 — empty ElementExtras bucket equality → fixed

Added ElementExtras.IsEmpty + Element.NormalizeExtras; all 14 bucketed init shims now (a) don't materialize a bucket for a null write and (b) collapse an all-null bucket back to null. New ElementExtrasBucketTests asserts (x with { Attached = null }) == x (and hash equality), the collapse case, and that a real value still differs.

🟡 #3 / #4 — resolved by #1

On the counter, ShouldSuppress gates the entire trampoline fire, so the multi-select snapshot OnSelectionChanged is governed too (#3). The guarded-no-op never arms suppression (counter stays 0), so the self-clear concern is moot for these controls (#4).

🟡 #5 — documented the per-extra ElementExtras construction-cost tradeoff in the §4.4 ElementExtras notes.

🟡 #6 — made Element.Extensions setter internal (no in-repo external writes; bucketed shim properties stay public), removing the initializer-ordering footgun from the locked-in public surface.

🟢 Nits

ComboBox stale ShouldSuppress doc corrected; PerformanceBudgets got a TODO(§4.9) dead-code guard; Reconciler ContinueDefaultTraversal cref namespace-qualified (clears the CS1574 warning); TabView "not byte-identical / pinnable tab appears immediately" callout added; ContentDialog OnClosed tag-routing follow-up noted in code.

Verification: core builds clean (ARM64 + x64), Reactor.Tests 9132 passed / 62 skip, and the selftest ValueDiff/Echo/ComboBox/GridView/ListBox/SelectionEvent/EventStateSplit fixtures are all green.

codemonkeychris and others added 21 commits May 29, 2026 22:29
Closes the XAML-interop slice of the §4.0 reachable-but-deferred carve:
- Register XamlPageDescriptor/XamlHostDescriptor handlers in
  RegisterV1BuiltInHandlers so V1 auto-registration owns the two
  reverse-embedding element types.
- Add internal Reconciler.IsElementTypeRegistered and make
  XamlInterop.Register idempotent (skips already-owned types) so the
  public API no longer trips EnsureRegistrableElementType under V1 ON.
- Update XamlInteropTests double-throw test to idempotency; add V1-ON
  registration + clashless-Register regression tests.

Verified: xunit XamlInterop/V1OnRegistration/TypeRegistry (91) green;
Hosting_XamlInteropRegister selftest green V1 ON == V1 OFF.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract Reconciler.CleanupNavigationHostNode and own navigation teardown
from NavigationHostHandler.Unmount on the V1 path. The flag-independent
UnmountRecursive intercept becomes a !UseV1Protocol fallback so cleanup
stays byte-identical V1 ON =/= V1 OFF until the V1-OFF escape path is
deleted in 4.6.

NavHost selftests 16/16 green under both flags; NavigationHostTests +
UseNavigationTests 30/30 pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…led descriptor entries

ControlledPropEntry.Update and HandCodedControlledPropEntry.Update wrote
under echo-suppression whenever the element prop changed (oldEl != newEl),
even when the control already held the new value. WriteSuppressed always
increments the suppress counter, consumed only by a real change event; a
suppressed no-op write strands the token and silently swallows the user's
next real interaction (the cross-state echo class spec 047 section 8 exists
to prevent). Suppress only on real control drift (current != newValue).

Add real-input echo-stranding regression fixtures across both entry types
(RadioButton + ToggleSplitButton via ControlledPropEntry; TextBox +
NumberBox via HandCodedControlledPropEntry). Verified red→green: all four
SecondEventNotSwallowed assertions fail with the bug reintroduced and pass
with the fix. Full Desc_ descriptor suite: 0 failures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a 'Selftests (V1 ON)' CI job that runs the full selftest suite with
REACTOR_USE_V1_PROTOCOL=true so V1-ON =/= V1-OFF parity is enforced by an
automated gate rather than asserted by a one-off manual run. Runs the
AppTests.Host directly (Program.cs maps the env var to the AppContext
switch before any Reconciler is built), since Reactor.Tests/xunit does not
map the flag. Mirrors the existing aot-selftests run-the-host pattern and
uploads TAP output as an artifact.

Also harden TabRenderer_TitleBarChrome/FlatChrome_BodyRendered with a second
Harness.Render() pump: TabView lazy-realizes selected-pane body content on
dispatcher messages scheduled by layout, which a single pump can race on
contended runners (consistent with the documented Harness.Render mitigation).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Flip the Reconciler ctor default so V1 dispatch is the production path when
neither the explicit ctor flag nor the AppContext switch is set. The flag now
exists only as an escape hatch to force V1 OFF during the legacy-deletion
window (gated to 4.5/4.6).

Fix 4 tests that assumed the OFF default:
- XamlInteropTests x2: assert via IsElementTypeRegistered (flag-independent)
  since XamlHost/Page are owned by the V1 registry under ON.
- TypeRegistryTests.Override_Builtin_Type: pin to useV1Protocol:false
  (legacy built-in override is V1-OFF-only per 13 Q17).
- RichEditBoxElementTests: tolerate TargetInvocationException-wrapped
  COMException from V1 new T() construction.

Verified: xunit ON = 9136 passed/0 failed; full selftest ON = 0 failures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ale selftest

The GridViewHandler already routes through the engine's virtualizing
MountGridView body (ItemsSource = Range + shared ItemTemplate +
ContainerContentChanging), mirroring ListViewHandler. Add RareControl_GridViewLazy
to lock that CCC lifecycle: 500 items in a 200px viewport realize only ~96
containers (< total/2), the tail item stays unrealized, the first item realizes.
Identical 96/500 under V1 ON and V1 OFF - A|B parity green. Guards against a
regression to the descriptor's non-virtualizing ItemsHost<> path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the stale 100/320/500 B byte-gate estimates with the measured 11.6
targets (Target = min(Direct + 100, ReactorToday x 0.4) -> 407 / 1520 / 19200)
in spec 14 cleanup, 15.1 goal 1, the 15.6 hard-gate sentence, and the 15.7
Phase 4 row. Fix the 15.6 'Phase 5 cleanup' reference to 'Phase 4' (this spec
has no Phase 5).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…alyzers

The final descriptor API is fully strongly-typed: events wire via typed
subscribe lambdas (no changeEvent string names) and Controlled<TValue,TArgs>
unifies set/readBack so the compiler already rejects a read-back type mismatch.
A repo sweep found zero string-form event references in production descriptors,
so REACTOR1001 (string event ref) and REACTOR1003 (read-back type) had no
source pattern left to match.

Remove both no-op analyzers + their test fixtures, the two reserved diagnostic
descriptors, and the REACTOR1001/1003 rows from AnalyzerReleases.Unshipped.md
and the guide table. REACTOR1002 (CustomEventDelegateTypeAnalyzer) remains as
the active Q10 compile-time check. Analyzer tests green (4 pass).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Document completed work (4.0.6 parity, 4.1 flip, 4.0.4 GridView lock,
4.4 spec-hygiene, 4.7 analyzer retirement) and the critical context for the
next session: the prelude delegate handlers call legacy MountXxx bodies, so
genuine 4.0.1 (modal-lifecycle decorator strategy) + 4.0.3 (TabView engine
features) ports must land before 4.5 can delete the legacy switch. Includes
verified build/test commands and the concurrent-selftest-build race warning.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ze overlay/TabView ports

Phase 4 close-out work, landing the genuine V1 ports together with the
legacy dispatch-switch deletion they unblock.

§4.0.1 overlay port: overlays are now owned by the new V1 OverlayLifecycle
static module; the engine no longer delegates into legacy bodies.
§4.0.3 TabView port: full TabViewDescriptor replaces the delegate
TabViewHandler (deleted), with drag pipeline, pinnable headers and
imperative-bridged strip slots.
§4.5: both Mount/Update dispatch to V1 registry -> _typeRegistry ->
composition-primitive-only switch (Component/Func/Memo/ErrorBoundary/
CommandHost/FormField/ValidationVisualizer/ValidationRule). Dead-body
sweep removed the orphaned legacy Mount*/Update* bodies including the 14
overlay delegators and Mount/UpdateTabView. Overlay leaf helpers
(Create/Update MenuFlyout/AppBar items) promoted to internal for
OverlayLifecycle; BuildTabHeader/TryUpdatePinHeaderInPlace stay internal
for TabViewDescriptor.

Tests: removed the obsolete descriptor-vs-handler parity harness
(Spec047V1ProtocolDescriptorFixtures + Desc_ registry entries; Echo_
real-input regression fixtures preserved); dropped the dead
UpdateCommandBarFlyout/UpdateFlyoutElement reflection probes.

Validation: build 0 err; xunit V1 ON 9136 pass/0 fail; full selftest 0 fail.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
V1 is the unconditional production path (§4.5 deleted the legacy dispatch
switch), so the entire A|B testing apparatus is dead. This removes it.

Reconciler:
- Collapse the four ctors into a single `Reconciler(ILogger? logger = null)`;
  drop the `useV1Protocol` / `registerBuiltinHandlers` params, the
  `public bool UseV1Protocol` property, and the AppContext-switch read.
- Drop the flag guard from both dispatch sites (Mount.cs:66, Update.cs:117),
  both unmount arms, and the NavigationHost pre-dispatch block.

Tests / harness:
- Remove the REACTOR_USE_V1_PROTOCOL env-var mapping (AppTests.Host/Program.cs)
  and the redundant `selftests-v1` CI job.
- De-switch the Spec047V1Protocol / Spec047ExternalProof selftest fixtures.
- Delete V1FeatureFlagTests, TypeRegistryTests.Override_Builtin, and the
  redundant TextBox echo-stranding fixture; migrate the echo-stranding fixtures
  and Ports/*PortTests to `new Reconciler()` against the built-in descriptors;
  reshape V1OnRegistrationTests to the always-on registration contract.

Perf / tooling:
- Delete the A|B perf duplicates (StressPerf.ReactorV2, BlankReactorV2,
  PerfBench DescriptorVariantFactory) and tools/spec047-phase1-checkpoint;
  rename the ReactorV2 column to Reactor in the aggregator/slnx/scripts.

Validation: core build = 0 err; xunit = 9128 pass / 0 fail; Echo + V1_* +
Spec047ExternalProof_* selftests = 0 fail. Perf-project consolidation
measurement is deferred to the ARM64 baseline machine (§4.9).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase 2 decided the V1 protocol is the production model and §4.5 deleted the
legacy path, so the author surface is now stable/supported. Graduate it out of
[Experimental] and confirm the external-assembly proof against the locked surface.

- Remove all 157 [Experimental("REACTOR_V1_PREVIEW")] attributes across 110
  src/Reactor files (public author surface — IElementHandler, MountContext/
  UpdateContext, ReactorBinding(<T>), ControlDescriptor + builders,
  Register{Type,Handler}, pool policy, WriteSuppressed, AddRawRoutedHandler —
  and the internal handlers/descriptors that carried it).
- Drop the now-dead REACTOR_V1_PREVIEW NoWarn from all six csprojs
  (Reactor, Reactor.Tests, Reactor.AppTests.Host, PerfBench.ControlModel, and
  both external_proof projects). The external_proof control now consumes the
  public surface with NO experimental opt-in — the strongest form of the proof.

KD-4 (external typed-event surface) was already shipped: the external
MarqueeControl wires a typed CLR event via the public MountContext.BindFor ->
ReactorBinding<TElement>.OnCustomEvent<TArgs> path with only a plain
ProjectReference (no InternalsVisibleTo). ReactorBinding<TElement>'s ctor stays
internal but is reached through public BindFor, so it is not a gap.

Validation: core build = 0 err; Reactor.External.TestControl builds clean (0 err,
no IL trim/AOT warnings, PublishTrimmed+IsAotCompatible on, no opt-in); xunit =
9128 pass / 0 fail; all six Spec047ExternalProof_Marquee_* selftests green;
PerfBench.ControlModel + AppTests.Host build clean without the NoWarn.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…bodies

§4.5 deleted the legacy reconciler dispatch switch arms but left the value-
control MountXxx/UpdateXxx handler method bodies behind as unreachable dead
code (controls now dispatch through the V1 descriptor system). Remove them:

  Mount.cs:  MountToggleSplitButton, MountPasswordBox, MountNumberBox,
             MountAutoSuggestBox, MountRadioButton, MountRadioButtons,
             MountComboBox, MountSlider, MountRatingControl, MountColorPicker,
             MountCalendarDatePicker, MountDatePicker, MountTimePicker, plus
             the now-dead helpers EnsureToggleSwitchWiring and (Reconciler's
             own) EnsureTextBoxWiring.
  Update.cs: UpdateToggleSplitButton, UpdateTextBox, UpdatePasswordBox,
             UpdateNumberBox, UpdateAutoSuggestBox, UpdateRadioButton,
             UpdateToggleSwitch, UpdateRatingControl, UpdateColorPicker,
             UpdateCalendarDatePicker, UpdateDatePicker, UpdateTimePicker,
             UpdateRadioButtons, UpdateComboBox, UpdateCalendarView.

Preserved live islands interleaved among these: MountCheckBox/UpdateCheckBox
(Path-B via CheckBoxHandler), the NumberBox immediate-mode chain
(NumberBoxImmediateTextChanged/...LoadedEnsureImmediateTextBox/
EnsureNumberBoxImmediateTextBoxWiring/HandleNumberBoxImmediateTextChanged plus
the CanSynchronize/AreNumberBoxValuesEquivalent helpers, all live via
NumberBoxDescriptor.Immediate), and SyncSelectedDates (reflection-probed).

This finishes the §4.5 dead-code sweep and cuts ChangeEchoSuppressor references
from ~55 to 16 (the remaining live descriptor paths are migrated in §4.2 part B).
No behavior change (deleted code was unreachable).

Validation: core build 0 err; xunit 9128 pass / 0 fail / 62 skip; selftests
PrivMount/Echo/NumberBox/CheckBox all 0 failures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Phase-0 begin-suppress-audit.csv is stale (cites §4.5/§4.2A-deleted legacy
line numbers). Add a refreshed, accurate live call-site inventory for the
ChangeEchoSuppressor elimination surface and record the go/no-go decision.

- New: docs/specs/047/audits/echo-suppressor-phase4-live-sites.md — the ~30 live
  echo sites (post-§4.2A), classified (general-controlled / coercion /
  float-precision / collection-batch / setter-scope / public-API), with the
  rationale for why full elimination (part B) is a dedicated, spec-author-involved
  effort rather than an autopilot pass: the counter is a causal token, value-compare
  is causally weaker (coincidental real-value events get swallowed), ApplySetters'
  EchoSuppressScopeDepth has no value to compare, and public WriteSuppressed carries
  no value/readback for external authors. New regression fixtures required first.

- Task doc: progress log records §4.2 part A done (commit 8a67e34) and a "Deferred"
  block for §4.2B / §4.3 / §4.4 / §4.8 / §4.10 / §4.9 with reasons; §4.2 section gets
  a status note pointing at the refreshed inventory. Also corrects the §4.5 log's
  "0 orphaned private members remain" over-claim (§4.2A removed ~28 it missed).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ry path

Per product decision, prove out the spec §8 "diff/value-compare" echo
suppression on the descriptor controlled fast path, keeping the causal counter
as the retained fallback everywhere else (so reverting this one path is trivial
if the diff approach causes issues in practice).

ControlledPropEntry (src/Reactor/Core/V1Protocol/Descriptor/PropEntry.cs) — the
generic two-way `.Controlled(...)` entry used by ~10 controls (RadioButton,
ToggleSplitButton, ToggleSwitch, RatingControl, ColorPicker, the date/time
pickers, etc.) — now suppresses its own programmatic-write echo by VALUE:

- Update arms a per-control ExpectedEcho on the DescriptorControlledPayload
  (value + comparer) instead of bumping ChangeEchoSuppressor's counter, then
  writes. Arming is gated on a non-null callback AND an existing payload (no
  subscription => no echo to suppress), via the new non-allocating
  Reconciler.TryGetControlEventPayload<T>.
- The change-event trampoline drops the single event whose readback equals the
  armed value (one-shot), then clears the arm. A mismatch means a real user
  change superseded the pending write -> clear and fire the callback.
- ChangeEchoSuppressor.ShouldSuppress is still honored first (external
  WriteSuppressed tokens + the ApplySetters setter scope, which carry no value);
  a matching pending arm is drained on that branch too so it cannot strand and
  swallow a later real interaction (rubber-duck + code-review blocking finding).
- Mount clears any stale arm left on a pooled payload (KD-3: payload survives
  rent/return).

The counter (ChangeEchoSuppressor / EchoSuppressCount / EchoSuppressScopeDepth /
public ReactorBinding.WriteSuppressed) is unchanged and remains the live
mechanism for the hand-coded / coercing / collection entries, the
Slider/TextBox/ToggleSwitch handlers, CheckBox, NumberBox-immediate, the
CalendarView shim, the setter scope, and external authors. The PoC's
synchronous-change-event assumption (and the migrate-back path) is documented
in-code on DescriptorControlledPayload.

New selftest fixtures (Spec047ValueDiffEchoFixtures) lock the value-diff DRIFT
path the existing Echo_*_RealInput stranding fixtures don't cover: a real
programmatic update lands on the control, does NOT echo into the callback, and a
subsequent real interaction still fires.

Validation (x64): core build 0 err; xunit 9128 pass / 0 fail; selftests
--filter Echo (no-echo + stranding), ValueDiff (new drift), SettersScope
(setter scope), Spec047ExternalProof (public WriteSuppressed counter) all 0
failures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extends the §8 value-diff echo-suppression proof-of-concept from the
ControlledPropEntry fast path to the live TextBox path (TextBoxHandler,
the registered handler — the descriptor is an unregistered proof-point).

The legacy counter (ChangeEchoSuppressor.WriteSuppressed / ShouldSuppress)
is replaced for TextBoxHandler's controlled `Text` writes by arming an
ExpectedEchoText on the per-control TextBoxEventPayload: the TextChanged
trampoline drops the single event whose readback matches (then clears the
one-shot arm). The arm is left pending after the write — like the counter,
which leaves its suppression elevated for a possibly-deferred TextChanged.
Stale arms are cleared on the next Mount (pool reuse).

The counter is retained for the paths it still owns: external public
ReactorBinding.WriteSuppressed and the ApplySetters setter-scope. The
trampoline checks ShouldSuppress first (and drains a matching arm on that
branch so a counter-suppressed echo can't strand the arm), then value-diff.

Arming is gated on a TextChanged trampoline being wired now OR about to be
wired this Update (newEl.OnChanged != null) — EnsureTextBoxWiring runs at
the end of Update, so a null->non-null OnChanged transition combined with a
value change still arms (creating the payload if needed) and the trampoline
goes live before any deferred echo. This preserves the legacy counter's
subscription-timing-independent suppression and avoids stranding a
SelectionChanged-only TextBox.

Accepted PoC tradeoff: value-diff only drops on an exact readback match, so
a write WinUI coerces (e.g. single-line stripping \r\n) surfaces as a real
change rather than being dropped. Documented in-code with a migrate-back
path to the counter.

Selftest fixtures added (Spec047ValueDiffEchoFixtures):
ValueDiff_TextBox_Drift, _SnapBack, _Transition.

Validated (x64): core build clean; xunit 9128/0; selftests Echo +
ValueDiff + SettersScope 0 failures (incl. EchoSuppress_TextBox_*);
DataGrid E2E (click cell -> type -> commit) + EventHandler TextBox keydown
E2E passed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Migrate the synchronous, exact-comparable, single-controlled-value round-trips
off the causal ChangeEchoSuppressor counter onto a value-diff arm, while
RETAINING the counter as the documented fallback for paths value-comparison
cannot model. End state is an intentional hybrid (no ReactorState byte win —
adds one ref field — chosen for correctness/self-healing on migrated paths).

Shared infra:
- ReactorState.PendingEchoMatch (one-shot Func<object?,bool>?), reset at the
  same 3 sites as the counter (ClearCurrentEventHandlers, DetachReactorState,
  pool-return).
- ChangeEchoSuppressor.ArmExpectedEcho / ClearExpectedEcho / ShouldSuppressEcho.
  Counter/scope wins first (external WriteSuppressed + ApplySetters carry no
  expected value); else the one-shot value predicate is consumed.
- HandCodedControlledPropEntry opt-in `valueDiffEcho` + ControlDescriptor
  builder param: Update arms the expected echo then writes bare; the per-
  descriptor trampoline calls ShouldSuppressEcho(ctrl, readback).

Migrated (valueDiffEcho / ShouldSuppressEcho): ComboBox, FlipView, GridView,
ListBox, Pivot, PipsPager, RadioButtons, SelectorBar, TabView,
TemplatedFlipView descriptors + ToggleSwitchHandler. (TextBoxHandler /
ControlledPropEntry were already value-diff via their own payload arm.)

Retained on the counter (rationale in spec 8.3): Slider/NumberBox doubles,
NumberBox coercion, CalendarView collection, AutoSuggest/Password/RichEdit
strings, Expander, CheckBox path-B, ApplySetters scope, public WriteSuppressed.

Key insight: WinUI selection (SelectionChanged) and Toggled fire SYNCHRONOUSLY
inside the property write (unlike deferred TextBox.TextChanged), so no
arm-ahead-of-wiring is needed; a null->non-null transition simply produces no
echo before the trampoline subscribes.

Code-review-driven strand fixes:
- ShouldSuppressEcho clears PendingEchoMatch UNCONDITIONALLY in the counter
  branch (safe: only synchronous controls use this arm; deferred TextBox uses a
  separate HasExpectedEcho payload arm), preventing a stranded arm from
  swallowing a later coincidental real event.
- HandCodedControlledPropEntry.Update clears the arm after a guarded/coerced
  no-op write (post-write readback != nv) so the never-fired echo cannot strand.

Docs: spec §8.3 (new) documents the hybrid + retained-counter rationale and
supersedes the wholesale-deletion plan; task-doc progress log records part B'.

Tests: new ValueDiff_ComboBox_Drift, ValueDiff_ToggleSwitch_Drift, and
ValueDiff_GridView_GuardedNoOpStrand selftest fixtures. Validated x64: core
build 0 err; xunit 9128/0; ValueDiff + Echo + migrated-control selftests 0 fail;
DataGrid E2E pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sk doc

Update the §4.2 section of the phase-4 task doc to reflect the value-diff echo
migration that landed in c5c1399:
- New "Part B'" status block + checked-off checklist for the shared value-diff
  arm, the migrated descriptors/handler, the strand-safety fixes, and the new
  regression fixtures.
- Mark the original "delete + tolerance metadata + ColorPicker shim" plan
  (now "Part B") as DEFERRED and SUPERSEDED by spec §8.3, with the
  counter-retained hybrid rationale; its full-elimination boxes stay unchecked.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tHandlerState

Carve the monolithic per-element EventHandlerState into the §9.2 shape:
the WinUI true-routed-input family (pointer/key/tap/focus/sizechanged/
accesskey — 21 Current* + 20 trampolines) is renamed in place to
ModifierEventHandlerState (lazy on ReactorState.Modifiers); control-intrinsic
events move fully onto the already-shipped per-control ControlEventStateBox
payloads.

- Delete dead legacy Image/ScrollViewer/ScrollView Mount/Update/Ensure bodies
  (the registered descriptors own their event wiring) + their 5 EHS trampoline
  fields. Delete 3 orphaned EHS fields (ToggleSwitchToggled/TextBoxTextChanged/
  TextBoxSelectionChanged — referenced only in the struct def).
- Migrate the 2 live control-intrinsic paths off the shared struct onto
  ControlEventStateBox payloads: Button.Click → ButtonEventPayload.ClickTrampoline
  and NumberBox immediate-mode wiring flag → NumberBoxEventPayload.ImmediateInnerWired.
  Both resolve the same native-DO-keyed ReactorState, so the issue #114/#86
  shared-trampoline dedup invariant is preserved; trampolines keep reading the
  live element via GetElementTag (CurrentClick untouched — it is preserved across
  pool return and must not stash a per-owner delegate).
- Rename EventHandlerState→ModifierEventHandlerState, ReactorState.Events→Modifiers,
  GetOrCreateEventState→GetOrCreateModifierState across all callers; refresh the
  5 <see cref> doc references.
- Correct the stale ControlEventStateBox/payload comments that claimed pool reset
  clears ControlEventState on return — it is PRESERVED across rent/return (#114),
  dropped only on full detach.

Validation (x64): core build 0 err; xunit 9128 pass / 0 fail; Button / NumberBox /
Image / Scroll / Pool / EventHandler selftests 0 fail. M10/M11 byte measurement
deferred to the ARM64 baseline machine (§4.9); the §9.2 pool-lifecycle hazard
fixtures land next.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Lock the post-split contract with AppTests.Host self-test fixtures
(Spec047EventStateSplitFixtures.cs, registered in SelfTestFixtureRegistry):

- NoDuplicateSubscriptionAcrossPoolReuse — issue #114 guard: rent→fire→return→
  re-rent→fire, the preserved ControlEventState trampoline fires the LIVE element
  exactly once (never double-subscribes).
- HandlerTypeMismatchResetsBox — the ControlEventStateBox HandlerType discriminator
  deterministically replaces the box on type mismatch (no InvalidCastException);
  doubles as the hot-reload type-identity proxy.
- DualReturnIdempotent — returning the same control twice is idempotent; re-rent
  still fires exactly once.
- ModifierStateLazyForIntrinsicOnly — §9.4 alloc-shape proxy for the ARM64-blocked
  M10/M11 byte measurement: an intrinsic-only control leaves ReactorState.Modifiers
  null while ControlEventState is allocated; a routed-modifier control allocates
  Modifiers.
- AddRawRoutedHandler_HandledEventsToo — the §9.5/Q11 escape hatch is intact on
  Mount/UpdateContext and independent of the split. The live "Handled child →
  parent still fires" leg is a documented TAP SKIP (WinUI 3 cannot synthesize a
  KeyRoutedEventArgs / RaiseEvent an input event headlessly) — covered by the
  Appium E2E KeyDownTest.

Validation (x64): host build 0 err; EventStateSplit / Pool / EventHandler
selftests 0 failures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rogress log

Mark all §4.3 task-doc checkboxes done (the monolith is gone; control-intrinsic
events fully on ControlEventStateBox payloads; routed family renamed to
ModifierEventHandlerState; §9.2 hazard fixtures landed). M10/M11 byte/frequency
measurement noted ARM64-deferred to §4.9; alloc-shape asserted via selftest in
this x64 env. Add the §4.3 progress-log entry.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
codemonkeychris and others added 7 commits May 29, 2026 22:36
…6 byte-gate constants

Mirror the proven spec-034 ElementModifiers shim-bucket pattern: move the 14
cross-cutting nullable Element base fields (Attached, ImplicitTransitions,
ThemeTransitions, ThemeBindings, LayoutAnimation, AnimationConfig,
ElementTransition, InteractionStates, StaggerConfig, KeyframeAnimations,
ScrollAnimation, ConnectedAnimationKey, ResourceOverrides, ContextValues) into a
single nullable value-equality sub-record `ElementExtras`, exposed via one
`Element.Extensions` slot. Each field name survives as a public get/init shim
(get => Extensions?.X; init => copy-on-write into Extensions), so in the lean
case (Extensions == null) the base carries only Key/Modifiers/Extensions — the
§11.7 byte win — while all ~180 existing readers and `with`-expression writers
(including the read-then-write composites in ElementExtensions.cs) compile and
behave unchanged. Named ElementExtras to avoid the existing ElementExtensions
static class.

- Add `Extensions is null && b.Extensions is null` fast-path in ShallowEquals so
  the common no-Extras element skips the three structural (Attached/ThemeBindings/
  ContextValues) compares in the hot reconcile diff path; update the
  CanSkipUpdate/ShallowEquals invariant doc. Record value-equality is preserved
  (the 14 move into the value-equality ElementExtras; the reconciler diffs via the
  structural helpers, not Element.Equals).
- Land the §11.6 hard byte-gate TARGET constants (PerformanceBudgets.cs):
  M1 ≤407 / M2 ≤1520 / M3 ≤19200 (measured baselines 1018/~3800/~48000 ×0.4).
  The gate MEASUREMENT/enforcement is ARM64-baseline-blocked → deferred to §4.9.

Validation (x64): core build 0 err; xunit 9128 pass / 0 fail (incl.
ElementModifiersBucketTests); Animation/Transition/Theme/Context/Attached/Stagger/
Keyframe/Scroll/ConnectedAnimation/Resource selftests 0 fail. Only Element.cs +
the new PerformanceBudgets.cs changed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…gress log

Mark the bucketing + reader/writer-migration boxes done (ElementExtras shim
bucket; zero call-site edits; PerformanceBudgets §11.6 target constants landed).
The byte-gate enforcement/measurement boxes stay open — ARM64-baseline-blocked,
deferred to §4.9. Add the §4.4 progress-log entry.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-split)

Promote the V1 control-authoring guide out of "preview/provisional" and bring it
in line with the post-Phase-4 reality (hand-maintained pages — extensibility-
preview.md has no .md.dt template; filename kept to preserve inbound spec links).

docs/guide/extensibility-preview.md:
- Drop the [Experimental]/REACTOR_V1_PREVIEW/breaking-change banner; mark the
  surface stable. Replace "Enabling the V1 path / off by default" with a
  "Dispatch order" section (V1 registry → _typeRegistry → composition-primitive
  switch; no flag, no legacy MountXxx fallthrough).
- Correct the pool-reset enumeration: ControlEventState is PRESERVED across
  rent/return (#114), not cleared — trampolines stay subscribed and read the live
  tag; dropped only on full detach / replaced on HandlerType mismatch. The shared
  modifier state is ModifierEventHandlerState (ReactorState.Modifiers, lazy),
  cleared via ClearCurrentHandlers.
- Correct the per-control event-state section: EventHandlerState was split into
  ModifierEventHandlerState + per-control ControlEventStateBox (§9.2) — done, not
  "Phase 3/4".
- Rewrite WriteSuppressed to the §8.3 hybrid (value-diff for synchronous
  single-value controls + retained suppress-counter fallback; signature stable).
- Replace the children-strategy table with the final 10-strategy set; drop the
  Phase-status column. Drop the "descriptor authoring = Phase 2 gate" item and the
  NoWarn REACTOR_V1_PREVIEW checklist step (plain ProjectReference, no IVT/opt-in).
- Add a "Choosing an authoring shape (decision tree)" section (spec §6.1.1):
  descriptor prop/engine shapes vs hand-coded IElementHandler, the children-strategy
  picker, echo + pool-policy notes.

AGENTS.md:
- Rewrite "Adding a new WinUI control" to the V1 descriptor path (Element record →
  ControlDescriptor/IElementHandler → RegisterV1BuiltInHandlers → selftest).
- Rewrite "Echo suppression for value controls" to the §8.3 hybrid (ChangeEchoSuppressor
  retained as the counter fallback; authors use WriteSuppressed/.Controlled/valueDiffEcho).
- Update the per-element-state line to ModifierEventHandlerState + ControlEventStateBox.

Docs-only; mur not available in this env so no generated pages were recompiled.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ess log

Mark all §4.8 boxes done (extensibility guide promoted to stable; §6.1.1 decision
tree added; AGENTS.md updated to the V1 descriptor model + §8.3 echo hybrid +
ModifierEventHandlerState/ControlEventStateBox). The generated-pages box is N/A
in this env (mur unavailable; the edited page is hand-maintained). Note: the
task's "ChangeEchoSuppressor is deleted" premise was corrected to the settled
hybrid (retained). Add the §4.8 progress-log entry.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-blocked)

§4.9 is entirely a measurement/ratification gate on LAPTOP-4MEP83VI and is not
executable in the x64 dev environment. Annotate the section with a clear handoff:
all code the gates measure is already landed (§11.6 target constants §4.4,
single-Reactor perf-project consolidation §4.6, EHS split §4.3, bucketed Element
base §4.4, AOT-clean external proof + CI AOT job §4.7); the KD-3 M1 binder-check
fold is deliberately NOT applied (measurement-gated, and prior related micro-opts
were net-negative). Boxes stay unchecked until the baseline capture lands; added
the baseline-operator runbook + progress-log entry.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Final close-out of the Phase 4 V1-migration cleanup.

- Status: marked the main tracker header + spec §14 "Phase 4 — cleanup" as
  code-complete — migration closed; V1 is the unconditional production path.
  Reconciled the exit-gate/cleanup wording against the settled outcomes: the
  "delete ChangeEchoSuppressor" item is superseded by the §8.3 value-diff/counter
  HYBRID (suppressor intentionally retained; WriteSuppressed signature unchanged),
  and the §11.6 byte-gate + ARM64 ratification are carved out as the only
  outstanding baseline-machine (LAPTOP-4MEP83VI) items.
- Dead-code sweep: grep-clean across src + tests — no live UseV1Protocol /
  REACTOR_USE_V1_PROTOCOL / ReactorV2 / registerBuiltinHandlers / EventHandlerState
  (monolith); only historical comments remain. ChangeEchoSuppressor is retained by
  design (hybrid), so it is removed from the sweep list.
- Tidied a stale PoolPolicyTests TODO: the real FrameworkElement rent/return reset
  contract is now covered by the §4.3 self-test fixtures; corrected its
  "ControlEventState cleared" wording to "preserved across rent/return (#114)".

Validation (x64): full solution build (Reactor.slnx -p:Platform=x64) 0 err; full
xunit 9128 pass / 0 fail; full selftest 0 fail modulo the known docking-family
flakiness (DockHooks_* / SidePopup_*) that fails intermittently under full-suite
headless load and passes deterministically when filtered (confirmed: both filters
0 failures; full-run count varied 7→6 across runs) — pre-existing, not a regression.

Outstanding (handed off, baseline-machine-only): §4.9 ARM64 stable-AC perf
ratification + §4.4 §11.6 hard byte-gate measurement/enforcement.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nt bucketing)

CR item #1 (GridView/ListBox deferred-echo) — revert SelectedIndex to the
causal counter (ShouldSuppress/WriteSuppressed; drop the §8 value-diff arm),
restoring main's mechanism. Empirically probed on this box: post-realization
SelectionChanged is synchronous (deferral is mount-only), the counter and the
value-diff arm behave identically in every production-reachable path, and the
guarded -1 no-op leaves the echo counter at 0 (no strand) so a later genuine
deselect still fires. The counter gate suppresses the whole trampoline fire,
so it also governs the multi-select snapshot OnSelectionChanged (CR #3) and the
guarded-no-op self-clear concern is moot (CR #4). ComboBox/toggles/pickers keep
the value-diff arm (synchronous by construction). Descriptor + fixture
narration updated to match.

CR item #2 (empty ElementExtras bucket) — normalize the 14 bucketed init shims
via ElementExtras.IsEmpty + Element.NormalizeExtras so writing a bucketed field
to null never materializes/keeps a non-null empty bucket. An empty bucket is
not Equals to null, which otherwise broke record equality between an extras-free
element and one with a field cleared to null. New ElementExtrasBucketTests.

CR item #6 — make Element.Extensions setter internal to remove the
initializer-ordering footgun from the public surface (no in-repo external
writes; bucketed shim properties stay public).

Nits — ComboBox stale ShouldSuppress doc; PerformanceBudgets §4.9 dead-code
TODO; Reconciler ContinueDefaultTraversal cref namespace fix (clears the CS1574
warning); TabView pinnable-tab "not byte-identical" callout; ContentDialog
OnClosed tag-routing follow-up note; §4.4 ElementExtras construction-cost note.

Verified: core builds clean (ARM64 + x64), Reactor.Tests 9132 passed / 62 skip,
selftest ValueDiff/Echo/ComboBox/GridView/ListBox/SelectionEvent/EventStateSplit
fixtures all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codemonkeychris codemonkeychris force-pushed the spec-047-phase4-v1-migration-closeout branch from 3554617 to 0f134d3 Compare May 30, 2026 05:39
@codemonkeychris
Copy link
Copy Markdown
Collaborator Author

Rebased onto main (force-pushed 0f134d3c)

Synced the branch onto latest origin/main (was 5 behind / 28 ahead). Resolved conflicts:

Re-validation (post-rebase)

  • Core builds clean on ARM64 + x64 (0 errors).
  • Reactor.Tests: 9132 passed / 62 skip.
  • Selftest host (--self-test): plan 1..982, 3734 ok / 0 failures.

CR fixes from 0f134d3c (the prior reply) are unchanged and re-verified green on top of main.

@codemonkeychris codemonkeychris merged commit 56a7940 into main May 30, 2026
15 checks passed
@codemonkeychris codemonkeychris deleted the spec-047-phase4-v1-migration-closeout branch May 30, 2026 05:50
@codemonkeychris
Copy link
Copy Markdown
Collaborator Author

ARM64 baseline-machine capture landed — partially addresses the two "Outstanding" items

Ran the post-Phase-4 micro suite (M1–M13) on LAPTOP-4MEP83VI (the ARM64 baseline box this PR was opened to validate on) and compared against the 2026-05-25-arm64 baseline. Results + analysis are in #465 (docs/specs/047/phase4-results/LAPTOP-4MEP83VI/2026-05-29-arm64/), and I've updated the spec/§4.9/§4.4 trackers there with the measured numbers.

On Outstanding #1 (§4.9 ARM64 perf ratification) and #2 (§11.6 byte-gate measurement):

⚠️ This is an indicative capture, not the formal ratification. §15.5 isolation (AC / High-Perf / DRR-off / foreground) wasn't enforced, the §4.9-mandated randomized/interleaved ordering + CPU-clock telemetry isn't wired, and the macro suite (L1–L14) is unrunnable because this PR deleted its projects (StressPerf.ReactorV2, BlankReactorV2). So the timing axis is throttled and must be disregarded (Direct — zero Reactor code — is itself +60–140% vs baseline).

The allocation axis is valid (deterministic; Direct alloc matches the baseline byte-for-byte), and it surfaces two real findings worth acting on before the gate is claimed closed:

result
§15.6 "M1–M3 alloc ≤ Today" M2 −5.1% ✅, M3 −6.0% ✅, M1 +20.3% ❌
§11.6 absolute byte gate M3 ≤19,200 ✅, M1 1,289 (3.2× over 407) ❌, M2 3,687 (2.4× over 1,520) ❌
vs baseline ReactorV2 mostly flat/better, M9 −41% 👍; but M1 +20% and M12 +17% regressed

The headline: the §4.4 bucketing made the leanest leaf (M1) ~+235 B/render heavier, not lighter — deterministic across all reps. This confirms the spec's own KD-3 trigger ("fold the M1 binder check if M1 is still over budget") and adds a new follow-up: investigate why bucketing regressed M1 (candidates: the added Element.Extensions slot, the §4.3 EHS split, the ReactorState.PendingEchoMatch slot) plus the M12 pool-reuse regression.

Net: the two outstanding items are measured but not closed — M1/M2 miss the byte gates, and a thermally-clean stable-AC re-capture (+ the macro suite rebuilt against the single Reactor variant) is still required. See #465RESULTS.md for the full breakdown and reproduction.

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