Ariver
2026-06-19 39e759d015af5d82f387b57a8d137bdeb844d430
Fix historical Apple default note migration
8 files modified
1 files added
375 ■■■■■ changed files
Docs/KM02-技术/07-构建发布与QA.md 138 ●●●●● patch | view | raw | blame | history
src/Apptag/AppleDefaultAppCatalog.swift 53 ●●●●● patch | view | raw | blame | history
src/Apptag/Info.plist 4 ●●●● patch | view | raw | blame | history
src/CHANGELOG.md 8 ●●●●● patch | view | raw | blame | history
src/Docs/Requirements/2026-06-19-apple-default-note-localization-qa.md 19 ●●●● patch | view | raw | blame | history
src/Docs/Requirements/2026-06-19-apple-default-note-localization-worklog.md 39 ●●●●● patch | view | raw | blame | history
src/Scripts/apple_default_apps_resource_qa.sh 66 ●●●● patch | view | raw | blame | history
src/Scripts/apple_default_note_migration_qa.sh 46 ●●●●● patch | view | raw | blame | history
src/Scripts/apple_default_note_policy_qa.sh 2 ●●●●● patch | view | raw | blame | history
Docs/KM02-技术/07-构建发布与QA.md
New file
@@ -0,0 +1,138 @@
# 构建发布与 QA
- 整理日期:2026-06-11
- 复核日期:2026-06-15
- 依据产品版本:TagLauncher 7.9.1,Build 20260614.1454
- 版本依据:`src/Apptag/Info.plist`、`src/CHANGELOG.md`、`src/Release/AppStore-7.9.1-20260614.1454/QA_RELEASE_EVIDENCE.md`
- 配套图示:[07-构建发布与QA.drawio](./07-构建发布与QA.drawio)
## 构建方式
项目没有 Xcode 工程构建入口,`src/build.sh` 是主构建脚本。它执行:
1. 读取 `src/Apptag/Info.plist` 的 `CFBundleShortVersionString`。
2. 使用 `APP_BUILD` 环境变量或当前时间生成 build 号,格式必须是 `YYYYMMDD.HHMM`。
3. 清理并重建 `src/build/TagLauncher.app` bundle 结构。
4. 调用 `swiftc` 编译全部 Swift 文件:
   - frameworks:AppKit、SwiftUI、Carbon、CoreServices
   - library:compression
   - target:`arm64-apple-macosx15.0`
   - optimization:`-Osize`
