Ariver
2026-07-02 01bab5422712299b49f656e6b4c42a3bf314f519
src/Apptag/DataLayer.swift
@@ -12,46 +12,18 @@
    let bundleIdentifier: String?
    let localizedNames: [String]
    let localizedNamesByLanguage: [String: String]
    let systemDisplayNames: [String]
    let bundleDisplayNames: [String]
    let icon: NSImage  // Pre-loaded during background scan
    var isUncommon: Bool = false
    var note: String? = nil
    var displayName: String {
        localizedDisplayName(for: L10n.currentCode)
        AppDisplayNameResolver.displayName(for: self, languageCode: L10n.currentCode)
    }
    func localizedDisplayName(for languageCode: String) -> String {
        let candidates = AppInfo.displayLanguageFallbacks(for: languageCode)
        for code in candidates {
            if let localizedName = localizedNamesByLanguage[code],
               !localizedName.isEmpty {
                return localizedName
            }
        }
        if let localizedName = localizedNames.first, !localizedName.isEmpty {
            return localizedName
        }
        return name
    }
    private static func displayLanguageFallbacks(for languageCode: String) -> [String] {
        var candidates = [languageCode]
        switch languageCode {
        case "zh-Hans":
            candidates.append("zh-Hant")
        case "zh-Hant":
            candidates.append("zh-Hans")
        case "nb":
            candidates.append("no")
        case "nn":
            candidates.append("no")
        case "no":
            candidates.append(contentsOf: ["nb", "nn"])
        default:
            break
        }
        candidates.append("en")
        return uniqueLanguageCodes(candidates)
        AppDisplayNameResolver.displayName(for: self, languageCode: languageCode)
    }
    fileprivate static func uniqueLanguageCodes(_ codes: [String]) -> [String] {
@@ -69,13 +41,247 @@
    static func == (lhs: AppInfo, rhs: AppInfo) -> Bool { lhs.path == rhs.path }
}
enum AppContainerID {
    static let uncategorized = "__container.uncategorized"
    static let appleBuiltIn = "__container.appleBuiltIn"
    static func tag(_ name: String) -> String {
        "tag:\(name)"
    }
    static func system(_ id: SmartCategoryID) -> String {
        "system:\(id.rawValue)"
    }
    static func forTag(_ name: String, definition: TagDatabase.TagDef?) -> String {
        if let categoryID = definition?.systemCategoryID {
            return system(categoryID)
        }
        return tag(name)
    }
    static func forLegacyGroupName(_ name: String) -> String {
        if isUncategorizedDisplayName(name) {
            return uncategorized
        }
        if isAppleBuiltInDisplayName(name) {
            return appleBuiltIn
        }
        return tag(name)
    }
    private static func isUncategorizedDisplayName(_ name: String) -> Bool {
        name == "Other" || name == tr("group.uncategorized")
    }
    private static func isAppleBuiltInDisplayName(_ name: String) -> Bool {
        name == "Mac自带" || name == tr("group.appleBuiltIn")
    }
}
enum AppDisplayNameResolver {
    static func displayName(for app: AppInfo, languageCode: String) -> String {
        let languageFallbacks = displayLanguageFallbacks(for: languageCode, includeEnglish: false)
        if let localizedName = firstLocalizedName(
            for: languageFallbacks,
            in: app.localizedNamesByLanguage
        ) {
            return localizedName
        }
        if languageCode == "en",
           let englishBaseName = firstDisplayCandidate([app.name] + app.bundleDisplayNames) {
            return englishBaseName
        }
        if let systemDisplayName = firstDisplayCandidate(app.systemDisplayNames) {
            return systemDisplayName
        }
        if let englishName = firstLocalizedName(for: ["en"], in: app.localizedNamesByLanguage) {
            return englishName
        }
        if let bundleDisplayName = firstDisplayCandidate(app.bundleDisplayNames) {
            return bundleDisplayName
        }
        let skippedLanguageCodes = Set(languageFallbacks + ["en"])
        let remainingLocalizedNames = L10n.supported.compactMap { language -> String? in
            guard !skippedLanguageCodes.contains(language.code) else { return nil }
            return app.localizedNamesByLanguage[language.code]
        }
        if let localizedName = firstDisplayCandidate(remainingLocalizedNames) {
            return localizedName
        }
        if let alias = firstDisplayCandidate(app.localizedNames) {
            return alias
        }
        return app.name
    }
    static func searchAliases(for app: AppInfo) -> [String] {
        uniqueDisplayNames(
            L10n.supported.compactMap { app.localizedNamesByLanguage[$0.code] }
                + app.systemDisplayNames
                + app.bundleDisplayNames
                + app.localizedNames
                + [app.displayName],
            excluding: app.name
        )
    }
    private static func displayLanguageFallbacks(
        for languageCode: String,
        includeEnglish: Bool
    ) -> [String] {
        var candidates = [
            languageCode,
            languageCode.replacingOccurrences(of: "_", with: "-")
        ]
        switch languageCode {
        case "zh-Hans":
            candidates.append("zh-Hant")
        case "zh-Hant":
            candidates.append("zh-Hans")
        case "pt-BR":
            candidates.append("pt")
        case "sr-Cyrl":
            candidates.append("sr")
        case "ar-Najdi":
            candidates.append("ar")
        case "nb":
            candidates.append(contentsOf: ["no", "nn"])
        case "nn":
            candidates.append(contentsOf: ["no", "nb"])
        case "no":
            candidates.append(contentsOf: ["nb", "nn"])
        default:
            if let base = languageCode.split(separator: "-").first.map(String.init) {
                candidates.append(base)
            }
        }
        if includeEnglish {
            candidates.append("en")
        }
        return AppInfo.uniqueLanguageCodes(candidates)
    }
    private static func firstLocalizedName(
        for languageCodes: [String],
        in localizedNamesByLanguage: [String: String]
    ) -> String? {
        firstDisplayCandidate(languageCodes.compactMap { localizedNamesByLanguage[$0] })
    }
    private static func firstDisplayCandidate(_ values: [String]) -> String? {
        values.lazy.compactMap(normalizedDisplayName).first
    }
    private static func uniqueDisplayNames(_ values: [String], excluding excludedValue: String) -> [String] {
        let normalizedExcluded = normalizedKey(excludedValue)
        var seen = Set<String>()
        var result: [String] = []
        for value in values {
            guard let trimmed = normalizedDisplayName(value) else { continue }
            let normalized = normalizedKey(trimmed)
            guard normalized != normalizedExcluded,
                  seen.insert(normalized).inserted
            else { continue }
            result.append(trimmed)
        }
        return result
    }
    private static func normalizedDisplayName(_ value: String) -> String? {
        let trimmed = value
            .replacingOccurrences(of: ".app", with: "")
            .trimmingCharacters(in: .whitespacesAndNewlines)
        return trimmed.isEmpty ? nil : trimmed
    }
    private static func normalizedKey(_ value: String) -> String {
        value
            .folding(options: [.diacriticInsensitive, .caseInsensitive], locale: .current)
            .lowercased()
            .trimmingCharacters(in: .whitespacesAndNewlines)
    }
}
struct TagGroup: Identifiable {
    var id: String { name }
    var id: String { containerID }
    let containerID: String
    let name: String
    let apps: [AppInfo]
    init(containerID: String, name: String, apps: [AppInfo]) {
        self.containerID = containerID
        self.name = name
        self.apps = apps
    }
    init(name: String, apps: [AppInfo]) {
        self.init(
            containerID: AppContainerID.forLegacyGroupName(name),
            name: name,
            apps: apps
        )
    }
}
// MARK: - Tag Color Mapping
struct TagCustomColor: Codable, Equatable {
    var red: Double
    var green: Double
    var blue: Double
    var alpha: Double
    init(red: Double, green: Double, blue: Double, alpha: Double = 1.0) {
        self.red = Self.clamp(red)
        self.green = Self.clamp(green)
        self.blue = Self.clamp(blue)
        self.alpha = Self.clamp(alpha)
    }
    init(nsColor: NSColor) {
        let rgbColor = nsColor.usingColorSpace(.sRGB) ?? nsColor
        self.init(
            red: Double(rgbColor.redComponent),
            green: Double(rgbColor.greenComponent),
            blue: Double(rgbColor.blueComponent),
            alpha: Double(rgbColor.alphaComponent)
        )
    }
    var nsColor: NSColor {
        NSColor(
            srgbRed: CGFloat(red),
            green: CGFloat(green),
            blue: CGFloat(blue),
            alpha: CGFloat(alpha)
        )
    }
    var signature: String {
        [
            String(format: "%.4f", red),
            String(format: "%.4f", green),
            String(format: "%.4f", blue),
            String(format: "%.4f", alpha)
        ].joined(separator: ":")
    }
    private static func clamp(_ value: Double) -> Double {
        min(1.0, max(0.0, value))
    }
}
enum TagColor {
    static func nsColor(for index: Int) -> NSColor {
@@ -91,6 +297,20 @@
        default: return NSColor.systemGray
        }
    }
    static func nsColor(for index: Int, customColor: TagCustomColor?) -> NSColor {
        customColor?.nsColor ?? nsColor(for: index)
    }
    static func prefersDarkText(for color: NSColor) -> Bool {
        let rgbColor = color.usingColorSpace(.sRGB) ?? color
        let red = Double(rgbColor.redComponent)
        let green = Double(rgbColor.greenComponent)
        let blue = Double(rgbColor.blueComponent)
        let luminance = (0.299 * red) + (0.587 * green) + (0.114 * blue)
        return luminance > 0.62
    }
    static let allIndices: [Int] = [1, 2, 3, 4, 5, 6, 7]
}
@@ -227,7 +447,7 @@
            }
        }
        return deduplicated(apps).sorted {
        return deduplicated(apps.filter { !isInternalHelperAppPath($0.path) }).sorted {
            $0.name.localizedStandardCompare($1.name) == .orderedAscending
        }
    }
