From a3d82000231b8a8e3709e1a6708dbbdaf81412dc Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sat, 20 Jun 2026 12:32:28 +0800
Subject: [PATCH] Add progressive window shortcut filtering

---
 C3.tools/round1-window-index-progressive-filter-fixture-qa.sh |  809 ++++++++++++++++++++++++++++++++++++++++++++
 C3.tools/round1-main-ui-qa.sh                                 |    3 
 C1.source/Sources/Aligner/QuickSwitchRootView.swift           |  219 +++++++++++
 3 files changed, 1,020 insertions(+), 11 deletions(-)

diff --git a/C1.source/Sources/Aligner/QuickSwitchRootView.swift b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
index 81e6310..e3811f7 100644
--- a/C1.source/Sources/Aligner/QuickSwitchRootView.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -99,6 +99,8 @@
     private var windowIndexKeyPendingClearTask: Task<Void, Never>?
     private var lastWindowIndexKeyCommand: String?
     private var lastWindowIndexKeyCommitCode: String?
+    private var windowShortcutFilterPhase: WindowShortcutFilterPhase = .none
+    private var lastWindowShortcutPreCommitFilterSnapshot: WindowShortcutPreCommitFilterSnapshot?
     private var lastBoundaryBounceAxis: String?
     private var lastBoundaryBounceDirection: String?
     private var boundaryBounceCount = 0
@@ -343,6 +345,47 @@
         let appSymbol: String
         let appGroupIndex: Int
         let startedAt: CFTimeInterval
+    }
+
+    private enum WindowShortcutFilterPhase: String {
+        case none
+        case appPending
+        case targetPreCommit
+        case invalid
+        case expired
+    }
+
+    private enum WindowShortcutFilterState: String {
+        case none
+        case matched
+        case dimmed
+        case target
+    }
+
+    private struct WindowShortcutPreCommitFilterSnapshot {
+        let targetCode: String
+        let targetWindowID: UInt32
+        let matchedWindowIDs: [UInt32]
+        let dimmedWindowIDs: [UInt32]
+
+        var reportDictionary: [String: Any] {
+            [
+                "phase": WindowShortcutFilterPhase.targetPreCommit.rawValue,
+                "targetCode": targetCode,
+                "targetWindowID": Int(targetWindowID),
+                "matchedWindowIDs": matchedWindowIDs.map(Int.init),
+                "dimmedWindowIDs": dimmedWindowIDs.map(Int.init)
+            ]
+        }
+    }
+
+    private struct WindowShortcutFilterReport {
+        let active: Bool
+        let phase: WindowShortcutFilterPhase
+        let pendingAppGroupIndex: Int?
+        let prefix: String?
+        let matchedWindowIDs: [UInt32]
+        let dimmedWindowIDs: [UInt32]
     }
 
     private struct ProjectionLayerSnapshot {
@@ -1087,6 +1130,8 @@
             clearWindowIndexKeyPending()
             lastWindowIndexKeyCommand = nil
             lastWindowIndexKeyCommitCode = nil
+            windowShortcutFilterPhase = .none
+            lastWindowShortcutPreCommitFilterSnapshot = nil
             lastBoundaryBounceAxis = nil
             lastBoundaryBounceDirection = nil
             boundaryBounceCount = 0
@@ -1405,6 +1450,7 @@
         let firstFrameGatePassed = firstFrameP95.map { $0 < 150 } ?? false
         let keyboardGatePassed = keyboardP95.map { $0 < 50 } ?? false
         let hoverGatePassed = hoverP95.map { $0 <= 32 } ?? false
+        let shortcutFilterReport = makeWindowShortcutFilterReport()
 
         return [
             "viewClass": String(describing: Self.self),
@@ -1535,6 +1581,15 @@
             "windowIndexKeyPendingAppSymbol": windowIndexKeyPending?.appSymbol ?? NSNull(),
             "lastWindowIndexKeyCommand": lastWindowIndexKeyCommand ?? NSNull(),
             "lastWindowIndexKeyCommitCode": lastWindowIndexKeyCommitCode ?? NSNull(),
+            "windowShortcutFilterActive": shortcutFilterReport.active,
+            "windowShortcutFilterPhase": shortcutFilterReport.phase.rawValue,
+            "windowShortcutFilterPendingAppGroupIndex": shortcutFilterReport.pendingAppGroupIndex ?? NSNull(),
+            "windowShortcutFilterPrefix": shortcutFilterReport.prefix ?? NSNull(),
+            "windowShortcutFilterMatchedCount": shortcutFilterReport.matchedWindowIDs.count,
+            "windowShortcutFilterDimmedCount": shortcutFilterReport.dimmedWindowIDs.count,
+            "windowShortcutFilterMatchedWindowIDs": shortcutFilterReport.matchedWindowIDs.map(Int.init),
+            "windowShortcutFilterDimmedWindowIDs": shortcutFilterReport.dimmedWindowIDs.map(Int.init),
+            "lastWindowShortcutPreCommitFilterSnapshot": lastWindowShortcutPreCommitFilterSnapshot?.reportDictionary ?? NSNull(),
             "lastBoundaryBounceAxis": lastBoundaryBounceAxis ?? NSNull(),
             "lastBoundaryBounceDirection": lastBoundaryBounceDirection ?? NSNull(),
             "boundaryBounceCount": boundaryBounceCount,
@@ -1708,7 +1763,12 @@
                 recordKeyboardCommand("window:\(code)")
                 lastWindowIndexKeyCommand = "window:\(code)"
                 lastWindowIndexKeyCommitCode = code
-                clearWindowIndexKeyPending()
+                lastWindowShortcutPreCommitFilterSnapshot = makeWindowShortcutPreCommitFilterSnapshot(
+                    target: card,
+                    code: code
+                )
+                windowShortcutFilterPhase = .targetPreCommit
+                clearWindowIndexKeyPending(resetShortcutFilter: false)
                 commitKeyboardWindowCard(card, trigger: "windowIndexKey")
                 return true
             }
@@ -1716,8 +1776,10 @@
             recordKeyboardCommand("windowInvalid:\(code)")
             lastWindowIndexKeyCommand = "windowInvalid:\(code)"
             lastWindowIndexKeyCommitCode = nil
+            windowShortcutFilterPhase = .invalid
+            lastWindowShortcutPreCommitFilterSnapshot = nil
             selectionChangedByLastCommand = false
-            clearWindowIndexKeyPending()
+            clearWindowIndexKeyPending(resetShortcutFilter: false)
             return true
         }
 
@@ -1783,6 +1845,9 @@
             appGroupIndex: appGroupIndex,
             startedAt: startedAt
         )
+        windowShortcutFilterPhase = .appPending
+        lastWindowShortcutPreCommitFilterSnapshot = nil
+        needsLayout = true
         windowIndexKeyPendingClearTask = Task { @MainActor [weak self] in
             do {
                 try await Task.sleep(nanoseconds: UInt64(Self.windowIndexKeyPendingInterval * 1_000_000_000))
@@ -1799,13 +1864,26 @@
             }
             self.windowIndexKeyPending = nil
             self.windowIndexKeyPendingClearTask = nil
+            self.windowShortcutFilterPhase = .expired
+            self.lastWindowShortcutPreCommitFilterSnapshot = nil
+            self.needsLayout = true
         }
     }
 