5. 拷贝 Info.plist,并只修改 app bundle 内的 `CFBundleVersion`。
6. 从 asset catalog 生成 `AppIcon.icns`。
7. 拷贝菜单栏 SVG、本地化 JSON、Apple 默认应用资源、Smart Start 资源。
8. 将多语言 catalog 压缩为 `.json.deflate`。
9. 拦截禁止进入 bundle 的开发/源资源。
10. 写入 `PkgInfo`、设置 executable、strip debug symbols、清 xattrs。
11. 按 `APP_STORE` 和 `CODESIGN_IDENTITY` 决定签名方式。
12. 校验源码 `Info.plist` hash 未被构建过程修改。
## 资源打包边界
运行时资源来自源码树下的 Research,但进入 app bundle 后名称有调整:
| 源资源 | Bundle 资源 |
| --- | --- |
| `Research/AppleDefaultApps/AppleDefaultApps.base.json` | `AppleDefaultApps.base.json` |
| `Research/AppleDefaultApps/AppleDefaultApps.manifest.json` | `AppleDefaultApps.manifest.json` |
| `Research/AppleDefaultApps/AppleDefaultApps.localizations.<lang>.json` | `AppleDefaultApps.localizations.<lang>.json.deflate` |
| `Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.base.json` | `SmartStartUltimateDefaultCatalog.base.json` |
| `Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.manifest.json` | `SmartStartUltimateDefaultCatalog.manifest.json` |
| `Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.<lang>.json` | `SmartStartUltimateDefaultCatalog.notes.<lang>.json.deflate` |
| `Apptag/Localization/*.json` | `Localization/*.json` |
`build.sh` 禁止把以下源/临时资源直接放入 bundle:
- `*TranslationCache*`
- `*invalid*`
- 未压缩的 `AppleDefaultApps.localizations.*.json`
- 旧单体 `SmartStartUltimateDefaultCatalog.json`
- SmartStart CSV
- 未压缩的 notes JSON
## DMG 构建
`src/make_dmg.sh` 依赖已存在的 `src/build/TagLauncher.app`:
1. 创建 `build/dmg_staging`。
2. 复制 `TagLauncher.app`。
3. 创建 `/Applications` symlink。
4. 用 `hdiutil create -format UDZO -imagekey zlib-level=9` 生成 `build/TagLauncher.dmg`。
5. 清理 staging。
发布 archive 中的 DMG 会按版本/build 重命名,例如:
`TagLauncher-7.9.1-build20260614.1454.dmg`
## App Store / Sandbox
`APP_STORE=1` 时:
- 要求 `src/Apptag/TagLauncher.entitlements` 存在。
- 如果未设置 `CODESIGN_IDENTITY`,脚本会 ad-hoc sign with entitlements,并打印警告。
- 设置 `CODESIGN_IDENTITY` 后执行带 entitlements 的 codesign。
7.9.1 QA 证据中记录:App Store `.pkg` 已生成,Installer certificate signature、Bundle ID、版本/build、sandbox entitlement、user-selected file read/write entitlement、Keychain access group、arm64 架构和 payload AppleDouble entries 均通过检查。
## 当前 release 证据
`src/Release/AppStore-7.9.1-20260614.1454/QA_RELEASE_EVIDENCE.md` 记录:
- Version:7.9.1
- Build:20260614.1454
- Bundle ID:com.taglauncher.app
- Source commit:`bd792cd36d57b8f0741af9a6a64e59969cdce490`
自动 QA 通过项:
- `Scripts/usage_tips_qa.sh`
- `Scripts/app_ordering_data_qa.sh`
- `Scripts/macos14_availability_typecheck_qa.sh`
- `Scripts/macos14_build_metadata_qa.sh`
- `Scripts/quick_search_system_app_qa.sh`
- `Scripts/smartstart_catalog_resource_qa.sh`
- `Scripts/quick_search_app_name_qa.sh`:本机缺少 Sunlogin wrapper fixture,记录为 SKIP
- `hdiutil verify`、App Store `.pkg` 签名和 payload 检查
发布产物:
- Archive DMG:`Archive/TagLauncher-7.9.1-build20260614.1454.dmg`
- Upload PKG:`Upload/TagLauncher-7.9.1-build20260614.1454.pkg`
- DMG SHA256:`2cb29528ef1c137db330ff8a28baa3c622705916f8368b28c373ca95aefd6981`
- PKG SHA256:`7ad097a23c736fc7a7b34a9575348cbc52a968d5fbf433584ace353b120a32ef`
## QA 脚本分层
| 脚本 | 关注点 |
| --- | --- |
| `Scripts/window_logic_qa.sh` | Overlay/Quick Search/Settings/文件面板/Dock/全屏/Split View/重复实例 |
| `Scripts/quick_search_app_name_qa.sh` | 多语言显示名、wrapper/helper 过滤、Quick Search 文档过滤 |
| `Scripts/quick_search_system_app_qa.sh` | 系统 App 扫描路径、Keychain Access、中文/拼音搜索候选 |
| `Scripts/smartstart_catalog_resource_qa.sh` | Smart Start 资源完整性、语言覆盖、Apple 边界、runtime loader |
| `Scripts/apple_default_apps_resource_qa.sh` | Apple catalog 资源质量、系统扫描覆盖、多语言备注质量、build 边界 |
| `Scripts/apple_default_note_policy_qa.sh` | Apple/SmartStart/manual 备注来源隔离和语言切换重本地化 |
| `Scripts/apple_default_note_migration_qa.sh` | 旧版 Apple 初始化备注迁移、历史默认备注 fixture、手写备注保护 |
| `Scripts/app_ordering_data_qa.sh` | `containerAppOrder` schema、导入导出、备份恢复、SmartStart 清理、拖拽 payload 静态门禁 |
| `Scripts/usage_tips_qa.sh` | usage tips 多语言文案、分隔符、两行渲染约束 |
| `Scripts/macos14_availability_typecheck_qa.sh` | macOS 14 可用性和 Swift 类型检查 |
| `Scripts/macos14_build_metadata_qa.sh` | macOS 14 最低版本、Info.plist、build metadata 边界 |
## 发布前建议门禁
发布或修改以下区域时,应按风险扩大 QA:
- 窗口、快捷键、Dock、activation policy、Quick Search panel:跑 `window_logic_qa.sh`,并人工 smoke `Fn+Space` 连续开关、立即输入、Esc、点击结果。
- 名称解析、AppIndexer、Quick Search 文档:跑两个 Quick Search QA。
- Smart Start 或 Apple catalog:跑三个资源/备注 QA。
- Apple 默认应用备注属于初始化内容,任一语种都不允许混入其他语种正文。修改 Apple catalog 或默认备注迁移时,`apple_default_apps_resource_qa.sh` 必须同时检查 translations 和运行用 localizations 的跨语种脚本污染;`apple_default_note_migration_qa.sh` 必须覆盖已知历史默认备注,不能只测单个样本。
- App Grid 容器排序:跑 `app_ordering_data_qa.sh`,并人工验证同容器拖拽排序、跨容器移动、Option 复制和拖空白移除互不串扰。
- 使用技巧悬浮条或 usage tips 文案:跑 `usage_tips_qa.sh`,并人工验证翻页按钮、hover 期间备注气泡抑制、Quick Search only 会话不显示悬浮条。
- 构建脚本或资源路径:跑资源 QA、`APP_BUILD=<目标build> bash build.sh`,必要时 `make_dmg.sh` 和 `hdiutil verify`。
- 发布包:记录 version/build、commit、命令、结果、产物路径和 SHA256。
## 修改风险提示
- 不要让构建脚本修改 tracked source `Info.plist`;当前 hash 校验是保护边界。
- 不要把未压缩多语言 catalog 放入 bundle;运行时 loader 优先 `.json.deflate`,QA 也以此为边界。
- 不要把 build 目录普通产物混入源码提交,除非明确作为 release archive。
- 不要只跑构建不跑专项 QA;TagLauncher 的高风险主要在窗口时序、搜索名称和资源质量,不是单纯编译。
src/Apptag/AppleDefaultAppCatalog.swift
@@ -61,6 +61,58 @@
    private static let cacheLock = NSLock()
    private static var cachedBaseSnapshot: BaseSnapshot?
    private static var cachedLocalizationSnapshots: [String: LocalizationSnapshot] = [:]
    private static let legacyDefaultNoteFingerprintsByBundleIdentifier: [String: Set<String>] = [
        "com.apple.chess": [
            "9f72f1448c7e9b47",
            "a3b754a4f06eee33",
            "abc8528f6a2ca356"
        ],
        "com.apple.freeform": [
            "32fee0886cf2c6bb",
            "503c25567e9640c2",
            "aee5e9a7730252f7"
        ],
        "com.apple.home": [
            "003e996159a187cc",
            "2707ce5206adfb2a",
            "5b0207586af532a9"
        ],
        "com.apple.music": [
            "545fa7c8a0d216fa",
            "8867798c868c8c30",
            "bce11863b08fac8b"
        ],
        "com.apple.passwords": [
            "58b38191593ec9b9",
            "a72f91e414c90254",
            "fa1ce5e1bb5998cd"
        ],
        "com.apple.mobilephone": [
            "703bd99a660e80e4",
            "80d0df54c7ddf2d1",
            "9d57741d3114a1a2"
        ],
        "com.apple.tv": [
            "6a29d8ece97cd432",
            "e1efb690bf1c6b94",
            "e28785e11f914401"
        ],
        "com.apple.backup.launcher": [
            "78edffa39f28cee2",
            "cbf81a2b5e77725b",
            "dc568bb30164e897"
        ],
        "com.apple.diskutility": [
            "77324bc5de72e176",
            "c43f8e117573f5ed",
            "fd581938fd34e888"
        ],
        "com.apple.screencontinuity": [
            "171a629bd17970fa",
            "2965e73ff5566d19",
            "bb4662c4f15ab83c"
        ]
    ]
    static func isKnownAppleApp(_ app: AppInfo) -> Bool {
        entry(for: app) != nil
@@ -238,6 +290,7 @@
                result.insert(TagDatabase.noteFingerprint(variant))
            }
        }
        result.formUnion(legacyDefaultNoteFingerprintsByBundleIdentifier[bundleIdentifier] ?? [])
        return result
    }