@@ -256,10 +476,10 @@
        guard url.pathExtension.lowercased() == "app" else { return }
        let displayURL = url.standardizedFileURL
        guard !isNestedInsideAppBundle(displayURL) else { return }
        guard !isInternalHelperAppPath(displayURL) else { return }
        let resolvedURL = displayURL.resolvingSymlinksInPath().standardizedFileURL
        guard !isNestedInsideAppBundle(resolvedURL) else { return }
        guard !isInternalHelperAppPath(resolvedURL) else { return }
        guard seenResolvedPaths.insert(resolvedURL.path).inserted else { return }
        let bundle = Bundle(url: displayURL) ?? Bundle(url: resolvedURL)
@@ -282,10 +502,13 @@
                }
            }
        }
        let systemDisplayNames = systemDisplayNames(for: displayURL, fallbackName: name)
        let bundleDisplayNames = bundleDisplayNames(bundle: bundle, fallbackName: name)
        let localizedNames = localizedAppNames(
            for: displayURL,
            fallbackName: name,
            localizedNamesByLanguage: localizedNamesByLanguage
            localizedNamesByLanguage: localizedNamesByLanguage,
            systemDisplayNames: systemDisplayNames,
            bundleDisplayNames: bundleDisplayNames
        )
        apps.append(AppInfo(
@@ -295,6 +518,8 @@
            bundleIdentifier: bundleId,
            localizedNames: localizedNames,
            localizedNamesByLanguage: localizedNamesByLanguage,
            systemDisplayNames: systemDisplayNames,
            bundleDisplayNames: bundleDisplayNames,
            icon: icon
        ))
    }
