---
name: "skill_code_add_qcom_icd"
description: "在 modem_proc_ext 中新增或维护 Qualcomm ICD 在线解析。用于把离线 QXDM/QCAT 语义迁移到 bytedance_modem_service。"
version: "0.5.17"
---

# 新增 Qualcomm ICD 在线解析

本技能用于在 `modem_proc_ext/bytedance_modem_service` 中新增或维护一个 Qualcomm ICD 的在线解析能力。

适用场景：
- 用户要把 `0xB884`、`0xB883`、`0xB887`、`0xB16E`、`0xB16F` 等离线 QCAT/QXDM 解析迁移到 modem 侧在线解析。
- 用户要确认某个 ICD 是否已经在线支持、归哪个模块、应该改哪些文件。
- 用户要排查“mdlog 里有包，但 online parser 没出结果 / 不上报 / 一改就 crash”的问题。

## 语言策略

所有面向用户的报告、分析结论、review 说明、run artifact 解释文字，以及 `references/` 下新增或更新的 Markdown 文档，默认必须使用中文。

只在以下内容中保留英文原文：
- C 代码、代码注释、宏名、结构体名、函数名、文件名、路径、命令行参数。
- Qualcomm/QCAT/QXDM 原始字段名、协议枚举、log 原文、parser view 原文、hex dump 原文。
- `locked by replay/pdf`、`corpus-bound inference`、`uncovered / unresolved` 等既有分类标签如果已经用于脚本或历史表格对齐，可以保留；但周边解释、结论和风险说明必须写中文。

生成或更新 `references/<log_id>/runs/<log_id>_semantic_vs_packed_struct_table.md` 时：
- 表头优先使用中文，例如“字段/偏移/长度/来源证据/结论/风险”。
- C struct 字段名、wire 字段名、bit 名、单位、枚举值必须保持原文，避免破坏协议可追溯性。
- 字段映射表必须显式包含“取值/单位/枚举语义”信息；如果某字段是 enum/boolean/bitmask/sentinel/scale/count/timer/dBm/ARFCN/RB/byte 等语义，必须在独立列或等价说明中写清，不能只给出 C 类型和字段名。
- 如果当前 corpus 只能锁定部分枚举值，必须写成 `当前 corpus locks ...; other values uncovered` 这类边界说明，不能把未覆盖枚举补全成看似完整的协议表。
- 每个表格后面的收敛结论、未锁定风险、下一步补料要求必须使用中文。

除非用户明确要求英文报告，不要默认把分析文档写成英文。

开始前先读 `references/modem_data_registry/` 下的分层登记表：
- `ICD_parsed.csv`：已经解析并沉淀的 ICD 参数。
- `ICD_under_parse.csv`：已经进入解析流程、相当于已完成初步 review 的 ICD 参数。
- `ICD_under_review.csv`：本轮新增待评审的解析/上报参数。

这三张表是判断下面三件事的登记入口：
1. 这个 ICD 是否已经在线支持或已经进入解析流程。
2. 它归 `rf / data / network / call` 哪个模块。
3. 它最终落到 `rf / power / dataKpi / network / call` 哪个输出 owner。

## 一、Source Of Truth

新增 ICD 时，真正的 source of truth 只有三个：
1. Qualcomm ICD / QCAT 结构定义。
2. 同版本的 `QCAT parser + hex dump`。
3. 代码实际 dispatch 路径。

不要把下面这些当成 source of truth：
- 旧注释
- 旧分支里的命名
- CSV 列名
- “看起来差不多”的历史实现

判断“某 ICD 已经在线支持”的唯一标准是：
- 它出现在 `bytedance_diag_log.c` 的 `log_id[]` / `event_id[]` 中；
- 且在 `diag_listener_log_ext_cb()` / `diag_listener_event_ext_cb()` 中真正 dispatch 到 handler。

只在 `.h` 里定义了宏，不算在线支持。

如果某个 ICD 已经在线支持，默认不要新开一条并行实现路径，而是先走“已有 ICD 维护/扩展”流程：
- 先读 `references/modem_data_registry/ICD_parsed.csv`、`ICD_under_parse.csv` 和 `ICD_under_review.csv`，确认当前 owner 模块、dispatch 入口和输出 owner。
- 优先在现有 owner 模块里扩展：补新 version、补字段、补边界检查、补聚合逻辑，保持单一 source of truth。
- 不要因为新增一个 version 或少量字段，就复制一份新的 parser、owner 状态或 protobuf 路径。
- 只有当现有 owner 明显错误，或输出契约发生实质变化时，才允许调整 owner/module 放置；这种调整必须同步更新 registry 和 run artifact。

换句话说：
- `log_id[]/event_id[] + dispatch` 用来判断“它是不是已经在线”；
- `modem_data_registry 分层表 + 现有 owner 模块` 用来决定“它应该按维护/扩展来做，而不是重起炉灶”。

## 二、Worktree 纪律

本项目必须区分“附加开发 worktree”和“主编译验证 worktree”。

任何 ICD 改动前，先执行：
1. `git worktree list --porcelain`
2. `realpath <target-path>`
3. `git -C <target-path> rev-parse --show-toplevel`
4. `git -C <target-path> rev-parse --abbrev-ref HEAD`
5. `git -C <target-path> rev-parse HEAD`

硬规则：
- 代码修改只落在附加开发 worktree。
- 主 worktree 不直接手改源码。
- 要同步到主 worktree，必须先在附加 worktree 提交。
- 然后主 worktree 执行：`git reset --hard <attached-worktree-commit>`
- 同步后必须再次核对：`HEAD`、`git status --short`、目标文件内容、版本标记。
- `boot marker` 不能只改 `date` 或只改 `worktree`；只要 payload 有变化，就必须同步更新 `BD_DIAG_BOOT_MARKER_VERSION`。
- `BD_DIAG_BOOT_MARKER_VERSION` 必须至少包含：`semantic_tag + payload_short_sha`，例如 `icd_add_lte_pdsch_0xB173_online_parser_g9336ef4`。
- 同一天内多次修改、同一 worktree 内多次 build，禁止仅靠 `BD_DIAG_BOOT_MARKER_DATE` 区分版本。

## 三、通用 ICD Struct 反解析与收敛规则

### 通用反解析迭代硬规则（LTE / NR）

对任何 ICD log，不允许“看一眼 hex dump 就开始写代码”。必须执行反解析迭代，直到当前资料下无法进一步加强，再决定是否进入代码阶段。

固定循环：
1. 先锁语义结构：
   - LTE 优先读官方 ICD PDF / 摘录；
   - NR 优先读 `semantic structure`、离线 parser 输出、现有结构文档。
2. 用同版本 `QCAT parser + hex dump` 回放字段值和字节边界。
3. 更新 `references/<log_id>/runs/<log_id>_semantic_vs_packed_struct_table.md`。
4. 把字段分成三类：
   - `locked by replay/pdf`
   - `corpus-bound inference`
   - `uncovered / unresolved`
5. 回看现有样本是否还能继续加强：
   - 能加强，就继续补样本、补 replay、补字段映射。
   - 不能加强，才允许进入“struct 收敛评估”。

至少要反复检查这些层次，而不是只盯一个目标字段：
- top header
- record 固定头
- fixed child block / tb / carrier
- count / length / tail / reserved 区域
- 目标输出字段之外、但会影响步进和边界检查的结构字段

硬规则：
- `runs/<log_id>_semantic_vs_packed_struct_table.md` 是 C struct 的中间真值文档，不是事后补材料。
- 每次重要结论变化后，都要先更新 run artifact，再更新 `.h/.c`。
- 如果 hex dump 还能继续验证更多字段，就不能把“当前够写代码了”当作收口理由。

### 通用 Struct 收敛门槛与停机规则

在下面条件满足前，禁止开始写 parser 代码、getter macro 或在线聚合逻辑：
- 顶层 header、record 边界、变长规则已经锁定
- 会影响长度检查和 pointer walking 的字段已经锁定
- 当前输出依赖的关键字段已经锁定
- `runs/<log_id>_semantic_vs_packed_struct_table.md` 已明确写出 locked / corpus-bound / unresolved 边界

如果任一条件不满足，必须暂停编码，并明确提醒用户补资料。不能边猜边写。

必须主动向用户索要的典型缺口：
- 缺同版本 `QCAT parser + hex dump`
- 缺覆盖不同 record / carrier / tb 形态的样本
- 缺目标 version 的 PDF 定义或有效摘录
- 现有样本无法锁定 top header / record 步进 / tail 语义

允许进入代码阶段的标准不是“已经有一个大概 struct”，而是“在当前输入条件下，C struct 文档已经无法进一步加强”。

### 输入不全时的强制停机规则

当用户只给出 `log ID`、目标字段、CSV 列名、离线抽取逻辑、log mask、口头描述，或者只说“参考某个离线工具处理方式”时，这些信息只能用于确认目标输出语义，不能作为 wire layout 的 source of truth。