-    private func clearWindowIndexKeyPending() {
+    private func clearWindowIndexKeyPending(resetShortcutFilter: Bool = true) {
+        let hadFilterState = windowIndexKeyPending != nil
+            || windowShortcutFilterPhase != .none
+            || lastWindowShortcutPreCommitFilterSnapshot != nil
         windowIndexKeyPendingClearTask?.cancel()
         windowIndexKeyPendingClearTask = nil
         windowIndexKeyPending = nil
+        if resetShortcutFilter {
+            windowShortcutFilterPhase = .none
+            lastWindowShortcutPreCommitFilterSnapshot = nil
+        }
+        if hadFilterState {
+            needsLayout = true
+        }
     }
 
     @discardableResult
@@ -5451,13 +5529,15 @@
         let isKeyboardFocused = card.item.window.id == keyboardFocusedWindowID
         let isHorizontalAppDefaultFocused = isHorizontalAppDefaultCard(card)
         let isAppLinked = isWaterfallCardAppLinked(card)
+        let shortcutFilterState = windowShortcutFilterState(for: card)
         applyWaterfallCardVisualState(
             card,
             selected: shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered)
                 || isKeyboardFocused
                 || isHorizontalAppDefaultFocused,
             hovered: isHovered,
-            appLinked: isAppLinked
+            appLinked: isAppLinked,
+            shortcutFilterState: shortcutFilterState
         )
 
         let outerInset: CGFloat = 0
@@ -5512,7 +5592,7 @@
                 windowIndex: card.item.windowIndex,
                 windowID: card.item.window.id
             ),
