# Evaluation — smoke2 gap fix (issue #12) - Commit graded: `8784fec` - Evaluator: independent session (Opus 4.6 [1m]) - Date: 2026-04-07 - Note: Issue #12 used a free-form issue body instead of a Sprint Contract (`docs/contracts/smoke2-gap-fix.md` does not exist). Acceptable per CLAUDE.md §0.1 for follow-up bug fixes, but recorded here. ## Verdict: **PASS** ## Verdict table | Item | Required | Observed | Status | |---|---|---|---| | Build | 0 warn / 0 err (TWAE) | Clean, 0/0 | pass | | Tests total | 71 pass / 0 fail / 0 skip | 71 pass / 0 fail / 0 skip (16+10+17+5+5+6+6+6) | pass | | Gap A — full path resolver | UiaPathParser splits `/`, parses `(Class, AutomationId?, Name?)`; `IUiaTreeNode` adapter; descend chain id→name→class; bounded fallback; UiaPlayerHost wired; null on miss | All present. `UiaPathParser.cs` quote-aware split, attribute parser. `IUiaPathResolver.cs` defines `IUiaTreeNode` + `UiaPathResolver` with `MatchRoot` + `FindChild` + `DescendantsBounded(maxDepth:4)` documented fallback. `Matches` priority AutomationId > Name > ClassName. `UiaPlayerHost.ResolveElement` uses `UiaPathResolver.Resolve(new FlaUiTreeNode(window), uiaPath)` via `Retry.WhileNull`, returns null on miss (engine handles throw). Old `ExtractAutomationId` shortcut removed. | pass | | Gap B — type target inheritance | `_lastFocusPath` / `_lastMousePath` state; FlushType fallback chain (typeRes → focus → mouse) | `DragCollapser` adds `lastFocusPath` + `lastMousePath` locals (line 46-47); `FlushType` fallback `typeRes ?? lastFocusPath ?? lastMousePath` (line 72); focus_change updates `lastFocusPath` (line 332); mouse_down_l/r updates `lastMousePath` from downRes (line 128, 188). | pass | | Gap C — window filter | `ShouldKeep`; mouse uses `WindowFromPoint`; key uses `GetForegroundWindow`; wired to SUT pid in Program | `WindowFilter.cs`: `IWindowFilter`, `PassThroughWindowFilter`, `SutProcessWindowFilter` with two pluggable lookups. Mouse path → `processFromPoint`, key path → `processFromForeground`, focus_change always kept, pid==0 permissive. `Program.cs` lines 87-107 wires `SutProcessWindowFilter` to `app.ProcessId` using `WindowFromPoint`+`GetWindowThreadProcessId` and `GetForegroundWindow`. `LowLevelHook` exposes mutable `Filter` and applies it in both Keyboard/Mouse procs. | pass | | Gap D — UTF-8 BOM-less | Explicit `UTF8Encoding(false)`; round-trip test with Korean strings + no BOM | `ScenarioWriter.WriteToFile` line 43-44: `new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)`. Round-trip test `ScenarioWriter_RoundTrip_PreservesKorean` asserts byte[0..2] != EF BB BF and Korean Name/Description/Path round-trip. | pass | | Thread.Sleep in PlayerEngine | 0 | 0 (grep) | pass | | EG-BIM Modeler writes | none | none | pass | ## Regression-trap analysis | New test | Would have failed pre-fix? | Why | |---|---|---| | `UiaPathParser_ParsesMultiSegment_WithClassAndId` | yes — `UiaPathParser` did not exist | Compile-trap | | `UiaPathParser_ParsesNameAttribute` | yes | Compile-trap | | `UiaPathResolver_Descend_FindsNestedElement` | yes | Compile-trap; also exercises chain descent | | `UiaPathResolver_LastSegmentWithoutId_UsesClassName` | yes | Validates ClassName fallback in `Matches` | | `UiaPathResolver_NotFound_ReturnsNull` | yes | Validates null-not-throw contract | | `SmokeRegression_BoxV4CleanLike_ParsesAndResolves` | yes — explicitly proves the resolver no longer collapses to "first descendant" (Assert.NotSame) | Direct guard against the smoke 1차 bug | | `DragCollapser_TypeAfterFocusChange_InheritsTarget` | yes — pre-fix `FlushType` only used `typeRes`; with all-null resolver result, `Target` would be null. Test asserts `Target.UiaPath == focusPath`. | Direct guard for Gap B | | `DragCollapser_TypeAfterMouseDown_FallbackToMouseTarget` | yes — same reason; asserts mouse path inheritance | Direct guard | | `WindowFilter_ExternalCoord_DropsEvent` | yes — `SutProcessWindowFilter` did not exist | Compile-trap; also asserts drop semantics | | `WindowFilter_SutCoord_KeepsEvent` | yes | Compile-trap; asserts keep + permissive pid=0 | | `ScenarioWriter_RoundTrip_PreservesKorean` | yes — pre-fix relied on default overload; the explicit byte-level `EF BB BF` assertion + Korean round-trip would only deterministically pass with the explicit encoder | Direct guard for Gap D | All new tests are load-bearing. ## Notes - `docs/guides/smoke-test.md` Gap D tip section confirmed (commit diff shows +18 lines). - No partial hacks observed; all four gaps are real code-level fixes with unit coverage and reasonable abstractions (pluggable lookups for the filter, pure adapter for the path resolver). - Recommend Generator update PROGRESS.md to mark issue #12 done.