# daoqi 项目协作约定（面向 Claude / AI 助手）

> 本文件随 daoqi 仓库分发，任意设备 clone 后均按此约定执行。

---

## 适用范围声明

**本 CLAUDE.md 仅适用于 daoqi 项目自身**。

- 父目录（任何上级目录）下可能存在其它 CLAUDE.md 文件，那些文件**不是为本项目编写**的，与 daoqi 项目无关，**不参照、不继承、不引用**。
- Claude / AI 助手在本项目工作时，只读取并遵循本文件（`./CLAUDE.md`）与本项目 `docs/` 下的文档，不向上递归引用父级 CLAUDE.md。
- 如果在对话上下文中出现父级 CLAUDE.md 的内容（由工具自动加载），应当忽略，以本文件为唯一权威。

---

## 项目内容

daoqi 仓库当前并存两条工作线：

1. **现有 iOS 外壳（`msext` target）** — 已上线版本。Objective-C（MRC，未启用 ARC），最低 iOS 9.0；原生外壳 + 内置 H5（`gamehall.zip` 解压到沙盒）+ WebViewJavascriptBridge 桥接。
2. **新外壳设计（greenfield 重写）** — 计划中的下一代版本。设计原则、接口契约、实施蓝图见 `docs/` 下两份文档。

---

## 原项目 daoqi：遇到问题时的参考来源

**原 daoqi 项目代码位于** `/Users/joywayer/Documents/Works/YouleApps/iOS/youle_app_ios/daoqi/`（与 ylgamehall 同级目录）。该项目已稳定运行多年（msext target），是同一 H5 业务、同一渠道分发模式、同一启动流程的"工作参照实现"。

### 何时主动去读原项目

新项目实施过程中**遇到任何一类下述问题，第一动作是去 daoqi 找参照**，而不是凭推测搭方案：

- **iOS 行为不符合预期 / 与文档描述有差异**（如本次的 LaunchScreen orientation 问题：iOS 26 + Xcode 26 默认 Build Settings 写法导致启动图被 portrait 渲染再旋转 90°）
- **某个 Info.plist key / Build Settings 不确定该不该加 / 加哪种变体**
- **某个 H5 桥接 handler 的返回值结构 / 字段命名 / 类型不确定**
- **某个三方 SDK 的初始化时序 / 回调链疑似有顺序依赖**
- **渠道注入 / 启动流水线 / 升级逻辑某一步与设计文档对不齐**
- **某个看似可改的"老 hack"想清掉，但不确定该 hack 是否解决过某个具体问题**

### 怎么用原项目

1. **先查关键文件**：`daoqi/msext/Info.plist`、`daoqi/msext/Class/AppDelegate.{h,m}`、`daoqi/msext/Class/UI/NewRootVC.m`（启动流水线全部在此）、`daoqi/Podfile`（看用了什么三方）
2. **grep 同名 key / 同名 handler**：例如 `grep -r "UISupportedInterfaceOrientations" daoqi/msext/Info.plist`
3. **看 git log / git blame**：原项目积累的"看似古怪的写法"通常都有当年的修复故事
4. **对照差异**：原项目稳定运行的写法是事实基准；新项目偏离它的部分必须有明确的"为什么改"的理由（通常出现在 `docs/H5-Native-Implementation-Design.md` 的 ADR 或 §6.3.6 等差异表里）

### 与「原则 B：内部自由重构」不冲突

- 参考原项目 ≠ 照抄原项目的实现细节
- 参考原项目 = 把它当作 **"哪些事必须做"的清单** 和 **"哪些坑已经踩过"的备忘**
- 新项目的现代化重构（Swift 6 / SwiftUI / actor 隔离 / SPM 依赖等）依然按原则 B 自由设计；只是设计前先确认"我没有漏掉原项目实际需要解决的事"

### 典型案例：LaunchScreen orientation（启动图方向错误）