如果本 skill 的 `references/` 下还没有该 ICD 对应的独立子目录，第一步不是继续猜结构，也不是先写代码，而是先在 `references/<log_id>/input/` 创建该 log 的资料落点和补料模板。

如果本 skill 的 `references/<log_id>/` 下没有该 ICD 的独立 `input/` 和 `runs/` artifact，且用户也没有提供同版本 `QCAT parser + hex dump` 或同等结构证据，必须停止在资料收集阶段。禁止继续做下面这些事：
- 新增 log id 注册或 dispatch。
- 新增 handler 空框架。
- 新增 proto 字段或聚合状态。
- 编写 packed struct、getter macro、pointer walking 代码。
- 用“先准备框架，后续补 struct”作为继续编码理由。

这种场景下，必须明确告诉用户：
1. 当前已经确认的事实，例如 log id、离线字段名、目标输出语义、现有 mask 是否存在。
2. 当前缺少的最小输入资料。
3. 用户应该如何提供资料。
4. 可以参考哪些已有 ICD 的输入文件格式。
5. 在资料补齐前，本轮不会修改业务代码。

这种场景下还必须额外完成两件事：
1. 在 `references/` 下仿照已有 log 目录创建该 ICD 的子目录。
2. 在 `references/<log_id>/input/` 放入一个补料说明文件，明确用户应放入哪些 parser + hex dump 样例、语义资料、PDF 摘录或复现命令。

推荐回复模板：

```text
当前不能进入代码实现阶段。

已确认：
- Log ID: <0xXXXX>
- 目标输出: <field list>
- 本地已有资料: <mask/offline extractor/registry result>

缺少进入 struct 收敛的必要资料：
- 同版本 QCAT parser view + hex dump，必须包含 Version、Num Records、Record/Report 树形字段和值、原始 hex bytes。
- 至少一组能覆盖目标字段的真实样本；如果该 ICD 有多 record / 多 carrier / 多 report / optional block，最好提供多组样本。
- 如果是 LTE，优先补官方 ICD PDF 摘录；如果是 NR，至少补 semantic structure 或 QCAT 树形输出。

请按以下任一方式提供：
- 直接提供 QCAT 导出的 parser view with hex dump 文本。
- 提供 mdlog + 当前 QCAT/parser 工具可复现命令，让我生成 parser view 和 hex dump。
- 参考 `<skill_dir>/references/0xB884/input/0xB884_Qcat_parser_with_hex_dump.md`
  或 `<skill_dir>/references/0xB16E/input/0xB16E_parser_view_with_hex_dump.txt`
  的格式补齐资料。

在这些资料补齐前，我不会新增 handler、proto、聚合器或 dispatch，避免伪造未验证的 wire layout。
```

允许继续做的只有只读动作：
- 查 registry，判断是否已经在线支持。
- 查本地 reference，判断是否已有 input/run artifact。
- 查离线工具，确认目标字段语义和输出聚合方式。
- 给出补料清单和参考样例路径。

## 四、获取 LTE ICD Struct 的方法

LTE ICD 的优先方法是：**先看官方 ICD 文档，再用 parser+hex 回放验证。**

首选参考：
- `resources/80-VP457-6_REV_BA_Serial_Interface_Control_Document__ICD__for_Long_Term_Evolution__LTE_.pdf`

本 skill 已有的 LTE 样例：
- `references/0xB16E/input/0xB16E_parser_view_with_hex_dump.txt`
- `references/0xB16E/runs/0xB16E_semantic_vs_packed_struct_table.md`
- `references/0xB16F_LTE_PUCCH/input/parser_with_hex_dump.txt`

推荐步骤：
1. 先在 LTE ICD PDF 中定位目标 `log ID` 和 `version`。
2. 找到该 log 的顶层 header、record 结构、变长数组定义、字段位宽和单位。
3. 只先锁定外层结构：header、record、固定子块。
4. 再用同版本 `QCAT parser + hex dump` 对照字段偏移和 bit range。
5. 最后把已经验证过的字段落成 packed struct 和 getter macro。

注意：
- LTE 文档能回答“字段语义”和“理想结构”，但最终仍要以真实 `parser + hex dump` 校验字节边界。
- 如果 PDF 结构与真实抓包不一致，优先信真实同版本 `parser + hex dump`，同时在 run artifact 里记录差异。
- 对 flexible array，固定头长度用 `offsetof(struct_type, records)`，不要直接拿 `sizeof(struct_type)` 当整包长度。

### LTE 资料沉淀硬规则

只要当前 LTE ICD 已经有可用 PDF / 截图 / 摘录资料，就不能只“脑内参考”后直接写代码，必须先把有效信息沉淀到该 log 自己的 `references/<log_id>/input/`。

最低要求：
- 原始 `QCAT parser + hex dump`
- 当前任务依赖的 LTE ICD PDF 有效摘录
- 若 PDF 中同一 log 有多个 version，必须写清本轮实际使用的 version

推荐文件：
- `references/<log_id>/input/<log_id>_lte_icd_excerpt.md`
- `references/<log_id>/input/<log_id>_parser_view_with_hex_dump.txt`

摘录时至少保留：
- top header
- record / sub-block 结构
- 变长数组规则
- 字段位宽、单位、枚举语义
- 与真实样本不一致的可疑点

禁止：
- 已经读过 PDF，但不把有效信息落到 `input/`
- 只在对话里描述 PDF 结论，不留下可追溯输入
- 把 PDF 结论直接抄成 C struct，却没有在 `input/` 中留下出处

## 五、获取 NR ICD Struct 的方法

NR ICD 通常没有像 LTE 那样稳定可依赖的公开 ICD PDF，所以方法必须换成：**先锁语义，再做 wire inference。**

注意：
- NR 与 LTE 一样，必须遵守上面的“通用反解析迭代硬规则（LTE / NR）”和“通用 Struct 收敛门槛与停机规则”。
- 差异只在于输入证据的优先级不同，不在于是否需要反复 replay / 更新 run artifact / 等待 struct 收敛后再写代码。

本 skill 里 NR 的主要参考：
- `references/0xB884/input/0xB884_semantic_structure.md`
- `references/0xB884/input/0xB884_Qcat_parser_with_hex_dump.md`
- `references/0xB884/input/0xB884_wire_inference_from_qcat_hex.md`
- `references/0xB883/input/0xB883_semantic_structure.md`
- `references/0xB887/input/0xB887_semantic_structure.md`
- `references/qcat_nr_log_struct_local.md`
- `references/qcat_nr_log_struct_assets/`
- `references/0xB883/runs/0xB883_semantic_vs_packed_struct_table.md`
- `references/0xB884/runs/0xB884_semantic_vs_packed_struct_table.md`
- `references/0xB887/runs/0xB887_semantic_vs_packed_struct_table.md`

### NR struct 获取的输入要求

最低要求：
- `log ID`
- `version`
- 至少一组同版本 `QCAT parser + hex dump`

最好同时有：
- `semantic structure` 文档或截图
- 离线 parser 输出
- 多组覆盖不同 record/carrier/channel 变体的样本

### NR struct 获取步骤

1. 先锁语义骨架
   明确：
   - 顶层有哪些 record
   - 每个 record 下有哪些 carrier/channel/block
   - 哪些字段是最终输出需要的
   - 哪些字段只是结构性字段

2. 再锁长度规则
   必须先搞清楚：
   - top header 长度
   - record 固定头长度
   - carrier 固定头长度
   - channel/block 长度
   - 哪些块是 optional / repeated / version-gated

3. 只先写外层 packed struct
   对已经稳定的部分写成：
   - 顶层 header
   - 固定 record header
   - 固定 carrier header
   - 固定 block header

4. 内层不稳定字段先保留为 `raw[N]`
   条件是：
   - 只知道块边界
   - 还没锁定 bit range
   - 样本还不足以证明字段位置

5. 用 hex replay 锁字段
   每个字段都要过同一闭环：
   - 猜测 bit range / byte range
   - 用 hex 回放数值
   - 和 QCAT parser 真值比较
   - 同时回放前后邻接字段，确认候选 bit range 没有吞掉或挤压相邻字段
   - 至少在当前样本范围内对得上才升级为“已锁定字段”
   - 如果扩展候选位宽只因为新增 bit 在当前 corpus 中恒为 `0` 而数值仍匹配，不能据此扩大字段位宽；必须证明新增 bit 不属于后续字段，且扩宽后相邻字段仍能独立回放

6. 已锁定字段再变成 getter macro
   保持两层分离：
   - packed struct 管布局
   - getter macro / shift-mask helper 管字段提取

### NR struct 的硬规则

