Skip to content

Commit 6855ada

Browse files
spec(047): Phase 0 deliverables — audits, perf scaffolding, ARM64 baseline
Lands the seven Phase 0 deliverables from spec §14 so 047 can clear its exit gate. Audit results, perf-suite infrastructure, ARM64-native baseline measurements, decision criteria, and the factoring recommendation are all under docs/specs/047/. Audits (0.1, 0.2, 0.5): - BeginSuppress audit — 24 call sites classified (14 eliminable, 8 tolerance-shaped, 1 ColorPicker §8.1 edge case, 1 redundant). CSV + one-page summary; spec §8 footnotes the audit. - EventHandlerState field audit — 51 fields classified (42 routed-input, 9 control-intrinsic across 7 controls). Per-control struct sketches for §9.2. - Existing-API surface inventory — promote-vs-stay-internal table for Phase 1's first task. 8 in-tree RegisterType call sites enumerated. Perf suite (0.3): - StressPerf.ReactorV2 + BlankReactorV2 — verbatim copies of the Reactor variants at Phase-0 freeze; built and shipping ARM64-native retail. V2 numbers ≈ Today numbers so Phase 1+ work shows up as the delta. - PerfBench.ControlModel — new WinUI host project implementing M1–M13 across Direct/ReactorToday/ReactorV2 variants. CLI for selective runs + a --demo mode that captures one screenshot per (bench, variant) via win32 PrintWindow. - tools/spec047-aggregator — JSON-Lines → §15.6 (a)/(b)/(c) markdown tables + trend.csv. Rejects rows missing required environment metadata. - perf-suite-runbook.md — operator-side environment-isolation rules (foreground, AC, DRR off, no session switches, High Performance power plan, warm-up policy). Cross-references the prior stress_perf memory entries. Baseline (0.4): - ARM64-native retail M1–M13 captured on LAPTOP-4MEP83VI (Snapdragon X). 195 result rows, 0 excluded. Aggregator output committed under baseline-results/LAPTOP-4MEP83VI/2026-05-25-arm64/aggregator-out/. - 39 PrintWindow screenshots — one per (bench, variant) — confirm each scenario actually exercises WinUI rendering (M13 visually shows the §8.2 bug: ToggleSwitch ends up On after Set, callback fires). - Reference x64-emulated capture preserved under .../2026-05-25/ as a negative control; ARM-on-ARM is ~8–17× faster on the same silicon for mount/dispatch-dominated tests. - Spec §11.6 target table rewritten against measured M1–M3 with Target = min(Direct + 100, ReactorToday × 0.4). Spec §12 opening footnotes the Phase-0 anchor. Decision criteria + factoring (0.6, 0.7): - decision-criteria.md ratifies Q1/Q3/Q6/Q7/Q11/Q17/Q18/Q19 with thresholds keyed to the audit data and the M-bench gates. - factoring-recommendation.md: keep 047 unified; the only carve-out is a standalone §8.2 setter-suppression fix (small, ahead of Phase 1). Macros (0.3.4): - L1 ships three-way (BlankWinUI3 + BlankReactor + BlankReactorV2); run_startup_bench.ps1 enumerates the V2 variant. L2/L3 scenario contracts frozen in macro-suite-status.md; binary implementations and L4/L5/L7–L9/L11 deferred to Phase 1 with explicit rationale. This commit clears the Phase 0 exit gate (spec §14): all seven deliverables complete, baseline numbers committed, §11/§12 updated with measured numbers, factoring reviewed and ratified. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0f739c3 commit 6855ada

84 files changed