- **症状**：新项目启动图渲染成竖向矩形再被整体旋转 90°，画面横躺在中间
- **错误做法**：凭 Xcode 26 文档推测、纯调整 storyboard / contentMode / 反复换素材
- **去原项目找答案**：`daoqi/msext/Info.plist` + `msext.xcodeproj/project.pbxproj`
  - 发现 1：Info.plist 用的是**顶层无后缀的 `UISupportedInterfaceOrientations`**（值 = landscape），**没有** `~iphone` / `~ipad` 后缀变体
  - 发现 2：根本**没有** `UILaunchStoryboardName`，用的是 `ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = "LaunchImage-1"`（iOS 8 时代的 LaunchImage Asset Catalog 机制）
- **原项目为什么稳**：
  - 它的所有 LaunchImage PNG（`Default-568h@2x` / `LaunchImage-1-800-667h@2x` 等）**物理像素都是竖向、画面内容横躺**
  - iOS 8 LaunchImage Asset Catalog 内置"按 device idiom + orientation 自动选图 + 系统级 90° 旋转"魔法，会读 Info.plist 顶层 `UISupportedInterfaceOrientations` 决定是否旋转 portrait 资源
  - 因此一张物理 portrait 的 PNG 在横屏设备上能被正确旋转后铺满
- **不能照搬到新项目**：
  - LaunchImage Asset Catalog 在 iOS 14+ 已 deprecated，新项目按苹果推荐用 `LaunchScreen.storyboard`
  - LaunchScreen.storyboard **没有自动旋转 portrait 资源**的魔法，UIImageView 直接渲染 PNG 物理像素
  - 把 `docs/res/Res/Default-568h@2x` 这类**物理 portrait + 画面横躺**的 msext 残留素材直接塞进 LaunchScreen.storyboard 就会看到躺倒画面
- **新项目（LaunchScreen.storyboard）的正确组合**：
  1. **素材必须物理像素就是横屏的**（必要时用 `sips -r -90 -s format png` 一次性把 msext 残留 portrait 素材逆时针 90° 输出为标准 PNG，物理像素正确）
  2. **Info.plist 显式补一份无后缀 `UISupportedInterfaceOrientations = landscape`**（Xcode 26 General → Deployment Info UI 只写带后缀的 `~iphone` / `~ipad`，少了无后缀 key 会让 LaunchScreen 阶段——device idiom 尚未识别的早期窗口——fallback 到 portrait 渲染再被旋转）
  3. UIImageView `contentMode = scaleAspectFit` + 黑底（比 16:9 更宽的现代横屏设备左右补黑边，保画面完整不裁切 logo）
- **iOS launch snapshot 缓存粘滞（开发期 iteration 坑）**：
  - iOS 为加速启动，会把首次渲染的 LaunchScreen 缓存为 PNG snapshot 存到 app sandbox（`Library/Caches/Snapshots/<bundleID>/com.apple.UIKit.SplashBoard/`），后续启动不重渲染
  - **Xcode 覆盖安装 .app 不会清 sandbox 缓存**，所以改了 Info.plist / LaunchScreen.storyboard / 启动图素材后，Run 出来仍可能是旧 snapshot
  - 改完启动相关任意一项**必须做**：长按 app → 删除 → Xcode 重新 Run；或模拟器 Erase All Content and Settings；或 `xcrun simctl uninstall booted <bundleID>`
  - **终端用户不受影响**：从 IPA 首次安装、或升级新 IPA 第一次启动后即被 iOS 自动刷新；只是开发期反复迭代同一台 device 时会被迷惑

### 典型案例：H5 与原生通讯接口的真实路径

- **症状**：Design / Contract 文档描述 H5 用 `window.settings.getXxx()` 同步函数读渠道值（如 `getothername` / `getchannelName`）；按文档实现完 polyfill 后 H5 业务行为不对，user 反馈"H5 读不到数据"
- **错误做法**：基于 Contract §附录 A 列出的旧桥 JSExport 28 方法实现 polyfill，把 9 项 getter 注入到 `window.settings`，认为这是 H5 端的主路径
- **去原项目找答案**：grep `var app_` 在 `daoqi/msext/Class/RootVC/NewRootVC.m:1204` / `gameController.m:2160` / `AppDelegate.m:259`
  - 发现 1：原项目 `initJSdata()` 是 `[NSString stringWithFormat:@"var app_xxx=...;"]` + `writeToFile:` **写 .js 文件到沙盒**，H5 业务用 `<script src="app_data.js">` 同步引入预生成的 12 个全局变量直接读
  - 发现 2：Contract §附录 A 自身标注「iOS<9 路径使用的旧协议，新外壳如最低系统 ≥ iOS 14 可不实现」，新项目最低 iOS 15.6 → 旧桥 polyfill **完全不必要**
  - 发现 3：Contract §4.2 之前列的 11 项变量与原项目代码不一致 — 漏 6 项（version / Launchtype / getwifisignalLevel / gamename / invitationcode / gamesname）、多 2 项（gameid / compareCode 原项目根本不写）、文件名与变量名混淆（文件 `app_battery.js` 内的变量名是带 get 的 `app_getbattery`，文件名不带 get）