@@ -325,16 +550,39 @@
    }
    private static func localizedAppNames(
        for appURL: URL,
        fallbackName: String,
        localizedNamesByLanguage: [String: String]
        localizedNamesByLanguage: [String: String],
        systemDisplayNames: [String],
        bundleDisplayNames: [String]
    ) -> [String] {
        var values = L10n.supported.map { localizedNamesByLanguage[$0.code] }
        values.append(resourceLocalizedName(for: appURL))
        values.append(spotlightDisplayName(for: appURL))
        values.append(FileManager.default.displayName(atPath: appURL.path).replacingOccurrences(of: ".app", with: ""))
        values.append(contentsOf: systemDisplayNames.map { Optional($0) })
        values.append(contentsOf: bundleDisplayNames.map { Optional($0) })
        return uniqueLocalizedNames(values, excluding: fallbackName)
    }
    private static func systemDisplayNames(for appURL: URL, fallbackName: String) -> [String] {
        uniqueLocalizedNames(
            [
                resourceLocalizedName(for: appURL),
                spotlightDisplayName(for: appURL),
                FileManager.default.displayName(atPath: appURL.path)
            ],
            excluding: fallbackName
        )
    }
    private static func bundleDisplayNames(bundle: Bundle?, fallbackName: String) -> [String] {
        uniqueLocalizedNames(
            [
                bundle?.localizedInfoDictionary?["CFBundleDisplayName"] as? String,
                bundle?.localizedInfoDictionary?["CFBundleName"] as? String,
                bundle?.infoDictionary?["CFBundleDisplayName"] as? String,
                bundle?.infoDictionary?["CFBundleName"] as? String
            ],
            excluding: fallbackName
        )
    }
    private static func localizedAppName(
@@ -488,12 +736,30 @@
            .trimmingCharacters(in: .whitespacesAndNewlines)
    }
    private static func isNestedInsideAppBundle(_ url: URL) -> Bool {
        let components = url.standardizedFileURL.pathComponents
        guard let lastAppIndex = components.lastIndex(where: { $0.lowercased().hasSuffix(".app") }) else {
    static func isNestedInsideAppBundle(_ url: URL) -> Bool {
        isNestedInsideAppBundlePath(url.standardizedFileURL.path)
    }
    static func isInternalHelperAppPath(_ url: URL) -> Bool {
        let path = url.standardizedFileURL.path
        let lowercasedPath = path.lowercased()
        return isNestedInsideAppBundlePath(path)
            || lowercasedPath.contains("/contents/helpers/")
            || lowercasedPath.contains("/contents/xpcservices/")
            || lowercasedPath.contains("/wrapper/")
    }
    private static func isNestedInsideAppBundlePath(_ path: String) -> Bool {
        let lowercasedPath = path.lowercased()
        guard lowercasedPath.hasSuffix(".app") else { return false }
        let components = lowercasedPath.split(separator: "/", omittingEmptySubsequences: true)
        guard let lastAppIndex = components.lastIndex(where: { $0.hasSuffix(".app") }) else {
            return false
        }
        return components[..<lastAppIndex].contains { $0.lowercased().hasSuffix(".app") }
        if components[..<lastAppIndex].contains(where: { $0.hasSuffix(".app") }) {
            return true
        }
        return lowercasedPath.range(of: ".app/", options: .caseInsensitive) != nil
    }
    private static func deduplicated(_ apps: [AppInfo]) -> [AppInfo] {
@@ -552,27 +818,86 @@
        apps: [AppInfo],
        nameOverrides: [String: String] = [:],
        defaultGroupName: String = "Other",
        tagOrder: [String] = []
        tagOrder: [String] = [],
        tagDefinitions: [String: TagDatabase.TagDef]? = nil,
        containerAppOrder: [String: [String]]? = nil
    ) -> [TagGroup] {
        let macCategory = "Mac自带"
        let loadedStore = (tagDefinitions == nil || containerAppOrder == nil) ? TagDatabase.load() : nil
        let effectiveTagDefinitions = tagDefinitions ?? loadedStore?.tags ?? [:]
        var appTagsByPath: [String: [String]] = [:]
        var validAppPaths = Set<String>()
        for app in apps {
            let path = app.path.path
            validAppPaths.insert(path)
            appTagsByPath[path] = uniqueOrdered((appTagsByPath[path] ?? []) + app.tags)
        }
        let effectiveContainerAppOrder = TagDatabase.normalizedContainerAppOrder(
            containerAppOrder ?? loadedStore?.containerAppOrder ?? [:],
            tags: effectiveTagDefinitions,
            appTags: appTagsByPath,
            validAppPaths: validAppPaths
        )
        var dict: [String: [AppInfo]] = [:]
        var containerIDsByGroupName: [String: String] = [:]
        var seenAppIDsByGroup: [String: Set<URL>] = [:]
        for app in apps {
            if app.isAppleApp {
                appendGroupedApp(app, to: macCategory, groups: &dict, seenAppIDsByGroup: &seenAppIDsByGroup)
                appendGroupedApp(
                    app,
                    to: macCategory,
                    containerID: AppContainerID.appleBuiltIn,
                    groups: &dict,
                    containerIDsByGroupName: &containerIDsByGroupName,
                    seenAppIDsByGroup: &seenAppIDsByGroup
                )
            }
            if app.tags.isEmpty {
                if !app.isAppleApp {
                    appendGroupedApp(app, to: defaultGroupName, groups: &dict, seenAppIDsByGroup: &seenAppIDsByGroup)
                    appendGroupedApp(
                        app,
                        to: defaultGroupName,
                        containerID: AppContainerID.uncategorized,
                        groups: &dict,
                        containerIDsByGroupName: &containerIDsByGroupName,
                        seenAppIDsByGroup: &seenAppIDsByGroup
                    )
                }
            } else {
                for tag in uniqueOrdered(app.tags) {
                    let displayName = nameOverrides[tag] ?? tag
                    guard displayName != macCategory else { continue }
                    appendGroupedApp(app, to: displayName, groups: &dict, seenAppIDsByGroup: &seenAppIDsByGroup)
                    appendGroupedApp(
                        app,
                        to: displayName,
                        containerID: AppContainerID.forTag(tag, definition: effectiveTagDefinitions[tag]),
                        groups: &dict,
                        containerIDsByGroupName: &containerIDsByGroupName,
                        seenAppIDsByGroup: &seenAppIDsByGroup
                    )
                }
            }
        }
        for tagName in orderedTagDefinitionNames(
            tagDefinitions: effectiveTagDefinitions,
            tagOrder: tagOrder
        ) {
            let displayName = nameOverrides[tagName] ?? tagName
            guard displayName != macCategory,
                  displayName != defaultGroupName
            else { continue }
            if containerIDsByGroupName[displayName] == nil {
                containerIDsByGroupName[displayName] = AppContainerID.forTag(
                    tagName,
                    definition: effectiveTagDefinitions[tagName]
                )
            }
            if dict[displayName] == nil {
                dict[displayName] = []
            }
        }
@@ -594,16 +919,72 @@
                if li != ri { return li < ri }
                return lhs.key.localizedStandardCompare(rhs.key) == .orderedAscending
            }
            .map { TagGroup(name: $0.key, apps: $0.value) }
            .map { groupName, apps in
                let containerID = containerIDsByGroupName[groupName] ?? AppContainerID.forLegacyGroupName(groupName)
                return TagGroup(
                    containerID: containerID,
                    name: groupName,
                    apps: orderedApps(
                        apps,
                        inContainer: containerID,
                        containerAppOrder: effectiveContainerAppOrder
                    )
                )
            }
    }
    static func orderedApps(
        _ apps: [AppInfo],
        inContainer containerID: String,
        containerAppOrder: [String: [String]]
    ) -> [AppInfo] {
        applyAppOrder(
            apps: apps,
            order: containerAppOrder[containerID] ?? []
        )
    }
    static func applyAppOrder(apps: [AppInfo], order: [String]) -> [AppInfo] {
        let normalizedOrder = TagDatabase.normalizedAppOrderPaths(order)
        guard !apps.isEmpty else { return [] }
        var appsByPath: [String: AppInfo] = [:]
        for app in apps where appsByPath[app.path.path] == nil {
            appsByPath[app.path.path] = app
        }
        let ordered = normalizedOrder.compactMap { appsByPath[$0] }
        let orderedPaths = Set(ordered.map { $0.path.path })
        let remaining = apps
            .filter { !orderedPaths.contains($0.path.path) }
            .sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending }
        return ordered + remaining
    }
    private static func orderedTagDefinitionNames(
        tagDefinitions: [String: TagDatabase.TagDef],
        tagOrder: [String]
    ) -> [String] {
        let ordered = tagOrder.filter { tagDefinitions[$0] != nil }
        let orderedSet = Set(ordered)
        let remaining = tagDefinitions.keys
            .filter { !orderedSet.contains($0) }
            .sorted()
        return ordered + remaining
    }
    private static func appendGroupedApp(
        _ app: AppInfo,
        to groupName: String,
        containerID: String,
        groups: inout [String: [AppInfo]],
        containerIDsByGroupName: inout [String: String],
        seenAppIDsByGroup: inout [String: Set<URL>]
    ) {
        if seenAppIDsByGroup[groupName, default: []].insert(app.id).inserted {
            if containerIDsByGroupName[groupName] == nil {
                containerIDsByGroupName[groupName] = containerID
            }
            groups[groupName, default: []].append(app)
        }
    }