src/Apptag/Info.plist
@@ -19,9 +19,9 @@
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>7.9.3</string>
    <string>7.9.4</string>
    <key>CFBundleVersion</key>
    <string>20260619.2142</string>
    <string>20260619.2221</string>
    <key>LSApplicationCategoryType</key>
    <string>public.app-category.utilities</string>
    <key>LSMinimumSystemVersion</key>
src/CHANGELOG.md
@@ -1,5 +1,13 @@
# TagLauncher Changelog
## [7.9.4] — 2026-06-19
- 补修 Apple 自带应用默认备注迁移:覆盖 7.9 之前已写入用户库、但没有 metadata 的历史默认文案,修复 Chess、TV、Phone、Freeform、Music、Time Machine 等应用切换到非中文语言后仍显示旧中文备注的问题
- Apple 默认备注迁移改为历史 fingerprint 白名单,不在运行时代码里维护旧中文展示文案,也不放宽为“看到中文就覆盖”,继续保护用户手写备注
- 加强 Apple 默认应用资源 QA:29 个语种的初始化备注禁止出现其他语种脚本污染,translations 和运行用 localizations 双重检查
- 加强迁移 QA:用真实暴露的旧中文默认备注作为 fixture,逐个验证迁移到 29 个语种后不会保留旧中文
- 版本号更新为 `7.9.4`,Build 更新为 `20260619.2221`
## [7.9.3] — 2026-06-19
- 修复 Apple 自带应用默认备注在切换语言后仍显示旧中文的问题;旧版本写入但缺少来源 metadata 的默认备注现在会按 bundle 精确匹配默认备注 fingerprint 后迁移为当前语言
src/Docs/Requirements/2026-06-19-apple-default-note-localization-qa.md
@@ -17,18 +17,31 @@
| SmartStart 资源边界 | `bash Scripts/smartstart_catalog_resource_qa.sh` | PASS |
| 使用技巧回归 | `bash Scripts/usage_tips_qa.sh` | PASS |
| macOS 14 typecheck | `bash Scripts/macos14_availability_typecheck_qa.sh` | PASS |
| 构建 | `APP_BUILD=20260619.2142 bash build.sh` | PASS |
| 真实用户库只读模拟 | Python 读取当前 `tags.json`,模拟 `ms/ja/ar/en` 迁移 | PASS |
| 构建 | `APP_BUILD=20260619.2221 bash build.sh` | PASS |
| 构建 metadata | `bash Scripts/macos14_build_metadata_qa.sh` | PASS |
| 签名验证 | `codesign --verify --deep --strict --verbose=2 build/TagLauncher.app` | PASS |
| DMG | `bash make_dmg.sh` + `hdiutil verify build/TagLauncher.dmg` | PASS |
## 7.9.3 漏测复盘
- 漏测现象:Chess、TV、Phone、Freeform、Music、Time Machine 等旧版本初始化备注在切换到非中文语言后仍显示中文。
- 漏测原因:上一版迁移 QA 只覆盖了 ColorSync 的旧中文默认备注和句末标点变体,没有覆盖 7.9 之前另一批已经写入用户库的历史默认文案。
- 修正后的 QA 原则:Apple 自带应用备注属于初始化内容,任何语种资源中都不允许出现其他语种正文;真实迁移 fixture 必须覆盖已知历史默认备注,而不是只测一个样本。
## 覆盖的关键场景
- `com.apple.colorsyncutility` 的旧中文默认备注带历史句号 `。`,无 metadata,能被识别为 Apple 默认备注。
- Chess、TV、Phone、Freeform、Music、Time Machine、Home、Passwords、Disk Utility、iPhone Mirroring 的历史中文默认备注无 metadata 时,能被识别为 Apple 默认备注。
- 对当前真实 `tags.json` 做只读模拟:上述 10 条 stale Apple 备注在 `ms`、`ja`、`ar`、`en` 下都会迁移到目标语种,不保留旧中文。
- 同样的备注 fingerprint 只在同 bundle 范围内生效,不能跨 bundle 误匹配。
- 用户追加内容后的中文备注不匹配 Apple 默认备注集合,不会被迁移。
- 29 个语种的 Apple 初始化备注均执行跨语种脚本污染检查,translations 和 localizations 任一失败都会阻断发布。
- `com.apple.campo` 在 base、translations 和每个 localization 文件中都存在。
- 产物版本与源码版本一致:`7.9.3 / 20260619.2142`。
- 产物版本与源码版本一致:`7.9.4 / 20260619.2221`。
- DMG:`/Users/ar/Projects/Taglauncher/src/build/TagLauncher.dmg`
- DMG SHA256:`e6a5b2d1aab96cfd8e8a09edda7ae64b0319c044c2517ad9527887f020507d41`
## 结论
本轮修复满足发布前自查要求。自动化未发现 Apple 默认备注迁移、资源完整性、macOS 14 构建 metadata 或签名问题。
本轮修复满足发布前自查要求。自动化未发现 Apple 默认备注迁移、跨语种资源污染、macOS 14 构建 metadata 或签名问题。
src/Docs/Requirements/2026-06-19-apple-default-note-localization-worklog.md
@@ -65,3 +65,42 @@
- 本轮修复只按同 bundle 的 Apple 默认备注 fingerprint 迁移旧数据,不按中文字符判断。
- Arabic 等非中文语言资源中 Apple 默认备注保持本地语言;旧中文默认备注会迁移到当前语言。
- 用户手写备注和手动清空备注仍由 `.manual` metadata 保护,不会被本轮迁移覆盖。
## 2026-06-19 22:21
### 用户复测失败
- 用户截图显示,在非中文语言下,Chess、TV、Phone、Freeform、Music、Time Machine 等 Apple 自带应用仍显示旧中文备注。
- 本机 `tags.json` 复查确认这些应用的备注为旧中文默认文案,且 `appNoteMetadata` 为 `None`。
- 7.9.3 的迁移只覆盖当前 29 语种资源和句末标点变体,未覆盖 7.9 之前已经写入用户库的另一批历史默认文案。
### 补修
- `AppleDefaultAppCatalog` 增加历史默认备注 fingerprint 白名单,覆盖真实暴露的旧中文默认备注。
- 白名单按 bundle id 精确匹配,不使用“包含中文就覆盖”的宽泛规则。
- 运行时代码保存 fingerprint,不保存旧中文展示文案。
- `apple_default_note_migration_qa.sh` 增加真实历史默认备注 fixture,并验证迁移到 29 个语种后不会保留旧中文。
- `apple_default_apps_resource_qa.sh` 增加跨语种脚本污染检查:Apple 初始化备注在 translations 和 localizations 中都不允许混入其他语种正文。
- 版本推进到 `7.9.4 / 20260619.2221`。
### 补充 QA 证据
- `bash Scripts/apple_default_apps_resource_qa.sh`:PASS
- `bash Scripts/apple_default_note_migration_qa.sh`:PASS
- `bash Scripts/apple_default_note_policy_qa.sh`:PASS
- `bash Scripts/smartstart_catalog_resource_qa.sh`:PASS
- `bash Scripts/usage_tips_qa.sh`:PASS
- `bash Scripts/macos14_availability_typecheck_qa.sh`:PASS
- 当前真实 `tags.json` 只读模拟:Chess、Freeform、Home、Music、Passwords、Phone、TV、Time Machine、Disk Utility、iPhone Mirroring 共 10 条 stale Apple 备注,在 `ms/ja/ar/en` 下均会迁移到目标语种。
- `APP_BUILD=20260619.2221 bash build.sh`:PASS
- `bash Scripts/macos14_build_metadata_qa.sh`:PASS
- `codesign --verify --deep --strict --verbose=2 build/TagLauncher.app`:PASS
- `bash make_dmg.sh`:PASS,生成 `/Users/ar/Projects/Taglauncher/src/build/TagLauncher.dmg`
- `hdiutil verify build/TagLauncher.dmg`:PASS
- DMG SHA256:`e6a5b2d1aab96cfd8e8a09edda7ae64b0319c044c2517ad9527887f020507d41`
### 追加 QA 原则
- Apple 自带应用备注属于初始化内容,不是用户内容。
- 初始化备注必须按当前语言显示;任一语种资源出现其他语种正文均视为 QA FAIL。
- QA 不能只验证资源存在或单个样本迁移,必须覆盖真实用户库里已知历史默认备注。
src/Scripts/apple_default_apps_resource_qa.sh
@@ -159,6 +159,55 @@
if not isinstance(notes_by_language, dict):
    fail("Apple translations notesByLanguage must be an object")