- **修复**：
  1. 撤销已实现的 `window.settings.getXxx()` polyfill 章节（Design §3.4.1）
  2. 新增 Design §7.5「H5 `app_*.js` 预注入文件机制」覆盖 15 个变量的写入逻辑
  3. 按原项目代码全面修订 Contract §4.2（删 gameid/compareCode、补 6 项、修正 battery/network 变量名带 get、注明大厅/子游戏字面差异、加修订记录小节）
- **教训**：
  - **文档与代码冲突时，原项目代码是真理**。Contract 也可能写错（一份 10 年前写就的文档遗漏 / 误传是常态），必须用 `grep` 验证字面字符串
  - **优先验证字面字符串、不要依赖语义猜测**：`app_getbattery` 与 `app_battery` 一字之差，AI 助手凭语义"应该就是 app_battery 吧" 100% 会犯错
  - **iOS 最低系统版本是契约边界的天然过滤器**：契约里 iOS<9 路径明示可不实现的接口（如 §附录 A 旧桥），新项目最低 iOS 15.6 应当主动剔除，不要"出于完备性"反而实现一份用不到的 polyfill

### 典型案例：`app_*` 注入时序问题（原则 A 不可妥协）

- **症状**：H5 端读到 `app_gameconfig` 等关键变量是 H5 zip 包内默认占位值，不是 ChannelConfig.plist 的实际渠道值
- **调研发现**（按 CLAUDE.md「先看原项目」规则）：原 msext 在 `NewRootVC.viewDidLoad → initJSdata`（`loadFileURL` **之前**）用 `writeToFile:` 把 `app_data.js` 写入沙盒，H5 启动时通过 `<script src>` 同步引入就能直接读到实际值。新外壳改用现代化的 `WKUserScript(.atDocumentEnd)` + `evaluateJavaScript` 路径，**时序无法 1:1 等价**：
  - `documentStart` 注入会被 H5 自带 `app_*.js` 内的 `var app_xxx = "默认值"` 声明覆盖回退
  - `documentEnd` 注入又太晚（H5 业务顶层 `<script>` 在 documentEnd 之前已经跑过 `var x = app_xxx`，读到的是默认占位）
  - WKUserScript 没有"在 `<script src="app_data.js">` 之后、业务 `<script>` 之前"的精确时机
- **错误做法**：提出 3 个方案，其中 A "H5 改一行：把 `app_xxx` 用法移到 `DOMContentLoaded` 之后"和 B "H5 改 4 个文件：用 `if(typeof xxx==='undefined')` 守卫" 都要求 H5 配合改动 — **违反原则 A 第一准则**，立即否决
- **正确做法**：方案 C — 原生 Swift 端**回到 msext 老路**，在 `loadFileURL` 前用 `String.write(to:)` 把 `app_*.js` 物理文件写到沙盒，覆盖 H5 zip 包内自带的版本。哪怕这是"现代 WKWebView 不推荐"的写法，但行为 100% 等价 msext，契约不破
- **教训**：
  - **遇到契约边界时不要琢磨"H5 改一点点就行"**——再小的改动都不行，第一准则不可妥协
  - **`WKUserScript` / `evaluateJavaScript` 不是万能药**：现代化 API 在某些时序场景下**无法**1:1 等价旧路径，遇到时序冲突就回到旧路径，不要为了"看起来现代"而牺牲契约
  - **原则 B "现代化"对原则 A 让步**：Swift 写文件 / 看似过时的模式在新外壳里完全可以存在；不优雅是可接受的代价，契约破裂不是

---

## 文档索引

