Ariver
2026-08-14 86f0b4bd20cdec86dfb5bdb7978d1f6ff4af111a
C3.tools/round1-horizontal-waterfall-fixture-qa.sh
@@ -1,7 +1,8 @@
#!/bin/bash
# Round01.1 horizontal Waterfall fixture QA. It verifies the P1 masonry view:
# no horizontal scroll, centered equal-width fixed-height cards, 150pt vertical
# placement steps, App Shelf anchoring, and horizontal-mode Tab navigation.
# placement steps, App Shelf anchoring, horizontal-mode Tab navigation, and
# horizontal arrow focus across App Shelf icons plus window cards.
set -euo pipefail
@@ -10,14 +11,33 @@
# 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"
LAYOUT_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-layout-report.json"
KEYBOARD_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-keyboard-report.json"
APP_ENTER_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-app-enter-report.json"
LETTER_INDEX_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-letter-index-report.json"
WINDOW_SHORTCUT_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-window-shortcut-report.json"
OVERFLOW_APP_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-window-shortcut-overflow-app-report.json"
OVERFLOW_WINDOW_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-window-shortcut-overflow-window-report.json"
ARROW_WRAP_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-arrow-wrap-report.json"
FILTER_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-filter-report.json"
FIXTURE_APP_COUNT="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_FIXTURE_APP_COUNT:-8}"
FIXTURE_WINDOWS_PER_APP="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_FIXTURE_WINDOWS_PER_APP:-6}"
HOVER_APP_INDEX="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_HOVER_APP_INDEX:-7}"
KEY_SEQUENCE="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_KEY_SEQUENCE:-tab}"
APP_ENTER_KEY_SEQUENCE="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_APP_ENTER_KEY_SEQUENCE:-physical-index:3,enter}"
APP_ENTER_TARGET_APP_INDEX="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_APP_ENTER_TARGET_APP_INDEX:-2}"
LETTER_INDEX_FIXTURE_APP_COUNT="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_LETTER_INDEX_FIXTURE_APP_COUNT:-12}"
LETTER_INDEX_KEY_SEQUENCE="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_LETTER_INDEX_KEY_SEQUENCE:-physical-index:A}"
LETTER_INDEX_TARGET_APP_INDEX="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_LETTER_INDEX_TARGET_APP_INDEX:-10}"
WINDOW_SHORTCUT_KEY_SEQUENCE="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_WINDOW_SHORTCUT_KEY_SEQUENCE:-physical-index:3,physical-index:6}"
WINDOW_SHORTCUT_TARGET_APP_INDEX="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_WINDOW_SHORTCUT_TARGET_APP_INDEX:-2}"
WINDOW_SHORTCUT_TARGET_WINDOW_INDEX="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_WINDOW_SHORTCUT_TARGET_WINDOW_INDEX:-5}"
ARROW_WRAP_KEY_SEQUENCE="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_ARROW_WRAP_KEY_SEQUENCE:-app:1,down,up,left,down,enter}"
REPORT_WAIT="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_REPORT_WAIT:-6.0}"
OLD_THEME_SET=0
OLD_THEME=""
fail() {
  echo "Round01.1 horizontal Waterfall fixture QA failed: $*" >&2
@@ -43,6 +63,23 @@
  done
  fail "current Aligner app did not exit before QA"
}
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
}
wait_for_loaded_report() {
@@ -122,11 +159,12 @@
require(root.get("horizontalMasonryMaxScrollOffset", 0) > 0, "horizontal masonry must expose positive vertical max scroll offset")
require(root.get("horizontalMasonryScrollOffset", 0) > 0, "hovering a later App must anchor masonry vertically")
require(root.get("appShelfHoveredIndex") == hover_app_index, "debug hover must mark the requested App Shelf item")
require(root.get("hoveredAppGroupIndex") == hover_app_index, "horizontal masonry must keep the hovered App group available to Waterfall visuals")
require(cards, "horizontal masonry fixture must expose cards")
widths = [round(card.get("frame", {}).get("width", 0), 1) for card in cards]
widths = [round(card.get("bounds", {}).get("width", 0), 1) for card in cards]
require(len(set(widths)) == 1, "horizontal masonry cards must be equal width")
heights = [round(card.get("frame", {}).get("height", 0), 1) for card in cards]
heights = [round(card.get("bounds", {}).get("height", 0), 1) for card in cards]
expected_card_height = 138.0
expected_thumbnail_height = 106.0
expected_card_step = 150.0
@@ -161,6 +199,44 @@
hover_visible = hover_first_card.get("visibleFrame", {})
require(hover_visible.get("y", -9999) < waterfall_height + 1, "hovered App first card should be brought into vertical viewport")
require(hover_visible.get("y", 0) + hover_visible.get("height", 0) > -1, "hovered App first card should not be fully below viewport")
hover_cards = hover_column.get("cards", [])
non_hover_cards = [
    card
    for column in columns
    if column.get("appGroupIndex") != hover_app_index
    for card in column.get("cards", [])
]
blue_reference = next(
    (
        color
        for segment in root.get("spaceLaneSegments", [])
        for color in segment.get("windowBlockColors", [])
        if color.get("alpha", 0) > 0
    ),
    None
)
require(blue_reference is not None, "fixture must expose a Space Lane blue reference color")
require(all("appLinked" in card.get("visualStates", []) for card in hover_cards), "all cards for the hovered App must expose appLinked visual state")
require(all("appLinked" not in card.get("visualStates", []) for card in non_hover_cards), "non-hovered App cards must not expose appLinked visual state")
require(root.get("selectedAppGroupIndex") == hover_app_index, "hovered App must select its first window as the default submit target")
require(root.get("selectedWindowIndex") == 0, "hovered App default submit target must be its first window")
require(root.get("horizontalAppDefaultWindowID") == hover_first_card.get("windowID"), "hovered App default window ID must match the first card")
require(hover_first_card.get("isSelected") is True, "hovered App first card must become the effective selection")
require(hover_first_card.get("isHorizontalAppDefault") is True, "hovered App first card must expose app default state")
require("appDefault" in hover_first_card.get("visualStates", []), "hovered App first card must report appDefault visual state")
require("selected" in hover_first_card.get("visualStates", []), "hovered App first card must get selected visual state")
require(all(card.get("shadowOpacity", 0) >= 0.15 for card in hover_cards), "hovered App cards must float with visible shadow opacity")
require(all(card.get("shadowRadius", 0) >= 18 for card in hover_cards), "hovered App cards must float with visible shadow radius")
require(all(card.get("shadowOpacity", 1) <= 0.01 for card in non_hover_cards if "spaceFocused" not in card.get("visualStates", []) and "hover" not in card.get("visualStates", []) and "selected" not in card.get("visualStates", [])), "normal non-hovered cards must not inherit App hover shadow")
for card in hover_cards:
    title_fill = card.get("titleBarBackgroundColor", {})
    require(title_fill.get("alpha", 0) >= 0.16, "hovered App card title bar must get a visible blue fill")
    for channel in ("red", "green", "blue"):
        require(
            abs(title_fill.get(channel, -1) - blue_reference.get(channel, -2)) <= 0.002,
            f"hovered App card title bar must use the Space Lane blue reference for {channel}"
        )
require(all(card.get("titleBarBackgroundColor", {}).get("alpha", 1) <= 0.01 for card in non_hover_cards), "non-hovered App card title bars must stay clear")
print(json.dumps({
    "mode": root.get("waterfallViewMode"),
@@ -170,6 +246,9 @@
    "cardHeight": heights[0],
    "thumbnailHeight": thumbnail_heights[0],
    "cardStep": expected_card_step,
    "hoveredAppGroupIndex": root.get("hoveredAppGroupIndex"),
    "hoveredAppLinkedCardCount": len(hover_cards),
    "hoveredAppTitleBarFill": hover_cards[0].get("titleBarBackgroundColor"),
    "horizontalMasonryScrollOffset": root.get("horizontalMasonryScrollOffset"),
    "horizontalMasonryMaxScrollOffset": root.get("horizontalMasonryMaxScrollOffset")
}, indent=2, ensure_ascii=False))
@@ -213,6 +292,298 @@
PY
}
assert_app_enter_report() {
  /usr/bin/python3 - "$APP_ENTER_REPORT" "$APP_ENTER_KEY_SEQUENCE" "$APP_ENTER_TARGET_APP_INDEX" <<'PY'
import json
import sys
path = sys.argv[1]
raw_key_sequence = [part for part in sys.argv[2].split(",") if part]
target_app_index = int(sys.argv[3])
with open(path, "r", encoding="utf-8") as file:
    report = json.load(file)
def normalize_key_sequence(sequence):
    normalized = []
    physical_symbols = []
    for part in sequence:
        lowered = part.lower()
        matched_prefix = None
        for prefix in ("physical-index:", "physical-index-", "keydown:", "keydown-"):
            if lowered.startswith(prefix):
                matched_prefix = prefix
                break
        if matched_prefix is None:
            normalized.append(part)
            continue
        symbol = part[len(matched_prefix):].strip().upper()
        physical_symbols.append(symbol)
        normalized.append(f"app:{symbol}")
    return normalized, physical_symbols
key_sequence, physical_symbols = normalize_key_sequence(raw_key_sequence)
def require(condition, message):
    if not condition:
        print(message, file=sys.stderr)
        print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr)
        sys.exit(1)