script_patterns = {
    "cjk": re.compile(r"[\u4e00-\u9fff]"),
    "kana": re.compile(r"[\u3040-\u30ff]"),
    "hangul": re.compile(r"[\uac00-\ud7af]"),
    "cyrillic": re.compile(r"[\u0400-\u04ff]"),
    "arabic": re.compile(r"[\u0600-\u06ff]"),
    "thai": re.compile(r"[\u0e00-\u0e7f]"),
}
expected_script_requirements = {
    "zh-Hans": ("cjk",),
    "zh-Hant": ("cjk",),
    "ja": ("kana",),
    "ko": ("hangul",),
    "ar": ("arabic",),
    "ar-Najdi": ("arabic",),
    "ru": ("cyrillic",),
    "uk": ("cyrillic",),
    "sr-Cyrl": ("cyrillic",),
    "th": ("thai",),
}
forbidden_scripts_by_language = {
    "zh-Hans": ("kana", "hangul", "cyrillic", "arabic", "thai"),
    "zh-Hant": ("kana", "hangul", "cyrillic", "arabic", "thai"),
    "ja": ("hangul", "cyrillic", "arabic", "thai"),
    "ko": ("cjk", "kana", "cyrillic", "arabic", "thai"),
    "ar": ("cjk", "kana", "hangul", "cyrillic", "thai"),
    "ar-Najdi": ("cjk", "kana", "hangul", "cyrillic", "thai"),
    "ru": ("cjk", "kana", "hangul", "arabic", "thai"),
    "uk": ("cjk", "kana", "hangul", "arabic", "thai"),
    "sr-Cyrl": ("cjk", "kana", "hangul", "arabic", "thai"),
    "th": ("cjk", "kana", "hangul", "cyrillic", "arabic"),
}
for code in l10n_codes:
    forbidden_scripts_by_language.setdefault(
        code,
        ("cjk", "kana", "hangul", "cyrillic", "arabic", "thai"),
    )
