From 39e759d015af5d82f387b57a8d137bdeb844d430 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 19 Jun 2026 22:40:08 +0800
Subject: [PATCH] Fix historical Apple default note migration

---
 src/Docs/Requirements/2026-06-19-apple-default-note-localization-worklog.md |   39 +++++
 Docs/KM02-技术/07-构建发布与QA.md                                                  |  138 +++++++++++++++++++
 src/CHANGELOG.md                                                            |    8 +
 src/Apptag/Info.plist                                                       |    4 
 src/Scripts/apple_default_apps_resource_qa.sh                               |   66 +++++++--
 src/Scripts/apple_default_note_migration_qa.sh                              |   46 ++++++
 src/Docs/Requirements/2026-06-19-apple-default-note-localization-qa.md      |   19 ++
 src/Scripts/apple_default_note_policy_qa.sh                                 |    2 
 src/Apptag/AppleDefaultAppCatalog.swift                                     |   53 +++++++
 9 files changed, 353 insertions(+), 22 deletions(-)

diff --git "a/Docs/KM02-\346\212\200\346\234\257/07-\346\236\204\345\273\272\345\217\221\345\270\203\344\270\216QA.md" "b/Docs/KM02-\346\212\200\346\234\257/07-\346\236\204\345\273\272\345\217\221\345\270\203\344\270\216QA.md"
new file mode 100644
index 0000000..5356d43
--- /dev/null
+++ "b/Docs/KM02-\346\212\200\346\234\257/07-\346\236\204\345\273\272\345\217\221\345\270\203\344\270\216QA.md"
@@ -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 的高风险主要在窗口时序、搜索名称和资源质量,不是单纯编译。
diff --git a/src/Apptag/AppleDefaultAppCatalog.swift b/src/Apptag/AppleDefaultAppCatalog.swift
index 9ef452c..66b884b 100644
--- a/src/Apptag/AppleDefaultAppCatalog.swift
+++ b/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
     }
 
diff --git a/src/Apptag/Info.plist b/src/Apptag/Info.plist
index d11db52..1bcd15b 100644
--- a/src/Apptag/Info.plist
+++ b/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>
diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md
index 5d9c1c6..feea838 100644
--- a/src/CHANGELOG.md
+++ b/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 后迁移为当前语言
diff --git a/src/Docs/Requirements/2026-06-19-apple-default-note-localization-qa.md b/src/Docs/Requirements/2026-06-19-apple-default-note-localization-qa.md
index 355c5a1..a87590f 100644
--- a/src/Docs/Requirements/2026-06-19-apple-default-note-localization-qa.md
+++ b/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 或签名问题。
diff --git a/src/Docs/Requirements/2026-06-19-apple-default-note-localization-worklog.md b/src/Docs/Requirements/2026-06-19-apple-default-note-localization-worklog.md
index 69b0a50..7d06bee 100644
--- a/src/Docs/Requirements/2026-06-19-apple-default-note-localization-worklog.md
+++ b/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 不能只验证资源存在或单个样本迁移,必须覆盖真实用户库里已知历史默认备注。
diff --git a/src/Scripts/apple_default_apps_resource_qa.sh b/src/Scripts/apple_default_apps_resource_qa.sh
index 19c09aa..1bb8840 100755
--- a/src/Scripts/apple_default_apps_resource_qa.sh
+++ b/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]}")
diff --git a/src/Scripts/apple_default_note_migration_qa.sh b/src/Scripts/apple_default_note_migration_qa.sh
index ce592d8..d1f4f17 100755
--- a/src/Scripts/apple_default_note_migration_qa.sh
+++ b/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" \
diff --git a/src/Scripts/apple_default_note_policy_qa.sh b/src/Scripts/apple_default_note_policy_qa.sh
index 11d215a..db0b990 100755
--- a/src/Scripts/apple_default_note_policy_qa.sh
+++ b/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" \

--
Gitblit v1.9.3