Ariver
2026-07-02 5dbabdb9df401b30e8c92cfb3fb089a0fe1b4f9b
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
#!/usr/bin/env bash
set -euo pipefail
 
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
 
python3 - "$ROOT_DIR" <<'PY'
import json
import sys
from pathlib import Path
 
root = Path(sys.argv[1])
app_dir = root / "Apptag"
localization_dir = app_dir / "Localization"
 
 
def fail(message: str) -> None:
    raise SystemExit(f"FAIL: {message}")
 
 
def read(name: str) -> str:
    return (app_dir / name).read_text(encoding="utf-8")
 
 
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")
tag_editor = read("TagEditorView.swift")
content = read("ContentView.swift")
preferences = read("PreferencesView.swift")
access = read("ProAccessViews.swift")
 
require(pro, "case customContainerQuota", "ProFeature.customContainerQuota is missing")
require(pro, "static let freeCustomContainerLimit = 6", "free custom container limit must be 6")
require(pro, "struct ProCustomContainerQuotaStatus", "custom container quota status model is missing")
require(pro, "customContainerMaintenanceDecision", "custom container maintenance decision helper is missing")
require(pro, "TagDatabase.customMaintainedContainerCount", "quota must count through TagDatabase")
require(access, "case .customContainerQuota:", "custom container quota prompt key is missing")
 
require(data_layer, "var isCustomMaintained: Bool? = nil", "TagDef must store an optional custom-maintenance marker for old-data compatibility")
require(data_layer, "static func customMaintainedContainerCount", "TagDatabase custom container count helper is missing")
require(data_layer, "guard let systemCategoryID = definition.systemCategoryID else", "user-created tags must count as custom maintained containers")
require(data_layer, "definition.color != systemCategoryID.defaultColorIndex", "system tags with changed base color must count")
require(data_layer, "definition.customColor != nil", "system tags with custom color must count")
require(data_layer, "markCustomMaintainedIfNeeded", "system tag manual maintenance marker is missing")
require(data_layer, "static func createTag(_ name: String, color: Int) -> TagEditorMutationResult", "createTag must return a mutation result")
require(data_layer, "customContainerMaintenanceResult(for: [name], in: store)", "createTag must check free quota before saving")
require(data_layer, "static func renameTag(from oldName: String, to newName: String) -> TagEditorMutationResult", "renameTag must return a mutation result")
require(data_layer, "def.isCustomMaintained = true", "renamed system tags must become custom maintained")
require(data_layer, "static func setColor(_ color: Int, for tag: String) -> TagEditorMutationResult", "setColor must return a mutation result")
require(data_layer, "willBeCustomMaintained", "setColor must check whether a system tag becomes custom maintained")
require(data_layer, "static func appendTags(_ tags: [String], to paths: [String]) -> TagEditorMutationResult", "appendTags must return a mutation result")
require(data_layer, "static func removeTags(_ tags: [String], from paths: [String]) -> TagEditorMutationResult", "removeTags must return a mutation result")
require(data_layer, "static func moveApp(path: String, from sourceTag: String, to targetTag: String, color: Int, copy: Bool) -> TagEditorMutationResult", "moveApp must return a mutation result")
require(data_layer, "TagDatabase.markCustomMaintainedIfNeeded", "membership changes must mark maintained system tags")
require(data_layer, "static func reorderTags(_ names: [String]) -> Bool", "tag order persistence must return whether it saved")
reorder_tags_body = function_body(data_layer, "static func reorderTags(_ names: [String]) -> Bool")
if "ProEntitlementPolicy.isUnlocked(.persistentAppSorting)" in reorder_tags_body:
    fail("tag list order persistence must be available to free users")
 
require(tag_editor, "onCustomContainerQuotaExceeded", "TagEditorView must expose quota-blocked callback")
require(tag_editor, "onCustomContainerQuotaStatusChanged", "TagEditorView must expose quota status refresh callback")
require(tag_editor, "onCustomContainerQuotaStatusChanged(ProEntitlementPolicy.customContainerQuotaStatus())", "TagEditorView must refresh quota status after successful mutations")
require(tag_editor, "onCustomContainerQuotaStatusChanged(status)", "TagEditorView must surface blocked quota status to the header")
require(tag_editor, "guard handleMutationResult(result) else { return }", "TagEditorView must not update local state before a save succeeds")
require(tag_editor, "blockedCustomContainerQuota", "TagEditorView must surface quota blocks")
 
require(content, "persistTagOrder()", "ContentView must persist tag list order")
persist_tag_order_body = function_body(content, "private func persistTagOrder()")
if "proEntitlement.isUnlocked" in persist_tag_order_body or "pro.tagSorting.previewToast" in persist_tag_order_body:
    fail("free users must be able to persist tag list order without a Pro preview toast")