def assert_note_language_clean(code: str, note: str, context: str) -> None:
    for script_name in expected_script_requirements.get(code, ()):
        if not script_patterns[script_name].search(note):
            fail(f"{context} does not contain expected {script_name} script for {code}")
    for script_name in forbidden_scripts_by_language.get(code, ()):
        if script_patterns[script_name].search(note):
            fail(f"{context} contains unexpected {script_name} script for {code}: {note!r}")
for code in l10n_codes:
    display_names = display_names_by_language.get(code)
    notes = notes_by_language.get(code)
@@ -187,6 +236,7 @@
            fail(f"Apple translations note for {code}/{normalized} is identical to zh-Hans")
        if code not in {"zh-Hans", "zh-Hant", "ja"} and re.search(r"[\u4e00-\u9fff]", note):
            fail(f"Apple translations note for {code}/{normalized} contains Chinese characters")
        assert_note_language_clean(code, note, f"Apple translations note for {code}/{normalized}")
bad_fragments = [
    "Apple 內建 App",
@@ -234,19 +284,6 @@
    "null",
]
script_requirements = {
    "zh-Hans": re.compile(r"[\u4e00-\u9fff]"),
    "zh-Hant": re.compile(r"[\u4e00-\u9fff]"),
    "ja": re.compile(r"[\u3040-\u30ff\u4e00-\u9fff]"),
    "ko": re.compile(r"[\uac00-\ud7af]"),
    "ar": re.compile(r"[\u0600-\u06ff]"),
    "ar-Najdi": re.compile(r"[\u0600-\u06ff]"),
    "ru": re.compile(r"[\u0400-\u04ff]"),
    "uk": re.compile(r"[\u0400-\u04ff]"),
    "sr-Cyrl": re.compile(r"[\u0400-\u04ff]"),
    "th": re.compile(r"[\u0e00-\u0e7f]"),
}
for code in l10n_codes:
    path = apple_dir / f"AppleDefaultApps.localizations.{code}.json"
    if not path.is_file():