- 不要直接把 QCAT 树形语义抄成 C struct。
- 不要只用一张截图就宣布结构已锁定。
- 不要为了“代码看起来完整”把未证实字段硬塞进 compiler bitfield。
- 对多层 NR log，先把块边界搞清楚，再锁位域；不要反过来。
- 只有当 byte boundary、bit range、样本回放都稳定后，才把 `raw[N]` 进一步拆成 nested packed struct。

### NR signature / bitmask 分类硬规则

当 NR log 里出现类似 `signature / bitmask / report id / channel id` 的字段时，禁止默认把完整数值当作固定 magic 精确匹配。

必须先判断它真实承担的角色：
- 完整 magic：完整值稳定对应一个唯一结构。
- family classifier：只有部分 bit 决定 report/channel family。
- subfamily / flags：低位或高位只影响附加语义，不改变 block layout。
- quantity bitmask：多 bit 组合决定字段集合或可选块。

实现前必须做三步验证：
1. 在 run artifact 中列出已知样本值，并用 `AND / OR / XOR / bit position` 对比推导稳定 mask。
2. 用 QCAT 语义树确认该 mask 对应的是结构 family、字段 quantity，还是单纯 flag。
3. 用在线 `ERROR_MSG.csv` 中的 unsupported 值反向回放，确认新分类不会把本应可解析的数据挡掉。

实现规则：
- 只有已证明完整值稳定决定结构时，才允许 `signature == 0x...`。
- 如果稳定部分是 family，必须定义 `*_MASK / *_FAMILY / *_SUBFAMILY`，用 mask 分类。
- 对已知 family 但 metric offset 未锁定的 subfamily，可以只按已锁定 block size 跳过，不得强行聚合字段。
- 对真正未知 family，必须保留明确 error，并打印原始值和 family/mask，便于下一轮补资料。
- 修复 unsupported signature 后，必须把“原始值 -> family/subfamily -> 处理动作”的表写回 `references/<log_id>/runs/<log_id>_semantic_vs_packed_struct_table.md`。

典型反例：
- `0xB883` 的 channel signature 低位可能变化，真实 block owner 是 `0x0800/0x1000/0x2000/0x4000` channel mask。
- `0xB8A7` 的 report signature 高位会变化，真实 report family 可由 `0x00FFF000` / `0x00FFFF00` 等 mask 分类。

## 六、ICD 代码的实现方法

### 1. 先选 owner 模块

按最终输出 owner 选模块：
- `power / rf` -> `bytedance_diag_rf_log.[ch]`
- `dataKpi` -> `bytedance_diag_data_log.[ch]`
- `network` -> `bytedance_diag_network_log.[ch]`
- `call` -> `bytedance_diag_call_log.[ch]`

不要把新 parser 扔进 `common`。

### 2. 新增一个 ICD 时必须改哪些文件

#### 注册与 dispatch

文件：
- `bytedance_modem_service/src/bytedance_diag_log.c`

通常要改：
- `log_id[]` 或 `event_id[]`
- `diag_listener_log_ext_cb()` 或 `diag_listener_event_ext_cb()`

你要做的事：
- 把 log/event id 注册到监听列表。
- 在 callback 里把它 dispatch 到正确 handler。
- 如果一个 ICD 要同时喂多个 owner，明确在这里 fan-out，不要分散到多个模块里偷偷处理。
- 把 `log_id[] / event_id[]` 当成在线链路的订阅白名单看待：只写 handler 或只写 `switch case` 不够；没进监听列表，通常就不会收到在线包。
- `bytedance_diag_log.c` 只允许做订阅和 dispatch。禁止在这里新增本地 `BD_LOG_*` / `BD_EVENT_ID_*` 宏、handler `extern` 声明、packet layout 常量或 owner 私有常量；这些定义必须回到对应 owner header。

#### ICD 宏、版本、结构体、函数声明

文件：
- `bytedance_modem_service/src/bytedance_diag_<owner>_log.h`

通常要改：
- log id 宏
- version 枚举
- packed struct
- getter macro / 位域辅助宏
- handler 声明

你要做的事：
- 定义 log id / event id。新增 log/event id 宏必须放在 `bytedance_diag_<owner>_log.h` 的对应 define area，不能为了减少 header diff 或临时让 dispatch 编译通过而放到 `bytedance_diag_log.c`。
- 定义 version 常量。
- 定义顶层 header、record、carrier、sub-block 的 packed struct。
- 对位域或不稳定字段，用 getter macro，不要把不确定布局硬塞成看似完整的 bitfield。
- 对外暴露 `handle_xxx()` 声明。

#### 真正的解析逻辑

文件：
- `bytedance_modem_service/src/bytedance_diag_<owner>_log.c`

通常要增加的函数：
- `handle_<log_name>()`
- `handle_<log_name>_v<version>()`
- 必要时的 `record_xxx()` / `update_xxx()` / `calc_xxx()` helper

你要做的事：
- 在入口函数里做 `NULL / len / slotId / version / num_records` 校验。
- 按版本分发到 `handle_xxx_v<version>()`。
- 在版本函数里做边界检查：header、record、carrier、channel、变长数组。
- 把原始字段转换成 service 侧需要的状态或聚合值。
- 更新唯一 owner 的状态，不要让多个地方重复维护同一份状态。

#### 通用参考风格约束

后续使用本 skill 新增或维护任意 ICD 在线解析时，必须把 `0xB884 / 0xB883 / 0xB887` 作为当前代码生成风格样板。不能只按本 skill 的通用模板重新生成一套“看起来更规整”、但与现有参考实现风格割裂的代码。

通用风格要求：
- 先学习参考提交的代码组织，再决定新增代码落点；优先沿用目标 owner 文件已有的 section、helper、accumulator 和日志分层。
- 入口函数保持 `data NULL -> slotId -> header_len -> num_records -> version switch -> v<version>()` 的分层。
- 版本函数用 `ptr / data_end` 做 pointer walking，每次 dereference 前先检查边界。
- 变长 record 必须按 wire contract 计算步进，不能假设 record 内只有一个固定子块。
- 高频 record 级日志只能放在 `DBG_LOGE_n` 或专用 `DBG_<ICD>_LOG`；`BD_MSG_HIGH` 只用于 1s summary、配置变化或低频收敛结论。
- 如果新增 ICD 需要聚合输出，优先使用单一 owner 的 accumulator；不要在多个模块重复维护同一份状态。
- 如果新增 ICD 需要输出到已有 owner，必须先复用该 owner 的既有 tick/timer/slot indication 发送链；禁止为单个 owner 私自新增 `qmi_diag_log_send_*` 这类旁路编码和发送 helper。
- 如果参考实现和通用模板冲突，以参考实现风格为准；只有输入事实、输出契约或边界检查要求不同，才允许偏离，并必须写入 run artifact。

内置参考资料：
- `0xB884`：以 `references/0xB884/runs/0xB884_semantic_vs_packed_struct_table.md` 中沉淀的功能、wire layout、线性功率均值和代码风格为基线。
- `0xB883`：以 `references/0xB883/runs/0xB883_semantic_vs_packed_struct_table.md` 中沉淀的 channel signature 分类、pointer walking 和边界检查风格为基线。
- `0xB887`：以 `references/0xB887/runs/0xB887_semantic_vs_packed_struct_table.md` 中沉淀的变长 record 步进、聚合和边界检查风格为基线。

分支集成约束：
- 本 skill 必须自洽；使用者不应依赖特定本机 worktree、私有分支名或历史 commit 才能理解并执行代码生成规则。
- 如果当前项目有自己的 ref/dev 分支或目标 rebase 分支，修改前记录两边的 `merge-base`，并确认目标文件在两边的同源改动范围。
- 优先做能在后续集成中自然贴合的最小补丁：只改必要字段、算法和 guard，不为了“生成风格统一”重排已有代码块。
- 维护目标分支已经存在的 handler 时，必须先把目标分支的当前函数体当作 rebase baseline。除非已经证明原函数的状态机边界、pointer walking 或 packet layout owner 本身错误，否则禁止用参考分支/文档里的“更完整实现”整函数替换。
- 如果参考实现需要迁入的只是一个字段 getter、一个 guard、一个 summary accumulator 或一个 unsupported signature 兼容规则，补丁必须落在现有最小语句块内；不得删除已有 `record -> carrier -> channel` 分层、已有 case 分支、已有边界检查和已有日志证据链。
- 对 `handle_nr5g_mac_ul_physical_channel_schedule_v3001B` 这类已经在目标分支承载 B883 UL tput/bandwidth 的函数，默认只允许追加 PUSCH 字段修正、summary 计数或局部 channel mask 兼容；不得为了套用 `BD_B883_*_raw` 参考模型而删除 `7fc22cd` 风格的大段 carrier/channel 解析代码。
- 生成或修改补丁后必须用 `git diff --function-context <baseline> HEAD -- <file>` 检查目标 handler：如果 diff 呈现整函数删除/重写，而需求只是维护已有 ICD，必须回退生成策略，重新收敛为局部 patch。
- 如果目标分支已经在同一函数里扩展了更多输出契约，允许保留扩展，但基础 parser 口径必须对齐本 skill 的内置参考资料；差异原因要写入 run artifact。
- 提交前至少用 `git diff` 和 `git merge-tree` 或等价 dry-run 检查目标文件的潜在集成冲突；如果存在冲突，必须说明冲突块和建议解决方式。
- 对已经在线支持的 log，先做目标 ref/dev 的同源差异检查；如果目标 ref 已经包含相同 log id、proto 字段、handler、accumulator 或发送链，后续补丁只能维护/修正这些既有入口，不能复制一套并行实现。
- 对需要集成到目标 ref 的开发提交，提交前必须做 `cherry-pick --no-commit`、`git apply --check` 或等价 dry-run；若 dry-run 冲突，优先回到生成策略收敛，减少大块重排和重复路径，而不是只记录手工解冲突步骤。