root = report.get("rootView", {})
columns = root.get("waterfallColumns", [])
require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true")
require(root.get("waterfallViewMode") == "horizontalMasonry", "App Enter run must use horizontalMasonry")
require(root.get("keyboardCommandsApplied") == key_sequence, "App Enter keyboard commands must be applied")
require(root.get("appShelfIndexKeyDownCount") == len(physical_symbols), "physical App index keyDown path must be exercised")
if physical_symbols:
    require(root.get("lastAppShelfIndexKeySymbol") == physical_symbols[-1], "last physical App index symbol must be reported")
require(target_app_index < len(columns), "target App index must exist")
current_target_cards = columns[target_app_index].get("cards", [])
committed_window_id = root.get("lastCommittedWindowID")
suppressed_window_ids = set(report.get("suppressedActivationWindowIDs", []))
require(root.get("hoverTargetKind") == "app", "App index focus must leave hover target on the App icon")
require(root.get("keyboardFocusedAppGroupIndex") == target_app_index, "keyboard App focus must target requested App")
require(root.get("keyboardFocusedWindowID") is None, "App icon focus must not keep stale window keyboard focus")
require(root.get("lastCommittedAppGroupIndex") == target_app_index, "Enter on App icon must commit the target App")
require(root.get("lastCommittedWindowIndex") == 0, "Enter on App icon must commit the first window")
require(committed_window_id in suppressed_window_ids, "Enter on App icon may remove committed first window only when suppression records it")
require(root.get("lastCommitSource") == "keyboard", "App Enter commit must be keyboard sourced")
require(committed_window_id not in [card.get("windowID") for card in current_target_cards], "suppressed committed App Enter target must not remain in current cards")
print(json.dumps({
    "commands": root.get("keyboardCommandsApplied"),
    "hoverTargetKind": root.get("hoverTargetKind"),
    "targetAppGroupIndex": target_app_index,
    "lastCommittedWindowID": root.get("lastCommittedWindowID")
}, indent=2, ensure_ascii=False))
PY
}
assert_letter_index_report() {
  /usr/bin/python3 - "$LETTER_INDEX_REPORT" "$LETTER_INDEX_KEY_SEQUENCE" "$LETTER_INDEX_TARGET_APP_INDEX" <<'PY'
import json
import sys
path = sys.argv[1]
raw_key_sequence = [part for part in sys.argv[2].split(",") if part]
target_app_index = int(sys.argv[3])
with open(path, "r", encoding="utf-8") as file:
    report = json.load(file)
def require(condition, message):
    if not condition:
        print(message, file=sys.stderr)
        print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr)
        sys.exit(1)