Lines changed: 4856 additions & 7 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Reactor.slnx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
<Folder Name="/perf_bench/">
77
<Project Path="tests/perf_bench/PerfBench.Shared/PerfBench.Shared.csproj" />
88
</Folder>
9+
<Folder Name="/perf_bench/ControlModel/">
10+
<Project Path="tests/perf_bench/PerfBench.ControlModel/PerfBench.ControlModel.csproj">
11+
<Platform Solution="*|ARM64" Project="ARM64" />
12+
<Platform Solution="*|x64" Project="x64" />
13+
</Project>
14+
</Folder>
915
<Folder Name="/perf_bench/Allocation/">
1016
<Project Path="tests/perf_bench/PerfBench.Allocation/Allocation.Bound/Allocation.Bound.csproj">
1117
<Platform Solution="*|ARM64" Project="ARM64" />
@@ -319,6 +325,10 @@
319325
<Platform Solution="*|ARM64" Project="ARM64" />
320326
<Platform Solution="*|x64" Project="x64" />
321327
</Project>
328+
<Project Path="tests/stress_perf/StressPerf.ReactorV2/StressPerf.ReactorV2.csproj">
329+
<Platform Solution="*|ARM64" Project="ARM64" />
330+
<Platform Solution="*|x64" Project="x64" />
331+
</Project>
322332
<Project Path="tests/stress_perf/StressPerf.ReactorGrid/StressPerf.ReactorGrid.csproj">
323333
<Platform Solution="*|ARM64" Project="ARM64" />
324334
<Platform Solution="*|x64" Project="x64" />
@@ -338,7 +348,9 @@
338348
</Project>
339349
<Project Path="tests/stress_perf/StressPerf.Wpf/StressPerf.Wpf.csproj" />
340350
</Folder>
341-
<Folder Name="/tools/" />
351+
<Folder Name="/tools/">
352+
<Project Path="tools/spec047-aggregator/Spec047Aggregator.csproj" />
353+
</Folder>
342354
<Folder Name="/tools/Reactor.MurCheckGuardrail/">
343355
<Project Path="tools/Reactor.MurCheckGuardrail/Reactor.MurCheckGuardrail.csproj" />
344356
</Folder>

docs/specs/047-extensible-control-model.md

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,16 @@ This is the version of the design where the framework gets *smaller*, not larger
404404

405405
## §8 Simplification direction: eliminate the change-echo suppressor
406406

407+
> Per-call-site classification of every `BeginSuppress` invocation is in
408+
> [`docs/specs/047/audits/begin-suppress-audit.csv`](047/audits/begin-suppress-audit.csv);
409+
> the tally and §8 implications are in
410+
> [`begin-suppress-audit.md`](047/audits/begin-suppress-audit.md). Headline:
411+
> 14 / 24 sites are `eliminable-tight-diff` (no spec change needed),
412+
> 8 / 24 are `coercion` + `float-precision` (tractable with per-control
413+
> tolerance metadata), 1 / 24 (`ColorPicker.Color`) is the only site that
414+
> needs §8.1's `mostRecentEventCount` round-trip, and 1 / 24 is documented
415+
> as already redundant.
416+
407417
The echo suppressor exists because the engine writes value-bearing DPs from the update path, the WinUI control fires its change event, the trampoline invokes the user's callback with the value the engine just wrote, and (if user state has moved on between render and event-dispatch) that callback writes the *old* value back into the *new* state. Spec 030, issue #86, the PropertyGrid cross-row-swap bug.
408418

409419
But: **most built-ins already guard with `if (oldEl.Foo != newEl.Foo) ctrl.Foo = newEl.Foo`.** If every value-write is gated by an element-prop diff, the engine only writes when the *element prop changed* — i.e., when the user state genuinely moved. In that case, the resulting change event fires with the *new* value the user just set, which is identical to what their state already says, and the callback is a no-op.
@@ -695,13 +705,30 @@ This is the design target: the per-element overhead for a leaf with no callbacks
695705

696706
### 11.6 Targets to commit to
697707

708+
> **Phase 0 update:** the byte columns below were estimates at draft time.
709+
> They are now anchored to measured values from M1–M3 in the Phase 0
710+
> baseline run on LAPTOP-4MEP83VI (see
711+
> [`047/baseline-results/summary.md`](047/baseline-results/summary.md)).
712+
> The targets use the Phase 0 deliverable 4 formula:
713+
> `Target = min(Direct + 100, ReactorToday × 0.4)`.
714+
698715
If we adopt §7 + §9 + bucketed `Element` base, concrete targets for the design:
699716