`0xB884` 的特定语义规则：
- wire layout 必须保持 `top header -> record -> carrier -> power_info[16B]` 的 pointer walking 形态。
- `BD_DBM_MIN / BD_DBM_MAX / g_dbm_to_linear_table` 这类 dBm 线性化口径必须保留；`PUSCH / PUCCH / SRS` 的 `tx power / mtpl` 窗口均值必须按 `dBm x10 -> linear -> average -> dBm x10` 的方式计算，不能退化成 x10 dBm 的算术平均。
- 如果后续为了离线一致性新增 `TPC / pathloss / slot count` 等字段，可以在同一个 channel accumulator 中扩展，但不能改变 `tx power / mtpl` 的线性均值口径。
- `0xB884` 的新增字段、summary 日志和 run artifact 必须明确写出：哪些字段来自内置参考资料已锁定解析，哪些字段是基于当前离线 contract 的扩展。

`0xB883 / 0xB887` 的特定语义规则：
- `0xB883` 的 channel signature 必须按 `BD_B883_CHAN_MASK_*` 分类；低位 flag 不得参与完整 magic 匹配，避免挡掉 `0x0810 / 0x0820 / 0x1010 / 0x2010 / 0x4010` 这类合法组合。
- `0xB883` 的组合 channel 只能在 input/replay 已经证明存在且 block sequence/step size 已写入 run artifact 时生成。`PUSCH|SRS`、`PUCCH|SRS` 这类组合不能凭枚举名自动推导；已支持组合也只能按已锁定 block size 跳过或解析，不得复用 `SRS-only` 的变长规则。
- `0xB887` 必须按 `system_time + record_prefix + num_pdsch_status * status_block` 步进，不得假设一个 record 只有一个 status。

同步要求：
- 新增或维护任意 ICD 的代码时，必须同步更新对应 `references/<log_id>/runs/<log_id>_semantic_vs_packed_struct_table.md` 或 call-chain 文档。
- `references/` 下解释性文字必须使用中文；Qualcomm 字段名、宏名、代码片段和原始 parser 文本可保留英文。
- 如果与参考提交不一致，必须在 run artifact 中说明不一致的输入事实、输出契约或边界检查理由。

#### 代码布局与分区注释强制规则

新增或维护 ICD 时，必须先遵循目标 owner 文件的既有组织方式，而不是按生成代码的方便程度堆放。

`.h` / `.c` 的归属规则：
- `bytedance_diag_<owner>_log.h` 是 ICD wire layout 的默认归属地。
- 新增 `typedef struct` / `typedef union` / version enum / log id macro / getter macro / handler declaration，默认必须放在 `.h`。
- 新增 ICD 解析常量默认必须放在 `.h`：log id、event id、version、record/carrier/channel 上限、block size、array count、bit mask、shift、scale factor、getter macro。
- `BD_LOG_*` / `BD_EVENT_ID_*` 的 source of truth 是 owner header 的 define area；`bytedance_diag_log.c` 中的 `log_id[] / event_id[]` 和 `switch case` 只能引用这些宏，禁止在 `.c` 顶部临时定义。
- 只要 `.h` 里的 typedef、数组长度、getter macro、inline-like macro 依赖某个宏，该宏必须在 `.h` 中先定义；禁止让 `.h` 依赖 `.c` 中的宏。
- `.c` 只放真正的实现逻辑：`handle_xxx()`、`handle_xxx_v<version>()`、解析 helper、聚合 helper、owner 状态更新 helper。
- `.c` 中只允许保留实现私有宏，例如 `DBG_<ICD>` 调试开关、只在单个 helper 内部使用且不属于 ICD wire/layout 契约的局部实现常量。
- `.c` 只允许放 `.h` 不该暴露的私有运行态类型，例如只服务于本文件内部缓存、队列、聚合器、统计状态的 `typedef struct`。
- 如果想把 ICD packet layout 的 `typedef struct` 放进 `.c`，必须先证明既有 owner 文件就是这种风格；否则禁止。
- 不要因为某个新 ICD 的 struct 或宏很多，就把它塞进 `.c` 以减少 `.h` diff；review 可读性服从既有文件风格和单一布局 owner。

分区注释规则：
- 新增同一 ICD 或同一功能的一组代码，必须用既有 begin/end 注释块包住。
- 注释块命名要体现 ICD 或功能域，不要使用泛化大块名称。
- 新增多个 ICD 时，每个 ICD 或强相关功能组分别分区；禁止把 `B825 / B883 / B884 / B887 / B890 / LTE B16E/B16F/B173` 等不同行为堆在同一个大段里。
- 不要把新 helper 插到已有功能块内部，除非它只服务于该功能块，并且不会破坏原有块的边界。

推荐格式：

```c
/*----------------------------b884 nr tx power update start-------------------------------------*/
...
/*----------------------------b884 nr tx power update end---------------------------------------*/
```

如果目标文件已有类似：

```c
/*----------------------------fbrx data update start--------------------------------------------*/
...
/*----------------------------fbrx data update end----------------------------------------------*/
```

就必须跟随这种风格。提交前要用 `git diff` 检查：既有功能块没有被无关新逻辑穿插，新 ICD 的代码边界清晰可 review。

#### 输出协议

文件：
- `bytedance_modem_service/pb/BdDiagLog.proto`

什么时候要改：
- 现有 `rf / power / dataKpi / network / call` 字段装不下新结果。

什么时候不要改：
- 只是复用已有 `powerInfo / rf / dataKpi` 字段时，不要为了“看起来更完整”随便扩 proto。

#### extphone Proto 同步动作

只要修改了 `bytedance_modem_service/pb/BdDiagLog.proto`，必须检查是否需要同步 extphone 侧的 `BdDiagLog.proto`。同步时以 modem 侧本轮新增或变更的协议项为输入，在 extphone 现有文件上做最小补丁；禁止整文件覆盖、删除重建、顺手改历史命名或格式。

同步前先确认三件事：
1. modem 侧 `BdDiagLog.proto` 是当前 source of truth。
2. extphone 目标分支和 worktree 已确认，且目标文件没有无关未提交改动。
3. 两边 proto 的差异只允许来自 extphone 侧 Java proto 需要保留的 `java_package` / `java_outer_classname`，以及 modem 侧 C/nanopb 专用标注不能进入 extphone 的约束；不允许凭手感删字段、改字段号或改 message 结构。

同步规则：
- 新增字段和新增 message 必须与 modem 侧保持一致：字段名、字段号、`required / optional / repeated`、`default` 都不能变。
- extphone 侧必须保留 Java 生成选项，例如 `java_package` 和 `java_outer_classname`。
- modem 侧的 `nanopb.proto` import、`(nanopb).max_count`、`(nanopb).max_size` 属于 C/nanopb 编码约束；当前 extphone Java lite proto 不依赖这些约束时，同步补丁不得引入这些 nanopb 标注，避免 proto 编译失败。
- 不要在字段同步时顺手修正 extphone 侧已有 message/type 名称、历史拼写、格式空白或其它兼容残留；这类 rename 会改变生成 API，必须作为单独变更评估并同步更新调用点。

同步后至少检查：
1. `diff` 中没有丢失 modem 新增 message / 字段号。
2. extphone proto 中没有无法解析的 `nanopb` 扩展引用。
3. extphone 现有 Java 调用点仍能通过 message 名解析。
4. 如有构建环境，执行 extphone/QtiTelephony 对应构建；无环境时至少说明未构建的原因。

#### PB 设计与放置规则

先分清三个东西，再决定 proto 怎么设计：
- 参数语义：它是瞬时值、1 秒累计值、1 秒平均值、时间区间推导值、状态值、配置值，还是事件值。
- 上报触发：它是 parser/event 到达即应独立上报，还是写入缓存后等待下一次 timer/tick 统一发。
- 生命周期：它是长期连续刷新，还是“变化一次、报一次、随后清空”的稀疏事件。