@@ -283,8 +320,7 @@
        for fragment in bad_fragments:
            if fragment in note:
                fail(f"{path.name} note for {bundle} contains blocked fragment: {fragment!r}")
        if code in script_requirements and not script_requirements[code].search(note):
            fail(f"{path.name} note for {bundle} does not contain expected script for {code}")
        assert_note_language_clean(code, note, f"{path.name} note for {bundle}")
    missing = bundle_ids - set(entries_by_bundle)
    if missing:
        fail(f"{path.name} is missing {len(missing)} Apple entries; first missing: {sorted(missing)[:5]}")
src/Scripts/apple_default_note_migration_qa.sh
@@ -78,7 +78,11 @@
languages = base.get("supportedLanguages", [])
note_limit = int(base.get("noteLimit", 80))
entries = base.get("entries", [])
bundle_ids = {entry.get("bundleIdentifier") for entry in entries}
bundle_ids = {
    entry.get("bundleIdentifier").lower()
    for entry in entries
    if isinstance(entry.get("bundleIdentifier"), str)
}
normalized_names = {entry.get("normalizedName") for entry in entries}
for bundle in ["com.apple.colorsyncutility", "com.apple.campo", "com.apple.siri.launcher"]:
@@ -104,7 +108,7 @@
    path = apple_dir / f"AppleDefaultApps.localizations.{language}.json"
    catalog = load_json(path)
    localizations[language] = {
        item["bundleIdentifier"]: item
        item["bundleIdentifier"].lower(): item
        for item in catalog.get("entries", [])
        if isinstance(item, dict) and isinstance(item.get("bundleIdentifier"), str)
    }