| 文档 | 角色 | 何时读 |
|------|------|--------|
| `CLAUDE.md`（本文件） | 项目协作约定、契约边界声明 | 每次对话先读 |
| `docs/H5-Native-Contract.md` | H5 ↔ 原生 桥接**契约**（黑盒可观察行为） | 涉及桥接接口必读 |
| `docs/H5-Native-Implementation-Design.md` | 新外壳实施蓝图（架构、模块、代码骨架） | 实现新外壳必读 |
| `docs/Development-Plan.md` | 开发执行计划（Phase 0–10、依赖图、进度追踪） | 排期 / 进度更新必读 |
| `docs/Verification-Checklist.md` | 功能验证清单（开发期累积、统一验收的操作手册） | 每完成 Phase 子项时补充 / 勾选；不要在 Phase 进行中频繁跑，等多个 Phase 累积后统一验证 |
| `docs/SDK-Integration-Guide.md` | 微信 / 高德定位 / opencore-amr / 七牛 接入操作手册（凭证 / Vendor / Info.plist / 代码触点 / BackGameData 清理 / 验收）| 项目方发 SDK 凭证 + 二进制后，按对应章节落地；接完回 §5 清单核对 |
| `docs/H5-Debug-Guide.md` | H5 调试方法（Safari Web Inspector 看大厅 / 子游戏 / 弹层网页输出的操作手册） | 调试 H5 端 console / Network / DOM / 断点时；新增 WKWebView 时也按其 §2 补 `isInspectable` 守卫 |
| `../daoqi/`（仓库同级目录） | 原项目 msext 工作参照实现 | 遇到 iOS 行为差异 / 渠道注入 / 桥接 / SDK 初始化等不确定问题时，**优先参考此处**而非凭推测，详见下文「原项目 daoqi：遇到问题时的参考来源」 |

---

## 依赖管理约定

新项目采用**纯 SPM + Vendor `.xcframework` 两层管理**，**禁止引入 CocoaPods**（详见 `docs/Development-Plan.md` ADR-006）。

- **首选 SPM**：开源 / 官方提供 Swift Package 的库（Sentry / ZIPFoundation / 七牛 `https://github.com/qiniu/objc-sdk` v8.9.x 等）
- **Vendor `.xcframework` 手动**：所有闭源二进制（微信 / 高德 AMap / opencore-amr 等），按 Design §14.1「Vendor 接入标准流程」放 `Vendor/<SDKName>/` + Target → General → Frameworks 加入 + `Vendor/<SDKName>/README.md` 记录版本与来源
- **不接入 QQ SDK / 也不接入抖音 SDK**：H5 调 `friendsShare(sharefriend=1, ...)` 时原生弹 SharePanel 三选一（微信好友 / QQ / 抖音，参 msext `gameController.m:585`）。QQ 走 URL Scheme（`mqqapi://share/to_fri?...`，参 msext `QQShareManager.m:14` `__has_include` fallback），抖音走 URL Scheme + Photos（`snssdk1128://share/video` + 保存截图到相册让用户在抖音里选，参 msext `DouyinShareManager.m`）。两者都无 SDK 依赖，Info.plist 仅需 `LSApplicationQueriesSchemes`（`mqq` / `mqqapi` / `mqqopensdkfriend` / `snssdk1128`）+ `NSPhotoLibraryAddUsageDescription`（抖音需要写入相册）
- **禁止 CocoaPods**：项目内不放 `Podfile` / `Podfile.lock` / `Pods/` / `.xcworkspace`。工程入口固定为 `ylgamehall.xcodeproj`
- **新增第三方依赖时**：按 SPM → Vendor 顺序尝试，PR 描述写明选型理由；若发现某 SDK 既无 SPM 又无 XCFramework，回到 ADR-006 决策框架重新评估

---

## docs/res/ 与项目资源的关系

`docs/res/` 是**项目维护者的私人原始素材池**，**与项目架构无关**：

- 仅作为开发期可能用到的素材的随手存放点（H5 团队提供的 zip / 美术给的 png / 原 msext 沉淀的 mp3 / 历史 Asset Catalog 等）
- **项目代码 / 构建脚本 / Xcode 工程都不感知它的存在**，不依赖它的目录结构，不引用它的任何文件
- 内容、结构、命名随时可由维护者调整，不影响构建
- git 跟踪它只是为了多机同步素材，而不是因为项目需要它

