Ariver
2026-06-30 bcceca83ce966598e00e0319d85ec11d6df9d272
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
#!/usr/bin/env bash
set -euo pipefail
 
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
 
python3 - "$ROOT_DIR" <<'PY'
import re
import sys
from pathlib import Path
 
root = Path(sys.argv[1])
app_dir = root / "Apptag"
 
 
def fail(message: str) -> None:
    raise SystemExit(f"FAIL: {message}")
 
 
def read(name: str) -> str:
    path = app_dir / name
    try:
        return path.read_text(encoding="utf-8")
    except Exception as error:
        fail(f"could not read {path}: {error}")
 
 
def require(source: str, needle: str, message: str) -> None:
    if needle not in source:
        fail(message)
 
 
def function_body(source: str, signature: str) -> str:
    start = source.find(signature)
    if start == -1:
        fail(f"missing function signature: {signature}")
    brace = source.find("{", start)
    if brace == -1:
        fail(f"missing function body for: {signature}")
    depth = 0
    for index in range(brace, len(source)):
        char = source[index]
        if char == "{":
            depth += 1
        elif char == "}":
            depth -= 1
            if depth == 0:
                return source[brace + 1:index]
    fail(f"unterminated function body for: {signature}")
 
 
pro = read("ProEntitlement.swift")
data_layer = read("DataLayer.swift")
app = read("ApptagApp.swift")
theme = read("AppGridTheme.swift")
preferences = read("PreferencesView.swift")
content = read("ContentView.swift")
access_views = read("ProAccessViews.swift")
 
for case in [
    "premiumThemes",
    "layoutImport",
    "layoutExport",
    "customTagColors",
    "unlimitedNotes",
    "persistentAppSorting",
    "customHotkeys",
]:
    require(pro, f"case {case}", f"missing ProFeature.{case}")
 
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 = 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, "AppTransaction.shared", "legacy original app version path is missing")
require(pro, "appTransaction.originalAppVersion", "legacy originalAppVersion check is missing")
require(pro, "appTransaction.originalPurchaseDate", "legacy originalPurchaseDate fallback is missing")
require(pro, "SemanticVersion(ProEntitlementConfig.firstFreeProVersion)", "legacy semantic version cutoff 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, .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")
require(app, "applyCustomHotkey(_ hotkey: LauncherHotkey, for kind: LauncherHotkeyKind)", "AppDelegate custom hotkey save entry is missing")
require(app, "guard ProEntitlementPolicy.isUnlocked(.customHotkeys) else", "custom hotkey save/restore must be hard-gated by Pro")
require(app, "LauncherHotkeySettings.effectiveHotkey(for: kind)", "global hotkey registration must use the entitlement-aware effective shortcut")
 
require(theme, "var requiresPro: Bool", "AppGridTheme does not expose Pro requirement")
require(theme, "ProEntitlementConfig.freeThemes.contains(self)", "theme Pro requirement must use central free theme set")
require(theme, "static var premiumThemes: [AppGridTheme]", "theme premium list is missing")
 
export_body = function_body(data_layer, "static func exportTo(_ url: URL) throws")
require(export_body, "try ProEntitlementPolicy.requireUnlocked(.layoutExport)", "layout export is not locked at data layer")
if export_body.find("try ProEntitlementPolicy.requireUnlocked(.layoutExport)") > export_body.find("data.write"):
    fail("layout export lock must run before writing data")
 
import_body = function_body(data_layer, "static func importFrom(_ url: URL) throws -> Store")
require(import_body, "try ProEntitlementPolicy.requireUnlocked(.layoutImport)", "layout import is not locked at data layer")
if import_body.find("try ProEntitlementPolicy.requireUnlocked(.layoutImport)") > import_body.find("Data(contentsOf: url)"):
    fail("layout import lock must run before reading user-selected file")
 
reorder_body = function_body(data_layer, "static func reorderApps(inContainer containerID: String, orderedPaths: [String])")
require(reorder_body, "guard ProEntitlementPolicy.isUnlocked(.persistentAppSorting) else { return }", "app sorting persistence is not locked")
if reorder_body.find("guard ProEntitlementPolicy.isUnlocked(.persistentAppSorting) else { return }") > reorder_body.find("var store = TagDatabase.load()"):
    fail("app sorting lock must run before loading/writing store")
 
note_body = function_body(data_layer, "static func setAppNote(_ note: String, for path: String) -> ProNoteSaveDecision")
require(note_body, "ProEntitlementPolicy.noteSaveDecision", "note save does not check Pro quota")
require(note_body, "guard case .allow = decision else", "blocked note save must return without mutating store")
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")
if export_ui.find("guard proEntitlement.isUnlocked else") > export_ui.find("prepareDataFilePanelPresentation()"):
    fail("export UI must check Pro before opening file panel")
 
import_ui = function_body(preferences, "private func importTags()")
require(import_ui, "guard proEntitlement.isUnlocked else", "import UI must prompt Pro before file panel")
require(import_ui, "presentSettingsProPrompt(for: .layoutImport)", "import UI prompt feature is missing")
if import_ui.find("guard proEntitlement.isUnlocked else") > import_ui.find("prepareDataFilePanelPresentation()"):
    fail("import UI must check Pro before opening file panel")
 
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(preferences, "formatThemePreviewCountdown(seconds:", "theme preview countdown formatter is missing")
require(preferences, ".countdown(text: themePreviewCountdownText ?? \"00:00\")", "current preview theme card must show countdown")
require(preferences, ".onReceive(themePreviewCountdownTimer)", "theme preview countdown UI timer is missing")
require(preferences, ".font(.system(size: 10, weight: .semibold, design: .monospaced))", "theme preview card countdown must use monospaced digits")
 
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(content, "ProUpgradePromptView(", "AppGrid Pro upgrade prompt view is missing")
 
require(access_views, "struct ProStatusPill", "ProStatusPill view 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")
 
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