@@ -139,9 +143,43 @@
            fingerprints.add(note_fingerprint(variant, note_limit))
    known_fingerprints_by_bundle[bundle] = fingerprints
historical_default_notes = {
    "com.apple.chess": "系统自带国际象棋,摸鱼时比刷网页更优雅。",
    "com.apple.freeform": "无限白板,适合头脑风暴、草图和乱七八糟的想法。",
    "com.apple.home": "控制 HomeKit 设备,让 Mac 也能管家里的灯和空调。",
    "com.apple.music": "本地音乐和 Apple Music 的播放器。",
    "com.apple.passwords": "系统密码管家,账号、验证码和通行密钥集中管。",
    "com.apple.mobilephone": "通过 iPhone 在 Mac 上接打电话。",
    "com.apple.tv": "Apple TV 和本地影视内容的入口。",
    "com.apple.backup.launcher": "系统备份工具,后悔药就靠它提前准备。",
    "com.apple.diskutility": "管磁盘、分区、格式化和急救,别乱点但很重要。",
    "com.apple.screencontinuity": "在 Mac 上直接操作 iPhone,少一次伸手拿手机。",
}
for bundle, note in historical_default_notes.items():
    if bundle not in known_fingerprints_by_bundle:
        fail(f"historical default bundle is not in Apple base: {bundle}")
    for variant in legacy_variants(note, note_limit):
        known_fingerprints_by_bundle[bundle].add(note_fingerprint(variant, note_limit))