### 项目自己的资源目录由项目独立规划

**禁止把 `docs/res/` 当作项目的资源目录使用**（即不要在代码 / 脚本 / Build Phase 中直接引用 `docs/res/xxx`）。当某项工作需要某个素材时：

1. 从 `docs/res/` **拷贝一份**到项目按现代 iOS 实践规划的目标目录（如 `ylgamehall/Resources/`、`ylgamehall/Assets.xcassets/`、`Vendor/<SDK>/` 等）
2. 由 Xcode 工程结构 / synchronized group / Build Phase 接管该素材的打包流程
3. 后续维护与 `docs/res/` 不再有任何关联——`docs/res/` 那份是"原始备份"，工程内那份是"在用版本"

**约定**：项目实际需要的资源目录结构由 Design / Plan 在各 Phase 实施时按需定义并落到工程内；不预先把"所有未来可能用到的资源"硬塞进某个固定 Resources 目录。

---

## 核心约定：两条原则（原则 A 是第一准则，地位高于原则 B）

> 早期版本曾把 A / B 描述为"地位对等"，**2026-06-22 升级**：原则 A 是**第一准则**，地位**高于**原则 B。两者冲突时无条件选 A、压力让原生 Swift 端承担。

### 原则 A：H5 端零修改（第一准则，不可妥协）

**H5 端不做任何改动**——一行代码、一个字符、一个文件名、一个调用时机，都不允许 H5 团队配合调整。这是新外壳所有设计 / 实现的**最高约束**，地位高于原则 B、高于"技术优雅"、高于"现代化"、高于任何关于"性能 / 可维护性 / 可观察性"的考量。

#### A.1 已经被否决的 / 永远不要再提出的方案

下面这些方案在任何场景下都**不要**作为建议提给项目方，即便它们更简洁 / 更现代 / 工作量更小：

- ❌ "H5 改一行"：哪怕只让 H5 把读 `app_xxx` 的代码挪到 `DOMContentLoaded` 之后
- ❌ "H5 改一个文件"：哪怕只让 H5 把 `app_*.js` 内容换成 `if(typeof xxx==='undefined') var xxx=...`
- ❌ "H5 加一个 polyfill"：哪怕只让 H5 引入一段 5 行的兼容垫片
- ❌ "H5 调整调用时机"：哪怕只让 H5 把同步表达式改成 `setTimeout(..., 0)`
- ❌ "新桥接口"：哪怕只让 H5 改成调一个新 handler 名

**唯一例外**：项目方主动要求改 H5 时（如 H5 团队自己提出重构），新外壳配合调整契约。其它一切场景下，H5 不可触碰。

#### A.2 替代方案：原生 Swift 端硬扛

遇到契约边界冲突时正确的应对：

1. **回到原 msext 老路**：原 msext 是上线 N 年的稳定实现，它怎么解决就照搬。哪怕老路是"现代 WKWebView 不推荐"的写法（如 `loadFileURL` 前 `writeToFile` 写 .js 文件、`UIWebView` 时代的 `JSContext` 注入约定），只要功能等价，照搬
2. **接受技术不优雅**：硬扛会让原生 Swift 代码出现"看似过时的写法"（写文件、轮询、特殊时机 hook 等）。**这是可以接受的**——原则 B "现代化"对原则 A 让步，不是相反
3. **不要用"虽然这是临时方案，等 H5 改了再优雅化"自我安慰**：H5 永远不会"等会儿改"。**今天的临时方案就是终身方案**

#### A.3 判定流程

写代码 / 写文档前问自己：

> "这个方案要 H5 端做任何动作吗？包括 H5 团队改一行、调整调用顺序、删一个文件、加一个守卫？"
>
> - 答"是" → **立即否决，重新设计**
> - 答"否" → 继续判定其它原则

### 原则 B：原生内部实现自由重构（内部架构）

**原生内部的实现可以也应当与原项目不一致**。新外壳不是旧 msext 的复刻，是按现代标准重新设计的产物，应当主动追求：

