Ariver
2026-07-01 0ae3a9359e4cdf6ae5b669c4affd91662463f370
src/Scripts/pro_feature_gate_qa.sh
@@ -61,6 +61,7 @@
    "layoutImport",
    "layoutExport",
    "customTagColors",
    "customContainerQuota",
    "unlimitedNotes",
    "persistentAppSorting",
    "customHotkeys",
@@ -85,7 +86,7 @@
require(pro, "ProEntitlementSnapshotStore", "sync entitlement snapshot for data-layer policy is missing")
require(pro, "manualNonEmptyNoteCount", "note quota must count existing manual notes")
require(pro, "manualNonEmptyNotePaths", "note quota must avoid double-counting existing notes")
require(pro, "case .premiumThemes, .layoutImport, .layoutExport, .customTagColors, .unlimitedNotes, .persistentAppSorting, .customHotkeys:", "custom tag colors and custom hotkeys must be unlocked only by the Pro access state")
require(pro, "case .premiumThemes, .layoutImport, .layoutExport, .customTagColors, .customContainerQuota, .unlimitedNotes, .persistentAppSorting, .customHotkeys:", "custom tag colors, custom container quota, and custom hotkeys must be unlocked only by the Pro access state")
require(pro, "NotificationCenter.default.post(name: .tagLauncherProEntitlementChanged", "Pro entitlement changes must notify hotkey registration")
effective_theme = function_body(pro, "static func effectiveTheme(for storedTheme: AppGridTheme) -> AppGridTheme")
@@ -248,15 +249,36 @@
if 'Text(tr("settings.language"))' in preferences:
    fail("Language tab must not render the removed header/title block")
require(preferences, "centeredSettingsScrollContent<Content: View>", "settings pages with sparse content must use the shared global centering container")
require(preferences, "centeredSettingsScrollContent(width: settingsContentWidth)", "language picker matrix must use the shared centered settings container")
require(preferences, "private let languageContentWidth: CGFloat = 820", "language picker matrix must have its own centered width aligned with fixed settings panels")
require(preferences, "centeredSettingsScrollContent(width: languageContentWidth)", "language picker matrix must use the centered settings container with the language-specific width")
if preferences.count("centeredSettingsScrollContent(width: dataPanelWidth)") < 2:
    fail("Pro and About pages must both use the shared centered settings container")
require(preferences, ".frame(minHeight: proxy.size.height, alignment: .center)", "centered settings content must be vertically centered in the visible area")
if "topLeadingAccessory: AnyView(" in preferences:
    fail("Tags page must not render a second Pro guidance row inside the tag editor")
if "topLeadingAccessory: AnyView(settingsProStatusOverview" in preferences:
    fail("Tags page must not render a second bulky Pro guidance row inside the tag editor")
if preferences.count("ScrollView(.vertical, showsIndicators: true)") < 3:
    fail("hotkeys/data/about pages must use vertical scroll containers to avoid clipping")
require(preferences, "NSEvent.addLocalMonitorForEvents(matching: .keyDown)", "custom hotkey recording must be local to Settings")
require(preferences, "settingsEscapeMonitor", "Settings must own a local Escape monitor for overlay presentation")
require(preferences, "installSettingsEscapeMonitor()", "Settings Escape monitor must be installed when Preferences appears")
require(preferences, "removeSettingsEscapeMonitor()", "Settings Escape monitor must be removed when Preferences disappears")
settings_escape_body = function_body(preferences, "private func handleSettingsEscapeKey() -> Bool")
for needle, message in [
    ("guard !isDataFilePanelPresented, window.attachedSheet == nil else { return false }", "Settings Escape must let file panels/sheets handle Escape first"),
    ("dismissSettingsProPrompt()", "Settings Escape must close the in-window Pro prompt before closing Settings"),
    ("showApplySystemSchemeConfirmation = false", "Settings Escape must close apply-confirmation overlays before closing Settings"),
    ("showResetToUncategorizedConfirmation = false", "Settings Escape must close reset-confirmation overlays before closing Settings"),
    ("stopHotkeyRecording(showCancelledToast: true)", "Settings Escape must cancel hotkey recording before closing Settings"),
    ("window.performClose(nil)", "Settings Escape must close the Settings window in the normal overlay case"),
]:
    require(settings_escape_body, needle, message)
settings_escape_filter_body = function_body(preferences, "private func isSettingsOverlayEscapeEvent(_ event: NSEvent) -> Bool")
for needle, message in [
    ("let parent = window.parent as? OverlayPanel", "Settings Escape must be limited to Settings shown over the AppGrid overlay"),
    ("parent.isVisible", "Settings Escape must require the overlay parent to be visible"),
    ("eventWindow == window || eventWindow == parent", "Settings Escape must handle both Settings-key and overlay-key event routing"),
]:
    require(settings_escape_filter_body, needle, message)
require(preferences, "proEntitlement.startThemePreview(for: theme, tuning:", "premium theme free preview is missing")
require(preferences, "proEntitlement.stopThemePreview()", "theme preview cleanup is missing")
require(preferences, "proEntitlement.startDisplayModePreview(for: mode)", "colored display mode free preview is missing")
@@ -287,7 +309,8 @@
if "localizedCaseInsensitiveContains" in access_views:
    fail("ProStatusPill must not hide the crown based on localized text content")
require(content, "onUnlock: { proEntitlement.purchasePro() }", "AppGrid Pro prompt is not wired to purchase action")
require(content, "onRestore: { proEntitlement.restorePurchases() }", "AppGrid Pro prompt is not wired to restore action")
require(content, "onRestore: { handleProPromptSecondaryAction(prompt) }", "AppGrid Pro prompt is not wired to secondary action handler")
require(content, "proEntitlement.restorePurchases()", "AppGrid Pro prompt secondary handler must still support restore purchase")
tag_editor = read("TagEditorView.swift")
require(tag_editor, "ColorPicker(", "Pro custom tag colors must use a native macOS color picker path")
@@ -303,5 +326,5 @@
require(tag_nav, "let customColor: TagCustomColor?", "Tag navigation items must carry custom colors")
require(tag_nav, "TagColor.nsColor(for: item.colorIndex, customColor: item.customColor)", "Tag navigation must render custom colors")
print("PASS Pro feature gate QA: StoreKit entitlement, legacy unlock, theme gating, import/export locks, 3-note quota, custom tag colors, custom hotkeys, sorting preview, and Pro UI prompts are wired")
print("PASS Pro feature gate QA: StoreKit entitlement, legacy unlock, theme gating, import/export locks, 3-note quota, custom tag colors, custom container quota, custom hotkeys, sorting preview, and Pro UI prompts are wired")
PY