def normalize_key_sequence(sequence):
    normalized = []
    physical_symbols = []
    for part in sequence:
        lowered = part.lower()
        matched_prefix = None
        for prefix in ("physical-index:", "physical-index-", "keydown:", "keydown-"):
            if lowered.startswith(prefix):
                matched_prefix = prefix
                break
        if matched_prefix is None:
            normalized.append(part)
            continue
        symbol = part[len(matched_prefix):].strip().upper()
        physical_symbols.append(symbol)
        normalized.append(f"app:{symbol}")
    return normalized, physical_symbols
key_sequence, physical_symbols = normalize_key_sequence(raw_key_sequence)
root = report.get("rootView", {})
columns = root.get("waterfallColumns", [])
require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true")
require(root.get("waterfallViewMode") == "horizontalMasonry", "letter App index run must use horizontalMasonry")
require(root.get("keyboardCommandsApplied") == key_sequence, "letter App index keyboard commands must be applied")
require(root.get("appShelfIndexKeyDownCount") == len(physical_symbols), "letter App index must exercise physical keyDown path")
require(physical_symbols, "letter App index test must contain a physical key")
require(root.get("lastAppShelfIndexKeySymbol") == physical_symbols[-1], "last letter App index symbol must be reported")
require(target_app_index < len(columns), "target letter App index must exist")
target_card = columns[target_app_index].get("cards", [])[0]
require(root.get("hoverTargetKind") == "app", "letter App index focus must leave hover target on App icon")
require(root.get("hoverTargetSource") == "keyboard", "letter App index focus must report keyboard source")
require(root.get("keyboardFocusedAppGroupIndex") == target_app_index, "letter App index must focus the expected App")
require(root.get("keyboardFocusedWindowID") is None, "letter App icon focus must not keep stale window keyboard focus")
require(root.get("selectedAppGroupIndex") == target_app_index, "letter App icon focus must select the target App")
require(root.get("selectedWindowIndex") == 0, "letter App icon focus must select the first window")
require(root.get("horizontalAppDefaultWindowID") == target_card.get("windowID"), "letter App default window must match the first card")
require(root.get("lastCommittedWindowID") is None, "letter App index focus without Enter must not commit")
require(root.get("lastCommitSource") is None, "letter App index focus without Enter must not report commit source")
require(target_card.get("isHorizontalAppDefault") is True, "letter target first card must report app default state")
require("appDefault" in target_card.get("visualStates", []), "letter target first card must expose appDefault visual state")
require("selected" in target_card.get("visualStates", []), "letter target first card must expose selected visual state")
print(json.dumps({
    "commands": root.get("keyboardCommandsApplied"),
    "physicalKeyDownCount": root.get("appShelfIndexKeyDownCount"),
    "lastSymbol": root.get("lastAppShelfIndexKeySymbol"),
    "targetAppGroupIndex": target_app_index,
    "defaultWindowID": root.get("horizontalAppDefaultWindowID")
}, indent=2, ensure_ascii=False))
PY
}
assert_window_shortcut_report() {
  /usr/bin/python3 - "$WINDOW_SHORTCUT_REPORT" "$WINDOW_SHORTCUT_KEY_SEQUENCE" "$WINDOW_SHORTCUT_TARGET_APP_INDEX" "$WINDOW_SHORTCUT_TARGET_WINDOW_INDEX" <<'PY'
import json
import sys
path = sys.argv[1]
raw_key_sequence = [part for part in sys.argv[2].split(",") if part]
target_app_index = int(sys.argv[3])
target_window_index = int(sys.argv[4])
with open(path, "r", encoding="utf-8") as file:
    report = json.load(file)
def require(condition, message):
    if not condition:
        print(message, file=sys.stderr)
        print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr)
        sys.exit(1)
