@@ -1343,3 +1343,116 @@ prior iteration), with `-Top` and `-MinMissed` knobs. Use it after
13431343 ` [ExcludeFromCodeCoverage] ` on the genuinely-host-bound
13441344 methods within those files (per-method exclusions, not
13451345 per-file).
1346+
1347+ ### 2026-05-18 — PropertyGrid EditChain + statics (machine A, twelfth pass)
1348+
1349+ - Baseline at start: ** 80.70% line / 68.83% branch** .
1350+ - Area picked: ** ` Controls/PropertyGrid/PropertyGridComponent.cs ` **
1351+ (68.4% line / 57.6% branch / 178 missed) — the ` EditChain ` internal
1352+ class and the file's static helpers (` RenderReadOnlyValue ` ,
1353+ ` IsPrimitiveOrEnum ` ). Picked because:
1354+ - The file's ` Render() ` and ` BlankButton ` are host-bound
1355+ (` SolidColorBrush ` , ` FlexColumn ` mount-time, ` WithFlyout ` ) — but
1356+ the bottom half of the file (≈110 lines of ` EditChain ` + 3
1357+ static helpers) is pure C# with substantial branch logic.
1358+ - ` EditChain.CannotPropagate ` has 5 distinct decision points
1359+ gating the read-only-fallback in ` RenderEditor ` — each one
1360+ represents a real product-bug shape (e.g. "mutable ancestor
1361+ in chain makes leaf editable even without leaf SetValue").
1362+ - ** Auditing existing tests** (` PropertyGridDecompositionTests.cs ` ):
1363+ ` EditChain ` was already partly exercised by
1364+ ` Fully_Immutable_Root_Fires_OnRootChanged ` and
1365+ ` EditChain_Propagates_Through_Multiple_Immutable_Levels ` , but
1366+ ` CannotPropagate ` , ` PropagateNewOwner ` , ` BuildPath ` , and the
1367+ "mutable ancestor absorbs composed child" branch of
1368+ ` PropagateImmutableEdit ` (lines 379-384) had zero coverage.
1369+ - ** Picked up the untracked ` PropertyGridDefaultsTests.cs ` from a
1370+ prior session's worktree** (14 tests, all passing). Targets
1371+ ` PropertyGridDefaults ` templates: ` PropertyLabelTemplate ` (label
1372+ fallback, AutomationName prefix, indent×4 margin, tooltip),
1373+ ` PropertyRowTemplate ` (FlexRow shape, editor AutomationName,
1374+ indent×16 padding), ` ArrayItemTemplate ` (expand glyph
1375+ ▶/▼, toggle inversion, [ N] bracket, 3/4/6-child branches,
1376+ remove ✕ callback). ` ArrayToolbarTemplate ` skipped — calls
1377+ ` .SemiBold() ` which dereferences ` FontWeights.SemiBold ` , a WinRT
1378+ activation factory that throws COMException without packaged WinUI
1379+ (same trap class as iteration 4's ColorCompact and iteration 7's
1380+ brush-shaped CellRenderers).
1381+ - Added ** 31 new tests** in
1382+ ` tests/Reactor.Tests/Controls/EditChainTests.cs ` :
1383+ - ** BuildPath (2)** — empty-chain returns just the property name;
1384+ multi-level joins with ` . ` (pin: a regression to ` / ` would
1385+ break every saved expand-state key).
1386+ - ** CannotPropagate (6)** — direct SetValue short-circuits false;
1387+ empty-path + Compose-less root + null callback → true (the
1388+ only "read-only" terminal); empty-path + OnRootChanged → false;
1389+ empty-path + Compose → false; path-entry SetValue → false
1390+ (mutable ancestor unfreezes a leaf); path-entry SetValue=null
1391+ AND Compose=null → true (terminal mid-chain).
1392+ - ** PropagateNewOwner (4)** — empty-path hits OnRootChanged with
1393+ the new owner; mutable ancestor ` SetValue ` returning Same(parent)
1394+ stops propagation (pin: a regression that always invoked
1395+ OnRootChanged would do a redundant root reassignment on every
1396+ leaf edit); multi-level Compose chain reaches root with
1397+ fully-immutable hierarchy; chain entry with neither SetValue
1398+ nor Compose silently drops.
1399+ - ** PropagateImmutableEdit (2)** — mutable-ancestor branch
1400+ (lines 379-384): Compose builds new immutable child, then
1401+ SetValue on the mutable holder absorbs it. Pin: dropping the
1402+ SetValue call after Compose would lose the edit silently.
1403+ No-Compose-chain + no-Compose-root + callback → silent drop
1404+ without NRE.
1405+ - ** RenderReadOnlyValue via reflection (6)** — bool true →
1406+ ` ToggleSwitchElement.IsOn=true ` with ` Modifiers.IsEnabled=false ` ;
1407+ bool null → IsOn=false (the ` ?? false ` coercion);
1408+ string → ` TextFieldElement ` with ` Modifiers.IsEnabled=false ` ;
1409+ string null → Value=""; other-type → ` TextBlockElement ` with
1410+ ` ToString() ` content; other-type null → ` (null) ` sentinel
1411+ (pin: a regression to empty would visually hide null values).
1412+ - ** IsPrimitiveOrEnum via reflection (Theory × 11)** — int/long/
1413+ double/bool/byte = primitive; string + decimal = special-cased
1414+ true; enum = true; object/class/record = false. Pin: the
1415+ ` IsPrimitive || IsEnum || string || decimal ` predicate exactly,
1416+ so that decimals don't get auto-decomposed in the grid.
1417+ - ** Test results:** 45/45 new (14 PropertyGridDefaults + 31
1418+ EditChain) pass; ** 8,053 / 8,099 unit suite pass** (was 7,996 — clean
1419+ +57; one transient WindowPersistedScopeIsolation flake on first run
1420+ passed cleanly on second).
1421+ - ** Coverage delta** (merged):
1422+ ** 80.70% → 80.79% line (+0.09)** , ** 68.83% → 69.00% branch (+0.17)** .
1423+ Branch swing 1.9× line — again validating the branch-shaped-target
1424+ heuristic. The 45 new tests hit ~ 70 net-new lines on a 101k
1425+ denominator (≈0.07% by arithmetic; the 0.02% surplus came from
1426+ incidental Element-record / FieldDescriptor constructor coverage).
1427+ - ** Surprises / non-obvious findings:**
1428+ - ** ` TypeRegistry.Register ` is generic-only**
1429+ (` Register<T>(TypeMetadata) ` ). My first draft used
1430+ ` Register(typeof(X), ...) ` and didn't compile. The next
1431+ agent should remember: to inject a Compose-less metadata
1432+ for an existing record-shaped type, call the generic
1433+ overload — there's no ` Register(Type, ...) ` non-generic.
1434+ - ** ` Decompose ` on a Compose-less re-registration** : when you
1435+ ` Register<T>(new TypeMetadata { Decompose = oldMeta.Decompose }) `
1436+ you can keep field decomposition while explicitly removing
1437+ Compose, which is exactly the setup needed to test the
1438+ "terminal mid-chain Compose=null" branch.
1439+ - ** The ` WindowPersistedScopeIsolation ` test fails intermittently
1440+ in parallel runs** — there's a ` tools/flake-loop.ps1 ` and
1441+ ` .flake-runs/ ` from prior work tracking it. It's pre-existing,
1442+ not caused by this iteration. The coverage script should retry
1443+ on transient failures; consider adding ` -MaxRetry 1 ` to
1444+ ` run-coverage.ps1 ` for the unit leg.
1445+ - ** Hand-off:** This iteration validates that the PropertyGrid
1446+ family still has unit-testable surface despite the WinUI-bound
1447+ ` Render() ` method — the pure-C# helpers and the EditChain logic
1448+ are the testable core. Same shape as the iteration-8 lesson:
1449+ "host-bound files often have a pure-C# core that's underrated."
1450+ The remaining mid-tier unit-testable targets (per gap-report):
1451+ - ` DevtoolsPropertyTools.cs ` (714 missed, U) — reflection over
1452+ records, the biggest remaining unit-only hot spot. The most
1453+ valuable next pick if focusing on raw line gain.
1454+ - ` ReflectionTypeMetadataProvider.cs ` (sibling of TypeRegistry,
1455+ PropertyGrid family) — likely ≤30 lines uncovered after the
1456+ existing TypedColumnsBehaviorTests, but worth a 10-min audit.
1457+ - The deferral approval discussion (still pending) — would
1458+ jump the metric ~ 1.2 points without writing a single test.
0 commit comments