把新字段放进已有 message 前，至少逐项确认下面五件事同时成立：
1. owner 相同。
2. 触发机制相同。
3. 聚合窗口相同。
4. 生命周期相同。
5. 消费者对它们的读取契约相同。

只要有一项不相同，就不要因为“字段还能塞进去”而强行复用原 message。

对于**不同 log 里出现的相似参数**，再额外强制检查下面四件事：
1. 它们是否真的是同一个语义 owner，而不只是“字段名相似 / UI 展示相似”。
2. 它们是否来自同一条 source-of-truth 路径，而不是一个 log 给 measurement snapshot、另一个 log 给 config/event。
3. 其中一个 log 是否只是“补充证据 / 辅助视角”，而不是应该抢占现有 owner。
4. 如果把它们写进同一个状态，是否会造成重复写同一份快照、重复触发 side effect、或形成双 source of truth。

硬规则：
- 不同 log 里都出现 `band / channel / pci / scs / cellId` 这类相似参数，不等于它们就应该共用同一个 parser 落点。
- 先看“这个 log 提供的是 measurement snapshot、cell snapshot、config snapshot、还是 state/event”。
- 如果现有 owner 已经稳定维护那份公共快照，新 log 默认不能再去重写它；新 log 只能补自己独有的语义，或进入独立事件 owner。
- 只有当你能证明旧 owner 缺失、错误，或者输出契约需要整体迁移时，才允许替换原 owner。

更适合新增独立 message / pb 结构的典型场景：
- 现有 message 是 `1s timer` 聚合输出，而新字段是变化触发的配置或事件。
- 现有 message 是连续功率/吞吐统计，而新字段是状态机事件、失败原因、配置快照。
- 新字段需要独立的清理/保留策略，不能跟随原 message 每个 tick 都覆盖。
- 新字段需要独立 fan-out、独立长度预算，或者可能高频变化，不适合跟其他聚合结果绑在同一 payload。

同一功能域后续还会扩到多个 RAT 时，优先采用：
- 一个功能总容器；
- RAT-specific 子 message；
- 不要把 LTE/NR 的字段平铺混在一个 message 里，也不要把同类语义拆成两个互不归类的散顶层字段。

例如：
- `cdrxInfo { lte, nr }`
- `rfInfo.cdrx.lte`
- `rfInfo.cdrx.nr`

这样既能复用同一功能域 owner/触发模型，又能保留 RAT 专属字段差异，例如 `NR scs / activeProcedure` 与 LTE 不同的 transition 字段。

判断 `required` / `optional` 时，不要只看“这个字段重不重要”，要看它在**每一条有效消息**里是否必然存在：
- 适合 `required`：
  - 该 message 的结构性骨架字段；
  - 该 message 只要出现，这个字段就必须有值；
  - 旧版/新版兼容风险可控，且不依赖“本次是否触发该语义”。
- 适合 `optional`：
  - 新增到现有 message 的字段；
  - 只在某些 version / 某些 RAT / 某些触发路径下才有值；
  - 不是每条 indication 都应带出；
  - 字段可能暂时只在部分 parser/owner 中落地。

保守规则：
- 对已有 message 追加新字段，默认先用 `optional`。
- 只有当该字段已经成为该 message 的稳定、必选契约时，才考虑 `required`。
- 不要用 `required` 强迫“没有语义值的路径”填假值。

以 `0xB890 NR5G CDRX` 为例：
- `drx_enable / on_duration / inactivity_timer / long_cycle` 本质是 config/state 变化，语义更接近 `RLF / RACH` 这类事件驱动输出；
- `inactive_ms` 才是按 1 秒窗口切分后得到的计算结果，适合跟 `powerInfo` 的 `1s rf tick` 对齐输出；
- 因此在做新设计时，优先考虑把 `CDRX config/event` 设计成独立 pb，再把 `inactive_ms` 这类 1 秒聚合结果接入 `powerInfo`；
- 如果因为兼容约束必须暂时复用已有 `powerInfo`，也必须把 config/event 和 1 秒 summary 的 owner、缓存、发送路径显式分开，不能共享一份“看起来方便”的状态载荷。
- 若同一功能后续还会扩到 LTE/NR 多 RAT，优先落成 `rfInfo.cdrx.{lte,nr}` 这类“总容器 + RAT-specific 子结构”，而不是继续往 `powerInfo` 里平铺一批 `nr5g_* / lte_*` 字段。
- 若当前字段仍处于开发阶段、尚未上线，且已确认 owner 设计错误，应直接迁到正确 message；不要为了照顾临时错误结构，把错误 owner 继续固化成“兼容包袱”。
- 以 `0xB825` 与 `0xB97F` 为例：两者都能看到一些 NR 小区相关参数，但 `B97F + CMAPI` 负责 `rf.cellInfo` 这类公共 cell snapshot；`B825` 更适合作为 `RRC config/event` owner，承载 `state / connectivityMode / standbyMode / active_cc/rb / drb bitmap` 等变化型配置语义。不要因为 `B825` 里也能看到 `PCI / ARFCN / Band / SCS`，就去重写 `rf.cellInfo`。

### 3. 参数上报机制设计与分类

在 struct 收敛、owner 确认、开始落代码之后，必须单独梳理一次“参数上报机制”。不要把“字段已经解析出来”直接等同于“应该这样上报”。

先按第一性原理给每个字段归类：
1. 原始输入是什么：瞬时采样、record 序列、状态变化、事件、配置。
2. 输出给消费者的是什么：瞬时值、最后值、求和、平均、比例、计数，还是区间计算结果。
3. 谁拥有状态：parser 临时变量、owner 模块缓存、1 秒聚合器、事件队列。
4. 什么时候发：每个 tick、每次事件、变化时、变化后入队再由 timer flush。
5. 发完是否清空：覆盖、保留、窗口滚动，还是一次性消费。

常见上报机制至少分成下面几类，并在设计前先对号入座：

#### A. timer 驱动的瞬时值/最后值上报

模式：
- parser 或其他模块先把最新状态写入 owner；
- 到 `tick/timer` 时直接取“当前最后值”发送；
- 不做 sum/avg，只是统一由 timer 节拍带出。

适用：
- 消费者只关心当前状态；
- 不要求保留秒内多次变化轨迹。

#### B. timer 驱动的 1 秒累加 / 平均上报

模式：
- parser 对每条样本更新累加器；
- 到 1 秒边界时输出 `sum / avg / count`。

注意先区分“测量值”和“位置标签 / 离散标签”：
- 对 `tput / mcs / rb / power / pathloss / bandwidth` 这类测量值，通常要做值的 `sum / avg`。
- 对 `slot_num` 这类字段，如果语义是“该类 record 落在哪个位置 / 类别标签”，则不应把字段值本身做数值求和，而应统计“该标签对应样本出现了多少次”。
- 换句话说，`count` 统计的是样本出现次数，不等于“字段数值求和”。

因此设计聚合器时，至少先回答：
- 这个字段是可加的测量量，还是不可加的标签量；
- 输出要的是值的总和/平均值，还是该标签在窗口内出现的次数。

典型例子：
- `powerInfo` 里的 `ul_tput_pusch / dl_tput_pdsch / ul_layers_pusch / dl_layers / dl_mcs / ul_mod_type / dl_mod_type / pusch_num_rb_avg / pdsch_num_rb_avg / ul_tx_num / dl_rx_num / ul_bandwidth / dl_bandwidth`；
- 各类 `power / tpc / pathloss` 的 1 秒数值聚合输出；
- `pusch_slot_num / pucch_slot_num / srs_slot_num` 这类位置标签字段，应按窗口内出现次数做计数汇总，而不是对字段值本身求和。

#### C. timer 驱动的“先计算、后汇总”上报

模式：
- 原始 record 不是现成可直接 sum/avg 的值；
- 先根据时间点、状态边界、协议约束把原始 record 还原成区间或派生量；
- 再把派生结果切到 1 秒窗口里做 summary。

典型例子：
- `0xB890 inactive_ms / inactive_pct`。

这类参数不能简单类比成“已有字段做 1 秒平均”，必须单独说明：
- 时间轴来源；
- 状态切换边界；
- 跨秒切分规则；
- 单次区间的协议上限/裁剪规则。

#### D. 事件驱动记录，timer 统一发送

模式：
- 事件到达时立即写 owner 状态或事件缓存，并标记 `isChanged`；
- 真正的 indication 仍由统一 timer/tick 线程发送；
- 发送后清空或按 owner 规则复位。

典型例子：
- 现有 `RLF`、部分 `network/dataKpi/call` 事件型字段。

适用：
- 需要复用统一 indication/timer owner；
- 但字段语义本质上是事件，而不是 1 秒连续聚合。

#### E. 变化触发记录，timer flush 发送

模式：
- 参数变化时立即记录一条变化事件；
- 不在变化线程直接做重发送；
- 由后续稳定 owner（通常 timer 线程）统一 flush。