def physical_symbol(part):
    lowered = part.lower()
    for prefix in ("physical-index:", "physical-index-", "keydown:", "keydown-"):
        if lowered.startswith(prefix):
            return part[len(prefix):].strip().upper()
    return None
physical_symbols = [symbol for part in raw_key_sequence if (symbol := physical_symbol(part))]
require(len(physical_symbols) == 2, "window shortcut test must use two physical symbols")
expected_code = "".join(physical_symbols)
expected_sequence = [f"app:{physical_symbols[0]}", f"window:{expected_code}"]
root = report.get("rootView", {})
columns = root.get("waterfallColumns", [])
require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true")
require(root.get("waterfallViewMode") == "horizontalMasonry", "window shortcut run must use horizontalMasonry")
require(root.get("keyboardCommandsApplied") == expected_sequence, "two-key window shortcut commands must be applied")
require(root.get("appShelfIndexKeyDownCount") == 2, "two-key window shortcut must exercise two physical keyDown events")
require(root.get("lastAppShelfIndexKeySymbol") == physical_symbols[-1], "last physical symbol must be the window index key")
require(root.get("lastWindowIndexKeyCommand") == f"window:{expected_code}", "last window shortcut command must be reported")
require(root.get("lastWindowIndexKeyCommitCode") == expected_code, "last window shortcut commit code must be reported")
require(root.get("windowIndexKeyPendingAppGroupIndex") is None, "two-key commit must clear pending App")
require(root.get("windowIndexKeyPendingAppSymbol") is None, "two-key commit must clear pending App symbol")
require(target_app_index < len(columns), "target App index must exist")
target_column = columns[target_app_index]
cards = target_column.get("cards", [])
committed_window_id = root.get("lastCommittedWindowID")
suppressed_window_ids = set(report.get("suppressedActivationWindowIDs", []))
pre_commit = root.get("lastWindowShortcutPreCommitFilterSnapshot", {})
require(pre_commit.get("phase") == "targetPreCommit", "two-key shortcut must record a pre-commit filter snapshot")
require(pre_commit.get("targetCode") == expected_code, "pre-commit snapshot must record target shortcut code")
require(pre_commit.get("targetWindowID") == committed_window_id, "pre-commit target must match committed window ID")
require(pre_commit.get("matchedWindowIDs") == [committed_window_id], "pre-commit snapshot must match exactly the committed window")
require(root.get("lastCommittedAppGroupIndex") == target_app_index, "two-key shortcut must commit target App")
require(root.get("lastCommittedWindowIndex") == target_window_index, "two-key shortcut must commit target window")
require(committed_window_id in suppressed_window_ids, "two-key shortcut may remove committed target only when suppression records it")
require(root.get("lastCommitSource") == "keyboard", "two-key shortcut commit must be keyboard sourced")
require(committed_window_id not in [card.get("windowID") for column in columns for card in column.get("cards", [])], "suppressed two-key target must not remain in current cards")
for column in columns:
    for card in column.get("cards", []):
        code = card.get("windowShortcutCode")
        require(code != "No Space" and code != "N...", "window shortcut code must not expose the old No Space label")