@@ -642,6 +1023,8 @@
    struct TagDef: Codable, Equatable {
        var color: Int
        var systemCategoryID: SmartCategoryID? = nil
        var customColor: TagCustomColor? = nil
        var isCustomMaintained: Bool? = nil
    }
    struct Store: Codable {
@@ -649,6 +1032,7 @@
        var tags: [String: TagDef] = [:]
        var appTags: [String: [String]] = [:]  // path → tag names
        var tagOrder: [String] = []  // display order; empty → alpha sort
        var containerAppOrder: [String: [String]] = [:]  // stable container ID → ordered app paths
        var uncommonAppPaths: [String] = []  // special marker; does not affect normal groups
        var uncommonSources: [String: UncommonSource] = [:]  // current uncommon source: auto/manual
        var appOpenCounts: [String: Int] = [:]  // launches opened from TagLauncher
@@ -665,6 +1049,7 @@
            case tags
            case appTags
            case tagOrder
            case containerAppOrder
            case uncommonAppPaths
            case uncommonSources
            case appOpenCounts
@@ -685,6 +1070,10 @@
            tags = try container.decodeIfPresent([String: TagDef].self, forKey: .tags) ?? [:]
            appTags = try container.decodeIfPresent([String: [String]].self, forKey: .appTags) ?? [:]
            tagOrder = try container.decodeIfPresent([String].self, forKey: .tagOrder) ?? []
            containerAppOrder = try container.decodeIfPresent(
                [String: [String]].self,
                forKey: .containerAppOrder
            ) ?? [:]
            uncommonAppPaths = try container.decodeIfPresent([String].self, forKey: .uncommonAppPaths) ?? []
            uncommonSources = try container.decodeIfPresent([String: UncommonSource].self, forKey: .uncommonSources) ?? [:]
            appOpenCounts = try container.decodeIfPresent([String: Int].self, forKey: .appOpenCounts) ?? [:]
@@ -705,6 +1094,11 @@
            for path in uncommonAppPaths where uncommonSources[path] == nil {
                uncommonSources[path] = .manual
            }
            containerAppOrder = TagDatabase.normalizedContainerAppOrder(
                containerAppOrder,
                tags: tags,
                appTags: appTags
            )
        }
        var hasUserTagAssignments: Bool {
@@ -758,8 +1152,11 @@
    static func load() -> Store {
        guard let data = try? Data(contentsOf: storeURL),
              let store = try? JSONDecoder().decode(Store.self, from: data)
              var store = try? JSONDecoder().decode(Store.self, from: data)
        else { return Store() }
        if normalizeContainerAppOrder(in: &store) {
            save(store)
        }
        return store
    }
@@ -772,10 +1169,190 @@
    }
    static func save(_ store: Store) {
        var normalizedStore = store
        _ = normalizeContainerAppOrder(in: &normalizedStore)
        let encoder = JSONEncoder()
        encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
        guard let data = try? encoder.encode(store) else { return }
        guard let data = try? encoder.encode(normalizedStore) else { return }
        try? data.write(to: storeURL, options: .atomic)
    }
    static func customMaintainedContainerCount(in store: Store? = nil) -> Int {
        customMaintainedContainerNames(in: store).count
    }
    static func customMaintainedContainerNames(in store: Store? = nil) -> [String] {
        let effectiveStore = store ?? load()
        return effectiveStore.tags
            .filter { isCustomMaintainedTag(name: $0.key, definition: $0.value) }
            .map(\.key)
            .sorted()
    }
    static func isCustomMaintainedTag(name: String, definition: TagDef) -> Bool {
        guard let systemCategoryID = definition.systemCategoryID else {
            return true
        }
        if definition.isCustomMaintained == true {
            return true
        }
        if definition.customColor != nil {
            return true
        }
        return definition.color != systemCategoryID.defaultColorIndex
    }
    static func markCustomMaintainedIfNeeded(_ tagName: String, in store: inout Store) {
        guard var definition = store.tags[tagName],
              definition.systemCategoryID != nil,
              !isCustomMaintainedTag(name: tagName, definition: definition)
        else { return }
        definition.isCustomMaintained = true
        store.tags[tagName] = definition
    }
    static func normalizedAppOrderPaths(_ paths: [String]) -> [String] {
        var seen = Set<String>()
        return paths.compactMap { rawPath in
            let path = rawPath.trimmingCharacters(in: .whitespacesAndNewlines)
            guard !path.isEmpty, seen.insert(path).inserted else { return nil }
            return path
        }
    }
    static func normalizedContainerID(_ rawValue: String, tags: [String: TagDef] = [:]) -> String {
        let value = rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !value.isEmpty else { return "" }
        if value == AppContainerID.uncategorized
            || value == AppContainerID.appleBuiltIn
            || value.hasPrefix("tag:")
            || value.hasPrefix("system:") {
            return value
        }
        if value == "Other" || value == tr("group.uncategorized") {
            return AppContainerID.uncategorized
        }
        if value == "Mac自带" || value == tr("group.appleBuiltIn") {
            return AppContainerID.appleBuiltIn
        }
        return AppContainerID.forTag(value, definition: tags[value])
    }
    static func normalizedContainerAppOrder(
        _ order: [String: [String]],
        tags: [String: TagDef] = [:],
        appTags: [String: [String]]? = nil,
        validAppPaths: Set<String>? = nil
    ) -> [String: [String]] {
        var normalized: [String: [String]] = [:]
        for (rawContainerID, rawPaths) in order {
            let containerID = normalizedContainerID(rawContainerID, tags: tags)
            guard !containerID.isEmpty,
                  isContainerOrderAllowed(containerID, tags: tags)
            else { continue }
            var paths = normalizedAppOrderPaths(rawPaths)
            if let validAppPaths {
                paths = paths.filter { validAppPaths.contains($0) }
            }
            if let appTags {
                paths = paths.filter {
                    pathBelongsToContainer(
                        path: $0,
                        containerID: containerID,
                        tags: tags,
                        appTags: appTags
                    )
                }
            }
            guard !paths.isEmpty else { continue }
            if let existing = normalized[containerID] {
                normalized[containerID] = normalizedAppOrderPaths(existing + paths)
            } else {
                normalized[containerID] = paths
            }
        }
        return normalized
    }
    @discardableResult
    static func normalizeContainerAppOrder(
        in store: inout Store,
        validAppPaths: Set<String>? = nil
    ) -> Bool {
        let original = store.containerAppOrder
        store.containerAppOrder = normalizedContainerAppOrder(
            store.containerAppOrder,
            tags: store.tags,
            appTags: store.appTags,
            validAppPaths: validAppPaths
        )
        return original != store.containerAppOrder
    }
    static func migrateContainerAppOrderKey(
        in store: inout Store,
        from oldContainerID: String,
        to newContainerID: String
    ) {
        guard oldContainerID != newContainerID else { return }
        let oldKey = normalizedContainerID(oldContainerID, tags: store.tags)
        let newKey = normalizedContainerID(newContainerID, tags: store.tags)
        guard !oldKey.isEmpty, !newKey.isEmpty,
              let oldOrder = store.containerAppOrder.removeValue(forKey: oldKey)
        else { return }
        let mergedOrder = (store.containerAppOrder[newKey] ?? []) + oldOrder
        store.containerAppOrder[newKey] = normalizedAppOrderPaths(mergedOrder)
    }
    static func removeContainerAppOrder(
        in store: inout Store,
        containerID: String
    ) {
        let key = normalizedContainerID(containerID, tags: store.tags)
        guard !key.isEmpty else { return }
        store.containerAppOrder.removeValue(forKey: key)
    }
    private static func isContainerOrderAllowed(_ containerID: String, tags: [String: TagDef]) -> Bool {
        if containerID == AppContainerID.uncategorized || containerID == AppContainerID.appleBuiltIn {
            return true
        }
        if containerID.hasPrefix("tag:") {
            let tagName = String(containerID.dropFirst("tag:".count))
            return tags[tagName] != nil
        }
        if containerID.hasPrefix("system:") {
            let rawValue = String(containerID.dropFirst("system:".count))
            guard let categoryID = SmartCategoryID(rawValue: rawValue) else { return false }
            return tags.values.contains { $0.systemCategoryID == categoryID }
        }
        return false
    }
    private static func pathBelongsToContainer(
        path: String,
        containerID: String,
        tags: [String: TagDef],
        appTags: [String: [String]]
    ) -> Bool {
        if containerID == AppContainerID.uncategorized || containerID == AppContainerID.appleBuiltIn {
            return true
        }
        if containerID.hasPrefix("tag:") {
            let tagName = String(containerID.dropFirst("tag:".count))
            return appTags[path]?.contains(tagName) == true
        }
        if containerID.hasPrefix("system:") {
            let rawValue = String(containerID.dropFirst("system:".count))
            guard let categoryID = SmartCategoryID(rawValue: rawValue),
                  let tagName = tags.first(where: { $0.value.systemCategoryID == categoryID })?.key
            else { return false }
            return appTags[path]?.contains(tagName) == true
        }
        return false
    }
    static func noteFingerprint(_ value: String) -> String {
@@ -816,6 +1393,7 @@
        let tags: [String: TagDef]
        let appTags: [String: [String]]
        let tagOrder: [String]
        let containerAppOrder: [String: [String]]
        let uncommonAppPaths: [String]
        let uncommonSources: [String: UncommonSource]
        let disabledSystemCategoryIDs: [SmartCategoryID]
@@ -932,6 +1510,11 @@
            tags: store.tags,
            appTags: store.appTags.mapValues(uniqueOrdered),
            tagOrder: uniqueOrdered(store.tagOrder),
            containerAppOrder: normalizedContainerAppOrder(
                store.containerAppOrder,
                tags: store.tags,
                appTags: store.appTags
            ),
            uncommonAppPaths: store.uncommonAppPaths.sorted(),
            uncommonSources: store.uncommonSources,
            disabledSystemCategoryIDs: store.disabledSystemCategoryIDs
@@ -980,6 +1563,7 @@
        for (tagName, tagDef) in store.tags {
            guard let categoryID = tagDef.systemCategoryID else { continue }
            guard !isCustomMaintainedTag(name: tagName, definition: tagDef) else { continue }
            let targetName = categoryID.localizedDisplayName
            guard targetName != tagName else { continue }
            if let existing = store.tags[targetName],
@@ -1007,7 +1591,9 @@
    ) -> (name: String, created: Bool)? {
        if let existingName = systemTagName(for: categoryID, in: store) {
            let targetName = categoryID.localizedDisplayName
            if existingName != targetName {
            let existingDefinition = store.tags[existingName]
            if existingName != targetName,
               existingDefinition.map({ !isCustomMaintainedTag(name: existingName, definition: $0) }) ?? true {
                if let existing = store.tags[targetName],
                   existing.systemCategoryID != categoryID {
                    return (existingName, false)
@@ -1039,6 +1625,8 @@
    private static func renameTagKey(in store: inout Store, from oldName: String, to newName: String) {
        guard oldName != newName, let tagDef = store.tags.removeValue(forKey: oldName) else { return }
        let oldContainerID = AppContainerID.forTag(oldName, definition: tagDef)
        let newContainerID = AppContainerID.forTag(newName, definition: tagDef)
        store.tags[newName] = tagDef
        store.tagOrder = uniqueOrdered(store.tagOrder.map { $0 == oldName ? newName : $0 })
@@ -1050,6 +1638,8 @@
                store.appTags[path] = renamedTags
            }
        }
        migrateContainerAppOrderKey(in: &store, from: oldContainerID, to: newContainerID)
        _ = normalizeContainerAppOrder(in: &store)
    }
    private static func uniqueOrdered(_ values: [String]) -> [String] {
@@ -1060,12 +1650,14 @@
    // MARK: Export / Import
    static func exportTo(_ url: URL) throws {
        try ProEntitlementPolicy.requireUnlocked(.layoutExport)
        let store = loadWithEnsuredCategoryScheme()
        let data = try JSONEncoder().encode(store)
        try data.write(to: url, options: .atomic)
    }
    static func importFrom(_ url: URL) throws -> Store {
        try ProEntitlementPolicy.requireUnlocked(.layoutImport)
        let previousStore = loadWithEnsuredCategoryScheme()
        let data = try Data(contentsOf: url)
        var store = try JSONDecoder().decode(Store.self, from: data)
@@ -1079,6 +1671,20 @@
        flushPendingCategorySchemeBackupBatch()
        save(store)
        return store
    }
    @discardableResult
    static func resetAppTagAssignmentsToUncategorized() -> Store {
        let previousStore = loadWithEnsuredCategoryScheme()
        var store = previousStore
        store.appTags = [:]
        store.containerAppOrder = [:]
        saveUserCategorySchemeMutation(
            store,
            previous: previousStore,
            reason: "reset-uncategorized"
        )
        return loadWithEnsuredCategoryScheme()
    }
    private static func applyImportedCategorySchemeMetadata(to store: inout Store, importedFileURL url: URL) {
@@ -1248,12 +1854,11 @@
        guard !FileManager.default.fileExists(atPath: storeURL.path) else { return }
        let starterCategoryIDs: [SmartCategoryID] = [
            .uiPrototyping,
            .ide,
            .writing,
            .game,
            .design,
            .development,
            .office,
            .entertainment,
            .system,
            .systemEnhancement,
            .gtd
        ]
@@ -1265,9 +1870,55 @@
    }
}
enum TagEditorMutationResult {
    case saved
    case noChange
    case blockedCustomContainerQuota(ProCustomContainerQuotaStatus)
    var didSave: Bool {
        if case .saved = self {
            return true
        }
        return false
    }
}
// MARK: - Tag Editor (CRUD against local TagDatabase)
enum TagEditor {
    private static func customContainerCandidates(
        _ tagNames: Set<String>,
        in store: TagDatabase.Store
    ) -> Set<String> {
        Set(tagNames.filter { tagName in
            guard let definition = store.tags[tagName] else {
                return true
            }
            return !TagDatabase.isCustomMaintainedTag(name: tagName, definition: definition)
        })
    }
    private static func customContainerMaintenanceResult(
        for tagNames: Set<String>,
        in store: TagDatabase.Store
    ) -> TagEditorMutationResult? {
        let addedCount = customContainerCandidates(tagNames, in: store).count
        switch ProEntitlementPolicy.customContainerMaintenanceDecision(
            addingCustomContainerCount: addedCount,
            in: store
        ) {
        case .allow:
            return nil
        case .blocked(let status):
            return .blockedCustomContainerQuota(status)
        }
    }
    private static func markCustomMaintained(_ tagNames: Set<String>, in store: inout TagDatabase.Store) {
        for tagName in tagNames {
            TagDatabase.markCustomMaintainedIfNeeded(tagName, in: &store)
        }
    }
    // MARK: Read
@@ -1280,13 +1931,18 @@
    /// Tag names in display order. Falls back to alpha sort if no custom order.
    static func orderedTagNames() -> [String] {
        let store = TagDatabase.load()
        return orderedTagNames(in: store)
    }
    static func orderedTagNames(in store: TagDatabase.Store) -> [String] {
        let ordered = store.tagOrder.filter { store.tags[$0] != nil }
        let remaining = store.tags.keys.filter { !ordered.contains($0) }.sorted()
        return ordered + remaining
    }
    /// Persist a new tag display order.
    static func reorderTags(_ names: [String]) {
    @discardableResult
    static func reorderTags(_ names: [String]) -> Bool {
        var store = TagDatabase.load()
        let previousStore = store
        store.tagOrder = names
@@ -1295,12 +1951,44 @@
            previous: previousStore,
            reason: "reorder-tags"
        )
        return true
    }
    /// Persist the visible app order for one stable container.
    static func reorderApps(inContainer containerID: String, orderedPaths: [String]) {
        guard ProEntitlementPolicy.isUnlocked(.persistentAppSorting) else { return }
        var store = TagDatabase.load()
        _ = TagDatabase.normalizeContainerAppOrder(in: &store)
        let previousStore = store
        let key = TagDatabase.normalizedContainerID(containerID, tags: store.tags)
        guard !key.isEmpty else { return }
        let normalizedPaths = TagDatabase.normalizedAppOrderPaths(orderedPaths)
        if normalizedPaths.isEmpty {
            store.containerAppOrder.removeValue(forKey: key)
        } else {
            store.containerAppOrder[key] = normalizedPaths
        }
        _ = TagDatabase.normalizeContainerAppOrder(in: &store)
        guard store.containerAppOrder != previousStore.containerAppOrder else { return }
        TagDatabase.saveUserCategorySchemeMutation(
            store,
            previous: previousStore,
            reason: "reorder-apps"
        )
    }
    /// Create a new tag definition (no app assignments yet).
    static func createTag(_ name: String, color: Int) {
    @discardableResult
    static func createTag(_ name: String, color: Int) -> TagEditorMutationResult {
        var store = TagDatabase.load()
        guard store.tags[name] == nil else { return }
        guard store.tags[name] == nil else { return .noChange }
        if let blocked = customContainerMaintenanceResult(for: [name], in: store) {
            return blocked
        }
        let previousStore = store
        store.tags[name] = TagDatabase.TagDef(color: color, systemCategoryID: nil)
        if !store.tagOrder.contains(name) { store.tagOrder.insert(name, at: 0) }
@@ -1309,6 +1997,7 @@
            previous: previousStore,
            reason: "create-tag"
        )
        return .saved
    }
    /// Annotate scanned apps with tags from the database.
@@ -1325,6 +2014,8 @@
                bundleIdentifier: app.bundleIdentifier,
                localizedNames: app.localizedNames,
                localizedNamesByLanguage: app.localizedNamesByLanguage,
                systemDisplayNames: app.systemDisplayNames,
                bundleDisplayNames: app.bundleDisplayNames,
                icon: app.icon,
                isUncommon: uncommonPaths.contains(app.path.path),
                note: store.appNotes[app.path.path]
@@ -1335,8 +2026,12 @@
    // MARK: Write
    /// Assign a tag to multiple apps. Preserves existing tags.
    static func assignTag(_ tag: String, color: Int, to paths: [String]) {
    @discardableResult
    static func assignTag(_ tag: String, color: Int, to paths: [String]) -> TagEditorMutationResult {
        var store = TagDatabase.load()
        if let blocked = customContainerMaintenanceResult(for: [tag], in: store) {
            return blocked
        }
        let previousStore = store
        // Ensure tag definition exists
        if store.tags[tag] == nil {
@@ -1344,6 +2039,7 @@
            // New tag: append to display order
            if !store.tagOrder.contains(tag) { store.tagOrder.insert(tag, at: 0) }
        }
        markCustomMaintained([tag], in: &store)
        for path in paths {
            var current = store.appTags[path] ?? []
            if !current.contains(tag) {
@@ -1356,14 +2052,27 @@
            previous: previousStore,
            reason: "assign-tag"
        )
        return .saved
    }
    /// Replace the full editable tag set for multiple apps.
    static func setTags(_ tags: [String], to paths: [String]) {
    @discardableResult
    static func setTags(_ tags: [String], to paths: [String]) -> TagEditorMutationResult {
        var store = TagDatabase.load()
        let previousStore = store
        let selectedUncommon = tags.contains(TagDatabase.uncommonTagKey)
        let validTags = tags.filter { store.tags[$0] != nil }
        let nextValidTags = Set(validTags)
        var touchedTags = Set<String>()
        for path in paths {
            let currentTags = Set(store.appTags[path] ?? [])
            touchedTags.formUnion(currentTags.symmetricDifference(nextValidTags))
        }
        touchedTags = touchedTags.filter { store.tags[$0] != nil }
        if let blocked = customContainerMaintenanceResult(for: touchedTags, in: store) {
            return blocked
        }
        let previousStore = store
        markCustomMaintained(touchedTags, in: &store)
        var uncommonPaths = Set(store.uncommonAppPaths)
        for path in paths {
            if validTags.isEmpty {
@@ -1385,16 +2094,29 @@
            previous: previousStore,
            reason: "set-tags"
        )
        return .saved
    }
    /// Append tags to multiple apps without disturbing their existing tag sets.
    static func appendTags(_ tags: [String], to paths: [String]) {
        guard !tags.isEmpty, !paths.isEmpty else { return }
    @discardableResult
    static func appendTags(_ tags: [String], to paths: [String]) -> TagEditorMutationResult {
        guard !tags.isEmpty, !paths.isEmpty else { return .noChange }
        var store = TagDatabase.load()
        let previousStore = store
        let selectedUncommon = tags.contains(TagDatabase.uncommonTagKey)
        let validTags = tags.filter { store.tags[$0] != nil }
        var touchedTags = Set<String>()
        for path in paths {
            let current = Set(store.appTags[path] ?? [])
            for tag in validTags where !current.contains(tag) {
                touchedTags.insert(tag)
            }
        }
        if let blocked = customContainerMaintenanceResult(for: touchedTags, in: store) {
            return blocked
        }
        let previousStore = store
        markCustomMaintained(touchedTags, in: &store)
        var uncommonPaths = Set(store.uncommonAppPaths)
        for path in paths {
@@ -1418,16 +2140,27 @@
            previous: previousStore,
            reason: "append-tags"
        )
        return .saved
    }
    /// Remove tags from multiple apps while preserving every unrelated tag.
    static func removeTags(_ tags: [String], from paths: [String]) {
        guard !tags.isEmpty, !paths.isEmpty else { return }
    @discardableResult
    static func removeTags(_ tags: [String], from paths: [String]) -> TagEditorMutationResult {
        guard !tags.isEmpty, !paths.isEmpty else { return .noChange }
        var store = TagDatabase.load()
        let previousStore = store
        let selectedUncommon = tags.contains(TagDatabase.uncommonTagKey)
        let validTags = Set(tags.filter { store.tags[$0] != nil })
        var touchedTags = Set<String>()
        for path in paths {
            let current = Set(store.appTags[path] ?? [])
            touchedTags.formUnion(validTags.intersection(current))
        }
        if let blocked = customContainerMaintenanceResult(for: touchedTags, in: store) {
            return blocked
        }
        let previousStore = store
        markCustomMaintained(touchedTags, in: &store)
        var uncommonPaths = Set(store.uncommonAppPaths)
        for path in paths {
@@ -1452,6 +2185,7 @@
            previous: previousStore,
            reason: "remove-tags"
        )
        return .saved
    }
    static func reconcileScannedApps(_ apps: [AppInfo]) -> TagDatabase.Store {
@@ -1462,8 +2196,12 @@
        if store.knownAppPaths.isEmpty {
            let seededDefaultNotes = seedDefaultAppleAppNotes(for: apps, in: &store)
            let markedUncommon = markUnfamiliarAppleAppsAsUncommon(apps, in: &store)
            let normalizedOrder = TagDatabase.normalizeContainerAppOrder(
                in: &store,
                validAppPaths: scannedPaths
            )
            store.knownAppPaths = scannedPaths.sorted()
            if apps.isEmpty == false || seededDefaultNotes || markedUncommon {
            if apps.isEmpty == false || seededDefaultNotes || markedUncommon || normalizedOrder {
                TagDatabase.save(store)
            }
            return store
@@ -1478,9 +2216,13 @@
            }
            store.knownAppPaths = knownPaths.subtracting(removedPaths).sorted()
        }
        let normalizedOrder = TagDatabase.normalizeContainerAppOrder(
            in: &store,
            validAppPaths: scannedPaths
        )
        guard !newPaths.isEmpty else {
            if !removedPaths.isEmpty {
            if !removedPaths.isEmpty || normalizedOrder {
                TagDatabase.save(store)
            }
            return store
@@ -1505,6 +2247,10 @@
        _ = seedDefaultAppleAppNotes(for: newApps, in: &store)
        store.uncommonAppPaths = uncommonPaths.sorted()
        store.knownAppPaths = Set(store.knownAppPaths).union(newPaths).sorted()
        _ = TagDatabase.normalizeContainerAppOrder(
            in: &store,
            validAppPaths: scannedPaths
        )
        TagDatabase.save(store)
        return store
    }
@@ -1589,11 +2335,22 @@
        TagDatabase.save(store)
    }
    static func setAppNote(_ note: String, for path: String) {
    @discardableResult
    static func setAppNote(_ note: String, for path: String) -> ProNoteSaveDecision {
        var store = TagDatabase.load()
        let previousStore = store
        let trimmed = note.trimmingCharacters(in: .whitespacesAndNewlines)
        let limited = String(trimmed.prefix(TagDatabase.maxAppNoteLength))
        let decision = ProEntitlementPolicy.noteSaveDecision(
            note: limited,
            for: path,
            in: store
        )
        guard case .allow = decision else {
            return decision
        }
        let previousStore = store
        if limited.isEmpty {
            store.appNotes.removeValue(forKey: path)
            store.appNoteMetadata[path] = TagDatabase.AppNoteMetadata(
@@ -1619,15 +2376,33 @@
            previous: previousStore,
            reason: "edit-app-note"
        )
        return decision
    }
    static func moveApp(path: String, from sourceTag: String, to targetTag: String, color: Int, copy: Bool) {
    @discardableResult
    static func moveApp(path: String, from sourceTag: String, to targetTag: String, color: Int, copy: Bool) -> TagEditorMutationResult {
        var store = TagDatabase.load()
        var touchedTags = Set<String>()
        let currentTags = Set(store.appTags[path] ?? [])
        if store.tags[targetTag] == nil || !currentTags.contains(targetTag) {
            touchedTags.insert(targetTag)
        }
        if !copy,
           !sourceTag.isEmpty,
           sourceTag != "Mac自带",
           sourceTag != tr("group.appleBuiltIn"),
           currentTags.contains(sourceTag) {
            touchedTags.insert(sourceTag)
        }
        if let blocked = customContainerMaintenanceResult(for: touchedTags, in: store) {
            return blocked
        }
        let previousStore = store
        if store.tags[targetTag] == nil {
            store.tags[targetTag] = TagDatabase.TagDef(color: color, systemCategoryID: nil)
            if !store.tagOrder.contains(targetTag) { store.tagOrder.insert(targetTag, at: 0) }
        }
        markCustomMaintained(touchedTags, in: &store)
        var current = store.appTags[path] ?? []
        if !copy, !sourceTag.isEmpty, sourceTag != "Mac自带", sourceTag != tr("group.appleBuiltIn") {
@@ -1647,16 +2422,37 @@
            previous: previousStore,
            reason: "move-app"
        )
        return .saved
    }
    /// Rename a tag on all apps that have it.
    static func renameTag(from oldName: String, to newName: String) {
    @discardableResult
    static func renameTag(from oldName: String, to newName: String) -> TagEditorMutationResult {
        var store = TagDatabase.load()
        guard oldName != newName, store.tags[newName] == nil else { return }
        guard oldName != newName, store.tags[newName] == nil else { return .noChange }
        guard let oldDefinition = store.tags[oldName] else { return .noChange }
        let needsCustomContainerSlot = !TagDatabase.isCustomMaintainedTag(
            name: oldName,
            definition: oldDefinition
        )
        if needsCustomContainerSlot,
           let blocked = customContainerMaintenanceResult(for: [oldName], in: store) {
            return blocked
        }
        let previousStore = store
        // Update tag definition
        if let def = store.tags.removeValue(forKey: oldName) {
        if var def = store.tags.removeValue(forKey: oldName) {
            let oldContainerID = AppContainerID.forTag(oldName, definition: def)
            let newContainerID = AppContainerID.forTag(newName, definition: def)
            if def.systemCategoryID != nil {
                def.isCustomMaintained = true
            }
            store.tags[newName] = def
            TagDatabase.migrateContainerAppOrderKey(
                in: &store,
                from: oldContainerID,
                to: newContainerID
            )
        }
        store.tagOrder = store.tagOrder.map { $0 == oldName ? newName : $0 }
        // Update all app assignments
@@ -1666,21 +2462,34 @@
                store.appTags[path] = Array(NSOrderedSet(array: tags).compactMap { $0 as? String })
            }
        }
        _ = TagDatabase.normalizeContainerAppOrder(in: &store)
        TagDatabase.saveUserCategorySchemeMutation(
            store,
            previous: previousStore,
            reason: "rename-tag"
        )
        return .saved
    }
    /// Delete a tag completely (from all apps and tag definitions).
    static func deleteTagCompletely(_ tag: String) {
        var store = TagDatabase.load()
        let previousStore = store
        if let removedTag = store.tags.removeValue(forKey: tag),
           let categoryID = removedTag.systemCategoryID,
        let removedTag = store.tags.removeValue(forKey: tag)
        if let categoryID = removedTag?.systemCategoryID,
           !store.disabledSystemCategoryIDs.contains(categoryID) {
            store.disabledSystemCategoryIDs.append(categoryID)
        }
        if let removedTag {
            TagDatabase.removeContainerAppOrder(
                in: &store,
                containerID: AppContainerID.forTag(tag, definition: removedTag)
            )
        } else {
            TagDatabase.removeContainerAppOrder(
                in: &store,
                containerID: AppContainerID.tag(tag)
            )
        }
        store.tagOrder.removeAll { $0 == tag }
        for (path, var tags) in store.appTags {
@@ -1691,6 +2500,7 @@
                store.appTags[path] = tags
            }
        }
        _ = TagDatabase.normalizeContainerAppOrder(in: &store)
        TagDatabase.saveUserCategorySchemeMutation(
            store,
            previous: previousStore,
@@ -1699,16 +2509,47 @@
    }
    /// Set the color for a tag.
    static func setColor(_ color: Int, for tag: String) {
    @discardableResult
    static func setColor(_ color: Int, for tag: String) -> TagEditorMutationResult {
        var store = TagDatabase.load()
        let previousStore = store
        var tagDef = store.tags[tag] ?? TagDatabase.TagDef(color: color, systemCategoryID: nil)
        let existingDefinition = store.tags[tag]
        var tagDef = existingDefinition ?? TagDatabase.TagDef(color: color, systemCategoryID: nil)
        let wasCustomMaintained = existingDefinition.map {
            TagDatabase.isCustomMaintainedTag(name: tag, definition: $0)
        } ?? false
        tagDef.color = color
        tagDef.customColor = nil
        let willBeCustomMaintained = TagDatabase.isCustomMaintainedTag(name: tag, definition: tagDef)
        if !wasCustomMaintained, willBeCustomMaintained,
           let blocked = customContainerMaintenanceResult(for: [tag], in: store) {
            return blocked
        }
        let previousStore = store
        store.tags[tag] = tagDef
        TagDatabase.saveUserCategorySchemeMutation(
            store,
            previous: previousStore,
            reason: "set-tag-color"
        )
        return .saved
    }
    /// Set a Pro custom color for a tag without changing its fallback base color.
    @discardableResult
    static func setCustomColor(_ customColor: TagCustomColor, for tag: String) -> Bool {
        guard ProEntitlementPolicy.isUnlocked(.customTagColors) else {
            return false
        }
        var store = TagDatabase.load()
        let previousStore = store
        var tagDef = store.tags[tag] ?? TagDatabase.TagDef(color: 0, systemCategoryID: nil)
        tagDef.customColor = customColor
        store.tags[tag] = tagDef
        TagDatabase.saveUserCategorySchemeMutation(
            store,
            previous: previousStore,
            reason: "set-tag-custom-color"
        )
        return true
    }
}