- **架构优雅** — 模块边界清晰、依赖单向、单一职责；不再受旧项目 PCH 全局注入、MRC、UIWebView、JSContext 等历史包袱约束
- **高效 / 高性能** — 启动并发优化、WKWebView ProcessPool 复用、桥消息批处理、actor 隔离的并发模型；性能指标可度量、有 SLO
- **专业** — 完整的单测覆盖、CI 流水线、代码规约、文档体系；不留"等会再补"的口子
- **成熟** — 选用社区主流、长期维护的库（URLSession、SPM、ZIPFoundation、async/await）；摒弃 ASIHttpRequest、SBJSON、UIWebView、AFNetworking 等已废弃技术

→ 详细技术选型与架构设计见 `docs/H5-Native-Implementation-Design.md`。

**两条原则的交叉判定**（A 是第一准则）：
- 如果某项决策影响 H5 可观察行为（接口名/字段/数据/JS 协议/时序）→ 必须遵循原则 A，**照搬原 msext 实现，哪怕看起来过时**
- 如果某项决策**只**影响原生内部（技术栈/模块结构/状态管理/SDK 选型/代码风格）→ 必须遵循原则 B，按现代标准设计，不要照搬旧项目
- 当两者冲突时，**无条件选 A**：原则 A 是不可妥协的第一准则。原生内部多一层适配层 / 多一段"看似过时"的代码 / 多一份与原 msext 等价的实现，都可以接受；契约被破，**永远不可以接受**

---

## 原则 A 详述：H5 适配的五类必须一致

为达到 H5 端零修改适配，以下五类内容**必须 1:1 与现网一致**：

### 1. 桥接接口名（handler 名字符串）

H5 端通过字符串名调用原生 handler，例如 `bridge.callHandler('mediaTypeAudio', ...)`、`bridge.callHandler('friendsSharetypeUrlToptitleDescript', ...)`。

- **新外壳注册的 handler 名必须字面一致**，少一个字符、改一个大小写、改一个驼峰都视为契约违反
- 含历史包袱命名照搬：`SwitchOverGameData`（驼峰首字母大写）/ `friendsSharetypeUrlToptitleDescript`（长形式）/ `gameui_play_voice`（下划线）/ `OpenurlTitleData` 等
- 完整 39 项 handler / callback 名见 `docs/H5-Native-Contract.md` §3

### 2. 参数字段名

H5 → Native 时 data 字典中的 key，以及 Native → H5 callback payload 中的 key。

- **字段名字面一致**，含历史包袱:
  - `sharelogin` 回包中 `Province` 是**大写 P**（不是 `province`）
  - `OpenurlTitleData` 入参中 `"title "` 末尾**有一个空格**（不是 `"title"`）
  - `getphoneinfo` 回包用**小写 i**（H5 调用方是 `getphoneInfo` 大写 I）
  - `appservice` 用字符串 `"2"`（后台）/ `"1"`（前台），不是数字 ⚠️ 值错位沿用 msext WKWebView 路径历史，参 `docs/H5-Native-Contract.md` §3.2 `appservice` 值错位说明
  - `getlocationinfo` 中 `latitude` / `longitude` 是 **string** 而不是 number（`stringWithFormat:@"%f"` 产生）

### 3. 参数数据结构

- H5 → Native 入参类型（string / number / bool / object / array）
- Native → H5 回包结构（每个字段的类型、嵌套层次）
- 字段是否可缺、缺时如何 fallback

→ 完整结构见 `docs/H5-Native-Contract.md` §3.1 / §3.2 表 A/B/C

### 4. H5 端注册方式（JS 协议）

H5 页面通过特定的 JavaScript 协议初始化桥并注册自己的 handler。**新外壳必须使用 H5 端代码一致的 JS 协议**：

- **大厅 / 子游戏页**：H5 使用 `WebViewJavascriptBridge` 的 JS 协议（`_setupWebViewJavascriptBridge` 握手、`bridge.callHandler(...)`、`bridge.registerHandler(...)`）。新外壳实现必须支持这套握手协议（推荐做法：直接复用 marcuswestin/WebViewJavascriptBridge 的 JS 端源码，原生侧自实现）
- **弹层（threeView 等价容器）页**：H5 使用 `window.settings.xxx(...)` 直接同步调用形式（JSExport 协议）。新外壳如改用 WKWebView，需注入 polyfill 让 `window.settings.{backgameData, browser, finishweb}` 仍然按相同形式可用