print(json.dumps({
    "commands": root.get("keyboardCommandsApplied"),
    "code": expected_code,
    "lastCommittedWindowID": root.get("lastCommittedWindowID")
}, indent=2, ensure_ascii=False))
PY
}
assert_window_shortcut_overflow_report() {
  /usr/bin/python3 - "$OVERFLOW_APP_REPORT" "$OVERFLOW_WINDOW_REPORT" <<'PY'
import json
import sys
app_path = sys.argv[1]
window_path = sys.argv[2]
with open(app_path, "r", encoding="utf-8") as file:
    app_report = json.load(file)
with open(window_path, "r", encoding="utf-8") as file:
    window_report = json.load(file)
def require(report, condition, message):
    if not condition:
        print(message, file=sys.stderr)
        print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr)
        sys.exit(1)
app_root = app_report.get("rootView", {})
app_columns = app_root.get("waterfallColumns", [])
require(app_report, app_report.get("snapshotLoaded") is True, "App overflow report must be loaded")
require(app_report, len(app_columns) == 37, "App overflow fixture must expose 37 columns")
require(app_report, app_columns[35].get("cards", [])[0].get("windowShortcutCode") == "Z1", "36th App must still expose a shortcut code")
require(app_report, app_columns[36].get("cards", [])[0].get("windowShortcutCode") == "", "37th App must not expose a two-key shortcut code")
window_root = window_report.get("rootView", {})
window_columns = window_root.get("waterfallColumns", [])
require(window_report, window_report.get("snapshotLoaded") is True, "Window overflow report must be loaded")
require(window_report, len(window_columns) == 1, "Window overflow fixture must expose one column")
cards = window_columns[0].get("cards", [])
require(window_report, len(cards) == 37, "Window overflow fixture must expose 37 windows")
require(window_report, cards[35].get("windowShortcutCode") == "1Z", "36th window must still expose a shortcut code")
require(window_report, cards[36].get("windowShortcutCode") == "", "37th window must not expose a two-key shortcut code")
print(json.dumps({
    "appOverflowCode": app_columns[36].get("cards", [])[0].get("windowShortcutCode"),
    "windowOverflowCode": cards[36].get("windowShortcutCode")
}, indent=2, ensure_ascii=False))
PY
}
assert_arrow_wrap_report() {
  /usr/bin/python3 - "$ARROW_WRAP_REPORT" "$ARROW_WRAP_KEY_SEQUENCE" "$FIXTURE_APP_COUNT" <<'PY'
import json
import sys
path = sys.argv[1]
key_sequence = [part for part in sys.argv[2].split(",") if part]
fixture_app_count = int(sys.argv[3])
with open(path, "r", encoding="utf-8") as file:
    report = json.load(file)
def require(condition, message):
    if not condition:
        print(message, file=sys.stderr)
        print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr)
        sys.exit(1)
