Files
youle_app_ios_v2/CLAUDE.md
T
joywayerandClaude Opus 4.7 b2ee1b38eb 新增 Verification-Checklist 功能验证清单 + Phase 1-2 验证步骤
按用户指示:功能验证写到专门的文档用来说明指导,等功能开发完毕统一
验证,不在 Phase 进行中频繁跑(避免反复重装 app 浪费时间)。

新增 docs/Verification-Checklist.md:
  - 文档定位:开发期累积 / 统一验收的操作手册,与 Contract §10
    契约边界保持独立
  - 通用准备:环境工具(Xcode console / Safari Web Inspector / 模拟器
    Features 菜单)+ 重装 app 清 launch snapshot + xcrun simctl
    get_app_container 沙盒访问
  - Phase 1 章节(已实现):
    A. 启动图 + LaunchScreen 方向(3 项)
    B. 启动流水线时序(含 Xcode console 期望输出 + 黑边 + 总耗时)
    C. app_*.js 落盘 + 命名(含 cat 命令)— 6 项硬约束
    D. window.app_* H5 console 对账(15 项变量 + 撤销变量验证)
  - Phase 2 章节(已实现):
    E. H5→Native handler 响应(13 项验证步骤)
    F. Native→H5 反向 callback(5 项触发场景含模拟器手动操作)
    G. ExternalSubscriptions 生命周期(Phase 6 才能完整验证)
  - Phase 3-10 占位章节:实施后陆续填充
  - 附录:与 Contract §10 26 项验收清单的章节映射表
  - 维护规则:每完成 Phase 子项补勾、不复制到 Contract §10、bug 修复
    时在 commit message 引用本文档 checkbox 编号

CLAUDE.md 文档索引表新增一行指向 Verification-Checklist.md。

Plan §6.4 里程碑加 3 行(前面遗漏的 4986f36 / 09f071e + 本次 commit)。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 19:08:32 +08:00

25 KiB
Raw Blame History

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-CMRC,未启用 ARC),最低 iOS 9.0;原生外壳 + 内置 H5gamehall.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.plistdaoqi/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
    • 发现 1Info.plist 用的是顶层无后缀的 UISupportedInterfaceOrientations(值 = landscape),没有 ~iphone / ~ipad 后缀变体
    • 发现 2:根本没有 UILaunchStoryboardName,用的是 ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = "LaunchImage-1"iOS 8 时代的 LaunchImage Asset Catalog 机制)
  • 原项目为什么稳
    • 它的所有 LaunchImage PNGDefault-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 = landscapeXcode 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 sandboxLibrary/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 完全不必要
    • 发现 3Contract §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_getbatteryapp_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 → initJSdataloadFileURL 之前)用 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 累积后统一验证
../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 手动:所有闭源二进制(微信 / QQ / 高德 AMap / opencore-amr 等),按 Design §14.1「Vendor 接入标准流程」放 Vendor/<SDKName>/ + Target → General → Frameworks 加入 + Vendor/<SDKName>/README.md 记录版本与来源
  • 禁止 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 不推荐"的写法(如 loadFileURLwriteToFile 写 .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 回包用小写 iH5 调用方是 getphoneInfo 大写 I
    • appservice 用字符串 "1"(后台)/ "2"(前台),不是数字
    • getlocationinfolatitude / longitudestring 而不是 numberstringWithFormat:@"%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. 空实现接口也必须保留注册

当前现网中 opensaomagetGameplay 等 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 pushgit reset --hardgit rebasegit push --force 等不可逆操作
  • 涉及桥接接口名 / 参数字段名 / 数据结构的改动,提交信息必须明示 "契约影响" 并附 docs/H5-Native-Contract.md 对应章节
  • 工作树有多个不相关改动时,按职责拆 commit,不混提
  • 及时提交:每完成一个可独立验证的工作单元(一项 bug 修复 / 一处配置变更 / 一次文档更新 / 一次依赖升级 / 一个 M 里程碑的子项),就应在工作树干净时主动提议提交。理由:
    • 项目处于 greenfield 重写阶段,commit 颗粒越小越容易在出错时二分定位 / 回滚
    • 长时间不提交容易让 pbxproj / Info.plist / SDK 二进制等结构化文件互相耦合,git diff 失去可读性
    • CLAUDE.md / docs 与 git log 一起构成项目记忆——commit message 是"为什么"的第一现场,比事后补文档更可信
  • 主动提议提交的时机判定:
    • 改动已通过最小验证(BuildProject 通过 / 关键路径手测通过 / 单测通过)→ 立即提议
    • 工作树还有其它无关待提交内容 → 先帮用户分类、按职责拆 commit,不混提
    • 用户未明确同意前不直接执行 git commit,但应当清晰地告诉用户"现在适合提交了",并附上建议的 commit message

进度同步规范

每完成一个 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 是全局视角