任何打破 H5 端注册方式的实现（如换用自创 JS 协议、改变 `window.settings` 名字）都视为契约违反。

### 5. 空实现接口也必须保留注册

当前现网中 `opensaoma`、`getGameplay` 等 handler 是空实现（`responseCallback("opensaoma")` 后无副作用），但 H5 仍然会调用。

- **新外壳必须以同名空 handler 注册**，否则 H5 调用时触发 "bridge handler not found" 错误分支，导致业务流程中断
- 完整空实现清单见 `docs/H5-Native-Contract.md` §3.1

---

## 不属于契约的部分（自由实现）

除上述五类外，原生内部一切自由：

- 技术栈（Swift / Objective-C / SwiftUI / UIKit 任选）
- 桥接库选型（自实现 WKScriptMessageHandler / 引用 WebViewJavascriptBridge 都行）
- 网络层（URLSession / Alamofire / AFNetworking）
- 状态管理、并发模型、命名风格、SDK 选型
- 启动时序内部并发优化、解压实现、零拷贝优化
- NSNotification 内部模块通讯命名（不是 H5 可见）
- 监控、日志、CI 选型

**判断某项是不是契约的方法**：问"H5 代码能不能感知到？"。能感知 → 契约；不能感知 → 自由。

---

## 验收原则

新外壳完成后，**在不修改 H5 任何一行代码**的前提下，逐项跑 `docs/H5-Native-Contract.md` §10 的 26 项验收清单。任意一项 H5 表现与现网不一致即视为契约违反，需要排查；表现一致即视为通过，原生内部实现自由。

---

## 提交规范

- 提交信息使用中文短句，描述"做了什么 + 为什么"
- 不在未授权情况下执行 `git push`、`git reset --hard`、`git rebase`、`git push --force` 等不可逆操作
- 涉及桥接接口名 / 参数字段名 / 数据结构的改动，提交信息必须明示 "契约影响" 并附 `docs/H5-Native-Contract.md` 对应章节
- **及时提交，无需每次确认**：改动通过最小验证（BuildProject 通过 / 关键路径手测通过 / 单测通过）后，可直接 `git commit`，不必每次先征求用户同意。
- **commit 颗粒按情况自行判断**：
  - 多个改动彼此相关、属于同一目标的，可合并为一个 commit
  - 多个改动彼此完全无关、合并后 git diff 失去可读性的，再拆开
  - 桥接契约改动 / 不可逆的结构性变更必须独立 commit，便于回滚定位
  - 不强制"按职责拆"——合并 vs 拆分以"未来回看时能不能看懂、能不能二分回滚"为准
- commit message 直接写"做了什么 + 为什么"，无需事先把建议 message 给用户审一遍。用户若不满意会主动反馈，再 amend / 重提即可

---

## 进度同步规范

**每完成一个 Phase 子项（如 1.10 / 1.11 / 1.12 ...）必须立即更新 `docs/Development-Plan.md` 的进度标记**，不允许"代码提交但 Plan 未同步"的滞后。具体：

- Plan §5 各 Phase 任务清单：完成的子项把 `- [ ]` 改 `- [x]`，附简短结果或耗时
- Plan §8 进度追踪 checklist：同步勾选 + 简短补充（如算法名 / 端到端测试结果）
- 时机：通常与该子项的 `git commit` 同一 commit 内完成，commit message 末尾备注"Plan 进度已勾选"
- 若改动跨多个 Phase 子项（极少见），逐项勾选不能漏
- 当任务清单中的描述与最终实现有差异（如新增子任务、合并子任务、改变实现策略）时，**同步修改任务描述**，不允许 Plan 文档与代码长期不一致

理由：
- Plan 是项目记忆的唯一权威进度视图，新人 clone 后只看 Plan §8 就能立刻知道整体进度
- 若代码已完成但 Plan 未勾选，新人会误判项目状态、做重复工作
- 若 Plan 描述与代码实现不一致，未来 Phase 验收 / 回归测试时会基于错误前提
- 与 commit message 双写互为校验：commit log 是细节，Plan 是全局视角