root = report.get("rootView", {})
columns = root.get("waterfallColumns", [])
target_app_index = fixture_app_count - 1
require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true")
require(root.get("waterfallViewMode") == "horizontalMasonry", "arrow wrap run must use horizontalMasonry")
require(root.get("keyboardCommandsApplied") == key_sequence, "arrow wrap keyboard commands must be applied")
require(len(columns) == fixture_app_count, "fixture App count must match")
committed_window_id = root.get("lastCommittedWindowID")
suppressed_window_ids = set(report.get("suppressedActivationWindowIDs", []))
require(root.get("lastCommittedAppGroupIndex") == target_app_index, "left arrow from first App icon must wrap to the last App")
require(root.get("lastCommittedWindowIndex") == 0, "down arrow from App icon must land on the first window")
require(committed_window_id in suppressed_window_ids, "arrow focus Enter may remove committed target only when suppression records it")
require(root.get("lastCommitSource") == "keyboard", "arrow focus Enter commit must be keyboard sourced")
require(committed_window_id not in [card.get("windowID") for column in columns for card in column.get("cards", [])], "suppressed arrow target must not remain in current cards")
print(json.dumps({
    "commands": root.get("keyboardCommandsApplied"),
    "wrappedAppGroupIndex": target_app_index,
    "lastCommittedWindowID": root.get("lastCommittedWindowID")
}, indent=2, ensure_ascii=False))
PY
}
assert_filter_report() {
  /usr/bin/python3 - "$FILTER_REPORT" <<'PY'
import json
@@ -246,9 +617,9 @@
require(root.get("projectionTransitionFadeOutLayerCount", 0) > 0, "horizontal Space filter must animate removed cards/apps")
require(root.get("projectionTransitionDurationMilliseconds", 0) >= 120, "horizontal Space filter must report projection duration")
widths = [round(card.get("frame", {}).get("width", 0), 1) for card in cards]
widths = [round(card.get("bounds", {}).get("width", 0), 1) for card in cards]
require(len(set(widths)) == 1, "filtered horizontal cards must remain equal width")
heights = [round(card.get("frame", {}).get("height", 0), 1) for card in cards]
heights = [round(card.get("bounds", {}).get("height", 0), 1) for card in cards]
require(len(set(heights)) == 1 and abs(heights[0] - 138.0) <= 1.0, "filtered horizontal cards must keep the fixed 138pt height")
visible_width = root.get("waterfallVisibleWidth", 0)
require(all(card.get("frame", {}).get("x", -1) >= -1 for card in cards), "filtered cards must not overflow left")
@@ -269,12 +640,29 @@
  local report="$1"
  shift
  run_fixture_with_app_count "$report" "$FIXTURE_APP_COUNT" "$@"
}
run_fixture_with_app_count() {
  local report="$1"
  local app_count="$2"
  shift 2
  run_fixture_with_shape "$report" "$app_count" "$FIXTURE_WINDOWS_PER_APP" "$@"
}
run_fixture_with_shape() {
  local report="$1"
  local app_count="$2"
  local windows_per_app="$3"
  shift 3
  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-fixture-app-count="$app_count" \
    --round01-fixture-windows-per-app="$windows_per_app" \
    --round01-disable-screenshot-refresh \
    --round01-waterfall-view-mode=horizontal \
    --round01-quick-switch-report="$report" \
@@ -302,6 +690,9 @@
if [ "$FIXTURE_APP_COUNT" -le "$HOVER_APP_INDEX" ]; then
  fail "hover App index must be inside fixture app count"
fi
if [ "$LETTER_INDEX_FIXTURE_APP_COUNT" -le "$LETTER_INDEX_TARGET_APP_INDEX" ]; then
  fail "letter index target App must be inside letter fixture app count"
fi
if [ "$FIXTURE_WINDOWS_PER_APP" -lt 2 ]; then
  fail "fixture must include at least two windows per App for Tab navigation"
fi
@@ -316,8 +707,11 @@
    wait "$APP_PID" 2>/dev/null || true
  fi
  stop_current_aligner
  restore_user_theme
}
trap cleanup EXIT
preserve_user_theme
/usr/bin/defaults write "$DOMAIN" "$THEME_KEY" -string "light"
run_fixture "$LAYOUT_REPORT" --round01-debug-mouse-sequence="hover-app:$HOVER_APP_INDEX"
wait_for_loaded_report "$LAYOUT_REPORT"
@@ -333,6 +727,47 @@
wait "$APP_PID" 2>/dev/null || true
stop_current_aligner
run_fixture "$APP_ENTER_REPORT" --round01-debug-key-sequence="$APP_ENTER_KEY_SEQUENCE"
wait_for_loaded_report "$APP_ENTER_REPORT"
assert_app_enter_report
kill "$APP_PID" 2>/dev/null || true
wait "$APP_PID" 2>/dev/null || true
stop_current_aligner
run_fixture_with_app_count "$LETTER_INDEX_REPORT" "$LETTER_INDEX_FIXTURE_APP_COUNT" --round01-debug-key-sequence="$LETTER_INDEX_KEY_SEQUENCE"
wait_for_loaded_report "$LETTER_INDEX_REPORT"
assert_letter_index_report
kill "$APP_PID" 2>/dev/null || true
wait "$APP_PID" 2>/dev/null || true
stop_current_aligner
run_fixture "$WINDOW_SHORTCUT_REPORT" --round01-debug-key-sequence="$WINDOW_SHORTCUT_KEY_SEQUENCE"
wait_for_loaded_report "$WINDOW_SHORTCUT_REPORT"
assert_window_shortcut_report
kill "$APP_PID" 2>/dev/null || true
wait "$APP_PID" 2>/dev/null || true
stop_current_aligner
run_fixture_with_shape "$OVERFLOW_APP_REPORT" 37 1
wait_for_loaded_report "$OVERFLOW_APP_REPORT"
kill "$APP_PID" 2>/dev/null || true
wait "$APP_PID" 2>/dev/null || true
stop_current_aligner
run_fixture_with_shape "$OVERFLOW_WINDOW_REPORT" 1 37
wait_for_loaded_report "$OVERFLOW_WINDOW_REPORT"
assert_window_shortcut_overflow_report
kill "$APP_PID" 2>/dev/null || true
wait "$APP_PID" 2>/dev/null || true
stop_current_aligner
run_fixture "$ARROW_WRAP_REPORT" --round01-debug-key-sequence="$ARROW_WRAP_KEY_SEQUENCE"
wait_for_loaded_report "$ARROW_WRAP_REPORT"
assert_arrow_wrap_report
kill "$APP_PID" 2>/dev/null || true
wait "$APP_PID" 2>/dev/null || true
stop_current_aligner
run_space_filter_fixture "$FILTER_REPORT"
wait_for_loaded_report "$FILTER_REPORT"
assert_filter_report
@@ -341,4 +776,5 @@
trap - EXIT
stop_current_aligner
restore_user_theme
echo "Round01.1 horizontal Waterfall fixture QA passed"