适用：
- 希望保留“秒内多次变化”；
- 同时要避免在 parser/QMI 回调线程里做重编码、重发送。

实现时再加两条硬规则：
- 如果消费者需要保留“秒内多次变化”，就必须按“每次变化一条事件”入队；不要只保留一个 latest snapshot，否则 timer flush 时天然只会剩最后一次变化。
- 优先复用现有稳定 owner 的统一发送链，例如写入 `diag_info->slot.<owner>` 后由 `qmi_diag_log_slot_indication()` 或同类 timer owner 统一发；只有现有发送链明确不适合时，才新增独立 helper。不要在 parser/QMI 回调线程里直接做重编码、重发送。

如果一个 ICD 同时产出多种机制的字段，必须拆开设计：
- 连续 1 秒 summary 放进聚合 owner；
- 变化型 config/event 放进独立事件 owner 或独立 pb；
- 不要为了省字段，把不同机制的数据硬捏进一个 message。

`0xB890` 的落地结论应作为范例记住：
- `config / transition` 属于 `rf.cdrx` owner；
- `inactive_ms / inactive_pct / current_ref_count` 属于 `powerInfo` 的 `timer 1s derived metric`；
- 不要把 `config / transition / 1s derived metric` 同时塞进 `powerInfo`。

对当前代码，至少要先把参数按下面几类建表后再动手：
- `event-only`
- `event-record + timer flush`
- `timer snapshot`
- `timer 1s sum/avg`
- `timer 1s occurrence count`
- `timer 1s derived metric`
- `change-only queue + timer flush`

如果这张表还没写出来，就说明上报机制还没真正收敛，暂时不要急着改 proto。

### 4. 代码实现顺序

按顺序做：
1. 查 `references/modem_data_registry/ICD_parsed.csv`、`ICD_under_parse.csv` 和 `ICD_under_review.csv`，先确认是否已在线支持或已进入解析流程。
2. 选 owner 模块。
3. 收集 `log ID + version + QCAT parser + hex dump`。
4. 若已有 LTE ICD PDF / 截图 / 摘录，先把有效信息沉淀到 `references/<log_id>/input/`。
5. 完成 `references/<log_id>/runs/<log_id>_semantic_vs_packed_struct_table.md` 的反解析迭代，直到当前资料下无法进一步加强。
6. 若 struct 证据仍不充分，暂停编码并向用户索要缺失资料。
7. 只有 struct 收敛后，才在 `.h` 中加 log id、version、packed struct、getter、handler 声明。
8. 在 `.c` 中按既有 begin/end 注释块风格实现 `handle_xxx()` 和 `handle_xxx_v<version>()`；不要把 ICD packet layout 的 `typedef struct` 或解析常量宏堆到 `.c`。
9. 在 `bytedance_diag_log.c` 中注册并 dispatch。
10. 先梳理新字段的上报机制：它是事件、瞬时值、1 秒 sum/avg，还是先计算再汇总的派生值。
11. 如有需要，把结果写入已有 owner 状态、事件缓存或聚合器。
12. 只有在 owner、触发机制、生命周期都匹配时，才复用已有 proto；否则新建独立 pb/message。
13. 只有现有输出结构真的适配时，才改 `BdDiagLog.proto`。

### 5. parser 入口函数的固定模板

`handle_xxx()` 的职责固定是：
- `data != NULL`
- `len >= header_len`
- cast 成 top-level struct
- 读取 `version` / `count`
- 校验 `slotId`
- 校验 `version`
- 校验 `count`
- dispatch 到 `handle_xxx_v<version>()`

不要把多个版本糊在一个大函数里边猜边兼容。

### 6. 变长数组解析方法

统一用 pointer walking：
- 维护 `ptr`
- 维护 `data_end`
- 每次 dereference 前先检查 `ptr + need <= data_end`
- 每层 `record/carrier/channel` 的计数都要有显式上限

对单层固定块，还要比较：
- 广告 count
- payload 实际长度能推出的 count

## 七、实现时的硬规则

### 1. 长度与边界检查必须完整

至少检查：
- `data != NULL`
- `len >= header_len`
- `slotId` 合法
- `version` 支持
- `num_records / num_carriers / num_channels` 不超上限
- 每次指针前进前都检查 `ptr + need <= data_end`

任何变长数组都不能靠“应该够长”来假设。

### 2. 版本分发必须显式

模式固定：
- `handle_xxx()` 做通用校验
- `switch(version)` 分到 `handle_xxx_v<version>()`
- unsupported version 明确返回

### 3. 一个状态只允许一个 owner

例如：
- `powerInfo` 归 `rf/power`
- `RACH failure` 归 `network`
- `IMS / RTP / QoE` 归 `call`

如果一个 ICD 要喂多个 consumer，fan-out 放在 `bytedance_diag_log.c`，不是到处复制解析。

### 4. 先复用现有上报结构，但先比对上报机制

只有当 owner、触发机制、生命周期、聚合窗口都匹配时，才复用现有 proto。

不要因为“message 里还能加字段”就把事件型参数混进 1 秒 summary message。
对 `config / cause / failure / state transition` 这类参数，先判断它是不是应该独立 message，再判断它是否需要进已有 owner。

如果当前设计还处于 bring-up / debug 阶段，字段尚未上线，而你已经确认它被放进了错误 owner：
- 直接迁到正确 owner；
- 同步删除错误 message 里的临时字段；
- 不要把“这几天刚加的临时 proto 字段”误当成不能动的稳定协议。

### 5. 改动要小

每次只引入一个可验证变量：
- 先只注册和 dispatch
- 再加 parser
- 再加聚合
- 再加额外输出

不要把“新 ICD + 重构 + 新 proto + 新聚合”堆在一个提交里。

## 八、Log 打印强制约束与检查点

日志在 ICD bring-up 阶段不是“辅助项”，而是证据链的一部分。必须把它当成结构化检查点来设计，而不是临时想到再补。

### 1. 日志分层规则

固定规则，不允许按个人习惯改写：

- 以下场景**必须**使用 `BD_MSG_ERROR`：
  - 参数非法，如 `data == NULL`、`slotId invalid`
  - version / length / count / pointer boundary guard 失败
  - 解析继续执行会导致越界、错读、状态污染的异常路径

- 以下场景**必须**使用 `BD_MSG_HIGH`：
  - `boot marker / worktree marker / build version`
  - 低频稳定里程碑
  - 已经完成聚合、即将写入 owner / proto / indication 的最终 summary
  - 对高频 parser，只允许把按窗口聚合后的低频 summary 或最终填充结果放在 `BD_MSG_HIGH`

- `boot marker` 额外强制规则：
  - `BD_DIAG_BOOT_MARKER_VERSION` 不是自由文本，必须能唯一指向当前 payload。
  - 推荐格式：`<semantic_tag>_g<payload_short_sha>`。
  - 若当前提交只是修 `boot marker` 本身，版本串里的 `payload_short_sha` 仍应指向“本次功能改动对应的 payload 提交”，而不是 marker 修订提交自己的 `HEAD`。

- 以下场景**必须**留在 `DBG_LOGE_*` 或专用 `DBG_<ICD>_LOG`：
  - dispatch 命中
  - handler 入口
  - version 分支命中
  - 高频 record / carrier / channel 字段
  - 锁内计数、累加器细节、中间态变量、逐字段解析值

- 以下做法**禁止**：
  - 把高频 record / carrier / channel 字段长期留在 `BD_MSG_HIGH`
  - 把 parser 每包入口、每 record、每 channel 的明细放到 `BD_MSG_HIGH`
  - 用 `BD_DBG_LOG` 代替 parser bring-up 的主路径
  - 把“日志方便看”当成向线上长期注入高频打印的理由
  - 忽略日志宏参数上限，把过多格式实参塞进单条 `DBG_<ICD>_LOG` / `DBG_LOGE_*` / `BD_MSG_*`，导致底层 `OEM_* -> MSG_n` 宏超限并编译失败
  - 在高频 parser 上一开始就打开全局 `DBG`，导致无关路径一起放量

- 日志宏参数上限的强制规则：
  - 不要想当然地把 `DBG_<ICD>_LOG` / `DBG_LOGE_*` / `BD_MSG_*` 当成标准 `printf`。
  - 先检查当前文件/模块里**已经验证可用**的打印风格并优先复用；不要跳过现有事实，拍脑袋指定“只能用某一种宏风格”。如果当前文件已经稳定使用 `DBG_B884_LOG(fmt, ...)` / `BD_MSG_*` 这类写法，就优先沿用同类模式；如果当前文件主要使用 `DBG_LOGE_1..6`，再跟随该风格。
  - 新增或扩展调试日志前，先数格式实参个数，再对照当前平台 `OEM_* -> MSG_n` 宏上限。
  - 只要存在超限风险，就必须拆成两条或多条日志；不要等到编译报 `MSG_10` / `MSG_n` 再回头修。
  - 高风险场景是 record 级 debug：`rec/slot/scs/sfn/slot/state/reason/ref/proc/...` 很容易一条超过上限，默认应按“主字段一条、补充字段一条”拆开。
  - 默认避免在 modem 侧 debug 打印里新引入 `%lld` / `%llu` / `%s`；优先改成已有风格中被反复证明可用的 32-bit 数值打印，必要时先缩放、截断到当前调试真正需要的量纲后再打印。