-            visible: isHovered,
+            visible: isHovered && shortcutFilterState != .dimmed,
             anchorTopRight: CGPoint(x: bounds.maxX - 12, y: bounds.maxY - 12)
         )
         card.shineLayer.frame = CGRect(
@@ -5532,9 +5612,11 @@
         _ card: WaterfallCardLayers,
         selected: Bool,
         hovered: Bool,
-        appLinked: Bool
+        appLinked: Bool,
+        shortcutFilterState: WindowShortcutFilterState
     ) {
         let isSpaceFocused = isWaterfallCardSpaceFocused(card)
+        let isShortcutDimmed = shortcutFilterState == .dimmed
         card.containerLayer.backgroundColor = (selected
             ? themePalette.cardSelectedBackground
             : hovered
@@ -5545,6 +5627,7 @@
                     ? accentTintedWindowBackground(fraction: 0.10, alpha: 0.94)
                 : themePalette.cardBackground
         ).cgColor
+        card.containerLayer.opacity = isShortcutDimmed ? windowShortcutFilterDimOpacity : 1
         card.containerLayer.borderColor = (selected
             ? NSColor.controlAccentColor.withAlphaComponent(0.52)
             : hovered
@@ -5558,7 +5641,7 @@
         card.containerLayer.borderWidth = (selected || hovered || appLinked || isSpaceFocused) ? 1.5 : 1
         card.containerLayer.shadowColor = NSColor.black.cgColor
         let darkShadowBoost: Float = resolvedTheme == .dark ? 0.08 : 0
-        card.containerLayer.shadowOpacity = selected
+        let baseShadowOpacity: Float = selected
             ? 0.24 + darkShadowBoost
             : hovered
                 ? 0.14 + darkShadowBoost
@@ -5567,6 +5650,7 @@
                     : isSpaceFocused
                         ? 0.20 + darkShadowBoost
                         : 0
+        card.containerLayer.shadowOpacity = isShortcutDimmed ? min(baseShadowOpacity, 0.03) : baseShadowOpacity
         card.containerLayer.shadowRadius = selected ? 22 : hovered ? 17 : appLinked ? 18 : isSpaceFocused ? 24 : 0
         card.containerLayer.shadowOffset = selected
             ? CGSize(width: 0, height: -6)
@@ -5579,10 +5663,10 @@
                     : .zero
         card.containerLayer.zPosition = selected ? 20 : hovered ? 12 : appLinked ? 10 : isSpaceFocused ? 8 : 0
         let focusedScale: CGFloat = selected ? WaterfallMetrics.selectedCardScale : isSpaceFocused ? 1.012 : 1.008
-        card.containerLayer.transform = (selected || hovered || isSpaceFocused)
+        card.containerLayer.transform = (!isShortcutDimmed && (selected || hovered || isSpaceFocused))
             ? CATransform3DMakeScale(focusedScale, focusedScale, 1)
             : CATransform3DIdentity
-        card.containerLayer.shadowPath = (selected || hovered || appLinked || isSpaceFocused)
+        card.containerLayer.shadowPath = (!isShortcutDimmed && (selected || hovered || appLinked || isSpaceFocused))
             ? CGPath(
                 roundedRect: card.containerLayer.bounds,
                 cornerWidth: card.containerLayer.cornerRadius,
@@ -5598,7 +5682,7 @@
         ).cgColor
         card.titleBarLayer.borderColor = NSColor.clear.cgColor
         card.titleBarLayer.borderWidth = 0
-        card.shineLayer.opacity = selected ? 1 : 0
+        card.shineLayer.opacity = (!isShortcutDimmed && selected) ? 1 : 0
     }
 
     private func layoutCloseButton(
@@ -6338,6 +6422,105 @@
         return Self.appShelfIndexSymbols[ordinal]
     }
 
+    private var visibleWaterfallCards: [WaterfallCardLayers] {
+        waterfallColumns.flatMap(\.cards)
+    }
+
+    private var windowShortcutFilterDimOpacity: Float {
+        resolvedTheme == .dark ? 0.44 : 0.38
+    }
+
+    private func windowShortcutFilterState(for card: WaterfallCardLayers) -> WindowShortcutFilterState {
+        switch windowShortcutFilterPhase {
+        case .appPending:
+            guard let pending = windowIndexKeyPending else { return .none }
+            return card.item.appGroupIndex == pending.appGroupIndex ? .matched : .dimmed
+        case .targetPreCommit:
+            guard let snapshot = lastWindowShortcutPreCommitFilterSnapshot else { return .none }
+            if card.item.window.id == snapshot.targetWindowID {
+                return .target
+            }
+            if snapshot.dimmedWindowIDs.contains(card.item.window.id) {
+                return .dimmed
+            }
+            if snapshot.matchedWindowIDs.contains(card.item.window.id) {
+                return .matched
+            }
+            return .none
+        case .none, .invalid, .expired:
+            return .none
+        }
+    }
+
+    private func makeWindowShortcutPreCommitFilterSnapshot(
+        target card: WaterfallCardLayers,
+        code: String
+    ) -> WindowShortcutPreCommitFilterSnapshot {
+        let targetWindowID = card.item.window.id
+        let allWindowIDs = visibleWaterfallCards.map { $0.item.window.id }
+        return WindowShortcutPreCommitFilterSnapshot(
+            targetCode: code,
+            targetWindowID: targetWindowID,
+            matchedWindowIDs: [targetWindowID],
+            dimmedWindowIDs: allWindowIDs.filter { $0 != targetWindowID }.sorted()
+        )
+    }
+
+    private func makeWindowShortcutFilterReport() -> WindowShortcutFilterReport {
+        switch windowShortcutFilterPhase {
+        case .appPending:
+            guard let pending = windowIndexKeyPending else {
+                return emptyWindowShortcutFilterReport(phase: .none)
+            }
+            let cards = visibleWaterfallCards
+            let matched = cards
+                .filter { $0.item.appGroupIndex == pending.appGroupIndex }
+                .map { $0.item.window.id }
+                .sorted()
+            let dimmed = cards
+                .filter { $0.item.appGroupIndex != pending.appGroupIndex }
+                .map { $0.item.window.id }
+                .sorted()
+            return WindowShortcutFilterReport(
+                active: true,
+                phase: .appPending,
+                pendingAppGroupIndex: pending.appGroupIndex,
+                prefix: pending.appSymbol,
+                matchedWindowIDs: matched,
+                dimmedWindowIDs: dimmed
+            )
+        case .targetPreCommit:
+            guard let snapshot = lastWindowShortcutPreCommitFilterSnapshot else {
+                return emptyWindowShortcutFilterReport(phase: .none)
+            }
+            return WindowShortcutFilterReport(
+                active: true,
+                phase: .targetPreCommit,
+                pendingAppGroupIndex: nil,
+                prefix: snapshot.targetCode,
+                matchedWindowIDs: snapshot.matchedWindowIDs,
+                dimmedWindowIDs: snapshot.dimmedWindowIDs
+            )
+        case .invalid, .expired:
+            return emptyWindowShortcutFilterReport(phase: windowShortcutFilterPhase)
+        case .none:
+            return emptyWindowShortcutFilterReport(phase: .none)
+        }
+    }
+
+    private func emptyWindowShortcutFilterReport(
+        phase: WindowShortcutFilterPhase
+    ) -> WindowShortcutFilterReport {
+        WindowShortcutFilterReport(
+            active: false,
+            phase: phase,
+            pendingAppGroupIndex: nil,
+            prefix: nil,
+            matchedWindowIDs: [],
+            dimmedWindowIDs: []
+        )
+    }
+
     private func waterfallStateText(for card: QuickSwitchWindowCardViewModel) -> String {
         var states: [String] = []
         if card.window.isMinimized {
@@ -6844,6 +7027,10 @@
                 dy: column.containerLayer.frame.minY + column.cardsClipLayer.frame.minY
             )
             let screenshotSource = screenshotSourcesByWindowID[card.item.window.id]
+            let shortcutFilterState = windowShortcutFilterState(for: card)
+            let shortcutFilterOpacity = shortcutFilterState == .dimmed
+                ? windowShortcutFilterDimOpacity
+                : Float(1)
 
             var report = [
                 "appGroupIndex": card.item.appGroupIndex,
@@ -6862,6 +7049,8 @@
                 "isSelected": card.item.window.id == effectiveSelection?.windowID,
                 "isKeyboardFocused": card.item.window.id == keyboardFocusedWindowID,
                 "isHorizontalAppDefault": isHorizontalAppDefaultCard(card),
+                "windowShortcutFilterState": shortcutFilterState.rawValue,
+                "windowShortcutFilterOpacity": Double(shortcutFilterOpacity),
                 "visualStates": waterfallCardVisualStates(for: card),
                 "thumbnailStrategy": thumbnailStrategyString(for: card.item.window),
                 "screenshotSource": screenshotSourceString(screenshotSource, for: card.item.window),
@@ -7006,6 +7195,16 @@
         if isWaterfallCardSpaceFocused(card) {
             states.append("spaceFocused")
         }
+        switch windowShortcutFilterState(for: card) {
+        case .matched:
+            states.append("shortcutMatched")
+        case .dimmed:
+            states.append("shortcutDimmed")
+        case .target:
+            states.append("shortcutTarget")
+        case .none:
+            break
+        }
         if card.item.window.isMinimized {
             states.append("minimized")
         }
diff --git a/C3.tools/round1-main-ui-qa.sh b/C3.tools/round1-main-ui-qa.sh
index 99c1b6d..c8e283d 100755
--- a/C3.tools/round1-main-ui-qa.sh
+++ b/C3.tools/round1-main-ui-qa.sh
@@ -117,6 +117,7 @@
 run_step horizontal-waterfall "$SCRIPT_DIR/round1-horizontal-waterfall-fixture-qa.sh"
 run_step theme-toggle "$SCRIPT_DIR/round1-theme-toggle-fixture-qa.sh"
 run_step space-lane-dark-hover "$SCRIPT_DIR/round1-space-lane-dark-hover-fixture-qa.sh"
+run_step window-index-progressive-filter "$SCRIPT_DIR/round1-window-index-progressive-filter-fixture-qa.sh"
 run_step no-quick-switch-residual swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-no-quick-switch
 
 assert_no_current_aligner
@@ -130,5 +131,5 @@
 screenshots/fallbacks, minimized-window restore/activation, mouse activation,
 multi-page precise activation/close targeting, App column alignment,
 Split View Space Lane rendering, theme toggle, Space Lane dark hover readability,
-and final no-residual check.
+Option two-key progressive Waterfall filtering, and final no-residual check.
 EOF
diff --git a/C3.tools/round1-window-index-progressive-filter-fixture-qa.sh b/C3.tools/round1-window-index-progressive-filter-fixture-qa.sh
new file mode 100755
index 0000000..3e1a74c
--- /dev/null
+++ b/C3.tools/round1-window-index-progressive-filter-fixture-qa.sh
@@ -0,0 +1,809 @@
+#!/bin/bash
+# Round01 window index progressive filter fixture QA. It verifies that the
+# first Option window-index key dims only non-target Waterfall cards, and that
+# the second key commits immediately while preserving the pre-commit snapshot.
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+OUTPUT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+# shellcheck source=build-output-paths.sh
+source "$SCRIPT_DIR/build-output-paths.sh"
+
+APP="$BUILD_CURRENT_APP"
+DOMAIN="com.ar.Aligner"
+THEME_KEY="com.ar.Aligner.preferences.appearance.theme"
+REPORT_WAIT="${ALIGNER_ROUND1_WINDOW_INDEX_FILTER_REPORT_WAIT:-8.0}"
+FIXTURE_APP_COUNT="${ALIGNER_ROUND1_WINDOW_INDEX_FILTER_APP_COUNT:-8}"
+FIXTURE_WINDOWS_PER_APP="${ALIGNER_ROUND1_WINDOW_INDEX_FILTER_WINDOWS_PER_APP:-6}"
+TARGET_APP_INDEX="${ALIGNER_ROUND1_WINDOW_INDEX_FILTER_TARGET_APP_INDEX:-2}"
+TARGET_WINDOW_INDEX="${ALIGNER_ROUND1_WINDOW_INDEX_FILTER_TARGET_WINDOW_INDEX:-5}"
+APP_SYMBOL="${ALIGNER_ROUND1_WINDOW_INDEX_FILTER_APP_SYMBOL:-3}"
+WINDOW_SYMBOL="${ALIGNER_ROUND1_WINDOW_INDEX_FILTER_WINDOW_SYMBOL:-6}"
+TARGET_CODE="${APP_SYMBOL}${WINDOW_SYMBOL}"
+
+REPORT_HORIZONTAL_LIGHT="$BUILD_REPORT_ROOT/round01-window-index-progressive-filter-horizontal-light-report.json"
+REPORT_VERTICAL_LIGHT="$BUILD_REPORT_ROOT/round01-window-index-progressive-filter-vertical-light-report.json"
+REPORT_HORIZONTAL_DARK="$BUILD_REPORT_ROOT/round01-window-index-progressive-filter-horizontal-dark-report.json"
+REPORT_COMMIT="$BUILD_REPORT_ROOT/round01-window-index-progressive-filter-commit-report.json"
+REPORT_INVALID="$BUILD_REPORT_ROOT/round01-window-index-progressive-filter-invalid-report.json"
+SUMMARY_DIR="$BUILD_TMP_ROOT/round01-window-index-progressive-filter-fixture-qa"
+
+APP_PID=""
+OLD_THEME_SET=0
+OLD_THEME=""
+SUMMARY_FILES=()
+
+fail() {
+  echo "Round01 window index progressive filter fixture QA failed: $*" >&2
+  exit 1
+}
+
+aligner_pids_for_current_app() {
+  ps -axo pid=,args= | while read -r pid command; do
+    if [[ "$command" == "$APP/Contents/MacOS/Aligner"* ]] \
+      || [[ "$command" == "/Applications/Aligner.app/Contents/MacOS/Aligner"* ]] \
+      || [[ "$command" == "$HOME/Applications/Aligner.app/Contents/MacOS/Aligner"* ]]; then
+      echo "$pid"
+    fi
+  done
+}
+
+stop_current_aligner() {
+  for pid in $(aligner_pids_for_current_app); do
+    kill "$pid" 2>/dev/null || true
+  done
+
+  for _ in {1..30}; do
+    [ -z "$(aligner_pids_for_current_app)" ] && return
+    sleep 0.1
+  done
+
+  fail "current Aligner app did not exit before QA"
+}
+
+stop_current_aligner_safely() {
+  for pid in $(aligner_pids_for_current_app); do
+    kill "$pid" 2>/dev/null || true
+  done
+
+  for _ in {1..30}; do
+    [ -z "$(aligner_pids_for_current_app)" ] && return
+    sleep 0.1
+  done
+
+  echo "warning: current Aligner app did not exit during cleanup" >&2
+}
+
+preserve_user_theme() {
+  if OLD_THEME="$(/usr/bin/defaults read "$DOMAIN" "$THEME_KEY" 2>/dev/null)"; then
+    OLD_THEME_SET=1
+  else
+    OLD_THEME_SET=0
+    OLD_THEME=""
+  fi
+}
+
+restore_user_theme() {
+  if [ "$OLD_THEME_SET" -eq 1 ]; then
+    /usr/bin/defaults write "$DOMAIN" "$THEME_KEY" -string "$OLD_THEME"
+  else
+    /usr/bin/defaults delete "$DOMAIN" "$THEME_KEY" >/dev/null 2>&1 || true
+  fi
+}
+
+cleanup() {
+  if [ -n "${APP_PID:-}" ]; then
+    kill "$APP_PID" 2>/dev/null || true
+    wait "$APP_PID" 2>/dev/null || true
+    APP_PID=""
+  fi
+  stop_current_aligner_safely
+  restore_user_theme
+}
+
+set_theme() {
+  local theme="$1"
+  /usr/bin/defaults write "$DOMAIN" "$THEME_KEY" -string "$theme"
+}
+
+mode_report_value() {
+  local mode="$1"
+  case "$mode" in
+    horizontal) echo "horizontalMasonry" ;;
+    vertical) echo "verticalColumns" ;;
+    *) fail "unsupported waterfall mode $mode" ;;
+  esac
+}
+
+pending_key_sequence() {
+  echo "physical-index:$APP_SYMBOL"
+}
+
+commit_key_sequence() {
+  echo "physical-index:$APP_SYMBOL,physical-index:$WINDOW_SYMBOL"
+}
+
+pending_expected_sequence() {
+  echo "app:$APP_SYMBOL"
+}
+
+commit_expected_sequence() {
+  echo "app:$APP_SYMBOL,window:$TARGET_CODE"
+}
+
+invalid_key_sequence() {
+  echo "physical-index:$APP_SYMBOL,physical-index:Z"
+}
+
+invalid_expected_sequence() {
+  echo "app:$APP_SYMBOL,windowInvalid:${APP_SYMBOL}Z"
+}
+
+run_fixture() {
+  local report="$1"
+  local theme="$2"
+  local mode="$3"
+  local key_sequence="$4"
+
+  stop_current_aligner
+  set_theme "$theme"
+  rm -f "$report"
+
+  "$APP/Contents/MacOS/Aligner" \
+    --round0-skip-permissions \
+    --round01-open-quick-switch \
+    --round01-fixture-app-count="$FIXTURE_APP_COUNT" \
+    --round01-fixture-windows-per-app="$FIXTURE_WINDOWS_PER_APP" \
+    --round01-disable-screenshot-refresh \
+    --round01-waterfall-view-mode="$mode" \
+    --round01-debug-key-sequence="$key_sequence" \
+    --round01-quick-switch-report="$report" >/dev/null 2>&1 &
+
+  APP_PID=$!
+}
+
+finish_case() {
+  if [ -n "${APP_PID:-}" ]; then
+    kill "$APP_PID" 2>/dev/null || true
+    wait "$APP_PID" 2>/dev/null || true
+    APP_PID=""
+  fi
+  stop_current_aligner
+}
+
+wait_for_pending_report() {
+  local report="$1"
+  local expected_mode="$2"
+  local expected_sequence="$3"
+  local case_name="$4"
+
+  /usr/bin/python3 - "$report" "$REPORT_WAIT" "$expected_mode" "$expected_sequence" "$case_name" <<'PY'
+import json
+import sys
+import time
+
+path = sys.argv[1]
+timeout = float(sys.argv[2])
+expected_mode = sys.argv[3]
+expected_sequence = [part for part in sys.argv[4].split(",") if part]
+case_name = sys.argv[5]
+deadline = time.monotonic() + timeout
+last_report = None
+
+while time.monotonic() < deadline:
+    try:
+        with open(path, "r", encoding="utf-8") as file:
+            report = json.load(file)
+        last_report = report
+        root = report.get("rootView", {})
+        if (
+            report.get("snapshotLoaded") is True
+            and report.get("quickSwitchVisible") is True
+            and root.get("waterfallViewMode") == expected_mode
+            and root.get("keyboardCommandsApplied") == expected_sequence
+            and root.get("windowShortcutFilterPhase") == "appPending"
+        ):
+            sys.exit(0)
+    except FileNotFoundError:
+        pass
+    except json.JSONDecodeError:
+        pass
+    time.sleep(0.2)
+
+if last_report is not None:
+    print(json.dumps(last_report, indent=2, ensure_ascii=False), file=sys.stderr)
+print(f"{case_name}: pending filter report did not reach appPending within {timeout:.1f}s", file=sys.stderr)
+sys.exit(1)
+PY
+}
+
+wait_for_commit_report() {
+  local report="$1"
+  local expected_mode="$2"
+  local expected_sequence="$3"
+
+  /usr/bin/python3 - "$report" "$REPORT_WAIT" "$expected_mode" "$expected_sequence" "$TARGET_CODE" <<'PY'
+import json
+import sys
+import time
+
+path = sys.argv[1]
+timeout = float(sys.argv[2])
+expected_mode = sys.argv[3]
+expected_sequence = [part for part in sys.argv[4].split(",") if part]
+target_code = sys.argv[5]
+deadline = time.monotonic() + timeout
+last_report = None
+
+while time.monotonic() < deadline:
+    try:
+        with open(path, "r", encoding="utf-8") as file:
+            report = json.load(file)
+        last_report = report
+        root = report.get("rootView", {})
+        snapshot = root.get("lastWindowShortcutPreCommitFilterSnapshot")
+        if (
+            report.get("snapshotLoaded") is True
+            and root.get("waterfallViewMode") == expected_mode
+            and root.get("keyboardCommandsApplied") == expected_sequence
+            and root.get("lastWindowIndexKeyCommitCode") == target_code
+            and isinstance(snapshot, dict)
+        ):
+            sys.exit(0)
+    except FileNotFoundError:
+        pass
+    except json.JSONDecodeError:
+        pass
+    time.sleep(0.2)
+
+if last_report is not None:
+    print(json.dumps(last_report, indent=2, ensure_ascii=False), file=sys.stderr)
+print(f"commit report did not reach pre-commit snapshot within {timeout:.1f}s", file=sys.stderr)
+sys.exit(1)
+PY
+}
+
+wait_for_invalid_report() {
+  local report="$1"
+  local expected_mode="$2"
+  local expected_sequence="$3"
+
+  /usr/bin/python3 - "$report" "$REPORT_WAIT" "$expected_mode" "$expected_sequence" "${APP_SYMBOL}Z" <<'PY'
+import json
+import sys
+import time
+
+path = sys.argv[1]
+timeout = float(sys.argv[2])
+expected_mode = sys.argv[3]
+expected_sequence = [part for part in sys.argv[4].split(",") if part]
+invalid_code = sys.argv[5]
+deadline = time.monotonic() + timeout
+last_report = None
+
+while time.monotonic() < deadline:
+    try:
+        with open(path, "r", encoding="utf-8") as file:
+            report = json.load(file)
+        last_report = report
+        root = report.get("rootView", {})
+        if (
+            report.get("snapshotLoaded") is True
+            and report.get("quickSwitchVisible") is True
+            and root.get("waterfallViewMode") == expected_mode
+            and root.get("keyboardCommandsApplied") == expected_sequence
+            and root.get("lastWindowIndexKeyCommand") == f"windowInvalid:{invalid_code}"
+            and root.get("windowShortcutFilterPhase") == "invalid"
+        ):
+            sys.exit(0)
+    except FileNotFoundError:
+        pass
+    except json.JSONDecodeError:
+        pass
+    time.sleep(0.2)
+
+if last_report is not None:
+    print(json.dumps(last_report, indent=2, ensure_ascii=False), file=sys.stderr)
+print(f"invalid report did not reach invalid state within {timeout:.1f}s", file=sys.stderr)
+sys.exit(1)
+PY
+}
+
+assert_pending_report() {
+  local report="$1"
+  local case_name="$2"
+  local theme="$3"
+  local expected_mode="$4"
+  local summary_file="$SUMMARY_DIR/$case_name.json"
+
+  /usr/bin/python3 - \
+    "$report" \
+    "$case_name" \
+    "$theme" \
+    "$expected_mode" \
+    "$(pending_expected_sequence)" \
+    "$FIXTURE_APP_COUNT" \
+    "$FIXTURE_WINDOWS_PER_APP" \
+    "$TARGET_APP_INDEX" \
+    "$APP_SYMBOL" \
+    "$summary_file" <<'PY'
+import json
+import numbers
+import sys
+
+path = sys.argv[1]
+case_name = sys.argv[2]
+theme = sys.argv[3]
+expected_mode = sys.argv[4]
+expected_sequence = [part for part in sys.argv[5].split(",") if part]
+fixture_app_count = int(sys.argv[6])
+fixture_windows_per_app = int(sys.argv[7])
+target_app_index = int(sys.argv[8])
+app_symbol = sys.argv[9]
+summary_file = sys.argv[10]
+
+with open(path, "r", encoding="utf-8") as file:
+    report = json.load(file)
+
+def require(condition, message):
+    if not condition:
+        print(f"{case_name}: {message}", file=sys.stderr)
+        print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr)
+        sys.exit(1)
+
+def sorted_ints(values):
+    require(isinstance(values, list), "window ID field must be a list")
+    return sorted(int(value) for value in values)
+
+def is_number(value):
+    return isinstance(value, numbers.Real) and not isinstance(value, bool)
+
+root = report.get("rootView", {})
+columns = root.get("waterfallColumns", [])
+cards = [card for column in columns for card in column.get("cards", [])]
+target_column = next((column for column in columns if column.get("appGroupIndex") == target_app_index), None)
+
+require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true")
+require(report.get("quickSwitchVisible") is True, "pending filter must keep Quick Switch visible")
+require(report.get("appCount") == fixture_app_count, "fixture appCount must match")
+require(report.get("windowCount") == fixture_app_count * fixture_windows_per_app, "fixture windowCount must match")
+require(root.get("waterfallViewMode") == expected_mode, "Waterfall view mode must match case")
+require(root.get("keyboardCommandsApplied") == expected_sequence, "pending key sequence must be applied")
+require(root.get("appShelfIndexKeyDownCount") == 1, "pending filter must exercise one physical keyDown")
+require(root.get("lastAppShelfIndexKeySymbol") == app_symbol, "pending physical symbol must be reported")
+require(root.get("lastCommitSource") is None, "pending filter must not commit")
+require(root.get("lastCommittedWindowID") is None, "pending filter must not commit a window")
+require(root.get("lastWindowIndexKeyCommitCode") is None, "pending filter must not expose a commit code")
+
+require(root.get("windowShortcutFilterActive") is True, "windowShortcutFilterActive must be true")
+require(root.get("windowShortcutFilterPhase") == "appPending", "filter phase must be appPending")
+require(root.get("windowShortcutFilterPendingAppGroupIndex") == target_app_index, "pending App group must match first key")
+require(root.get("windowShortcutFilterPrefix") == app_symbol, "filter prefix must match first physical key")
+require(root.get("lastWindowShortcutPreCommitFilterSnapshot") is None, "pending run must not expose a pre-commit snapshot")
+
+require(target_column is not None, "target App column must exist")
+target_cards = target_column.get("cards", [])
+target_ids = sorted(int(card.get("windowID")) for card in target_cards)
+dimmed_cards = [card for card in cards if int(card.get("windowID")) not in set(target_ids)]
+dimmed_ids = sorted(int(card.get("windowID")) for card in dimmed_cards)
+
+require(len(target_cards) == fixture_windows_per_app, "target App must keep all fixture windows")
+require(root.get("windowShortcutFilterMatchedCount") == len(target_ids), "matched count must equal target App window count")
+require(root.get("windowShortcutFilterDimmedCount") == len(dimmed_ids), "dimmed count must equal non-target Waterfall cards")
+require(sorted_ints(root.get("windowShortcutFilterMatchedWindowIDs")) == target_ids, "matched window IDs must be exactly target App windows")
+require(sorted_ints(root.get("windowShortcutFilterDimmedWindowIDs")) == dimmed_ids, "dimmed window IDs must be exactly non-target Waterfall windows")
+
+matched_opacities = []
+dimmed_opacities = []
+for card in target_cards:
+    states = card.get("visualStates", [])
+    opacity = card.get("windowShortcutFilterOpacity")
+    require(card.get("windowShortcutFilterState") == "matched", "target App cards must report matched filter state")
+    require(is_number(opacity), "matched cards must expose numeric filter opacity")
+    require(0 <= opacity <= 1.01, "matched card opacity must be normalized")
+    require("shortcutMatched" in states, "matched cards must expose shortcutMatched visual state")
+    require("shortcutDimmed" not in states, "matched cards must not expose shortcutDimmed")
+    require("shortcutTarget" not in states, "pending matched cards must not expose shortcutTarget")
+    matched_opacities.append(float(opacity))
+
+for card in dimmed_cards:
+    states = card.get("visualStates", [])
+    opacity = card.get("windowShortcutFilterOpacity")
+    require(card.get("windowShortcutFilterState") == "dimmed", "non-target Waterfall cards must report dimmed filter state")
+    require(is_number(opacity), "dimmed cards must expose numeric filter opacity")
+    require(0 <= opacity <= 1.01, "dimmed card opacity must be normalized")
+    require("shortcutDimmed" in states, "dimmed cards must expose shortcutDimmed visual state")
+    require("shortcutMatched" not in states, "dimmed cards must not expose shortcutMatched")
+    require("shortcutTarget" not in states, "pending dimmed cards must not expose shortcutTarget")
+    dimmed_opacities.append(float(opacity))
+
+require(matched_opacities, "fixture must have matched cards")
+require(dimmed_opacities, "fixture must have dimmed cards")
+require(min(matched_opacities) > max(dimmed_opacities), "matched cards must stay visually brighter than dimmed cards")
+
+for node in root.get("appShelfItems", []) + root.get("spaceLaneSegments", []):
+    states = node.get("visualStates", [])
+    require("shortcutDimmed" not in states, "shortcut dim visual state must not leak to Space Lane or App Shelf")
+    if "windowShortcutFilterState" in node:
+        require(node.get("windowShortcutFilterState") != "dimmed", "Space Lane/App Shelf must not report dimmed filter state")
+    if "windowShortcutFilterOpacity" in node:
+        opacity = node.get("windowShortcutFilterOpacity")
+        require(is_number(opacity), "Space Lane/App Shelf filter opacity must be numeric if present")
+        require(opacity >= 0.99, "Space Lane/App Shelf must not be dimmed by window shortcut filter")
+
+summary = {
+    "case": case_name,
+    "status": "passed",
+    "theme": theme,
+    "mode": root.get("waterfallViewMode"),
+    "report": path,
+    "phase": root.get("windowShortcutFilterPhase"),
+    "prefix": root.get("windowShortcutFilterPrefix"),
+    "pendingAppGroupIndex": root.get("windowShortcutFilterPendingAppGroupIndex"),
+    "matchedCount": root.get("windowShortcutFilterMatchedCount"),
+    "dimmedCount": root.get("windowShortcutFilterDimmedCount"),
+    "matchedWindowIDs": target_ids,
+    "dimmedWindowIDs": dimmed_ids,
+    "matchedOpacityMin": min(matched_opacities),
+    "dimmedOpacityMax": max(dimmed_opacities),
+    "quickSwitchVisible": report.get("quickSwitchVisible"),
+    "commitSource": root.get("lastCommitSource"),
+}
+with open(summary_file, "w", encoding="utf-8") as file:
+    json.dump(summary, file, indent=2, ensure_ascii=False)
+PY
+
+  SUMMARY_FILES+=("$summary_file")
+}
+
+assert_commit_report() {
+  local report="$1"
+  local case_name="valid-commit-horizontal"
+  local summary_file="$SUMMARY_DIR/$case_name.json"
+
+  /usr/bin/python3 - \
+    "$report" \
+    "$case_name" \
+    "light" \
+    "horizontalMasonry" \
+    "$(commit_expected_sequence)" \
+    "$FIXTURE_APP_COUNT" \
+    "$FIXTURE_WINDOWS_PER_APP" \
+    "$TARGET_APP_INDEX" \
+    "$TARGET_WINDOW_INDEX" \
+    "$APP_SYMBOL" \
+    "$WINDOW_SYMBOL" \
+    "$TARGET_CODE" \
+    "$summary_file" <<'PY'
+import json
+import numbers
+import sys
+
+path = sys.argv[1]
+case_name = sys.argv[2]
+theme = sys.argv[3]
+expected_mode = sys.argv[4]
+expected_sequence = [part for part in sys.argv[5].split(",") if part]
+fixture_app_count = int(sys.argv[6])
+fixture_windows_per_app = int(sys.argv[7])
+target_app_index = int(sys.argv[8])
+target_window_index = int(sys.argv[9])
+app_symbol = sys.argv[10]
+window_symbol = sys.argv[11]
+target_code = sys.argv[12]
+summary_file = sys.argv[13]
+
+with open(path, "r", encoding="utf-8") as file:
+    report = json.load(file)
+
+def require(condition, message):
+    if not condition:
+        print(f"{case_name}: {message}", file=sys.stderr)
+        print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr)
+        sys.exit(1)
+
+def sorted_ints(values):
+    require(isinstance(values, list), "window ID field must be a list")
+    return sorted(int(value) for value in values)
+
+def is_number(value):
+    return isinstance(value, numbers.Real) and not isinstance(value, bool)
+
+root = report.get("rootView", {})
+columns = root.get("waterfallColumns", [])
+cards = [card for column in columns for card in column.get("cards", [])]
+target_column = next((column for column in columns if column.get("appGroupIndex") == target_app_index), None)
+
+require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true")
+require(report.get("appCount") == fixture_app_count, "fixture appCount must match")
+require(report.get("windowCount") == fixture_app_count * fixture_windows_per_app, "fixture windowCount must match")
+require(root.get("waterfallViewMode") == expected_mode, "commit run must use horizontal Waterfall")
+require(root.get("keyboardCommandsApplied") == expected_sequence, "commit key sequence must be applied")
+require(root.get("appShelfIndexKeyDownCount") == 2, "commit must exercise two physical keyDown events")
+require(root.get("lastAppShelfIndexKeySymbol") == window_symbol, "last physical symbol must be the window key")
+require(root.get("lastWindowIndexKeyCommand") == f"window:{target_code}", "last window shortcut command must be reported")
+require(root.get("lastWindowIndexKeyCommitCode") == target_code, "last window shortcut commit code must be reported")
+require(root.get("windowIndexKeyPendingAppGroupIndex") is None, "commit must clear legacy pending App group")
+require(root.get("windowIndexKeyPendingAppSymbol") is None, "commit must clear legacy pending App symbol")
+require(root.get("lastCommitSource") == "keyboard", "valid shortcut commit must be keyboard sourced")
+
+require(target_column is not None, "target App column must exist")
+target_cards = target_column.get("cards", [])
+require(target_window_index < len(target_cards), "target window index must exist")
+target_card = target_cards[target_window_index]
+target_window_id = int(target_card.get("windowID"))
+all_window_ids = sorted(int(card.get("windowID")) for card in cards)
+dimmed_ids = sorted(window_id for window_id in all_window_ids if window_id != target_window_id)
+
+require(target_card.get("windowShortcutCode") == target_code, "target card shortcut code must match committed code")
+require(root.get("lastCommittedAppGroupIndex") == target_app_index, "commit must target expected App")
+require(root.get("lastCommittedWindowIndex") == target_window_index, "commit must target expected window index")
+require(root.get("lastCommittedWindowID") == target_window_id, "commit must target expected window ID")
+require(root.get("selectedAppGroupIndex") == target_app_index, "selection must land on target App")
+require(root.get("selectedWindowIndex") == target_window_index, "selection must land on target window")
+require(root.get("selectedWindowID") == target_window_id, "selectedWindowID must match target window")
+
+snapshot = root.get("lastWindowShortcutPreCommitFilterSnapshot")
+require(isinstance(snapshot, dict), "pre-commit filter snapshot must be an object")
+require(snapshot.get("phase") == "targetPreCommit", "pre-commit snapshot phase must be targetPreCommit")
+require(snapshot.get("targetCode") == target_code, "pre-commit snapshot targetCode must match committed shortcut")
+require(int(snapshot.get("targetWindowID")) == target_window_id, "pre-commit snapshot targetWindowID must match target card")
+require(sorted_ints(snapshot.get("matchedWindowIDs")) == [target_window_id], "pre-commit snapshot must match only the target window")
+require(sorted_ints(snapshot.get("dimmedWindowIDs")) == dimmed_ids, "pre-commit snapshot must dim all non-target Waterfall windows")
+
+states = target_card.get("visualStates", [])
+opacity = target_card.get("windowShortcutFilterOpacity")
+require(target_card.get("windowShortcutFilterState") == "target", "target card must report target filter state")
+require(is_number(opacity), "target card must expose numeric filter opacity")
+require(0 <= opacity <= 1.01, "target card opacity must be normalized")
+require("shortcutTarget" in states, "target card must expose shortcutTarget visual state")
+
+for card in cards:
+    if int(card.get("windowID")) == target_window_id:
+        continue
+    card_states = card.get("visualStates", [])
+    card_opacity = card.get("windowShortcutFilterOpacity")
+    require(card.get("windowShortcutFilterState") == "dimmed", "non-target cards must remain dimmed in target pre-commit state")
+    require(is_number(card_opacity), "non-target cards must expose numeric filter opacity")
+    require("shortcutDimmed" in card_states, "non-target cards must expose shortcutDimmed in target pre-commit state")
+    require("shortcutTarget" not in card_states, "non-target cards must not expose shortcutTarget")
+
+for node in root.get("appShelfItems", []) + root.get("spaceLaneSegments", []):
+    states = node.get("visualStates", [])
+    require("shortcutDimmed" not in states, "shortcut dim visual state must not leak to Space Lane or App Shelf during commit")
+    if "windowShortcutFilterState" in node:
+        require(node.get("windowShortcutFilterState") != "dimmed", "Space Lane/App Shelf must not report dimmed filter state during commit")
+
+summary = {
+    "case": case_name,
+    "status": "passed",
+    "theme": theme,
+    "mode": root.get("waterfallViewMode"),
+    "report": path,
+    "commands": root.get("keyboardCommandsApplied"),
+    "targetCode": target_code,
+    "targetWindowID": target_window_id,
+    "matchedWindowIDs": snapshot.get("matchedWindowIDs"),
+    "dimmedCount": len(snapshot.get("dimmedWindowIDs", [])),
+    "lastCommitSource": root.get("lastCommitSource"),
+    "lastWindowIndexKeyCommitCode": root.get("lastWindowIndexKeyCommitCode"),
+    "targetCardState": target_card.get("windowShortcutFilterState"),
+}
+with open(summary_file, "w", encoding="utf-8") as file:
+    json.dump(summary, file, indent=2, ensure_ascii=False)
+PY
+
+  SUMMARY_FILES+=("$summary_file")
+}
+
+assert_invalid_report() {
+  local report="$1"
+  local case_name="invalid-second-key-horizontal"
+  local summary_file="$SUMMARY_DIR/$case_name.json"
+
+  /usr/bin/python3 - \
+    "$report" \
+    "$case_name" \
+    "horizontalMasonry" \
+    "$(invalid_expected_sequence)" \
+    "$APP_SYMBOL" \
+    "${APP_SYMBOL}Z" \
+    "$summary_file" <<'PY'
+import json
+import numbers
+import sys
+
+path = sys.argv[1]
+case_name = sys.argv[2]
+expected_mode = sys.argv[3]
+expected_sequence = [part for part in sys.argv[4].split(",") if part]
+app_symbol = sys.argv[5]
+invalid_code = sys.argv[6]
+summary_file = sys.argv[7]
+
+with open(path, "r", encoding="utf-8") as file:
+    report = json.load(file)
+
+def require(condition, message):
+    if not condition:
+        print(f"{case_name}: {message}", file=sys.stderr)
+        print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr)
+        sys.exit(1)
+
+def is_number(value):
+    return isinstance(value, numbers.Real) and not isinstance(value, bool)
+
+root = report.get("rootView", {})
+columns = root.get("waterfallColumns", [])
+cards = [card for column in columns for card in column.get("cards", [])]
+
+require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true")
+require(report.get("quickSwitchVisible") is True, "invalid shortcut must keep Quick Switch visible")
+require(root.get("waterfallViewMode") == expected_mode, "invalid run must use horizontal Waterfall")
+require(root.get("keyboardCommandsApplied") == expected_sequence, "invalid key sequence must be applied")
+require(root.get("appShelfIndexKeyDownCount") == 2, "invalid shortcut must exercise two physical keyDown events")
+require(root.get("lastAppShelfIndexKeySymbol") == "Z", "last physical symbol must be invalid window key")
+require(root.get("lastWindowIndexKeyCommand") == f"windowInvalid:{invalid_code}", "invalid command must be reported")
+require(root.get("lastWindowIndexKeyCommitCode") is None, "invalid shortcut must not expose commit code")
+require(root.get("lastCommitSource") is None, "invalid shortcut must not commit")
+require(root.get("lastCommittedWindowID") is None, "invalid shortcut must not commit a window")
+require(root.get("windowIndexKeyPendingAppGroupIndex") is None, "invalid shortcut must clear legacy pending App group")
+require(root.get("windowIndexKeyPendingAppSymbol") is None, "invalid shortcut must clear legacy pending App symbol")
+require(root.get("windowShortcutFilterActive") is False, "invalid shortcut must not leave active filter")
+require(root.get("windowShortcutFilterPhase") == "invalid", "invalid shortcut must report invalid phase")
+require(root.get("windowShortcutFilterPendingAppGroupIndex") is None, "invalid shortcut must clear pending App group")
+require(root.get("windowShortcutFilterPrefix") is None, "invalid shortcut must clear filter prefix")
+require(root.get("windowShortcutFilterMatchedCount") == 0, "invalid shortcut must clear matched count")
+require(root.get("windowShortcutFilterDimmedCount") == 0, "invalid shortcut must clear dimmed count")
+require(root.get("windowShortcutFilterMatchedWindowIDs") == [], "invalid shortcut must clear matched IDs")
+require(root.get("windowShortcutFilterDimmedWindowIDs") == [], "invalid shortcut must clear dimmed IDs")
+require(root.get("lastWindowShortcutPreCommitFilterSnapshot") is None, "invalid shortcut must not expose pre-commit snapshot")
+
+for card in cards:
+    opacity = card.get("windowShortcutFilterOpacity")
+    states = card.get("visualStates", [])
+    require(card.get("windowShortcutFilterState") == "none", "invalid shortcut must restore card filter state")
+    require(is_number(opacity), "cards must expose numeric filter opacity")
+    require(float(opacity) >= 0.99, "invalid shortcut must restore card opacity")
+    require("shortcutDimmed" not in states, "invalid shortcut must not leave shortcutDimmed state")
+    require("shortcutMatched" not in states, "invalid shortcut must not leave shortcutMatched state")
+    require("shortcutTarget" not in states, "invalid shortcut must not leave shortcutTarget state")
+
+summary = {
+    "case": case_name,
+    "status": "passed",
+    "mode": root.get("waterfallViewMode"),
+    "report": path,
+    "commands": root.get("keyboardCommandsApplied"),
+    "firstSymbol": app_symbol,
+    "invalidCode": invalid_code,
+    "phase": root.get("windowShortcutFilterPhase"),
+    "quickSwitchVisible": report.get("quickSwitchVisible"),
+    "cardCount": len(cards)
+}
+with open(summary_file, "w", encoding="utf-8") as file:
+    json.dump(summary, file, indent=2, ensure_ascii=False)
+PY
+
+  SUMMARY_FILES+=("$summary_file")
+}
+
+assert_pending_summaries_consistent() {
+  local baseline="$1"
+  local candidate="$2"
+  local label="$3"
+
+  /usr/bin/python3 - "$baseline" "$candidate" "$label" <<'PY'
+import json
+import sys
+
+baseline_path = sys.argv[1]
+candidate_path = sys.argv[2]
+label = sys.argv[3]
+
+with open(baseline_path, "r", encoding="utf-8") as file:
+    baseline = json.load(file)
+with open(candidate_path, "r", encoding="utf-8") as file:
+    candidate = json.load(file)
+
+fields = [
+    "phase",
+    "prefix",
+    "pendingAppGroupIndex",
+    "matchedCount",
+    "dimmedCount",
+    "matchedWindowIDs",
+    "dimmedWindowIDs",
+]
+
+for field in fields:
+    if baseline.get(field) != candidate.get(field):
+        print(f"{label}: pending field {field} differs between baseline and candidate", file=sys.stderr)
+        print(json.dumps({"baseline": baseline, "candidate": candidate}, indent=2, ensure_ascii=False), file=sys.stderr)
+        sys.exit(1)
+PY
+}
+
+emit_summary() {
+  /usr/bin/python3 - "$APP" "$FIXTURE_APP_COUNT" "$FIXTURE_WINDOWS_PER_APP" "${SUMMARY_FILES[@]}" <<'PY'
+import json
+import sys
+
+app = sys.argv[1]
+fixture_app_count = int(sys.argv[2])
+fixture_windows_per_app = int(sys.argv[3])
+case_paths = sys.argv[4:]
+cases = []
+for path in case_paths:
+    with open(path, "r", encoding="utf-8") as file:
+        cases.append(json.load(file))
+
+print(json.dumps({
+    "status": "passed",
+    "script": "round1-window-index-progressive-filter-fixture-qa.sh",
+    "app": app,
+    "fixture": {
+        "appCount": fixture_app_count,
+        "windowsPerApp": fixture_windows_per_app
+    },
+    "cases": cases,
+    "timeoutCleanup": {
+        "status": "skipped",
+        "reason": "Current --round01-debug-key-sequence has no stable wait token for the 0.9s expiry path; no sleep injection was added."
+    }
+}, indent=2, ensure_ascii=False))
+PY
+}
+
+if [ ! -x "$APP/Contents/MacOS/Aligner" ]; then
+  fail "current build app executable not found at $APP/Contents/MacOS/Aligner"
+fi
+if [ "$FIXTURE_APP_COUNT" -le "$TARGET_APP_INDEX" ]; then
+  fail "fixture app count must include target App index $TARGET_APP_INDEX"
+fi
+if [ "$FIXTURE_WINDOWS_PER_APP" -le "$TARGET_WINDOW_INDEX" ]; then
+  fail "fixture windows per App must include target window index $TARGET_WINDOW_INDEX"
+fi
+
+mkdir -p "$SUMMARY_DIR"
+rm -f "$SUMMARY_DIR"/*.json
+
+stop_current_aligner
+"$SCRIPT_DIR/package-app.sh" >&2
+preserve_user_theme
+trap cleanup EXIT
+stop_current_aligner
+
+run_fixture "$REPORT_HORIZONTAL_LIGHT" "light" "horizontal" "$(pending_key_sequence)"
+wait_for_pending_report "$REPORT_HORIZONTAL_LIGHT" "$(mode_report_value horizontal)" "$(pending_expected_sequence)" "pending-horizontal-light"
+assert_pending_report "$REPORT_HORIZONTAL_LIGHT" "pending-horizontal-light" "light" "$(mode_report_value horizontal)"
+finish_case
+
+run_fixture "$REPORT_VERTICAL_LIGHT" "light" "vertical" "$(pending_key_sequence)"
+wait_for_pending_report "$REPORT_VERTICAL_LIGHT" "$(mode_report_value vertical)" "$(pending_expected_sequence)" "pending-vertical-light"
+assert_pending_report "$REPORT_VERTICAL_LIGHT" "pending-vertical-light" "light" "$(mode_report_value vertical)"
+finish_case
+
+run_fixture "$REPORT_HORIZONTAL_DARK" "dark" "horizontal" "$(pending_key_sequence)"
+wait_for_pending_report "$REPORT_HORIZONTAL_DARK" "$(mode_report_value horizontal)" "$(pending_expected_sequence)" "pending-horizontal-dark"
+assert_pending_report "$REPORT_HORIZONTAL_DARK" "pending-horizontal-dark" "dark" "$(mode_report_value horizontal)"
+assert_pending_summaries_consistent \
+  "$SUMMARY_DIR/pending-horizontal-light.json" \
+  "$SUMMARY_DIR/pending-horizontal-dark.json" \
+  "pending-horizontal-dark"
+finish_case
+
+run_fixture "$REPORT_COMMIT" "light" "horizontal" "$(commit_key_sequence)"
+wait_for_commit_report "$REPORT_COMMIT" "$(mode_report_value horizontal)" "$(commit_expected_sequence)"
+assert_commit_report "$REPORT_COMMIT"
+finish_case
+
+run_fixture "$REPORT_INVALID" "light" "horizontal" "$(invalid_key_sequence)"
+wait_for_invalid_report "$REPORT_INVALID" "$(mode_report_value horizontal)" "$(invalid_expected_sequence)"
+assert_invalid_report "$REPORT_INVALID"
+finish_case
+
+cleanup
+trap - EXIT
+emit_summary

--
Gitblit v1.9.3