| | |
| | | "premiumThemes", |
| | | "layoutImport", |
| | | "layoutExport", |
| | | "customTagColors", |
| | | "unlimitedNotes", |
| | | "persistentAppSorting", |
| | | "customHotkeys", |
| | |
| | | require(pro, 'static let lifetimeProductID = "com.taglauncher.pro.lifetime"', "StoreKit lifetime product id changed or missing") |
| | | require(pro, 'static let firstFreeProVersion = "8.1.0"', "legacy unlock cutoff must be 8.1.0") |
| | | require(pro, 'static let qaStateEnvKey = "TAGLAUNCHER_QA_PRO_STATE"', "QA Pro-state override is missing") |
| | | require(pro, "static let freeNoteLimit = 5", "free note limit must remain explicit") |
| | | require(pro, "static let freeNoteLimit = 3", "free note limit must be 3") |
| | | require(pro, "static let freeThemes: Set<AppGridTheme> = [.defaultLight, .black]", "free theme set must be default/black") |
| | | require(pro, 'static let premiumDisplayModes: Set<String> = ["coloredContainer", "coloredGridContainer"]', "colored container/grid display modes must be Pro-only") |
| | | require(pro, "static func effectiveDisplayMode(for storedDisplayMode: String) -> String", "effective display mode gate is missing") |
| | | require(pro, "func startDisplayModePreview(for displayMode: String)", "Pro display-mode trial preview is missing") |
| | | require(pro, "Product.products(for: [ProEntitlementConfig.lifetimeProductID])", "StoreKit product lookup is missing") |
| | | require(pro, "Transaction.currentEntitlements", "current entitlement restore/read path is missing") |
| | | require(pro, "Transaction.updates", "transaction update listener is missing") |
| | |
| | | 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, .unlimitedNotes, .persistentAppSorting, .customHotkeys:", "custom hotkeys must be unlocked only by the Pro access state") |
| | | 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, "NotificationCenter.default.post(name: .tagLauncherProEntitlementChanged", "Pro entitlement changes must notify hotkey registration") |
| | | |
| | | effective_theme = function_body(pro, "static func effectiveTheme(for storedTheme: AppGridTheme) -> AppGridTheme") |
| | | require(effective_theme, "return .defaultLight", "locked premium theme must fall back to defaultLight") |
| | | require(effective_theme, "canUseTheme(storedTheme)", "effectiveTheme must check Pro theme entitlement") |
| | | |
| | | effective_display_mode = function_body(pro, "static func effectiveDisplayMode(for storedDisplayMode: String) -> String") |
| | | require(effective_display_mode, "snapshot.previewDisplayMode", "effective display mode must prefer active preview") |
| | | require(effective_display_mode, "canUseDisplayMode(storedDisplayMode)", "effective display mode must check Pro entitlement") |
| | | require(effective_display_mode, "fallbackDisplayMode(for: storedDisplayMode)", "locked colored display mode must fall back to its free variant") |
| | | |
| | | require(app, "ProEntitlementCenter.shared.start()", "App startup does not start Pro entitlement center") |
| | | require(app, "observeProEntitlementChanges()", "App startup must observe Pro entitlement changes for custom hotkeys") |
| | |
| | | if note_body.find("guard case .allow = decision else") > note_body.find("let previousStore = store"): |
| | | fail("note quota decision must happen before store mutation snapshot") |
| | | |
| | | require(data_layer, "var customColor: TagCustomColor? = nil", "tag definitions must store an optional custom color") |
| | | require(data_layer, "struct TagCustomColor: Codable, Equatable", "custom tag color must be Codable and comparable") |
| | | require(data_layer, "static func nsColor(for index: Int, customColor: TagCustomColor?) -> NSColor", "tag rendering must prefer custom colors with base-color fallback") |
| | | set_color_body = function_body(data_layer, "static func setColor(_ color: Int, for tag: String)") |
| | | require(set_color_body, "tagDef.customColor = nil", "selecting a base tag color must clear the custom color") |
| | | set_custom_color_body = function_body(data_layer, "static func setCustomColor(_ customColor: TagCustomColor, for tag: String) -> Bool") |
| | | require(set_custom_color_body, "guard ProEntitlementPolicy.isUnlocked(.customTagColors) else", "custom tag color writes must be hard-gated by Pro") |
| | | if set_custom_color_body.find("guard ProEntitlementPolicy.isUnlocked(.customTagColors) else") > set_custom_color_body.find("var store = TagDatabase.load()"): |
| | | fail("custom tag color lock must run before loading/writing store") |
| | | |
| | | export_ui = function_body(preferences, "private func exportTags()") |
| | | require(export_ui, "guard proEntitlement.isUnlocked else", "export UI must prompt Pro before file panel") |
| | | require(export_ui, "presentSettingsProPrompt(for: .layoutExport)", "export UI prompt feature is missing") |
| | |
| | | require(preferences, "ProStatusPill(text: tr(\"pro.card.badge\"), style: .locked, compact: true)", "locked data actions must show compact Pro pill") |
| | | require(preferences, "presentSettingsProPrompt(for: .premiumThemes)", "premium theme selection prompt is missing") |
| | | require(preferences, "presentSettingsProPrompt(for: .customHotkeys)", "custom hotkey Pro prompt is missing") |
| | | require(preferences, "presentSettingsProPrompt(for: .customTagColors)", "custom tag color Pro prompt is missing") |
| | | require(preferences, "private var settingsFixedProHeader: some View", "Settings pages must share one fixed-position Pro guidance header") |
| | | require(preferences, "private var settingsProHeaderText: String", "fixed Pro guidance header must switch copy by selected tab") |
| | | require(preferences, "private var settingsProHeaderFeature: ProFeature", "fixed Pro guidance header must route unlock prompts by selected tab") |
| | | require(preferences, "private var shouldShowSettingsFixedProHeader: Bool", "Language/About tabs must be able to hide the fixed Pro header") |
| | | require(preferences, "selectedTab != .language && selectedTab != .about", "fixed Pro header must be hidden on Language and About tabs") |
| | | require(preferences, "if shouldShowSettingsFixedProHeader", "Settings body must conditionally render the fixed Pro header") |
| | | fixed_header_body = function_body(preferences, "private var settingsFixedProHeader: some View") |
| | | require(fixed_header_body, "if !proEntitlement.isUnlocked", "fixed Pro header must branch on unlocked state") |
| | | require(fixed_header_body, 'Button(tr("pro.card.restore"))', "fixed Pro header must keep restore purchase for free users") |
| | | if fixed_header_body.find("if !proEntitlement.isUnlocked") > fixed_header_body.find('Button(tr("pro.card.restore"))'): |
| | | fail("fixed Pro header restore purchase button must only render for users who have not unlocked Pro") |
| | | purchase_guidance_body = function_body(preferences, "private func proPurchaseGuidanceLine(") |
| | | require(purchase_guidance_body, "if !proEntitlement.isUnlocked", "purchase guidance restore actions must be hidden after Pro unlock") |
| | | for unlocked_benefit_key in [ |
| | | "settings.proBenefit.general", |
| | | "pro.theme.status.unlocked", |
| | | "settings.proBenefit.hotkeys", |
| | | "settings.proBenefit.tags", |
| | | "settings.proBenefit.data", |
| | | ]: |
| | | require(preferences, unlocked_benefit_key, f"missing unlocked Pro benefit line for {unlocked_benefit_key}") |
| | | for purchase_guidance_key in [ |
| | | "settings.proGuide.general", |
| | | "settings.proGuide.hotkeys", |
| | | "settings.proGuide.tags", |
| | | "settings.proGuide.data", |
| | | ]: |
| | | require(preferences, purchase_guidance_key, f"missing Pro purchase guidance line for {purchase_guidance_key}") |
| | | require(preferences, 'freeStatusKey: "settings.proStatus.unavailableFeature"', "Pro comparison locked rows must show unavailable for Free users") |
| | | require(preferences, 'freeStatusKey: "settings.proStatus.previewFiveMinutes"', "Pro comparison preview rows must show the 5-minute preview state for Free users") |
| | | require(preferences, 'proLockedStatusKey: "settings.proStatus.lockedFeature"', "Pro comparison Pro column must show supported before purchase") |
| | | require(preferences, 'proUnlockedStatusKey: "settings.proStatus.unlockedFeature"', "Pro comparison Pro column must show unlocked after purchase") |
| | | if preferences.count("freeStyle: .neutral") < 6 or preferences.count("proLockedStyle: .neutral") < 6 or preferences.count("proUnlockedStyle: .neutral") < 6: |
| | | fail("Pro comparison status chips must use the neutral style without crown icons") |
| | | if 'freeStatusKey: "settings.proStatus.limitedFeature"' in preferences or 'freeStatusKey: "settings.proStatus.unsupportedFeature"' in preferences: |
| | | fail("Pro comparison table must not use the old Limited/Unsupported status for locked rows") |
| | | if "settingsProStatusOverview(" in preferences or "SettingsProOverviewFeature" in preferences: |
| | | fail("settings pages must not repeat the bulky Pro status overview card across tabs") |
| | | for removed_about_pro_ui in [ |
| | | "proStatusCard", |
| | | "settingsProComparisonTable", |
| | | "SettingsProComparisonFeature", |
| | | ]: |
| | | if removed_about_pro_ui in preferences: |
| | | fail(f"about page must not render the removed Pro summary/comparison UI: {removed_about_pro_ui}") |
| | | require(preferences, "case pro", "Settings must include a dedicated Pro tab") |
| | | require(preferences, 'case .pro: return "pro.card.badge"', "Pro tab title must use the existing Pro label") |
| | | require(preferences, 'case .pro: return "crown"', "Pro tab must use the line-style crown icon") |
| | | for filled_tab_icon in ["paintpalette.fill", "tag.fill", "externaldrive.fill"]: |
| | | if filled_tab_icon in preferences: |
| | | fail(f"settings tab icons must use line style, found filled icon: {filled_tab_icon}") |
| | | require(preferences, "HStack(spacing: 6)", "settings tab bar spacing must be tightened for 8 tabs") |
| | | require(preferences, ".frame(width: 94, height: 72)", "settings tab buttons must be tightened for 8 tabs") |
| | | require(preferences, "case .pro:", "Settings tab switch must render the Pro tab") |
| | | require(content, 'static let pro = "pro"', "Settings tab target must include the Pro tab") |
| | | require(app, "openPreferences(targetTab: SettingsTabTarget.pro)", "status menu Pro CTA must open the Pro settings tab") |
| | | require(preferences, 'selectedTab == .pro', "fixed Pro header must render the Pro tab user status state") |
| | | require(preferences, "proTabComparisonTable(width: dataPanelWidth)", "Pro tab must render the Free-vs-Pro comparison table") |
| | | require(preferences, "private struct ProTabComparisonFeature", "Pro tab comparison row model is missing") |
| | | if 'Text(tr("settings.proCompare.title"))' in preferences: |
| | | fail("Pro tab comparison table must not render the removed title") |
| | | require(preferences, 'Text(tr("settings.proCompare.free"))', "Pro tab comparison table free column is missing") |
| | | require(preferences, 'Text(tr("settings.proCompare.pro"))', "Pro tab comparison table Pro column is missing") |
| | | require(preferences, 'Image(systemName: "crown")', "Pro tab Pro column header must show a crown icon before the text") |
| | | 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") |
| | | 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 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, "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") |
| | | require(preferences, "displayModeOptionAccessory(for mode: String)", "colored display mode Pro badge/countdown accessory is missing") |
| | | require(preferences, "isSelected: effectiveDisplayMode ==", "display mode buttons must select by effective display mode") |
| | | require(preferences, ".frame(width: 64, alignment: .trailing)", "display mode buttons must reserve a fixed accessory slot") |
| | | require(pro, "func remainingSeconds(at date: Date = Date()) -> Int", "theme preview remaining-seconds helper is missing") |
| | | require(pro, "ceil(endsAt.timeIntervalSince(date))", "theme preview countdown must derive from endsAt") |
| | | require(preferences, "pro.theme.status.previewCountdown", "theme preview status countdown localization is missing") |
| | |
| | | |
| | | require(content, "@ObservedObject private var proEntitlement = ProEntitlementCenter.shared", "ContentView does not observe Pro entitlement") |
| | | require(content, "ProEntitlementPolicy.effectiveTheme(for: appGridTheme)", "ContentView does not apply effective Pro theme") |
| | | require(content, "ProEntitlementPolicy.effectiveDisplayMode(for: displayMode)", "ContentView does not apply effective Pro display mode") |
| | | require(content, "displayMode: renderedDisplayMode", "AppGrid does not render the effective display mode") |
| | | require(content, "presentProPrompt(for: .unlimitedNotes", "note quota prompt is missing from AppGrid") |
| | | require(content, "tr(\"pro.sorting.previewToast\")", "free sorting preview toast is missing") |
| | | require(content, "if let preview = proEntitlement.themePreviewState", "theme preview acceptance path is missing") |
| | |
| | | require(access_views, "struct ProUpgradePromptView", "ProUpgradePromptView is missing") |
| | | require(access_views, "let onUnlock: () -> Void", "Pro upgrade prompt purchase callback is missing") |
| | | require(access_views, "let onRestore: () -> Void", "Pro upgrade prompt restore callback is missing") |
| | | 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") |
| | | |
| | | print("PASS Pro feature gate QA: StoreKit entitlement, legacy unlock, theme gating, import/export locks, note quota, sorting preview, and Pro UI prompts are wired") |
| | | tag_editor = read("TagEditorView.swift") |
| | | require(tag_editor, "ColorPicker(", "Pro custom tag colors must use a native macOS color picker path") |
| | | require(tag_editor, "onLockedCustomColor", "locked custom color entry must route to the Pro prompt") |
| | | require(tag_editor, "TagEditor.setCustomColor", "tag editor must persist Pro custom colors") |
| | | require(tag_editor, "tagCustomColors.removeValue(forKey: tagName)", "base color selection must remove existing custom color") |
| | | require(content, "tagCustomColors = snapshot.tagDefinitions.compactMapValues { $0.customColor }", "AppGrid snapshots must propagate custom tag colors") |
| | | require(content, "customColor: tagCustomColors[$0.name]", "tag navigation must receive custom tag colors") |
| | | app_grid = read("AppGridCollectionView.swift") |
| | | tag_nav = read("TagNavigationView.swift") |
| | | require(app_grid, "let tagCustomColors: [String: TagCustomColor]", "AppGrid must accept custom tag colors") |
| | | require(app_grid, "func tagColor(for groupName: String) -> NSColor", "AppGrid must resolve custom tag color per group") |
| | | 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") |
| | | PY |