- 对高频 ICD 的低频 summary，再补一条固定做法：
  - 优先在 owner 的聚合输出点打印，例如 `tick / timer / flush / commit` 之后
  - 优先按窗口输出，例如 `avg_over_last_1s`
  - 一条日志只打一类对象，不要把多个对象硬拼在一条里
  - 对同一个功能的多条 summary，优先抽成 helper，避免在主流程里内联堆日志

### 2. 调试阶段的强制开关

ICD 解析开发/调试阶段，不要默认打开全局 `DBG`。

固定顺序：
- 先保留 `boot marker`
- 先加低频 `BD_MSG_HIGH` summary
- 再按需要增加专用 `DBG_<ICD>_LOG`
- 只有确实需要排全局通路时，才短时打开全局 `DBG`

如果当前问题表现为“mdlog 明明有包，但 online parser 没结果 / 字段异常 / 提前早退原因不明”，则专用 `DBG_<ICD>_LOG` 不是可选项，而是默认动作。不要在缺少专用 debug 证据链时反复猜 struct 或猜 dispatch。

解析异常 bring-up 的默认顺序：
1. 保留 `boot marker`
2. 增加 dispatch 命中日志
3. 增加 `handle_<log_name>()` 入口日志
4. 增加 version 命中日志
5. 增加关键 guard 早退日志
6. 仅在需要时增加 record / channel / tb 级别的 `DBG_<ICD>_LOG`
7. 再补 1Hz / tick summary，证明结果已经进入 owner 或聚合器

原因：
- 全局 `DBG` 会把无关 parser 一起放开，容易把高频路径问题和目标 ICD 问题混在一起
- 高频 ICD 往往每秒样本很多，先用低频 summary 就足够证明在线链路是否跑通
- 专用 `DBG_<ICD>_LOG` 比全局 `DBG` 更适合做最小变量 bring-up

同时注意：
- `DBG_LOGE_* -> BD_MSG_ERROR -> OEM_ERROR` 这条日志链不要假定为“可靠支持 `%s` 的标准 printf”
- modem 侧若要打印固定版本/日期/worktree 标记，优先使用编译期字符串拼接，不要依赖 `%s`
- 对 `band/plmn/path` 这类字符串字段，默认不要直接放进 `DBG_LOGE_*` / `BD_MSG_*` 的 `%s`
- 若确实需要观察内容，优先改成有界字节打印、定长 `%.*s`（仅在已确认后端可用时），或直接拆成数值字段
- 若出现“打开 DBG 才 crash、关闭后恢复”，除检查高频日志量外，优先排查新增 `DBG_LOGE_*` 中是否混入了 `%s`
- 若怀疑某个 ICD 的打印导致 crash，第一步先整体关闭该 ICD 的专用 `DBG_<ICD>_LOG`，不要先在打开状态下比较哪几条日志更重

merge / 收口阶段：
- 如果临时 `DBG_LOGE_*` 已完成使命，应提醒关闭 `DBG` 或清理不再需要的高频调试打印
- 只保留对长期维护有价值的低频稳定里程碑日志

### 3. bring-up 的最小日志证据链

每个新增或维护中的 ICD，最少要能回答下面四个问题：
1. 包有没有被收到并进入 dispatch。
2. handler 有没有被命中。
3. version 有没有命中到目标分支。
4. 是成功继续解析，还是在某个 guard 提前返回。

因此 bring-up 阶段最少应具备这几类日志证据：
- `boot/worktree/build marker`
- dispatch 命中目标 `log_id/event_id`
- `handle_<log_name>()` 入口
- `version` 命中或 unsupported version
- 关键 guard 早退原因，如 `len too short`、`num_records invalid`、`ptr overrun`
- 若该 ICD 属于高频 parser，再补一条低频 summary 证据，证明解析结果已经写入 owner / 聚合器 / 最终输出

注意：
- 这里要求的是“证据点”，不是要求把所有字段都打印出来
- 字段级打印只服务于局部定位，一旦问题锁定，应尽快回收

### 4. 提交前检查点

在提交 ICD 改动前，至少逐项确认：
1. 当前 worktree 使用的固件版本能通过 `boot marker` 区分，且 `BD_DIAG_BOOT_MARKER_VERSION` 已包含 `semantic_tag + payload_short_sha`。
2. 若依赖高频 parser 调试，已优先使用低频 summary 或专用 `DBG_<ICD>_LOG`，没有默认打开全局 `DBG`。
3. dispatch / handler / version / guard 四类证据中，当前问题所需的证据点已经具备。
4. `BD_MSG_ERROR / BD_MSG_HIGH / DBG_*` 的使用位置已经按本节强制分层执行，没有混层。
5. 没有把高频字段打印常驻到 `BD_MSG_HIGH`。
6. 没有在 `DBG_LOGE_*` / `BD_MSG_*` 中直接新增 `%s` 字符串打印，尤其是来自 struct/cache/变长缓冲区的字段。
7. 新增 `DBG_<ICD>_LOG` / `DBG_LOGE_*` / `BD_MSG_*` 后，已经检查过单条日志的格式实参数量，不会撞到底层 `MSG_n` 宏上限；如有风险，已拆成多条日志。
8. 同一功能的多条 summary 已收敛到 helper，没有在主流程里堆大段内联日志。
9. 不再需要的临时日志已经删除，或明确仅在当前 debug 分支保留。
10. 若本轮修改依赖 LTE ICD PDF 结论，相关有效摘录已经沉淀到 `references/<log_id>/input/`。
11. `runs/<log_id>_semantic_vs_packed_struct_table.md` 已更新到当前结论，且明确写出 locked / corpus-bound / unresolved 边界。
12. 若 struct 证据曾不足，本轮已经向用户显式索要过缺失资料，而不是带着未闭环结构直接编码。
13. ICD packet layout 相关 `typedef struct` / `typedef union` / getter macro / 解析常量宏已按 owner 文件既有风格放在 `.h`；`.c` 中只保留私有运行态状态、实现 helper 和明确私有的 debug 宏。
14. 新增 ICD 或同一功能域代码已用既有 begin/end 注释块包住，没有把多个无关 ICD 堆在一个大段里，也没有把无关逻辑插入旧功能块内部。
15. `.h` 不依赖 `.c` 中的宏定义；所有被 `.h` 类型、数组长度或 getter 使用的宏已经在 `.h` 内自洽定义。
16. 若 parser 使用 `signature / bitmask / report id / channel id` 分支，已经证明它应当全值精确匹配还是 mask/family 匹配；已用 `ERROR_MSG.csv` 中的 unsupported 值反向验证不会误挡合法数据。

## 九、验证流程

### A. 离线真值验证

用 `skill_log_filter_mdlog` 或已有离线 parser，确认：
- 日志里确实有该 `log ID`
- version 与样本一致
- 关键字段有真值

### B. 在线链路验证

按链路看：
1. boot marker / 版本号是否是当前固件，且能区分同一天内的多次修改
2. `diag_listener` 是否收到并 dispatch
3. handler 是否命中支持的 version
4. owner 状态是否变化
5. indication / response 是否带出结果

### C. 必做对照

对照关系：
- `QCAT parser truth`
- `hex replay`
- `online parser output`

至少要能说明：
- 哪些字段已经锁定
- 哪些字段还只是推测
- 推测的范围和样本边界是什么

## 十、run artifact 要写什么

非平凡 ICD 至少在 `references/<log_id>/runs/` 留一份结果，推荐：
- `<log_id>_semantic_vs_packed_struct_table.md`
- `<log_id>_<branch_or_worktree>_call_chain.md`

至少包含：
1. 证据来源
2. 覆盖的 version 和样本范围
3. 外层长度规则
4. 语义字段到 packed 字段的映射
5. 已锁定字段与未锁定字段
6. hex replay 与真值比对结果
7. 当前 parser 草稿
8. 剩余风险

如果当前任务是“mdlog 有包，但 online parser 没结果 / 结果不稳定 / 先只 bring-up 一个 ICD”，还必须补一份调用链文档，至少写清楚：
1. `log_id[] / event_id[]` 注册位置
2. `diag_listener_*_cb()` dispatch 位置
3. `handle_xxx()` 入口与 version 分发
4. `handle_xxx_v<version>()` 的 record / carrier / channel 解析层次
5. 状态 owner / 聚合器写入点
6. 最终输出 owner 和 tick / timer 触发点
7. 当前 worktree 是否带未提交修改