legacy_zh_note_with_period = zh_default_note + "。"
if note_fingerprint(legacy_zh_note_with_period, note_limit) not in known_fingerprints_by_bundle[colorsync_bundle]:
    fail("legacy zh-Hans ColorSync note with historical period must match known Apple defaults")
for bundle, note in historical_default_notes.items():
    if note_fingerprint(note, note_limit) not in known_fingerprints_by_bundle[bundle]:
        fail(f"historical default note must match for {bundle}")
    for language in languages:
        localized_entry = localizations[language].get(bundle)
        if not localized_entry:
            fail(f"historical default note target is missing localization for {language}/{bundle}")
        localized_note = normalized_note(localized_entry.get("note"), note_limit)
        if not localized_note:
            fail(f"historical default note target is empty for {language}/{bundle}")
        if language not in {"zh-Hans", "zh-Hant"} and localized_note == note:
            fail(
                "historical default note migration would keep Chinese text "
                f"for {language}/{bundle}: {localized_note!r}"
            )
manual_chinese_note = zh_default_note + " 我自己加的备注"
if note_fingerprint(manual_chinese_note, note_limit) in known_fingerprints_by_bundle[colorsync_bundle]:
@@ -161,6 +199,10 @@
  || fail "AppleDefaultAppCatalog must match legacy notes against known Apple defaults"
rg -Fq 'legacyDefaultNoteVariants' "$APPLE_CATALOG_SWIFT" \
  || fail "AppleDefaultAppCatalog must include historical punctuation variants"
rg -Fq 'legacyDefaultNoteFingerprintsByBundleIdentifier' "$APPLE_CATALOG_SWIFT" \
  || fail "AppleDefaultAppCatalog must include historical Apple default notes"
rg -Fq 'a3b754a4f06eee33' "$APPLE_CATALOG_SWIFT" \
  || fail "AppleDefaultAppCatalog must include pre-metadata Chess default note"
rg -Fq 'metadata?.origin == .manual' "$APPLE_CATALOG_SWIFT" \
  || fail "AppleDefaultAppCatalog must protect manual notes"
rg -Fq 'apple-default-note-migration' "$APPLE_CATALOG_SWIFT" \
src/Scripts/apple_default_note_policy_qa.sh
@@ -44,6 +44,8 @@
  || fail "Apple relocalization must adopt legacy default notes by exact known-default fingerprint"
rg -Fq 'legacyDefaultNoteVariants' "$APPLE_CATALOG_SWIFT" \
  || fail "Apple relocalization must include historical default-note punctuation variants"
rg -Fq 'legacyDefaultNoteFingerprintsByBundleIdentifier' "$APPLE_CATALOG_SWIFT" \
  || fail "Apple relocalization must include pre-metadata historical Apple default notes"
rg -Fq 'apple-default-note-migration' "$APPLE_CATALOG_SWIFT" \
  || fail "Apple relocalization must back up before adopting legacy no-metadata notes"
rg -Fq 'origin: .appleDefault' "$APPLE_CATALOG_SWIFT" \