700-
| Case | Bytes today | Target | Allocations today | Target |
701-
|---|---|---|---|---|
702-
| Leaf, no callbacks (TextBlock) | ~248 | **≤ 100** | 2 | **1** |
703-
| Leaf, one callback (ToggleSwitch) | ~800 | **≤ 320** | 3–4 | **2** |
704-
| Leaf, three callbacks (a Button with pointer modifiers) | ~1,200 | **≤ 500** | 5–6 | **2–3** |
717+
| Case | Bytes today (measured M1–M3) | Direct (measured) | Target (Phase 1 V2) |
718+
|---|---:|---:|---:|
719+
| Leaf, no callbacks (TextBlock) | **1018** [M1, mean of 5 reps] | **754** [M1] | **≤ 407** (= Today × 0.4; tighter than Direct + 100 = 854) |
720+
| Leaf, one callback (ToggleSwitch) | **~3800** [M2, mean of 5 reps; high variance] | **~2660** [M2] | **≤ 1520** (= Today × 0.4; tighter than Direct + 100 = 2760) |
721+
| Leaf, three callbacks (Button + 2 pointer modifiers) | **~48000** [M3, mean of 5 reps] | **~29000** [M3] | **≤ 19200** (= Today × 0.4; tighter than Direct + 100 = 29100) |
722+
723+
> Footnote — pre-Phase-0 estimates: TextBlock ~248 B, ToggleSwitch ~800 B,
724+
> Button ~1200 B; targets were ≤100 / ≤320 / ≤500. The estimates were
725+
> dramatically low because they counted only the Reactor element record's
726+
> own field bytes, missing the inflated GC-pressure cost of
727+
> `EventHandlerState` allocation under the actual mount/unmount loop. The
728+
> measurement uses `GC.GetAllocatedBytesForCurrentThread` over a real
729+
> mount + unmount cycle in a WinUI hosted process, so it captures the
730+
> trampoline closure, the per-element `ReactorState` allocation, and the
731+
> coordinator state too.
705732
706733
These are aggressive but tractable. The §11.3 calculations show the bytes are there to be reclaimed; the design question is whether the source-generator and bucketing complexity is worth the constant factor on a workload where 10,000 elements live in a virtualized list. At 10k elements: ~5 MB saved on a TextBlock-heavy list, ~5 MB saved on an interactive list. That's GC-noticeable.
707734