用途：
- 它是 `.h/.c` 最终落代码前的中间真值文档。
- 先更新它，再把内容物化到 C 代码里。

## 十一、常见排障顺序

### 1. mdlog 有包，online 没结果

按顺序查：
1. `log_id[]` 是否注册
2. `diag_listener_log_ext_cb()` 是否 dispatch
3. version 是否支持
4. 长度/边界检查是否早退
5. owner 状态是否被写入
6. qmi indication 是否因为长度超限被裁掉

其中第 1 步要按第一性原理理解：
- `log_id[] / event_id[]` 是订阅白名单
- `switch case` 只是收到包之后的第二层分发
- 只补 handler、不补注册，在线链路通常不会启动

### 2. 只有 boot marker，没有 parser 结果

说明：
- 服务起来了
- indication 定时器可能在跑

但不代表：
- 某个 ICD 一定 dispatch 到了 handler
- 某个 handler 一定进入了目标 version 分支

这时优先查：
1. 当前固件是否为目标 worktree 版本
2. `DBG_LOGE_*` 依赖的 `DBG` 是否为 `1`
3. dispatch 证据是否存在
4. handler / version / guard 证据是否存在
5. 状态更新是否发生

而不是先放开更多 ICD。

### 3. mdlog 有真值，但 online 字段不对

优先怀疑：
- packed struct 偏移错
- 位域宽度错
- little-endian 解释错
- record/carrier/channel 步进错
- 聚合窗口或 owner 选错

### 4. 改一行日志就 crash / 不 crash

优先怀疑：
- `bytedance_task` 栈不够
- 局部大对象导致栈踩踏
- 边界检查前已经越界访问
- 高频 debug 打印把 boot 阶段压力放大

先用 `skill_code_check_bytedance_task_crash` 的方法排。
同时按下面顺序做最小隔离：
1. 先关闭目标 ICD 的专用 `DBG_<ICD>_LOG`
2. 保留 `BD_MSG_ERROR`
3. 保留低频 `BD_MSG_HIGH` summary
4. 确认稳定后，再逐层恢复 `DBG` 证据点

## 十二、最短执行清单

每次新增一个 ICD，按下面执行：

1. 读 `references/modem_data_registry/ICD_parsed.csv`、`ICD_under_parse.csv` 和 `ICD_under_review.csv`
2. 确认 owner 模块
3. 获取 LTE 或 NR 的 struct 证据
4. 收集 `log ID + version + QCAT parser + hex dump`
5. 若已有 LTE ICD PDF / 摘图，先沉淀到 `references/<log_id>/input/`
6. 迭代更新 `references/<log_id>/runs/<log_id>_semantic_vs_packed_struct_table.md`
7. 若 C struct 还不能收敛，立即向用户索要更多信息
8. struct 收敛后，再修改 `bytedance_diag_log.c` 与对应 `bytedance_diag_<owner>_log.h/.c`
9. 先补齐最小日志证据链；解析异常默认先加专用 `DBG_<ICD>_LOG`
10. 必要时修改 `BdDiagLog.proto`
11. 用 mdlog 离线真值对照在线输出
12. 更新 registry 和 `references/<log_id>/runs/`
13. 在附加 worktree 提交
14. 主 worktree `git reset --hard <commit>` 同步并复核

## 十三、离线解析 vs 在线解析对比（HTML）

当用户不是在“新增 parser”，而是在问：
- 在线 ICD 解析后的 1s summary 是否和离线解析一致
- 想看参数整体趋势，而不是逐点人工比对
- 需要把 offline 和 online 的差异做成可复用的 HTML 看板

必须使用本节流程，而不是只贴几条 sample 行。

### 1. 输入约束

默认输入是两份已经存在的 CSV 目录：

- `offline-dir`: 离线 mdlog/QCAT 解析后的 `auto_analysis/` 目录
- `online-dir`: 在线 ICD 解析后的 `auto_analysis/` 目录

当前脚本已覆盖并验证的对比项：
- `B16E power summary`
- `B16E UL summary`
- `B16F power summary`
- `B173`
- `B883`
- `B884`
- `B887`
- `B8A7`
- `B890 config`
- `B890 summary invariant`
- `B825 coverage`

硬规则：
- 执行本节 HTML 对比前，必须先调用 `skill_code_ai_md_power` 的 modem merge 流程，生成同一 run 下的 `tmp/modem_merged_1s.csv`。即使当前 case 没有 power CSV，也要先生成 modem-only 1s 聚合产物，作为后续 offline 1s 口径的统一证据。
- HTML 对比产物必须使用 `scripts/icd_offline_online_compare.py` 生成，输出到 `<run_dir>/output/html_compare/`。禁止临时另写一套 `html/online_offline_compare.html` 或其他不一致风格的看板。
- 每次新增或扩展 ICD 在线解析后，如果该 ICD 有 `SUMMARY_MSG`，必须同步扩展 `scripts/icd_offline_online_compare.py` 的 pattern、offline 回算函数、`build_compare_details()` 和 `error_category_counts`，保证新增 ICD 自动进入统一 HTML 对比页。

### 2. 工具入口

脚本：

- `scripts/icd_offline_online_compare.py`
- `scripts/rerun_icd_compare_batch.py`

标准调用：

```bash
python scripts/icd_offline_online_compare.py \
  --offline-dir <offline_auto_analysis_dir> \
  --online-dir <online_auto_analysis_dir> \
  --output-dir <run_dir>/output/html_compare \
  --title "<case title>"
```

当同一 case 需要保留多版 compare 产物时，追加：

```bash
  --file-tag v<skill_version>
```

这样输出文件会变成：

- `icd_offline_online_compare_v<skill_version>.html`
- `icd_offline_online_compare_details_v<skill_version>.csv`
- 以及其他同批 CSV 的版本化文件名

批量重生成已有 run 的 compare 产物时，使用：

```bash
python scripts/rerun_icd_compare_batch.py \
  --root <case_root>
```

脚本行为：

- 发现 `<case>/runs/*/run_manifest.json`
- 默认取每个 case 的最新 run
- 从 `run_manifest.json` 读取 `skill_version`
- 自动兼容以下 online compare 输入目录：
  - `tmp/online_required_input/auto_analysis/`
  - `tmp/online_required_input/auto_analysis/backup/<latest>/`（历史兼容）
  - `tmp/online_required_input/temp_*/`
- 输出带版本号的 compare 文件名，并把 HTML title 写成 `<case_name> v<skill_version>`

常用选项：

```bash
python scripts/rerun_icd_compare_batch.py \
  --root <case_root> \
  --all-runs \
  --overwrite
```

### 3. 输出物

脚本会输出：

- `icd_offline_online_compare_details.csv`
  - 每个 summary 参数一行
  - 包含 `series_name / field / timestamp / offline / online / delta`
- `icd_offline_online_compare.html`
  - 概览页
  - 各参数 exact match rate / delta 统计
  - 每个参数一张独立图的 offline vs online 对比
  - `B890` config timeline
  - `B890` invariant issue 表
  - `B825` coverage 表
  - `ERROR_MSG` 分类计数表
- `b890_config_timeline.csv`
- `b890_config_sequence_compare.csv`
- `b890_summary_invariant_issues.csv`
- `b825_coverage.csv`
- `error_category_counts.csv`

若使用了 `--file-tag`，上述文件名会带同样的版本后缀。

### 4. 口径规则

对比时必须坚持同一个窗口定义：

- 在线 summary 的时间戳是右边界
- 离线值按 `(ts-1s, ts]` 窗口回算

不要直接按自然秒整点去 join，否则容易把窗口边界误判成 parser 错误。

### 5. 读图重点

HTML 中每个 `series` 会把同一组参数拆成多个子图：

- 蓝线：offline
- 红线：online
- 橙色菱形：mismatch 点

注意区分两类输出：

- `SUMMARY_MSG`
  - 用于 offline vs online 数值对比
  - 当前包括 `B16E/B16F/B173/B883/B884/B887/B8A7`
- `ERROR_MSG`
  - 用于诊断在线 parser 健康度
  - 只做分类计数 / 明细排查，不和 offline 真值做数值对比

优先看：
1. 是否整体趋势一致
2. 差异是全程系统性偏移，还是只发生在启动首拍 / config 切换点
3. `B890` 是否出现违反 1s 不变量的 summary
4. `B825` 是否存在“离线有数据、在线无产出”的 coverage 缺口
5. `ERROR_MSG` 是否突然出现、是否集中在某个 logId

### 6. 结论输出要求

给用户结论时，不要只说“有差异”。

至少分成三类：
1. 明确一致：趋势和数值基本一致，可认为在线解析正确
2. 边界差异：只在窗口边界、启动首拍、config 变更点出现
3. 明确异常：违反不变量、缺少 coverage、或持续系统性偏移