require(content, "presentCustomContainerQuotaPrompt", "ContentView must show custom container quota prompt")
require(content, "customContainerQuotaAccessory", "ContentView tag editor must show quota count")
require(content, "@State private var customContainerQuotaStatus = ProEntitlementPolicy.customContainerQuotaStatus()", "ContentView quota count must be backed by SwiftUI state")
require(content, "let status = effectiveCustomContainerQuotaStatus", "ContentView quota accessory must read state-backed effective status")
require(content, "onCustomContainerQuotaStatusChanged: refreshCustomContainerQuotaStatus", "ContentView must refresh quota count after TagEditorView saves")
require(content, "refreshCustomContainerQuotaStatus(in: store)", "ContentView must initialize quota count from the edit-mode store")
require(content, "pro.customContainers.manageExisting", "quota prompt secondary action must manage existing tags")
require(content, "handleTagMutationResult(result)", "ContentView membership mutations must respect quota blocks")
 
require(preferences, "presentSettingsCustomContainerQuotaPrompt", "Settings tag page must show quota prompt")
require(preferences, "customContainerQuotaAccessory", "Settings tag page must show quota count")
require(preferences, "@State private var settingsCustomContainerQuotaOverviewStatus = ProEntitlementPolicy.customContainerQuotaStatus()", "Settings quota count must be backed by SwiftUI state")
require(preferences, "let status = effectiveCustomContainerQuotaStatus", "Settings quota accessory must read state-backed effective status")
require(preferences, "onCustomContainerQuotaStatusChanged: refreshSettingsCustomContainerQuotaStatus", "Settings must refresh quota count after TagEditorView saves")
require(preferences, "settingsCustomContainerQuotaOverviewStatus = quotaStatus", "Settings scanApps must initialize quota count from the same store")
require(preferences, 'title: tr("pro.feature.maintainableTagCount")', "Pro comparison must include maintainable tag count row")
if 'title: tr("pro.feature.tagOrderPersistence")' in preferences:
    fail("Pro comparison must not list tag list order; it is available to free users")
require(preferences, 'title: tr("pro.feature.containerAppOrderPersistence")', "Pro comparison must include container app order row")
 
first_two = [
    preferences.find('title: tr("pro.feature.maintainableTagCount")'),
    preferences.find('title: tr("pro.feature.containerAppOrderPersistence")'),
]
custom_color_index = preferences.find('title: tr("pro.feature.customTagColors")')
if any(index == -1 for index in first_two) or custom_color_index == -1:
    fail("could not locate Pro comparison feature rows")
if not (first_two[0] < first_two[1] < custom_color_index):
    fail("Pro comparison rows must begin with tag quota and container app order")
 
required_l10n = [
    "pro.feature.customContainerQuota",
    "pro.feature.customContainerQuota.benefit",
    "pro.prompt.customContainerQuota",
    "pro.customContainers.quotaShort",
    "pro.customContainers.quotaUnlimitedShort",
    "pro.customContainers.quotaPrompt",
    "pro.customContainers.manageExisting",
    "pro.feature.maintainableTagCount",
    "pro.feature.containerAppOrderPersistence",
    "settings.proStatus.maxSixTags",
    "settings.proStatus.unlimitedTags",
    "settings.proStatus.previewOnly",
    "settings.proStatus.supported",
]
 
files = sorted(localization_dir.glob("*.json"))
if len(files) != 29:
    fail(f"expected 29 localization files, found {len(files)}")
 
english = json.loads((localization_dir / "en.json").read_text(encoding="utf-8"))
for path in files:
    data = json.loads(path.read_text(encoding="utf-8"))
    missing = [key for key in required_l10n if not data.get(key)]
    if missing:
        fail(f"{path.name} missing keys: {', '.join(missing)}")
    if not data.get("settings.proStatus.unlimitedTags", "").startswith("✅"):
        fail(f"{path.name} unlimited tag status must include the leading checkmark")
    if path.name != "en.json":
        copied = [key for key in required_l10n if data.get(key) == english.get(key)]
        if copied:
            fail(f"{path.name} copies English quota text: {', '.join(copied)}")
 
zh_hans = json.loads((localization_dir / "zh-Hans.json").read_text(encoding="utf-8"))
if zh_hans.get("pro.customContainers.quotaShort") != "免费用户可以使用6个自定义标签(容器):已使用 %used% / %limit%":
    fail("zh-Hans custom container quota line must use the approved free-user wording")
if zh_hans.get("settings.proStatus.unlimitedTags") != "✅没有限制":
    fail("zh-Hans unlimited tag status must include the approved leading checkmark")
 
print("PASS Pro custom container quota QA: quota gate, free tag-order persistence, Pro table rows, and 29-language keys are wired")
PY