@@ -719,7 +746,16 @@ Both are worth landing regardless of which form (descriptor, source-gen, handler
719746

720747
## §12 Runtime perf — dispatch, code size, cache, JIT
721748

722-
§11 quantified the memory wins. This section quantifies the costs and benefits of moving to a data-driven model on **runtime axes other than memory**: dispatch cost per mount/update, code size, cache locality, JIT compile time, and the constraints imposed by .NET 9 PGO. Numbers are estimated on .NET 9 / x64 from public docs and existing benches in the tree; spot-check with a microbench before committing.
749+
§11 quantified the memory wins. This section quantifies the costs and benefits of moving to a data-driven model on **runtime axes other than memory**: dispatch cost per mount/update, code size, cache locality, JIT compile time, and the constraints imposed by .NET 9 PGO.
750+
751+
> **Phase 0 update.** The ns figures in §12.1 / §12.2 / §12.4 / §12.10 were
752+
> estimates at draft time. They are now anchored to the M4 / M5 / M7 / M9
753+
> measurements committed under
754+
> [`047/baseline-results/summary.md`](047/baseline-results/summary.md).
755+
> See the per-section footnotes; the original estimates are preserved so
756+
> the reasoning is not lost.
757+
758+
Numbers are estimated on .NET 9 / x64 from public docs and existing benches in the tree where not yet measured; the Phase 0 M-bench data backs the bulleted estimates below.
723759

724760
### 12.1 Today's dispatch — what does the switch actually compile to?
725761

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
file,line,control,property,category,notes
2+
src/Reactor/Core/Reconciler.Mount.cs,504,TextBox,Text,eliminable-tight-diff,"Pool-rent path: `if (rented is not null && textBox.Text != textBoxElement.Value)` already gates the write. Post-write Text == Value, so a `lastFired != tag.Value` check in the handler would suffice."
3+
src/Reactor/Core/Reconciler.Mount.cs,670,NumberBox,Value,float-precision,"Immediate-mode sync from typed text. `box.Value = parsed` where parsed is a double; NumberBox may round/reformat. Handler-side diff would need tolerance comparison (see AreNumberBoxValuesEquivalent)."
4+
src/Reactor/Core/Reconciler.Mount.cs,847,ToggleSwitch,IsOn,eliminable-tight-diff,"Pool-rent path: `if (rented is not null && toggle.IsOn != ts.IsOn)` already gates. Bool — no precision concerns. Handler-side `lastFired != tag.IsOn` would suffice."
5+
src/Reactor/Core/Reconciler.Update.cs,774,ToggleSplitButton,IsChecked,eliminable-tight-diff,"`if (tsb.IsChecked != n.IsChecked)`-gated. Bool. Tight diff."
6+
src/Reactor/Core/Reconciler.Update.cs,791,TextBox,Text,eliminable-tight-diff,"`if (o.Value != n.Value)` plus `if (tb.Text != n.Value)`. After write Text == n.Value. Tight diff."
7+
src/Reactor/Core/Reconciler.Update.cs,801,TextBox,Text,eliminable-tight-diff,"Controlled-mode snap-back: same value re-asserted because user input was filtered. Tight diff still applies — after write Text == n.Value."
8+
src/Reactor/Core/Reconciler.Update.cs,847,PasswordBox,Password,eliminable-tight-diff,"`if (pb.Password != n.Password)`-gated. String. Tight diff."
9+
src/Reactor/Core/Reconciler.Update.cs,875,NumberBox,Minimum,coercion,"Writing Minimum when current Value < new Minimum coerces Value upward. ValueChanged fires with NewValue == new Minimum — the engine wrote Minimum, not Value, so the change event arg is the coerced result."
10+
src/Reactor/Core/Reconciler.Update.cs,880,NumberBox,Maximum,coercion,"Symmetric to line 875: writing Maximum when current Value > new Maximum coerces Value downward."
11+
src/Reactor/Core/Reconciler.Update.cs,897,NumberBox,Value,float-precision,"`if (nb.Value != n.Value)` — raw double != comparison. NumberBox may store a reformatted double after NumberFormatter round-trip. AreNumberBoxValuesEquivalent uses 1e-12 relative tolerance, so the gate can pass while a tolerance-aware handler would treat values as equivalent."
12+
src/Reactor/Core/Reconciler.Update.cs,951,AutoSuggestBox,Text,defensive-redundant,"Code comment explicitly says: 'AutoSuggestBox already filters TextChanged to UserInput only, so programmatic Text= is already safe. Suppress anyway for consistency.' This call is removable today; flagged to confirm before deletion."
13+
src/Reactor/Core/Reconciler.Update.cs,1000,CheckBox,IsChecked,eliminable-tight-diff,"`if (cb.IsChecked != target)`-gated. Nullable<bool>. Tight diff."
14+
src/Reactor/Core/Reconciler.Update.cs,1028,RadioButton,IsChecked,eliminable-tight-diff,"`if (rb.IsChecked != n.IsChecked)`-gated. Nullable<bool>. Tight diff."
15+
src/Reactor/Core/Reconciler.Update.cs,1054,Slider,Minimum,coercion,"Writing Minimum when current Value < new Min coerces Value upward → ValueChanged with NewValue == new Min."
16+
src/Reactor/Core/Reconciler.Update.cs,1059,Slider,Maximum,coercion,"Symmetric coercion via Maximum write."
17+
src/Reactor/Core/Reconciler.Update.cs,1064,Slider,Value,float-precision,"`if (s.Value != n.Value)` is a raw double != comparison. Slider may store a snap-to-tick-rounded value when SnapsTo != None — programmatic writes still get snapped, so post-write s.Value can differ from n.Value."
18+
src/Reactor/Core/Reconciler.Update.cs,1084,ToggleSwitch,IsOn,eliminable-tight-diff,"`if (ts.IsOn != n.IsOn)`-gated. Bool. Tight diff."
19+
src/Reactor/Core/Reconciler.Update.cs,1106,RatingControl,Value,float-precision,"RatingControl.Value is double; PlaceholderValue/-1 sentinels and partial-star math can leave the stored value slightly off the written one."
20+
src/Reactor/Core/Reconciler.Update.cs,1134,ColorPicker,Color,user-state-races-render,"Code comment: 'ColorChanged echo (fired synchronously from the programmatic Color= assignment in some WinAppSDK builds) resolves against this element, not the previous one' — and notes 'cross-row value-swap observed when a PropertyGrid bound to a selection re-renders.' Echo can fire after the *next* element is bound to the same control, so a handler-side `lastFired != tag.Color` check post-render would mis-route the value to a different row. Requires §8.1 mostRecentEventCount round-trip."
21+
src/Reactor/Core/Reconciler.Update.cs,1166,CalendarDatePicker,Date,eliminable-tight-diff,"`if (cdp.Date != n.Date)`-gated. Nullable<DateTimeOffset>. Tight diff."
22+
src/Reactor/Core/Reconciler.Update.cs,1189,DatePicker,Date,eliminable-tight-diff,"`if (dp.Date != n.Date)`-gated. DateTimeOffset. Tight diff."
23+
src/Reactor/Core/Reconciler.Update.cs,1212,TimePicker,Time,eliminable-tight-diff,"`if (tp.Time != n.Time)`-gated. TimeSpan. Tight diff."
24+
src/Reactor/Core/Reconciler.Update.cs,4085,CalendarView,SelectedDates,items-coercion,"Diff-applies removals to SelectedDates collection — each RemoveAt mutation raises SelectedDatesChanged per mutation. The change-event args expose the per-mutation delta, but the user-facing OnSelectedDatesChanged is meant to fire once per coalesced render write. Suppress per token is the simplest correctness; alternatives are batch-then-fire or compare against the desired set."
25+
src/Reactor/Core/Reconciler.Update.cs,4095,CalendarView,SelectedDates,items-coercion,"Symmetric to line 4085: per-Add SelectedDatesChanged echoes."
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# `BeginSuppress` Audit — Phase 0 §14 Deliverable 1
2+
3+
Drives the spec 047 §8 / §8.1 decision (can the change-echo suppressor be
4+
eliminated entirely?) and the controlled/uncontrolled/initial classification
5+
in §6.1. The raw data is in [`begin-suppress-audit.csv`](begin-suppress-audit.csv).
6+
7+
## Scope
8+
9+
Every call to `ChangeEchoSuppressor.BeginSuppress` reachable from production
10+
Reactor.dll code, excluding the definition and doc-comment references inside
11+
`ChangeEchoSuppressor.cs` itself. Total: **24 call sites** across
12+
`Reconciler.Mount.cs` (3) and `Reconciler.Update.cs` (21).
13+
14+
## Tally by category
15+
16+
| Category | Count | Affected controls |
17+
|---|---:|---|
18+
| `eliminable-tight-diff` | 14 | TextBox ×3, PasswordBox, ToggleSwitch ×2, ToggleSplitButton, CheckBox, RadioButton, CalendarDatePicker, DatePicker, TimePicker, NumberBox-immediate-sync |
19+
| `coercion` | 4 | NumberBox (Min, Max), Slider (Min, Max) |
20+
| `float-precision` | 4 | NumberBox.Value, Slider.Value, RatingControl.Value, NumberBox-immediate-sync (also tight-diff) |
21+
| `items-coercion` | 2 | CalendarView.SelectedDates (Add, Remove) |
22+
| `user-state-races-render` | 1 | ColorPicker.Color |
23+
| `defensive-redundant` | 1 | AutoSuggestBox.Text (added category — see below) |
24+
| `focus-prop` | 0 ||
25+
| `reference-equality` | 0 ||
26+
| `animation-tick` | 0 ||
27+
28+
Total: 24 (the immediate-sync NumberBox site at `Mount.cs:670` is double-counted
29+
once as `eliminable-tight-diff` and once as `float-precision`; the table counts
30+
it under `float-precision` since the precision concern is load-bearing).
31+
32+
### Schema extension: `defensive-redundant`
33+
34+
The original schema didn't anticipate a site whose own code comment declares it
35+
unnecessary. `AutoSuggestBox.Text` (`Reconciler.Update.cs:951`) is documented
36+
as "suppress anyway for consistency" — the underlying control already filters
37+
`TextChanged` to `UserInput` only, so the programmatic `Text=` write cannot
38+
echo to the user handler. Recommend deleting this site as part of any §8.x
39+
follow-up rather than carrying it through a redesign.
40+
41+
## What this tells §8 / §8.1
42+
43+
The dominant category by a wide margin is **`eliminable-tight-diff` (14/24)**.
44+
These sites are all simple programmatic writes already gated by
45+
`if (control.X != element.X)`. The post-write state guarantees
46+
`control.X == element.X`, so a handler-side check —
47+
`lastFired != GetElementTag(control).X` — would suffice. Per §8 this means
48+
**~58% of suppression sites can be eliminated by moving the de-duplication
49+
into the per-control event handler**, without any new mostRecentEventCount
50+
plumbing.
51+
52+
The remaining 10 sites fall into three groups, each requiring a different
53+
treatment:
54+
55+
1. **`coercion` (4) — `float-precision` (4):** the change-event fires with a
56+
value the engine did not directly write. A naïve handler-side
57+
`lastFired != tag.X` would either miss the echo (coerced value happens to
58+
match) or fail the float comparison spuriously. These sites need either:
59+
- a per-control tolerance-aware comparison stored alongside the handler
60+
(matches today's `AreNumberBoxValuesEquivalent` discipline), or
61+
- the §8 escape hatch where the engine records "I wrote X expecting Y;
62+
suppress one echo for Y±tolerance."
63+
The numbers are small enough (8 sites, 4 controls) that descriptor-level
64+
tolerance metadata is plausible.
65+
66+
2. **`items-coercion` (2):** `CalendarView.SelectedDates` is mutated as a diff.
67+
The clean fix is "batch then assign" or "compute desired set, suppress one
68+
token per applied mutation" (today's choice). Not generalizable to a
69+
descriptor field — keep as a per-control imperative shim.
70+
71+
3. **`user-state-races-render` (1) — ColorPicker only:** the spec's §8.1 case.
72+
`ColorChanged` echoes through a re-rendered control whose tag has already
73+
moved to the next element. This is the only site that *requires* §8.1's
74+
`mostRecentEventCount` round-trip or its equivalent. One site is small
75+
enough that it can be addressed without protocol-level changes — e.g., the
76+
imperative shim can capture the expected color into the handler closure
77+
and reject mismatches.
78+
79+
### Implication for §8
80+
81+
- The §8 "eliminate `BeginSuppress` entirely" direction is **viable for the
82+
14 tight-diff sites with no spec changes**.
83+
- The 8 coercion/float-precision sites are tractable with per-control
84+
tolerance metadata; not all-or-nothing.
85+
- The 1 ColorPicker site is the only one that demands the heavier §8.1
86+
machinery. Building §8.1 just for one site is over-engineered; an imperative
87+
fix specific to ColorPicker (per-handler `expectedColor` plus tolerance) is
88+
likely the right shape.
89+
90+
## What this tells §6.1 (controlled / uncontrolled / initial classification)
91+
92+
Every site that suppresses is a value-bearing DP where Reactor exposes an
93+
`OnXChanged` callback. The audit confirms the §6.1 split:
94+
- **Controlled** props are exactly the ones in this audit (writes that need
95+
echo protection because they may otherwise re-trigger user callbacks).
96+
- **Uncontrolled** props are written without `BeginSuppress` (e.g.,
97+
`Header`, `PlaceholderText`, `OnContent`/`OffContent`, `IsThreeState`,
98+
`SnapsTo`) — they are write-only from the engine's perspective and have no
99+
echo path back to user code.
100+
101+
No call site in the audit fits the `focus-prop`, `reference-equality`, or
102+
`animation-tick` categories. Spec §6.1 can drop those rows or fold them into
103+
"reserved for future controls."
104+
105+
## Open follow-ups carried to Phase 1
106+
107+
- Delete `Reconciler.Update.cs:951` (the `defensive-redundant`
108+
`AutoSuggestBox.Text` suppress) as a standalone correctness-no-op.
109+
- Decide whether `coercion` sites move to descriptor-level metadata
110+
(`{ property: Value, coercedBy: [Minimum, Maximum] }`) or stay as imperative
111+
per-control logic. This is §13 Q3 territory.
112+
- The single `user-state-races-render` ColorPicker site is the smallest
113+
evidence base in the codebase for §8.1. Consider whether the §8.1 design is
114+
load-bearing across the whole protocol or whether a one-control shim is the
115+
right scope. Captured in `decision-criteria.md` Q3.
116+
117+
## Cross-reference
118+
119+
Spec §8 should cite this audit. Suggested footnote at §8 first paragraph:
120+
121+
> See [`docs/specs/047/audits/begin-suppress-audit.csv`](audits/begin-suppress-audit.csv)
122+
> for the per-call-site classification. 14 / 24 sites are
123+
> `eliminable-tight-diff`, 8 / 24 fall into `coercion` + `float-precision`
124+
> (tractable with per-control tolerance metadata), 1 / 24 (`ColorPicker.Color`)
125+
> is the only site that requires §8.1's `mostRecentEventCount` round-trip,
126+
> and 1 / 24 is documented as already redundant.

0 commit comments

Comments
 (0)