# API 设计变更说明


## v170 → v171：`Q-100`／W4（`T-1041`）—— 舒适域默认值/语义标签换算下沉：9 处参数不再 required；`intensity/mood/color_name/hint` 由域换算（`device_control/domain/comfort_semantics.py`）（2026-11-10）

> **来源**：`Q-100` §3 W4（`R-088`／`T-1041`）—— LLM 只给语义标签，用户没说参数 ⇒ **不传**；由设备控制域补默认/换算并过 bounds。⛔ 映射表不进 LLM 提示词。
> **口径订正**：`StartWakeUp.mode` 默认取协议内已有 **`gradual`**（`auto` 不在 `wake_up_mode` 枚举，⛔ 不改协议）；`StartSnoreIntervention.mode=auto` 保留（已有枚举）。`air_purifier.target` 的「auto」＝**不传 target**（设备自动模式），⛔ 不下发字符串。
> **兼容性**：参数 required 面放宽（只影响 LLM 注入面）；用户明说数值/枚举 ⇒ **直通**，⛔ 不被默认覆盖；执行层仍走既有事件类与 bounds。

| 件 | 变更 |
|---|---|
| `data/ai-tools/tools.js` | **9 处不再 required**：`StartAirflow.intensity`、`StartAdjustTemperature.target`、`StartHumidifier`／`StartDehumidifier`／`StartAirPurifier.target`、`screen_brightness.brightness`、`StartLulling.intensity`、`StartSnoreIntervention.mode`、`StartWakeUp.mode`（卡上 `required` 面与描述同步）。**`SetLight`** 增 `color_name`／`mood`／`hint`；**`StartAdjustAngle`／`StartAdjustHeight`／`screen_brightness`** 增 `hint`（`value`/`brightness` 可省略；`mode` 由平台按 hint 置 relative）。`intensity` 枚举扩相对标签（up/strong/stronger/down/weak/lower；由域换算 ±1 档）。描述改为「未给 ⇒ 不传，平台补默认」＋「相对量用标签、⛔ 不要自己算数值」。卡数不变（52／分组 49）。 |
| `SetLight` 条件默认（复核修正） | `brightness=60` **仅当用户只说「开灯/把灯打开」**（未给 `brightness`/`hint`/`color_name`/`mood`/`color`/`color_temp`）才补；只换颜色/色温 ⇒ **不补 brightness**（设备保持当前亮度）；`hint` 是相对量（按影子当前亮度 ±10%），不算默认。`CANONICAL_DEFAULT_VALUES` 同步为**条件归一**（颜色语义存在时保留显式 brightness；开灯未给 ≡ 显式 60）。 |
| `publish/api/data/ai-tools/tools.js` | 上述单件**逐字节镜像**（双侧 SHA256 相等） |
| `publish/api/CHANGELOG.md` | 本 CHANGELOG 逐字节镜像 |
| `src/server/modules/device_control/domain/comfort_semantics.py`（同批新增） | 域内**单一出处**：`ACTION_DEFAULTS`（强度 medium／震动 wave／打鼾 auto／唤醒 gradual／温度 26／湿度 60／灯 60／屏幕 70／净化 auto=不传）、`INTENSITY_ALIASES`＋`resolve_intensity`（±1 档、端点如实）、`LIGHT_COLOR_NAMES`（白/蓝/红/绿/暖白→RGB）、`MOOD_COLOR_TEMP`（warm20/neutral50/cool80）、`HINT_STEPS`（角度/高度 ±5、亮度 ±10） |
| `src/server/modules/ai_agent/**`（同批代码） | `domain/tools.py`（required/enum/描述/`hint`/`color_name`/`mood`）；`application/tool_executor.py`（`_apply_comfort_semantics`：显式值优先→标签换算→默认，`_tool_payload`/bounds 之前、重试只算一次）；`domain/cacheability.py`（`CANONICAL_DEFAULT_VALUES` 同步；相对 `intensity`/`hint` 判 `none`）；`domain/device_context.py`（删「模型自己换算」引导，改「不说就不传／相对量用标签」） |

- **门禁读数（本批实测）**：`test_ai_tool_card_parity_gate`／`test_play_music_contract_gate`／`test_event_producer_gate`／`test_tool_visibility_gate`／渲染/镜像/对账门禁全绿；`comfort_semantics` 单测与 W4 定向用例见回执。
- **⚠️ 未覆盖面（如实登记）**：C 面舒适子集 20×2 属 W5 验证批（本件零付费调用）；`color_name` 先只收 5 色（白/蓝/红/绿/暖白，后续扩表）；`mood` 只对照明灯（氛围灯走 `color_name`/RGB）；无 per-device 时区（W3 已登记）；`screen_brightness` 设计卡在本资产中本就缺失（预存缺口，本批未新增卡）。

## v169 → v170：`Q-100`／W3（`T-1040`）—— 闹钟域下沉：`ScheduleAlarm{when_label,repeat,actions}`／`QueryAlarms{selector?}`／`CancelAlarms{selector}` 取代旧闹钟工具面；`sched_id` 不跨 LLM 边界（2026-11-10）

> **来源**：`Q-100` §3 W3（`R-087`／`T-1040`）—— 闹钟域下沉：LLM 只给**语义时间标签**（「明早7点」「每晚10点」「工作日8点」…）＋ 语义 actions；日期/星期/生效窗口/协议 `action_type/action_payload`/`sched_id` 全部由设备控制域 `AlarmService` 计算/生成。
> **兼容性**：旧 `SetUpScheduleAction`／`RemoveScheduleAction` **退役 LLM 工具面**（`inToolFace=false`），执行面/后台/历史直调保留；协议下行仍走既有 `schedule_action`（payload 形状不变）。
> **时区口径（如实登记）**：域**无 per-device 时区载体** ⇒ 固定 **UTC+8（CST）**，与 `sleep_data/domain/daily_summary.py::CST` 及旧 `SetUpScheduleAction` 描述「Asia/Shanghai(UTC+8)」一致；不新增设备字段、不改协议契约。
> **缓存**：创建/取消/查询**均保持不可缓存**（`ScheduleAlarm`/`CancelAlarms`→`schedule` 桶，`QueryAlarms`→`query` 桶）；本批不为闹钟开缓存口子。

| 件 | 变更 |
|---|---|
| `data/ai-tools/tools.js` | **新增 `ScheduleAlarm`**（`when_label`＋`repeat` once/daily/weekdays/custom＋语义 `actions[]`；⛔ 无 `sched_id`/`time`/`weekdays`/`start_date`/`end_date`/协议 payload）；**新增 `QueryAlarms`**（`selector?` 空=全部）；**新增 `CancelAlarms`**（`selector` 必填；0 条如实、多条候选澄清、`全部` 全删）；旧 `SetUpScheduleAction`／`RemoveScheduleAction` 加 `"inToolFace": false` 并更新描述。`toolVisibility.resident` 31→**34**；分类「闹钟」工具列表 2→5。工具卡 49→**52**（分组面 46→**49**）。 |
| `data/ai-tools/protocol.js` | 自述面同步：可调用 Tool **46 → 49**；「Tool 分组」表闹钟行 **2 → 5**（+3 新工具）；`categories` 闹钟成员同步 |
| `publish/api/data/ai-tools/tools.js`＋`protocol.js` | 上述两件**逐字节镜像**（双侧 SHA256 相等） |
| `publish/api/CHANGELOG.md` | 本 CHANGELOG 逐字节镜像 |
| `src/server/modules/device_control/application/preference_services.py`（同批代码） | `AlarmService` 增 `resolve_when_label`（明早/今晚/每天/每晚/工作日/周末/明天中午/指定日期/每周X；歧义抛 `AlarmIntentError`）、`create_from_intent`（语义 actions→协议 payload，复用 `validate_alarm_capability`；`sched_id` 域内生成）、`query_alarms`/`cancel_alarms`（selector：时间/日期/序号/全部；0 条如实、多条候选澄清）；固定 `ALARM_TZ=UTC+8` |
| `src/server/modules/ai_agent/**`（同批代码） | `domain/tools.py`（3 新工具＋旧 2 退役面）；`application/tool_executor.py`（3 个专用 handler；删除走既有 `schedule_action_remove`）；`domain/cacheability.py`（query/schedule 桶）；`domain/tool_visibility.py`（resident +3）；`application/llm_failover.py`（`ScheduleAlarm`/`CancelAlarms` 计副作用）；`domain/device_context.py`（提示词：只用 when_label/repeat/语义 actions，⛔ 算日期/给 sched_id/构造 payload） |

- **门禁读数（本批实测）**：`test_ai_tool_card_parity_gate`（PlayMusic 卡↔实现）／`test_play_music_contract_gate`／`test_event_producer_gate`（PROVENANCE_THREADS 33→35 ＋ `_publish_schedule_remove` 发布点登记）／`test_tool_visibility_gate`（设计投影 resident 34）＋ 渲染/镜像/对账门禁全绿（详见回执）。
- **⚠️ 未覆盖面（如实登记）**：C 面闹钟子集 10 条属 W5 验证批（本件零付费调用）；**无 per-device 时区载体**（本批以固定 UTC+8 落，若未来要按设备时区须先补字段，另立任务）；语义 actions 本批覆盖 12 类常见动作（wake_up/bed_mode/light_on/light_off/massage/angle_adjust/height_adjust/temperature_adjust/airflow/humidifier/dehumidifier/air_purifier），其余协议动作仍可走旧直调面。

## v168 → v169：`Q-100`／W2（`T-1039`）—— 音乐域下沉：`MusicControl{action}` 取代 `ControlMusic` 工具面；`PlayMusic` 只收标签/歌名；`ControlMusic`／`SearchMediaLibrary` 退役工具面（2026-11-10）

> **来源**：`Q-100` §3 W2（`R-086`／`T-1039`）—— 音乐域下沉：LLM 不再产 `media_id`、不再先搜库；控制"当前曲目"由音乐库按**播放记忆**定位；音量属音频输出域（`SetupBluetoothAudio`），⛔ 不由音乐域发事件（producer 面静态钉住）。
> **兼容性**：LLM 工具面**删除** `PlayMusic.media_id/tag_ids/min_duration_sec/max_duration_sec`（旧直调执行面仍兼容）；`ControlMusic` 执行面保留（历史/后台直调），仅退役 LLM 注入面。
> **语义边界（如实登记）**：`volume_*` 动作由 `tool_executor` 内部转既有 `SetupBluetoothAudio`（audio_output 域）；音乐域**不发布** `BluetoothAudioControlCommand`（`scripts/check_event_producer_subset.py` 的 producer 静态契约只有媒体事件）。

| 件 | 变更 |
|---|---|
| `data/ai-tools/tools.js` | **`PlayMusic` 卡参数重写**：删 `media_id`/`tag_ids`/`min_duration_sec`/`max_duration_sec`，留 `tag_names`/`keyword`/`volume`/`loop`、新增 `name`/`duration_hint`；`required=[]`；`anyOf` 四支→**三支**（`tag_names`／`keyword`／`name`）。**新增 `MusicControl` 卡**（`action` 8 值闭集：pause/resume/next/prev/stop/volume_up/volume_down/set_volume；`value` 0-100 仅 `set_volume`；`required=["action"]`；⛔ 无 `media_id`）。`ControlMusic`／`SearchMediaLibrary` 加 `"inToolFace": false`（退役 LLM 工具面、执行面/后台保留）并更新描述/notes。`toolVisibility.resident` 30→**31**（+`MusicControl`）。分类「媒体控制」工具列表补 `MusicControl`；设计侧提示词段媒体路由/音量路由同步（放/播＝`PlayMusic` 标签、控制＝`MusicControl{action}`、音量仍走 `SetupBluetoothAudio`）。工具卡总数 48→**49**。 |
| `publish/api/data/ai-tools/tools.js` | 上述单件**逐字节镜像**（双侧 SHA256 相等） |
| `data/ai-tools/protocol.js` | 自述面同步：可调用 Tool **45 → 46**；「Tool 分组」表媒体控制行 **3 → 4**（+`MusicControl`）；`categories` 媒体控制成员同步（`scripts/probes/ai_tools_selfdesc.cjs` 三处判据转绿） |
| `publish/api/data/ai-tools/protocol.js` | 上述单件**逐字节镜像**（双侧 SHA256 相等） |
| `publish/api/CHANGELOG.md` | 本 CHANGELOG 逐字节镜像 |
| `src/server/modules/ai_agent/**`（同批代码） | `domain/tools.py`（新增 `MusicControl`；`ControlMusic`/`SearchMediaLibrary` `in_tool_face=False`；`PlayMusic` 参数改标签）；`domain/cacheability.py`（`MusicControl` 入 global；canonical 音乐标签排序；历史 `media_id` none 边保留）；`application/tool_executor.py`（`MusicControl` → 域 `request_control_current`；`volume_*` → `SetupBluetoothAudio`；`PlayMusic` 透传 `name`/`duration_hint`）；`domain/tool_visibility.py`（resident +`MusicControl`）；`domain/device_context.py`（媒体/音量路由提示词） |
| `src/server/modules/media_library/events/__init__.py`（同批代码） | 新增 `PlaybackRequestService.request_control_current`（按只读播放记忆定位当前曲目：pause/resume/stop/next/prev；无记忆 ⇒ `no_current_playback`）；`request_play` 支持 `name` 精确点名与 `duration_hint`→秒区间换算；`SELECTION_KEYS` 扩 `name`/`duration_hint` |

- **门禁读数**：`test_ai_tool_card_parity_gate`（PlayMusic 卡↔实现：6 参数／anyOf 3／镜像逐字节）＋ `test_play_music_contract_gate`（required=[]／anyOf 三支／properties≥6）＋ `test_event_producer_gate`（PROVENANCE_THREADS 31→33）＋ `test_tool_visibility_gate`（设计投影 resident 31）**合计 66 passed**。
- **⚠️ 未覆盖面（如实登记）**：C 面音乐子集 20×2（廉价模型）属 W5 验证批，不在本件；`annotate_cache` 等 W1 项由 `T-1038` 独立登记。

## v167 → v168：`Q-096` —— `status_notification` 按类型字段契约收口：53 条属性补全 `required|optional`（36 条补 `optional: true`），明确全量/增量语义（2026-11-05）

> **来源**：`Q-096`（需求方发现“每个类型能上报哪些字段/必选可选/类型值域”定义不全，要求立件补齐）。本件只补契约，不改三元式行结构、不改运行时接收行为。
> **读数**：38 类型 / 53 属性；改前 12 `required`、5 `optional`、**36 未表态**；改后 **12 required / 41 optional / 0 unspecified**。
> **语义**：`required` ＝ 全量 `status_notification_trigger` 响应**必须包含**、增量按变化；`optional` ＝ 可缺报，缺报按**未知**、⛔ 不判故障。三个 `attributes: []` 类型保留现有 `attributes_note`（只报故障行）。

| 件 | 变更 |
|---|---|
| `data/iot/smart-bed.js` 的 `sharedEnums.device_type.values[].attributes[]` | 36 条非 `is_running` 属性补 `optional: true`（`height`/`angle`/`target`/`intensity`/`mode`/`speed`/`brightness`/`color`/`value`/`volume`/`stage`/`result`/`url`/`severity`/`slot` 等）；12 条 `is_running` 保持 required；5 条现有 optional 保持；**每条属性现均可机械判定 required/optional** |
| 同文件 `status_notification.sub_device_events` | desc 补 `Q-096` 语义：required 全量必含、optional 可缺报按未知 |
| 同文件 `sharedEnums.device_type` | desc 补 `Q-096`：每条 attribute 必须显式二选一；三段语义与上面一致；3 个空类型说明保持 |
| `src/server/events/identifier_rules.py::DEVICE_ATTRIBUTES` | 36 条 `AttributeSpec` 补 `optional=True`（与协议逐条对齐；12 required / 41 optional） |
| `tests/design-regression/test_subdevice_field_map_gate.py` | 新增 Q096-A/B/C 判据与反向臂：flag 二选一、type 允许集合、空类型 `attributes_note` 非空；读数钉 12/41/0 |

- **兼容性**：纯契约补全，不新增/删除 attr，不改设备上报行为；36 条补 optional 只是把“可缺报”的现状显式化。
- **⛔ 未改**：三元式行结构、故障行/结果行、`last_result_reportable`、运行时接收/丢弃逻辑、`Q-093` 下行协议。
- **镜像/门禁**：`publish/api/data/iot/smart-bed.js` 与本 CHANGELOG 逐字节；`sync_design_to_publish.ps1 -Verify` GREEN；`test_subdevice_field_map_gate` PASS；**待发布** Cloudflare 技术文档站。
- **未覆盖面（如实登记）**：本件不把 36 条提升为 required（那会新增设备上报义务）；若后续要提升，必须另立 v2 决策。

## v166 → v167：`Q-093` v2（`T-1025`/`T-1027`/`T-1030`）—— 新增聆听控制/聆听模式两条下行 action ＋ `voice_monitor.listening_mode` ＋ AI 内部 `MarkAwaitingReply`（2026-11-05）

> **来源**：`Q-093` v2（会话边界/等待回答/首轮缓存）实现批；协议面由 `T-1025` 落代码、`T-1030` 同步设计资产。  
> **兼容性**：新增 action/字段均为**增量**；旧客户端不识别时只丧失该能力，⛔ 不改变既有动作语义。  
> **表达限制（如实登记）**：`expect_reply` 是「仅 `start` 条件必填」，协议卡 `required` 只有单布尔 ⇒ 用 `required:false` ＋ desc 写明；`control_type`/`mode` 闭集用 `string` ＋ desc，暂不扩 `sharedEnums`。

| 件 | 变更 |
|---|---|
| `data/iot/smart-bed.js` | `sharedEnums.action_name` 业务值末尾追加 `listening_control` / `listening_mode`（57→**59** 业务、全 **60**）；`endpoints` 末尾新增两张端点卡（`sub_device_id` required；`control_type:start\|stop`、`expect_reply`、可选 `timeout_ms`；`mode:mute\|standby`）；`listening_control_ack` / `listening_mode_ack` 为纯 ACK；`sharedEnums.device_type.voice_monitor` desc 改为「可接收平台控制」并增加 `listening_mode`（`standby\|mute`，optional）；新增「🎧 聆听控制」分类（2 action） |
| `data/iot/event-identifier.js` | `downlink` 增加 `listening_control` / `listening_mode` 映射；`ackOnly` 末尾增加两条 ACK（与运行时 `ACK_ONLY` 同序） |
| `data/ai-tools/tools.js` | 新增内部工具 `MarkAwaitingReply`（`kind: clarification\|confirmation`；`internal=True`，不下发设备）；`toolVisibility.resident` 25→**26** |
| `data/ai-tools/protocol.js` | 内部工具自述 **2→3**（`knowledge_search` / `annotate_cache` / `MarkAwaitingReply`） |
| `publish/api/**` | `smart-bed.js` / `event-identifier.js` / `ai-tools/tools.js` / `ai-tools/protocol.js` / 本 CHANGELOG 逐字节镜像 |

- **同批代码**：`src/server/events/**`（事件类/ACK 映射）、`conversation_coordinator`（marker→`listening_control:start`）、`voice_monitor` 状态链；判定语义见 `Q-093/design-v2.md`。
- **门禁读数**：`check_downlink_action_parity` / `check_ack_only_set_bearing_gate` / `check_event_contract_protocol_parity` / `check-design-asset-render` / `sync_design_to_publish -Verify` 全绿（详见 `T-1030.journal.md`）。

## v165 → v166：`R-047` 残留／`T-920` 批 1（按摩契约同源 · 纯同步／契约修正件）—— `rest-massage-control` 描述改为 `speed` 口径；`mode_control` 示例能力集摘除 `none`（2026-10-24）

> **来源**：`.pipeline/PIPELINE.md` 的 `T-920`（`R-076` 取件：`R-047` 残留整批）的**批 1**（分批计划留在第 3 层设计工作区，按 `rule-0c` ⛔ 不在此处引用未定稿路径）。治的病 ＝ `G-506①` 的「协议／实现／设计卡不同源」：协议 `massage` 自 `T-670` 起载荷为 `sub_device_id ＋ intensity ＋ speed ＋ duration`（**有 `speed`、无 `mode`**），而 `rest-massage-control` 卡仍写「intensity+duration+mode 三维度」并断言「协议 `massage` 无 `speed`」—— `T-670` 之后**逐字反了**。
> **改动面**（域 04 两件）：

| 件 | 变更 |
|---|---|
| `data/rest/02-core-domains.js` 的 `rest-massage-control.description` | 改为「sub_device_id ＋ intensity ＋ speed ＋ duration；`speed` 引用公共枚举 5 档 …」＋「`mode` 已自 `massage` 移除（`massage_mode` 语义由 `vibration` 承担）」；按 `rule-0d` **保留 `T-175` 旧句**并追加 `T-920` 取代注（旧句 ≠ 现行口径） |
| `data/rest/examples.js` 的 `mode_control.limits.options` | **删 `none`** —— `none` 是 ER 状态值（`test_bed_mode_set_parity.py` 的 `ER_STATUS_ONLY`）、⛔ 非能力集；能力集与 `sharedEnums.mode` 同为 **6 值** `zero_gravity/cinema/lounge/reading/yoga/flat` |

- **同批代码（`src/**`／`scripts/**`，同批另件）**：`device_control/api/router.py` —— `_validate_massage` 删 `mode` 分支（传 `mode` ⇒ **400**，甲案 fail loud）＋ 新增 `_MASSAGE_SPEEDS`（5 档）校验 `speed`；删除死常量 `_UNDECLARED_KEYS`。`scripts/check_enum_coverage_matrix.py` 的 `PROTO_ENUM_NO_CODE["speed"].reason` 改按**活值域**表述（⛔ 值面／计数未动）。`design/07-tdd` 的 `TC-013` 标题「5 档」→「公共枚举 3 档」。
- **⛔ 未改**：协议卡（`data/iot/smart-bed.js`）、`sharedEnums` 任何值域、REST 端点卡数量（**252**）、工具卡（**47**）、`src/server/events/base.py`。
- **镜像**：`publish/api/data/rest/02-core-domains.js`、`publish/api/data/rest/examples.js`、`publish/api/CHANGELOG.md` 三件 `Copy-Item` 逐字节（双侧 sha256 相等）；`scripts/sync_design_to_publish.ps1 -Check` ＝ **GREEN**（四类清单均 0）。
- **门禁读数**：定向 pytest（`test_t920_massage_contract.py`／`test_vibration_mode_off.py`／`test_device_control_http_contract.py`／`test_device_control_commands.py`）＝ **PASS**；`check_enum_coverage_matrix.py` 的 `E1`／`E2`／`E3` 读数与改前**逐字相同**；`node scripts/check-viewer-data-consistency.mjs` ＝ 断言全绿／**0 页错误**；`node scripts/check-design-asset-render.mjs` ＝ **0 页错误**。
- ⚠️ **未覆盖面（如实登记）**：`R-06`（`BUILTIN_SLOTS` 加语言维）／`R-07`（6 条需真 LLM 的判据用例）**不在本批**（见批计划 §四 `OUT-1`／`OUT-2`）。

## v164 → v165：`G-337②`（纯同步件）—— `data/ai-tools/tools.js` 的 `SendTts.voice_role` 补齐 **4 值角色 `enum`**（对齐 `T-401` 代码侧枚举）＋ `set_preference_config` 的「音色名」口径订正为「角色码」（2026-10-24）

> **来源**：`.pipeline/PIPELINE.md` 的 `G-337`② —— `T-401`（`7038b368`）已把**代码侧** `voice_role` 由 8 值取代为 **4 值角色**（单一常量 `src/server/modules/ai_agent/domain/voices.py::VOICE_ROLE_VALUES`；`domain/tools.py::_VOICE_ROLE_ENUM` 引用它并写进 **AI 工具 schema 的 `enum`**），而**设计资产侧 `tools.js` 仍是无 `enum` 的泛描述**（`G-148` 假脏面已失效：实测零假脏）⇒ 本条**纯同步**：⛔ 零业务代码改动（`src/**` 逐字节未动）。
> **改动面**（`data/ai-tools/tools.js` 单件，2 张卡 / 5 处）：

| 位置（`data/ai-tools/tools.js`） | 变更 |
|---|---|
| `SendTts` 卡 `openai.function.parameters.properties.voice_role`（`:1066`） | 补 `enum`：`["boy","girl","young_man","young_woman"]`；`description` 由「发声角色（按用户喜好指定）」同步为「发声角色（4 值角色：少年男 boy／少年女 girl／青年男 young_man／青年女 young_woman；按用户喜好指定。具体音色由「语言＋TTS供应商」的配置解析，⛔ 不要填音色名）」 |
| `set_preference_config` 卡 `description` ＋ 同卡 `openai.function.description`（两处逐字相同） | 「`reply_voice`（**TTS 音色**）」⇒「`reply_voice`（播报**音色角色**：boy／girl／young_man／young_woman）」；「后续播报会用已确认的 `reply_voice` **音色**」⇒「…用**已确认角色解析出的音色**」 |
| 同卡 `pref_key.description` | 「`reply_voice`（音色）」⇒「`reply_voice`（音色角色）」 |
| 同卡 `value.description` | 「`reply_voice`=**音色名**」⇒「`reply_voice`=**角色码**（boy／girl／young_man／young_woman；⛔ 不要填音色名）」；语言补「（zh/en）」 |

- **⛔ 未改**：工具卡数量（`AI_TOOLS.tools` **47**／`categories` **12** 组／分组面卡 **45**）／`promptTemplates`／`toolVisibility`／REST 端点卡（**252**）／`protocol.js` 自述数字／任何 `src/**`。
- **四件套（`G-337①` 口径）**：① **CHANGELOG** ＝ 本条（域 04 自己的册）；② **`publish` 镜像** ＝ `publish/api/data/ai-tools/tools.js` 单件 `Copy-Item` 逐字节 ＋ `publish/api/CHANGELOG.md` 同批逐字节（本条）；③ **期望表** ＝ `scripts/check-design-asset-render.mjs::checkAITools` 的 12 组 l2 ／内容区 45 ／卡数 45 —— **核对后无需改**（本件不增删组与卡）；④ **渲染镜像** ＝ viewer 数据↔渲染器双向一致 ＋ 渲染门禁 `0 页错误`，逐项读数见下。
- **门禁读数**：`scripts/check_design_asset_reconciliation.py`（三面全跑，**含渲染面**）＝ **rc=0、3/3 全绿**：golden `units=279／dependency=4969／assetRef=0`；渲染镜像期望表 `一级节点=12／每节点二级数正确／二级可表达／内容区=45／卡数=45／0 页错误`；自述面 `grouped_cards=45／total_tools=47／internal_tools=2／group_count=12` —— **与改动前基线逐项相同**（纯散文＋`enum` 改动 ⇒ 零渲染面变化）。`scripts/sync_design_to_publish.ps1 -Check` ＝ **GREEN**（新增/修改/删除/已作废残留四类均 0）。`node scripts/check-viewer-data-consistency.mjs --views api-rest` ＝ **4 断言 ＋ 0 页错误**（数据量 252 ↔ 渲染量 252）。`scripts/check_ai_tool_card_parity.py` ＝ **全绿**（P3-1~P3-5，含 `design ↔ publish` 逐字节）。
- ⚠️ **未覆盖面（如实登记）**：**无任何机械门禁核对 `voice_role` 的 `enum` 值** —— `check_ai_tool_card_parity.py` 的 `P3-4` 只比 `type/min/max/items.type`（`TYPE_KEYS` 刻意不含 `enum`），本器三面只比计数 ⇒ 本条同步**没有判据守着**，下次再漂移不会被机器拦下（同 `T-417` journal 已登记的族）。⛔ 本条不越权立件、不新建门禁。

## v163 → v164：`R-068`／`T-904` T2 —— `data/ai-tools/tools.js` 新增 **`AI_TOOLS.toolVisibility` 只读投影**（按床组态裁剪工具面登记表；role 型 23 ＋ 常驻 25）（2026-10-23）

> **来源**：`R-068`（`Q-069` 定稿：A/B 实测准确率 +4.72pp／局部床输入 token −48.6%／延迟不升、三条预注册门限全 PASS）⇒ `T-904` T1~T3 实现。T2 同批把「工具 ↔ 必需 `device_type`」映射**登记为设计资产**（Lead 2026-10-23 批准：**单一真相源＝运行时** `domain/tool_visibility.py`，设计侧只是**只读投影**并由门禁对账）。
> **改动面**：`data/ai-tools/tools.js` 文末新增 `AI_TOOLS.toolVisibility`（`JSON.parse` 严格 JSON：`resident` ＝ 25 个常驻工具名；`role` ＝ 23 项「工具 → 必需 `device_type[]`（**任一存在即保留**）」；含 `Q-069 §10.2-1` 提常驻的 `SavePositionMemory`／`RecallPositionMemory`）。⛔ **未改**任何工具卡（`AI_TOOLS.tools` 仍 47 卡）／`categories`／`promptTemplates`／REST 端点卡。
> **单一真相源与对账**：运行时常量 ＝ `src/server/modules/ai_agent/domain/tool_visibility.py`（`ROLE_TOOL_DEVICE_TYPES` ＋ `RESIDENT_TOOL_NAMES`）；设计侧投影由 `tests/api/ai_agent/test_tool_visibility_gate.py::test_design_asset_registry_matches_runtime_mapping` **逐值对账**（漂移判红），并由 `test_publish_mirror_identical_to_design_asset` 断言 design↔publish **字节相同**。
> **镜像**：`publish/api/data/ai-tools/tools.js` 单件 `Copy-Item` 逐字节同步（双侧 sha256 相等）；`publish/api/CHANGELOG.md` 同批逐字节同步。
> **计数**：REST 端点卡 **252** 不变；AI 工具卡 **47** 不变；`categories` **12** 组不变。
> **门禁读数**：`scripts/sync_design_to_publish.ps1 -Check` ＝ **GREEN**（新增/修改/删除/已作废残留四类均 0）；`node scripts/check-viewer-data-consistency.mjs --views api-rest` ＝ 4 断言 + **0 页错误**；`tests/design-regression/test_ai_tool_card_parity_gate.py` 绿；`tests/api/ai_agent/test_tool_visibility_gate.py`（含投影对账 2 条）绿。⚠️ `tests/design-regression/test_publish_content_parity_gate.py` 的 publish 面在**提交前**必为 `CONTENT_MISMATCH`（工作树 ≠ index blob）——本批不自行 commit，Lead 提交 publish 镜像后即自动转绿。
> **对读（同批代码）**：`tool_visibility.py`（T1 映射＋T2 观测/缓存）／`domain/tools.py::tool_definitions(only=)`／`domain/device_context.py::build_tool_calling_rules`（P4 条件规则）／`application/conversation_coordinator.py`（工具面接线＋观测＋flag）／`config/settings.py`（`ai_tool_filter_enabled`／`ai_tool_filter_shadow`）／`tests/api/ai_agent/test_t2_tool_face_integration.py`／`test_tool_rules_consistency.py`。

## v162 → v163：`R-068`／`T-903`（AI 工具面补齐高度调节）—— `data/ai-tools/tools.js` 新增 `StartAdjustHeight`／`StopAdjustHeight` 两卡（2026-10-23）

> **来源**：需求方 2026-10-23 03:1x 指正——「设备侧有高度控制设备和角度控制设备，AI 的工具面就应该有高度控制工具和角度控制工具，否则 AI 的操作不全乱套了」。设备侧协议 `height_adjust`／`height_adjust_stop`、catalog `height_control`／`position_control`、事件类 `HeightAdjustCommand`／`HeightAdjustStopCommand` **均已存在**，缺的只是 AI 工具面（46 → 48）⇒ 本卡补齐。
> ⚠️ **版本号口径**：改前本文件实际最大号 ＝ **`v162`**（`R-064`／`T-898` 占用）⇒ 本条接续 **`v162 → v163`**；`v162` 及更早条目**逐字保留**。

| 位置（`data/ai-tools/tools.js`） | 变更 |
|---|---|
| `StartAdjustHeight` 卡（新增） | `action=height_adjust`；`category=姿态控制`；`openai.function.parameters`：`role`（`height_control`／`position_control`）、`mode`（`absolute`／`relative`）、`value`（integer，**毫米 mm，−2000~2000**），`required=[role,mode,value]`；与运行时 `TOOL_SPECS` 逐字一致（parity 门禁校验） |
| `StopAdjustHeight` 卡（新增） | `action=height_adjust_stop`；`role` 可选（不填＝停止全部）；`required=[]` |
| `AI_TOOLS.categories["姿态控制"].tools` | 由 `[StartAdjustAngle, StopAdjustAngle, SavePositionMemory, RecallPositionMemory]` 扩为 ＋`StartAdjustHeight`、`StopAdjustHeight`（4→6；全卡 45→47） |

- **⛔ 未改**：`data/iot/*.js`（协议卡/枚举零改动 —— `height_adjust` 早已存在）、角度工具语义、其它 44 张卡。
- **配套代码**（同批 commit）：`src/server/modules/ai_agent/domain/tools.py`（2 ToolSpec ＋ `DEVICE_CONTROL_TOOL_NAMES`）、`cacheability.py`（`StartAdjustHeight` 与角度同规则；`StopAdjustHeight` 入可选 role 表）、`src/server/events/base.py`（`HeightAdjustCommand` docstring/注释 `cm`→`mm`，以协议为准）、`device_context.py`（v3 目标选择六条）。

## v161 → v162：`R-064`／`T-898`（单床绑定详情多睡眠位 500 小修）—— `rest-binding-device` 新增可选 `zone` 与 409 `zone_required`（2026-10-22）

> **依据**：`R-064` 原文（`docs/requirements-backlog.md` 已出册进 `T-898`；2026-10-22 需求方「内容不大的话就可以加（进今晚发版）」）。根因背景：`T-833` 只把 `get_by_imei` 改成多行容错，同族的 `CloudUserBindingRepository.get(device_id)` 未改（`scalar_one_or_none()`）。
> **病根／现场**：`GET /api/v1/sleep-monitor/bindings/{device_id}` 一床**两行**（线上 `CS-0002` 的 `left`＋`right`）⇒ `MultipleResultsFound` ⇒ **500**；2026-10-22 实测 `CS-0001` 单行 200 ／ `CS-0002` 双行 500 ／ 列表接口 200。前端自 `T-079` 起**无调用方**（`api/sleepData.ts` 的 `getBinding` 已删除）⇒ 后台页面不受影响，直接调 API／回归脚本才命中。
> **改什么（`rest-binding-device` 卡）**：
> ① 新增**可选 query** `zone: enum?(left|right|both)`；
> ② **多行 ＋ 显式 `zone`** ⇒ 200 该 `(device_id, zone)` 行；**多行 ＋ 缺省 `zone`** ⇒ **409**（`detail.reason="zone_required"`，与 `rest-binding-unbind` **同口径**，⛔ 不静默挑一条）；
> ③ **单行**（含唯一 `both`）⇒ 200，**行为逐字不变**（响应体/状态码/字段集）；
> ④ 该 `zone` 无行 ⇒ **404**（与「床无任何绑定」同一出口）；`zone` 非枚举值 ⇒ **422**；
> ⑤ 卡内补 `errors` 口径（403／404／409／422）。
> **实现对读**：`SleepDataService.get_binding(device_id, *, tenant_id, zone=None)` 复用与手动解绑**同一份**行解析（`_binding_row_for_device`：单 `both` 优先／`left`+`right` 歧义 ⇒ `zone_required`）＝**唯一多行判据**，⛔ 不另立第二份；`ZoneRequiredError` ⇒ 路由 409；响应 DTO 仍为共用 `_binding_dict`（`Q-041` B9：不含 `remoteUserId`）。
> **⛔ 未改**：绑定列表 `rest-binding-list` 的契约；`rest-binding-unbind` 的语义（其既有 `zone_required` 口径逐字不变）；前端（无调用方，`sleepData.spec.ts` 继续钉住不得回归）。
> **回归**：`tests/api/sleep_data/test_t898_binding_detail_multi_zone.py` 4 条（多行+zone 200／多行缺 zone 409／单行逐字不变 200／多行不存在 zone 404＋非法 zone 422）。


## v160 → v161：`R-065`／`T-897`（AI 语音三修 · A「按摩≡震动」等价语义）—— `data/ai-tools/tools.js` 的 `StartMassage`／`StopMassage` 卡（2026-10-22）

> **依据**：需求方 2026-10-22 裁决 `U-1`（记录见 `.pipeline/journal/2026-10-22-v230-release.md` §三 ＋ `docs/requirements-backlog.md` 归档件「2026-10-22 出册批（R-065→T-897）」原文）——「**按摩」与「震动」在 AI 侧视为等价/近义语义**；组态没有按摩执行器、只有震动执行器时，用户说「按摩」可能就是指 `vibration`。实现层落点见 `.pipeline/journal/2026-10-22-v230-release.md` §三 与 `tool_executor._equivalent_spec`（三条同时成立才映射）。本件把**实现已落代码**的口径同步进 API 设计卡的**工具描述**面，⛔ 不改协议卡/枚举、⛔ 不改 `alembic/`。
> ⚠️ **版本号口径**：改前本文件实际最大号 ＝ **`v160`**（`Q-041`／`T-894` 占用）⇒ 本条接续 **`v160 → v161`**；`v160` 及更早条目**逐字保留**。

| 位置（`data/ai-tools/tools.js`） | 变更（改前 ⇒ 改后） |
|---|---|
| `StartMassage` 卡 · 顶层 `description` ＋ `openai.function.description` | 原句「启动智能床的按摩功能（强度 3 档、速度 5 档，可设定时长）。」⇒ 同句 ＋「🔴 `R-065`／`T-897`（U-1）：本机**无 `massage_device`、只有 `vibration_device`** 时，本工具在**工具层**等价映射为 StartVibration（`role.device_type` 改写 massage_device→vibration_device，`speed` 丢弃并记日志）；有按摩执行器的机型零变化。」 |
| `StopMassage` 卡 · 顶层 `description` ＋ `openai.function.description` | 原句「停止按摩。不指定 role 则停止全部。」⇒ 同句 ＋「🔴 … 等价映射为 StopVibration（下发 `vibration{mode:\"off\"}`）；⚠️ 只保证「按等价语义下发」，**不保证设备物理停止**（`vibration{mode:off}` 的 400 属 `Q-065` U-3 硬件面，⛔ 本批不改协议）。」 |
| 两卡 · `notes`（本批新增） | 记录 A2 的三条映射判据、`speed` 丢弃、`role.device_type` 改写、`StopVibration` 的 `payload_override={\"mode\":\"off\"}`（`T-736` 既有）、未给 role 时 StartMassage 不映射（交回既有 fail-closed）；⛔ 不动协议卡/枚举、有按摩执行器的机型零变化 |

- **⛔ 未改**：`data/iot/*.js`（协议卡/枚举：`U-3` 未定，`vibration{mode:off}` 语义不动）、`StartVibration`/`StopVibration` 卡、工具 `required`/参数面、任何实现代码（实现改动见 `Q-065` 任务回执与代码提交）。
- **风险声明（逐字进回执）**：A 修完后「关闭按摩」稳定走 `StopVibration`，但设备若不认 `mode=off`（`U-3` 待硬件）仍不会物理停、话术仍播"已关闭"——本批**不解决"谎报"**，只解决"解释/走错工具/三路分裂"；⛔ 不得写成"关闭不响应已修复"。

## v159 → v160：`Q-041`／`T-894`（`R-053` 身份面剩余规则）—— `rest-binding-list`／`rest-report-daily` 响应**不再外露 `remoteUserId`**（2026-10-22）

> **依据**：`Q-041/design.md` §0.2 主张②「**业务域只认监控设备**，通过睡眠域获取报告与数据，**不关心虚拟用户的绑定**」＋ §2 **B9**（「`remote_user_id`/`remote_phone` 属睡眠域内部实现细节，⛔ 不外露到接口面」；任务 `T-894`／`R-053` 取件）。
> **改什么（仅**减少**响应键，⛔ 不新增/不改既有键）**：
> ① `rest-binding-list`（`GET /api/v1/sleep-monitor/bindings`）—— DTO 删 `remoteUserId`；该 DTO 由列表与详情（`rest-binding-device`）**共用** ⇒ 两卡同口径；
> ② `rest-report-daily`（`GET /api/v1/sleep-data/reports/daily`）—— `reports[]` 删 `remoteUserId`。
> **实现对读**：`src/server/modules/sleep_data/application/services.py::_binding_dict` / `get_daily_report`；前端 `src/web-admin/src/api/sleepData.ts` 的 `BindingItem`/`ReportEntry` 同步删除（全仓**零消费方**：设备端/模拟器/文档/视图均无引用，grep 证据见 `T-894` 报告）。
> **⛔ 未改**：`rest-binding-unbind` 的语义与请求/响应（本就不含该键）；`stolenDetected`（`RB-020` 纯增量键）逐字不动；`cloud_user_bindings` 内部列仍保留（只是不出现在任何 REST 响应）。
> **同批实现层规则（无接口形状变化，故不改卡正文）**：`Q-041` **B1**（身份唯一锚＝imei，身份写点白名单）／**B2**（换床 ⛔ 用平台 `bind`/`unbind`，调用点白名单）／**B3**（平台侧只允许两类写 ＋ 运维兜底解绑）／**B4**（`status==3` ⛔ 单独作"被第三方占用"证据，判定点唯一）—— 机械判据 ＝ `scripts/check_identity_rules.py`（`--self-test` 15/15）。
> **门禁**：`scripts/check_identity_rules.py` rc=0；`pytest tests/design-regression/test_identity_rules_gate.py`、`tests/api/sleep_data/{test_b4_occupancy_judgement,test_binding_stolen_detection,test_binding_stolen_visibility,test_sleep_data_api}.py`；前端 `vitest stoleDetected.spec.ts` ＋ `npm run build`。


## v158 → v159：`Q-063`／`T-893`（R-063 音乐轮提速 · 批A A-1/A-4）—— `data/ai-tools/tools.js` 的 `PlayMusic`/`SearchMediaLibrary` 工具卡与 systemPrompt（2026-10-22）

> **依据**：需求方 2026-10-22 裁决「批A 先行 ＋ B-α」；方案与取证 ＝ `.pipeline/design/Q-063/{design,constraints,evidence}.md`（`Q-063/evidence.md` 的 `E2`/`E4`/`E5`/`H2`）。本件把**实现已落代码**的口径同步进 API 设计卡：⛔ 不改协议、⛔ 不改 `required`/`anyOf` 语义、⛔ 不改代码。
> ⚠️ **版本号口径**：改前本文件实际最大号 ＝ **`v158`**（`R-062`／`T-892` 占用）⇒ 本条接续 **`v158 → v159`**；`v158` 及更早条目**逐字保留**。

| 位置（`data/ai-tools/tools.js`） | 变更（改前 ⇒ 改后） |
|---|---|
| `PlayMusic` 卡 · `parameters.properties.media_id.description` | 改前：「可选。点名某一首时用它（**先用 SearchMediaLibrary 取曲目 id**）。与筛选条件…至少给一个。」⇒ 改后：「可选。**点名某一首**时用它（用户明确说了歌名/曲名）；泛放请只用 `tag_names`/`keyword`，⛔ **不需要先调 SearchMediaLibrary**。…」（**删除"先用 SearchMediaLibrary"的引导** —— 它主动把模型推向"先搜再放"） |
| 同卡 · 顶层 `description` ＋ `openai.function.description` | 追加同句：「🔴 **Q-063／T-893 补充**：泛放（「放点轻音乐」这类）⇒ **直接调本工具、⛔ 不要先调 SearchMediaLibrary**（先搜会多一轮 LLM ≈ +1.3s）；只有「点名某首/下一首/查曲库清单」才需要 SearchMediaLibrary。」 |
| 同卡 · `parameters.properties` | **新增 `loop`（boolean）**（A-4）：执行层 `tool_executor._request_media_play` 早已在读 `args.get("loop")` 并透传给域 09（再进 `PlayOnlineMedia.loop`），schema 一直缺该参数 ⇒ 模型看不见、永远填不了。⛔ 未改 `required`（仍 `[]`）／`anyOf`（仍 4 支）／其它参数类型 |
| 同卡 · `notes` | 追加**取代注记**（`rule-0d`：旧句一字未改）：记录 A-1 删除 search 引导 ＋ A-4 补 `loop` 的缘由与边界（`event_class` 仍 `None`；B-α 终态语义走 `ToolSpec.delegated_device_action`，⛔ 不用 `event_class`） |
| `SearchMediaLibrary` 卡 · 顶层 `description` ＋ `openai.function.description` | 追加：「⛔ **泛放不需要先调本工具**：直接 PlayMusic(tag_names/keyword) 由音乐库选曲；本工具用于：① 问「有什么歌/曲库清单」；② 换曲/下一首（mode=next）；③ 点名某首的精确定位（by_name）。」（`E5`：该工具**仍必须存在**，⛔ 不删） |
| `systemPrompt.sections` | 新增两行：① 音乐泛放 ⇒ 直接 `PlayMusic(tag_names/keyword)`、⛔ 先搜（只有三种情况例外）；② 话术边界：只说「好的，我来为你放 X」/「正在为你找 X」，⛔ 不说"已开始播放"（`E4` 两类假成功约束） |

- **跨域同批（`design/02-business-workflow/data/04-ai-agent.js`）**：处理链路段落的「`SearchMediaLibrary → PlayMusic → SendText` 3 轮」说明追加**取代注记** —— 批A 后泛放 3→2 轮、B-α 后 1 轮收尾＋静默门禁；⚠️ `MAX_TOOL_ROUNDS` **仍为 3**（本批不改上限）。⚠️ **`design/02-business-workflow/` 无 CHANGELOG 册**（该域历来不设）⇒ 本条**兼记**该处同步，避免留无主改动。
- **⛔ 未改**：协议 `play_online_media`（`data/iot/smart-bed.js`）／`event-contracts.js` 的 producer／`ControlMusic` 卡／`PlayMusic.required`/`anyOf`／任何实现代码（实现改动见任务 journal 与代码提交）。

## v157 → v158：`R-062`／`T-892`（`Q-061` 甲4）—— `data/ai-tools/tools.js` 的 `SendText` 描述改为「**仅供长文本/结构化屏显**」（2026-10-22）

> **依据**：实现已落代码（`R-062`／`T-892` 第一批）—— `device_context.TOOL_CALLING_RULES`（甲1）明确「**在同一条回复里**先用一句 ≤40 字简短中文口语确认/说明，再发起 function call；⛔ **不要**用 `SendText` 工具代替这段口语回复（那是屏显，不是说话）」。本件把该口径同步进 API 设计卡的**工具描述**面，⛔ 不改代码、⛔ 不改账本。
> ⚠️ **版本号口径**：改前本文件实际最大号 ＝ **`v157`**（`Q-060` 收尾批占用）⇒ 本条接续 **`v157 → v158`**；`v157` 及更早条目**逐字保留**。

| 位置（`data/ai-tools/tools.js`） | 变更（改前 ⇒ 改后） |
|---|---|
| `SendText` 卡 · 顶层 `description` | 改前：「向智能床设备发送文本消息（告知用户操作结果）。」⇒ 改后：「向智能床设备发送**长文本/结构化屏显**消息。🔴 `R-062`/`T-892`（2026-10-22）：**仅供长文本/结构化屏显**；常规回话走同轮口语确认，⛔ 不要用它代替说话。」 |
| 同卡 · `openai.function.description` | 同上（与顶层 `description` **同句同源**，避免模型侧与文档面两套措辞） |

- **⛔ 未改 `SendTts` 语义**：`SendTts` 的 `description`／`action`／参数面**一字未动**（本批只订正 `SendText` 的「不是说话面」定位）。
- **`systemPrompt` 段核查**：`tools.js` 的 `systemPrompt.sections` **无**「用 `SendText` 告知结果」类措辞（实测），故无同批订正对象；`sampleOutput.persona` 为**样例渲染**（非工具卡语义），⛔ 不在本件改面。
- **镜像**：`publish/api/data/ai-tools/tools.js` 单件同步 ＋ `publish/api/CHANGELOG.md` 同步（双侧 sha256 相等）。

## v156 → v157：`Q-060` 收尾批 · `R-026`（位置记忆／召回）**域留痕**（`D23` 落点；2026-10-22）

> **依据**：[`.pipeline/journal/handover-2026-10-22-r026-r057-batch.md`](../.pipeline/journal/handover-2026-10-22-r026-r057-batch.md) 批① **A8**（`R-026` 收口；`rule-0d` 只追加）＋ 92 项落地清单的**收口读数与余项口径**以 [`docs/requirements-backlog.md`](../docs/requirements-backlog.md) 的「`R-026` 收口取代注」为准（⚠️ 第 3 层工作区件按 `J7a` 边界⛔ 不在此引用，其原文留档由 `docs/` 侧指向）。
> **`D23` 原义**（清单 `W1-18`）：落点＝**本文件（域根，非 `data/`）** —— 要求"设计变更留痕按本仓既有域惯例裁"；本仓 `design/01-raw-input/` **无** CHANGELOG.md、域 04 的在**域根** ⇒ 本条目即该裁决的落点。
> **本批未改本域设计卡**：`R-026` 清单里域 04 的 `D8`/`D9`/`D10`/`D11`/`D14`~`D17`**已在前批落地**，`D22`/`D23` 属**余 5 条**（待裁决/待写面放开）。⇒ 本条只补**留痕**，⛔ 不伪造设计变更、⛔ 不改任何 `data/**` 卡。

| 项 | 读数 |
|---|---|
| `R-026` 落地进度 | **83/92**（批① 76 ＋ 批⑨ +5 ＋ `T-603` +2） |
| 余 5 条（未落） | `D29`（`src/server/seeds/__init__.py`，写面冲突）／`D22`（`design/04-platform-api/data/iot/smart-bed-examples.js`）／**`D23`（本文件）**／`D13`（`design/03-entity-relationship/data/07-event-infrastructure.js`）／`D20`（`design/05-backend-architecture/data/module-boundaries.js`） |
| 判不做 4 条 | `D53`（仅当需要上行／stop 配对时）／`D76`（本件自身受约束）／`D39`（复用 `D40` 预检钩子、不新开分支）／`D68`（存在性正向断言，纯增不会红） |
| 本域 `D23` 状态 | ✅ **留痕已补**（本条）；⚠️ 其"设计内容变更"本身**无对象**（本批未改本域卡）⇒ 清单计数仍按 **83/92** 计，⛔ 不自升 84/92 |

**镜像与门禁**：`publish/api/CHANGELOG.md` **单件 `Copy-Item` 逐字节同步**（双侧 sha256 相等）；本批不部署 docs 站点（`Q-060` A-12 的第二次发布按需求方裁定**攒到下次一起发**）。
**⛔ 未改**：本域任何 `data/**` 卡；其它域 CHANGELOG。
**指针**：[`docs/requirements-backlog.md`](../docs/requirements-backlog.md)（`R-026` 收口取代注：83/92 ＋ 余 5 ＋ 判不做 4）／[`.pipeline/journal/handover-2026-10-22-r026-r057-batch.md`](../.pipeline/journal/handover-2026-10-22-r026-r057-batch.md) 批①。
⚠️ **第 3 层边界（`J7a`）**：本条⛔ 不引用未获批的第 3 层工作区稿（2026-10-22 修：原引用曾令 `test_design_boundary_gate` 判红）。

## v155 → v156：`Q-060` A-1（乙-11，2026-10-22）—— **14 张「登录即可读」端点卡权限口径订正：`permission: "device.read"` → `permission: null`**（⛔ 端点卡数量 / 路径 / 请求 / 响应语义未动）

> **依据**：需求方 2026-10-21 裁定「**保持实现（login-only）、改设计卡**」＋ `docs/web-admin-design-impl-deviations.md` §0.14 **A-1（乙-11）**：实现＝login-only，而设计卡仍写 `device.read` ⇒ 设计比实现**更严**。订正理由（已逐字落进 14 张卡的 `description`）：**该只读数据不含凭据；收紧成 `device.read` 会让「审计员」等无该码角色 403**。
> **实现实测锚点**（本批逐条对读；⛔ 未改任何代码）：`src/server/modules/device_control/api/router.py` 与 `src/server/modules/device_iot/api/router.py` 的 router 级依赖均为 `dependencies=[Depends(get_current_user)]`，这 14 个 GET handler **均无** `require_permission(...)`；租户圈定仍在（租户不符 ⇒ 403，本批未动）。
> **卡面表达口径（本仓设计卡首次采用）**：`permission: null` ＝「**无权限码（登录即可读）**」。14 张卡 `description` 前缀统一加 `🔴 **Q-060／乙-11（2026-10-22 订正）**…` 注记；`business_logic.preconditions` 同步补「登录即可读（无权限码）」（改前写作 `device.read` / 仅「登录 + X-Tenant-Id」）。

| 域 / 文件 | 卡片 id（共 14） | 变更 |
|---|---|---|
| 02 device-control · `02-core-domains.js`（11） | `rest-maintenance-diag-tree` / `rest-maintenance-diag-files` / `rest-maintenance-diag-file-detail` / `rest-maintenance-diag-download` / `rest-maintenance-network-messages` / `rest-maintenance-network-message-dates` / `rest-maintenance-network-messages-export` / `rest-maintenance-non-event-logs` / `rest-maintenance-device-errors` / `rest-maintenance-device-error-detail` / `rest-maintenance-firmware-packages` | `permission: "device.read"` → `permission: null`（＋ description 注记 ＋ preconditions 补「登录即可读」） |
| 02 device-control · `02-core-domains.js`（2，设备面） | `rest-device-usage-preference-merged` / `rest-bed-user-profile-get` | 同上；另 `rest-device-usage-preference-merged` 的 403 文案由「无 device.read 权限」改为租户不符口径（`X-Tenant-Id 非岗位租户 / 设备不属于当前租户`） |
| 01 device-iot · `01-device-iot.js`（1，设备面） | `rest-device-connection` | 同上 |

**计数（同源）**：REST 端点卡总数 **252 → 252**（14 张卡**只改字段**，⛔ 无新增/删除卡）；域 02 / 域 01 端点数各不变 ⇒ `tests/design-regression/test_rest_domain_migration.py` 的 `EXPECTED_MOUNTS` 与 total 断言**无需改**（本批实测 rc=0）。
**⛔ 未改**：14 张卡的 `id`／`addressing`（method+path）／`query_params`／`responses`／`caching`；`errors` 仅上述 1 处文案订正、其余行未动；其它任何卡；`src/**` 代码；`publish/api/**` ⛔ **未镜像**（属下一次设计稿发布批次，见 §0.14 A-12）。
**对读**：`src/server/modules/device_control/api/router.py`（`/maintenance/**` 11 个 GET ＋ 设备面 2 个 GET）＋ `src/server/modules/device_iot/api/router.py`（`/devices/{id}/connection`）＋ `docs/web-admin-design-impl-deviations.md`（A-1 / 乙-11）。
**门禁**：`node scripts/check-viewer-data-consistency.mjs --views api-rest` ＋ `tests/design-regression/test_rest_domain_migration.py`、`tests/design-regression/test_rest_response_field_subset_gate.py`（读数见本轮任务报告）。


## v154 → v155：`Q-060`（2026-10-21）—— 设计追平批：**新增审计端点卡** ＋ **导出卡权限订正** ＋ **设备认证端点卡 deprecated**（⛔ 既有卡仅此 3 处改动）

> **依据**：需求方 2026-10-21 裁定「都按建议」——审计日志 v1（5 类操作 / 90 天 / 既有 `audit.read`）、读接口权限按「读=read 码」修正、设备认证区整区下线。
> **版本号口径**：改前本文件实际最大号 ＝ `v154` ⇒ 本条接续 `v154 → v155`；`v154` 及更早条目**逐字保留**。

| 位置（`data/rest/`） | 变更 | 依据任务 |
|---|---|---|
| `system.js` `endpoints[]` 末尾 | **新增 1 张卡** `rest-audit-logs`：`GET /api/v1/audit/logs`，权限 `audit.read`；query=page/size/actor/action/result/date_from/date_to；响应 `{items[],total,page,size}`（items 14 字段）；写明租户隔离口径与只读性（append-only、无更新/删除端点） | task-5（审计 v1 后端） |
| `01-device-iot.js` `rest-device-export` | 权限码 `device.write` → **`device.read`**（纯读取导出：不写库/不落盘/无副作用，CSV 逐字不含 `device_secret`；只持 `device.read` 的客服专员/只读访客改前 403）；`preconditions`/`steps`/`errors[403]` 同批订正 | task-9（读接口权限修正） |
| `system.js` `rest-system-general-auth` | **标 `deprecated: true` / `removed: 2026-10-21（Q-060）`**，description 前缀【已下线／removed】；卡**保留留痕**（⛔ 不物理删卡）。实现侧路由已删除（PUT ⇒ 404/405）、前端卡片/API/i18n 同批删除、GET 的两键仅只读兼容 | task-7（设备认证区下线） |

**计数（同源三处，已同批刷新）**：域 08（`domain-system`）端点数 **39 → 40**；REST 端点卡总数 **251 → 252** ⇒ `tests/design-regression/test_rest_domain_migration.py`（EXPECTED_MOUNTS + total 断言）与 `scripts/check-design-asset-render.mjs`（`{name:'平台支撑',l2}` ＋ REST 卡数 252）同批改。
**⛔ 未改**：其它域任何卡；`publish/api/**` 未镜像（属下一次设计稿发布批次）。
**对读**：`system/api/router.py`（新增 `GET /audit/logs`；移除 `PUT /system/general/auth`）＋ `system/application/audit_log.py` ＋ `main.py`（采集中间件/启动 purge）＋ `device_iot/api/router.py`（export 权限码）＋ `tests/api/system/test_system_audit_logs.py` ＋ `tests/api/device_iot/test_read_permissions.py` ＋ `tests/api/system/test_system_general.py`。

## v153 → v154：`Q-060`（2026-10-21 需求方裁定）—— **帮助读取端点权限口径订正**（`data/rest/system.js` 的 `implementation.note`；⛔ 端点卡定义未动）（2026-10-21）

> **依据**：需求方 2026-10-21 裁定（「严重不匹配的先修，文档/设计滞后的修文档/设计」＋ 对偏差清单建议的确认）；工作区 `.pipeline/design/Q-060/`，分析见 `docs/web-admin-design-impl-deviations.md`（甲-1）。
> **事实**：改前 `GET /help/pages/{slug}` 与 `GET /help/locales` 的实现挂 `admin.read`；而「设备运维 / 客服专员 / 只读访客」三类预置角色的权限码集合都不含 `admin.read` ⇒ 三类角色在任意页面右侧帮助**恒 403 / 面板空**（实测 console：`缺少权限：admin.read`）。帮助正文是全角色阅读辅助，不是管理操作。
> **改动面**：`data/rest/system.js` 的 domain-08 `implementation.note` **追加一条订正**（保留原「权限读=admin.read 写=admin.write」原文与 `T-068`/`T-082`/`T-083` 等历史段，`rule-0d`）——写明两条**读取**端点改为登录即可读、**管理面口径不变**。
> **⛔ 未改**：`endpoints[]` 任何一张卡的 `addressing`/`response`/`id`；端点数不变；`publish/api/**` 未同步（属下一次设计稿发布批次）。
> **对读（同批代码）**：`src/server/modules/system/api/router.py`（两处 `Depends(get_current_user)`）＋ `tests/api/system/test_help.py::test_help_009`（正向：读端点无 `require_permission`；反向：`GET /help/pages`、`/help/media` 仍必须带门禁）。
> **门禁**：`pytest tests/api/system/test_help.py` 14 passed；`check-viewer-data-consistency.mjs --views api-rest` 见本轮复查记录。
## v152 → v153：`T-786`（`Q-054` 批 1）—— 上行改**三元式** `{sub_device_id, attr, value}` ＋ 38 类属性表 ＋ `light` 删色温 ＋ `device_fault` 加 `none`（2026-10-19）

> **依据**：`.pipeline/design/Q-054/final-changelog.md`（新增 N1~N15／修改 M1~M14／删除 X1~X11）＋
> `final-design.md` §2（三元式与三形态）§5（38 类属性表）§6（18 组 19 帧）＋ `constraints.md` 的 C-1~C-35 与 PA-1~PA-7。
> **本批只改协议资产**（`design/04-platform-api/**`）并镜像 `publish/api/**`；⛔ 不改 `src/**`（云端/模拟器/固件属后续批次）。
> ⚠️ **`rule-0d` 取代注**：本条目同时是**改前逐字留痕处**（被删/被替换的原文逐字收在 §三，⛔ 未静默删除）。

### 一、`data/iot/smart-bed.js`（协议卡面）

- **`status_notification`**：`sub_device_events[]` 的行结构由「三槽权威轴（`event`／`status`／`value`）」改为
  **三元式** `{sub_device_id, attr, value}`（C-24）：`attr` 是闭集属性名、`value` 是唯一取值槽、类型由 `attr` 决定（C-25）。
  - 行三形态：**属性行**（`attr`＋`value`，可附 `last_result`／`last_code`）／**故障行**（`fault_code`，`fault` 可选）／
    **结果行**（`last_result`＋`last_code`，无 `attr`、无 `fault_code`）；三者皆无＝非法行（丢弃＋WARN）。
  - **删** `event` 槽、**删** `status` 槽（C-15／C-24）；`sub_device_id` 改**必填**（C-11）。
  - **新增可选** `last_result`（`operation_result`）／`last_code`（int，0＝无错误）；缺报不判失败（C-5）。
  - `fault` 描述改 8 值（含 `none`）；`fault_code` 明确单值、可多次、`0`＝该子设备全清（C-8／C-9）。
  - `notes` 补：全量应答 `ref_id`＝trigger 的 `msg_id`（N7）／平台发起时机四条（含**连接起每 10 分钟**，C-18）／
    **传感器建议 60 秒且不是心跳**（C-6／C-7／C-17）／**超限直接分多条、无 `total`/`seq`/`batch`、不要求按序或全收**（C-33）。
- **`status_notification_trigger`**：payload 三筛选项**一字未改**；`notes` 的「② 周期兜底（规划）」改为
  「**自连接上线起每 10 分钟一次**（平台侧定时器，设备零改动）」，并补「响应帧信封 `ref_id` ＝本 trigger 的 `msg_id`」。
- **`sharedEnums.device_type`**：每个取值新增 **`attributes[]`** ＝ 该类型在 `status_notification` 里的上报属性闭集
  （`attr`＋`type`＋`unit`/`min`/`max`/`values`；`attributes: []`＝只报故障行），并新增 `last_result_reportable`。
  ⇒ **38/38** 个 `device_type` 全覆盖（与 `DEVICE_TYPE_CATALOG` 集合相等）；`is_running` **15 类 ＝ 12 必选 ＋ 3 可选**
  （可选类＝`display_output`／`voice_monitor`／`tts_generator`＋`voice_output`，共 4 行折 3 类，C-30）。
- **`light`**：**删** `color_temp` 整项（C-26／C-28）；`color` 补**值域 0~16777215**（`0xFFFFFF`）；
  `brightness` 保持 0~100（**0＝关灯**）；⛔ 不加 `power`。
- **`sharedEnums.device_fault`**：新增第 8 值 **`none`**（未分类／无类别；≠「无故障」，清除仍用 `fault_code=0`）。
- **`sharedEnums.device_event`**：**整枚举删除**（唯一消费点＝已删的 `event` 槽；`src/**` 零消费者；见 §三留痕）。
- **`position_control`**：沿用 `T-784`／C-35 已落的 mm 口径（控制面 `height_adjust`、报告面 `height`＋`is_running`），
  本批只在该类型的 `attributes[]` 里把它**逐项写成与 `height_control` 同口径**，⛔ 不重写既有取代注。

### 二、`data/iot/smart-bed-examples.js`（示例帧同步）

- `status_notification` 的示例改三元式（属性行＋故障行），删 `event`／`status` 两键；描述改为增量口径。
- `light` 示例删**色温**取值（`color` 保持数值）；描述去掉色温字样。

### 三、`rule-0d` 改前逐字留痕（原文照抄，⛔ 未删）

**(1) 被删的 `status_notification` 字段表（含 `event`／`status`／`value` 三槽）：**

```js
          sub_device_events: { type: "array", optional: true, desc: "子设备事件列表。每个子设备以其 sub_device_id 唯一标识——其设备类型、位置、分区均通过注册时信息可查，上报无需重复携带。value/status/fault/event 为该设备当前状态的属性。🔴 **三槽权威轴**（`T-780`／`Q-054` 批 E，逐槽钉死、⛔ 不得互换）：`event` ＝ **动作阶段**（start/running/stop，阶段语义的**唯一**权威）；`status` ＝ **状态值／模式／档**（如 on/off、sleep/cinema、low/medium/high），⛔ **不承担** start/stop/running 语义；`value` ＝ **数值**（角度/温度/亮度/强度等），⛔ 不放阶段或状态串", fields: {
              sub_device_id: { type: "string", desc: "子设备ID——connect 注册时配置。平台据此关联该设备的所有属性" },
              event:      { type: "enum", ref: "device_event", desc: "动作阶段：start/stop/running。🔴 三槽权威轴之「阶段」槽 —— **唯一**承载阶段语义的槽（`status` ⛔ 不承担该语义）" },
              status:     { type: "string", desc: "状态值／模式／档。对应后端枚举的字符串表示（如 mode 值 sleep/cinema、档位 high/medium/low、on/off/dim 等）。🔴 三槽权威轴：本条 ⛔ **不承担** start/stop/running —— 阶段只认 `event`" },
              value:      { type: "float", desc: "数值。具体事件发生的数值（如温度、角度、亮度、强度等）。🔴 三槽权威轴：本条只放**数值**，⛔ 不放阶段/状态串" },
              fault:      { type: "enum", ref: "device_fault", optional: true, desc: "故障类别。无故障时不传。7 大类：communication/mechanical/execution/power_supply/protection/user_intervention/sensor_acquisition" },
              fault_code: { type: "int", optional: true, desc: "具体故障码。与 fault 字段配合查表。编码段：1xx 通信/2xx 机械/3xx 执行/4xx 动力/5xx 保护/6xx 干预/7xx 传感采集" }
```

**(2) 被整块删除的公共枚举（动作事件，3 值）：**

```js
    device_event: {
      name: "device_event", desc: "设备动作事件",
      values: [
        { value: "start", desc: "开始动作" },
        { value: "stop", desc: "结束动作" },
        { value: "running", desc: "动作中" }
      ]
    },
```

**(3) 被替换的 `light` 下行 payload（含已删的色温参数）：**

```js
      envelope: { payload: { sub_device_id: { type: "string", required: true, desc: "灯具子设备ID（connect 注册时上报）" }, duration: { type: "int", required: false, desc: "持续时长（秒），最多运行时间，不填=不限" }, brightness: { type: "int", required: false, desc: "亮度 0-100（步进 1）。0=关灯" }, color_temp: { type: "int", required: false, desc: "色温（K，范围 1000~9000）。范围受平台按 device_type 固化的能力边界（unit: kelvin）约束" }, color: { type: "uint32", required: false, desc: "RGB 颜色值，32位无符号数（0xRRGGBB）" } } },
```

**(4) 被替换的 `status_notification_trigger.notes` 第 ② 条：**

```text
② **周期兜底**（**规划**：批 B 在影子侧记「最近一次该设备上报时刻」，超阈值再发**同一条** trigger；阈值可配、默认宽松，⛔ 不改本卡 payload）
```

### 四、刻意未动

- action 名（`status_notification`／`status_notification_trigger`）、信封字段、`sub_device_events` 键名。
- `status_notification_trigger.envelope.payload` 三筛选项（`device_type`／`position`／`zone`）。
- `firmware_update_progress`（`stage` 5 阶段 ＋ `result` ＋ `url`，⛔ 不加百分比）。
- `fault_code` 18 个三位码、`fault` 其余 7 大类、`heartbeat` 与在线判定、`connect` 组态。
- ⛔ 不新增分帧标记（`total`／`seq`／`batch`）；⛔ 不给灯加 `power`；⛔ 不给 OTA 加百分比。

### 五、镜像

- `publish/api/data/iot/smart-bed.js`、`publish/api/data/iot/smart-bed-examples.js`、`publish/api/CHANGELOG.md`
  与本件同批镜像（`D-272` 单件 `Copy-Item`；`scripts/sync_design_to_publish.ps1 -Check` 四类 0）。


## v151 → v152：`T-780`（`Q-054` 批 E）—— `status_notification` 口径反转（**设备只报变化**、全量由平台 `status_notification_trigger` 发起）＋ 三槽权威轴写死（2026-10-18）

> **来源／依据**：需求方 **2026-10-18 定稿口径**（`.pipeline/design/Q-054/design-status-report-v2.md` 的「9 条定稿口径」）：**设备只报"变化了的状态"（增量）**、没变化不发、**绝不主动推全量**；**全量由平台发起**，复用既有 `status_notification_trigger`（三个筛选项全空＝全量）；**action 名不改**；本轮**不引入** `props` 属性字典。
> **改的是什么**：① `status_notification` 卡的 `description` 由「变化时主动上报全量」改为「只报变化（增量）＋ 全量由平台 trigger 发起」；② 该卡 `notes` 的「① 设备 connect 后主动推」改为「① 变化时只报变化」；③ `sub_device_events` 及其 `event`／`status`／`value` 三槽写死**权威轴**；④ `status_notification_trigger` 的 `notes` 补「何时由平台发起」四条（上线后 3s／周期兜底／用户刷新／不信任设备）。
> ⚠️ **版本号口径**：改前本文件实际最大号 ＝ `v151`（`T-743` 占用）⇒ 本条接续 `v151 → v152`；更早条目**逐字保留**（本条插在文件顶部）。
> ⚠️ **`rule-0d` 取代注**：被替换的原句**逐字保留**在 `data/iot/smart-bed.js` 的 `// 🔴 T-780` 注释块里，⛔ 未静默删除。

### 一、`data/iot/smart-bed.js`（协议卡面 —— 本批唯一的设计数据资产改动）

| 位置（改后行号） | 字段 | 改前 | 改后 |
|---|---|---|---|
| `:425` | `status_notification.description` | 「设备 connect 成功后必须立即发送 status_notification 上报初始全量状态；之后任意部件状态变化时也要主动上报全量状态。」 | 「**设备只上报变化了的状态（增量）**：只上报变化的子设备；**没变化不发**，⛔ **绝不主动推全量**。**全量一律由平台发起**：平台经 `status_notification_trigger`（三筛选项全空＝全量）反向拉取。」（原句按 `rule-0d` 逐字留在注释块） |
| `:432` | `status_notification.notes` | 「① 设备 connect 后按本卡主动推；② 平台反向拉」 | 「① 设备状态变化时按本卡**只报变化**（增量，⛔ 不推全量）；② 平台反向拉（覆盖首连对齐／周期兜底／用户刷新／不信任设备）」 |
| `:436` | `sub_device_events.desc` | 「value/status/fault/event 为该设备当前状态的属性」 | 追加 **三槽权威轴**：`event`＝动作阶段（唯一权威）／`status`＝状态值·模式·档（⛔ 不承担 start/stop/running）／`value`＝数值（⛔ 不放阶段或状态串） |
| `:438` | `event.desc` | 「动作阶段：start/stop/running」 | 追加「三槽权威轴之「阶段」槽 —— **唯一**承载阶段语义」 |
| `:439` | `status.desc` | 「状态值。对应后端枚举的字符串表示…」 | 追加「三槽权威轴：本条 ⛔ **不承担** start/stop/running —— 阶段只认 `event`」 |
| `:440` | `value.desc` | 「数值。具体事件发生的数值…」 | 追加「三槽权威轴：本条只放**数值**，⛔ 不放阶段/状态串」 |
| `:475` | `status_notification_trigger.notes` | 只有 `T-566` 的「反向拉取约定」 | 追加「**何时由平台发起**」四条：① 上线后约 3s（已实现）② 周期兜底（规划，批 B）③ 用户刷新（已实现，REST 手动触发）④ 不信任设备（按需再拉） |

### 二、刻意未动（⛔ 不扩面）

| 面 | 为什么不动 |
|---|---|
| `action` 名（`status_notification`／`status_notification_trigger`） | 定稿口径明令**不改 action 名**；本轮只改语义表述与字段描述 |
| `status_notification_trigger.envelope.payload` 三筛选项 | 定稿口径要求**复用既有** `device_type`／`position`／`zone`（全空＝全量），⛔ 不改形状、⛔ 不加字段 |
| 公共枚举（含 `device_event`／`device_fault`／`intensity` 等） | 本轮⛔ 不动任何枚举值域 |
| `props` 属性字典 | 定稿口径明令**本轮不引入**（7／8 类用现有三槽即可） |

### 三、镜像口径

| 件 | 处置 |
|---|---|
| `publish/api/data/iot/smart-bed.js` | ✅ `design` ↔ `publish` **逐字节相同**（单件 `Copy-Item` ＋ 双侧 `sha256` 相等） |
| `publish/api/CHANGELOG.md` | ✅ 本条目同步镜像（扩展映射行；单件 `Copy-Item` ＋ 双侧 `sha256` 相等） |
| `design/README.md`／`design/05-backend-architecture/sleep-report-analysis-T-029.md` | 不在映射表（`publish/**` 无对应件）⇒ ⛔ 无镜像动作 |

### 四、设备端影响（⛔ 本批不改固件，但语义变化须告知）

| # | 影响 | 说明 |
|---|---|---|
| 1 | 变化时**只报变化** | 设备⛔ 不再"每次变化都发全量"；平台侧影子做 diff，重复帧不产事件（既有能力） |
| 2 | 全量只在 trigger 后发 | 设备**必须**实现「响应 trigger」路径（`T-566` 已列为必须）；connect 后⛔ 不强制先推全量 |
| 3 | 三槽语义 | `status` ⛔ 不再承载 start/stop/running；阶段只认 `event`（旧固件若仍双填，平台侧不做拦截，留待后续批） |

### 五、批 A 同任务的设计资产追加（同源；实现侧见 `src/server/CHANGELOG.md` 的 `v154`）

> 口径：`T-780` 的**批 A**（纯云端）在实现侧补 7 类的映射/槽位后，按「**设计资产双向同源**」把两件数据资产同批补齐（⛔ 不是新协议、⛔ 未改任何 action/枚举）。

| 件 | 处置 | 双侧 sha256（相等） |
|---|---|---|
| `data/iot/event-identifier.js` → `publish/api/data/iot/event-identifier.js` | 补派生键 `bed_position_memory → DeviceMessage.ActionPosture`（**最小存在性**）；与代码侧 `DEVICE_TYPE_DERIVED` **双向同源**（单边新增会被 `test_design_derived_key_set` 判红） | `CB90AD25D09CF730A12EE11ECB903B264637E6CD33990212130DBEE814EE640E` |
| `design/05-backend-architecture/data/event-contracts.js` → `publish/architecture/data/event-contracts.js` | 5 条 `fields` 补槽：`ActionPosture`＋`slot`、`ActionLulling`＋`status`/`intensity`、`OutputAudio`/`OutputDisplay`/`OutputTts`＋`status`（`gen_event_contracts.py --check-fields` rc=0） | `10BE6C2C51F01F27560C5DF1EB60AF757D023C187426CFC09437A9E52F8D1360` |

> ⚠️ **本条只登记「与本域有关的两件」**：`event-contracts.js` 属 05 域，其变更的完整口径见该域 `CHANGELOG.md` 的 `v72 → v73` 与本条；⛔ 两处均未改协议卡字段。

## v150 → v151：`T-743`（`Q-031`）—— `show_text.body.maxLength` **512 ⇒ 4000**（超出由**设备端自行分页**）＋ 睡眠报告语音上限 **1000 字**（< 300 s）（2026-10-17）

> **来源／依据**：需求方 **2026-10-17 逐字裁定**：「**文本 4000 字那个就完全放开限制，全都改成 4000 就可以了，多了就让设备自己分页去**。然后**语音那个东西改成 1000 字**吧，1000 字应该够用了，**也不应该超过 300 秒**。**按这个口径改所有的代码、设计资产和协议**，然后**更新**。**如果协议改了，那就也发布一下**」。
> **改的是什么**：① 协议卡 `show_text` 的正文上限由 `512` 放开到 **`4000`** 字符（`desc` 同步写明**设备端自行分页**）；② 睡眠报告**语音**文案上限 `4000 ⇒ 1000` 字（≈200–250 s，＜ `play_stream_voice.rpc.timeout = 300s`）；③ `publish/**` 镜像同批（逐字节）。
> ⚠️ **版本号口径**：改前本文件实际最大号 ＝ **`v150`**（`T-736` 占用）⇒ 本条接续 **`v150 → v151`**；`v150` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。

### 一、`data/iot/smart-bed.js`（协议卡面 —— 本批唯一的设计资产改动）

| 位置（改后行号） | 字段 | 改前 | 改后 |
|---|---|---|---|
| `:657` | `show_text.envelope.payload.body.maxLength` | `512` | **`4000`** |
| `:657` | 同字段 `desc` | 「消息正文…最长 512 字符（约 170 个汉字）」 | 「最长 **4000** 字符…⚠️ **超出设备屏幕一屏容量时，由设备端自行分页显示**（云端不再按 512 字符截断）」 |
| `:667` | `show_text.notes` | 「若文本超出屏幕宽度，应自动换行或滚动显示。」 | 追加：`body` 上限 4000 ⇒ **超一屏由设备端自行分页**；⛔ 不按屏幕尺寸排版、⛔ 不因超长拒发 |

### 二、刻意未动（⛔ 不扩面）

| 面 | 为什么不动 |
|---|---|
| `play_stream_voice.rpc.timeout` | **仍 `300s`**：1000 字 ≈ **200–250 s**（中文 TTS 4–5 字/秒）＜ 300 s ⇒ 无需放宽 |
| `play_voice.body`（小量一次性语音） | 该卡无 `maxLength` 声明；本批口径只针对 `show_text`（睡眠报告文本面） |
| `sleep_report/domain/notifications.py::MAX_BODY_LEN`（`512`） | 只服务「空报告／取数失败」两条**短提示**（逐字「未能获取这个报告」），⛔ 不随报告正文放大 |
| `show_text.title.maxLength`（`64`） | 本批只放开正文；标题仍是短标题 |

### 三、镜像口径

| 件 | 处置 |
|---|---|
| `publish/api/data/iot/smart-bed.js` | ✅ `design` ↔ `publish` **逐字节相同**（单件 `Copy-Item` ＋ 双侧 `sha256` 相等） |
| `publish/api/CHANGELOG.md` | ✅ 本条目同步镜像（扩展映射行） |

### 四、设备端待办（⛔ 协议仲裁不了的部分）

| # | 待办 | 说明 |
|---|---|---|
| 1 | **正文分页** | `body` 可达 4000 字符 ⇒ 设备端按自身屏容量分页/滚动；云端⛔ 不保证一屏可见 |
| 2 | **语音播完预算** | 单条报告语音 ≤ 1000 字 ⇒ 端侧须在 300 s 内播完（含网络与合成耗时） |


## v149 → v150：`T-736`（`Q-053`）—— `StopVibration` 工具卡改为「**等价于下发 `vibration` 且 `mode=off`**」（工具层映射落地；⛔ 协议卡/枚举零改动）（2026-10-17）

> **来源／依据**：需求方 **2026-10-17 逐字裁定**（`Q-053` 工作区）：「Vibration 的情况是这样的：虽然改了协议，但给到 AI 的工具层没有改，所以这里应该有一个映射关系。① 如果工具层调了 `start vibration`，那 `mode` 就是正常用的那几种。② 如果调了 `stop vibration`，那工具层给的就是 `mode` 等于 `off`。这个我觉得应该一起上，跟 vibration 的改造一起上。」
> **修的是什么**：`T-724` 把**协议** `vibration.mode` 扩到 5 值（含 `off`），但 `StopVibration` 工具卡**仍不带任何载荷字段** ⇒ AI 调它实际发出一条**没有 `mode` 的 `vibration`**（端侧只能按"新一轮下发"处理 ⇒ 停止不可靠）。`T-724` 报告 §⑩ `U1` 即本件要修的点。
> ⚠️ **版本号口径**：改前本文件实际最大号 ＝ **`v149`**（`T-724` 占用）⇒ 本条接续 **`v149 → v150`**；`v149` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。

### 一、`data/ai-tools/tools.js`（工具卡面 —— 本件唯一的设计面改动）

| 位置（改后行号） | 变更（改前 ⇒ 改后） |
|---|---|
| `StopVibration` 卡级 `description`（`:723`） | 「停止震动（🔴 `T-670`：协议**不单列 `vibration_stop`** —— 停止语义 ＝ **覆盖式重新下发**…）」⇒ 「停止震动 —— **等价于下发 `vibration` 且 `mode=off`**（`T-736` 工具层映射…）⛔ 本工具**不携带** `intensity`／`duration`（给了也不下发）」＋旧口径标注为**已被本映射取代** |
| 同卡 `openai.function.description`（`:728`） | 「停止震动（覆盖式重新下发强度/时长为 0 或最短档；协议无独立 stop action）」⇒ 「停止震动 —— 等价于下发 `vibration` 且 `mode=off`（工具层映射；⛔ 不需要也⛔ 不要给 intensity／duration；协议无独立 stop action）」 |
| 同卡**参数面** | ⛔ **一字未改**：`properties` 仍只有 `role`、`required` 仍 `[]`（⛔ 未新增 `mode` 参数 —— 映射在**执行器**里做，AI 不需要也无法传 `mode`） |
| `StartVibration` 卡 | ⛔ **一字未改**（`mode` 语义不变：正常 4 值 ＋ 可选第 5 值 `off`；`off` ⛔ 不是默认值；`enum` 仍 5 值、`required` 仍 `["role"]`） |

### 二、刻意未动（⛔ 不扩面）

| 面 | 为什么不动 |
|---|---|
| `data/iot/smart-bed.js`（`vibration` 卡 ／ `sharedEnums.massage_mode`） | `T-724` 已定稿（`mode` 5 值含 `off`）；本件只补**工具层 → 下发层**的映射，⛔ 不动协议 |
| 协议 `vibration.mode` 枚举值面 | ⛔ 不改枚举（`off` 已在位；本件⛔ 不增删任何值） |
| 公共枚举 `intensity`（3 档） | ⛔ 语义/值面一字未改 |
| `vibration_stop` 独立 action | ⛔ 仍**不新立**（停止 ＝ `mode=off`，`T-670` 契约不变） |
| 其余 44 张工具卡 | ⛔ 零改动（`AI_TOOLS.tools` 长度 **45 逐字不变**） |

### 三、镜像口径

| 件 | 处置 |
|---|---|
| `publish/api/data/ai-tools/tools.js` | ✅ `design` ↔ `publish` **逐字节相同**（`sha256` 双侧一致；RL4／`P3-5` 两臂把守） |
| `publish/api/CHANGELOG.md` | ✅ 本条目同步镜像（扩展映射行） |


## v148 → v149：`T-724`（`Q-053`）—— 公共枚举 `massage_mode` 新增第 1 值 **`off`（＝停止震动）** ⇒ **5 值** ＋ `vibration` 卡字段面/notes 同批 ＋ `StartVibration` 工具 enum 同批（2026-10-17）

> **来源／依据**：需求方 **2026-10-17 逐字裁定**（`Q-053` 工作区）：「**Mode 里面增加 Off 就可以了**」⇒ `vibration.mode` 值面 ＝ **`off`／`pulse`／`wave`／`continuous`／`follow`**（5 值）；语义：**`off` ＝ 停止震动**（与打鼾 `snore_intervention` 的 `mode=disable` **同族**）。
> **未新建 `vibration_mode` 枚举**（依据见报告 `evidence-t724-vibration-off.md` §①）：该枚举自 `T-670` 起**消费方只有 `vibration` 一个**（协议 `massage` 卡已无 `mode` 字段、`massage` 侧对该枚举引用数 ＝ 0）⇒ 追加 `off` **不会**在按摩侧产生无为值。
> ⚠️ **版本号口径**：改前本文件实际最大号 ＝ **`v148`**（`T-700` 占用）⇒ 本条接续 **`v148 → v149`**；`v148` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。

### 一、`data/iot/smart-bed.js`（协议面 —— 枚举值面 ＋ 卡面）

| 位置（改后行号） | 变更（改前 ⇒ 改后） |
|---|---|
| `sharedEnums.massage_mode`（`:1760` 区） | 值面 **4 ⇒ 5**：`pulse`／`wave`／`continuous`／`follow` ⇒ **`off`（第 1 值，＝停止震动）／`pulse`／`wave`／`continuous`／`follow`**（既有 4 值相对序一字未动）；`desc` 追加 `T-724` 取代注（含 `off` 语义、消费方只有 `vibration`、⛔ `intensity` 语义未动） |
| `vibration` 卡 `envelope.payload.mode`（`:851`） | `desc` 改「**5 值**：off 停止震动 / pulse 脉冲 / wave 波浪 / continuous 持续 / follow 随震」，默认 `wave`（⛔ 默认不是 `off`）＋ 取代注（原句逐字留痕）；`ref` 仍 `massage_mode`、⛔ `required` 仍 `false` |
| 同卡 `duration.desc` | 追加「🔴 `T-724`：`mode=off` 时忽略」 |
| 同卡 `notes`（`:855`） | 追加 `T-724` 取代注：`off` ＝ 停止震动（与 `mode=disable` 同族、⛔ 仍不单列 `vibration_stop`）、`off` 时 `intensity`／`duration` **可省且被忽略**、⛔ `intensity` 语义未动 |
| `actionRelationships["vibration"].notes`（`:1338`） | 同上（原句逐字留痕后追加） |

### 二、`data/iot/event-identifier.js`

| 位置（改后行号） | 变更 |
|---|---|
| `downlink["vibration"].desc`（`:52`） | 追加 `T-724` 口径：`mode` 扩为 **5 值**（`off` ＝ 停止震动 ＋ 4 值）、`off` 时 intensity/duration 可省且被忽略、⛔ intensity 语义未动 |

### 三、`data/ai-tools/tools.js`

| 位置（改后行号） | 变更 |
|---|---|
| `StartVibration.mode`（`:696-706`） | `enum` **4 值 ⇒ 5 值**（`off` 在第 1 位）；`description` 改「5 值 ＋ `off` ＝ 停止震动（与 `mode=disable` 同族、⛔ 不是强度档位）＋ `off` 时 intensity/duration 可省」 |

### 四、计数口径（⛔ 不静默改数）

| 计数项 | 改前 ⇒ 改后 | 依据 |
|---|---|---|
| 协议 `sharedEnums` **键数** | **29（不动）** | 本批**不新增/不删除枚举**，只给既有枚举加 1 值 |
| 协议 `sharedEnums` **值总数** | 279 ⇒ **280** | `massage_mode` ＋1（`off`）；无删除项 |
| `test_enum_coverage_matrix_gate.py` 的值总数 pin | `== 279` ⇒ **`== 280`** | 同上一行；⚠️ pin 形态仍是**逐字相等**（⛔ 未放宽） |
| `PROTO_ENUM_VALUE_FLOORS["massage_mode"]` | 4 ⇒ **5** | 下限**对准实测** ＝ **收紧**（⛔ 不是放宽；与 `T-670` 3 → 4 同款） |
| `MIN_SHARED_ENUM_VALUES_TOTAL`（下限） | **271（不动）** | 它是**下限**；实测 280 仍高于它 ⇒ ⛔ 不上调（属另一次裁定） |

### 五、刻意未动（⛔ 不扩面）

- ⛔ **不新建 `vibration_mode` 枚举**、⛔ 不改 `massage_mode` 的**枚举名**（`T-670` 已裁「名保留」）；
- ⛔ **`intensity` 语义未动**（仍 3 档 `low`／`medium`／`high`，`off` ⛔ 不进强度维度）；
- ⛔ **不新立 `vibration_stop` action**（停止由 `mode=off` 承担）；
- ⛔ **REST 卡**：`vibration` 本就**无** `rest-*` 卡（`T-670` 未立）⇒ 本批无 REST 面改动；
- ⛔ **`rest/examples.js` 的 `vibration` 示例**沿用 `zone` 而非卡面必填 `sub_device_id` 的**既有瑕疵**（同 `T-688` 对同族瑕疵的处置：只登记、本批不改）。


## v147 → v148：`T-700`（`Q-053`）—— 公共枚举 `intensity` **5 档 ⇒ 3 档**（`low`／`medium`／`high`，默认 `medium`）＋ 四个消费动作同批 ＋ 错误码 `4004` 描述订正（2026-10-17）

> **来源／依据**：需求方 **2026-10-17 逐字裁定**（`Q-053` 工作区）：① 「我确认那个 **Intensity 字段我们统一改成 3 档**」② 「**相关的受影响内容也是 3 档**」③ 「然后**档位改成高中低**」④ 「**默认现在是给到 2 吧，或者给到中间档就行了**」⇒ 枚举值面 ＝ **`low`／`medium`／`high`**（`very_low`／`very_high` **退出** ✗）；默认 ＝ **`medium`**（语义不变、值集合收缩）。需求稿 ＝ `Q-053/requirement.md`（含改前逐面取证）。
> ⚠️ **版本号口径**：改前本文件实际最大号 ＝ **`v147`**（`T-688` 占用）⇒ 本条接续 **`v147 → v148`**；`v147` 及更早条目**逐字保留**。

### 一、`data/iot/smart-bed.js`（协议面 —— 枚举值面 ＋ 卡面）

| 位置 | 变更（改前 ⇒ 改后） |
|---|---|
| `sharedEnums.intensity` | 🔴 **值面 5 ⇒ 3**：`very_low`／`low`／`medium`／`high`／`very_high` ⇒ **`low`（低）／`medium`（中，**默认档**）／`high`（高）**；`desc` 同步改「动作强度（公共枚举，**3 档**）」＋ 就地取代注（原句逐字留痕）。⚠️ **`very_low`／`very_high` 退出**（⛔ 不留别名、⛔ 不留过渡期） |
| `massage` 卡 `intensity.desc` | 「按摩强度，引用公共枚举 intensity，默认 medium」⇒ 「…（**3 档** low/medium/high），默认 medium（中档）」 |
| `vibration` 卡 `intensity.desc` | 「震动强度，引用公共枚举 intensity（5 档），默认 medium（＝3）」⇒ 「…（**3 档** low/medium/high），默认 medium（中档）」＋取代注（原句逐字留痕） |
| `vibration` 卡 `notes` | **追加** `T-700` 取代注：「默认强度 `3`」＝**旧 5 档口径**；现行默认 ＝ `medium`（中档）、值为**命名档**（⛔ 非数字） |
| `airflow` 卡（`description`／`intensity.desc`／`notes`） | 「intensity 引用 intensity 公共枚举 **5 档**：very_low~very_high」⇒ 「**3 档**：low/medium/high」；**必填性不变**（`required: true`）＋三处取代注 |
| `lulling` 卡 `intensity.desc` | 「哄睡强度，引用公共枚举 intensity（5 档）」⇒ 「…（**3 档** low/medium/high），默认 medium」＋取代注 |
| `massage` 卡 `errors[]` | 说明串补「（intensity 为公共枚举 **3 档** low/medium/high；speed 仍 5 档）」—— ⛔ 码值 `4001` 一字未改 |
| `actionRelationships.vibration` / `.lulling` 的 `notes` | 各追加「引用 intensity 公共枚举（**3 档**）」＋取代注（原句逐字留痕） |

- 🔴 **四个消费者一次改齐（⛔ 无一处留在 5 档）**：`massage`（可选）／`vibration`（可选）／`airflow`（**必填**）／`lulling`（**必填**）。⚠️ `lulling` 虽不在需求方点名的三动作内，但它**与三者共用同一公共枚举** ⇒ 公共枚举改 3 档后它**不可能**留在 5 档（否则协议自相矛盾）⇒ 属**同一处改动的连带面**（非扩面、非发明）。
- ⛔ **`sharedEnums.speed`（5 档 `very_slow`…`very_fast`）** 与本件**无关、一字未改**（需求方未提；`massage` 卡的 `speed` 仍 5 档）。

### 二、`data/iot/protocol.js`（错误码面）

| 位置 | 变更 |
|---|---|
| `错误码 4004`（对象键） | `description`：「按摩强度超出允许范围（**1-5**）」⇒ 「强度超出允许范围（公共枚举 `intensity` **3 档**：`low`／`medium`／`high`；服务 `vibration`／`massage`／`airflow`／`lulling`）」—— ⛔ 码值／`meaning`／键名**一字未改**；⚠️ 原描述把本码**窄化到按摩**且**写死数字区间**，两处均与现状不符 |
| `错误码 4004`（`参数错误 4xxx` 表的 `<td>`） | 同上（表行与对象键**必须同批**，否则「表 ↔ 卡」两处对不上） |

### 三、`data/iot/smart-bed-examples.js` ／ `data/rest/examples.js`（示例面）

| 位置 | 变更 |
|---|---|
| `smart-bed-examples.js` 的 `airflow` 示例 | `description` 改「**3 档**：low/medium/high」；`call.intensity` `"very_low"` ⇒ **`"low"`**（`very_low` 已退出） |
| `rest/examples.js` 的 `airflow` 示例 | 同上（`"very_low"` ⇒ `"low"`）；⚠️ 该示例用 `zone` 而**非**协议卡必填的 `sub_device_id` 的**既有瑕疵**，`T-688` 已登记、本件 ⛔ 不改 |
| `rest/examples.js` 的 `lulling` 示例 | `description` 「（intensity **5 档** + duration 可选）」⇒ 「（intensity **3 档** low/medium/high + duration 可选）」；`call.intensity` `"medium"` **本就在 3 档内** ⇒ 值不动 |
| ⚠️ **本件刻意不改（登记为范围外）** | `rest/examples.js` 的 **`wake_up`** 示例仍写 `"intensity": "very_low"` —— 但该示例**整条字段面早已陈旧**（`wake_up` 现行契约 ＝ `mode` ＋ `duration`，无 `intensity`／无 `sub_device_id`／无 `zone`）。**只改值 ＝ 把一条本就错的示例改得"看起来对"** ⇒ 本件 ⛔ 不做半吊子订正（`T-688` 对同族既有瑕疵采取同一处置：只登记、不代修） |

### 四、`data/ai-tools/tools.js`（AI 工具卡面）

| 位置 | 变更 |
|---|---|
| `StartMassage` | `intensity.enum` 5 值 ⇒ **3 值**；`description` 改「按摩强度（公共枚举 intensity **3 档**）」，默认 medium；工具 `description` 「强度/速度**各 5 档**」⇒ 「**强度 3 档、速度 5 档**」 |
| `StartVibration` | `intensity.enum` 5 值 ⇒ **3 值**；默认说明改「默认 medium（**中档**）」（原「（＝3）」是旧 5 档口径） |
| `StartAirflow` | `intensity.enum` 5 值 ⇒ **3 值**（`required` **仍含 `intensity`**，⛔ 未放宽）；`description` 补 3 档说明 |
| `StartLulling` | `intensity.enum` 5 值 ⇒ **3 值**；`description` 「（intensity 公共枚举 **5 档**：very_low~very_high）」⇒ 「（**3 档**：low/medium/high），默认 medium」 |
| ⛔ **未动** | `speed` 参数（`StartMassage` 的 5 档 enum）、`StopMassage`／`StopVibration`／`StopAirflow` 的**无 intensity 参数**形态、`AI_TOOLS.categories`（工具名集合**零变动** ⇒ `protocol.js` 的 12 组 l2 记账**不属本件**） |

### 五、同批落地（⛔ 不在本册写面、只登记指针）

ER 面（`design/03-entity-relationship/data/01-device-iot.js`：`IntensityLevel` 枚举值面 5 ⇒ 3 ＋ `sub_air_flow_device` 的 `description`）· 工作流面（`design/02-business-workflow/data/02-device-control.js` 的按摩控制能力行）· 后端实现（`src/server/modules/device_control/api/router.py::_INTENSITY_LEVELS` ＋ 校验器 docstring／错误串；`ai_agent/domain/tools.py::_INTENSITY_ENUM`；**`device_iot/domain/device_type_catalog.py` 五处能力位 1..5 ⇒ 1..3 ＋ `snore_intervention` 撤 `intensity`（并批 `T-691`）**）· 端侧（`src/zre_c/**` 只判**字段存在性**、⛔ 不持 intensity 值表 ⇒ **结构性不适用**）· 前端（`src/web-admin/**`）· 判据面（`tests/**` 三档断言 ＋ `test_enum_coverage_matrix_gate.py` 值总数 pin）· 以及 `publish/**` 镜像。

## v146 → v147：`T-688`（`Q-051`／取件 `R-057`）—— `snore_intervention` **改用 `mode`**（新增公共枚举 `snore_mode`；`intensity` **撤下**）（2026-10-17）

> **来源／依据**：需求方 **2026-10-16 逐字裁定**（`Q-051` 工作区 `constraints.md` §一，本册按「轮次 ＋ 件名」引用）：① **`mode` 彻底替代 `intensity`**（删除该字段，⛔ 不是并存）② 「开启防打鼾」默认 **`auto`** ③ 「**打鼾只有模式没有力度档位，而且我们也不需要预制给他的话术**。正常来讲，AI 看到打鼾的接口里没有所谓这种档位设置的时候，他就应该能给用户回复了」。设计定稿 ＝ `Q-051/design.md` §2；取证 ＝ `Q-051/evidence-impact-faces.md`（逐面 `file:line`）。
> ⚠️ **版本号口径**：改前本文件实际最大号 ＝ **`v146`**（`T-662` 占用；⚠️ `Q-051/design.md` §6 第 3 步写的「追加 `v145 → v146`」是**定稿当刻读数、已被 `T-662` 占用** —— 按本册「改前最大号 ＋ 1」的既有口径改判 **`v146 → v147`**，⛔ 不回改 `T-662` 条目）⇒ 本条**追加在文件顶部**；`v146` 及更早条目**逐字保留**。

### 一、`data/iot/smart-bed.js`（协议面）

| 位置 | 变更（改前 ⇒ 改后） |
|---|---|
| `snore_intervention` 卡 `description` | 「打鼾干预——以子设备ID定位干预设备（干预中重新下发用新参数覆盖）」⇒ 「…，**设置干预模式**（干预中重新下发用新参数覆盖）」 |
| `snore_intervention` 卡 `envelope.payload` | 🔴 `intensity`（`enum`/`ref:"intensity"`/**required:true**）**整键撤下** ⇒ 新增 `mode`（`enum`/**`ref:"snore_mode"`**/**required:true**）；`duration` 保留（desc 改「该模式的**最长运行时长**（秒），不填＝持续到下一次模式设置」）；`sub_device_id`（必填）不变 |
| `snore_intervention` 卡 `errors` | 「`intensity` 不在枚举范围」⇒ 「`mode` 不在枚举范围」＋ 新增「`mode` 缺失」 |
| `snore_intervention` 卡 `notes` | 追加 `T-688` 口径块：三值语义 ＋ 🔴 **执行主体**（`execute` 平台即时介入／`auto` **设备端本地闭环**，平台只下发模式）＋ `disable` 忽略 `duration`（幂等）＋ `T-670` 的停止语义**改由 `mode=disable` 承担** |
| `sharedEnums` | 🔴 **新增 `snore_mode`**（`execute`／`auto`／`disable`，3 值），插在 `wake_up_mode` 之后；desc 明写「**替代**原「启动/结束 ＋ 强度档位」口径」＋「⛔ 不得复用裸名 `mode`（它已是床模式）」 |

- **未被顺手改脏（⛔ 刻意不动）**：`sharedEnums.intensity`（5 档）**保留** —— 它仍被 `airflow`（必填）／`lulling`（必填）／`massage`（可选）／`vibration`（可选）消费；`sharedEnums.device_type` 的 `snore_intervention`（**设备类型**，与同名下行 action **不同面**）不动；`sharedEnums.mode`（床模式 6 值）不动。

### 二、`data/iot/smart-bed-examples.js` ／ `data/rest/examples.js`（示例面）

| 位置 | 变更 |
|---|---|
| `smart-bed-examples.js` 的 `snore_intervention` 示例 | `call` 的 `"intensity": "very_low"` ⇒ `"mode": "auto"`；desc 同向改「设置模式」 |
| `rest/examples.js` 的 `snore_intervention` 示例 | 同上（`"intensity": "very_low"` ⇒ `"mode": "auto"`）；⚠️ **该示例的既有瑕疵**（用 `zone` 而**非**协议卡必填的 `sub_device_id`，与卡面字段面不一致）**只登记、本件不改**（就地写明，见 `Q-051/evidence-impact-faces.md` E1-A 的 A13） |

### 三、`data/rest/02-core-domains.js`（REST 动作卡）

| 位置 | 变更 |
|---|---|
| `rest-snore-intervention` 的 `description` | 「打鼾干预**启动**」⇒ 「打鼾干预**设置模式**（`mode` 三值 `execute`/`auto`/`disable`，`intensity` 已撤下）」 |
| `rest-snore-intervention-stop` 退役注（`:185-190`） | 只**追加** `T-688` 取代注（停止语义改由 `mode=disable` 承担；原句「改小 `intensity`/`duration`」⛔ 一字未删，按 `rule-0d` 留痕） |

### 四、`data/ai-tools/tools.js`（AI 工具卡面）

| 位置 | 变更 |
|---|---|
| `StartSnoreIntervention`（`:642-679`） | 🔴 **工具名保留**（⛔ 不改名 —— 改名会动 prompt 缓存面与既有判据，收益不抵代价）；`intensity` 参数（enum 5 值）**整块撤下** ⇒ 新增 `mode`（enum `execute`/`auto`/`disable`）；`required` 由 `["role","intensity"]` ⇒ `["role","mode"]`；`duration` 的 desc 改「该模式的最长运行时长…（`mode=disable` 时忽略）」；两处 `description` 改「设置…**模式**」并写明三值语义 |
| 🔴 **能力事实的表达方式**（需求方裁定③） | 工具 `description` 里**明写**「**打鼾干预只有模式、没有力度档位**：本工具**没有**强度参数，用户若问『强一点/弱一点』⇒ 按本契约**如实告知没有该档位**，⛔ 不得编造档位或数值」 —— ⛔ **不新增任何预制话术／"默认使用说明"条目**（能力边界**只由工具契约表达**） |

### 五、`data/iot/event-identifier.js`（事件标识面）

| 位置 | 变更 |
|---|---|
| `downlink["snore_intervention"]` 的 `desc` | 「打鼾干预（`T-670`：停止语义并入覆盖式重新下发）」⇒ 「打鼾干预**设置模式**（`T-688`／`Q-051`：`mode` 三值；**`auto` 的执行主体＝设备端本地闭环**，平台只下发模式；`T-670`：停止语义并入同一条 action）」 |

- ⚠️ **本件不动**：`ackOnly` 的 `snore_intervention_ack`（`:112`）、`uplink["snore_intervention"]`（`:201`，上行别名**不变**）、`snore_detector` → `DeviceMessage.SensorSnoring`（`:171`，**鼾声观测**，⛔ 与本件无关）。

### 六、`data/ai-tools/protocol.js`（工具分组说明面 · 并批 `T-686`）

| 位置 | 变更 |
|---|---|
| `:84` 的「睡眠辅助」表行 | 🔴 **撤下已退役工具 `StopSnoreIntervention`**（该工具随协议 action `snore_intervention_stop` 于 `T-670` 退役）：`4` ⇒ `5`、成员改为 `StartSnoreIntervention / StartWakeUp / StopWakeUp / StartLulling / StopLulling` |
| `:117` 的 `categories["睡眠辅助"].tools` | 同上（撤 `StopSnoreIntervention`、补 `StartLulling`/`StopLulling`） |
| `:81`／`:82`／`:114`／`:115`（同表/同映射的**其它行**） | 🔴 **一并订正为与 `tools.js` 的 `AI_TOOLS.categories`（`protocol.js:79` 自述的「单一真相源」）逐项一致**：`姿态控制` `2 → 4`（补 `SavePositionMemory`/`RecallPositionMemory`）、`舒适调节` `13 → 15`（补 `StartVibration`/`StopVibration`）—— 这 3 组差值是**本件实测的既存漂移**（同表自述「须与之保持一致」），逐条给依据、⛔ 非静默改数 |

### 七、取代注（`rule-0d`）

- 本批**反转** `Q-037/protocol-changes.md:52`（需求方 2026-10-14 原话）的「**别的参数都不改**，就在 `snore intervention` 里**增加一个 `mode`**」—— 该旧句的**原文逐字**保留在 `Q-037` 工作区内（⛔ 未改、未删），由本条取代 ⇒ **现行**：`snore_intervention` ＝ `sub_device_id`（必填）＋ `mode`（必填，`snore_mode` 三值）＋ `duration`（可选）；**`intensity` 撤下**。
- 本批**同批订正** `T-670` 在本册 `v145` 条目里的产品口径取代注（`v145` 原句「停止语义 ＝ **覆盖式重新下发**…改小 `intensity`／`duration`」）：**现行** 停止语义 ＝ `mode=disable`（原句按 `rule-0d` 逐字保留、⛔ 不删）。

### 八、同批落地（⛔ 不在本册写面、只登记指针）

ER 面（`design/03-entity-relationship/data/{01-device-iot,07-event-infrastructure,02-device-control}.js`：新公共枚举 `SnoreMode` ＋ 下行/上行两实体字段 `intensity`→`mode` ＋ `sub_snore_intervention` 运行态改模式并撤 `is_on`）· 工作流面（`design/02-business-workflow/data/02-device-control.js`：能力表／场景表／`snore-intervention-flow` 流程与"平台判断"旧叙述／权限点）· 后端架构面（`design/05-backend-architecture/data/event-contracts.js` 的 `fields` 行**逐字替换**，生成器 `--check`／`--check-fields` 双绿）· 后端实现（`src/server/events/base.py` 新增 `SnoreMode` 枚举 ＋ 两处字段替换；`device_control/api/router.py` 的 `_validate_snore` ＋ 新白名单 `_SNORE_MODES`；`ai_agent/domain/tools.py` 参数）· 端侧（`src/zre_c/src/zre_payload.c` 的 `required_mask`：`snore_intervention` 由 `REQ_SUB_DEVICE|REQ_INTENSITY` ⇒ `REQ_SUB_DEVICE|REQ_MODE`，⛔ 零结构体改动、⛔ `k_names[]` 计数不变）· 前端（`src/web-admin/**`：`snore_mode` 枚举选项、下行指令卡、模拟器参数表与引擎分支）· 判据面（`tests/design-regression/test_subdevice_field_map_gate.py` 的上行字段表与豁免理由；`tests/design-regression/test_er_enum_domain_gate.py` 的 `UNCHECKED_COUNT` 47→48）· 以及 `publish/**` 镜像。
- ⛔ **不新增任何预制话术／默认使用说明条目**（需求方裁定③）；⛔ **不误删 `snore_detector.snore_intensity`**（鼾声**观测值**，与被撤下的干预控制字段是两回事：`src/server/events/base.py::SensorSnoring` ＋ `event-contracts.js:969` ＋ ER `SensorSnoring` 三面**仍在位**）。

## v145 → v146：`T-662`（`Q-047` **口径批 B**）—— `smart-bed.js` **`device_lifecycle` 由 6 态收缩为 2 态**（＋在线性/故障**另立两节**）／REST 资产**订正第二套值域**（`registered/bound/error` ✗）（2026-10-16）

> **来源／依据**：需求方 **2026-10-16 最终裁定**（`Q-047` 需求稿 §九③④；⚠️ 该稿属**第 3 层设计工作区**，本册按「轮次 ＋ 件名」引用，⛔ **不写该工作区字面路径** —— 见本册 `G-493`⑤／`T-589` 同族订正注）：**「一次性改好，不要留历史残留」「⛔ 不要过渡期」「`deactivate` 就是 `disable`」「注销＝禁用」**（⛔ 不留 `unregistered` 状态）。**实现批 A** ＝ `T-661`；取证件 ＝ `Q-047` 的**只读设计取证报告**（**11 面／约 57 处逐处 `file:line`**）。
> ⚠️ **版本号口径**：改前本文件实际最大号 ＝ **`v145`**（`T-670` 占用）⇒ 本条接续 `v145 → v146`；`v145` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。

### 一、`data/iot/smart-bed.js`（协议面，文件头新增 `v14` 变更块）

| 位置 | 变更（改前 ⇒ 改后） |
|---|---|
| `connect.state_machine` | 🔴 **链式表述拆解**（旧文是一条单向链，把四个维度读成一个状态机）—— 旧句**逐字**留在同处注释（`rule-0d`）；现文＝生命周期 2 值 ⇄ ＋ 在线性（推导：最近 3 次心跳窗口 90s 内至少 1 次 ack）＋ 故障（`device_faults` 独立记录）＋ 凭据（`device_secret IS NULL`）**四维分立** |
| `statusDefinitions.device_lifecycle` | **6 态 → 2 态**（`imported`/`deactivated`）；`description` 明写真值源 ＝ `devices.deactivated`（本枚举是**派生显示副本**）＋ 准入判据逐字；被删/被改的 6 条旧 `states[]` **逐字**留在文件头 `v14` 的取代注记内 |
| `statusDefinitions` | 🔴 **新增两节**：`device_connectivity`（在线性推导态，`online`/`offline`，⛔ 不落库）／`device_fault_record`（故障独立记录，`active`/`resolved`）—— 把混在同一枚举里的另两个维度**另立**，⛔ 不是新机制 |
| 文件头 | **新增 `v14` 变更块**（含 `rule-0d` 取代注记）；⛔ `:51`（`A4` 变更史）与 `v13` 及更早块**一字未改** |

### 二、`data/rest/01-device-iot.js`（REST 资产）

| 位置 | 变更（改前 ⇒ 改后） |
|---|---|
| `design_decisions` 的「设备状态机」 | 🔴 **第二套值域订正**：旧文『registered → bound → offline → error → **unregistered**』⇒ 现文「生命周期只有 2 值 ＋ 旧值从不是本仓 `DeviceStatus` 取值」；旧句**逐字**留痕在 `detail` 内 |
| `rest-device-export` 的 `query_params[status]` | 🔴 **第二套值域订正**（原文『registered/bound/offline/error/unregistered』，与后端实际白名单**不一致**）⇒ 现文「`online`/`offline`（连接态，走影子过滤）或 `imported`/`deactivated`（生命周期，DB 等值）」 |
| `rest-device-list`／`rest-device-detail` 响应字段表 | `status` 描述由「imported/deactivated/fault…」「…fault/unregistered 等」⇒ 「**恰 2 值**」；**新增** `deactivated`（boolean）字段行（`T-661` 实现已回，本批补声明 —— 与 `T-531` 同范式） |
| `rest-device-detail`／`rest-device-update`／`rest-device-list`／`rest-device-create` 的示例 JSON | **新增** `"deactivated": false`（示例与字段表**同批**，防「声明有、示例无」） |
| `rest-device-deregister` 卡的 `summary`/`scenario`/`description`/`steps`/`post_effects`/`state_machine`/`responses` | 🔴 「注销＝停用」落卡：置 `deactivated=true` ＋ 作废 `device_secret`（幂等判据改布尔）；响应**新增** `deactivated: true`（`json` ＋ `fields` ＋ `response.body` 三处同批）；⛔ 旧值 `unregistered` 退役（被改处旧文逐字留在 `description` 的取代注记内） |
| `rest-device-secret-rotate` 的 `preconditions`/`steps`/`errors`/`description` | 前置「设备**未**注销（`status != unregistered`）」⇒「设备**未被停用**（`deactivated == false`）」；409 文案「设备已注销（`unregistered`）」⇒「设备已停用（`deactivated=true`，含原注销）」；🔴 **409 语义一字未删**（`D-188-补`「⛔ 不借登记密钥复活」保留） |
| `rest-device-secret-read` / `rest-device-options` / `rest-device-export` 的文案 | 「已注销设备」⇒「已停用（含注销）设备」；`options.statuses` 明写恒为 2 值；导出列由 **6 列 → 7 列**（＋`deactivated`，与 `T-661` 实现同批）＋ 登记**归档件语义漂移**（历史导出件原 `unregistered` 行现显示 `deactivated`） |
| T-127 / T-518 块注释 | 只**追加** `T-662` 取代注记（⛔ 旧句一字未删，`rule-0d`） |

- **计数**：REST 端点数 **不变**（⛔ 未增删端点卡）；被判面（`check_rest_response_field_subset.py`）**声明面只增不减**（`deactivated` 由实现真实返回 ⇒ 仍满足「卡声明 ⊆ 实现顶层键」）。
- **判据**：本轮**新增**一条对账门禁 `scripts/check_device_status_caliber_parity.py`（**契约声明 ↔ 实产值域** 机械对账；并入 `Q-046` 的 `R5` 缺口族）＋ pytest 薄包装；`rc=0` 读数见 `T-662` 报告。

## v144 → v145：`T-670`（`Q-037` §4/§5/§6）—— **振动与按摩执行器拆开**（`massage` 恢复「强度＋速度」／新立下行指令 `vibration`／同批废止 `snore_intervention_stop`）（2026-10-16）

> **来源／依据**：需求方 2026-10-16 裁定「**这些相关内容，全部前移至 ab 面换代之前**」＋ 三条边界（范围＝拆分四项／固件＝照发、另行跟进／时序＝硬前置：本批不收口就不切流）。**口径源** ＝ `.pipeline/design/Q-037/`（`protocol-changes.md` §4／§5／§6、`type-default-notes.md:19`、`OVERVIEW.md:55-84`）；任务书 ＝ `.pipeline/journal/T-670.taskbook.md`。
> ⚠️ **版本号口径（⛔ 不得回改）**：改前本文件实际最大号 ＝ **`v144`**（`T-625` 占用）⇒ 本条接续 `v144 → v145`；`v144` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。

### 一、改了什么（三笔，同批）

- **第 ① 笔｜`P-2` 按摩「强度 ＋ 速度」**：`data/iot/smart-bed.js` 的 `massage` 卡 —— `envelope.payload` 的 `mode` **移除**、`speed` **恢复**（引用**已现成**的公共枚举 `sharedEnums.speed` 5 档 `very_slow…very_fast`，⛔ 未新造）；`actionRelationships["massage"]` 的 notes 同向反转；`data/ai-tools/tools.js` 的 `StartMassage` 参数同步 `mode`→`speed`；`data/rest/examples.js` 的 `massage` 示例（原描述早已逐字「position + speed + intensity + duration」而 `call` 却写 `mode`）同批订正。**被取代的旧句原文逐字（⛔ 只此一处留痕）**：「2026-09-04：speed 字段改为 mode（引用公共枚举 massage_mode：pulse 脉冲 / wave 波浪 / continuous 持续，默认 wave）；intensity 仍为公共枚举（各 5 档）。mode=按摩手法（节奏），intensity=按摩力度，两者独立。duration 单位为秒，与旧版 duration_ms 不同；不填=持续到 stop（=0 亦为持续）。」
- **第 ② 笔｜公共枚举 `massage_mode` 语义迁移 ＋ 第 4 值**：枚举**名保留**（改名会打断协议／ER／代码三面的「自述对齐」引用），**语义现为震动模式**（由新立指令 `vibration` 引用），并**末尾追加**第 4 值 `follow`（随震＝跟随音乐）⇒ 3 → 4 值。
- **第 ③ 笔｜`P-1` 新立下行指令 `vibration` ＋ 同批废止 `snore_intervention_stop`（一次入／一次出）**：`data/iot/smart-bed.js` 新增 `vibration` endpoint 卡、`action_name.values` 业务序**末尾追加** `vibration`（`error` 之前）并**撤下** `snore_intervention_stop`、`actionSystem` 分类「床体动作指令」同步；`data/iot/protocol.js` 的分类清单、`data/iot/event-identifier.js` 的 `downlink`＋`ackOnly`、`data/iot/smart-bed-examples.js`、`data/rest/examples.js`、`data/ai-tools/tools.js`（两张新卡 ＋ 分组索引）同批同步。
  - 🔴 **「计数不变、集合变了」**：业务值 `55 − 1 ＋ 1 ＝ 55` **不变** ⇒ 三个**只钉计数**的既有面（`check_action_registry_parity.py` 的 `MIN_*` 下限／`P3` 内置值条数／真仓读数行 `[T19 OK] P_all 56｜P_biz 55｜卡 55｜TS 56｜C 56`）**逐字照旧绿**。⇒ `T-670` **A 步**给 `check_action_registry_parity.py` 新增 **`A4` 集合臂**（`vibration ∈ 协议业务值 ∧ 卡面 ∧ TS ∧ C` ∧ `snore_intervention_stop ∉ 该四面`，⛔ **不比计数**）。
  - ⚠️ **废止项 `snore_intervention_stop` 的旧句原文逐字（⛔ 只此一处留痕）**：卡 `notes`「干预中重新下发用新参数覆盖。停止通过 snore_intervention_stop。」
  - 🔴 **固件依赖风险（本批最高风险项，转步骤 `C`）**：`src/zre_c/**` ＝「面向设备端固件的 ZYRIC v1 协议 C 参考库」，本批**仓内参考库已同步、固件未同步** ⇒ ① `vibration` 下发到未升级固件 ⇒ `ZRE_ERR_UNKNOWN_ACTION`，而云端 `downlink_consumer._send_organized` 的 `if msg is None: return` ⇒ **设备零字节、AI 侧仍回 `success: True`**（假成功、无告警）；② `massage` 的 `speed` 在老固件侧被**静默丢弃**。**用户已裁**：照发、固件另行跟进；⛔ 固件未升级期间**不得据「已下发」宣称生效**。

### 二、取代注记（`rule-0d`）

- 本批**反转** `v3.1`（2026-09-04）的「按摩 `speed`→`mode`」：该旧口径的**原文逐字**保留在本册 v3.1 条目内（⛔ 未改），并已由本条取代 ⇒ **现行**：`massage` ＝ `sub_device_id ＋ intensity ＋ speed ＋ duration`（`mode` 自 `massage` **移除**）；震动模式枚举 ＝ `massage_mode`（现为震动模式，4 值）。
- ⚠️ **`vibration` 卡的字段面 ＝ 编排器推定**（`sub_device_id` 必填 ＋ `intensity`／`mode`／`duration` 可选；依据 `type-default-notes.md:19`「震动设备 ＝ 强度 ＋ 模式」＋ 默认强度 `3`／默认模式 `wave`）⇒ 已就地在卡 `notes` 标注「**编排器推定、待需求方一句纠正**」（形态照 `T-607` 对 `flat` 的处置）。

### 三、同批落地（⛔ 不在本册写面、只登记指针）

端侧 C 参考库（`src/zre_c/**`：`k_names[]`／`zre_action_id_t` **末尾追加** `vibration` ＋ 撤下 `snore_intervention_stop`，`k_stop_pairs[]` 删 snore 对）、端侧 TS 注册表（`src/web-admin/src/lib/zre/zreActions.ts`）、后端事件面（`src/server/events/base.py` 的 `MassageCommand.mode→speed` ＋ `VibrationCommand` **原地取代** `SnoreInterventionStopCommand`；`identifier_rules.py`；`design/05-backend-architecture/data/{event-contracts,module-boundaries}.js`）、ER 面（`design/03-entity-relationship/data/07-event-infrastructure.js`／`02-device-control.js`）、TDD 设计（`design/07-tdd/api/02-device-control-tdd-design.md` TC-011／TC-012）、模拟器与前端面板（`src/web-admin/**`）以及 `publish/**` 镜像。

### 四、收尾批（第 ④ 笔，**同一条 `v145` 内追加**；⛔ 上文三笔逐字未改）

> **来源／依据**：需求方 2026-10-16 裁定「**按退役撤**」＋ **授权扩面**到设计卡与计数判据。**背景**：第 ③ 笔只撤了协议／TS／C 三面，**REST 端点与设计卡留在位** ⇒ 调用 `POST /api/v1/control/devices/{id}/snore-intervention/stop` 时 `router.py` 的 `DOWNLINK[action]` 抛 `KeyError` ⇒ **未捕获 500**（活缺陷）。

- **撤 `rest-snore-intervention-stop` 设计卡（本册写面）**：`data/rest/02-core-domains.js` 域 02 块原第 29 条控制卡删除；同处 `implementation.note` 追加变更留痕；域动作端点 **29 → 28**、`/control/*` **31 → 30**、REST 卡总数 **243 → 242**。
- **实现侧同批撤**（⛔ 两侧不可分）：`src/server/modules/device_control/api/router.py` 的 `_ACTION_ENDPOINTS` 撤 `("snore-intervention/stop", ("snore_intervention_stop", ()))`；顺带清掉同批连带的死注释（`_REST_ONLY_KEY_ACTIONS` 上方「7 条 stop 类」→ **6 条**）。**为什么不可分**：`_action_missing_paths()` 取「设计卡路径集 ↔ `_ACTION_ENDPOINTS`」的**对称差**，非空即启动期 `RuntimeError`（`D-59` A2 删 `rest-set-user-profile` 同型先例）。
- **引用面同批清零**：`src/server/modules/device_control/application/preference_services.py`（闹钟 action 白名单 `_SUPPORTED_ALARM_ACTION_TYPES`）· `src/web-admin/src/api/deviceIot.ts`（`CONTROL_ACTION_PATHS` **26 → 25 键**）· `src/web-admin/src/api/deviceAlarm.ts`（执行指令类型联合）· `src/server/modules/ai_agent/domain/cacheability.py`（`_OPTIONAL_ROLE_TOOLS` 里的 `StopSnoreIntervention` —— 该工具已于第 ③ 笔退役）。
- **计数／清单判据同批**（⛔ 不同批必红）：`tests/design-regression/test_device_control_commands.py`（29 → 28）· `tests/design-regression/test_device_actions_flat.py`（29 → 28）· `src/web-admin/src/api/deviceIot.spec.ts`（`DESIGN_CONTROL_PATHS` 26 → 25、设计卡 29 → 28）· `tests/design-regression/test_device_control_actions_contract.py`（卡数断言 29 → 28 ＋ 两处范围下限 29 → 28）。
- **判据侧 3↔3 换项**：`scripts/check_event_producer_subset.py` 的 `PROVENANCE_SITES` —— `PlatformCommand.SnoreInterventionStopCommand` **出** ×3（三个漏斗 `_publish`／`_send_text`／`_publish_control`）、`PlatformCommand.VibrationCommand` **入** ×3；⚠️ **条数不变**（`PROVENANCE_SITES_SIZE` 仍 **106**）⇒ ⛔ 未靠改棘轮通关。⚠️ 另 6 条 `Lulling*` 未登记**不属本批**（哄睡，`T-607` 面）⇒ 只登记、不代修。
  - ⚠️ **取代注（产品口径，`rule-0d`；上文旧句一字未改，仅追加本注）**：`snore_intervention` 的**停止语义 ＝ 覆盖式重新下发** —— 重新调用 `snore_intervention`（`StartSnoreIntervention`），改小 `intensity`／`duration`（被取代的原句原文逐字见本条第 ③ 笔的 ⚠️ 行，⛔ 不在此二次留痕）。⚠️ `Q-037` 的 `protocol-changes.md:54` 里 `mode=disable` **仍是提案且未裁** ⇒ ⛔ **不得据它造停止语义**（`snore_intervention` 卡的 `envelope` 现无 `mode` 字段）。

## v143 → v144：`T-625`（`G-453`② 定论落地）—— `protocol.js` 术语表 `media_id` 条目**归属口径订正**（「由设备在 ack 中返回」⇒「由平台在 `play_online_media` 的 call 中下发」）（2026-10-15）

> **来源／依据**：需求方 2026-10-15 逐字授权「**既然已经调查清楚，你就按我们的这个正确结论去做就可以了**」；定论件 ＝ **第 3 层设计工作区**（`.pipeline/design/Q-031/`，定论稿 `evidence-r033-explainer.md`）§6.7「取证⑥：结论」＋ §6.8「建议改哪一句」（改后措辞由其逐字给出）。**定论内容**：`data/iot/smart-bed.js` 的 `play_online_media_ack` 卡 `notes`「ack 仅确认收到，无需返回 media_id（平台已在 call 中下发）」**对**；本册 `glossary.media_id`「由设备在 ack 中返回」**错**（`2026-07-04` `71e718e7` 已把归属由设备改为平台，同日 `9ccbce4a` 新建本件时把 5 小时前刚废的旧口径抄进术语表 ⇒ 该句自落地第一天起即为陈旧副本；`b7dc3b32`（`T-030`）只改对 `media_id_ownership`、漏改本条目）。
> ⚠️ **版本号口径（⛔ 不得回改）**：改前本文件实际最大号 ＝ **`v143`**（`T-621` 占用）⇒ 本条接续 `v143 → v144`；`v143` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。
> 🔴 **本条判据要点**：改后本册三处归属叙述**同向**（`glossary.media_id` · `keyDistinctions.media_id_ownership` · `data/iot/smart-bed.js` 的 ack 卡 `notes`）⇒ ⛔ 不得再把「ack 里返回 `media_id`」读成现行口径。**现行**「ack 里带 id」只有两种形态、且**返回方都是平台**：`stream_voice_command_ack.payload.stream_id` 与 `schedule_action_query_ack.payload.schedule_actions[]`。

### 一、改了什么（逐件：内容锚点 ＋ 改前 → 改后）

- **1｜`data/iot/protocol.js`** —— `glossary` 块的 `media_id` 条目（内容锚点：`stream_id:` 条目之后、`EOS:` 条目之前）：**改前逐字** `    media_id: "媒体播放标识，由设备在 ack 中返回，后续暂停/恢复/停止操作通过此 ID 指定目标",` ⇒ **改后逐字** `    media_id: "媒体播放标识，由平台在 play_online_media 的 call 中下发（首播建立 media_id↔URL 映射），后续暂停/恢复/停止操作通过此 ID 指定目标",`（⚠️ 措辞对齐同件 `keyDistinctions.media_id_ownership`）。⛔ 单行替换 —— 本件**只改这一行**。
- ⚠️ **`rule-0d` 取代注记**：被取代的旧口径**原文逐字**即上列「**改前逐字**」（⛔ 本册不再于别处复述该句，以免二次留痕分歧）。

### 二、⛔ 未改动面（如实登记）

- 同件 `keyDistinctions.media_id_ownership`：⛔ 一字未动（它**本就站在正确一边**，是本次订正的**对齐基准**）。
- 同件 `glossary.stream_id` · `changeLog` 字段 · 渲染页相关叙述：⛔ 一字未动。
- `data/iot/smart-bed.js`：⛔ 一字未动（其 `play_online_media_ack` 的 `notes` 是定论的**正确一侧**）。
- `design/01-raw-input/**`：⛔ 一字未动（原始输入档里「`media_id` 由设备在 ack 中返回」是 2026-06-17 的**历史原文**，冻结不改）。

### 三、`publish` 镜像（本批收口）

- 单件镜像 ×1：`publish/api/data/iot/protocol.js`（**逐件 `Copy-Item` ＋ 双侧 `Get-FileHash`**，sha256 相等）。
- 同批 ×1：`publish/api/CHANGELOG.md`（本册镜像，`ExtraMappingTable` 单件）随本条目同步。
- ⛔ **未跑 `-Apply`**（纪律：单件 `Copy-Item` 为主，`-Apply` 会连带删除"残留"类孤儿）。

## v142 → v143：`T-621`（`Q-038` 实现轮）—— `sleep_report_trigger` **新增 `image_resolution`**（新公共枚举 `report_image_size`，闭集恰 2 档）＋ 断开 `display_output.resolution` 那根线（2026-10-15）

> **来源／依据**：用户 2026-10-14 原话（逐字）「**记一个设计任务：在睡眠报告 trigger 时增加一个参数，让调用方能够设置渲染图片输出的分辨率。先把这个设计的桩建起来，然后再继续细化。**」；三条前置裁定（`A` 值域形态／`D` 与 `display_output.resolution` 的关系／`E` 非法值报错方式）＋ `P-148`（"不传"时的默认值取哪个数）＝ 需求方 2026-10-15 逐字裁定；定稿设计件 ＝ `.pipeline/design/Q-038/`（`design.md` §10 ＋ §10.1b、`constraints.md` §8）。
> ⚠️ **版本号口径（⛔ 不得回改）**：改前本文件实际最大号 ＝ **`v142`**（`T-610` 占用）⇒ 本条接续 `v142 → v143`；`v142` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。
> 🔴 **本条的判据要点（⛔ 退回"计数相等"即失效）**：`image_resolution` 两档**像素数相等**（1280×720 ＝ 720×1280 ＝ **921600**）⇒ 判据**必须判宽高对**，⛔ 像素数**判不出**两档差异；反向臂按 `P-148` **新口径**（「**同一入参下的确定性** ＋ **未传时 ＝ 平台默认**」），⛔ **不再**用"与改前逐字节一致"。

### 一、改了什么（逐件：内容锚点 ＋ 改前 → 改后）

- **1｜`data/iot/smart-bed.js`** —— `sleep_report_trigger.envelope.payload` **新增键**（内容锚点 `enable_json:` 之后）：**改前** 该 payload 末键 ＝ `enable_json`；**改后** 追加 `image_resolution`（`{ type: "enum", ref: "report_image_size", required: false, desc: … }`）。⚠️ **⛔ 不设 `default`** —— 键缺席 ⇒ 平台默认参数；`desc` 逐字写明：**闭集恰 2 档**（`standard` 1280×720 横版 ／ `portrait` 720×1280 竖版）、**键在场即校验**（非法 ⇒ `4001`、⛔ 不静默忽略、⛔ 不产生 image push）、**生命期＝单次请求**。
- **2｜`data/iot/smart-bed.js`** —— `sharedEnums` **新增公共枚举** `report_image_size`（紧跟 `report_period` 之后、结构同级；⛔ `action_name` **一字未动** ⇒ 55 业务值 ＋ 1 内置 `error` **不变**）＝ `standard`（1280×720，**平台默认档**）／`portrait`（720×1280），**逐值写死 `width×height`**（⛔ 不留"档位名 → 尺寸"的第二处散文）。⚠️ 方向依据 ＝ **三处可执行读数**（`renderer.py:89-90` 的 `DEFAULT_WIDTH`/`DEFAULT_HEIGHT` · `templates/default/page.svg:24` 的 `viewBox` · `svg_report.py:76` 的 `DESIGN_WIDTH = 1280.0`）⇒ 一致指向 **1280×720 横版**；⛔ **不照四处散文**（写成 720×1280 竖版者）。
- **3｜`data/iot/smart-bed.js`** —— `sleep_report_trigger` 的 `description` ＋ `notes`：**改前**两处**均无**渲染尺寸来源的任何陈述；**改后** 均补「渲染尺寸**可由调用方指定**（`image_resolution`）＋ ⛔ **不传 ⇒ 平台默认参数 1280×720** ＋ ⛔ **不回落** `connect.configurations[].resolution`」＋「取值非法 ⇒ 受理即回 `4001`、**不产生任何 image push**」。
- **4｜`data/iot/smart-bed.js`** —— 同卡的 `errors[]`：**改前** 第 1 项说明串逐字 `period 不在枚举范围`；**改后** `period 不在枚举范围 / image_resolution 不在枚举范围（闭集恰 2 档）`（⛔ **码值 `4001` 一字未改**）。
- **5｜`data/iot/smart-bed.js`** —— 🔴 **断线连带面 ×3**（**既有取证漏点，本批补入**）：
  - ① `sleep_report_image_push.images.desc`：**改前逐字**「每张分辨率=设备注册上报的 resolution」⇒ **改后**「**每张分辨率由本次 `sleep_report_trigger` 的 `image_resolution` 入参决定；未传 ⇒ 平台默认参数 1280×720**；⛔ 不再取设备注册上报的 `resolution`」（该句与实现**直接矛盾** ⇒ 必须同批改）。
  - ② `connect.notes` 末句：**改前逐字**「display_output 子设备经 resolution 字段一次性上报像素宽/高（**睡眠报告图片渲染用**）」⇒ **改后** 该括注改为「（**设备显示能力登记**；🔴 `Q-038` 起 ⛔ **不再作为睡眠报告图片渲染尺寸来源**）」（⛔ 旧括注原样失效）。
  - ③ `connect.fields.resolution` 卡 `desc`：**改前逐字**「…用于睡眠报告图片渲染分页（每张图分辨率=本值）」⇒ **改后** 声明用途改为「**设备显示能力登记**；⛔ 不再用于睡眠报告图片渲染分页」＋「**字段本体保留**，退役与否另案」。⚠️ **⛔ 未改**该字段的 `type`／`required`／`fields`（`width_px`／`height_px`）**一个字节**。
- **6｜`data/iot/smart-bed-examples.js`** —— `sleep_report_trigger` 示例：**改前** `call` ＝ `{sub_device_id, date, period, enable_text, enable_voice, enable_image, enable_json}`；**改后** 追加 `"image_resolution": "standard"`，`description` 同步补渲染尺寸来源与断线口径。
- **7｜`data/iot/event-identifier.js`** —— `sleep_report_trigger` 的 `desc`：**改前** 只列 `enable_text/enable_voice/enable_image`；**改后** 补 `enable_json` ＋ `image_resolution`（档位闭集 ＋ 未传走平台默认 ＋ 非法 `4001`）。
- **8｜`data/iot/smart-bed.js`** —— **文件头新增 `v13` 变更块**（`rule-0d` 形态：**追加**，⛔ 未改 `v12` 及更早块）：逐条记 `image_resolution` 的五处落点 ＋ 被改各处**旧文逐字 → 现文** ＋ 🔴 **已知不可消去处**（`v9` 块 `:122` 的 `layout` 原文内嵌旧句『每张分辨率=设备注册上报的 resolution』⇒ 属历史留痕，⛔ **不得改写**；故「全仓 grep 旧句 0 命中」这条臂的射程**必须排除变更块历史留痕**，只判**活卡**）。
- **9｜`src/server/events/base.py`** —— `SleepReportImagePush` 的 **docstring**：**改前逐字** `payload 仅 images URL 数组：single/long 布局=1 项、multi 布局=N 项（每张分辨率=设备注册上报的 resolution）；`；**改后** 改述为「每张图片的**分辨率**由本次 `trigger` 的 `image_resolution` 入参决定、未传 ⇒ 平台默认 1280×720；⛔ 不再取设备上报的 `resolution`」＋ 补一条「`single/long/multi` 三版式已随 `R-037` 批② 退役 ⇒ 上句「多布局 N 项」只作历史留痕」（⚠️ **纯 docstring，代码零改**）。

### 二、与实现／契约的**三面同批**（⛔ 少一面即 `F1` 判红）

- 实现 dataclass：`src/server/events/base.py` 的 `SleepReportTriggerCommand` **新增类字段** `image_resolution: str | None = None`（＋ docstring 语义）。
- 事件契约：`design/05-backend-architecture/data/event-contracts.js` 的 `DeviceCommand.SleepReportTriggerCommand` 的 `fields` **补同一键**（＋ `note` 追加取代注记）。
- ⇒ `scripts/check_event_contract_protocol_parity.py`（`T20`／`F1` **零豁免严格判红** 的 4 张上行卡之一）：三面同批后 **`rc=0`**（实测该卡三面字段集**逐名相等**）。

### 三、`publish` 镜像（本批收口）

- 单件镜像 ×3：`publish/api/data/iot/smart-bed.js` ＋ `publish/api/data/iot/smart-bed-examples.js` ＋ `publish/api/data/iot/event-identifier.js`（**逐件 `Copy-Item` ＋ 双侧 `Get-FileHash`**，sha256 逐件相等）。
- 同批 ×1：`publish/api/CHANGELOG.md`（本册镜像，`ExtraMappingTable` 单件）随本条目同步。
- ⛔ **未跑 `-Apply`**（纪律：单件 `Copy-Item` 为主，`-Apply` 会连带删除"残留"类孤儿）。

### 四、⛔ 未改动面（如实登记）

- **`sharedEnums.action_name`**：⛔ 本批**一字未改**（`image_resolution` 是 `payload` 字段、`report_image_size` 是**另一个**枚举）⇒ `ZRE_ACTION_COUNT`／端侧 `k_names[]`／`zreActions.ts` 计数面**均不变**（已知 `check_action_registry_parity.py` 的 `P_all 56` ／ `P_biz 55` 读数照旧）。
- **`connect.configurations[].resolution` 字段本体**：⛔ 保留（只改三处 `desc` 散文，见上「一、5」）⇒ **⛔ 不改协议结构、⛔ 不删字段**。
- **`images[]` 的长度／顺序语义**：⛔ 一字未改（本批只改**尺寸**，⛔ 不改**页数**、⛔ 不改 URL 结构、⛔ 不改登记列结构）⇒ `[]` 仍**只**表"整份报告拿不到"。

## v141 → v142：`T-610`（`R-047` 收口批）—— 床模式 **`game` 并入 `cinema`**（`sharedEnums.mode` `7 → 6`）＋ `flat` 标注由「推定」升为「用户明示」（2026-10-14）

> **来源／依据**：用户 2026-10-14 **第二批裁定**原话（逐字节选）：「**观影和游戏就是一种模式，我们用观影这个名字就好**，**阅读是一种独立模式，要保留**，**放平也是独立模式**」；引在册裁定 `D-297`（该条原写「`flat` 按"上一轮单独保留过它"**推定为第 7 值**，⛔ 非用户明示」⇒ **该推定已由本批升为「用户明示」**）。原文见 `.pipeline/journal/decisions-2026-10-14-r047-and-parallel.md` §2。
> ⚠️ **版本号口径（⛔ 不得回改）**：改前本文件实际最大号 ＝ **`v141`**（`T-607` 占用）⇒ 本条接续 `v141 → v142`；`v141` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。
> ⚠️ **本条目用列表而非表格**（同 `v139 → v140`／`v140 → v141` 条目的处置与理由）。
> 🔴 **判据形态（本批要点，⛔ 不得退回"计数相等"）**：本轮计数**确实变了**（协议 `7 → 6`、ER 含状态值 `8 → 7`、`tools.py` `7 → 6`），但这⛔ **不构成"可以只钉计数"的理由** —— 同一次改动里 `−game` 与任何 `＋x` 可以互相抵消。⇒ 判据仍是**集合比对 ＋ 反向臂**（在册判据 ＝ `tests/design-regression/test_bed_mode_set_parity.py`；本轮新加两条反向臂：把 `game` 加回来须判红、单独丢 `reading` 须判红）。

### 一、改了什么（逐件：内容锚点 ＋ 改前 → 改后）

- **1｜`data/iot/smart-bed.js`** —— `sharedEnums.mode`（内容锚点 `mode: {` 的 `name: "mode"`）：**改前** `values` ＝ `zero_gravity`／`cinema`／`game`／`lounge`／`reading`／`yoga`／`flat`（**7 值**）；**改后** ＝ `zero_gravity`／`cinema`／`lounge`／`reading`／`yoga`／`flat`（**6 值**）。⚠️ `game` **不是删除语义而是并入** ⇒ `cinema` 的 `desc` 就地写明「含原「游戏」语义」＋ 用户原话；`desc` 头部的清单同步改为 6 值口径（⛔ 仍`别名归并、不新增值`）。
- **2｜`data/ai-tools/tools.js`** —— `SetBedMode` 的 `openai.function.parameters.properties.mode`：**改前** `enum` ＝ 7 值（含 `game`）；**改后** ＝ 6 值；`description` 两处（工具卡 `description` ＋ `function.description`）与 `mode` 参数 `description` 同步重述（「游戏」**同样** ⇒ `cinema`、⛔ 无独立 `game` 值）。⚠️ 其余工具卡**一字未改**。
- **3｜`data/rest/examples.js`** —— `mode_control` 的 `limits.options`：**改前** ＝ `[zero_gravity, cinema, game, lounge, reading, yoga, flat, none]`；**改后** ＝ `[zero_gravity, cinema, lounge, reading, yoga, flat, none]`（去掉 `game`，**保留状态值 `none`** —— 它⛔ 不是预设）。
- **4｜`bed_mode` 卡 `notes`**：**未改**（`v140 → v141` 那条取代注仍然成立、逐字有效）；本批对该卡的语义无新增改动 ⇒ ⛔ 不为凑数造改动。

### 二、`publish` 镜像（本批收口）

- 单件镜像 ×3：`publish/api/data/iot/smart-bed.js` ＋ `publish/api/data/ai-tools/tools.js` ＋ `publish/api/data/rest/examples.js`（**逐件 `Copy-Item` ＋ 双侧 `Get-FileHash`**，sha256 逐件相等；纪律 ＝ 引在册裁定 `D-285`）。
- 同批 ×1：`publish/api/CHANGELOG.md`（本册镜像，`ExtraMappingTable` 单件）随本条目同步，形态同上。
- `scripts/sync_design_to_publish.ps1 -Check`：本批**入场读数** ＝ `RED`（修改 `4`／新增 `0`／删除 `0`／残留 `0`）；**出场读数** ＝ `GREEN`（四类均为 `0`，`rc=0`）。
- ⛔ `publish/index.html` 未动（`NoSyncTargets`：不判定／不复制／不计残留）。

### 三、⛔ 未改动面（如实登记）

- **ER 域（域 03）**：`BedMode` 枚举真身在 `design/03-entity-relationship/data/01-device-iot.js` ⇒ 本域**无对象**（该面的 `game` 摘除见 `design/03-entity-relationship/data/CHANGELOG.md` 的 `v7.60 → v7.61`）。
- **`bed_mode` 卡 `description`／`envelope`／`response`／`errors`**：本批**一字未改**。
- **`lulling` 卡**：本批**一字未改**。

## v140 → v141：`T-607`（`R-047` 批 4）—— **床模式枚举集合变更**（`sharedEnums.mode` `6 → 7`：`−sleep`／`−custom`，`+game`／`+lounge`／`+yoga`）＋ 哄睡 **AI 工具面补卡**（`StartLulling`／`StopLulling`）＋ `rest` 示例同批（2026-10-14）

> **来源／依据**：引在册裁定 `D-297`（用户 2026-10-14 五条裁定之「床模式」与 `U-11`：**以本轮 6 值清单为准** ＝ 观影／游戏／休闲／零重力／阅读／瑜伽，并把「平放」按"上一轮单独保留过它"**推定为第 7 值**）＋ 在册挂账 `G-505①`（`tools.py` 底表与裁定清单**不一致**）。
> ⚠️ **就地订正（`T-610`，2026-10-14 同日）**：上句「**推定为第 7 值**」的标注**已作废** —— 用户第二批裁定**明示**「放平也是独立模式」⇒ 现行口径是**用户明示**（⛔ 不是编排器推定）；同理 `game` 已并入 `cinema`（`7 → 6`）。原句其余部分逐字保留（⛔ 不重写）。
> ⚠️ **版本号口径（⛔ 不得回改）**：改前本文件实际最大号 ＝ **`v140`**（`T-605` 占用）⇒ 本条接续 `v140 → v141`；`v140` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。
> ⚠️ **本条目用列表而非表格**（同 `v139 → v140` 条目的处置与理由）。
> 🔴 **本批的核心不是"改了几个字"，而是"判据形态"**：`mode` 的成员**全变**（改前 `6` ⇒ 改后 `7`；⚠️ `tools.py` 那侧是 `7 → 7`）—— 与本仓实测过的 **`55 → 55` 同型陷阱**（端侧注册表计数不变、集合变了）同族 ⇒ 本批的判据一律**集合比对**，⛔ **不得**只钉计数（在册判据 ＝ `tests/design-regression/test_bed_mode_set_parity.py` 的四面对拍 ＋ 逐值钉死的预期集）。

### 一、改了什么（逐件：内容锚点 ＋ 改前 → 改后）

- **1｜`data/iot/smart-bed.js`** —— 三处同批：
  - ① `sharedEnums.mode`（内容锚点 `mode: {` 的 `name: "mode"`）：**改前** `values` ＝ `zero_gravity`／`sleep`／`cinema`／`reading`／`flat`／`custom`（**6 值**）；**改后** ＝ `zero_gravity`／`cinema`／`game`／`lounge`／`reading`／`yoga`／`flat`（**7 值**）。⚠️ **别名归并、⛔ 不新增值**：零压力／太空舱／天空舱 ⇒ `zero_gravity`；小飞燕 ⇒ `yoga`；放平复位／一键放平／一键复位／平躺 ⇒ `flat`（逐条写进各 `desc`）。
  - ② `bed_mode` 卡**追加 `notes`（取代注，`rule-0d`：⛔ 不静默改）**：`sleep`／`custom` 已摘除 ⇒「睡眠模式／晚安／Good night」**如实告知不支持**；哄睡相关说法改走 `lulling`（哄睡执行器）。⚠️ 该卡 `description`／`envelope`／`response`／`errors` **一字未改**。
  - ③ `lulling` 卡**追加 `notes` 尾句**：AI 工具面已补 `StartLulling`／`StopLulling`（**带 `role`**，目标 `device_type = sleep_lulling_executor`）⇒ 真下发本 action，⛔ 不再降级成 `bed_mode(sleep)`。⚠️ 该卡字段**一字未改**。
- **2｜`data/ai-tools/tools.js`** —— 两张**新工具卡** ＋ 一张卡改枚举：
  - ① `SetBedMode` 的 `openai.function.parameters.properties.mode.enum`：**改前** `sleep`／`zero_gravity`／`cinema`／`reading`／`flat`／`massage`／`custom`（`7`）；**改后** `zero_gravity`／`cinema`／`game`／`lounge`／`reading`／`yoga`／`flat`（`7`）＋ `description` 与 `function.description` 同步重述（含别名与「睡眠说法如实不支持」）。
  - ② **新增** `StartLulling`（`action: lulling`，`required: ["role","intensity"]`；`role` 的目标 `device_type = sleep_lulling_executor`）与 `StopLulling`（`action: lulling_stop`，`required: []`）。
  - ③ 分组索引 `睡眠辅助.tools` 追加 `StartLulling`／`StopLulling`。⚠️ 改前 `massage` 是 `SetBedMode` 的取值之一 —— 它在协议 `mode` 里**从来没有过**（＝改前"设计↔实现不同源"的一处实证，同批收敛）。
- **3｜`data/rest/examples.js`** —— `mode_control` 的 `limits.options` 同步为同一集合，并保留状态值 `none`。

### 二、`publish` 镜像（本批收口）

- 单件镜像 ×3：`publish/api/data/iot/smart-bed.js` ＋ `publish/api/data/ai-tools/tools.js` ＋ `publish/api/data/rest/examples.js`（**逐件 `Copy-Item` ＋ 双侧 `Get-FileHash` 比对**，sha256 逐件相等；⛔ 不写「按脚本同步」—— `-Apply` 形态会连带改授权面之外的件，纪律 ＝ 引在册裁定 `D-285`）。
- 同批 ×1：`publish/api/CHANGELOG.md`（本册镜像，`ExtraMappingTable` 单件）随本条目同步，形态同上。
- `scripts/sync_design_to_publish.ps1 -Check`：本批**入场读数** ＝ `RED`（修改 `4`／新增 `0`／删除 `0`／残留 `0`；四件全是本批真身改动 ⇒ 预期形态）；**出场读数** ＝ `GREEN`（四类均为 `0`，`rc=0`）。
- ⛔ `publish/index.html` 未动（`NoSyncTargets`：不判定／不复制／不计残留）。

### 三、⛔ 未改动面（如实登记）

- ⛔ **未动 `data/iot/protocol.js`／`event-identifier.js`／其余 `data/iot/**` 件**（本批 `mode` 只改 `smart-bed.js` 一处公共枚举 ＋ 两张卡 `notes`）。
- ⛔ **未动 `action_name`／卡数**：`action_name` 仍 **56**、卡面仍 **55**（本批不加卡、不加 action —— 哄睡走**既有** `lulling`／`lulling_stop`）。
- ⚠️ **未动按摩「速度」面**（`P-2`／`massage_mode`）：该项的协议侧改动**代价 ≥15 件**（含 ER／架构／前端／多处判据）⇒ 本批**如实登记为未落**，留给编排器按 `rule-0b` 六步另批处置（⛔ 本批不半改）。
- ⛔ 未改任何**门禁阈值／棘轮**（`scripts/check_enum_coverage_matrix.py` 的 `MIN_*` 一个都没动；变的是**真值面**，薄包装的真值断言与集合判据同批更新）。


## v139 → v140：`T-605`（ζ2 合批 · `R-027` 片 `B` **余项**）—— `sleep_report_image_push.images` **字段级空列表语义落卡** ＋ 同 `desc` 的**已落形态读数订正**（单侧恒 `4` ／双睡眠位 `4 × 侧数` = `8`）＋ 示例件**四形式空态示例补齐**（2026-10-14）

> **来源／依据**：`R-027` 片 `B` 的**两项余量**（片 `B` ① ＝ `sleep_report_image_push.images` 的**字段级空列表语义**；⑤ ＝ 示例件**四形式空态示例**）。片 `B` ②③④ 与片 `C` 四处**四处同批已落**于 `00136ad8` ⇒ ⛔ 本批一字未动。
> ⚠️ **本条目用列表而非表格**（同 `v138 → v139`／`v137 → v138`／`v136 → v137` 条目的处置与理由）。
> ⚠️ **行号口径**：本条一律以**内容锚点**为准（多泳道共享同一 `main` 工作区；⛔ 不引旧行号）。
> ⚠️ **发布面收口**：两件镜像（`publish/api/data/iot/smart-bed.js`／`publish/api/data/iot/smart-bed-examples.js`）与 `publish/api/CHANGELOG.md` 均已**逐字节同步**（**单件 `Copy-Item` ＋ 双侧 `Get-FileHash`**；⛔ 不写「按脚本同步」—— `-Apply` 形态会连带改授权面之外的件，纪律 ＝ 引在册裁定 `D-285`）；逐件 sha256 读数见本批收口回执（`T-605`）。
> ⚠️ **版本号口径**：上一条 `v138 → v139` 已由 `T-600` 占用（同日落地）⇒ 本条接续为 `v139 → v140`（⛔ 不改写既有条目、⛔ 不重号）。

### 一、改了什么（逐件：内容锚点 ＋ 改前 → 改后）

- **1｜`data/iot/smart-bed.js`** —— 只动 `sleep_report_image_push` 一张 endpoint 卡的 `images` 字段 `desc`（内容锚点 `id: "sleep_report_image_push"` 的 `images: { type: "array"` 行），**两处同批**：
  - ① 🔴 **补「空列表语义」**（片 `B` 余项 ①）：**改前**该 `desc` 全文对「空列表」／「`images: []`」**零命中**（空列表口径**只**写在 `sleep_report_trigger.notes`）⇒ 只读本卡的固件开发者**读不到**「帧必发、数组为空」；**改后**在「（从前到后显示即可）。」之后**插入**整段：`images: []` ＝ **整份报告拿不到**的空态帧（**帧必发**）＋「⛔ `[]` **不用于**任何『部分拿不到』的情形」＋「**有报告**（哪怕字段稀疏）⇒ 长度**恒等于本次渲染的计划页数**（关系式：计划页数 ≡ 落盘 PNG 数 ≡ 本数组元素数，⛔ 不写死 `3`／`4`）、**恒满不缩**」。⚠️ 口径与 `sleep_report_trigger.notes` 的「🔴 `R-027` 四形式空态」段**同源**（⛔ 未另立第二口径）。
  - ② ⚠️ **同 `desc` 的已落形态读数订正**（**不是新增口径**，是陈旧读数）：**改前**逐字『① 单睡眠位——摘要页（＋体征页）（＋环境页，由平台 `show` 开关决定）；② 双睡眠位——**每侧 1 张侧别摘要页**』＋『`Q-025` 的目标形态（单侧恒 4／双睡眠位恒 8）与 `1280×720` 设计空间**另批**落地（依据 `P-129`：四页的第 3/4 页构成待裁）』；**改后**『① 单睡眠位 —— **恒 4 页**（`Q-025` 目标形态；`P-129` 的四页构成已由 `P-129b` `C3` 第四轮落地）；② 双睡眠位 —— **每侧 4 页 ⇒ `4 × 侧数` = 8 张**』。⚠️ 判据：`P-129b` 的 `C3` 第四轮**已落**（提交 `10c85ef7`，2026-10-12）⇒ 原「另批」承诺**已兑现**、卡上文字成为陈旧读数；实现锚点 ＝ `domain/svg_report.py::_single_side_pages`（四条 `plans.append`）＋ `renderer.py` 的 `DEFAULT_WIDTH = 1280`／`DEFAULT_HEIGHT = 720`。
  - ③ **同件文件头追加 `v12` 变更块**（＝本条的**设计侧留痕**：改面 ＋ `rule-0d` 取代注记「被改处旧文逐字」）；⛔ 文件头 `v11` 变更块**一字未改**（历史留痕只追加、不改写）。
  - ⛔ **只改字段 `desc`**：⛔ 未加卡、⛔ 未改卡数、⛔ 未动 `errors[]`／`rpc`／`response` ⇒ 引在册挂账 `G-502` 的「设计侧**加卡** ⇒ 至少四处钉会陈旧」**不触发**（本批非加卡）。
- **2｜`data/iot/smart-bed-examples.js`** —— 补**四形式空态示例**（片 `B` 余项 ⑤）：
  - ① **新增** `show_text_sleep_report_fetch_failed`（`show_text` 空态示例：`level: "warning"` ＋ 标题「睡眠报告」＋ 正文逐字「未能获取这个报告」）；
  - ② **新增** `play_stream_voice_sleep_report_fetch_failed`（`play_stream_voice` 空态示例，与 ① **同句同源**）；
  - ③ **新增** `sleep_report_image_push_empty`（`images: []` 空态帧示例）；
  - ④ **既有** `sleep_report_json_push_empty`（`report_json: {}`）**逐字未动**（片 `B` ⑤ 的原读数：该件 `_empty` 标识**全件命中 1** ⇒ 本批补 ①②③ 三形式后**命中 3**）。
  - ⑤ ⚠️ **一处同批的陈旧注释订正**（如实登记，面外但同件同段落）：同册节头注释原写「sleep_report_trigger（**三开关**…）」—— `R-027` 片 `B` 已把开关改为**四开关**（含 `enable_json`）⇒ 订正为「四开关」。⛔ 仅注释文字，⛔ 不影响任何数据结构。
  - ⚠️ **如实登记（本批未动）**：既有 `sleep_report_image_push` 示例项**缺 `responseAction`**（同册 `data/iot/smart-bed.js` 的同名卡有 `sleep_report_image_push_ack`）—— 属**存量不一致**、⛔ 不在片 `B` ⑤ 的写面（片 `B` ⑤ 只要求「四形式**各**一条空态示例」）⇒ 本批**未改**，留编排器裁决（见收口报告「未判定项」）。
- ⛔ **只动这两件**：同册其余卡（含 `sleep_report_trigger` 卡）、`data/iot/**` 其它件（`protocol.js`／`event-identifier.js`）、其它 `design/**` 册**一字未改**。

### 二、`publish` 镜像（本批收口）

- 单件镜像 ×2：`publish/api/data/iot/smart-bed.js` ＋ `publish/api/data/iot/smart-bed-examples.js`（**逐件 `Copy-Item` ＋ 双侧 `Get-FileHash` 比对**，sha256 逐件相等）。
- 同批 ×1：`publish/api/CHANGELOG.md`（本册镜像，`ExtraMappingTable` 单件）随本条目同步，形态同上。
- `scripts/sync_design_to_publish.ps1 -Check`：本批**入场读数** ＝ `GREEN`（四类清单均为 `0`，`rc=0`）；**出场读数** ＝ `GREEN`（逐字读数见收口报告）。
- ⛔ `publish/index.html` 未动（`NoSyncTargets`：不判定／不复制／不计残留）。

### 三、⛔ 未改动面（如实登记）

- ⛔ 未改任何**判据／门禁／棘轮**（本件写面不含 `scripts/**`、`design-workbench/**`、`tests/**`）。
- ⛔ **未改 `src/**`**（片 `B` 是**协议面**；片 `E` 的 `voice` 臂与 `R-037` 的 `T3` 属**另两件**，其读数见 `T-605` 收口报告）。
- ⚠️ **公共枚举 `action_name` 集合面 ⛔ 未动**（本批**不加取值**）：`endpoint_id` 命中数 **55**、`ackOnly` **55**、TS 注册表 **56**、C 库 **56** —— 与 `v137 → v138` 的读数**逐数相同**（片 `C` 四处同批已落，本批无对象可改）。

## v138 → v139：`T-600`（承 `G-433`）—— `rest-device-create` 卡**补声明** `device_secret`（同卡三处同批；⛔ 门禁读数不变）（2026-10-14）

> **来源／依据**：挂账 `G-433`（`T-531` 回执 §5.2 实测：`rest-device-create` 的实现响应含 `device_secret`，而卡的 `responses[].fields` 只声明 `id` ＋ 实时三键）。⚠️ 该卡在静态判据里是 `unresolved:nonstr-key` ⇒ **不判、无红**（`T-531` 按"补实时三键"的面执行、⛔ 未扩面）⇒ 本条是**声明完整性**修正，⛔ 不是"修红"。
> ⚠️ **本条目用列表而非表格**（同 `v137 → v138`／`v136 → v137`／`v135 → v136`／`v134 → v135` 条目的处置与理由）。
> ⚠️ **行号口径**：本条一律以**内容锚点**为准（多泳道共享同一 `main` 工作区；⛔ 不引旧行号）。

### 一、改了什么（逐件：内容锚点 ＋ 改前 → 改后）

- **1｜`data/rest/01-device-iot.js`** —— 只动 `rest-device-create` 一张卡（内容锚点 `id: "rest-device-create"`），**三处同批**补 `device_secret`：
  - ① 成功响应**示例 JSON**（内容锚点 `"fault_level": "none"`，201 段）：**改前** 以 `"fault_level": "none"` 收尾；**改后** 追加 `,` ＋ `"device_secret": "<一次性下发的准入密钥原文>"`。
  - ② `responses[0].fields`（内容锚点 `["fault_level", "enum"`）：**改前** **4** 行（`id` ＋ 实时三键）；**改后** 追加第 **5** 行 `device_secret`（`string`；desc 逐字载「**创建时一次性下发**的准入密钥原文（平台生成的缺省值，明文、Token 口径；⛔ 不写日志／不落盘／不进 URL）」＋「它是**占位、⛔ 不是设备可用凭据**」）。
  - ③ 卡尾 `response.body`（内容锚点 `status: 201, body:`）：**改前** **4** 键；**改后** 追加 `device_secret: "string"`（**5** 键）。
  - ④ 同卡 `description` **追加一段留痕注**（内容锚点 `G-433`）：记三处同批 ＋ 实现侧锚点（`modules/device_iot/api/router.py:457-458` 的展开式返回：在 `_device_to_dict(...)` 之上再挂 `device_secret`）＋ **如实登记"实现侧尚有 8 个静态键未在本卡声明"**（`G-433` 只点名 `device_secret`，⛔ 本件未扩面）。
- ⛔ **只动这一张卡**：同册其余卡、`data/iot/**`、其它 `design/**` 册**一字未改**。

### 二、`publish` 镜像（本批收口）

- 单件镜像：`publish/api/data/rest/01-device-iot.js`（**单件 `Copy-Item` ＋ 双侧 `Get-FileHash` 比对**，sha256 相等）。
- 同批：`publish/api/CHANGELOG.md`（本册镜像，`ExtraMappingTable` 单件）随本条目同步，形态同上。
- ⛔ `publish/index.html` 未动（`NoSyncTargets`：不判定／不复制／不计残留）。

### 三、⛔ 未改动面（如实登记）

- ⛔ 未改任何**判据／门禁／棘轮**（本件写面不含 `scripts/**`、`design-workbench/**`）。
- ⛔ **未改 `src/**`**（实现侧一字未动 —— 本件是**卡向实现对齐**，⛔ 不是反过来）。
- ⚠️ **门禁读数不变**（本件实测）：`scripts/check_rest_response_field_subset.py` ⇒ 声明面 `58` ／被判面 `44` ／未解析面 `14` ／字段级漂移 `0`（逐值复算见 `T-600` 报告）。


## v137 → v138：`R-027`（片 `B`／片 `C`）—— 协议面落**第四形式 `json`**（睡眠报告结构化数据推送）：`sleep_report_trigger` **三开关 → 四开关** ＋ 新增 `sleep_report_json_push` endpoint 卡 ＋ 公共枚举业务值 `54 → 55`（2026-10-13）

> **来源／依据**：`R-027` 的四形式口径（`image`／`text`／`voice`／`json`）＋ `Q-030` `§6.0` 的 `Q6` 裁定（取数失败**不再回 `6003`**，改回 `code=0`）＋ 空报告口径的收口（**帧照发、`report_json` 为裸 `{}`**，取代 `Q-029` `§3.3` 的 `empty`／`empty_reason`／`empty_message` 三态）。落地提交 ＝ `00136ad8`（提交主题逐字：「`R-027` 片B片C一次落地：协议面与事件层加 JSON 报告形式」）。
> ⚠️ **本条目用列表而非表格**（同 `v136 → v137`／`v135 → v136`／`v134 → v135` 条目的处置与理由）。
> ⚠️ **行号口径**：本条一律以**内容锚点**为准（多泳道共享同一 `main` 工作区；⛔ 不引旧行号）。
> ⚠️ **发布面收口**：四件镜像与 `publish/api/CHANGELOG.md` 均已**逐字节同步**（**单件 `Copy-Item` ＋ 双侧 `Get-FileHash`**；⛔ 不写「按脚本同步」—— `-Apply` 形态会连带改授权面之外的件）；逐件 sha256 读数见本批收口报告（`Q-031` 工作区证据件）。

### 一、改了什么（逐件：内容锚点 ＋ 改前 → 改后）

- **1｜`data/iot/smart-bed.js`** —— 件头变更史**追加 `v11` 块**（内容锚点 `v11（`）：记四开关／新 endpoint 卡／四形式空态进 `notes`／分类与枚举追加／实现面另件同步的如实登记；**改前** ＝ 件头变更史最大号 `v10`。正文四处：
  - ① `sleep_report_trigger` 的 `payload`（内容锚点 `enable_image: { type: "bool"`）：**三开关 → 四开关** —— `enable_image` 行末补 `,` 并**新增 `enable_json`**（`bool`／`required: false`／`default: false`；desc 逐字「返回/推送形式开关——结构化 JSON（走 `sleep_report_json_push` 专用推送）。默认 false（与 enable_voice/enable_image 一致）」）。
  - ② `sleep_report_trigger` 的 `description` 与 `notes`（内容锚点 `按 enable_text/enable_voice/enable_image`）：开关分发枚举**同批补 `enable_json` → `sleep_report_json_push`**（⛔ 其余分发语义逐字未改）；`notes` 同批落**四形式空态段**（射程 ＝ 已开启的形式）。
  - ③ **新增 `sleep_report_json_push` endpoint 卡**（落点 ＝ `sleep_report_image_push` 卡之后；内容锚点 `睡眠报告结构化数据推送`）：`cloud-to-device`／`type: call`；`payload` ＝ `{schema_version, report_json}`（`schema_version` **恒为 `1`**，供设备分支与后续演进；`report_json` 为报告全部结构化数据）；`errors: []`；`rpc` 照姊妹卡（`timeout: 30s`／`retry: command`）；`response` 指 `sleep_report_json_push_ack`。🔴 **空报告／取数失败 ⇒ 帧照发、`report_json` 为裸 `{}`**。
  - ④ `categories["睡眠数据"].actions` **末尾追加** `sleep_report_json_push`；`sharedEnums.action_name` **业务值 `54 → 55`** 并新增同名取值项（**插在业务序末尾、`error` 之前**，带 `endpoint_id`）；`actionRelationships` 补 `sleep_report_json_push` 条目。
- **2｜`data/iot/smart-bed-examples.js`** —— 示例面三处：① `sleep_report_trigger` 示例的 `description` 三开关 → 四开关，`call` 内补 `"enable_json": true`；② `sleep_report_image_push` 示例的 `description` 补**空态口径**（「🔴 `R-027`：**整份报告拿不到** ⇒ `images: []` 空态帧**必发**，⛔ 不是整帧不发；**有报告** ⇒ 长度 ＝ 本次渲染的**计划页数**、恒满不缩」）；③ **新增示例 `sleep_report_json_push_empty`**（空报告／取数失败的空态示例：`schema_version: 1` ＋ `report_json: {}`，`ack: null`，`rpc` 同卡，`responseAction` 指 `_ack`）。
- **3｜`data/iot/protocol.js`** —— `changeLog` **追加 `v3.8`**（内容锚点 `v3.8 (`）：**`6003` 的 `producer` 收敛到 `sleep_status_trigger` 面** —— 删去「与 trigger 路径上另一处 `_send_sleep_error_ack(..., 6003, ...)`」这句并列；保留 `devices[].error` 与 `{action}_ack code=6003` 两个 token，并逐字补写「🔴 报告面（`sleep_report_trigger`）自 `R-027` 判据 12 起**不再回 `6003`**（改回 `code=0`）⇒ **本码在该面无产出点**；✅ 本码在 `sleep_status_trigger` 面的**真产出点已核实**」。⛔ 未改同条 `description`、⛔ 未改 `version`（仍 `3.6`）、⛔ 未引任何行号。
- **4｜`data/iot/event-identifier.js`** —— 两处：① `downlink` 映射块**新增** `"sleep_report_json_push"` 条目（指向 `PlatformMessage.SleepReportJsonPush`；desc 载「payload ＝ `schema_version` ＋ `report_json`；空报告／取数失败 ⇒ `report_json` 为裸 `{}`」）；② `ackOnly` 清单**新增** `"sleep_report_json_push_ack"`（⚠️ **⛔ 不进 `values[]`／`action_name`** —— 纯 ack）。

### 二、`publish` 镜像（本批收口）

- 四件镜像（`publish/api/data/iot/` 下的 `smart-bed.js` · `smart-bed-examples.js` · `protocol.js` · `event-identifier.js`）与真身**逐字节同步**：**单件 `Copy-Item` ＋ 双侧 `Get-FileHash` 比对**，逐件 sha256 相等。
- 同批：`publish/api/CHANGELOG.md`（本册镜像，`ExtraMappingTable` 单件）随本条目同步，形态同上。
- ⛔ **`publish/index.html` 未动**（`NoSyncTargets`：不判定／不复制／不计残留）。

### 三、⛔ 未改动面（如实登记）

- ⛔ 未改任何**判据／门禁／棘轮**（本条写面不含 `scripts/**`、`design-workbench/**`）。
- ⛔ 未改 `data/iot/` 下其余件（`rest/**`／`internal/**`／`ai-tools/**` 零改动）。
- ⛔ 未改 `design/index.html` 与 `publish/index.html`。
- ⚠️ **本册变更史与 `version` 字段的既存不一致不在本条处置面**（`version` 仍 `3.6`，沿用 `v137 → v138` 的既立口径）。

## v136 → v137：`T19`（补臂 `A1`／`A2`／`A3` · 销 `G-464` · 挂 `G-484`）—— 上行判定 doc 注释补 `sleep_status_trigger` ＋ 设备端 C 库注册表陈旧读数订正（2026-10-13）

> **来源／依据**：`scripts/check_action_registry_parity.py`（`T19`／`R-038`）**新增三臂**的实跑读数（`A2` 判红 ⇒ 本件改 ①；`A3` 判红 ⇒ 本件改 ②）＋ `G-464`（C 库／TS 注册表对齐 `error`）的**销账触发条件已兑现**（C 注册表 `ZRE_ACTION_COUNT = 55` 含 `error`；TS `ZRE_ACTIONS` 55 含 `error`）＋ `.pipeline/PIPELINE.md`（销 `G-464`／挂 `G-484`）。
> 🔴 **本条的改 ② 是"陈旧读数就地订正 ＋ 史实移入本册"**（⛔ 不是"改协议语义"）：被移出的原句是 `R-032` **落地前**的读数，本册按 `rule-0d` 以取代注记形式**逐字**留档（见 §三）。
> ⚠️ **行号口径**：本条一律以**内容锚点**为准（多泳道共享同一 `main` 工作区；⛔ 不引旧行号）。
> ⚠️ **本条目用列表而非表格**（同 `v135 → v136`／`v134 → v135` 条目的处置与理由）。

### 一、改了什么（逐处：内容锚点 ＋ 改前 → 改后）

- **1｜`src/zre_c/include/zre_actions.h`** —— `int zre_actions_is_uplink` 上方 doc 注释（内容锚点 `上行（设备可主动发送）判定`）：在 `sleep_report_trigger` 之后、`stream_stop` 之前**补 `sleep_status_trigger`** ⇒ 注释名数 **9 → 10**，与 `src/zre_c/src/zre_actions.c` 的 `k_uplink[]`（**10** 项）**双向集合相等**（`A2` 为**集合**比对 ⇒ 顺序不必同）。
- **2｜`design/04-platform-api/data/iot/smart-bed.js`** —— `sharedEnums.action_name` **`error` 取值项**紧邻上方的三行 `//` 注释（内容锚点 `权威定义见` ＋ `errorFallback`）：把 `R-032` 落地**前**的陈旧读数改写为**已含本值**（⛔ 该 span 内**不得**再出现「尚未含」类字面、⛔ **不得**用「`C 库…（N 值）`」括号计数形态 —— 两者都会被 `A3` 判红）。
- **3｜`publish/api/data/iot/smart-bed.js`** ／ **`publish/api/CHANGELOG.md`** —— 与 `1`／`2` 同批镜像（逐件 `Copy-Item -LiteralPath`，**双侧 sha256 相等**自证；⛔ **未跑** `sync_design_to_publish.ps1 -Apply`）。⚠️ `src/zre_c/include/zre_actions.h` **无镜像**（`publish/zre_c` 不存在）。

### 二、复算口径（四；⛔ 换口径即换读数）

- **`A2` 双向相等**：抠 `zre_actions.h` 的 `int zre_actions_is_uplink(` **紧邻其上的 `/** … */`** 区间（`rfind("/**")`），切 `\b[a-z][a-z0-9_]*\b` ⇒ 得注释名集；与 `zre_actions.c` 的 `k_uplink[]` 字符串集取**双向差集**，两侧均须为 **∅**（实测：改前 `k_uplink ∖ 注释 = {sleep_status_trigger}`，改后 **∅**）。
- **`A3` 读数一致**：取 `design/**/data/iot/*.js` 里 `sharedEnums.action_name` 逐取值项，**span 起点 ＝ 上一个元素结束之后（含前置注释）**；span 内见 `C 库…（N 值）` ⇒ 须 `N == len(k_names)`；见「尚未含」⇒ 该值须**不在** `k_names`；见「已含」⇒ 须**在**；都不见 ⇒ 不出声。
- **C 端 ＝ 55**：`zre_actions.h` 的 `ZRE_ACTION_COUNT = 55` ∧ `zre_actions.c` 的 `k_names[]` 恰 **55** 项 ∧ 枚举成员恰 **55** 个（`ZRE_ACTION_COUNT` 哨兵不计入成员）⇒ `A1` 三向相等。
- **门禁实跑**：`.venv\Scripts\python.exe scripts/check_action_registry_parity.py` ⇒ 改前 `rc=1`（两条真红：`A2` 漏列 `sleep_status_trigger` · `A3` 陈旧计数 `54 ≠ 55`）／改后 **`rc=0`**（读数行逐字见执行报告）。

### 三、`rule-0d` 取代注记（被改各处**原文逐字**）

**`src/zre_c/include/zre_actions.h`（改点 1）被改两行原文逐字：**

```c
/** 上行（设备可主动发送）判定：connect/status_notification/text_command/voice_command/
 *  stream_voice_command/firmware_update_progress/set_user_profile/sleep_report_trigger/stream_stop。 */
```

**`data/iot/smart-bed.js`（改点 2）被改行原文逐字（`R-032` 落地前的读数）：**

```text
        //    权威定义见 `protocol.js` 的 `errorFallback`。⚠️ 设备端 C 库 action 注册表（54 值）尚未含本值 ⇒ 归 `R-032`。
```

⇒ 该句**逐字史实**自此以本条取代注记形式留档（⛔ 协议数据面不再保留该读数：`A3` 的受检面含 `design/**/data/iot/*.js`，而本册 `design/**/CHANGELOG.md` 是**不入面**之一 ⇒ 逐字原文**只能**落在本册）。

### 四、本件**未落地**／**未判定**的面（⛔ 不冒充已做）

- ⚠️ **`design/04-platform-api/CHANGELOG.md` 的既有历史条目里仍有同类陈旧语句**（例：`T-566` 段与 `v132 → v133` 段的「设备端 C 库注册表尚未含本值」）—— 那是**当刻读数**，按 `rule-0d` **⛔ 不改**（本册是不入面 ⇒ 不判红）；本条 ⛔ **不代改**它们。
- ⚠️ **`docs/**` 与 `tests/README.md` 的同类引注** 同属 `A3` 的**不入面**，本件未碰。
- ⛔ 本件**未**改任何 `ZRE_ACTION_*` 枚举值／顺序、⛔ **未**改 `k_names[]`／`k_uplink[]` 内容、⛔ **未**改任何判据阈值／棘轮。

### 五、写面（逐件显式）

`src/zre_c/include/zre_actions.h`（**无镜像**：`publish/zre_c` 不存在）· `design/04-platform-api/data/iot/smart-bed.js` ＋ `publish/api/data/iot/smart-bed.js` · `design/04-platform-api/CHANGELOG.md` ＋ `publish/api/CHANGELOG.md` · 门禁件（`scripts/check_action_registry_parity.py` ＋ 其薄包装 `tests/design-regression/test_action_registry_parity_gate.py`）· 报告件（`Q-031`／`Q-027` 设计暂存区的执行报告；⛔ 不入 `design/`）。

### 六、自证

- **双侧 sha256**：`design`／`publish` 的 `smart-bed.js` 与 `CHANGELOG.md` 各一对，改后**同对相等**（前/后四值见执行报告）。
- **落盘口径**：改动前后 **`CR=0`／无 BOM**（机械复算）。
- **只读自证**：门禁跑动不改动受检件（薄包装 `test_gate_is_read_only_on_the_real_tree` 的 `READONLY_FILES` 面）。

## v135 → v136：`D-210`（`P-077`／`X-21`）—— 🔴 **破坏性单位变更**：`batch_action.steps[].at` 由「毫秒 ＋ 字符串」改为「**秒 ＋ 数值**」（2026-10-12）

> **来源／依据**：需求方 `D-210` 逐字裁定 **B —— 统一为「相对偏移」，且单位 ＝ 秒**（`.pipeline/decisions/DECISIONS-BODY.md` 的 `## D-210` 段）＋ 其「顺带必改两处」与「破坏性提示」两句；`P-077`（`R-045` 前置 ① ／ `X-21`）· 第 3 层取证稿两份（`Q-027` 的「增量发现」件 `X-21` 行 ／ `Q-031` 的 `pending-briefs` `§29.2`；⚠️ 均为 `draft` ⇒ 按 J7a 本处**只引稿名、⛔ 不写工作区路径**）。
> 🔴 **本条自陈「破坏性单位变更」**（`D-210` 明文要求「须在 CHANGELOG 明写」）—— 语义比 ＝ **1000×**（旧毫秒 ⇒ 新秒；旧 `3000` ＝ 3 秒，新 `3000` ＝ 3000 秒）。
> ⚠️ **行号口径**：本条一律以**内容锚点**为准（多泳道共享同一 `main` 工作区；⛔ 不引旧行号）；下文行号是**落笔当时**的读数，只作定位参考。
> ⚠️ **本条目用列表而非表格**（同 `v134 → v135`／`v133 → v134` 条目的处置与理由）。

### 一、改了什么（逐处：内容锚点 ＋ 改前 → 改后）

- **1｜`data/iot/smart-bed.js`** —— `batch_action` 卡（内容锚点 `{ id: "batch_action"`）的 `steps.fields.at` 字段行（落笔时 `:892`）：`type` **`"string"` → `"int"`** 且 `desc` 删去「'HH:MM' 绝对时间」那半句、`offset_ms` 秒字样订正为**秒数值**（改前改后逐字见 §三）。
- **2｜同上** —— **同卡 `notes`**（内容锚点 `at 为 'HH:MM'`）：同样删去绝对时间半句、`毫秒偏移` → **延迟秒数**。
- **3｜`data/iot/smart-bed-examples.js`** —— `batch_action` 示例块（内容锚点 `"id": "batch_action"`）的 `call.steps[]` 两条 `at` 示例值：`"00:00"`（字符串绝对时间）→ **`0`**；`"3000"`（字符串毫秒）→ **`3`**（＝ 3000 ms ÷ 1000）。
- **4｜`src/server/events/base.py`** —— `BatchActionCommand` 的 `steps` 行内注释（内容锚点 `steps: list | None = None`）：`[{at:"HH:MM"|offset_ms, …}]` → `[{at:<相对当前时刻的延迟秒数, int>, …}]`（⚠️ **只改注释，运行时零影响**；`to_proto` 对该字段是 **verbatim 透传**、本仓无换算点）。
- **5｜`publish/api/data/iot/smart-bed.js`** ／ **`publish/api/data/iot/smart-bed-examples.js`** —— 与 `1`~`3` 同批镜像（逐件 `Copy-Item -LiteralPath`，**双侧 sha256 相等**自证；⛔ **未跑** `sync_design_to_publish.ps1`）。
- **6｜`publish/api/CHANGELOG.md`** —— 与本条同批镜像（单文件映射，双侧 sha256 相等自证）。

### 二、复算口径（可照跑；⛔ 换口径即换读数）

- **整仓毫秒残留 ＝ 0**：`grep -rn 'offset_ms' -- design src publish`（⚠️ `docs/requirements-backlog.md` 的 `R-036` 行**另有 1 处**，那是**历史取证记录的原文**、⛔ 非本件写面 ⇒ **不改**，见 §三）。
- **协议面毫秒字样 ＝ 0**：对 `design/04-platform-api/data/iot/smart-bed.js` 搜 `毫秒`，命中应只剩 `duration_ms` 一处（**另一字段**，⛔ 与本件无关）。
- **`type` 取值依据（仓内先例，机械可复算）**：`smart-bed.js` 现存数值型 token 实测 ＝ `int` **32** 处 · `uint64` **8** 处 · `float` **1** 处 · `uint32` **1** 处（⛔ 全仓 `type: "number"` **0 命中**）⇒ 本件取 **`int`**；同卡邻域先例：「持续时长（**秒**）」逐字即 `type: "int"`。

### 三、`rule-0d` 取代注记（被改各处**原文逐字**）

**`data/iot/smart-bed.js` · `at` 字段行（改前 `:892`）：**

```text
          at: { type: "string", required: true, desc: "执行延迟：'HH:MM' 绝对时间 或 offset_ms 相对毫秒偏移" },
```

**同文件 · 同卡 `notes`（改前 `:899`）：**

```text
      notes: "at 为 'HH:MM' 绝对时间或 offset_ms（相对当前时刻毫秒偏移）。batch 一旦开始执行，内部各 step 按各自延迟顺序执行；batch_action_stop 可停止整个 batch。" },
```

**`data/iot/smart-bed-examples.js` · 示例两条 `at`（改前 `:895`~`896`）：**

```js
        { "at": "00:00", "action": { "sub_device_id": "head_motor_left", "mode": "absolute", "value": 30 } },
        { "at": "3000", "action": { "sub_device_id": "massage_module", "intensity": "medium", "mode": "wave" } }
```

**`src/server/events/base.py` · `steps` 注释（改前 `:1518`）：**

```python
    steps: list | None = None        # [{at:"HH:MM"|offset_ms, action:<原始 payload>}]
```

### 四、🔴 破坏性单位变更 ＋ 设备侧通知（`D-210` 明文要求的两句）

- **语义比 ＝ 1000×**：旧口径 `at` 是**毫秒**（可字符串、可为 `'HH:MM'` 绝对时间），新口径是**秒**（整数相对偏移）⇒ 同一数值 `3` 旧读 3 ms、新读 3 s；旧值 `3000` 旧读 3 s、新读 3000 s。
- **为什么必须通知：本仓无真正消费方，⛔ 不会自己报错、⛔ 不会优雅降级** —— 云侧 `to_proto` 对该字段 **verbatim 透传**（`src/server/events/identifier.py` 的 `PROTO_FIELD_ALIASES` **空表**），`src/zre_c/**` 只登记 action 名、⛔ 不解释 `steps[].at` ⇒ **口径不一致时是静默的 1000× 偏差**，⛔ 只有设备侧能发现。
- **通知对象**：设备端（`R-032` 面，硬件/固件 ＋ C 库/端侧脚本团队）。
- **要设备侧回什么（可验收）**：① 是否已按**毫秒**实现 `batch_action` 延迟；② 若已实现 ⇒ 切到**秒**的时点；③ 若已按秒实现 ⇒ 只需回执，⛔ 无返工。
- **时点**：随本条发布（本条随册部署，见 `publish/api/CHANGELOG.md` 镜像）＋ 在设备侧下次取件前。

### 五、受影响方与通知落点（如实登记：仓内**无**既有「设备侧通知」渠道）

- ⚠️ **实测**：全仓搜「通知设备侧」**仅 1 处**，即 `D-210` 自身那段；**⛔ 无**任何通用「设备侧通知」台账／渠道机制（`grep` 对「通知设备侧」／「设备侧 ＋ 通知」跨 `design/**`·`docs/**`·`.pipeline/**` 实测）。
- **本条落点（＝ 通知载体本身）**：`publish/api/CHANGELOG.md` 是**已部署的静态页**（`publish/api/index.html` 内有 `href="./CHANGELOG.md"`）⇒ **本条即面向设备侧的书面通知**，落笔后随册发布可达。
- **建议补强（⛔ 不在本件写面，给出即可）**：
  - ① **取件前置**：把「设备侧核对 `at` 单位（毫秒 → 秒）」并入 `R-032` 取件前置（与 `D-211` 的「设备端核对抖动口径」**同址**，⛔ 均**不涉真机**）；
  - ② **账本留痕**：若需账本侧可见 ⇒ 建议在 `.pipeline/PENDING-DECISIONS.md` 的「🟡 仅需你知晓」区落一条 `N-`（⛔ 本件**未**自行加行）。
- 🔴 **张力如实登记**：`Q-031` 的第 3 层取证稿（「`D-213` 落地」件，⚠️ `draft` ⇒ 按 J7a 只引稿名）的 `§9 U3` 已具名「`D-210` 的『通知设备侧』执行形态（载体／时点／是否落 `N-` 行）**未有裁定**」⇒ 本件**只做可做的那一半**（协议订正 ＋ CHANGELOG 明文 ＋ 双侧镜像），㊟ 上述 ①② 属**建议**、⛔ 不是已裁定的机制。

### 六、自证

- **镜像口径**：`design/04-platform-api/data/iot/smart-bed.js` ↔ `publish/api/data/iot/smart-bed.js` · `smart-bed-examples.js` ↔ 同名镜像 · `design/04-platform-api/CHANGELOG.md` ↔ `publish/api/CHANGELOG.md` —— **逐对 sha256 相等**（改前改后两对读数见执行报告）；⛔ **未跑** `sync_design_to_publish.ps1`（含 `-Check`）。
- **落盘口径**：改动件 **CR=0／无 BOM**（逐字节机械复算）。
- ⛔ **未改任何判据／阈值／棘轮**；⛔ **未删任何断言**；本批改动**全部是散文字符串与注释**（⛔ 无常量赋值行、⛔ 无函数体行）。
- ⛔ **本件未碰** `src/server/events/identifier.py`（verbatim 透传逻辑）／`src/zre_c/**`（C 库只登记 action 名）／`.pipeline` 账本各册。

## v134 → v135：`R-035`（阶段 I 第 6 件 · 协议文档维护级修正）—— 事件映射页四处陈旧计数订正 ＋ C 库头注释过时值订正（2026-10-12）

> **来源／依据**：清单① `R-035` 行逐字（`N1` downlink `41→43` · `N2` `ackOnly` `52→53`（并订正 `heartbeat_ack` 说法）· `N3` `DomainMessage` `19→20` · `C-⑫` C 库头注释「协议 v4／56 个 action」）· 设计权威 `Q-027` 的 `fix-design.md` §2 第 7 批 ＋ §3 复算命令 · `D-235`（须同改「协议 51 消息」）· `D-236`（订正 `heartbeat_ack` 说法）· `G-478`（`C-⑫` 归 `R-035`）· 取证件 `evidence-r035-prep.md`（四项现刻读数）· `evidence-u2-handover-verify.md`（写锁交接）。
> 🔴 **上游四个目标值里有两个已过期**（本件一律以**实测**为准，逐条见 §一）：`N2` 的上游区间「`52→53`」在两面**都不存在**（改前 **55／55** ⇒ 删多余键后 **54／54**）；`C-⑫` 的上游目标「`v3.2／52`」已过期两次（现 `version` ＝ **`3.6`**、`action_name` ＝ **`55`**）。
> ⚠️ **行号口径**：本条一律以**内容锚点**为准（多泳道共享同一 `main` 工作区；⛔ 不引旧行号）。
> ⚠️ **本条目用列表而非表格**（同 `v133 → v134`／`v132 → v133` 条目的处置与理由）。

### 一、改了什么（逐处：内容锚点 ＋ 改前 → 改后）

- **1｜`data/iot/protocol.js`** —— 渲染页「事件映射表（EventIdentifier）」的**层 1 直接映射**条目（内容锚点 `层 1 直接映射（direct）`）：`downlink` **41 → 43** 条；其分解 **`38 PlatformCommand + 3 PlatformMessage` → `41 PlatformCommand + 2 PlatformMessage`**。
- **2｜同上** —— **同页覆盖句**（内容锚点 `覆盖：协议`）：**「协议 51 消息」→「协议 53 消息」**（`D-235`；与上条**算术同源**：`43 ＋ 9 ＋ 1 ＝ 53`，⛔ 不是第二个独立改动）。
- **3｜同上** —— **同句的 ack 计数**（内容锚点 `设备上行 ack`）：**`52 条` → `54 条`**（＝ `data/iot/event-identifier.js` 的 `ackOnly` 数组在**删掉多余键 `heartbeat_ack`**后的长度，亦即 `event-identifier-check.mjs` 出声行 `ackOnly 清单: N 条` 的读数；🔴 **改前该数组为 55**，见 §四 的**跨泳道留痕**）。
- **4｜同上** —— **同句末的 `DomainMessage` 计数**（内容锚点 `DomainMessage`）：**`19 种` → `20 种`**（＝ 设计权威 `design/05-backend-architecture/data/event-contracts.js` 的 `DomainMessage.*` 键数；亦 ＝ `event-identifier.js` 的 `domainInternal` 内 `DomainMessage.*` 项数，两者**同集**）。
- **5｜`publish/api/data/iot/protocol.js`** —— 与 `1`~`4` 同批镜像（单件 `Copy-Item -LiteralPath`，**双侧 sha256 相等**自证；⛔ **未跑** `sync_design_to_publish.ps1 -Apply`）。
- **6｜`src/zre_c/include/zre_payload.h`** —— 文件头「权威源」行（内容锚点 `权威源：design/04-platform-api/data/iot/smart-bed.js`）：**「协议 v4，T-030 后 56 个 action」→「协议 v3.6，T-030 后 55 个 action」**。
- **7｜`src/zre_c/include/zre_actions.h`** —— 文件头行（内容锚点 `ZYRIC action 注册表`）＋ 枚举块首注释（内容锚点 `公共枚举 action_name`）：两处 **`52 值` → `55 值`**（该文件正文实测 `ZRE_ACTION_COUNT = 55` 且 `k_names[]` 恰 **55** 项）；后一处同批补上此前缺失的增量出处（`R-026` ＋2 · `R-032` ＋1），⛔ **只订正注释、枚举值与顺序一字未动**。
- **8｜`publish/api/CHANGELOG.md`** —— 与本条同批镜像（单文件映射，双侧 sha256 相等自证）。

### 二、复算口径（四 ＋ 二；⛔ 换口径即换读数）

- **`downlink ＝ 43`**：对 `data/iot/smart-bed.js` 抠 `direction: "cloud-to-device"` 卡（实测 **44**，含 `heartbeat` 纯 ack 卡）⇒ 扣除 `response.action` **逐字等于自身 `id`** 者（恰 `heartbeat` 1 张）⇒ **43**。分解经 `event-identifier.js` 的 `downlink` 块逐键对表：`PlatformCommand.*` ＝ **41**、`PlatformMessage.*` ＝ **2**（该块共 45 键 ＝ 43 ＋ `heartbeat`（`packetOnly`）＋ `stream_stop`（协议侧 `bidirectional`））。
- **`协议消息 ＝ 53`**：`43（下行映射） ＋ 9（上行） ＋ 1（双向 `stream_stop`）`。⚠️ 另有一档「可枚举全部协议 action 卡」＝ **54**（含 `heartbeat`）—— ⛔ **不得**写进本句（会与同句的 `downlink 43` 自相矛盾）。
- **`ackOnly ＝ 54`**：数 `data/iot/event-identifier.js` 的 `ackOnly: [` 数组内引号串；同法数 `src/server/events/identifier_rules.py` 的 `ACK_ONLY`（两面**逐项同序**，实测各 **54**）。🔴 **改前为 55／55** —— 其中 `heartbeat_ack` 是**协议里不存在的多余键**（协议帧名集零命中；`heartbeat` 的 ack 名即 `heartbeat`，见 `D-236`）⇒ 由**同批另一泳道**按 `R-035` 的 `U-1` 面**已删**（落点 ＝ 上述两面，⛔ 不在本件写面）⇒ 本件据此把协议页的数订正为 **54**（见 §四 跨泳道留痕）。
- **`DomainMessage ＝ 20`**：行首形态 `"DomainMessage.<Name>": {` 数 `event-contracts.js`（＝ **20**）；交叉核 `domainInternal` 内 `DomainMessage.*` 项数（＝ **20**）。
- **`P_all ＝ 55 ＝ P_biz 54 ＋ P_builtin 1`**：⚠️ ⛔ **禁用「从 `action_name:` 后整段数到 `sharedEnums` 末尾」的朴素法**（会把 `position` 等公共枚举一并吃进 ⇒ 读数虚高）；须**花括号配平**取 `action_name` 块 —— 块内 `{ value: "` 行数 ＝ **55**；其中同行含 `endpoint_id:` 者 ＝ **54**；差 ＝ 内置 `error` **1**。
- **`C 端 ＝ 55`**：`zre_actions.h` 的 `ZRE_ACTION_COUNT = 55` ∧ `zre_actions.c` 的 `k_names[]` 恰 **55** 项（**长度一致是隐式约束**）。⚠️ `zre_payload.h` 取 **55 而非 54**：该行自陈「权威源 ＝ 协议 `action_name` 公共枚举」⇒ 取**协议面**的 `P_all`（含内置 `error`）；业务面 **54** 是同批的另一个量（TS 端 ＝ C 端 ＝ 55）。

### 三、`rule-0d` 取代注记（被改各处**原文逐字**）

**`data/iot/protocol.js`（改点 1~4）被改三行原文逐字：**

```text
        + "<li><b>层 1 直接映射（direct）</b>：协议消息 ↔ 事件一对一 —— downlink 41 条（38 PlatformCommand + 3 PlatformMessage，T-032：删 sleep_minutes/report_data/trend push 3 条、新增 sleep_report_image_push）、uplink 9 条（5 DeviceCommand + 4 DeviceMessage，T-032：删 sleep_minutes/report_data/trend trigger 3 条）、bidirectional 1 条（stream_stop）</li>"
        + "<p>覆盖：协议 51 消息 + 37 设备类型 ↔ 事件体系 95+ 类型全部闭合；设备上行 ack 52 条不产生事件（连接层回执）；"
        + "DomainMessage 19 种为领域内部事件（无协议映射）。"
```

**`src/zre_c/include/zre_payload.h`（改点 6）被改行原文逐字：**

```c
 * 权威源：design/04-platform-api/data/iot/smart-bed.js（协议 v4，T-030 后 56 个 action）。
```

**`src/zre_c/include/zre_actions.h`（改点 7）被改两行原文逐字：**

```c
 * zre_actions.h — ZYRIC action 注册表（对齐 smart-bed.js action_name 公共枚举，T-030/T-032/T-034 后 52 值）
/** 公共枚举 action_name 52 值（T-030：改名/删除/新增环境动作；T-032：删睡眠触发/推送 6 值、新增 sleep_report_image_push；T-034：新增 schedule_action_remove；顺序同 smart-bed.js 公共枚举 action_name）。 */
```

### 四、本件**未落地**／**未判定**的面（⛔ 不冒充已做）

- 🔴 **`N2` 的第二半「删多余键 `heartbeat_ack`」＝ 由同批另一泳道落地**（⛔ 不在本件写面：落点 ＝ `data/iot/event-identifier.js` 的 `ackOnly` 数组首行 与 `src/server/events/identifier_rules.py` 的 `ACK_ONLY` 集首行；本件明令禁碰 `src/server/**`）⇒ **本件只订正协议页的计数**（由改前 **55** ⇒ 删后 **54**），并在此**留痕跨泳道读数的时序**：
  - **本件实测（`01:2x`）**：两面各 **55**（含 `heartbeat_ack`）；
  - **同批另一泳道（文件 mtime `01:44:23`，`index`／工作区）**：两面各 **54**，且其**就地留痕逐字**为「⛔ `heartbeat_ack` 已于 `R-035`（`U-1` 面）**删除** … 删后本清单 55 → 54。勿回加。」
  - ⇒ ⚠️ **本笔提交的树里，`event-identifier.js` 仍是改前版（55）**（该泳道的改动**已 staged 但尚未提交**）⇒ **同一个数的两个读者会差一拍**：以**工作区／`index`** 读 ⇒ **54**（本件取此值，＝ 删后终态）；以**本笔提交的树**读 ⇒ 55。⇒ 🔴 **本件取 54**（理由：① 该删除已 staged、落地在即；② 上游 `N2` 的语义就是"删多余键后"的计数；③ ⛔ 写 55 会在下一笔提交后**立刻**变成陈旧数——正是本件要修的病）。
- ⚠️ **`protocol.js` 的 `version` 字段（`3.6`）与 `changeLog` 顶端（`v3.7`）的既有漂移**（由 `R-028` 引入：它置顶 `v3.7` 段却未同批升 `version` 字段）—— 本件取 **`version` 字段**（＝ 取证件候选甲），⛔ **未改 `version`、⛔ 未加 `changeLog` 段**。
- ⚠️ **同页「37 设备类型」未判**：`device_type` 公共枚举实测 **38** 值，而该句写 37（可能指「映射覆盖的设备类型数」＝ `subDeviceEvents` 16 ＋ `deviceTypeDerived` 21 ＝ 37）⇒ 语义**未判定**，本件**不改**、具名登记。
- ⛔ 本件**未**扩充到 `N1`／`N2`／`N3`／`C-⑫` 四项之外的任何数字。

### 五、写面（逐件显式）

`design/04-platform-api/data/iot/protocol.js` ＋ `publish/api/data/iot/protocol.js` · `src/zre_c/include/zre_payload.h` · `src/zre_c/include/zre_actions.h`（**无镜像**：`publish/zre_c` 不存在）· `design/04-platform-api/CHANGELOG.md` ＋ `publish/api/CHANGELOG.md` · 报告件（`Q-031` 设计暂存区的 `R-035` 执行报告；⛔ 不入 `design/`）。

### 六、自证

- ⛔ **未改任何判据／阈值／棘轮**；⛔ **未删任何断言**；本批改动**全部是散文字符串与注释**（`git diff -U0` 实测：新增/删除的常量赋值行 ＝ **0**、函数体行 ＝ **0**）。
- **落盘口径**：改动前后 **`CR=0`／无 BOM**（机械复算，见报告 §3）。
- **C 面**：`src/zre_c/include/**.h` 改动后**重编译**（`scripts/build_zre_c.ps1`，含 C 自测）＋ 逐位比对 `zre_actions_name(i)` 读数（`i = 0..54`）**与改前逐项相同**（见报告 §5）。

## v133 → v134：`U-2`（取向 `A′`「**真丢弃**」）—— `set_user_profile.payload` 真删 `preferences`／`contact`／`emergency_config` 三对象（8 子字段）＋ 判据「实际命中数」改口径 ＋ 豁免台账同批销账（2026-10-11）

> **来源／依据**：需求方 `U-2` 裁定（取向 `A′` ＝ 把三对象**从协议 payload 真删**）· `D-278`（已裁「三对象**可丢弃**」）· 设计暂存区 `PENDING-RULINGS.md` 第 **6** 项逐字「✅ **改成「实际命中数」**」· `T20`／`R-038` 判据 `judge_f1()` 的报红文案**自称**比较的是「登记 N 条，**实际命中** M 条」（改前实现算的是「协议差集条数」⇒ 实现与自己的文案矛盾）· 前一泳道 `7f1f742d` 的四档实测（销豁免而**不**删协议键 ⇒ `[F1 RED] 上行字段无处接` 逐字 **3** 条）。
> ⚠️ **版本号口径（机械实测，改前取数）**：本文件头部改前**实际最大条目号 ＝ `v133`**（`R-033` 占用，标题逐字 `## v132 → v133：…`）⇒ **本件落 `v134`**（编排器 2026-10-11 裁定：用 `v134` 避免与 `R-035` 争号）。
> ⚠️ **行号口径**：本条一律以**内容锚点**为准（本仓多泳道在写；⛔ 不引旧行号）。
> 📌 **本件自证**：⛔ 未改任何门禁的**断言算子**（机械扫：改动行含 `!=`／`==` 者 **0**）；⛔ 未抬任何**阈值型**棘轮（**唯一**的阈值型常量改动 ＝ `UP_PROTO_EXEMPT_FIELD_COUNT` `8 → 5`，**销账的必然**）；⛔ 未删任何既有断言；**历史留痕面一字未改、只追加**。
> ⚠️ **接手泳道补正（2026-10-11）**：上面那行**原句逐字**为「⛔ 未抬任何棘轮（**唯一**常量改动 ＝ `UP_PROTO_EXEMPT_FIELD_COUNT` `8 → 5`，**销账的必然**）」—— 该括注**与本条改点 8 自相矛盾**。**机械实测（机械扫 `git diff -U0` 的 `^[+-].*= ` 行）**：本次**常量赋值行改动共 4 行**，即 `UP_PROTO_EXEMPT_FIELD_COUNT` `8 → 5`／`SELF_TEST_CASES` `12 → 13`／`SELF_TEST_CASES_EXPECTED` `12 → 13`／`tests/README.md` 末格 `**12** → **13**` ⇒ 「**唯一**常量改动」**不成立**；且 `SELF_TEST_CASES` **本身就是**「用例数棘轮（只增不减）」⇒ 该轮**确有**棘轮同批 +1。⇒ 本行已就地改述为「未抬任何**阈值型**棘轮」＋「**唯一**的**阈值型**常量改动」，**原句保留于本注**（`rule-0d`）。⚠️ 该 +1 是**新增口径判别器**的必然（见改点 8），⛔ **不是**放宽判据。
> ⚠️ **本条目用列表而非表格**（同 `v132 → v133` 条目的处置与理由）。

### 一、改了什么（逐处：内容锚点 ＋ 改前 → 改后）

- **1｜`data/iot/smart-bed.js`** —— `set_user_profile` 卡（内容锚点 `id: "set_user_profile"` ＋ `direction: "device-to-cloud"`）的 `envelope.payload`。改前 ＝ **4** 个一级键（`profile`／`preferences`／`contact`／`emergency_config`）；改后 ＝ **1** 个（`profile`）。即 `preferences`（2 子字段）／`contact`（4 子字段）／`emergency_config`（2 子字段）**三对象共 8 子字段整键删除**；`profile` **逐字保留**（它有契约落点 `profile_json` ＋ 实现槽位 `profile_json: dict | None = None`）。
- **2｜`data/iot/smart-bed-examples.js`** —— `set_user_profile` 上行示例的 `call` 块（内容锚点 `"action": "set_user_profile"` 之后）。改前含 `profile`／`preferences`／`contact`／`emergency_config` 四键；改后只剩 `profile`（⚠️ 同批删去 `profile` 块后的**尾随逗号** ⇒ 仍是合法 JS 字面量）。
- **3｜`data/rest/examples.js`** —— 同一上行示例在本册的副本（内容锚点 `"action": "set_user_profile"`），改法同 `2`。
- **4｜`publish/api/data/iot/smart-bed.js` ・ `publish/api/data/iot/smart-bed-examples.js` ・ `publish/api/data/rest/examples.js`** —— 与 `1`／`2`／`3` 同一批内容锚点；改后 ＝ **镜像同步**（单件 `Copy-Item -LiteralPath`，**双侧 sha256 相等**自证；⛔ **未跑** `sync_design_to_publish.ps1 -Apply`）。
- **5｜`scripts/check_event_contract_protocol_parity.py`** —— 判据 `judge_f1()` 的 `live_fields` 累加行。改前 ＝ `live_fields += len(proto - cfields)`（＝ **协议差集条数**）；改后 ＝ `live_fields += len((proto - cfields) & exempt)`（＝ **实际命中数**）。⚠️ **断言算子 `!=` 与 `proto - cfields - exempt` 命门臂一字未动**（「销了键、协议上却仍有引用」仍由后者逐字段判红）。
- **6｜同上** —— `UP_PROTO_EXEMPT["set_user_profile"]`。改前 ＝ **4** 条豁免（`profile`／`preferences`／`contact`／`emergency_config`）；改后 ＝ **1** 条（`profile`）。
- **7｜同上** —— `UP_PROTO_EXEMPT_FIELD_COUNT`。改前 ＝ `8`；改后 ＝ `5`（＝ `firmware_update_progress` 3 ＋ `set_user_profile` 1 ＋ `status_notification` 1）。
- **8｜同上 ＋ `tests/design-regression/test_event_contract_protocol_parity_gate.py` ＋ `tests/README.md`** —— `SELF_TEST_CASES` `12 → 13` ／ `SELF_TEST_CASES_EXPECTED` `12 → 13` ／ 登记表「自检条数」末格 `12 → 13`（新增**口径判别器**用例 ⑬：台账登记 **2** 键、协议差集只命中 **1** 键 ⇒ 必须报「登记 2 条，实际命中 1 条」；⚠️ 该用例在**旧口径**下**必失败**，是本语义改动**唯一的钉子**）。
- **9｜`publish/api/CHANGELOG.md`** —— 与本条同批镜像（本册 ↔ `publish/api/CHANGELOG.md` 是**单文件映射**，双侧 sha256 相等自证）。

### 二、`rule-0d` 取代注记（被删各处**原文逐字**）

**a｜`data/iot/smart-bed.js` 的 `set_user_profile.payload`（改点 1）被删三行原文逐字：**

```js
        preferences: { type: "object", required: false, desc: "偏好设置", fields: { language: { type: "enum", ref: "locale", desc: "用户偏好的语言区域（BCP 47）。引用 locale 公共枚举" }, tts_voice: { type: "string", desc: "语音合成音色标识（如 zh-CN-Xiaoxiao、en-US-Jenny）。选项列表与用户 language 偏好关联，由选择器动态提供" } } },
        contact: { type: "object", required: false, desc: "联系方式", fields: { owner_phone: { type: "string", desc: "设备主人手机号" }, owner_email: { type: "string", desc: "设备主人邮箱" }, emergency_contact_name: { type: "string", desc: "紧急联系人姓名" }, emergency_contact_phone: { type: "string", desc: "紧急联系人手机号" } } },
        emergency_config: { type: "object", required: false, desc: "紧急配置", fields: { notification_template: { type: "string", desc: "触发紧急通信时的消息模板（用于表达请求身份）" }, auto_notify: { type: "bool", default: true, desc: "监测到异常时自动通知紧急联系人" } } }
```

（⚠️ 同批去掉 `profile` 行末尾的**一个逗号**；`profile` 行自身**逐字未改**。）

**b｜`data/iot/smart-bed-examples.js`（改点 2）与 `data/rest/examples.js`（改点 3）被删三块原文逐字：**

```json
      "preferences": {
        "language": "zh-CN",
        "tts_voice": "zh-CN-Xiaoxiao"
      },
      "contact": {
        "owner_phone": "13800000000",
        "owner_email": "zhangsan@example.com",
        "emergency_contact_name": "李四",
        "emergency_contact_phone": "13900000000"
      },
      "emergency_config": {
        "notification_template": "您的家人可能需要您的帮助，请及时确认。",
        "auto_notify": false
      }
```

（⚠️ `data/rest/examples.js` 的同三块**逐字为 `"SM-001"` 占位值版**，此处⛔ 不复述第二遍以免二次留痕分歧；锚点 ＝ 该册 `"action": "set_user_profile"` 之后的 `"call"` 块。⚠️ 两册同批各删去 `profile` 块后**一个尾随逗号**。）

**c｜判据豁免台账（改点 6）被删三条原文逐字：**

```python
        "preferences": {
            "reason": "🔴 **已在册的缺口**（`G2` 逐字：「`preferences`/`contact`/`emergency_config` "
                      "**3 个对象（8 子字段）在契约与实现里完全无表示**」）",
            "src": "同上（`G2`）",
        },
        "contact": {"reason": "同上（`G2`）", "src": "同上（`G2`）"},
        "emergency_config": {"reason": "同上（`G2`）", "src": "同上（`G2`）"},
```

**d｜判据 docstring 的「判据落地日」实测表** —— `set_user_profile` 行**原文一字未改**，就地**追加**取代注记（该行实测差集由 4 键缩为 `profile` 1 键）。

⛔ 被删三键**不是**「判定为无问题」：它们在册的理由是 `G2` 的「**契约与实现里完全无表示**」缺口，本件按 `D-278` 裁决**弃该通道**（协议不再声明该 payload ⇒ 缺口消失；⛔ 不是「把缺口改判为无问题」）。⛔ 与 `R-034`／`B6`（`firmware_update_progress` 的 `result`／`url`／`rollback_result`）**无关** —— 那 3 键与 `profile` **逐字未动**。

### 三、⛔ 本件未改（防「顺手改宽」，具名登记）

- **`data/iot/protocol.js`**（＋ `publish/api/data/iot/protocol.js`）—— 该件**不含**这 3 键、也**不含** `set_user_profile`；本判据的取数模块把协议真身逐字钉为 `smart-bed.js` ⇒ **改它不会让本判据变绿**；该面**归 `R-035`** ⇒ **本件未改**（编排器 2026-10-11 裁定 1）。
- **模拟器派生视图面**（编排器 2026-10-11 裁定 3：不碰、具名登记、另立一件）：`src/web-admin/src/views/device-simulator/components/ProtocolLogPanel.vue`（4 个硬编码 text 字段）· `src/web-admin/src/views/device-simulator-v2/components/ProtocolFeed.vue` · `src/web-admin/public/locales/zh.json` 与 `en.json`（`set_user_profile` 段）· `src/web-admin/src/i18n/localeMessages.spec.ts`（断言这 4 个 JSON 占位符渲染）。⚠️ 本件删协议键后该四处**变陈旧**（模拟器仍渲染三键输入）—— 需**另立一件**对齐。
- **史实面**（`rule-0d`：原文**逐字保留**，⛔ 不删史实；编排器裁定 4）：`design/01-raw-input/设备操作指令需求.md`（原始需求原文含 `preferences`／`contact`／`emergency_config`）· `src/web-admin/CHANGELOG.md`（历史条目）。


## v132 → v133：`R-033`（阶段 I 第 3 件）—— **`media_id` 四方一致**：协议示例由数字订正为字符串 ＋ 设备端 C 库改字符串承载（`C4` ＋ `G-453`①）（2026-10-11）

> **来源／依据**：`docs/requirements-backlog.md` 的 `R-033` 条目（「类型与范围口径统一」）· `Q-027` 第 4 批（`C4`：`media_id` 类型 `string` vs `uint64` vs 示例数字）· 清单① 的 `G-453`①（`smart-bed-examples.js` 两处 `media_id` 仍是数字）· `T-555`（`Q11-U6`）已把**协议定义**订正为 `string` ⇒ 本件**只补「协议示例 ＋ C 库」两侧**（⛔ 不重复订正定义）。⚠️ 设计稿属第 3 层设计暂存区，⛔ 按纪律不在本文件引其路径。
> ⚠️ **版本号口径（机械实测，改前取数）**：本文件头部改前**实际最大条目号 ＝ `v132`**（`R-028` 占用，标题逐字 `## v131 → v132：…`）⇒ **本件落 `v133`**、⛔ 不顺延、⛔ 不跳号；`v132` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。
> ⚠️ **行号口径**：本条一律以**内容锚点**为准（本仓多泳道在写；⛔ 不引旧行号）。
> 📌 **本件自证**：⛔ 未改任何门禁脚本／判据／阈值；⛔ **未抬棘轮**；⛔ 未删任何既有断言；**历史留痕面一字未改、只追加**。

### 一、改了什么（逐处：内容锚点 ＋ 改前 → 改后）

⚠️ **本条目用列表而非表格**：`check_rendered_view_tables_parity.py` 的 `A7` 面完备性是**文本计数**判据 ⇒ 若本条用**含表头标记**的表格写字，会被它**如实**数成「未登记的渲染码表」（同 `v131 → v132` 条目的处置与理由）。⛔ 这是**判据在正常工作**，故本条**未改该判据一个字节**。

- **1｜`data/iot/smart-bed-examples.js`** —— `play_online_media` 示例块的 `call.media_id`。改前 ＝ `"media_id": 123456789012345,`（**数字**）；改后 ＝ `"media_id": "123456789012345",`（**字符串**）。
- **2｜同上** —— `play_online_media_control` 示例块的 `call.media_id`。改前／改后同 `1`（两处逐字相同 ⇒ 按外层 `action` 锚点区分；⛔ **未波及其它数字示例**）。
- **3｜`publish/api/data/iot/smart-bed-examples.js`** —— 与 `1`／`2` 同一批内容锚点。改后 ＝ **镜像同步**（单件 `Copy-Item`，双侧 sha256 相等自证；⛔ **未跑** `sync_design_to_publish.ps1 -Apply`）。

### 二、`rule-0d` 取代注记（被改各处**原文逐字**）

- **a｜`play_online_media` 示例的 `media_id`**（本件改点 1）原文逐字 ＝ `"media_id": 123456789012345,`
- **b｜`play_online_media_control` 示例的 `media_id`**（本件改点 2）原文逐字 ＝ `"media_id": 123456789012345,`

⛔ 被改两处**均非删除**：`media_id` 字段本身、其值数字面、以及邻位字段（`url`／`title`／`volume`／`loop`／`play_control_type`）**逐字未动**，只把**类型**由数字改为字符串 —— ＝ 对齐 `data/iot/smart-bed.js` 已订正为 `type: "string"` 的**协议定义**，以及**实现**侧 `media_id: str`。

### 三、⛔ 本件未改（防「顺手改宽」，具名登记）

- **`data/iot/smart-bed.js`** —— `play_online_media`／`play_online_media_control` 的 `media_id: { type: "string", … }` **已是订正后状态**（`T-555`／`Q11-U6`）⇒ ⛔ **未「再订正一次」**（本件**反向自证锚点**）。
- **`data/iot/protocol.js`** —— `media_id` 的字段说明行仍写「由设备在 ack 中返回」，与同册 `smart-bed.js` 的「ack 仅确认收到，无需返回 `media_id`」及 `media_id_ownership`（「由平台在 call 中下发」）**自相矛盾** ⇒ 该面**不在本件写面**（`protocol.js` 归 `R-035`）⇒ **本件未改**，`G-453` 的**第 ② 项仍未销账**（如实登记）。
- **`data/rest/examples.js`** —— 该册另有 **4 处** `"media_id": 123456789012345,`（数字，`rest` 协议示例）⇒ **不在 `G-453` 点名面内**、亦不在本件写面内 ⇒ **本件未改**（如实登记）。
- **`src/zre_c/**` 的其余面** —— `stream_id` 类型（`C5`）经裁定**本轮暂不改**（降级为「登记为已知差异」）；`sched_id`／`batch_id` ⛔ **面外**；`capabilities` 容量（`C-⑦`）与文件头注释（`C-⑫`）属 `R-032`／`R-035` ⇒ **本件未碰**。
- **`data/ai-tools/tools.js`** —— `ControlMusic` 卡（控制类**仍必须**点名 `media_id`）与其余字段／枚举 ⇒ **本件未改**。

---

## v131 → v132：`R-028`（阶段 I 第 2 件）—— **`protocol.js` 新建 `wsCloseCodes` 表 ＋ 三处声明改指新表 ＋ 补渲染页「连接关闭」章**（2026-10-11）

> **来源／依据**：`D-247`（实测「本仓**无** close 码表」⇒ **必须新建**，键名 `wsCloseCodes`）· `D-248`（close 码**以实测全集为准** `{1000,1011,4001,4400,4401,4402,4403}` ＋ 补「穷举 close 码」机械判据）· `docs/requirements-backlog.md` 的 `R-028` 条目（设备侧允许／禁止集）· 阶段 I 落地设计件（`Q-031` 的 close 码表章整改方案定稿，其 `W2`／`W2b`／`W3`／`W3b`／`W3c`／`W4` 给的是**逐字改法**；⚠️ 该稿属**第 3 层设计暂存区**，⛔ 按 J7a 纪律**不在本文件引其路径**）。
> ⚠️ **版本号口径（机械实测，改前取数）**：本文件头部改前**实际最大条目号 ＝ `v131`**（`T-573`／`R-030` 占用，标题逐字 `## v130 → v131：…`）⇒ **本件落 `v132`**、⛔ 不顺延、⛔ 不跳号；`v131` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。
> ⚠️ **行号口径**：本表一律以**内容锚点**为准（本仓多泳道在写；⛔ 不引旧行号——原 `statusCodes` 段的三处声明行号已因 `R-030` 删章而位移）。
> 📌 **本件自证**：⛔ 未改任何门禁脚本／判据／阈值；⛔ **未抬棘轮**（`check_ws_close_code_parity.py` 的 `MIN_PROTOCOL_CODES` 仍为 `1`，见 §四）；⛔ 未删任何既有断言；**历史留痕面一字未改、只追加**。

### 一、改了什么（协议资产侧，逐处：内容锚点 ＋ 改前 → 改后）

⚠️ **本条目用列表而非表格**：`scripts/check_rendered_view_tables_parity.py` 的 `A7` 面完备性是**文本计数**判据（它数**全 `design/**`** 里出现「码表表头标记」的件与张数，登记面 ＝ 渲染页那 6 张）⇒ 若本条用**含该表头标记**的表格写字，会被它**如实**数成"未登记的渲染码表"（改前实测：本条原名下 2 处）。⛔ 这是**判据在正常工作**，不是它误判；故本条改用列表 —— ⛔ **未改该判据一个字节**。

- **1｜`data/iot/protocol.js`** —— 🔴 **新建顶层键**。落点 ＝ `statusCodes: { … }` 块的**闭合 `},` 之后**、`errorFallback` 的注释块（逐字 `// ── 兜底错误帧（T-566 / A2）`）**之前**。改前 ＝（不存在）；改后 ＝ **新增 `wsCloseCodes` 顶层键**：`location` ＋ `命名空间` ＋ `authority` ＋ `categories`（`正常与协议_1xxx` ＝ `1000`／`1011`；`平台拒绝_4xxx` ＝ `4001`／`4400`／`4401`／`4402`／`4403`）＋ `deviceSideAllowedSet` ＋ `deviceSideForbidden`（4 值）＋ `deviceSideForbiddenRanges`（3 段）＋ `deviceSideViolationConsequence`。
- **2｜同上** —— `statusCodes` 的空白注释行（逐字含 `在 close 码里是「心跳超时」` 与**陈旧行号锚点** `` iot_gateway.py:2173 ``）。改前 ＝ ``//    在 close 码里是「心跳超时」（`iot_gateway.py:2173`）。close 码表归 `R-028`，⛔ 不得混读。``；改后 ＝ ``//    在 close 码里是「心跳超时」（实测产出点 ＝ `await conn.websocket.close(code=4001, reason="heartbeat timeout")`）。close 码表见本文件 `wsCloseCodes`，⛔ 不得混读。`` —— 🔴 **陈旧行号订正为内容锚点**（实测该产出点早已位移；⛔ 按纪律**不再引任何行号**）。
- **3｜同上** —— `statusCodes` 的 `命名空间:` 字段。改前 ＝ `命名空间: "信封码（envelope.code）。⛔ WS close 码（1000/1011/4400-4403/close 面的 4001）不属本表，是另一命名空间——两表⛔ 不得混读。"`；改后 ＝ **逐字保留 ＋ 追加**「`close 码表见**本文件 `wsCloseCodes`**（独立顶层键）。`」（⛔ **未删**「不得混读」语义）。
- **4｜同上** —— 渲染页「状态码体系」章的「概述」项末句。改前 ＝ `——两表⛔ 不得混读，close 码表见连接关闭章（<code>R-028</code>）。`；改后 ＝ `——两表⛔ 不得混读，close 码表见**本章「连接关闭」节**。` —— 🔴 原句是**指向不存在章**的**前向引用**（`D-247` 实测），改后指向**实际新增**的节。
- **5｜同上** —— `envelopeContent` 的 `状态码体系` 节点**之后**、`字段定义与编码` 节点**之前**。改前 ＝（无该章）；改后 ＝ **新增 `{ type: "nested", title: "连接关闭", … }` 章**（4 项：概述／正常与协议 1xxx／平台拒绝 4xxx／设备侧允许／禁止集）⇒ `envelopeContent` 节点数 **5 → 6**。
- **6｜同上** —— 文件头 `changeLog` 的**首条**（`v3.6 (…)` 之前）。改前 ＝ 首条 `v3.6 (2026-10-11 T-571/…`；改后 ＝ **追加 `v3.7` 条目置顶**（含本件自证 ＋ `rule-0d` 取代注记**指针**：被改行原文逐字落在**本条目**）。
- **7｜`publish/api/data/iot/protocol.js`** —— 与 `W2`~`W4` 同一批内容锚点。改后 ＝ **镜像同步**（单件 `Copy-Item`，双侧 sha256 相等自证；⛔ **未跑** `sync_design_to_publish.ps1 -Apply`）。

### 二、`rule-0d` 取代注记（被改各处**原文逐字**，⛔ 不复述于 `protocol.js` 内）

- **a｜`statusCodes` 空白注释行**（本件改点 2）原文逐字 ＝ ``//    在 close 码里是「心跳超时」（`iot_gateway.py:2173`）。close 码表归 `R-028`，⛔ 不得混读。``
- **b｜`statusCodes` 的 `命名空间:` 串**（本件改点 3）原文逐字 ＝ `信封码（envelope.code）。⛔ WS close 码（1000/1011/4400-4403/close 面的 4001）不属本表，是另一命名空间——两表⛔ 不得混读。`
- **c｜渲染页「状态码体系」概述末句**（本件改点 4）原文逐字 ＝ `——两表⛔ 不得混读，close 码表见连接关闭章（<code>R-028</code>）。`

⛔ 被改三处**均非删除**：语义（「两表⛔ 不得混读」＋「close 码是另一命名空间」）**逐字保留**，只把**指针**从「归 `R-028`」/「不存在的章」改为**已存在的表与章**。

### 三、验了什么（读数为执行泳道实测）

- **1｜`scripts/check_ws_close_code_parity.py`**（`T23`；`D-248` 穷举判据）—— **改前 `rc=2`**（左集空 ⇒ 范围自检拒绝判绿）⇒ **改后 `rc=0` `[GREEN]`**：`协议 close 码 7｜产出点 8（字面量 6／形参回溯 1／常量解析 1）｜产出点码集 6｜豁免 1（1000）｜双向差集 1`。
- **2｜穷举判据的一条自证**（抗"漏一个码仍假绿"）—— 左集 **7** 码逐值＝`{1000,1011,4001,4400,4401,4402,4403}`；实现侧产出点码集 **6** 码；唯一对称差 ＝ 豁免码 `1000`（**实测 0 命中** ⇒ 免责成立，⛔ 非白名单）。
- **3｜`scripts/check_rendered_view_tables_parity.py`**（`A7` 面完备性）—— `rc=0`：渲染页 **6 张表／22 行**与权威表逐值一致；**新增「连接关闭」章的码表⛔ 未混进 `statusCodes` 登记面**（其表头逐字为「close 码」列，⛔ 不命中 `A7` 的码表标记判据面 —— 这是**刻意**的，见 §四-3）。
- **4｜3 档 viewer 门**（`check-viewer-data-consistency.mjs`／`check-design-asset-render.mjs`／`check-viewer-file-render.mjs`；`rule-0b` §2／§1）—— 全 `rc=0`：25 视图／150 断言双向一致 ＋ 0 页错误；api-viewer 4 type 页二级可表达性全通过；`file://` 实渲染 44 页／**0 页有错误**。
- **5｜`tests/design-regression/test_ws_close_code_parity_gate.py`** —— **改前 1 红**（`test_empty_left_set_refuses_to_judge_green`：表已建 ⇒ 其 `left == set()` 断言必假）⇒ **改写后 7 passed**（改写为「左集非空 ＋ 双向差集为空 ⇒ `rc=0`」正控，⛔ 未删除该臂；fail-closed 语义仍由 `--self-test` 第 ④ 例承载）。
- **6｜`tests/design-regression/test_protocol_t030_contract.py` ＋ `test_protocol_t032_contract.py`** —— 全绿（本件**未**改这两档；两侧 `load_data()` 只读各自关心的键 ⇒ 新增顶层键**不改变可加载性**）。
- **7｜`tests/api/device_access/{test_asgi_adapter,test_gateway_close_observability}.py` ＋ `tests/api/device_iot/test_device_secret_api.py`** —— 全绿 ⇒ 本件是**纯协议资产**改动，⛔ **未改任何实现行为**（`4401`／`4403` 等实测产出点一字未动）。

### 四、**不在本件**（🔴 逐条具名，⛔ 不掩护）

- **1｜`check_ws_close_code_parity.py` 的 `MIN_PROTOCOL_CODES` 由 `1` 抬到 `7`** —— `rule-0d`／任务书**明令 ⛔ 不得抬棘轮**；且该常量由**判据脚本**持有（改它＝另开判据件）。⚠️ 后果已如实登记：该下限现恒真 ⇒ 抗"表被清空"的**最后一道**范围自检**暂缺**。
- **2｜握手阶段改 HTTP 401 ＋ body `{code:2001}`**（落地设计件 `W5`／`W6`／`W7`）—— 属**实现侧**改动（结构性搬家），⛔ 不在本件写面；本件**未改** `src/**` 一个字节。
- **3｜「连接关闭」章码表并入 `statusCodes` 登记面** —— `A7` 面完备性按该标记取数；WS close 码属**另一命名空间**（本件第一要义即「两表⛔ 不得混读」）⇒ 若并入登记面会**篡改**该判据的语义面；⛔ 故本件**未动该判据**，改用语义正确的表头。
- **4｜`W1a`~`W1c`（`notification_level` `warn→warning` ＋ `G-468` 的 `_TEXT_LEVELS`）· `W10`~`W12`（`level` 3 条 ＋ 握手拒绝 5 用例）** —— 同属 `R-028` 但**不同文件面**，须**逐件独立提交**（落地设计件 §3 的 `J1`~`J4` 串行纪律）；本件为**可整组闭合的最小切片**。

### 五、复算命令（机械可复跑）

```powershell
.venv\Scripts\python.exe scripts\check_ws_close_code_parity.py                    # 期望 rc=0 [GREEN]
.venv\Scripts\python.exe scripts\check_ws_close_code_parity.py --list             # 期望 left 7 码 / points 8
.venv\Scripts\python.exe scripts\check_rendered_view_tables_parity.py             # 期望 rc=0
.venv\Scripts\python.exe scripts\run_pytest_watchdog.py tests\design-regression\test_ws_close_code_parity_gate.py
```


## v130 → v131：`T-573` —— **阶段 I 第 4 批（`R-045`）：`X-03` 类型判别 ＋ `X-04` 码三分 ＋ `%188` 硬拒 ＋ `P-127`／`P-128`**（2026-10-11）

> **来源／依据**：需求方裁定 `D-231`（`P-097`：`%188` **硬拒**）· `D-226`（`P-096`：口径 A —— `errors[]` 第 2 格的码属**设备↔平台的协议通道**）· `D-260`（`P-127` 裁 A：`emergency_stop` 连带＝**设备端连带**，只进 `notes`）· `D-261`（`P-128` 裁 B：`steps` 禁嵌套**同批补**实现校验）· `D-228`（`P-092` 裁 A：`batch_id` 必填成立 ⇒ 改实现）；落地设计件 ＝ **第 3 层设计工作区** `.pipeline/design/Q-031/`（落地设计稿 `evidence-r045-landing.md`；**23 处／7 文件**）。
> ⚠️ **版本号口径（机械实测，改前取数）**：本文件头部改前**实际最大条目号 ＝ `v130`**（`T-573`／`R-030` 占用，标题逐字 `## v129 → v130：…`）⇒ **本件落 `v131`**、⛔ 不顺延、⛔ 不跳号；`v130` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。
> 📌 **本件自证**：⛔ 未改任何门禁脚本／判据／阈值；⛔ 未抬棘轮；⛔ 未删任何既有断言（`A1`~`A5`／`A7` 逐条＝**契约随裁定变更**／**新增判据**，见落地报告 §5）；**历史留痕面一字未改、只追加**。

### 一、改了什么（协议资产侧，逐处：内容锚点 ＋ 改前 → 改后）

**（a）`data/iot/smart-bed.js` —— `P-127`（`D-260`）连带语义进 `notes`（2 处 ＋ 文件头块 1 处）**

| # | 内容锚点（逐字） | 改前 | 改后 |
|:--:|---|---|---|
| 1 | 关系表 `"emergency_stop": {` 条的 `notes` 行 | 『最高优先级，绕过所有安全校验；停止所有机械动作+媒体+环境；比 all_play_stop 更高级别』 | 原句**逐字保留在句首**，其后**追加**「🔴 `D-260`（`P-127` 裁 A）**连带停播＝设备端连带**：本指令的「全停」**包含停止全部播放**（URL 媒体／TTS 流／音频流）⇒ 设备端收到后**必须连带停止全部播放**（效果等价于 `all_play_stop`，见该卡逐字「适用于急停场景」）；**平台不另发 `all_play_stop`**（本指令已覆盖，急停下行保持单条）。⚠️ 本语义⛔ **不写成 `triggers`** —— `triggers` ＝「该动作会触发谁」（`D-227`／`P-091` 定的语义），而此处是**伴随**，不是**触发**；机器可判的那一半已在 `related` 里（`all_play_stop` **本就在位**，⛔ 无需新增）。」 |
| 2 | 端点卡 `id: "emergency_stop",` 的 `notes` 行 | 『绕过所有安全校验，设备收到后必须无条件执行全停。触发源：物理急停键/管理后台/AI Agent 安全触发。』 | 原句**逐字保留在句首**，其后**追加**「🔴 `D-260`（`D-230②`）**连带停播（设备端连带）**：本指令的「全停」**包含停止全部播放**（URL 媒体／TTS 流／音频流）⇒ 设备端收到后**必须连带停止全部播放**（效果等价于 `all_play_stop`）；**平台不另发 `all_play_stop`**（急停下行保持单条）。」 |
| 3 | 文件头变更块（现最大块 ＝ `v9`） | 无 `v10` 块 | **追加 `v10`**（逐字见该文件头）：`P-127`／`D-260` 的语义定型 ＋ ⛔ 不写成 `triggers` 的理由 ＋ 「为什么是**设备端连带**」的两条硬证据（`tests/api/device_access/test_event_dispatcher.py` 逐字 `assert sent == ["PlatformCommand.EmergencyStopCommand"]`；`tests/api/ai_agent/test_tool_executor.py` 逐字 `assert len(bus.published) == 1` ＋ `assert isinstance(bus.published[0], EmergencyStopCommand)`） |

> ⛔ **逐字不动的四锚点（反向自证）**：`emergency_stop.triggers` 保持 `[]`（⛔ 未新增 `all_play_stop` —— 那会复发 `P-091` 的"同槽两语义"病）；`emergency_stop.related` 保持 `["all_play_stop"]`（⛔ 未增、未删）；端点卡 `description`（逐字『全局急停 — 最高优先级，停止所有机械动作+媒体+环境』）逐字未动；`all_play_stop` 卡与 `bluetooth_audio_control.related` 逐字未动。

**（b）`data/iot/protocol.js` —— `X-04` 的 `producer` 面（4 处）**

| # | 内容锚点（逐字） | 改前 | 改后 |
|:--:|---|---|---|
| 4 | `参数错误_4xxx` 段的 `4001:` 条目（该键**原无** `producer`） | 『`4001: { code: 4001, meaning: "invalid_params", description: "参数校验失败——字段缺失、类型错误、值超限" }`』 | **补 `producer`**（逐字见该行）：`D-226` 裁定 —— 本码属**设备↔平台的协议通道**（`envelope.code`）⇒ **平台侧只需兑现 2 行**（① `connect` 的 `payload` 字段非法 ② `sleep_report_trigger` 的 `date`／`period`／`enable_*` 非法）；其余 **21 行**的第 4 元素逐字是「忽略指令」＝**接收方（设备）**的校验义务 ⇒ ⛔ 不由平台侧兑现 |
| 5 | `业务错误_6xxx` 段 `6001:` 的 `producer` 内锚点 | 『（`sleep_data/application/services.py:652/735/770` 的 `{"code": 6001}`）』 | 『（`sleep_data/application/services.py` 三处 `"code": 6001, "message": "该设备未绑定睡眠监测设备"` 的返回点；⛔ 不引行号）』 |
| 6 | 同上 `6003:` 的 `producer` 内两处锚点 | 『（`sleep_data/application/services.py:673/752/788`）』 ＋ 『（`iot_gateway.py:807/1091`）』 | 『（`services.py` 三处 `{"code": 6003, "message": "睡眠平台拉取失败"}`）』 ＋ 『（`iot_gateway.py` 的 `result = {"code": 6003, "message": "睡眠数据拉取失败"}` 及其后 `_send_sleep_error_ack(...)` 调用点，与 trigger 路径上另一处 `_send_sleep_error_ack(..., 6003, ...)`）』 |
| 7 | 同上 `6004:` 的 `producer` 内锚点 | 『（`iot_gateway.py:599` 区段）』 | 『（`iot_gateway.py::_validate_sleep_trigger` 内 `sleep_status_trigger` 分支的两处 `return 6004`）』 |

> 🔴 **口径**（`evidence-r045-u2u1u8.md` §2.1-`U2-15`）：`producer` 一律改**内容锚点**（返回点／赋值点的逐字串），⛔ 不复述行号 —— 行号会被同文件后续改动**成片打漂**（本批即实测到 6 处全漂）。
> ⛔ **未动的两处**：`protocol.js` 的 `parameterMatching.description`（逐字『缺少必填字段返回 code=4001 invalid_params』）逐字不动（`D-226` 口径 A 下该句**被坐实**）；渲染页 `sections` 的「参数错误 4xxx」表逐字不动（该表只有 `code｜meaning｜说明` 三列，⛔ 不渲染 `producer` ⇒ 加 `producer` 不改渲染面）。

### 二、实现侧同批（⛔ 不在本册；此处只留指针）

`src/server/gateway/iot_gateway.py`（`X-03` 类型判别 ＋ `X-04` 的 `4001` ＋ `%188` 硬拒含**格式闸门**）· `src/server/gateway/protocol_adapter.py`（`D-1`，**1 行 → 1 行**）· `src/server/modules/device_iot/domain/sub_device_config.py`（载荷层拆档）· `src/server/events/identifier.py` · `src/server/modules/device_control/api/router.py`（`D-228` ＋ `P-128` 同址）—— 逐处改前→改后与验证读数见**第 3 层设计工作区** `.pipeline/design/Q-031/` 的落地执行报告（`evidence-batch4-exec.md`）。

### 三、⛔ 不在本件（逐条具名，防"读成遗漏"）

| # | 项 | 为什么 |
|:--:|---|---|
| 1 | `batch_action` 卡**自身**的 `steps.desc`／`notes` 禁嵌套文案（`D-230①`／`P-095`） | 属 `P-095` 的**协议侧**面；本批面 ＝ `R-045` 的 **23 处／7 文件**（⛔ 未含它）⇒ 由 `P-095` 面另行落地。⚠️ 本批的 `steps` 禁嵌套**实现校验**（`router.py::_validate_batch_action`）**不依赖**该文案亦成立（同族先例在实现侧已存在），但该文案落地前"卡上声明的禁令"仍缺位 ⇒ 已登记 |
| 2 | `protocol.js` 命名空间注释里的陈旧行号『（`iot_gateway.py:2173`）』 | `D-245`（`P-112`）已把「命名空间切分／close 码表」归 **`R-028`** ⇒ ⛔ 本件不动、⛔ 不重复计数 |
| 3 | `X-18`／`X-19`／`X-20` 里"对齐"之外的另两条（`frame_index` 起点、`decode_frame` 补 64 KB 上限） | `D-233`（`P-100`）裁 A 归**另批**：其落点 `stream_frame.py::decode_frame` 与本批的 `%188` 拒收**不同函数**（单一写者要求） |
| 4 | `publish/api/**` 的镜像件 | 走 `scripts/sync_design_to_publish.ps1 -Apply`（⛔ 不手改）＋ publish 面**逐件显式路径提交**（`check_publish_content_parity.py --byte-exact` 判据 ＝「工作树字节 ≡ index blob」） |


## v129 → v130：`T-573` —— **阶段 I 第 3 件（`R-030`）：删协议 `capabilityTaxonomy` 全章 ＋ 悬空引用同批处置**（2026-10-11）

> **来源／依据**：需求方裁定 裁-03a（实现侧 `CapabilityProvider` 基座已删、**当前无消费者**）· `R-030` 条目 · `P7`（协议声明的能力标签体系与实现侧 `capabilities` 上报面自相矛盾）。
> ⚠️ **版本号口径（机械实测，改前取数）**：本文件头部改前**实际最大号 ＝ `v129`**（`T-573` 的 `R-037` 批② `F2` 占用）⇒ **本件落 `v130`、⛔ 不顺延、⛔ 不跳号**；`v129` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。
> 📌 **本件自证**：⛔ **未改任何门禁脚本／判据／阈值**；⛔ **未改任何既有断言**（被删章内**无 `id:` 标记、无 `direction:` 字段** ⇒ `PROTO_ACTIONS` 与三条棘轮读数**删章前后相等**）；**历史留痕面一字未改、只追加**。

### 一、改了什么（逐处：内容锚点 ＋ 改前 → 改后）

| # | 文件 | 内容锚点 | 改前 | 改后 |
|:--:|---|---|---|---|
| 1 | `data/iot/protocol.js` | 起 `// ── 设备能力标签体系` ＋ 紧接的 `capabilityTaxonomy: {`；止 `deviceProfiles` 的闭合 `},`（其前一行逐字 `voice_controller: ["voice_command", "speech_recognition", "tts", "audio", "text_display", "image_display", "video_play"]`） | 整块 **39 行**（注释行 ＋ `description`（声称「共 15 个标签，按功能域分为 5 类…错误码 1002」）＋ `categories` 5 类 ＋ `allTags` 15 项 ＋ `deviceProfiles` 4 档） | **整块删除**（连其后那一个空行一并删除，避免留下连续空行）⇒ 顶层键 **15 → 14** |
| 2 | 同上 | `relatedDocuments` 的 `<li>` 项 | `<li>能力标签：data/iot/capability-taxonomy.js</li>` | **删该 `<li>` 项** —— 🔴 **删的是悬空引用**：`design/04-platform-api/data/iot/capability-taxonomy.js` **实测不存在**；⛔ **不得改指别处**（改指＝制造**新**悬空引用） |
| 3 | `src/web-admin/src/stores/deviceSimulator.ts` | `capabilityTags` 的文档注释 | `/** 设备类型 → 协议能力标签（对齐 protocol.js capabilityTaxonomy.allTags 15 标签） */` | `/** 设备类型 → 能力标签（本地硬编码，⛔ 不再对齐协议章） */`（⚠️ 只改注释；`function capabilityTags(...)` 的**取值本体逐字未动**） |

### 二、`rule-0d` 取代注记（被删／被改各处**原文逐字**）

- 被删章（`data/iot/protocol.js` 顶层键 `capabilityTaxonomy`）**全文**：本文件既有条目与 git 历史（本件基线 `HEAD` 上该块的**完整原文**）保留 —— 摘要：`description` ＝「设备在 connect 注册时通过 capabilities 字段上报的能力标签全集定义。共 15 个标签，按功能域分为 5 类。平台根据能力标签控制可下发的 action 范围（未声明 capability 的 action 将被拒绝，错误码 1002）。」；`categories` 5 类 ＝ 姿态与动作_PostureAction（`angle_ctrl`／`massage`／`dual_zone`）· 环境调节_EnvironmentAdjustment（`light`／`heating`／`ventilation`）· 传感监测_SensingMonitoring（`sleep_monitor`／`environmental_sensors`）· 指令交互_CommandInteraction（`voice_command`／`speech_recognition`／`tts`／`text_display`）· 媒体输出_MediaOutput（`audio`／`image_display`／`video_play`）；`allTags` 15 项 ＝ 上述五类并集，顺序逐字如上；`deviceProfiles` 4 档 ＝ `basic_bed`／`standard_bed`／`premium_bed`／`voice_controller`。
- 被删 `<li>` 原文逐字：`<li>能力标签：data/iot/capability-taxonomy.js</li>`。
- 被改注释原文逐字：`/** 设备类型 → 协议能力标签（对齐 protocol.js capabilityTaxonomy.allTags 15 标签） */`。

### 三、**不在本件**（🔴 逐条具名）

| # | 项 | 为什么 |
|:--:|---|---|
| 1 | `smart-bed.js` 的 `categories` 7 分类 `actions:` 数组 · `actionCapabilityMap` · 扁平 action 定义 | ⛔ 删的是协议**章** `capabilityTaxonomy`，**不是** action 能力映射表；`test_device_actions_flat.py` 正钉着它们 |
| 2 | `smart-bed.js` 的 `connect` 卡 payload（**无 `capabilities` 键**） | 该侧本就无字段 ⇒ ⛔ **不得为对称而补一个 `capabilities` 进去** |
| 3 | `src/zre_c/**` 的 `capabilities[8]` 上报字段 | 属 **`R-032`** `C-⑦` 的面（跨件边界） |
| 4 | `capabilityTaxonomy` 的**历史留痕面**（各 `CHANGELOG.md`／`design/01-raw-input/**`／`docs/**`／`tests/test-reports/**`） | `rule-0d`：**只追加、不改写** ⇒ 逐条保留 |

## v128 → v129：`T-573` —— **`R-037` 批② `F2`：删「指定版式（`layout`）」整链（协议 / 端点卡 / 示例面；含 DB/ORM）**（2026-10-11）

> **来源／依据**：需求方裁定 `D-239`（`R-037` 批② `F2` ＝ **全链删除**，**含 DB/ORM**）· `D-240`（`smart-bed-examples.js` **纳入**）· `D-250`（退役字段按**未知字段 ⇒ 忽略**，⛔ 不 6004）· `D-238`（冲突用例**缩窄 ＋ 补负控 ＋ 改名**）；**改后形态**依据 ＝ `Q-025` §0.3 `F2`／§4.1／§4.3／§5。
> ⚠️ **版本号口径（机械实测，改前取数）**：本文件头部改前**实际最大号 ＝ `v128`**（`T-571` 占用）⇒ **本件落 `v129`、⛔ 不顺延、⛔ 不跳号**；`v128` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。
> ⚠️ **行号口径**：本表一律以 **内容锚点** 为准，行号只作参考（本仓多泳道在写）。
> 📌 **本件自证**：⛔ **未改任何门禁脚本／判据／阈值**；⛔ 未动 `publish/index.html`；**历史留痕面一字未改、只追加** —— `smart-bed.js` 文件头 `v4`~`v8` 变更块（含 `v8` 的「`layout` **一字未动**（`T-573` 反向自证）」）、本文件既有条目、其它三域的 `CHANGELOG.md` 均**未改写**。

### 一、改了什么（逐处：内容锚点 ＋ 改前 → 改后）

| # | 文件 | 内容锚点 | 改前 | 改后 |
|:--:|---|---|---|---|
| 1 | `data/iot/smart-bed.js` | `sleep_report_trigger.envelope.payload` 的 `layout` 字段 | `layout: { type: "enum", required: false, desc: "图片布局（仅 enable_image=true 时有效）：single=单页 … multi 为「每侧 1 页 + 1 汇总页」…" }` | **整条删除**（三枚举 `single`／`long`／`multi` 一并消失） |
| 2 | 同上 | `sleep_report_trigger.errors[]` | `"period 不在枚举范围 / layout 非法"` | `"period 不在枚举范围"` |
| 3 | 同上 | `sleep_report_trigger.notes` 的 **N 构成** 句 | 含 `single/long=1、multi=3`／`multi=N+1（每侧 1 页 + 汇总页）`／末句 `layout 是 trigger 侧参数…` | 改为**不含版式选项**的写法：单睡眠位 ＝ 摘要（＋体征）（＋环境，由 `show` 开关决定）；双睡眠位 ＝ 每侧 1 张侧别摘要页；**末句整句删除** |
| 4 | 同上 | `connect.configurations.display_output.resolution.desc` | `…分页（layout=multi 时每张图分辨率=本值）` | `…分页（每张图分辨率=本值）` |
| 5 | 同上 | `sleep_report_image_push.images.desc` 的 **N 的构成** | 含 `single/long=1 项`／`multi=3 项`／`multi=N+1（每侧 1 页 + 1 张「多睡眠位汇总」对比页）`／`协议不带 width/height/layout 元数据` | 改为**不含版式选项**的写法（每侧 1 张侧别摘要页；汇总对比页**已退役**）；末句去 `/layout` |
| 6 | 同上 | 分类卡 `sleep_data` 的 `description` 与 `actionRelationships.sleep_report_trigger.description` | `…三开关 + layout…` | `…三开关…`（两处） |
| 7 | 同上 | 文件头变更块 | 最大块 ＝ `v8` | **追加 `v9`** 块（含 `rule-0d` **取代注记**：被删/被改各处**旧文逐字**） |
| 8 | `data/iot/smart-bed-examples.js` | `sleep_report_trigger.call` 的 `"layout": "multi"` 与分节注释 | `"enable_image": true,` ＋ `"layout": "multi"` | 示例值**删除**；注释 `（三开关+layout，ack 纯 ack）` → `（三开关，ack 纯 ack）` |
| 9 | `data/iot/event-identifier.js` | `sleep_report_trigger.desc` | `…（enable_text/enable_voice/enable_image + layout，ack 纯 ack——T-032）` | `…（enable_text/enable_voice/enable_image，ack 纯 ack——T-032；R-037 批② F2 去 layout）` |
| 10 | `data/rest/system.js` | `implementation.note` 的 `T-068` 段之后 | 段末 ＝ `…（卡数即端点数字段随之更新）。` | **追加** `R-037` 批② 段（`default_layout` 退役／预览 `layout?` 退役／`show.sides_compare` 退役）＋ `rule-0d` 明示上文各段为**当时事实、逐字保留**；⚠️ **两条 `template` 端点卡的 `layout` 是「模板 layout 段」（非版式选项），逐字未动** |
| 11 | 同上 | `rest-system-sleep-report-put.description` | `字段级校验：layout/locale/颜色/开关` | `字段级校验：locale/颜色/开关` |
| 12 | 同上 | `rest-system-sleep-report-preview.description` 的请求体键清单 | `…, layout?, locale?, width?, …` | `…, locale?, width?, …` |

### 二、**不在本件**（🔴 逐条具名，交下一批；⛔ 不是漏做）

| # | 项 | 为什么 |
|:--:|---|---|
| 1 | **`Q-025` 的四页结构与 `1280×720` 设计空间**（`N 构成` 的目标值「单侧恒 4／双睡眠位恒 8」· 模板重排 · `DESIGN_WIDTH` 等） | 依赖 **`P-129`**（四页的**第 3/4 页构成**待裁；现随包块模板不足以拼出页 3/4）⇒ 本件**只做 `F2` 相关面**，⛔ **不预先写出该组数值** |
| 2 | `blocks/section_header.svg` 去留 · `svg_render` 缩放口径是否连带改 | 同属待裁项 ⇒ 本件未动（`render_report` 已无版式分支，但该资产与缩放口径**保留原状**） |
| 3 | `design/03-entity-relationship/CHANGELOG.md` · `design/05-backend-architecture/CHANGELOG.md` · `design/06-web-admin/CHANGELOG.md` 的域级变更条目 | 本件只按 A-31 追加 **本域** CHANGELOG；另三域的数据件已同批改（见下），其域级 CHANGELOG 条目**待补**（逐条具名移交，⛔ 不静默） |

> 🔴 **同批改到的、不属本域 CHANGELOG 管辖的数据件**（逐条登记，供跨域追溯）：
> `design/03-entity-relationship/data/03-sleep-data.js`（实体卡 `sleep_report_image` 去 `layout` 列 ＋ 索引 `(device_id, report_date, period)`；读契约 `sleep_report_image_read_contract` 的 `row_fields` 9→8、`summary` 去 `byLayout`、`filters_excluded` 去 `layout`）·
> `design/05-backend-architecture/data/event-contracts.js`（`DeviceCommand.SleepReportTriggerCommand.fields` 去 `layout`）·
> `design/06-web-admin/data/tree.js`（「报告产物清单」的 `desc` 与 `refFields` 去 `layout`）。
> 镜像面：以上三件 ＋ 本域四件（`iot/smart-bed.js` · `iot/smart-bed-examples.js` · `iot/event-identifier.js` · `data/rest/system.js`）＋ 本 CHANGELOG **全部走 `sync_design_to_publish.ps1 -Apply` 同步进 `publish/**`**，并按 `scripts/check_publish_content_parity.py --byte-exact` 逐字节核。

### 三、实现侧同步（同批，供协议 ↔ 代码对读）

`src/server/events/base.py`（`SleepReportTriggerCommand` 去 `layout`）· `src/server/gateway/iot_gateway.py`（`_validate_sleep_trigger` 去该键三枚举校验；去 `resolve_layout` 调用与 `save_pages(layout=)` 实参）· `domain/report_prefs.py`（去 `resolve_layout`／`normalize_layout`／`REPORT_LAYOUTS`／`DEFAULT_REPORT_LAYOUT`／`REPORT_LAYOUT_PREF_KEY`／`DeviceReportPrefs.layout`）· `domain/report_settings.py`（去平台 `default_layout`／`_pick_layout`／`LAYOUT_VALUES`）· `domain/renderer.py`（`RenderOptions.layout` 去；`render_report`/`render_trend_report` 的 `layout` 形参去；`single`/`long` 分支与 `auto_height` 链、`render_long*`/`render_trend_long` 退役）· `domain/svg_report.py` · `domain/svg_bind.py` · `infrastructure/preferences.py` · `infrastructure/orm.py`（列 ＋ 索引）＋ 迁移 `alembic/versions/2026_10_11_1000-e3f4a5b6c7d9_drop_sleep_report_images_layout.py` · `application/preview.py` · `application/image_listing.py`（行白名单 9→8、`summary` 去 `byLayout`）· `modules/system/api/schemas.py` · `modules/system/api/router.py` · `modules/system/application/services.py` · `modules/sleep_data/api/router.py` · `modules/sleep_data/events/__init__.py` · `src/web-admin/**`（设置项／预览入参／模拟器透传／产物清单列）。


## v127 → v128：`T-571` —— **`1005` 语义订正 ＋ `1004` `delivery` 声明并入 ＋ 「只在 ack」载体声明 `carrier`（`T-3`／`D-206`／`D-207`；阶段 I「协议规范冻结」追加件）**（2026-10-11）

> **来源／依据**：需求方 2026-10-10 裁定 `T-3`（`stream_stop`／`all_play_stop` **不是** cancel 的产生源）＋ `D-206`（`1004` 回执只发往调用方／观测面）＋ `D-207`（`1005` 一个产出点都不补）＋ 任务书 `T-571`（协议**文本**面）。⚠️ 改法的**逐字依据件**属**第 3 层设计暂存区**（未获批稿）⇒ 🔴 **本 CHANGELOG⛔ 不引用其路径**（`J7a`：未定稿内容不得渗进 `design/**`；第 3 层边界铁律）。
> ⚠️ **版本号口径（机械实测，改前取数）**：核过本文件头部 —— 改前**实际最大号 ＝ `v127`**（`T-573` 占用，`:4`）；`v128` 未被占用 ⇒ **本件落 `v128`、⛔ 不顺延、⛔ 不跳号**；`v127` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。
> ⚠️ **行号口径**：本条的「快照行」列取自**落地设计件**的记录值；🔴 实测本仓快照与设计件记录值存在**整体漂移**（如设计件的 `1005` 条目 `:375`／渲染 `1005` 行 `:728`，实测为 `:376`／`:733`）⇒ 下表一律以 **内容锚点** 为准，行号只作参考；`rpc.cancel` 之后的锚点 **+5**（`mechanisms` 整键删除 ＋ `note` 数组化 ⇒ 该块净 +4 行），`statusCodes` 之后 **+1**（`carrier` 新增一行）。
> 📌 **任务书自证**：本件**⛔ 未改判据／阈值**、⛔ **未改实现行为**、⛔ **未动 `tests/**`**（实测对 `1005` 信封码零覆盖）。

### 一、改了什么（逐处：内容锚点 ＋ 改前 → 改后）

| # | 内容锚点 | 改前 | 改后 |
|:--:|---|---|---|
| **A1** | `statusCodes.categories.通用错误_1xxx[1005].description` | `操作已被中断——设备收到 cancel 指令（stream_stop/all_play_stop）后终止当前操作` | `对上一个动作的取消——发送方在等待 ack 期间主动中断该次调用，取消的对象是被中断的那条 call。⚠️ 这⛔ 不是「设备终止当前播放/当前媒体」（那是对媒体的新操作，见 rpc.cancel）；本码的承载形态恒为 ack 信封，见 statusCodes.carrier。` |
| **A2** | 同条目 `.producer` | `平台侧（调用中断，见 rpc「调用中断」）：发送方在等待 ack 期间主动中断调用 ⇒ 该 call 以 1005 收尾；设备侧收到 cancel 类指令终止当前操作后亦可以本码回 ack。⚠️ 实现侧落地归 R-029…` | `🔴 唯一来源 ＝ 中断方：…该 call 在**中断方本地收尾**（本地落记录／上抛），⛔ 中断方不向对端发任何帧；被中断 call 的接收方仍照常回它自己的 ack，中断方收到后**丢弃**…… 🔴 stream_stop／all_play_stop 不是本码的产生源——它们是对媒体的**新操作**…… 📌 本码当前**无产出点**，且按裁定**一个都不补**…… ⚠️ 实现侧产出点补齐归 G-465` |
| **A3-①** | `…[1003].producer` 末句 | `⚠️ 实现侧闸门落地归 R-029（本件只做协议侧产出点声明）。` | `✅ 实现侧闸门**已落地**（T-567／R-029：iot_gateway.py 的 _DeviceCallGate ＋ _emit_device_busy…）。⚠️ 调用方补偿（downlink_consumer.py 收到 1003 后的重试/排队）归 G-465…` |
| **A3-②** | `…[1004].producer` | `…未收到 ack ⇒ 该次调用以 1004 收尾（记录/上抛）；…⚠️ 实现侧落地归 R-029…` | `…未收到 ack（且 rpc.retry 各档重试耗尽）⇒ 该次调用在**本地收尾**（落记录／上抛），⛔ 不向对端发任何帧（同 1005 的形态约束）…📌 本码当前**无产出点**（src/server/** 对 1004 **零字面量**），产出点补齐归 G-465…` |
| **A4** | `…[1004]` 新键（与 `description`／`producer` 同级） | —（不存在） | 新增 `delivery`：`🔴 投递范围（D-206 裁定）：本码的回执**只发往调用方／观测面**…⛔ **不发往设备** —— 超时判定是调用方的**本地结论**…设备侧的「执行超时」若需回执，走它自己的 {action}_ack（code=1004）常规 RPC。` |
| **B** | `rpc.cancel`（整块） | `mechanisms: [ {action:"stream_stop",…}, {action:"all_play_stop",…} ]` ＋ `flow: 发送 cancel call → … → 原 call 返回 code=1005 cancelled` | 🔴 **`mechanisms` 整键删除**（取消⛔ 不是一种 action）⇒ 改 `statement`（取消＝对上一个动作的取消、中断方**本地行为**、⛔ 不产生任何帧）＋ `flow`（三段：中断方本地中断 → 对端 ack 照常 → 中断方收到即**丢弃**）＋ `note`（**5 条**） |
| **C1** | `statusCodes` 顶层新键（紧邻 `location`，⛔ 不落 `categories` 内） | —（不存在） | 新增 `carrier`（**单字符串**）：`🔴 本表**全部码只在 ack 里携带**：位置恒为 ack 信封的 envelope.code（type==ack）；⛔ **不存在独立通知帧**形态。……⚠️ 本键只声明**承载位置**，⛔ 不得读成「承诺某处会产出某个码」` |
| **C2** | `envelopeContent → 状态码体系 → 概述`（渲染面） | `…close 码表见连接关闭章（R-028）。` | 追加：`🔴 <strong>本表全部码只在 <code>ack</code> 里携带</strong>（位置恒为 <code>envelope.code</code>）；⛔ <strong>不存在独立通知帧形态</strong>。` |
| **D1** | `通用错误 1xxx` 表渲染行 `1005` | `<td>操作已被中断——设备收到 cancel 指令后终止当前操作</td>` | `<td>对上一个动作的取消——取消对象是被中断的那条 call（⛔ 不是「设备终止当前播放」，也⛔ 不是独立通知帧）</td>` |
| **D2** | `通用错误 1xxx` 表渲染行 `1004` | `<td>操作超时——设备在规定时间内未完成操作</td>` | 追加同义括注：`（🔴 该码只发往调用方／观测面，⛔ 不发往设备）` |
| **E** | 调用语义章 → `调用中断` 条 `content` | `发送方在等待 ack 期间可主动中断调用（逻辑中断非网络层）。中断后释放该设备并发槽位。被中断的 call 收到 ack 时丢弃。` | **前 3 句一字不动**，其后**追加**两句：`🔴 **取消不产生任何帧**：…⛔ 不向对端发取消帧、也不以 1005 回应对端…🔴 **本条不改变 RPC 的 ack 数**：每个 call 仍**只回一个** ack…` |
| **①** | 元数据 `version` | `"3.5"` | `"3.6"`（先例：`T-566` `3.2⇒3.3`／`T-567` `3.3⇒3.4`／`T-572` `3.4⇒3.5` **逐件升版**） |
| **②** | `changeLog` 首条 | `"v3.5 (2026-10-10 T-572/…` | **就地前置** `"v3.6 (2026-10-11 T-571/…`（⛔ **旧条逐字保留**，⛔ 未删） |

### 二、`rule-0d` 取代注记（被改行**原文逐字**，⛔ 不复述于 `protocol.js` 内）

| 对象 | 被改行原文（逐字） |
|---|---|
| `1005.description` | `操作已被中断——设备收到 cancel 指令（stream_stop/all_play_stop）后终止当前操作` |
| `1005.producer` | `平台侧（调用中断，见 rpc「调用中断」）：发送方在等待 ack 期间主动中断调用 ⇒ 该 call 以 1005 收尾；设备侧收到 cancel 类指令终止当前操作后亦可以本码回 ack。⚠️ 实现侧落地归 R-029（本件只做协议侧产出点声明）。` |
| `1003.producer` 末句 | `⚠️ 实现侧闸门落地归 R-029（本件只做协议侧产出点声明）。` |
| `1004.producer` | `平台侧（RPC 超时判定）：平台作为调用方在 rpc.timeout 规定时间内未收到 ack ⇒ 该次调用以 1004 收尾（记录/上抛）；设备侧执行超时亦可以本码回 ack。⚠️ 实现侧落地归 R-029（本件只做协议侧产出点声明）。` |
| `rpc.cancel.mechanisms`（**两条整行**，🔴 **删的是「取消机制」这一归属**，两条动作的语义与方向**未丢**：权威定义仍在 `smart-bed.js` 的 `stream_stop`／`all_play_stop` 动作卡，`protocol.js` 的 `stream_stop_vs_play_stop`／`streaming.stopActions` 两处逐字亦未动） | `{ action: "stream_stop", direction: "receiver→sender", description: "接收方通知发送方停止发送流数据。停流≠停播——已缓存的帧仍可能继续播放" }` ／ `{ action: "all_play_stop", direction: "platform→device", description: "无论当前在播放什么（URL媒体、TTS流、音频流），全部强制停止。无需任何参数" }` |
| `rpc.cancel.flow` | `发送 cancel call → 设备/平台中断当前操作 → 回复 ack 确认中断 → 原 call 返回 code=1005 cancelled` |
| `调用中断` 条 | `发送方在等待 ack 期间可主动中断调用（逻辑中断非网络层）。中断后释放该设备并发槽位。被中断的 call 收到 ack 时丢弃。`（本条为**追加**，原文逐字保留） |
| 渲染行 `1005` | `<tr><td>1005</td><td>cancelled</td><td>操作已被中断——设备收到 cancel 指令后终止当前操作</td></tr>` |
| 渲染行 `1004` | `<tr><td>1004</td><td>timeout</td><td>操作超时——设备在规定时间内未完成操作</td></tr>` |

### 三、机械读数（改前 ⇒ 改后，★ ＝ 本件实测）

| # | 探针 | 改前 | 改后 |
|:--:|---|---|---|
| **R1** | `rpc.cancel` 块内 `action:\s*"` 计数 | **2** | 🔴 **0** |
| **R2** | 全文 `action:\s*"(stream_stop｜all_play_stop)"` | **0** | 🔴 **0** |
| **R3①** | 全文「设备收到 cancel 指令」 | **2** | 🔴 **0** |
| **R3②** | `carrier:\s*"` 键形态 ／「只在 ack 里携带」 | **0** ／ **0** | 🔴 **1** ／ 🔴 **3** |
| **R3③** | 全文「独立通知帧」 | **0**（⚠️ 设计草案记的「1」是错的） | 🔴 **3** |
| **R4** | 勿误伤面 `301`／`309`／`592→597`／`620→625`／`629→634`／`703→708`（行号因 `rpc.cancel` 净 +4 而漂移） | 逐字在位 | 🔴 **6 行逐字未变**（已按**行内内容哈希**逐行比对） |
| **R5** | `\b1005\b` 命中行/计数 | `24, 375, 551, 728`（**4**） | `24, 375(#1004 引用), 376(**唯一定义点**), 549, 553, 554, 728, 733`（**8**）＝ 🔴 **定义点不增（仍 1）**；增量**全部**是本批**必须**新增的**非定义**引用（`changeLog` ／`carrier` 互指 ／ `rpc.cancel` flow＋note ／ `调用中断` 条） |
| **R5b** | 三处 `producer` 内 `R-029` ／ `G-465` | **3** ／ **0** | 🔴 **1** ／ 🔴 **3**（⚠️ 残留那 1 处在 `1003.producer` 的 **✅ 已落地引证**「（`T-567`／`R-029`…）」内，**不是**指向未兑现承诺的死指针 ⇒ 判据期望值订正为 1，见「四」） |
| **R5c** | `changeLog` 内 `R-029` | 在（首条） | 🔴 **仍在**（`rule-0d`：⛔ 不改写历史；原样落在新的 `v3.6` 行之后的 `v3.5` 段内） |
| **R9** | `sync_design_to_publish.ps1 -Check` ／ 两侧 `protocol.js` sha256 | 0 差异 ／ `cacc376c…326974`（两侧同） | 🔴 见本件收口读数（`-Apply` 后 `-Check` ＝ 0；两侧 sha256 **再次相等**） |
| **R10** | 字节级纪律 | `CR=0`·无 BOM·777 行·111,664 B | 🔴 `CR=0`·无 BOM·**782 行**·**117,170 B**（`+5` 行 ＝ `carrier` +1／`changeLog` +1／`rpc.cancel` 净 +4，另 `-1` 为 `mechanisms` 两行换 `note` 五条的行内净额在块内的体现） |

### 四、判据期望值订正（🔴 实测定值，⛔ 不是"改判据换绿"）

| # | 设计件期望 | 实测定值 | 为什么订正 |
|:--:|---|---|---|
| **R5b** | 三处 `producer` 内 `R-029` ⇒ **0** | **1** | 设计件 A3-① 的**改后逐字本身**就含「`T-567`／`R-029`」作为**已落地引证**（✅ 既成事实，⛔ 不是指向未来的死指针）。⇒ 判据的**语义**（死指针归零）**达成**；**字面枚举值**由 0 订正为 1。 |
| **R5** | `\b1005\b` 计数仍 ＝ 4 | **8** | 设计件判据的**语义**是「⛔ 无第二权威定义点」；该语义**达成**（定义点仍恒为 **1**，`:376`）。计数上升**全部**来自本批**必须**新增的非定义引用。⇒ 判据按**语义**（定义点）复核，⛔ 不按字面枚举值。 |
| **R3③** | 恰 3 | **3** ✅ | 落地时按设计件 §1.2-**M6** 取「A1 不写该词」一侧；⚠️ 且 `rpc.cancel.note[⑤]` 的初稿**误用**该词（会使计数变 4）⇒ 已改写为「整表承载位置与帧形态的唯一声明处是 `statusCodes.carrier`」。**同形**要求已就地满足。 |
| **R5c** | `changeLog` 内 `R-029` 仍在 | ✅ 仍在 | 本件**删除**了 v3.6 条里对旧句的**逐字复述**（否则 `R3①`／`R3③` 各自会被 `changeLog` 抬高 1、且与「不复述」原意相悖）；历史留痕由 `v3.5` 段原样承载，**另**在本 CHANGELOG 的「取代注记」表内逐字留档。 |

### 五、本件⛔ 未做（分层声明）

| # | 未做项 | 归属 |
|:--:|---|---|
| 1 | `1004`／`1005` 的**实现侧产出点**（把「重试耗尽 ⇒ 上抛/记录 `1004`」等接进调用方） | **`G-465`** |
| 2 | `1003` 的**调用方补偿**（`downlink_consumer.py` 收到 `1003` 后的重试/排队） | **`G-465`** |
| 3 | `smart-bed.js` 的 **`v9` 变更块** | 🔴 **本件判不新增** —— 本件 9 处写面**无一处**在该文件内（两条动作卡逐字未变） |
| 4 | `tests/**` 断言 | ✅ **无须改**（实测对 `1005` 信封码零覆盖）；收口仍跑 `tests/design-regression/` 全档 |
| 5 | 「渲染表 ↔ 权威表逐值一致」**门禁** | ⛔ **不新建**（`rule-0d`；本批靠 `:727` 的约定 ＋ 同批编辑 D1／D2） |
| 6 | `src/server/gateway/iot_gateway.py` 注释里的**裸行号**（`:365`／`:481`／`:364`） | ⚠️ **本件⛔ 未并入**（设计件 §10-**U8** 未判定；并入会触碰「实现侧零改动」判据）⇒ 但本批使这些行号**再次漂移**（实测：`1004.producer` 在 `:375`、`retryOnTimeout` 在 `:542`、`1003` 条目在 `:374`） |

## v126 → v127：`T-573` —— **睡眠状态取数面（`R-037` 批①）：默认窗 30 min → 3 min ＋ 只回窗内最新一条 ＋ 删 `sleep_status_trigger` 三形式开关**（2026-10-10）

> **来源／依据**：`R-037` 批①（取数面；`Q-025` 设计**已冻结**，需求方 2026-10-10 15:10 批准）＋ `T-573` 编排器任务书（§19 「`R-037` 批① 取数面」）。覆盖项 `T8`／`T5`／`T7`／`T4`。
> ⚠️ **版本号口径**：核过本文件头部 —— 改前**实际最大号 ＝ `v126`**（`T-572` 占用）；`v127` 未被占用 ⇒ **本件落 `v127`、不顺延**；`v126` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。
> 📌 **任务书自证**：**6 项订正** —— ①~③ 3 项**行号漂移且落在错误卡片上**（任务书给的 `sleep_status_trigger` 卡 `:359`／三字段 `:364-366`；实测卡在 **`:374`**、三字段在 **`:379-381`**，而 `:359`／`:364-366` 实测是 **`sleep_report_trigger` 的 `layout`／`errors`／`notes`** ⇒ 照抄行号**会误删报告触发卡**）；④~⑤ 2 项范围收窄（取数窗只改**设备侧** `fetch_status`，⛔ 不动 AI 侧 `fetch_status_raw` 的 1 小时窗；`T7` 只做**写面白名单内**的协议卡 ＋ 网关校验循环）；⑥ 1 项复核一致（`T-572` 的 `samples: []` 口径现值为真）。全文见 `.pipeline/journal/T-573.journal.md` §0；基线读数见同文件 §1。

| 落点 | 改前 | 改后 |
|---|---|---|
| `data/iot/smart-bed.js` · `sleep_status_trigger.start_time.desc` | 『…。不填=最近 30 分钟』 | 『…。🔴 **默认窗（`start_time` 与 `end_time` 都不填）= 当前时间往前 3 分钟**…**两个时间参数都不填**时**只返回窗内时间最新的一条**…**给了任一时间参数** ⇒ 按显式窗返回**序列**』 |
| 同上 · `end_time.desc` | 『时间窗终点，ISO 8601 字符串。不填=当前时间』 | 逐字保留 ＋ 追加『⚠️ 与 `start_time` **同时**不填 ⇒ 退化为**默认窗（过去 3 分钟）＋ 只回窗内最新一条**』 |
| 同上 · `sleep_status_trigger` payload **三字段** | `enable_text`／`enable_voice`／`enable_image`（`T-032` 起） | 🔴 **整条删除**（`T7`；`T5` 已把承载定为**结构化 `sleep_status_push`** ⇒ 形式开关失去意义）。⛔ **`sleep_report_trigger` 的同名三开关 ＋ `layout` 一字未动**（反向自证，见下）。 |
| 同上 · `sleep_status_trigger.notes` | 原句（见下「取代注记」逐字） | 原句**逐字保留** ＋ 追加 `T8`（默认窗 3 分钟／只回最新一条／窗内无数据回空）与 `T7`（无形式开关）两段 |
| 同上 · `sleep_status_push.devices.desc` | 『…（固定全量）。元素结构：{sub_device_id, device_status, samples[], error?}』 | 追加『🔴 **结构化数据包（`T-573`/`T5`）**：四类字段（体动 `samples[].body`／心率 `samples[].heart`／呼吸 `samples[].breathe`／在床离床 `device_status`）；**该床全部睡眠监测设备都发**（双睡眠位 ⇒ 两条）；⛔ **不做分钟级**（长度不随窗长/分钟数增长）』 |
| 同上 · `sleep_status_push.devices[].samples.desc` | 『…🔴 **该侧无数据 ＝ `samples: []`**…』（`T-572` 现值，**本件承接、⛔ 未相悖**） | 追加『🔴 `T-573`（`T8`）：**默认窗**（trigger 未给时间参数）时长度 ∈ **{0,1}**（只含窗内**时间戳最大**的一条，⛔ 不是数组末元素）；**显式窗**时仍为窗内**序列**；⛔ 不给窗外/上一次的旧数据』 |
| 同上 · 文件头变更块 | 末块 ＝ `v7`（`T-572`） | **追加 `v8`**（本条；含 `rule-0d` 取代注记：被改/被删的各处旧文**逐字**）—— ⛔ `v7` 块（`:56`–`:70`）**一字未改** |
| **实现侧** `src/server/modules/sleep_data/application/services.py`（`fetch_status`） | `:657` `start = … (end - 30 * 60 * 1000)`（**30 分钟**）＋ 默认窗把上游 `list[]` **逐条全量**映射 | 新增模块常量 `DEFAULT_STATUS_WINDOW_MS = 3 * 60 * 1000`；**默认窗**（`start_time is None and end_time is None`）只取**时间戳最大**的一条（⛔ 非数组末元素）⇒ `samples` 长度 ∈ {0,1}；**显式窗**仍全量；补默认窗**命中/落空**结构化日志。⛔ `fetch_status_raw`（AI 侧 1 小时窗）**未动** |
| **实现侧** `src/server/gateway/iot_gateway.py`（`_validate_sleep_trigger`） | `sleep_status_trigger` 分支含 `enable_*` 类型校验循环 | 🔴 删该循环（协议字段已删 ⇒ 校验无对象）；⛔ `sleep_report_trigger` 分支的**同名**循环**未动** |
| `publish/**` 同步 | — | 按 `scripts/sync_design_to_publish.ps1` 镜像；核验读数见 `.pipeline/journal/T-573.journal.md` §6 |
| `CHANGELOG.md`（本文件） | — | v126 → **v127**（本条；V 号核过：改前文件头部实际最大号 ＝ `v126`） |

- 🔴 **`rule-0d` 取代注记 —— 被改 / 被删原文逐字**（来源 `data/iot/smart-bed.js`；⛔ 以下为**原文**，未改一字）：
  - `start_time.desc` 原文：『时间窗起点，ISO 8601 字符串（如 2026-08-31T10:00:00+08:00）。不填=最近 30 分钟』
  - `end_time.desc` 原文：『时间窗终点，ISO 8601 字符串。不填=当前时间』
  - `sleep_status_trigger.notes` 原文：『trigger 的 ack 仅确认受理（纯 ack），数据随后由 sleep_status_push 下发。固定查询该床全部睡眠监测设备（left/right 各一或 both 单条），devices[] 数组返回，不提供 sub_device_id 参数。时间/日期一律 ISO 8601 字符串（T-032 通用约定；原 date 字段已删除，只留 start_time/end_time）。』
  - **被删三字段原文逐字**（`smart-bed.js`）：

```text
        enable_text:  { type: "bool", required: false, desc: "返回形式开关——文本（走 show_text 直推）" },
        enable_voice: { type: "bool", required: false, desc: "返回形式开关——语音播报（走 play_voice 直推）" },
        enable_image: { type: "bool", required: false, desc: "返回形式开关——图片渲染" }
```

  - **`iot_gateway.py` 被删校验循环原文逐字**：

```text
            for flag in ("enable_text", "enable_voice", "enable_image"):
                v = payload.get(flag)
                if v is not None and not isinstance(v, bool):
                    return 6004
```

- ✅ **反向自证（防误删，本件验收判据之一）**：`sleep_report_trigger` 卡（含 `enable_text`/`enable_voice`/`enable_image`/`layout`）与 `iot_gateway.py` 的 `sleep_report_trigger` 校验循环**逐字节未变**（`sha256` 读数见 journal §6）；`T4` 语音通道四卡（`play_voice`／`play_stream_voice`／`voice_command`／`stream_voice_command`）同样**逐字节未变**。
- ⛔ **未做（进 `T-573` 报告 §⑨，判据 ＝ 不在本件写面白名单）**：① `data/iot/smart-bed-examples.js` 的两条示例（`Q-025` §9.3 要求，但白名单只到 `smart-bed.js`）；② `src/server/events/base.py::SleepStatusTriggerCommand` 的三字段与 `design/05-backend-architecture/data/event-contracts.js` 的同名三字段（⚠️ `tests/design-regression/test_protocol_t032_contract.py::test_sleep_status_push_fields` 仍钉它们**存在**，故本件不删、该档仍绿）；③ 全量 pytest 与 `e2e:sleep-report`（按需求方 2026-10-10「批次化共享」裁定，由编排器在**批次末**统一跑）。


## v125 → v126：`T-572` —— **删 `6002`（`sleep_no_data`）约定 ＋ 「该侧无数据」口径收敛为 `samples: []`**（`T-2` 裁定；阶段 I「协议规范冻结」追加件）（2026-10-10）

> **来源／依据**：需求方 **2026-10-10 裁定 `T-2`＝B** —— `6002` 想表达的「这一侧无数据」，**实现已经用「空 `samples` 数组」表达了** ⇒ 同一个意思有**两条表达通道**，而实现选了空数组 ⇒ **删掉 `6002` 的约定**，让**协议与实现一致**（本批先例：`A4`/`A6` 都是「承认现状、补进协议」）。
> ⚠️ **版本号口径**：核过本文件头部 —— 改前**实际最大号 ＝ `v125`**（`T-567` 占用）；`v126` 未被占用 ⇒ **本件落 `v126`、不顺延**；`v125` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。`protocol.js` 的 `version` 由 `3.4` ⇒ `3.5`（同批追加 `v3.5` changeLog 条目）。
> 📌 **任务书自证**：**6 项订正**（含 `smart-bed.js` 的 `6002` 实测在 `:48`／`:49`／`:389` 而非任务书给的 `:354`；`design/04-platform-api/sleep-data-pull-protocol-plan.md` 命中 4 行而任务书**未覆盖**）—— 见 `.pipeline/journal/T-572.journal.md` §0。

| 落点 | 改前 | 改后 |
|---|---|---|
| `data/iot/protocol.js` · `statusCodes.业务错误_6xxx` | `6001`／**`6002` `sleep_no_data`**／`6003`／`6004`（4 条） | 🔴 **删 `6002` 整条**（`code`／`meaning`／`description`／`producer`／`待定`）⇒ **3 条**（`6001`／`6003`／`6004`，**逐字未动**） |
| 同上 · 渲染页「业务错误 6xxx」表 | 4 行（含 `<tr><td>6002</td>…</tr>`） | **3 行**（同名行同批删除，使 `:728` 概述句「本页各表与权威表 `statusCodes` 逐值一致」**仍为真**） |
| 同上 · `version` / `changeLog` | `"3.4"` ＋ 顶部 `v3.4` 条目 | `"3.5"` ＋ 顶部**追加** `v3.5` 条目（含 `rule-0d` 取代注记指针：被删条目原文逐字落在**本条目**） |
| `data/iot/smart-bed.js` · `sleep_status_push.devices[].samples.desc` | 『快检序列。元素 {ts, body, heart, breathe}』 | 追加 🔴『**该侧无数据 ＝ `samples: []`**（空数组；这是「无数据」的**唯一**表达通道，⛔ **不给错误码** —— `error` 只承载**失败**）』 |
| 同上 · `sleep_status_push.devices[].error.desc` | 『单侧失败/无数据时出现：…**`6002`** `sleep_no_data`（…⚠️ 实现侧**当前无产出点**…）／**`6003`** …』 | 『单侧**失败**时出现：…**`6003`** `sleep_upstream_failed`（✅ 实现真产出）』＋『⚠️「该侧无数据」⛔ 不在此通道表达 —— 一律用 `samples: []`』 |
| 同上 · `sleep_status_push.notes` | 『单侧失败不阻塞：…一侧失败置该 devices[] 项为 error 对象…』 | 补『🔴 **单侧「无数据」不置 error** —— 该侧 `samples: []` 即为唯一表达』 |
| 同上 · 文件头变更块 | 末块 ＝ `v6`（`T-566`） | **追加 `v7`**（本条；含 `rule-0d` 取代注记：`samples.desc`／`error.desc` 旧文**逐字**）—— ⛔ `v6` 块**一字未改** |
| **实现侧注释**（`src/`，非本目录） | `src/server/modules/sleep_data/application/services.py:634`『…置 error 对象（code 6002/6003）…』 | 『…置 error 对象（code 6003）…』＋ 补『单侧「无数据」⛔ 不置 error —— 该侧 `samples: []` 即唯一表达（`T-572` / `T-2` 裁定）』 |
| `publish/**` 同步 | — | 按 `scripts/sync_design_to_publish.ps1` 镜像；核验读数见 `.pipeline/journal/T-572.journal.md` §4 |
| `CHANGELOG.md`（本文件） | — | v125 → **v126**（本条；V 号核过：改前文件头部实际最大号 ＝ `v125`） |

- 🔴 **`rule-0d` 取代注记 —— 被删条目原文逐字**（`data/iot/protocol.js` · `statusCodes.业务错误_6xxx.6002`；⛔ 以下为**原文**，未改一字）：

```text
        6002: { code: 6002, meaning: "sleep_no_data", description: "睡眠监测该侧无数据——`sleep_status_push.devices[].error` 承载的单侧语义（语义名依据本文件 `changeLog` v2.5 逐字「6002 无数据」）", producer: "⛔ 实现侧**当前无产出点**（`X-08`：`smart-bed.js` 的 `devices[].error` 描述声明 6002/6003，而实现只产 6003）。本件只把语义补进权威表；产出点落地须另立条目。", 待定: "产出点与触发条件（「哪一侧、在什么条件下算无数据」）未定 ⇒ 见 `T-566` 报告 §⑨。" },
```

- 🔵 **任务书预置分歧点 · 执行取向（⛔ 逐条给判据，非静默）**：① **不保留「预留码」** —— 需求方已裁「删掉它的约定」（语义**已有别的通道表达** ⇒ 留着＝协议声明却**永不兑现**）② **`6003` 不动** —— 它**有真实产出点**（`services.py:673/752/788`；✅ 实现真产出）③ **⛔ 不改实现行为** —— 本件是**协议口径收敛**，实现本就以空 `samples` 数组表达 ⇒ **现状即目标** ④ **渲染页「以下是全部状态码定义」无需再改** —— `T-566` 已把该句改为「本页各表与权威表 `statusCodes` 逐值一致…」；两表同步删 `6002` 后该句**仍为真** ⇒ ⛔ 不重复触碰。**四条预置取向均未偏离。**
- ⛔ **未做（进 `T-572` 报告 §⑨）**：`design/04-platform-api/sleep-data-pull-protocol-plan.md`（**⛔ 非本件写面、亦非禁面**）命中 **4 行**（`:152`／`:635`／`:661`／`:663`）—— 其中 `:635` 把 `6002` 定义为 **`sleep_data_not_found`**（与权威表 `sleep_no_data` **冲突的第三套口径**）⇒ 该计划稿的订正**须另立条目**（本件⛔ 不动，依 `rule-0d` 不擅自改写计划稿）；`CHANGELOG.md` 的 **3 处历史行**（`:40`／`:52`／`:1839`）与 `protocol.js` 的 `changeLog` 旧条目（`v3.3`／`v2.5`）同依 `rule-0d` **不改**（仅追加本条）。


## v124 → v125：`T-567` —— **RPC 语义补全（幂等回放／重试三档／per-device 串行）**（阶段 I「协议规范冻结」第 2 件；`R-029`）（2026-10-10）

> **来源／依据**：`R-029`（**条目号锚** ＝ `docs/requirements-backlog.md` 的 `R-029` 条目；修复设计 §2 第 2 批 ＋ `protocol-baseline-v3.3.md` §4；裁-03b／03c／03d／03e）｜诊断面 `defects-36.md` 的 `B8`/`B9`/`B10`/`P1`。🔴 依 `rule-0c`／`J7a`：⛔ 本册不写**未获批**的第 3 层工作区稿路径（原引用形态已移除；**改前本行原文逐字可回查 `09511aa2`**）—— `R-029` 条目／裁-03b～03e／`defects-36.md` 证据链**一条未删**。
> **战略意义**：`T-566` 冻结了「码 → 语义」的权威表；本件冻结「**调用语义**」——幂等（同一 `msg_id` 重发必须回放同一结果）／重试（三档、有界）／串行（同设备不交错）。三者都是**对接方按协议写就必须依赖**的行为，⛔ 不冻则设备端只能各猜一套。
> ⚠️ **版本号口径**：核过本文件头部 —— 改前**实际最大号 ＝ `v124`**（`T-566` 占用）；`v125` 未被占用 ⇒ **本件落 `v125`、不顺延**；`v124` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。
> 📌 **任务书自证**：**7 项订正** —— 5 项行号实测不符（RPC 章 `:414-444`⇒**`424-493`**、幂等章 `:390-399`⇒**`430-439`**、串行章 `:406-412`⇒**`446-452`**、重试三档⇒**`454-484`**、`_is_duplicate`/`_mark_seen` 同在 `:2949-2965`⇒**分处 `:2965-2973`/`:2975-2981`**）＋ 2 项口径订正。见 `.pipeline/journal/T-567.journal.md` §0。
> 🔴 **最要紧的一项订正（判据前置不成立）**：任务书称「本件会打破 4 档，四档全钉『重复 `msg_id` 只丢弃、不回 ack』」——**两重实测均否**：① 那 4 档**一行 ack 断言都没有**（它们钉的是「重复**仍被识别**」）；② **实现当前本就回 ack**（`iot_gateway.py` 在 `handle_uplink` 后**无条件** `_reply_for` ＋ `send`）。真正的缺陷是**回放不幂等**（`_reply_for` 对 `stream_voice_command` 每次现分配新 `stream_id`）。⇒ 本件**未改这 4 档任何断言**（⛔ 不为凑判据改测试）。

| 落点 | 改前 | 改后 |
|---|---|---|
| `data/iot/protocol.js` · `rpc.idempotency`（`B9`） | `mechanism` ＋ `flow` ＋ `conflictHandling`；**容量与窗口无明文** | 新增 **`cacheMaxSize: 100000`**（🔴 必须设上限，⛔ 不得无上限）＋ **`cacheTtlSeconds: 300`**（与 `envelope.deduplication.window` 的「建议 5 分钟」**同源**，依据 `flow[③]` 逐字『设置 TTL=DEDUP_WINDOW』）＋ `cacheMaxSizeRationale`／`cacheTtlRationale`（取值理由）＋ **`cacheScope`**（per-device，`connect` 须与去重窗口**同批清空**）＋ **`ackMsgIdPolicy`**（回放 ack **沿用被 ack 的 call 的 `msg_id`**，⛔ 不新分配；与设备端 C 库 `zre_envelope.c` 的 `ack->msg_id = call->msg_id` 同源）＋ `flow[②]` 补「⛔ 也不得重新分配任何副作用（如 `stream_id`）」 |
| 同上 · `envelope.deduplication`（**`P1`**） | `algorithm[②]`＝『② 收到 call 时检查: if call.msg_id in seen_messages → **丢弃重复消息，不回复**』 | **订正为幂等回放**（裁-03e：以幂等章为准）—— 新增 `idempotentReplay` 字段，`algorithm[②]` 改为「**不再重复执行业务逻辑**，改为**幂等回放** `RPC_RESULT_CACHE` 中该 `msg_id` 的 `{code, payload}`」，`algorithm[④]` 补「并把结果存入 `RPC_RESULT_CACHE`」。⚠️ `rule-0d`：**被改行原文逐字**已就地写进新行 ＋ `changeLog` v3.4 ① |
| 同上 · 信封字段 `urgent.desc`（`B8`） | 『紧急标记。为 true 时触发**无限重试**策略（见 §5.5.3），无论何种网络环境都必须送达』 | 『…触发**强重试**策略（见 `rpc.retry`）：**有限次，默认重试 3 次**（⛔ 原文为「无限重试」，理由与原文见 `changeLog` v3.4 ②）…**尽力**送达』。⚠️ `rule-0d`：原文逐字留痕于 `changeLog` v3.4 ② |
| 同上 · `rpc.retry.strategies[urgent]`（**`B8` 核心订正**） | `maxRetries: "unlimited"`；`description`＝『无限重试直到收到 ack 或设备被判定离线…』；`appliesTo`＝『…在**所有情况下都必须送达**…』 | 🔴 **`maxRetries: 3`**；`description`＝『**有限次强重试**：最多重试 3 次…3 次后仍无 ack ⇒ 标记失败（该次调用以 `1004 timeout` 收尾），⛔ 不再无限重发』；`appliesTo` 由『在所有情况下都必须送达』订正为『**尽力**送达』。**订正理由（⛔ 不静默改数）**：『无限重试』在丢包下会与上层「未收到 ack 就重新下发」**叠成指令风暴** —— 对 `emergency_stop` 类急停指令尤其危险（持续占信道、放大故障面，**反而降低送达确定性**）；有限次强重试（默认 3）＋ 幂等保护后「尽力送达」语义不变而**有界**。⚠️ `rule-0d`：三处原文逐字留痕于 `changeLog` v3.4 ② |
| 同上 · `rpc.retry.strategies[log].maxRetries`（`B8`） | `1`（与其自身 `description`『发送一次…**未收到不重试**』**矛盾**） | **`0`** —— 使 `maxRetries` ＝ **重试次数（不含首次发送）** 的明文语义在全三档成立；同时新增 `maxRetriesSemantics` 字段把该语义**逐字钉死**（此前未明文，正是 `log`/`command` 两档各按一种读法的根因） |
| 同上 · `rpc.retry` 新增字段 | — | `maxRetriesSemantics`（『**重试次数（不含首次发送）**⇒ 总发送次数 ＝ 1 ＋ maxRetries』）＋ `tierOf`（下行 call 档位判定：`urgent=true`⇒`urgent`；其余⇒`command`；⚠️ **`log` 档在本仓下行面如实声明为「无自然命中」**——`log` 档要求"不需要设备侧确认"，而 `rpc.model` 要求"每个 call 必须收到 ack"⇒ 该档仅**显式指定**时可达，⛔ 不虚构自然命中） |
| 同上 · `rpc.timeout`（**悬空引用补实**） | 🔴 **被两处引用却从未定义**：`retry.retryOnTimeout`（『call 发出后 timeout 内未收到 ack』）与 `statusCodes[1004].producer`（『在 `rpc.timeout` 规定时间内未收到 ack』） | **就地补定义**：`defaultSeconds: 10` ＋ `semantics`（发送方等 ack 的预算；超时 ⇒ 按档位重发、`msg_id` 不变；用尽 ⇒ 以 `1004 timeout` 收尾）＋ `perActionNote`（各 action 可自声明）。⛔ **未改**上述两处引用文本 |
| 同上 · `rpc.concurrent`（`B10`） | `description`（『按到达顺序**串行**执行』⇒ 排队）与 `deviceBusyResponse`（『正在执行前一个 call 时收到新 call → **返回 1003**』⇒ 拒绝）**同一 block 两半字面相反**，无收敛读法 | **新增 `gate` 子块就地收敛**（⛔ `description` 与 `deviceBusyResponse` 原文**一字未改**）：`inFlight: 1` ＋ `maxQueued: 8` ⇒ **有界串行**（容量内**排队** ⇒ 帧序严格串行；**超出容量 ⇒ `1003 device_busy`**）＋ `maxQueuedRationale`（为何不是 1：调用方**未必**实现"收到 1003 后重试"⇒ 容量过小会把『忙』退化成『静默丢指令』）＋ `releaseEvent`（ack 到达**或重试耗尽**都释放槽位，⛔ 不得让一条 call 永久占位）＋ `ackShape` ＋ `delivery`（🔴 `1003` **只发调用方／观测面**，⛔ **不发往设备**——该 call 从未下线缆）＋ `cacheInteraction`（被回绝的 call ⛔ 不得写入 `RPC_RESULT_CACHE`） |
| 同上 · `version` / `changeLog` | `"3.3"` | `"3.4"` ＋ 顶部**追加** `v3.4` 条目（含 `rule-0d` 取代注记：**被改文本的原文逐字** —— dedup `algorithm[②]`、`urgent.maxRetries`、`urgent.description`、`urgent.appliesTo`、信封 `urgent.desc`、`log.maxRetries`） |
| **实现侧**（`src/`，非本目录） | | `src/server/gateway/iot_gateway.py`：新增 `RPC_RESULT_CACHE`（`_rpc_result_get/put/clear_device`；`_reply_for` 命中即**回放**，⛔ 不再重新分配 `stream_id`；`connect` 与去重窗口**同批清空**）＋ 重试三档引擎（`_retry_tier_for`/`_arm_call_retry`/`_call_retry_loop`：重发同一 `raw` ⇒ `msg_id` 不变）＋ per-device 串行闸门（`_DeviceCallGate`/`_send_call_now`/`_resolve_outstanding_call`/`_release_gate`/`_emit_device_busy`；`transport_send` 拆出纯写 `_transport_write`）。⚠️ `protocol_adapter.py` **未改**（`ack.msg_id` 沿用 call 的约定原本已成立，见 `:213`） |
| **测试侧**（`tests/api/device_access/**`） | | ⛔ **未改判据前置点名的 4 档任何断言**（实测其并未钉"不回 ack"，见上）；`test_downlink_command_flow.py` 的 6 命令场景**订正为串行契约**（"逐条收 → 逐条回 ack"，旧写法"先收齐再统一回 ack"在串行下必然死锁），**断言一字未删**；新增 `test_rpc_semantics_t567.py`（幂等正控/反证、重试三档帧数、串行与 `1003`） |
| `publish/**` 同步 | — | 按 `scripts/sync_design_to_publish.ps1` 镜像；核验读数见 `.pipeline/journal/T-567.journal.md` §5 |
| `CHANGELOG.md`（本文件） | — | v124 → **v125**（本条；V 号核过：改前文件头部实际最大号 ＝ `v124`） |

- 🔵 **任务书预置分歧点 · 执行取向（⛔ 逐条给判据，非静默）**：① 缓存上限 **10 万条 ＋ 300s**（照行；常量具名＋注释）② ack 的 `msg_id` 沿用被 ack 的 call（**实测原本已如此**，本件零改动）③ `urgent` 次数 **3**（写成协议明文常量，⛔ 未引入新配置项）。⚠️ **一处偏离**：`DEVICE_CALL_QUEUE_DEPTH` 取 **8**（任务书未给该值）—— 判据见 `maxQueuedRationale`（容量 1 会把"忙"退化成"静默丢指令"，比不排队更坏）。**未偏离**其余三条。
- ⛔ **未做**：`X-24`/`X-27`/`X-29`（归属待需求方裁定；与本件无同一处）· 调用方（`device_control` 消费者）"收到 `1003` 后重试"的补偿逻辑（不在本件写面）· `1004`/`1005` 的**实际产出点**（本件只补 `rpc.timeout` 定义与 `1003` 产出点）· WS close 码表（`R-028`）· 设备端 C 库（`R-032`）· `docs/requirements-backlog.md` 等禁面。


## v123 → v124：`T-566` —— **错误码体系收敛 ＋ 协议自洽**（阶段 I「协议规范冻结」第 1 件；`R-031`）（2026-10-10）

> **来源／依据**：`R-031`（**条目号锚** ＝ `docs/requirements-backlog.md` 的 `R-031` 条目；修复设计 §1 的 `P5`/`P6`+`A3`/`B11`/`A2`/`A4`/`A6`/`A7`/`A9`/`B12`/`C9`/`C10`/`C11` ＋ `defects-36.md`）。🔴 依 `rule-0c`／`J7a`：⛔ 本册不写**未获批**的第 3 层工作区稿路径（原引用形态已移除；**改前本行原文逐字可回查 `e8321289`**）。**战略意义**：阶段 II 会新增协议内容，若「码 → 语义」的权威表不先冻结，新内容会按互斥的双表各写一份 ⇒ 本件的验收线 ＝「**权威表唯一 ＋ 引用面全对齐**」。
> ⚠️ **版本号口径**：核过本文件头部 —— 改前**实际最大号 ＝ `v123`**（`T-558` 占用）；`v124` 未被占用 ⇒ **本件落 `v124`、不顺延**；`v123` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。
> 📌 **任务书自证**：7 项订正（含 `P5` 实测为 **6 组**而非 4 组、`registered` 实测 **3 处**而非 2 处、渲染页逐字为「以下**为**全部状态码定义」）—— 见 `.pipeline/journal/T-566.journal.md` §0。

| 落点 | 改前 | 改后 |
|---|---|---|
| `data/iot/protocol.js` · `statusCodes` | 18 码（`1xxx`/`2xxx`/`4xxx`/`5xxx`/`9xxx`），**无 6xxx 段**；无「谁是权威」的明文 | **22 码**：新增 `业务错误_6xxx` 段（`6001` `sleep_binding_not_found` · **`6002` `sleep_no_data`** · `6003` `sleep_upstream_failed` · `6004` `sleep_range_invalid`）＋ 新增 `authority` 字段（**唯一权威表**：per-action 的 `errors[]` 只声明条件，码的语义一律以本表为准）＋ 新增 `命名空间` 字段（**信封码 ⇏ WS close 码**；`4001` 两义，close 码表归 `R-028`） |
| 同上 · `1003`/`1004`/`1005`（`B11`） | 有码、**无产出点** | 各补 `producer` 字段：平台侧 per-device 串行闸门（`1003`）／RPC 超时判定（`1004`）／调用中断（`1005`）；⚠️ **实现落地归 `R-029`**，本件只做协议侧声明 |
| 同上 · `4002` 描述（`C9`） | `角度超出允许范围（0-75）` | `角度超出允许范围（-180~+180）`（与 `smart-bed.js` 的 `-180~+180` 及实现 `device_control/api/router.py:602` 的 `lo=-180, hi=180` 一致） |
| 同上 · 新增顶层 `errorFallback`（`A2`） | — | 协议**内置兜底 action `error`** 的权威定义：`{"ver":1,"msg_id":<唯一非0>,"ts":<非0>,"type":"ack","action":"error","ref_id":<自指哨兵＝本帧 msg_id>,"code":1001}`；触发＝上行信封不可解析。⛔ `type` 不得改 `call`（既有判据钉 `type=="ack"`＋`code==1001`） |
| 同上 · 渲染页「状态码体系」章（`P6`/`A3`） | 只到 `9xxx`，且概述写「以下为**全部**状态码定义」（与表内容不符） | 补 `业务错误 6xxx（T-566 补）` 表 ＋ 补「兜底错误帧」条 ＋ **订正过时断言**（改为「本页各表与权威表 `statusCodes` 逐值一致…」并就地写明命名空间切分）；4xxx 行的 `（0-75°）` 同批订正为 `（-180~+180°）` |
| 同上 · `version` / `changeLog` | `"3.2"` | `"3.3"` ＋ 顶部**追加** `v3.3` 条目（含 `rule-0d` 取代注记：被改文本的**原文逐字**） |
| `data/iot/smart-bed.js` · 各 action 的 `errors[]`（`P5`） | 别名与权威表**互斥**（同一码两义）：`INVALID_PAYLOAD (1002)`×19 · `UNSUPPORTED_MODE (1002)` · `INVALID_VALUE (1002)` · `INVALID_CONFIGURATION (1003)` · `DEVICE_NOT_FOUND (1004)` · `UNAUTHORIZED (2001)` · `RATE_LIMITED (4001)` · `BT_NOT_AVAILABLE (4001)` · `DEVICE_ERROR (5001)`×5 · `SERVICE_UNAVAILABLE (5001)` | **37 处**逐处订正到权威表（35 处别名 ＋ 2 处 6xxx 大小写对齐）：全部改为权威表的 `meaning (code)` —— `invalid_envelope (1001)` · `invalid_params (4001)`（载荷/枚举/越界/不存在） · `device_busy (1003)` · `auth_failed (2001)` · `hardware_error (5001)` · `internal_error (9999)` · `sleep_binding_not_found (6001)` · `sleep_range_invalid (6004)`。⛔ **未新增任何码**（一律**并入既有码**）；文件头追加 `v6` 变更块（含 10 组旧文的**逐字取代注记**） |
| 同上 · `sharedEnums.action_name`（`A2`） | 52 值 | **52 业务值 ＋ 1 协议内置值 `error`**（⛔ 无 `endpoint_id`、无 endpoint 卡、非业务指令；⚠️ 设备端 C 库注册表尚未含本值 ⇒ 归 `R-032`） |
| 同上 · `statusDefinitions.device_lifecycle`（`A4`） | `imported/online/offline/fault/deactivated` | **新增 `unregistered`（设备已注销）**，且**逐字写明何时出现**（`deregister` API 后凭据作废、与 `deactivated` 的区别） |
| 同上 · `status_notification` 卡（`A6`） | 只写「设备 connect 后**必须立即推**」 | **补写反向拉取约定**：平台 `connect_ack` 后约 3s 主动下发 `status_notification_trigger`（`STATUS_TRIGGER_DELAY_SECONDS=3.0`）⇒ 设备**必须**实现响应路径；`status_notification_trigger` 卡同批互指 |
| 同上 · `connect` 卡（`C11`） | `registered`×3（`:86`/`:90`/`:102`） | `imported`×3 ＋ **取代注记**（旧文逐字保留在 `related_apis`）；依据：实现只认 `imported`（`models.py:62`、`orm.py:58`），全仓 `registered` 零命中 |
| 同上 · `wake_up.duration`（`C10`） | 字段定义无上限 | 字段定义补 **上限 86400 秒**（与实现 `router.py:540-547` `hi=86400` 对齐） |
| 同上 · `sleep_status_push.devices[].error`（`P6`） | `desc: "…（6002/6003）"` | 改引权威语义名（`6002 sleep_no_data`（⚠️ 无产出点，已就地登记）／`6003 sleep_upstream_failed`（✅ 真产出）） |
| `data/iot/event-identifier.js`（`B12`） | `// status_notification.readings[] / 独立上报` | 订正为 `sub_device_events[]` ＋ **取代注记**（原文逐字保留）；取「**删注释**」一侧（⛔ 不补字段——那会造一条无 producer 的上行通道） |
| 同上 · `domainInternal`（`A7`） | 4 个在实现中存在的事件类型**全无登记** | **补登记 4 个**：`DeviceCommand.DeviceRequirementCommand` · `PlatformCommand.SendTtsCommand` · `DomainMessage.AiVoiceStreamEnded` · `DomainCommand.SleepDataRequest`；`DeviceMessage.OutputBluetooth` **判死、不登记**并就地留「全仓无 producer」反证与**不删**理由（删事件类型属 `R-036` 写面） |
| **实现侧**（`src/`，非本目录） | | `src/server/gateway/iot_gateway.py`（`_error_ack` 兜底帧改协议内置形态，⛔ `msg_id:0` 已去）· `src/server/events/base.py`（`DeviceStatusReported` 删死字段 `status`/`readings`）· `src/server/seeds/maintenance.py`（种子同步去 `status`） |
| **事件契约镜像**（`design/05-…`，非本目录） | | `design/05-backend-architecture/data/event-contracts.js` 的 `DeviceMessage.DeviceStatusReported.fields` 同步删 `status`/`readings`（`gen_event_contracts.py --check-fields` 要求两侧逐条一致；先例 `T-159` 删 `ActionMassage.speed`） |
| `publish/**` 同步 | — | 按 `scripts/sync_design_to_publish.ps1` 镜像；核验读数见 `.pipeline/journal/T-566.journal.md` §5 |
| `CHANGELOG.md`（本文件） | — | v123 → **v124**（本条；V 号核过：改前文件头部实际最大号 ＝ `v123`） |

- 🔵 **机械读数**（`node _tmp/T-566/mapping-extract.mjs`；⛔ **非肉眼比对**，原始输出见 journal §1/§5）：
  `statusCodes` 基数 **22**（改前 18）；渲染页表格码基数 **22**（与权威表不一致项 **0**）；`smart-bed.js` `errors[]` **23** 块、「标签(码)」配对 **36** 处；实现信封码（排除 `seeds/**`）去重 **2**（`6001`·`6003`）＋ 关键字形态 **3**（`1001`·`6001` ＋ 文档串里的 close 码 `4401`，已核读剔除）；
  **三向差集全为空**：`smart-bed→statusCodes` ＝ **0** · `实现信封码→statusCodes` ＝ **0** · 同码不同义 ＝ **0 处 / 0 码**。
- 🔵 **判据读数**：`run_pytest_watchdog.py` 三档 ＋ `G-2` 五项 ＋ `sync -Check`（0 项）＋ `check_publish_content_parity.py --byte-exact`（rc=0）—— 逐条见 journal §5。
- ⛔ **未做**：`X-01`~`X-33`（待需求方裁定归属，`Y-1`）· WS close 码表（`R-028`）· per-device 串行闸门实现（`R-029`）· 文档数字 `N1`/`N2`/`N3`（`R-035`）· 设备端 C 库（`R-032`）· `scripts/check_pipeline_archive.py` 与 `scripts/retire_rows.py`（`T-564`/`T-565` 刚交付）。


## v122 → v123：`T-558` —— `PlayMusic` 卡补 **`tag_names`**（`anyOf` 由 **3 支扩为 4 支**）：把 `T-556` 加进实现的「人类可读标签名」写回设计契约（2026-10-10）

> **来源／依据**：`T-557` 停手件的**冲突②**（实测分叉：卡缺 `tag_names`、`anyOf` **3 支 vs 实现 4 支**）⇒ 编排器裁为 **B** 并另立本件。🔴 **根因是编排器自身的缺口**（已留痕）：`T-556` 为实现"避面"被收窄为「⛔ 不动 `design/**`／`publish/**`」，但它把 **`tag_names` 加进了实现** ⇒ 按 **`rule-0b`**，**设计卡 ＋ 本册 `CHANGELOG` ＋ `publish` 镜像**都是它的**连带件** ⇒ 收窄后**无人认领** ⇒ 卡与实现分叉（`P3` 一落地即抓到）。
> **性质**：**设计契约同步件**（本册**零实现改动** —— 实现已在 `T-556`（`8d834c7a`）定稿，`src/**` 一字未动）；按 **`rule-0b` 六步**执行。
> ⚠️ **版本号口径**：核过本文件头部 —— 改前**实际最大号 ＝ `v122`**（`T-555` 占用）；`v123` 未被占用 ⇒ **`v123` 空闲、本件不顺延**；`v122` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。

| 落点（本目录） | 改前 | 改后 |
|---|---|---|
| `data/ai-tools/tools.js` · `PlayMusic.openai.function.parameters.properties` | `{media_id, tag_ids, keyword, min_duration_sec, max_duration_sec, volume}` —— **无** `tag_names` | **新增** `tag_names`（`array of string`）：**人类可读的音乐标签名**（如「轻音乐」「钢琴」「助眠」「白噪音」），**直接填名字、⛔ 不要填 UUID**，由服务端按名解析成曲库标签 —— 与实现 `tools.py:513` **逐句同口径**。同批把 `media_id` 描述里的筛选条件枚举补全为 `tag_names/tag_ids/keyword`、并给 `tag_ids` 补上「⚠️ 这是 **UUID**，一般拿不到 ⇒ **优先用 `tag_names`**」（对齐实现 `:512`／`:514`）。⛔ **未动 `required`** —— 两侧**已经都是** `[]` |
| 同上 · `…function.parameters.anyOf` | **3 支**：`[media_id]` ／ `[tag_ids]` ／ `[keyword]` | **4 支**：**新增** `{"required": ["tag_names"]}`，**插在 `media_id` 之后、`tag_ids` 之前** —— **与实现 `tools.py:532-537` 的支序一致** |
| 同上 · `…function.description` | 「…（**筛选条件**：tag_ids/keyword/时长区间，或点名的 media_id）…没点名时把用户话里摘出的音乐标签/关键词交给音乐库，由音乐库用标签筛曲库…」 | 「…（**筛选条件**：**tag_names**/tag_ids/keyword/时长区间，或点名的 media_id）…没点名时把用户话里的**音乐标签名填 `tag_names`**（如「轻音乐」「钢琴」「助眠」）交给音乐库；由服务端**按名解析成曲库里的标签**再由音乐库用标签筛曲库、按匹配度选一首…」—— 与实现 `:508` 同口径（既有的「🔴 **用户不必给曲目 ID**」句**逐字保留**） |
| 同上 · `PlayMusic.notes` · ⚠️ **非任务书点名的第四处** | 止于 `T-555` 取代注记（其中把 `anyOf` 的枚举**逐字写作**「`media_id` ／ `tag_ids` ／ `keyword` 至少其一」） | **追加** `T-558` **取代注记**（旧句**一字未改**）：记 `anyOf` 已扩为 4 支、`tag_names` 的口径与实现落点，并**明写上一段 `T-555` 注记里的 3 支枚举已被本注记取代**。🔴 **为什么必须做**：不追加，同一张卡内会**留下与自身 `anyOf` 相反的旧枚举**（`rule-0d` 单一真相）。⛔ 本处**不改任何结构面** |
| `publish/**` 同步 | — | `data/ai-tools/tools.js` **逐字节**镜像到 `publish/api/data/ai-tools/tools.js`（**单件** `Copy-Item`，⛔ 不整体 `-Apply`；双侧 `Get-FileHash` 相等，见 `.pipeline/journal/T-558.journal.md`）；本册 `CHANGELOG.md` **整份逐字节**镜像到 `publish/api/CHANGELOG.md`（`ExtraMappingTable` 行） |
| `CHANGELOG.md`（本文件） | — | v122 → **v123**（本条；V 号核过：改前文件头部实际最大号 ＝ `v122`） |

- 🔵 **结构面机械读数**（`node _tmp/T-558/structural-readout.mjs`；⛔ **非肉眼比对**）—— 设计卡 ↔ 实现 `tools.py:505-539` **逐项相等**：
  `参数名集合` ＝ `["keyword","max_duration_sec","media_id","min_duration_sec","tag_ids","tag_names","volume"]`；
  `required` ＝ `[]`；`anyOf` 的 `required` 集合 ＝ `["keyword","media_id","tag_ids","tag_names"]`（4 支）。
- 🔵 **判据读数**（原始输出见 `.pipeline/journal/T-558.journal.md`）：`check_publish_content_parity.py`（rc=0）／`scripts/check_event_producer_subset.py`（rc=0）／`design/04-platform-api/check-viewer-render.mjs`（rc=0）＋ **规则 0b 三项** ＋ `scripts/sync_design_to_publish.ps1 -Check`（GREEN）。
- ⛔ **未改**：`PlayMusic` 的 `required` · 同册**其余任何工具卡** · `data/iot/**` · `src/**`（实现已在 `T-556` 定稿）· `tests/**`／`scripts/**`／`AGENTS.md`／`.agents/**`／`.pipeline/**` 账本（journal 除外）。

## v121 → v122：`T-555` —— `PlayMusic` 卡把「用户不必给曲目 ID」**从散文变成可执行契约**（`required` 放开 ＋ `anyOf` ＋ 筛选条件参数）＋ `Q11-U6` 的协议 `media_id` 类型口径订正（2026-10-10）

> **来源／依据**：`Q-011` 裁决 `U7`（需求方 2026-10-10 原话，正本 ＝ 清单③ 的 `D-` 区裁决留痕）＋ `Q11-U6`（裁决 (a)：改**协议卡**为 `string`）。⚠️ **本册刻意不写第 3 层工作区稿的路径**：裁决**正文**载于清单③ 的 `D-` 区，`design/**` 只许引用 `status: approved` 的第 3 层稿（晋级门 `J7a` 机械判红）—— 与本册 `v120`/`v119` 同款写法。
> **性质**：**设计契约订正件**（本册零实现改动）＋ **发布镜像同步件**；实现同批落在 `T-555`（`src/server/**`，见 `src/server/CHANGELOG.md` 的 `v141`）。
> ⚠️ **版本号口径**：核过本文件头部 —— 改前实际最大号 ＝ **`v121`**（`T-549` 占用）⇒ **`v122` 空闲、本件不顺延**；`v121` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。

| 落点（本目录） | 改前 | 改后 |
|---|---|---|
| `data/ai-tools/tools.js` · `PlayMusic.openai.function.required` | `["media_id"]`（**点名某首 ＝ 唯一入口**） | **`[]`** —— `media_id` 由**必填**放开为**可选**（裁决：「用户肯定不能被要求给 ID，这个是一定的」） |
| 同上 · `…function.parameters.anyOf` | **无**（"与 tag/category 等筛选条件二选一"只写在 `media_id.description` 的**散文**里 ⇒ 无机械约束；且实现侧只收 `media_id`） | **新增** `anyOf: [{required:[media_id]}, {required:[tag_ids]}, {required:[keyword]}]` —— 「点名某首 **或** 给筛选条件让音乐库选」**至少其一**（schema 层；⚠️ 执行侧另有两处显式拒绝） |
| 同上 · `…function.parameters.properties` | `media_id`（描述写「曲目 ID…与 tag/category 等筛选条件二选一」）／`volume` | `media_id` 描述改为「**可选**…与筛选条件 **至少给一个**」＋ **新增** `tag_ids`（array of string，标签词表 **id**）／`keyword`（string）／`min_duration_sec`（integer）／`max_duration_sec`（integer）—— 与实现 `tools.py` 的参数面**同批对齐** |
| 同上 · `…function.description` | 「AI 只给出「放什么」的意图（曲目 ID 或筛选条件）」 | 「…（**筛选条件**：tag_ids/keyword/时长区间，或点名的 media_id）；🔴 **用户不必给曲目 ID** —— 没点名时把用户话里摘出的音乐标签/关键词交给音乐库，由音乐库**用标签筛曲库、按匹配度选一首**（明显最高直接放；前几名差不多则在并列池中随机取一；**匹配度低也照放**）」（裁决第 2~4 条**逐条**写进模型可见面） |
| 同上 · `PlayMusic.notes` | 止于 `T-130-07` 取代注记 | **追加** `T-555` **取代注记**（旧句一字未改）：记裁决原话、「二选一」从散文变契约、实现落点（域 09 `SelectionService` 的标签筛选/打分/保底分**为既有**，`T-555` 新增**并列池随机** `TIED_MARGIN`；⚠️ 随机**只作用于首播/筛选选曲 `pick`** —— 换曲 `next` 保持确定性，其 5 条承重判据（`T-400`/`T-402`）本件**未放宽**，见 `src/server/CHANGELOG.md` 的 `v141`）、以及「schema 只是软约束 ⇒ 执行侧两处显式拒绝」 |
| `data/iot/smart-bed.js` · `play_online_media.envelope.payload.media_id`（`:461`） | `{ type: "uint64", required: true, desc: "平台分配的播放标识…" }` | `{ type: "string", required: true, desc: "…（🔴 T-555／Q11-U6：类型口径由 uint64 订正为 string —— 平台实现是 UUID 字符串…）" }` |
| `data/iot/smart-bed.js` · `play_online_media_control.envelope.payload.media_id`（`:480`） | `{ type: "uint64", … }` | `{ type: "string", …（同 U6 注记） }` —— 🔴 依据是**实现事实**：`media_files.id` 是 UUID 字符串（`src/server/modules/media_library/infrastructure/orm.py`），`events/base.py` 的 `PlayOnlineMedia.media_id` 同为字符串 ⇒ 卡与实现**同批对齐**（改卡，⛔ 不是改实现为数字 id） |
| `publish/**` 同步 | — | `data/ai-tools/tools.js` ＋ `data/iot/smart-bed.js` **逐字节镜像**到 `publish/api/data/…`（**逐件** `Copy-Item`；双侧 sha256 相等，见 journal）；本册 `CHANGELOG.md` **整份逐字节**镜像到 `publish/api/CHANGELOG.md`（`ExtraMappingTable` 唯一一行） |
| `CHANGELOG.md`（本文件） | — | v121 → **v122**（本条；V 号核过：改前实际最大号 ＝ `v121` ⇒ `v122` 空闲、本件不顺延） |

- 🔴 **本件**⛔ **未改**的**同域**连带面（**具名上报，防"顺手改宽"**）：① `data/iot/smart-bed-examples.js` 的两处示例仍是**数字** `"media_id": 123456789012345`（`:351`／`:371`）—— 与本条订正后的 `type: "string"` 不一致 ⇒ **不在**本件授权的触碰面内，**未改**，建议**另立一件**（或由编排器授权就地改）；② `data/iot/protocol.js:41` 的 `media_id` 字段说明仍写「**由设备在 ack 中返回**」—— 与同册 `smart-bed.js:473`（「ack 仅确认收到，无需返回 media_id」）及 `protocol.js:302`（`media_id_ownership`「由平台在 call 中下发」）**自相矛盾**，且与 `T-555` 的裁决无关 ⇒ **未改**，登记为待裁决项。
- 🔵 **判据读数**（原始输出见 `.pipeline/journal/T-555.journal.md`）：`check_publish_content_parity.py`（提交后 rc=0）／`check_event_producer_subset.py`（rc=0）／`design/04-platform-api/check-viewer-render.mjs`（rc=0）＋ **规则 0b 三项**（`check-viewer-file-render.mjs`／`check-viewer-data-consistency.mjs`／`check-design-asset-render.mjs`）＋ `sync_design_to_publish.ps1 -Check`（GREEN）＋ `scripts/check_ai_behavior_surface.py`（**rc=1** —— 实现侧触及 AI 行为面 ⇒ 按 `D-108` 挂 `-m real_ai` 真模型档，读数见 journal）。
- ⛔ **未改**：`data/ai-tools/tools.js` 的 `ControlMusic` 卡（控制类**仍必须**点名 `media_id`）· `data/iot/smart-bed.js` 的其余字段/枚举 · `src/**`（实现改动记入 `src/server/CHANGELOG.md` 的 `v141`）· `AGENTS.md`／`.agents/**`／`.pipeline/**` 账本（journal 除外）。

## v120 → v121：`T-549` —— `EventBus.publish` 的 **write-ahead 陈旧口径**在域 04 内部接口正稿与总览册的**最后三处**订正（承 `T-548`／`T-546`／`Q-023`）（2026-10-06）

> **来源**：`T-548` 执行者**中途上报的 A 项**（`T-548` 按「触碰面未授权」未改、如实交回）；编排器定位后立 **`T-549`**。
> **依据**：`Q-023` 设计正稿 §2/§3（⚠️ 刻意**不写**第 3 层工作区稿路径 —— `design/**` 引用未获批工作区稿会被 `check_design_boundary` 的 `J7a` 判红，先例 `T-537`／`T-545`／`T-548`）＋ 实现定稿 `src/server/events/event_bus.py`（`publish` 已**不再** `await self._store.append`；`submit` 非阻塞入队；队列满 `_degrade_to_sync`；`stop()` drain）。
> **性质**：**设计资产订正件（叙述面） ＋ 发布镜像同步件**；本册**零实现改动**（实现已在 `T-546`／`c4137e37` 定稿，`src/**` 一字未动）。
> ⚠️ **版本号口径**：核过本文件头部 —— 改前实际最大号 ＝ **`v120`**（`T-537` 占用）⇒ **`v121` 空闲、本件不顺延**；`v120` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。

| 落点（本目录） | 改前 | 改后 |
|---|---|---|
| `data/internal/07-event-infrastructure.js` · 文件头（`:7`／`:9`） | `版本：v2 | 日期：2026-08-01` ＋ 「v2 变更：publish 语义对齐 write-ahead（先持久化后投递，at-least-once）」 | `版本：v3 | 日期：2026-10-06` ＋ **新增 v3 变更段**（`publish` ＝ 校验 → **非阻塞入有界审计队列** → 立刻 fan-out → 返回 `event_id`；落库由后台 `EventAuditWriter` 按序完成；正常停机 `drain`；队列满降级为同步落库）＋ **明写被削弱的保证**（返回时该事件可能尚未落库；进程崩溃会丢队列中最近若干条审计事件）；原 v2 行**原文留痕**并就地标注「⛔ 已被 v3 取代」 |
| 同上 · `eventbus-publish.description`（原 `:129` ⇒ 现 `:134`） | 「**先写 EventStore 持久化（write-ahead——持久化成功后才继续）**，再对事件深拷贝后投递给所有匹配的订阅者」 | 「校验通过后**非阻塞入有界审计队列**（⛔ 不再先写 `EventStore` —— `T-546`／`Q-023` 已把落库从发布关键路径上摘除）…」＋ 落库由 `EventAuditWriter`／drain／队列满降级 ＋ **被削弱的保证**；`at-least-once` 处**明写「投递侧语义不变」** |
| 同上 · `eventbus-publish.business_logic.steps` ③（原 `:140` ⇒ 现 `:145`） | 「③ 调用 `EventStore.append` 持久化（write-ahead——持久化成功后才返回）」 | 「③ **非阻塞入有界审计队列**（⛔ 不再调用 `EventStore.append` —— 落库已与发布解耦…队列满时降级为同步落库）」——**步骤序号不变**（校验仍在 ①②、仍在关键路径上） |
| 同上 · `eventbus-publish.business_logic.post_effects[0]`（原 `:147` ⇒ 现 `:152`） | 「事件**已写入** event_store 表（持久化，write-ahead）」 | 「事件**已入有界审计队列**（⚠️ 返回时该事件**可能尚未**写入 event_store 表 —— 落库由后台 `EventAuditWriter` 按序完成；正常停机 drain 排空）」 |
| `../domain-design-overview.md` `:236` | 「`EventStore` **先写后发（write-ahead）**保证不丢」 | 「`EventBus.publish` 与 `EventStore` 落库**已解耦**（`T-546` 实现定稿 ／ `Q-023` 设计正稿 §2/§3）—— ⚠️ **被削弱的保证**：…」——🔴 **只改 write-ahead 那一句**；**同一行**的 `T-545` `outbox` 注记（「末句描述的是 Outbox 机制…以此为准」）**逐字未动**（`rule-0d`） |
| `publish/**` 同步 | — | `data/internal/07-event-infrastructure.js` **逐字节镜像**到 `publish/api/data/internal/`（单件 `Copy-Item`；双侧 sha256 相等，见 journal）；本册 `CHANGELOG.md` **整份逐字节**镜像到 `publish/api/CHANGELOG.md`（`ExtraMappingTable` 行） |
| `CHANGELOG.md`（本文件） | — | v120 → **v121**（本条；V 号核过：改前实际最大号 ＝ `v120` ⇒ `v121` 空闲、本件不顺延） |

- 🔵 **判据读数**：`check_er_protocol_enum_parity.py`（rc=0）／`check_event_producer_subset.py`（rc=0）／`check_publish_content_parity.py`（rc=0）＋ 规则 0b 三项 ＋ 三件门禁 —— 原始读数见本件 journal 与收口回执。
- ⚠️ **未改面（防"顺带改宽"，逐条实测）**：① `:159` 的 `side_effects`「event_store 表新增一条记录」**逐字未动**（"最终会新增"仍成立，且它不是返回时点的承诺）；② `:160` 的 dlq 行**逐字未动**（属**投递失败**路径，与 `publish`／落库解耦无关）；③ `publish_batch`（`:269` 起，`signature` 在 `:274`）**仍是 write-ahead**（`T-546` 范围外、`Q-023` §2.4 未覆盖，实现侧亦如此）⇒ 本条**未动**它。
- ⛔ **未改**：`src/**`／`tests/**`／`scripts/**`／`AGENTS.md`／`.agents/**`／`.pipeline/**` 账本（journal 除外）。

## v119 → v120：`T-537` —— 域 04 块叙述面订正：`DomainCommand.SleepDataRequest` 的**溯源已接通**（撤销「`ref_id` 判定 = 不补」＋「AI 侧无生产者」两条旧口径）（2026-10-06）

> **依据**：**`Q-022` 设计正稿 §3.3** ＋ **`Q-022` 裁决 3**（需求方 2026-10-03 撤销 `T-130-09d`／`D-59` A10 的「不补」判定，并**一并撤销「AI 侧无生产者」断言**；规制：**AI 是跨域事件的主要生产者**）。⚠️ **本册刻意不写第 3 层工作区稿的路径** —— `design/**` 只许引用 `status: approved` 的第 3 层稿（晋级门 `J7a` 机械判红），而本条的并入依据是**需求方裁决本身**（裁决留痕见清单③ 的 `D-` 区）。
> **性质**：**设计资产订正件（叙述面） ＋ 发布镜像同步件**；本册**零实现改动**（实现落在域 07 `events/base.py` docstring ＋ AI/睡眠域/网关三处代码，见 `src/server/CHANGELOG.md` 的 `v136`）。
> ⚠️ **版本号口径**：核过本文件头部 —— 改前实际最大号 ＝ **`v119`**（`T-531` 占用）⇒ **`v120` 空闲、本件不顺延**；`v119` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。

| 落点（本目录） | 改前 | 改后 |
|---|---|---|
| `data/rest/02-core-domains.js` · 域 04 块 `description` 的**事件字段级契约**句 | 「**`ref_id` 判定 = 不补**（AI 侧无生产者，且裁决 36.1「无 callback / 无 ack」⇒ 无请求可关联者本就该取网关缺省 `0`，补之即为死字段 —— `D-31`）」 | 「✅ **溯源已接通（`T-537` 取代注记）**：溯源载体 = **`Event` 根信封字段 `source_msg_id`**（`Q-022` W1 / `T-536` 上提；语义 = 承载该用户请求的协议上行 msg_id，`T-017`），AI 工具 `GetSleepReport` 执行时透传 ⇒ 域 03 handler 作 `ref_id=` 交给网关 ⇒ **三形式帧可溯源到该轮上行**」＋⛔ 撤销「AI 侧无生产者」断言 ＋ 明写「旧依据『无 ack ⇒ 该取 0』已不成立（溯源与 ack 无关，不得互推）」＋ 明写「事件条目 `fields` **不变**（`source_msg_id` 是信封字段）」 |
| `publish/**` 同步 | — | `data/rest/02-core-domains.js` **逐字节镜像**到 `publish/api/data/rest/`（单件 `Copy-Item`；双侧 sha256 相同，见收口回执）；本册 `CHANGELOG.md` **逐字节镜像**到 `publish/api/CHANGELOG.md` |

> ⚠️ **未改面（防止"顺带改宽"）**：域 04 块其余叙述（发布点、`GetSleepStatus` 不连带发布、字段依据两条）**一字未动**；本册**未**新增／删除任何 REST 卡。

## v118 → v119：`T-531` —— `G-432` 补卡：`rest-device-detail` 补声明实时 `action_status`／`fault_level`（**10 → 12** 键）＋ `rest-device-create`／`rest-device-update` 响应补声明**实时三键**（「**卡是权威**」⇒ 补卡，⛔ 不是改实现少回）（2026-10-05）

> 来源＝挂账 **`G-432`** ＋ `T-528`（`G-418` 实现轮，2026-10-04）实测：`_device_to_dict(d, realtime)` 把 `realtime` 改为**必填**并回**实时三键**（`connection_status`／`action_status`／`fault_level`）⇒ 详情／创建／更新三端点**必然连带**多出这三键。
> 🔴 **方向（卡是权威）**：卡是契约、实现是履约 ⇒ 实现**超前于卡**时**补卡**；⛔ **不得**为了让卡“对上”而改实现少回（`T-528` 已判明：留默认值会回 `connection_status: None` 这类**比缺字段更糟**的假值）。本件为**设计资产件 ＋ 发布镜像同步件**（⛔ **零代码改动**：`src/**` 一字未动）。
> ⚠️ **本件⛔ 未改判据、⛔ 未动棘轮**：`check_rest_response_field_subset.py` 的三张表（声明面 **58**／被判面 **43**／未解析面 **15**）**读数不变**（本件只**补充**既有卡已在其声明面内的字段名，⛔ 未新增／删除卡）；`KNOWN_DRIFT` **维持 `T-528` 后的空表**（⛔ **未把它加回来**）。

| 落点（本目录） | 变更（改前 ⇒ 改后） |
|---|---|
| `data/rest/01-device-iot.js` · 卡 `rest-device-detail` 的 `response.body` | 改前 **10** 键（9 静态 ＋ `connection_status`）⇒ 改后 **12** 键（＋ `action_status: "enum"`、`fault_level: "enum"`） |
| 同上 · 卡 `responses[].json` 样例 ＋ `responses[].fields` | 样例补 `"action_status": "idle"`／`"fault_level": "none"`；`fields` 补同名两项（含语义与取值，与列表项 `items[].action_status`／`items[].fault_level` **同口径**） |
| 同上 · 卡叙述面（`description`／`business_logic.steps` ③④／`post_effects`） | 改前「＋ 实时 `connection_status`」／③「聚合实时**连接状态**」／`post_effects`「扁平 ＋ **连接状态**」⇒ 改后写明**实时三键**、合计 **12** 键、与列表项 `_decorate` **同源**（`T-528`）—— ⚠️ 六步第 2 条「同一结论散布多处必须一起改（防漂移）」：只改 `response.body` 会留下卡内自相矛盾 |
| 同上 · 卡 `rest-device-create` 的 `response.body` | 改前 `{ id: "UUID" }` ⇒ 改后 `{ id, connection_status, action_status, fault_level }`（＋ `responses[].fields` ＋ 示例 JSON 同步） |
| 同上 · 卡 `rest-device-update` 的 `responses[].fields` | 改前 **9** 键（扁平设备 dict）⇒ 改后 **12** 键（＋ 实时三键）＋ 示例 JSON 同步。⚠️ **本卡 `response` 块本就无 `body` 键**（声明面取自 `responses[].fields`）⇒ 按该卡**既有形态**只同步 `fields`／示例 JSON，⛔ **未新增 `body`**（改声明来源属另一动作）；`description` 就地写明该口径 |
| `publish/**` 同步 | `data/rest/01-device-iot.js` → `publish/api/data/rest/01-device-iot.js`、`CHANGELOG.md`（本文件）→ `publish/api/CHANGELOG.md` **各单件 `Copy-Item`**（双侧 SHA256 一致）；🔴 **⛔ 未跑 `sync_design_to_publish.ps1 -Apply`** |
| `CHANGELOG.md`（本文件） | v118 → **v119**（本条；V 号核过：改前实际最大号 ＝ `v118` ⇒ `v119` 空闲、本件不顺延） |

- 🔵 **判据读数**：`check_rest_response_field_subset.py` 默认档 **rc=0**（`✓ 绿：被判 43 张卡全部满足『声明 ⊆ 实现』`）／`--strict` **rc=0**（漂移 **0** 张 —— `KNOWN_DRIFT` 空表维持）／`--self-test` **18/18**（含 `T-418` 负控 ④a／④b／④c）；`pwsh -File scripts/sync_design_to_publish.ps1 -Check` ＝ **GREEN**；规则 0b 三项 ＋ 三件门禁读数见本件 journal 与收口回执。
- ⚠️ **未改（同族面，实测在、⛔ 非本件点名面 —— 只登记不顺手改）**：① `rest-device-create` 的**实现响应还含 `device_secret`**（`return {**_device_to_dict(...), "device_secret": d.device_secret}`）—— 该卡 `responses[].fields` **只声明 `id` ＋ 本件补的实时三键**，`device_secret` **未声明**（该卡在判据里是 `unresolved:nonstr-key` ⇒ **不判**，故⛔ 无红）；本件按任务书「补实时三键」的面执行、⛔ 未扩面 ⇒ **点名上报待裁**（补声明与否属另一裁决）。② `rest-device-update` 的 `body_params` 仍声明 `name`（设备表无 `name` 列、实现忽略该键）—— `T-411` 已具名登记，本件未动。
- ⛔ **未改内容**：本条为**追加**；`v118` 及更早条目**逐字保留**。⛔ 未改 `src/**`／`tests/**`／判据脚本／棘轮表／`release/environments.yaml`。

## v117 → v118：`T-520` —— ① 建档/导入卡补**入口分叉显式警告**（`D-190`① 取甲）＋ ③ 登记 `409` 认可（实现已在 `T-519` **在位**）＋ ④ **同步 `G-409`**（heartbeat 方向口径两处复核 ＋ `publish` 单件镜像）＋ ② 订正 `domain-design-overview:424` 一格（2026-10-03）

> 来源＝需求方 **2026-10-03 逐条裁定 `D-190`**（① 入口分叉取甲；② 订正 `domain-design-overview:424` 一格；③ 认可 `409`；④ 授权同步 `G-409`）。本件为**设计资产件 ＋ 发布镜像同步件**（⛔ **零代码逻辑改动**）。
> 🔴 **① 的动机（`D-190`①）**：建档/导入入口**仍写入一把平台生成的缺省准入密钥**（既有行为）⇒ 若不显式警告，运营会**看到接口回 200 就以为通了**；设备真钥出厂自带且每台唯一 ⇒ **登记前该设备连不上**（签名必对不上）。

| 落点 | 变更（改前 ⇒ 改后） |
|---|---|
| `data/rest/01-device-iot.js` · 卡 `rest-device-create` 的 `description` | 追加**显式警告**：本端点（建档入口）写入的 `device_secret` 是**平台生成的缺省准入密钥**（既有行为）—— ⚠️ **它是占位、⛔ 不是设备可用凭据**；真钥出厂自带且每台唯一 ⇒ **必须**经「登记新硬件出厂密钥」登记，🔴 **登记前该设备连不上**（签名必对不上） |
| 同文件 · 卡 `rest-device-import-execute` 的 `description` | 同上（导入入口，新建的每台设备须逐台登记） |
| 同文件 · 卡 `rest-device-secret-rotate` 的 `errors` 的 `409` 行 | ⛔ **本件未改** —— `T-519` **已在位**：`["409", "CONFLICT (40901)", "设备已注销（unregistered）—— 资源状态不允许该动作（D-188-补；⛔ 不借登记密钥复活）", "资源状态冲突"]`，且两条**分离正控**（已注销＋非法形态仍 409／未注销＋非法形态 400）已在 `:1206`／`:1211`／`:1212` 写清 ⇒ **③ 点名「已在位」**，⛔ 不重复 |
| `data/iot/protocol.js` · 渲染卡「心跳与重连 → 应用层心跳」（`:594`） | 🔴 **两处方向口径复核后的订正**：① **主句方向本就正确**（服务端下发 call、设备回 ack），但插入语「（含 ts 用于时钟同步）」把同步依据挂在 **ack** 上 ⇒ 改为「ack 的 ts 是**设备自身本地时间**，仅用于服务端关联与判活」；② 「时间同步：设备提取 `heartbeat_ack.ts`」🔴 **方向错**（ack 由设备发出，其 ts 是设备本地时间）⇒ 改为「设备提取 heartbeat **call 信封**的 ts 字段（即服务端毫秒时间戳）」 |
| `publish/**` 同步 | `data/iot/protocol.js` → `publish/api/data/iot/protocol.js`、`data/rest/01-device-iot.js` → `publish/api/data/rest/01-device-iot.js`、`design/02-business-workflow/data/01-device-iot.js` → `publish/workflow/data/01-device-iot.js` **各单件 `Copy-Item`**（双侧 SHA256 一致）；`CHANGELOG.md`（本文件）按 `ExtraMappingTable` 口径**整份逐字节**镜像到 `publish/api/CHANGELOG.md`。🔴 **⛔ 未跑 `sync_design_to_publish.ps1 -Apply`** |
| `../domain-design-overview.md` · §5.1 落点表 ②「准入的设置」的**缺口格**（`:424`） | 🔴 **口径订正（`D-190`②；⛔ 不是「缺失」）**：改前「…但**没有任何可配置载体**：无准入策略实体、无白名单页、无认证模式开关、无密钥管理页…」⇒ 改后「① **白名单、准入策略实体、认证模式开关 —— 是「设计上已取消」**（需求方原话：「只拿设备和密钥去进行设备准入的控制就可以了」）；② **准入控制方式 ＝ 设备身份（`product_model` ＋ `device_id`）＋ 密钥（`device_secret`，HMAC-SHA256）**；③ ✅ **设备密钥管理页与对应端点 —— 已有**（如实登记：`POST /devices/{id}/secret`／`GET /devices/{id}/secret` ＋ 后台「设备密钥」弹窗）」；⚠️ **保留**原句对**合同附录 1.1.1** 的引用关系；⛔ 未写成「完全满足合同要求」 |
| `CHANGELOG.md`（本文件） | v117 → **v118**（本条；V 号核过：改前实际最大号 ＝ `v117` ⇒ `v118` 空闲、本件不顺延） |

- ⛔ **未改（逐条，实测）**：① `design/01-raw-input/**`（原始需求留档的旧口径是**历史事实**）；② 同文件卡 `rest-device-import-preview`（**只校验、不写密钥** ⇒ 无需该警告，已逐件判过并点名）。
- ⚠️ **镜像面逐件判定**：`design/domain-design-overview.md` **无 publish 镜像**（不在映射表、不在任何 viewer 的 `data/` 面）；`design/02-business-workflow/data/CHANGELOG.md` **不在映射表内**（`data/*.js` glob 不匹配 `*.md`）；唯 `design/04-platform-api/CHANGELOG.md` 例外（`ExtraMappingTable` 行）。
- 🔵 **判据读数**（复核结论逐字 ＋ `-Check` 0 项 ＋ `--byte-exact` rc=0 ＋ 双侧 sha256）见本件 journal 与收口回执。

## v116 → v117：`T-519` —— 设备密钥端点按**真实业务语义**收窄：**登记新硬件出厂密钥**（端点只受理人工录入，自动生成**下线**）＋ `device_secret` 形态/归属口径回改 ＋ 已注销 ⇒ **409**（承需求方口述 ＋ 裁定 `D-188-补`）（2026-10-03）

> 来源＝需求方 **2026-10-03 口述** ＋ 裁定 **`D-188-补`**（★ 本件的权威依据）。本件为**设计资产变更 ＋ 实现件**同批：实现落点 ＝ `src/server/modules/device_iot/{application/services.py, api/router.py}` ＋ `src/server/gateway/device_auth.py`（**仅 docstring**）。
> 🔴 **真实语义（`D-188-补`）**：`device_secret` **出厂烧录、每台唯一、设备侧⛔ 不可改写**；**可改写的是 `device_id`** ⇒ 板子坏 ⇒ 新板写旧 ID ⇒ 新板拿「旧 ID ＋ **新板自己那把出厂密钥**」来连 ⇒ 平台侧**把该逻辑设备登记的密钥改成新板那把**，使新板**顶替旧板以同一 ID 运行**。⛔ **不是** crypto 意义的“轮换”。
> 🔴 **为什么必须收窄（改前 ＝ 会把逻辑设备变成连不上的死号）**：`T-518`/`D-188` `Q2` 的「不带 `secret` ⇒ 服务端自动生成」**没有业务依据** —— 平台生成的值**设备不认识**（设备拿自己烧录的密钥算签名）⇒ 该逻辑设备**再连不上**，而接口仍回 **200**、密钥正常回显，**运营看不出哪里错了**。「重置 ＝ 自动生成」**裁定下线** ⇒ 端点只保留**人工录入**。

| 落点（本目录） | 变更（改前 ⇒ 改后） |
|---|---|
| `data/iot/protocol.js` · `glossary.device_secret`（`:36`） | 改前：「设备密钥，32字节二进制值，设备接入平台前写入；🔴 **设备侧可改写**（换钥）——订正原「写入并固化」」⇒ 改后：「**64 字符十六进制文本；其 UTF-8 字节即 HMAC 密钥（对应 32 字节熵）**（依据 `device_auth.py` 实测 `device_secret.encode("utf-8")`）。🔴 **出厂烧录、每台唯一、设备侧⛔ 不可改写** —— **恢复原「设备接入平台前写入并固化」口径**（`D-188-补`：⛔ 订正 `T-518` 按错误口径写入的「设备侧可改写」；🔴 **可改写的是 `device_id`**）」 |
| 同文件 · `identity.credentials[]` 的 `device_secret` 行（`:186`） | `format` 由「32字节二进制值」⇒ **「64 字符十六进制文本；其 UTF-8 字节即 HMAC 密钥（对应 32 字节熵）」**；`assignedBy` 由「…🔴 **设备侧可改写**（换钥）；平台侧在设备登记后**可生成**或由运营录入新值」⇒ **「设备接入平台前写入并固化（出厂烧录、每台唯一，设备侧⛔ 不可改写）；平台侧在设备登记后**登记/录入该硬件出厂的既有密钥**（⛔ 平台侧不生成）」** |
| `data/rest/01-device-iot.js` · 卡 `rest-device-secret-rotate` | 🔴 **收窄为「登记新硬件出厂密钥」**：`summary` 明确**只受理人工录入**；`scenario` 改写为「① **硬件置换/顶替（主场景）**（旧板坏 ⇒ 旧 ID 写进新板 ⇒ 新板带**自己那把出厂密钥**连不上 ⇒ 运营在本弹窗录入新板出厂密钥）② 台账核对/补录」，⛔ **删掉原「设备密钥遗失/疑似泄漏 ⇒ 一键自动生成」那条**；`description` 写明「平台侧⛔ 不生成」及其危害形态；`body_params.secret` 由「否」⇒ **「是」**（缺失/为空 ⇒ 400，⛔ 无缺省）；`errors` 由「400（含已注销）」⇒ **拆为 400（参数类）＋ 409（资源状态：已注销）**；`responses[].fields` 与 `response.body` **删除 `input_mode`**（单一输入方式 ⇒ 该字段失去信息量，留着等于暗示仍有另一条输入路径）；`request.body.secret` 由 `"string?"` ⇒ `"string"` |
| 同文件 · 卡 `rest-device-secret-read` | 措辞随新语义订正（「换钥响应」⇒「**登记新硬件出厂密钥的响应**」；`scenario` 由「与设备侧实际写入的值核对」⇒「与**新硬件出厂时烧录的密钥**核对/抄录」，并点名置换场景）；⛔ 契约字段与红线**一字未改** |
| `publish/**` 同步 | `data/rest/01-device-iot.js` **定点逐字节**拷贝到 `publish/api/data/rest/01-device-iot.js`。🔴 **本件 ⛔ 不跑 `sync_design_to_publish.ps1 -Apply`**：本仓存在**既有分叉** `publish/api/data/iot/protocol.js`（`G-409`）—— ⚠️ **如实登记**：本件改了 `design` 侧 `data/iot/protocol.js` ⇒ 该分叉**范围变大**，`publish` 侧那件**仍按授权面不覆盖**（`G-409` 处置须需求方逐次授权）。`CHANGELOG.md`（本文件）按 `ExtraMappingTable` 口径**整份逐字节**镜像到 `publish/api/CHANGELOG.md` |
| `CHANGELOG.md`（本文件） | v116 → **v117**（本条；V 号核过：改前实际最大号 ＝ `v116` ⇒ `v117` 空闲、本件不顺延） |

- 🔴 **可测断言（本件承重）**：`tests/api/device_iot/test_device_secret_api.py`（**硬件置换登记**／**自动生成下线（不带 `secret` ⇒ 400）**／旧钥立即失效／在线设备不受影响／`deregister` ⇒ **409**／读路径／权限／留痕不含值／**反证**）。
- 🔵 **判据读数**：见本件收口回执（定向 pytest ＋ 规则 0b 三项 ＋ 静态门禁复扫）；⚠️ **卡数/字段数棘轮核实**：卡数 **58 不变**（本件**不加不减卡**）、实现端点数 **225 不变**、`rest-device-secret-rotate` 仍在被判面内 ⇒ `check_rest_response_field_subset.py` 的三张棘轮表**无需改动**（⚠️ 若有变动须同批更新，⛔ 只记账不放宽判据）。
- ⛔ **未改（同族冲突面，实测在、但⛔ 不属本件授权面 —— 只登记不顺手改）**：① `design/01-raw-input/**`（原始需求留档的旧口径是**历史事实**）；② `design/domain-design-overview.md:424` 一行的「⛔ **无密钥管理页**」**已部分过期**（`T-518`/`T-519` 的凭据端点与后台弹窗已落地）—— 本件按任务书「**只补一句、⛔ 不要大改**」的口径**未动该格**，点名上报。

## v115 → v116：`T-518` —— 设备密钥**换钥**（一个端点 · 两种输入方式）＋ **明文存储**口径订正（承需求方裁决 `D-188`）（2026-10-02）

> 来源＝需求方 **2026-10-02 逐条答复 `D-188`**（`Q-021` §11 的 `Q1`~`Q9` ＋ 存储口径）。本件为**设计资产变更 ＋ 实现件**同批：实现落点 ＝ `src/server/modules/device_iot/{application/services.py, api/router.py}` ＋ `src/web-admin/src/views/device-iot/DeviceManagement.vue`。
> 🔴 **为什么必须改（改前 ＝ 设计与实现/需求方口径三面互斥）**：① 协议明文写 `device_secret`「设备接入平台前写入并**固化**」＋「**云端加密存储**」＋「**绝不以明文出现在网络传输**」，而实现是**云端 `secrets.token_hex(32)` 现生成、明文直存**、需求方又要求**页面能看到当前密钥值**（`D-188` `Q6`）⇒ 三处同句冲突；② 业务面写「**不在云端动态生成**」，与实现（`services.py:122`）**正面矛盾**（`D-188` `Q1` 已裁定「设备层密钥**可以改写**」）。
> 🔴 **版本号口径（⛔ 不得回改）**：改写 **`v115 → v116`**。⚠️ 核过本文件头部：**改前实际最大号 ＝ `v115`**（`T-514` 占用，见 `:4`）⇒ **`v116` 空闲、本件不顺延**。`v115` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。

| 落点（本目录） | 变更（改前 ⇒ 改后） |
|---|---|
| `data/iot/protocol.js` · `glossary.device_secret`（改前 `:36`） | 改前：「设备接入平台前写入并**固化**。**绝不以明文出现在网络传输**、日志或错误消息中」⇒ 改后：「设备接入平台前写入；🔴 **设备侧可改写**（换钥）——订正原「写入并固化」（`D-188` `Q1`）。设备链路（WebSocket / MQTT）上绝不以明文出现；管理端 REST 的**凭据变更/读取**响应在 TLS 通道内返回明文（`D-188` `Q6`）；⛔ 绝不以明文出现在日志或错误消息中」 |
| 同文件 · `identity.credentials[]` 的 `device_secret` 行（改前 `:186`） | 改前：`assignedBy` ＝「设备接入平台前写入并固化」／`purpose` ＝「…**云端加密存储**。**绝不以明文出现在网络传输**、日志或错误消息中」⇒ 改后：`assignedBy` ＝「…🔴 **设备侧可改写**（换钥）；平台侧在设备登记后可生成或由运营录入新值」／`purpose` ＝「🔴 **云端明文存储**（Token 口径，`D-188`）——订正原「云端加密存储」；设备链路上绝不以明文出现，管理端 REST 凭据响应在 TLS 内返回明文（`Q6`）；⛔ 绝不写入日志或错误消息」 |
| `data/rest/01-device-iot.js` · **新增卡** `rest-device-secret-rotate`（`POST /api/v1/devices/{id}/secret`） | 🔴 **一个端点、一个动作、两种输入方式**（`D-188` `Q2`：⛔ **不拆**「轮换/重置」两个动作）：`body` **带 `secret` ⇒ 人工输入**（`input_mode="manual"`）／**不带 ⇒ 服务端自动生成**（`input_mode="auto"`）；🔴 **旧钥立即失效**（`Q3`）⇒ ⛔ 无宽限窗口、⛔ 无 `previous_secret_valid_until`、零迁移；权限**复用 `device.write`**；⛔ 不限频（`Q9`）；⛔ 不批量（`Q8`）；`idempotency.is_idempotent = false`（显式声明非幂等）；防呆：新值＝当前值 ⇒ 400；已注销设备 ⇒ 400 拒绝（fail-closed，⛔ 不复活） |
| 同文件 · **新增卡** `rest-device-secret-read`（`GET /api/v1/devices/{id}/secret`） | `D-188` `Q6`「我们只需要知道它当前是啥」⇒ 读路径返回 `device_secret`（明文）；🔴 **凭据读权限 ＝ `device.write`**（⛔ 不是 `device.read`）；**已注销设备的明确定义**：恒回 `device_secret: null`，⛔ 不为其回填/生成。⚠️ **破例面收窄**：`rest-device-list` / `rest-device-detail` / `rest-device-export` 的「**逐字不含密钥**」红线**一字未动** |
| `publish/**` 同步 | `data/iot/protocol.js` 与 `data/rest/01-device-iot.js` 两件**定点逐字节**拷贝到 `publish/api/…`。🔴 **本件 ⛔ 不跑 `sync_design_to_publish.ps1 -Apply`**：本仓存在**既有分叉** `publish/api/data/iot/protocol.js`（`G-409`）—— ⚠️ **如实登记**：本件改了 `design` 侧的 `data/iot/protocol.js` ⇒ 该分叉**范围变大**，`publish` 侧那件**仍按授权面不覆盖**（`G-409` 的处置须需求方逐次授权）；另一件 `01-device-iot.js` 双侧 sha256 一致（见收口回执）。`CHANGELOG.md`（本文件）按 `ExtraMappingTable` 口径**整份逐字节**镜像到 `publish/api/CHANGELOG.md` |
| `CHANGELOG.md`（本文件） | v115 → **v116**（本条） |

- 🔴 **可测断言（本件承重）**：`tests/api/device_iot/test_device_secret_api.py`（人工换钥／自动换钥／旧钥立即失效／在线设备不受影响／`deregister` 边界／读路径／权限／留痕不含值／反证）。
- 🔵 **判据读数**：见本件收口回执（定向 pytest ＋ 规则 0b 三项 ＋ `check_rest_response_field_subset.py`）；⚠️ `check_rest_response_field_subset.py` 的**卡数棘轮**随本件 ＋2 卡同步更新（`T-420`/`T-421` 同族口径：卡与实现**同批落地** ⇒ 该卡进**字段级被判面**，⛔ 不是放宽棘轮）。
- ⛔ **未改（同族冲突面，实测在、但⛔ 不属本件授权面 —— 只登记不顺手改）**：① `design/01-raw-input/**`（原始需求留档，`设备通信协议规范.md:228`／`01-device-iot.md:99` 的旧口径**是历史事实**，⛔ 不回改）；② `design/domain-design-overview.md:56`（「`device_secret` 出厂烧录、云端不动态生成」）与 `:424`（「出厂烧录 `device_secret`（AES-256-GCM 存储）」）—— **同族冲突第 3、4 处**，非本件点名面；
- ⚠️ **本件未覆盖、须有人拍（`LL-055` 点名）**：① 协议同句写「**32字节二进制值**」，而平台实现是 `secrets.token_hex(32)` 的 **64 位十六进制文本**（HMAC 的 key ＝ 该文本的 UTF-8 字节）⇒ **「人工输入」的合法形态只能钉在实现口径上**（本件实现按 64 位十六进制校验），协议那半句**未动**；② `design/06-web-admin/data/tree.js` 的**页面功能点**未同步（`design/06-web-admin/**` ⛔ 不在本件触碰面）。

## v114 → v115：`T-514` —— 数值类模糊**分界**写准（`D-186` / `P-069` 取 C）：**有相对量词且目标唯一** ⇒ 相对调整 ＋ **如实说明调到了多少**；**数值语义无法判定** ⇒ 先反问（2026-10-02）

> 来源＝需求方 **2026-10-02 裁决 `D-186`**（`P-069` 取 **C**）。本件为**设计资产变更 ＋ 实现件**同批：实现落点 ＝ `src/server/modules/ai_agent/domain/device_context.py` 的 `TOOL_CALLING_RULES`。
> 🔴 **为什么必须改（改前 ＝ 设计资产自相矛盾）**：本册 `data/ai-tools/tools.js` 的数值条写「用户指令**模糊**（如「调高一点」）⇒ 参考当前状态值做相对调整」（＝**猜着调**），而 `design/05-backend-architecture/data/ai-conversation-pipeline.js` 的 `clarificationFlow` 写「模糊 ⇒ **先问、不得猜测**」（＝**先问**）—— 两册对**同一类输入**给出互斥口径；实现 `device_context.py:427` 站在「猜着调」一侧，而 `T-513` 刚加的那句排除（「数值/程度类……不属本条」）**过宽**。`D-186` 取 C ⇒ 按「**可否安全推断**」分层：**有相对量词且目标唯一**（可安全推断 ⇒ 相对调整，⚠️ 必须如实说明调到了多少）／**对象或指代不清 ＋ 完全没听明白**（⇒ 先问）／**数值语义无法判定**（⇒ 先问）。
> 🔴 **版本号口径（⛔ 不得回改）**：改写 **`v114 → v115`**。⚠️ 核过本文件头部：**改前实际最大号 ＝ `v114`**（`T-503` 占用，见 `:4`）⇒ **`v115` 空闲、本件不顺延**。`v114` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。

| 落点（本目录） | 变更（改前 ⇒ 改后） |
|---|---|
| `data/ai-tools/tools.js` · `systemPrompt.sections` 的数值条（改前 `:1598`） | 改前：「如果用户指令模糊（如「调高一点」），参考当前状态值做相对调整」⇒ 改后：「如果用户指令**有相对量词且目标唯一**时（如「调高一点」），参考当前状态值做相对调整，**并如实说明调到了多少**」 |
| `data/ai-tools/protocol.js` · 完整调用样例的 system prompt 正文（`:101` 的超长 content 内，按内容定位） | 改前：「- 用户指令模糊时参照当前状态做相对调整」⇒ 改后：「- 用户指令**有相对量词且目标唯一**时参照当前状态做相对调整，并如实说明调到了多少」（与上条**同口径对齐**） |
| `publish/**` 同步 | `data/ai-tools/tools.js` ＋ `data/ai-tools/protocol.js` 两件**定点逐字节**拷贝到 `publish/api/data/ai-tools/`（⛔ **未用 `-Apply` 整批**：`publish/**` 存在 `G-409` 的 `publish/api/data/iot/protocol.js` 既有分叉，⛔ 不属本件授权面）。双侧 sha256 见收口回执 |
| `CHANGELOG.md`（本文件） | v114 → **v115**（本条） |

- 🔴 **可测断言（本件承重）**：① 确定性层 `tests/api/ai_agent/test_ambiguity_boundary.py`（① 相对量词 ＋ 如实说明；③ 数值语义无法判定 ⇒ 先反问；明确数值 ⇒ 直接执行；**反证**：摘掉本件新增 ⇒ ①/③ 判据必红、② 回归仍绿）；② `T-513` 回归 `tests/api/ai_agent/test_clarification_rules.py`（② 对象／指代面**逐字未动**）。
- ⛔ **未改内容**：本条为**追加**；`v114` 及更早条目**逐字保留**。⛔ 未改本册其它契约卡；⛔ 未改 `design/05-backend-architecture/data/ai-conversation-pipeline.js`（**另一册** ⇒ 其订正记在 **`design/05-backend-architecture/CHANGELOG.md`**）。

## v113 → v114：`T-503` —— 「音色预览（试听）」契约卡按 `D-182`=A 统一为「**试听不落库**」（需求方 2026-09-30 裁决）（2026-09-30）

> 来源＝`P-067` → 需求方 **2026-09-30 裁决 `D-182` = A**：把 `preview`（音色试听）统一为「**试听不落库**」。⛔ 本件**只动设计资产面 1 张卡**；实现面（`src/server/modules/device_control/api/router.py::preview_reply_voice` 的失败分支 upsert **删除** ＋ 三处错自述改正）在**同一 commit** 内随件落地。⛔ **AI 工具侧 `tool_executor.py` 的 `set_preference_config(action=preview)` 本就正确，未动**。
> 🔴 **为什么必须改（改前 ＝ 同卡自相矛盾）**：改前该卡 `description` 写「AI/后台选定角色后可先 preview 试听，再 **confirm 落库（status=pending→confirmed）**」，而同卡 `business_logic.post_effects` 写「无（试听不置 confirmed，不改变生效音色）」、`caching.note` 写「试听不产生持久资源」⇒ **同卡三处口径互斥**；而实现侧（改前）**失败分支确实 upsert 一行 `status=pending`** ⇒ 三说中**只有那句错的与实现相符**（这正是 `P-067` 报的形态）。
> 🔴 **版本号口径（⛔ 不得回改）**：改写 **`v113 → v114`**。⚠️ 核过本文件头部：**改前实际最大号 ＝ `v113`**（`T-495` 占用 `v112 → v113`，见 `:4`）⇒ **`v114` 空闲、本件不顺延**。`v113` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。

| 落点（本目录） | 变更（改前 ⇒ 改后） |
|---|---|
| `data/rest/02-core-domains.js` · `rest-device-preference-configs-reply-voice-preview`（`:889`）卡 `description`（`:896`） | 改前：「AI/后台选定角色后可先 preview 试听，再 confirm 落库（`status=pending→confirmed`）。」⇒ 改后：「🔴 **试听不落库**（`D-182`=A，2026-09-30 需求方裁决）：本端点**不写 `user_preference_configs`**（播放失败分支同样不写），落库只能走 `reply-voice/confirm`；preview 与 confirm 之间**不存在** `status=pending→confirmed` 的落库链路（响应里的 `status: "pending"` **仅为回显**）。」⛔ 该句其余部分（`T-401` 角色域／`preview_url` 不下发）**逐字未动** |
| 同卡 `business_logic.steps` ④（`:899`） | 改前：「返回角色码、解析出的音色名与播放结果（不置 confirmed）」⇒ 改后：「…（⛔ 不写库、不置 confirmed）」 |
| 同卡 `business_logic.post_effects`（`:900`） | 改前：「无（试听不置 confirmed，不改变生效音色）」⇒ 改后：「无（试听**不落库**：不写 `user_preference_configs`，不置 confirmed，不改变生效音色）」 |
| 同卡 `business_logic.state_machine`（`:901`） | 改前：「无状态变更（preview 不置 confirmed）」⇒ 改后：「无状态变更（preview ⛔ 不写库、不置 confirmed ⇒ `status=pending→confirmed` 不发生在 preview 上）」 |
| 同卡 `responses[0].fields` 的 `status`（`:910`） | 改前：「配置状态（本端点恒回 pending —— 试听不置 confirmed）」⇒ 改后：「**仅为响应回显**（本端点恒回 pending —— `D-182`=A：试听不落库 ⇒ 库内不产生该行）」；⚠️ 该行**响应示例 JSON 逐字未动**（`status: "pending"` **保留** —— 依任务书「可留，但必须写明它只是回显、不落库」） |
| 同卡 `idempotency.method`（`:913`） | 改前：「每次试听都重新解析并播放（不置 confirmed）」⇒ 改后：「…（⛔ 不写库、不置 confirmed）」 |
| `publish/**` 同步 | `data/rest/02-core-domains.js` ＋ `CHANGELOG.md` 两件**定点逐字节**拷贝到 `publish/api/…`（⛔ **未用 `-Apply` 整批**：`publish/**` 存在 `G-148` 假脏面，且 `G-409` 的 `publish/api/data/iot/protocol.js` **不属本件授权面**）。⚠️ **如实登记**：`CHANGELOG.md` 是**整份逐字节镜像**（脚本 `ExtraMappingTable` 口径）⇒ 连带把 `T-495`／`G-409` 的 `v113` 条目一并带进 `publish` 面 |
| `CHANGELOG.md`（本文件） | v113 → **v114**（本条） |

- 🔴 **可测断言（本件关键验收点）**：① **实现侧判据** `tests/api/device_iot/test_t020_preference_config_api.py::test_tc_pc005_reply_voice_preview_no_coordinator` —— **试听（失败场景）之后该 `(device_id, reply_voice)` 在库内零行**（改前该断言**实测失败**，原始报错 `Left contains one more item: {'pref_key': 'reply_voice', ..., 'status': 'pending', ...}` ⇒ 即「失败分支写库」的实证）；② `rule-0b` 三项（`scripts/check-viewer-data-consistency.mjs` ／ `scripts/check-design-asset-render.mjs` ／ `scripts/check-viewer-file-render.mjs`）；③ `python scripts/check_rest_response_field_subset.py`（本卡字段级漂移面 —— 本件**未改响应形状**）。
- 🔵 **判据读数（本件收口实跑；内核路径已注入 `PLAYWRIGHT_BROWSERS_PATH='C:\Soft\Playwright MCP\browsers'`，⛔ 未跑 `playwright install`）**：① 定向 pytest ＝ **`9 passed`／rc=0**（改前基线同一命令 **rc=0**；插入零行断言后、改实现前 ＝ **`1 failed, 8 passed`**）；② 三门禁均 **`rc=0`**（`[OK] 25 个视图 / 150 条断言 渲染器↔数据双向一致 + 0 页错误` ／ `[OK] api-viewer 4 type 页二级节点可表达性全部通过` ／ `==== file:// 实渲染：44 页 / 0 页有错误 ====`）；③ `check_rest_response_field_subset.py` ＝ **`rc=0`**（默认档绿：41 张卡无**未登记**漂移；唯一登记项 `rest-device-detail 缺 connection_status` 属 `KNOWN_DRIFT`、与本件无关）。
- ⚠️ **未改（同源表述，⛔ 非本件落点，只登记不顺手改 —— 依任务书「⛔ 不得扩到其它卡」）**：
  ① 相邻卡 `rest-device-preference-configs-reply-voice-confirm`（`:920`）的 `scenario`（`:926`）／`description`（`:927`）／`state_machine`（`:932`）仍写「音色试听通过后 **confirm 落库（status pending→confirmed）**」；
  ② 卡 `rest-device-preference-configs-upsert`（`:818`）的 `description`（`:825`）「status 为 pending（新增走确认流程）」／`state_machine`（`:830`）「status 由确认流程驱动 pending→confirmed→active」／`body_params.status`（`:841`）「缺省 pending」。
  ⇒ 改后 `preview` **不再产生** pending 行；`status=pending→confirmed` 该转移**仍可由** `PUT /devices/{id}/preference-configs/{key}` 显式传 `status=pending` 走到（⚠️ 该端点实现侧缺省实为 `confirmed`，与本卡 `:841` 的「缺省 pending」本就有既有漂移，⛔ 非本件引入）。⇒ **处置须另立件**。
- ⛔ **未改内容**：本条为**追加**；`v113` 及更早条目**逐字保留**。⛔ 未改本卡之外任何契约卡；⛔ 未改 `design/01-raw-input/**`；⛔ 未改 `src/**` 本卡对应实现之外的任何内容。

## v112 → v113：`T-495` —— iot 协议「**应用层心跳**」段方向口径统一为「**服务端下发 call、设备回 ack**」（需求方 2026-09-30 裁决）（2026-09-30）

> 来源＝需求方 **2026-09-30 裁决原话**：「**然后设计资产冲突，按照我刚才给你的，就是服务端发靠设备回 ACT（ack）这个口径来进行统一**」（任务书 `.pipeline/journal/T-495-brief.md`，已随 `57bffeef` 入库）。⛔ 本件**只动设计资产面**（1 行超长 JS 字面量内的 **1 个** content 字符串）；⛔ **未改任何 `src/**`** —— 心跳代码面已由 `T-494` 交付（`cfc2b720`）。
> 🔴 **为什么必须改（改前 ＝ 与三处正面矛盾）**：改前该段写「**设备**每 30s 发送 heartbeat action。平台返回 ack」，与 ① 同文件 `:231`「**服务端**按间隔发送 heartbeat call…设备必须立即回复 ack，**不得拒绝**」、② `design/01-raw-input/设备通信协议规范.md` §3.3（`:448`／`:465`：服务端 → 设备 `call`；设备收到 `heartbeat` 必须立即回复、**不得拒绝**）、③ **实现**（`src/server/gateway/iot_gateway.py::_send_heartbeat`）**三处正面矛盾**。🔴 疑为**承载层口径残留**（MQTT 时代心跳由设备发起）—— 与同文件 `:7-11` 已声明的「MQTT 为**可选链路**、WebSocket 为**唯一已实现链路**」自相矛盾。
> 🔴 **版本号口径（⛔ 不得回改）**：改写 **`v112 → v113`**。⚠️ 核过本文件头部：**改前实际最大号 ＝ `v112`**（`T-442` 占用，见 `:4`）⇒ **`v113` 空闲、本件不顺延**。`v112` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。
> ⚠️ **⛔ 未 bump 协议内版本号**：`data/iot/protocol.js` 的 `version: "3.2"` 与其 `changeLog` **本件未动** —— 依据：本册 **`## v90：`**（`:483`）的第 ② 行（`:491`）已有**同构先例** —— 对 `data/iot/protocol.js` 的**纯渲染文本 1 行更正**（「25 条」⇒「24 条」）**未 bump 协议版本**，只在 CHANGELOG 登记。⇒ 本轮沿用同一口径；若编排器判定须 bump，**另立件**（⛔ 本件不擅自扩面）。

| 落点（本目录） | 变更（改前 ⇒ 改后） |
|---|---|
| `data/iot/protocol.js` · `:594`「心跳与重连 → **应用层心跳**」段 content | 改前：「<p>**设备**每 30s 发送 heartbeat action。平台返回 ack（含 ts 用于时钟同步）。连续 3 次未收到（90s）标记离线。</p>」⇒ 改后：「<p>**服务端**每 30s 发送 heartbeat call（无 payload，消息存在本身即为心跳信号）。**设备必须立即回复 ack**（含 ts 用于时钟同步），**不得拒绝**。连续 3 次未收到（90s）标记离线。</p>」—— 🔴 **逐字保留不动**：`30s`／`90s`／`3 次` 三个数字、「连续 3 次未收到（90s）标记离线」整句、「时间同步：设备提取 `heartbeat_ack.ts`，结合 RTT/2 修正时钟偏斜」整句（该句本就依赖**设备侧拿 ack 的 ts**，方向更正后语义更自洽） |
| `data/iot/protocol.js` · `:594` 同段**兄弟段** | ⛔ **逐字未动**：「传输层保活」（MQTT Keep Alive 60s ／ WebSocket JSON ping/pong）与「断线重连」（1s ／ 60s ／ ×2 ／ ±20%）两段**保持原样**；⚠️ 其中「**WebSocket：JSON ping/pong 帧保持连接**」属**传输层**话题（`ws_ping_*` 面），**本件不裁、未动** |
| `publish/**` 同步 | 🔴 **本件⛔ 未同步**（`publish/**` 是**发布面**，⛔ 不属本件授权面）⇒ `publish/api/data/iot/protocol.js:594` **仍为改前旧口径**，由编排器收口时**登记挂账**（`G-`）。⚠️ **已知后果（⛔ 非缺陷）**：`pwsh -File scripts/sync_design_to_publish.ps1 -Check` 现读数 **RED（差异合计 1 项 ＝ `M publish/api/data/iot/protocol.js`）** —— 该红是**授权面切割的预期产物**；⛔ 执行者**不得**为消红而擅自同步 `publish/**` |
| `CHANGELOG.md`（本文件） | v112 → **v113**（本条） |

- 🔴 **可测断言（本件关键验收点）**：① 同族全量检索 `git grep -n "设备每 30s 发送 heartbeat\|设备发送 heartbeat\|设备每 30 秒发送"` ⇒ **设计资产面内容命中数 0**（仅剩 `publish/` 镜像 1 处 ＋ `.pipeline/**` 3 处**指令自指引用**）；② `node scripts/check-design-asset-render.mjs`；③ `node scripts/check-viewer-data-consistency.mjs`；④ `node scripts/check-viewer-file-render.mjs`；⑤ 面内 `data/iot/check-menu-consistency.mjs` ＋ `data/iot/event-identifier-check.mjs`。
- 🔵 **判据读数（本件收口实跑；内核路径已注入 `PLAYWRIGHT_BROWSERS_PATH='C:\Soft\Playwright MCP\browsers'`，⛔ 未跑 `playwright install`）**：② `rc=0`（`[OK] api-viewer 4 type 页二级节点可表达性全部通过`）；③ `rc=0`（`[OK] 25 个视图 / 150 条断言 渲染器↔数据双向一致 + 0 页错误`）；④ `rc=0`（`44 页 / 0 页有错误`）；⑤ `rc=0`／`rc=0`；`python scripts/check_publish_content_parity.py` ＝ `rc=0`（`[状态分布] MATCH=64` —— **该门禁判的是 `publish` 工作树 ≡ 自身 index blob，与 design 单边改动无关**）。
- ⚠️ **未测／未做（如实登记，⛔ 不隐去）**：① `design/04-platform-api/check-viewer-render.mjs` ＝ **`rc=1`**，属**改前即红的既有环境缺陷** —— 其 `:15` **硬编码** `C:/Sync/zy-iot-ai/design/04-platform-api/api-viewer.html`（本机该路径**不存在**，实测 `Test-Path=False`）⇒ 在 `page.goto` **导航阶段**即失败，**与 `protocol.js` 内容无关**（⛔ `proc.stdout` 非空，非"内核未注入"症状）；✅ **正向对照已做**：仅把该硬编码路径换成**现行仓根** `C:/Projects/zy-iot-ai/...` 后**同一脚本 `rc=0`**（`[OK] 4 页签渲染一致性校验全部通过`）⇒ 判定为**陈旧/环境路径问题**，⛔ 非本件引入、⛔ 非设计资产不一致。**处置须另立件**（⛔ 本件不顺手改）。
  ② 同族检索余下的 **3 处为「检索命令」自指引用**（⛔ 非协议文本），且**均不在本册资产面**（落在流水线账本侧，非既定设计资产）⇒ **只登记、未改**；🔴 依规则 0c／`J7a`，⛔ **本册不写该等路径**（未获批的第 3 层工作区稿路径不得渗进 `design/`）—— 逐条路径清单见 `T-495` 心跳件的改前本行原文（`caf951f8`，可回查）。
- ⛔ **未改内容**：本条为**追加**；`v112` 及更早条目**逐字保留**。⛔ 未改 `src/**`（含 `src/server/gateway/iot_gateway.py` —— 心跳实现面归 `T-494`）；⛔ 未改 `publish/**`；⛔ 未改 `design/01-raw-input/**`（原始输入＝只读归档，本件不碰）；⛔ 未改 `:594` 同段其它 content 字符串（「传输层保活」／「断线重连」／「WebSocket ping/pong」逐字原样）。

## v111 → v112：`T-442` —— `rest-binding-unbind` 卡补「**清残行**」语义（`Q-010` ③）：`remote_user_id` 为空的残行 ⇒ 手动解绑**清掉该内部残行**并返回 **200 `removed=true`**（⛔ 零外部调用）；改前的「409 `no_user_id` ＋ 行不动」＝已中毒行**产品内无出口**的缺陷（2026-09-28）

> 来源＝`T-442`（`R-025` 实现件；设计＝**第 3 层设计轮已交付件**的 §5.3 提案 ＋ §5.2 判据 `X3`，约束＝同轮约束件；⚠️ **依规则 0c／`J7a`，⛔ 本册不写该第 3 层工作区稿的路径**（未定稿内容不得渗进 `design/`；来源经 `T-442` 追溯）；任务书级约束见 `.pipeline/PIPELINE.md` 的 `T-442` 行）。⛔ 本条目**只记设计资产面**；实现面（`src/server/modules/sleep_data/**`）在**同一 commit** 内随件落地。
> 🔴 **为什么必须补卡（`D-31`「不得自己发明」）**：本卡改前只声明「**第三方平台主动解除** ＋ 同步清空内部映射行」，**⛔ 无「清残行／复位」语义** —— 而 `remote_user_id` 为空的残行（＝ beta 事件期中毒行的**实测形态**，`.pipeline/journal/_q10u1-beta-poison-probe-2026-09-27.md`）在该语义下**没有任何出口**（现场只能人工改库复位三字段：`docs/sleep-report-device-integration-pre.md:566-569`）⇒ **不补卡就实现 ＝ 直接违 `D-31`**。
> 🔴 **版本号口径（⛔ 不得回改）**：改写 **`v111 → v112`**。⚠️ 核过本文件头部：**改前实际最大号 ＝ `v111`**（`T-424` 占用 `v110 → v111`，见 `:4`）⇒ **`v112` 空闲、本件不顺延**。`v111` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。
> ⚠️ **语义选型留痕（⛔ 不隐去）**：`Q-010` §6 `Q10-U3` 把「**删行** ／ 复位三字段（现场人工口径）／ 二者按条件分叉」列为**未决**；本件按该设计 §5.2 判据 `X3` 的**断言面**（200 `removed=true` ＋ 内部行**少一行** ＋ 零外部调用）取「**删行**」。⛔ 该口径**只**适用本卡，⛔ 不得据此推及其他端点。
> ⛔ **不得触碰 `beta`／`release` 存量**（订正属 `D-135` 破坏性 ⇒ 须逐次点名另案）：本件**未连任何库**、未改任何环境数据、⛔ 未对存量中毒行做任何复位/删除。

| 落点（本目录） | 变更（改前 ⇒ 改后） |
|---|---|
| `data/rest/02-core-domains.js` · `rest-binding-unbind` 卡 `description` | 改前：仅「第三方平台主动解除 ＋ 同步清空内部映射行」，且解绑失败面**含缺 `remote_user_id`** ⇒ **409 且行不动** ⇒ 改后：**新增「清残行」段** —— `remote_user_id` **为空**（＝身份从未建立，在 `orm.py` 的「非空即终值」语义之外）⇒ **不调第三方平台**（`remoteUnbound=false`）、**清掉该内部残行**、返回 **200 `removed=true`**；⚠️ 判据**只有**「`remote_user_id` 为空」，**非空**行仍走平台解绑路径（⛔ 不得放宽成「`remote_phone` 为空／不符」—— 那会在**已认领**行上动手，破 **HC1**「已注册账号必须沿用」）。⛔ 未改路径／方法／权限码／请求体 |
| `publish/**` 同步 | 本目录 `data/rest/02-core-domains.js` ＋ `CHANGELOG.md` 两件**定点逐字节**拷贝到 `publish/api/…`（⛔ **未用 `-Apply`**：`publish/**` 存在 `G-148` 假脏面） |
| `CHANGELOG.md`（本文件） | v111 → **v112**（本条） |

- 🔴 **可测断言（本件关键验收点，⛔ 不只是卡内文字）**：① **`rule-0b` 三项**（`node scripts/check-viewer-data-consistency.mjs` ／ `check-design-asset-render.mjs` ／ `check-viewer-file-render.mjs`）；② `pwsh -File scripts/sync_design_to_publish.ps1 -Check`（`publish` 平价面）；③ **实现侧判据** `tests/api/sleep_data/test_manual_unbind.py::test_unbind_identityless_residual_row_is_cleared` —— 它**由旧钉整条反转而来**（旧用例 `test_unbind_failed_row_without_remote_user_is_409` 钉的正是「409 ＋ 行不动」这个**缺陷行为**）。
- 🔵 **判据读数（本件收口实跑，原始行见 `.pipeline/journal/T-442.journal.md`）**：viewer 三门禁 **`rc=0`**（`[OK] 25 个视图 / 150 条断言 渲染器↔数据双向一致 + 0 页错误` ／ `[OK] api-viewer 4 type 页二级节点可表达性全部通过` ＋ `wf: 全部域 0 页错误` ／ `44 页 / 0 页有错误`）；`pwsh -File scripts/sync_design_to_publish.ps1 -Check` ＝ **`GREEN`**（四类清单均 0）。
- ⚠️ **未改（明文残留，⛔ 非本件落点，只登记不顺手改）**：`src/web-admin/src/views/sleep-monitor/bindingUnbind.ts`（`:24`／`:69`／`:108-111`／`:182`／`:235`）与 `src/web-admin/src/api/sleepData.ts:127` 的注释/分支仍把 `no_user_id` 记为**本端点的 409 原因之一** —— 改后该 `reason` **不再由本端点产出**（**死分支**，⛔ 不影响前端行为：200 `removed=true` 走既有成功路径）。⚠️ 前端面**不在本件设计改动面内**（`Q-010` §5.1 未列）⇒ 本件**只登记、不顺手改**；处置须另立件。

## v110 → v111：`T-424` —— 音色校验域**设计资产漂移修正件**：`rest-ai-config-tts-voice-map-get` / `-put` 两张卡的音色域口径由「**该 provider 的音色域**」对齐为「**该候选 `(provider, model)` 的音色域**」＋ GET 卡补只读字段 `preset_voice_names`（2026-09-28）

> 来源＝**`T-422`（`959c1c9a`）上报的设计资产漂移**（任务书级约束见 `.pipeline/PIPELINE.md` 的 `T-424` 行；改法原文＝ `.pipeline/journal/T-422.journal.md` §4-1）。⛔ 本件**只动设计资产与 `publish/` 镜像**，⛔ 未改任何业务代码（`src/**` 的代码面**已由 `T-422` 改完**）。
> 🔴 **为什么必须改**：`T-422` 已把**代码**的校验域（`src/server/modules/ai_agent/domain/voices.py::provider_voice_names()`）降码到 **`(provider, model)`** 粒度，而**设计资产仍写 provider 粒度** ⇒ **代码与设计资产不一致**。provider 粒度的域是**同一 provider 各模型音色表的并集** ⇒ 文字上**结构性地允许跨模型放行**（`Cherry` → `qwen-audio-3.0-tts-flash` ＝ 真机 `[cosyvoice:]Engine error [411]`，`G-340` §2.2 逐字复现）—— 那正是 `T-422` 要封掉的入口。
> 🔴 **版本号口径（⛔ 不得回改）**：改写 **`v110 → v111`**。⚠️ 核过本文件头部：**改前实际最大号 ＝ `v110`**（`T-418` `df31fdc2` 占用 `v109 → v110`，见 `:4`）⇒ **`v111` 空闲、本件不顺延**（`T-421`／`T-436`／`T-439` **均未触设计资产**，已核实）。`v110` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。
> ⚠️ **同结论的另一半落点在域 03**（`model_fallback_candidate.extra_params` 字段 `desc`），记在 `design/03-entity-relationship/data/CHANGELOG.md` 的 `v7.53 → v7.54`；🔴 **两侧同批**（「设计资产变更六步」第 2 条）—— 只改其一即**跨册自相矛盾**。

| 落点（本目录） | 变更（改前 ⇒ 改后） |
|---|---|
| `data/rest/02-core-domains.js` · `rest-ai-config-tts-voice-map-get` 卡 `description` — `allowed_voice_names` 口径 | 改前「`allowed_voice_names` ＝ **该 provider 的音色域**（`mimo` ＝ 官方名单；别家 ＝ **该 provider** 在链上**已在用**的音色名）」 ⇒ 改后「＝ **该候选 `(provider, model)` 的音色域**（🔴 `T-422`：降码到 **`(provider, model)`** 粒度＝ `PROVIDER_MODEL_VOICE_NAMES` 覆盖的**实测集** ∪ 该 `(provider, model)` 在链上**已在用**的音色名；⚠️ ⛔ 不再是"该 provider 的音色域" —— **同一 provider 的不同模型音色表互不相交**（`bailian` 一家 3 个 tts 模型即如此），按 provider 取域＝并集＝**结构上允许跨模型放行**，真机形态＝百炼 `[cosyvoice:]Engine error [411]`）」 |
| 同上卡 · **返回字段表补只读字段** | 改前 `candidates[{…, voice_map, allowed_voice_names}]` ⇒ 改后 `candidates[{…, voice_map, **preset_voice_names**, allowed_voice_names}]` ＋ 逐字说明：`preset_voice_names` ＝ 🔴 `T-422` **新增只读字段**＝ 该格**预制实测目录**（＝ `PROVIDER_MODEL_VOICE_NAMES` 的那一格），供下游分辨「**实测目录里已有**」与「**仅因在链上在用才被接受**」 |
| `data/rest/02-core-domains.js` · `rest-ai-config-tts-voice-map-put` 卡 `description` — 保存期校验口径 | 改前「音色名非空且 ∈ **该 provider 的音色域** ⇒ 非法一律 **422**（⛔ 不静默取默认值；⛔ 不拿 MiMo 的名字去卡别家 —— 跨家名字正是阿里云百炼 `411` 的根因）」 ⇒ 改后同上句 ＋ 「🔴 `T-422`：该域**再收窄到 `(provider, model)`** —— **跨模型混用**同样是百炼 `411`；⚠️ 该 `(provider, model)` 一格**未登记且链上无在用名** ⇒ 域＝**空集** ⇒ **响亮 422**（⛔ 不放宽成"任意非空字符串"）；响应 `allowed_voice_names` 同取该 `(provider, model)` 域」（🔴 **⛔ 未改保留该卡既有的「⛔ 不拿 MiMo 的名字去卡别家」半句** —— 跨家护栏与跨模型护栏**两条并存**，删任一条即回退 `411` 防线） |
| `publish/**` 同步 | 本目录 `data/rest/02-core-domains.js` ＋ `CHANGELOG.md` 两件**定点逐字节**拷贝到 `publish/api/…`（⛔ **未用 `-Apply`**：`publish/**` 存在 `G-148` 假脏面） |
| `CHANGELOG.md`（本文件） | v110 → **v111**（本条） |

- 🔴 **可测断言（本件关键验收点，⛔ 不只是卡内文字）**：落进**既有已登记**门禁（⛔ 未新增判据文件 ⇒ ⛔ 未动 `tests/README.md` 的判据登记面）：① `node scripts/check-viewer-data-consistency.mjs` 的 `er-04` / `api-rest` 双向一致；② `node scripts/check-design-asset-render.mjs`（棘轮逐域比对"渲染量 ＝ 数据量"）；③ `node scripts/check-viewer-file-render.mjs`；④ `pwsh -File scripts/sync_design_to_publish.ps1 -Check`（`publish` 平价面）。
- 🔵 **判据读数（本件收口实跑，原始行见 `.pipeline/journal/T-424.journal.md`）**：viewer 三门禁 `rc=0`／**0 页错**；`pwsh -File scripts/sync_design_to_publish.ps1 -Check` ＝ **`GREEN`**（四类清单均 **0**）；`python scripts/check_publish_content_parity.py` 读数附于该 journal。
- ⚠️ **未改（明文残留，⛔ 非本件落点，只登记不顺手改）**：本条**上方历史条目**（`v105 → v106` 等）里的旧 provider 粒度文字是**当日实况留痕**，⛔ **不回改**（更正由本条承担）；`T-422` journal §4-2 登记的「**未登记的 `(provider, model)` 格 ⇒ 域＝空集**」是**能力边界**（属 `G-340` §3.3 的"未探测名不入目录"口径），⛔ 不在本件处置面。
- ⛔ **未改内容**：本条为**追加**；`v110` 及更早条目**逐字保留**。⛔ 未改任何端点的路径／方法／权限码／请求体／状态码；⛔ 未改 `design/03-entity-relationship/data/*.js` 的**字段结构**（本批只改 1 个 `desc` 文本，其域 03 册记录见 `v7.53 → v7.54`）；⛔ 未改 `src/**`／`tests/**`／音乐库媒体面（`T-432`／`T-436`／`T-439` 的成果）。

## v109 → v110：`T-418` —— `G-349` 复核件落地：`rest-device-detail` 由「复合体」订正为「**主设备自身信息独立端点**」（扁平 9 键 ＋ 需求方点名字段 `connection_status`）；⛔ 不收成扁平的反面（子设备仍走**已存在**的 `/sub-devices`，⛔ 未新建第二端点）＋ 销账 `KNOWN_DRIFT` 旧项（2026-09-28）

> 来源＝**需求方 2026-09-27 当场裁定①**（「主设备自身信息独立端点 ＋ 子设备另端点」；`connection_status`「确认没问题」）＋ 只读评估件 `.pipeline/journal/_g349-device-detail-consumers-2026-09-27.md`（`G-349` 复核；「必做 8 件」与「两条不能做」在该件 §4）。本件走「设计资产变更六步」＋ 四件套；任务书级约束见 `.pipeline/PIPELINE.md` 的 `T-418` 行。
> 🔴 **版本号口径（⛔ 不得回改）**：改写 **`v109 → v110`** —— ⚠️ 原计划的 `v108 → v109` **已被 `T-423`（`b111e938`）占用**（本文件 `:4`），且 `v109` 亦是 T-423 号 ⇒ 本件顺延至 **`v110`**。`v109` 及更早条目**逐字保留**（本条为**追加**，插在文件顶部）。
> 🔴 **`KNOWN_DRIFT` 销账是「叠加」不是「回退」**：`T-420`（`6b8e236b`）已把三处同源计数上调（声明面 **56**／未解析面 **16**／implements 端点 **222**）⇒ 本件**三张计数一个不动**（本件只**订正**一张既有卡的字段口径，⛔ 未新增/删除端点卡），只动 `KNOWN_DRIFT` 这一张表。

| 落点（本目录） | 变更（改前 ⇒ 改后） |
|---|---|
| `data/rest/01-device-iot.js` · `rest-device-detail` 卡 `response.body` | 改前 复合体 `{ device: "Device", modules: "SensorModule[]", history: "ChangeRecord[]" }` ⇒ 改后 **扁平 10 键** `{ id, device_id, product_model, firmware_ver, hardware_ver, status, device_group_id, last_heartbeat, created_at, connection_status }`（🔴 需求方点名的 `connection_status` **在册**） |
| 同上 · 卡 `responses[].json` 样例 ＋ `responses[].fields` | 改前 复合体样例（`device{…}` ＋ `peripherals[]` ＋ `history[]`，⚠️ 且写得**内部不自洽**：`response.body` 写 `modules`、`fields` 写 `peripherals`；样例里还缺一个 `}`） ⇒ 改后 **扁平 10 键**样例 ＋ 与之**逐项一致**的 `fields`（⛔ 卡内三处不自洽一并消除） |
| 同上 · 卡叙述面（`summary`／`scenario`／`description`／`business_logic.steps`／`post_effects`／`related_apis`） | 改前「查看设备详情，包含**传感器模块**和**变更历史**」＋ `steps` ③「关联查询…**传感器模块**」④「查询设备**变更历史**」 ⇒ 改后 `summary`＝**主设备自身信息**；`steps` ③ 改「聚合实时连接状态 `connection_status`（设备影子优先、gateway 兜底；DD-028）」；`related_apis` **补** `/sub-devices`（第二端点）与 `/connection`。⚠️ 六步第 2 条「同一结论散布多处时必须一起改（防漂移）」—— 只改 `response.body` 会留下「删了传感器模块却还写着传感器模块」的**卡内自相矛盾** |
| ⛔ **未做的两条**（评估件结论，本件遵守） | ① 前端 `deviceApi.detail(` **零调用点** ⇒ 本件**不动** `src/web-admin/**`；② `GET /devices/{id}/sub-devices` **已存在**（`rest-device-sub-devices` 卡，`permission: public`，`response.body {device_id, sub_devices} ⊆ 实现`）⇒ **第二端点不新建**。⛔ **不改 `src/**`**（实现轮另立） |
| `design/07-tdd/api/01-device-iot-regression-tdd-design.md` | **5 条用例**（TC-R001／R002／R003／R005／R006）的 `response.body.device.*` ＋ `response.body.peripherals` 断言**订正为扁平键**；TC-R006 的调用面改指 `/sub-devices`；文首加 `T-418` 订正段。🔴 **不静默删断言**：旧断言里**实现侧根本不存在**的三组字段（`sleep_monitor_left_id`/`_right_id`/`zone_capabilities`、`peripherals[].peripheral_type`/`peripheral_device_id`、`device_secret` 不存在）**逐条具名登记为设计资产缺口**（处置须另立件） |
| `scripts/check_rest_response_field_subset.py` ＋ `tests/design-regression/test_rest_response_field_subset_gate.py` | 🔴 **`KNOWN_DRIFT` 销账（叠加口径）**：旧项 `("device","history","modules")` ⇒ 实测差集随卡订正变为 **`("connection_status",)`** —— ⛔ **不是清空**：卡已按裁定写死该字段，而实现 `get_device`（`src/server/modules/device_iot/api/router.py:541-553`）**直接** `return _device_to_dict(d)`、⛔ 未并入 `_realtime_status(...)`（同文件 4 处调用点里**只有列表项** `:383` 经 `_decorate` 带 `connection_status`）⇒ 该行是「**卡已按裁定写死、实现待补**」的**明账**（与 `T-420` 的 `rest-report-images-list` `no-impl-endpoint` 同族的待实现态）。两表（脚本 `KNOWN_DRIFT` ↔ 测试 `EXPECTED_KNOWN_DRIFT`）**同批改** |
| `tests/README.md` | `§49` 的 `T-411` 停手叙述 ＋ 门禁表读数（漂移 **1** 条的口径）同步；🔴`tests/README.md` **未新增判据文件**、未新增行段（本件落到**既有已登记**门禁里） |
| 🔴 **可测断言（本件关键验收点，⛔ 不只是卡内文字）** | 落进**既有已登记**门禁 `scripts/check_rest_response_field_subset.py`（⛔ 未新增判据文件 ⇒ ⛔ 未动 `tests/README.md` 的判据登记面）：**新增 `T-418` 专项负控 ④a/④b/④c**（`--self-test`，走**纯函数 ＋ 合成夹具**）：④a 口径在册（扁平 10 键）⇒ 无红；**④b 卡侧回退为复合体 ⇒ 判红并点名 `device`/`history`/`modules`**；**④c 卡侧漏掉 `connection_status` ⇒ 判红（F4 登记未销账）**。⚠️ ⛔ **不用"真树当夹具"** —— 真树是**受判对象**、随本件一起变 ⇒ 那样写会**恒绿**。另在 `tests/design-regression/...` 的 `test_real_tree_registry_matches_every_face` 加 **真树承重断言**（差集 `== ["connection_status"]` 且复合体三键与之 `disjoint`） |
| `publish/**` 同步 | 本目录 `data/rest/01-device-iot.js` ＋ `CHANGELOG.md` 两件**定点逐字节**拷贝到 `publish/api/…`（⛔ **未用 `-Apply`**：`publish/**` 存在 `G-148` 假脏面） |
| `CHANGELOG.md`（本文件） | v109 → **v110**（本条） |

- 🔵 **判据读数（本件收口实跑，原始行见建件 journal）**：`python scripts/check_rest_response_field_subset.py` `rc=0`（`[WARN]` 点名 `rest-device-detail 缺 connection_status`）／`--strict` `rc=1`（真读数）／`--self-test` **18/18**；三张计数读数 **声明面 56／被判面 40／未解析面 16**（⛔ 与 `T-420` 后一致，未被本件回退）；viewer 三门禁 `rc=0`；`pwsh -File scripts/sync_design_to_publish.ps1 -Check` ＝ `GREEN`。
- ⚠️ **未改（明文残留，⛔ 非本件落点，只登记不顺手改）**：`src/server/modules/device_iot/api/router.py` 的 `get_device`（**实现待补** `connection_status`）＋ 评估件 §4「可后置」6 项（`/sub-devices` 三项表达力缺口／`tree.js:489` 双端点并列表述／`deviceIot.ts:193` 的 TS 返回类型／前端**补消费者**＋孤儿 i18n 键清理／两处 `CHANGELOG`）。
- ⛔ **未改内容**：本条为**追加**；`v109` 及更早条目**逐字保留**。⛔ 未改 `src/**`／⛔ 未改 `rest-device-sub-devices` 卡／⛔ 未改权限码／⛔ 未动 `design/03-entity-relationship/**`（`T-424` 的面）／音乐库媒体面（`T-425` 已交付 `5f24c3e7`）／`T-421` 的报告清单面。

## v108 → v109：`T-423` —— `Q9-U8` 改判修正件：报告可见集 `_report_visibility` 的**平台级语义**由「编排器保守收窄 ＋ 标"待裁"」改为「**平台级保持全量**」（需求方 2026-09-27 裁决）＋ 同口径**双落点**订正 ＋ 常驻判据「检查 H」（2026-09-28）

> 来源＝**需求方 2026-09-27 逐条裁决 `Q9-U*`**（编排器落账原话见 `.pipeline/PIPELINE.md` 的「`Q9-U*` 8 条裁决留痕」表），其中 `U8` 原话：「**U8 我们选 B，一次放行就好了。平台级保持全量**」⇒ 落为 `T-423`（**设计资产变更件**；任务书＝ `.pipeline/journal/T-423.taskbook.md`）。
> 🔴 **改判的实质**：`T-420` 当日把这一态写成**编排器保守默认**（收窄，并逐处标注"待需求方确认、⛔ 不是需求方裁决"）；**本件依裁决改为「平台级保持全量」并撤掉"待裁"标注**（⛔ 该收窄作废）。
> ⚠️ **边界（⛔ 不得扩张解释）**：本次放宽**只**针对本口径 —— ⛔ **不得据此推及其他端点**（同族 `rest-report-daily`／`-trend`／`-export` 的平台级「放行」是**单设备**语义，不在本裁决范围内）；本键的**租户级**与**空集合**两态**逐字未改**。

| 落点（本目录） | 变更（改前 ⇒ 改后） |
|---|---|
| `data/rest/02-core-domains.js` · `_report_visibility.states[0]`（平台级态） | `value`：改前「**仅本平台自建／未归属设备**的报告产物」 ⇒ 改后「**全部租户**的报告产物（平台级**不过滤** `tenant_id`，含他租户产物与图片直链）」；`note`：改前「编排器保守默认…⛔ 不是需求方裁决」 ⇒ 改后「**需求方裁决**（2026-09-27 逐条裁定 `Q9-U8`，原话逐字引）⇒ **平台级保持全量**」 |
| 同上 · `_report_visibility.status` | 改前「编排器保守默认，待需求方确认（…）」 ⇒ 改后「**需求方裁决已落**（`T-423`）：`Q9-U8` 裁定平台级**保持全量**（⛔ 非编排器默认、⛔ 非"待裁"）」 |
| 同上 · `Q9-K3` 块头注释 | 追加 `T-423` 订正段（引 `Q9-U8` 原话 ＋ 边界声明 ＋ 判据落点），⛔ 不改 `Q9-U3` 落点结论 |
| 同上 · 端点卡 `rest-report-images-list` 的 `description`（**同口径的第二落点**） | 改前「平台级 … **按 `_report_visibility` 的保守口径收窄**（⛔ 不是无条件全量）」 ⇒ 改后「平台级 … **保持全量**（**不过滤** `tenant_id`，**含他租户产物** —— 需求方 2026-09-27 裁决 `Q9-U8`）」。⚠️ 同口径**两处落点同批改**（六步第 2 条「同一结论散布多处时必须一起改（防漂移）」）—— 只改其一即卡内自相矛盾 |
| `CHANGELOG.md`（本文件） | v108 → **v109**（本条） |

- 🔴 **可测断言（本件关键验收点，⛔ 不只是卡内文字）**：`scripts/check_tenant_scoping.py` **新增检查 H** —— 与本门禁自身的 `D-34` 平台级口径**同源**（⛔ 非语义正交 ⇒ 依 `tests/README.md:1023` 的先例，只有**正交**语义才须另立判据），故落进**既有已登记**门禁（四件套 ③④ 已在位，⛔ 本件未新增判据文件、⛔ 未动 `tests/README.md`）：
  - ① **判据面** ＝ 报告簇 `Q9-K3` 区域（`Q9-K3` 块注释 ＋ `_report_visibility` 对象 ＋ `rest-report-images-list` 卡那一行）；锚点取不到 ⇒ **判红**（⛔ 不静默跳过）；
  - ② **必须写**（`REPORT_VISIBILITY_REQUIRED`，**棘轮** `REPORT_VISIBILITY_MIN_RULES = 5`，⛔ 只增不减）：`平台级保持全量`／`需求方`＋`裁决`＋`2026-09-27`＋`Q9-U8`／`T-423`／`全部租户`＋`不过滤`／卡内点名 `_report_visibility`；
  - ③ **禁写**（改回"保守收窄"⇒ 必判红；字面量按**自指纪律**拼接构造）：旧收窄值／旧"待裁"标注／旧"放大暴露面"理由／旧 `status`；
  - ④ **双副本同源**：`design/04-platform-api/data/rest/02-core-domains.js` ↔ `publish/api/data/rest/02-core-domains.js` **逐字节**相同（`DESIGN_ASSET_PAIRS` 同族）；
  - ⑤ **正/负控**：`python scripts/check_tenant_scoping.py --self-check` 内含 正控（真树绿）／负控（口径改回"保守收窄"⇒ **必红**）／双副本反证。
- 🔵 **判据读数（本件收口实跑，原始行见 `.pipeline/journal/T-423.journal.md`）**：正控 `python scripts/check_tenant_scoping.py` `rc=0`（含检查 H 面读数）；**负控 `rc=1`**（口径改回"保守收窄"⇒ 逐条点名）；`--self-check` 全绿；`pwsh -File scripts/sync_design_to_publish.ps1 -Check` ＝ `GREEN`；viewer 三门禁 `rc=0`；`python scripts/check_publish_content_parity.py` `MATCH` 读数同批附于 journal。
- **`publish/**` 同步**：本目录 `data/rest/02-core-domains.js` ＋ `CHANGELOG.md` 两件**定点逐字节**拷贝到 `publish/api/…`（⛔ 未用 `-Apply`：`publish/**` 存在 `G-148` 假脏面；`data/ai-tools/tools.js` **具名排除**）。
- ⚠️ **未改（明文残留，⛔ 非本件落点，只登记不顺手改）**：同卡末句仍标「🔴 **编排器保守默认（待需求方确认）**：…`Q9-U1`／`U4`／`U5`／`U6`」—— 那四条**不属 `Q9-K3` 平台级语义**（本件改判对象只有 `Q9-U8`）⇒ 依任务书「只改 `Q9-K3` 涉及平台级的那一段」**只登记**；若要求方口径统一（四条已在 2026-09-27 裁定为"与默认一致"），须**另立件**逐条区分"裁决"与"保守默认"。
- ⛔ **未改内容**：本条为**追加**；`v108` 及更早条目**逐字保留** —— ⚠️ **历史留痕**：`T-420` 当日确实按保守默认落卡，v108 条目里那两句**不得回改**，**更正由本条承担**。`T-420` 其余 **5 卡**（`rest-report-images-list` 除平台级那句外／`Q9-K1` 读契约不在本册／`Q9-K4` 页面功能点／`Q9-K5` 省卡／`Q9-K6` 漂移登记／`Q9-K7` 裸链订正）**一字未动**；⛔ 未改 `src/**`／`tests/**`／权限码／`D-112`／`D-34`。

## v107 → v108：`T-420` —— `R-024`（`Q-009`）设计资产变更件：新增 `rest-report-images-list`（报告产物清单端点）＋ 报告可见集口径 `_report_visibility` ＋ `rest-sleep-report-image` 卡**裸链文字订正** ＋ `reports` scope 漂移登记（2026-09-27）

> 来源＝**需求方 2026-09-27 授权取件**（原话：「R24 和 25 这两个你就直接取件吧，我授权了，取件了之后直接做掉」）⇒ 落为 `T-420`（**只做设计资产**；实现轮＝ `T-421`）。
> 设计输入＝**第 3 层设计轮已交付件**（§3.1 卡级提案 ＋ 其约束件；出口 2b）—— ⚠️ **依规则 0c／`J7a`，⛔ 本册不写该第 3 层工作区稿的路径**（未定稿内容不得渗进 `design/`）；已由编排器按 `R-024` 取件并落为 `T-420`。本件把其 §3.1 的卡级提案**落成真实设计资产**（走「设计资产变更六步」＋ 四件套）。
> 🔴 **8 条编排器保守默认（⛔ 不是需求方裁决）**：`Q-009` §6 的 `Q9-U1`~`U8` 未决由**编排器**按保守默认裁定，本件**照此落地**并逐条标「编排器保守默认，待需求方确认」：见下表。

| # | 编排器保守默认（待需求方确认） | 本册落点 |
|:--:|---|---|
| `Q9-U1` | 主过滤维度＝**按设备**（`device_id` **必填** ＋ `report_date` **可选·单日**）；再延续既有索引 `(device_id, report_date, period, layout)` 的最左前缀 | `data/rest/02-core-domains.js` 的 `rest-report-images-list.request.query`（2 键）＋ 域 03 读契约的 `read_contract.filters`／`filters_excluded_basis` |
| `Q9-U2` | **并入既有 `reports` 节点**（`permission-mapping-design.md:49`，路由 `/sleep-monitor/reports`）；**下载用 `sleep_data.read`**，⛔ 不新增权限码 | 本卡 `description` 的权限/租户口径 ＋ `design/06-web-admin/data/tree.js` 的两个新功能点 |
| `Q9-U3` | `Q9-K3`（报告可见集）**落 `design/04-platform-api`**（API 契约面；⛔ 不是架构分层） | 本册新增域级键 `_report_visibility`（紧邻 `rest-report-images-list`） |
| `Q9-U4` | 不可用态＝**被动式**：⛔ 不加 `available` 字段、⛔ 不加 `summary.unavailableCount`；由**下载 404 ⇒ 前端回填** | 域 03 读契约的 `read_contract.unavailable_state` ＋ `summary_excluded.unavailableCount` |
| `Q9-U5` | ⛔ **不含** `sleep_report_raws`（有 raw 未渲染）—— 本件只列**渲染产物**；跨表**登记为待裁项** | 域 03 读契约的 `read_contract.cross_table` |
| `Q9-U6` | `Q9-K7`（直链卡文字订正）**本批一并落** | 本册 `data/rest/02-core-domains.js` 的 `rest-sleep-report-image` 卡 |
| `Q9-U7` | ⛔ **不要**「重建/刷新」入口 —— 不落卡、不提案 | 本册**零落点**（无新端点、无新功能点）；登记见 `.pipeline/journal/T-420.journal.md` |
| `Q9-U8` | 平台级（`tenant_id is None`）⛔ **不无条件全量** ⇒ **默认按「仅本平台自建/未归属」收窄**，并在卡内**明写该收窄是保守默认、待裁** | 本册 `_report_visibility.states[0].note`（含「⛔ 不是需求方裁决」逐字） |

| 落点（本目录） | 变更 |
|---|---|
| `data/rest/02-core-domains.js` · 域 `domain-sleep-data` **新增端点卡** `rest-report-images-list`（`GET /api/v1/sleep-data/reports/images`） | 域内端点卡 **14 → 15**；REST 卡总数 **249 → 250**。权限 `sleep_data.read`（⛔ 不新增码）；他租户 `device_id` ⇒ **403 且不发起任何外部调用**（与同族 `rest-report-daily`／`rest-report-export` **逐字同口径**）；`device_id` 缺失 ⇒ 422；空结果 ⇒ 200 空 `items`。🟢 **本端点不发起任何外部调用**（只读本地登记行，⛔ 不调 anyishui）—— 与同族 3 张「实时外呼」端点**本质不同**。契约（过滤/排序/分页/行字段白名单/`summary`/不可用态）**唯一出处** ＝ 域 03 的 `sleep_report_image_read_contract`。 |
| 同上 · 域 `domain-sleep-data` **新增域级键** `_report_visibility`（紧邻上卡） | 报告产物**可见集口径唯一出处**：连接键 `sleep_report_images.device_id` ↔ tenancy 设备可见集；**三态**（平台级／租户级／空集合 0 台）逐态取值；⛔ 不新增报告维度（`D-31`）；`id`／`url` ⛔ 不作为准入凭据。🔴 平台级取**保守收窄**（`Q9-U8`）。⚠️ 该键**不渲染**（与 `_trace`／`implementation` 同族的域级元数据键）⇒ 由卡的 `description` 点名引用。 |
| 同上 · `rest-sleep-report-image` 卡（`Q9-K7`） | **文字订正**：`HMAC-SHA256(image_id.exp) 签名 + exp 过期校验` → **裸链：无签名、无过期判定**；同批把 `request.query {exp, sig}` 声明**一并订正**（两者是**同一条过时事实的两处落点**）。⚠️ **只订正文字**，⛔ 不改 `D-112` 裁决、⛔ 不新增/撤回签名或 TTL 语义。实测依据＝`src/server/modules/sleep_report/api/router.py` 的 handler docstring（「取报告页 PNG（裸链：无签名、无过期判定）」，签名只有 `image_id`）。 |
| 同上 · 本域 `stats` 旁注释 | 卡片数口径注释同步 **14 → 15**，并点明 `rest-report-images-list` 与那 7 张 `planned` 卡**不同**（它是**待实现契约**，⛔ 不是「不排期」）。 |
| `data/rest/10-business-orchestration.js` · `handler_registry.entries[]` 的 `reports` 条目 `note`（`Q9-K6`） | **只增不改**：在既有结论后追加「**漂移登记（`Q-009`／`T-420`）**」段 —— 指向清单① `R-024` ＋ 本批已落的三件设计资产；并明写 `reports` scope 的**最终挂载/废除**（含 `DESIGN_SCOPES`／`MOUNTED_SCOPES`、`aggregate_handlers.py` 代码面）**仍待独立裁决**（`Q8-A7`／`Q8-A9②`）⇒ ⛔ 卡内不得出现「已废除/已挂载」。⚠️ **`aggregate_handlers.py` 一字未动**（属实现面，⛔ 不在本件）。 |
| `CHANGELOG.md`（本文件） | v107 → **v108**（本条） |

- 🔴 **本件⛔ 不写产品代码**：`src/**`／`tests/**` 的**产品实现与测试一字未动**（唯二例外＝下条**棘轮常量具名上调**，方向与理由逐条写明）。
- 🔴 **棘轮具名上调（⛔ 不是"静默改常量"；门禁口径要求「若触棘轮 ⇒ 具名上调并写明」）**：本册新增 1 张端点卡 ⇒ 三处**同源镜像读数**必须同批上调，判据本身**未放宽**（仍逐域逐卡比对）：
  - `scripts/check-design-asset-render.mjs`：REST 卡数断言 **249 → 250**、`睡眠数据` 二级节点 **14 → 15**（镜像表同步，注释随批写明）；
  - `tests/design-regression/test_rest_domain_migration.py`：`EXPECTED_MOUNTS["domain-sleep-data"]` **14 → 15**、`test_total_endpoint_cards` **249 → 250**；
  - `scripts/check_rest_response_field_subset.py` ＋ `tests/design-regression/test_rest_response_field_subset_gate.py`：新卡**声明了 `response.body`**（`{items,total}`）⇒ F1 声明面 **55 → 56**（登记 `DECLARED_CARD_IDS`）；因该端点**尚未实现**（实现轮＝ `T-421`）⇒ F2 **未解析面 15 → 16**（登记 `UNRESOLVED_DECLARED_CARDS`，tier＝`no-impl-endpoint`）—— ⚠️ 这是**明账**、⛔ 不是豁免；`T-421` 落地后该卡应移到 `JUDGED_CARD_IDS` 并把这本账销掉。
- 🔵 **判据读数**（本件收口实跑，原始行见 `.pipeline/journal/T-420.journal.md`）：`node scripts/check-viewer-data-consistency.mjs` `rc=0`／`node scripts/check-viewer-file-render.mjs` `rc=0`／`node scripts/check-design-asset-render.mjs` `rc=0`（`卡数=250`）／`python scripts/check_rest_response_field_subset.py` `rc=0`。
- **`publish/**` 同步**：本目录 `data/rest/02-core-domains.js`、`data/rest/10-business-orchestration.js`、`CHANGELOG.md` 三件按 `scripts/sync_design_to_publish.ps1` 的映射表**定点逐字节**拷贝到 `publish/api/…`（⛔ **未用 `-Apply`**：`publish/**` 存在 `G-148` 假脏面，整体同步会改到与本件无关的字节；`data/ai-tools/tools.js` **具名排除**）。
- ⚠️ **未改（明文残留，⛔ 非本件落点，属另册／另件）**：`Q9-K7` 订正的**同一过时事实**（`HMAC 签名 ＋ exp`）在本件落点之外仍有 **2 处**：① `design/domain-design-overview.md` 的域 03 行（「`/sleep-report/images/{image_id}`（HMAC 签名直链、无登录态）」）；② `design/07-tdd/e2e/sleep-report-display-tdd-design.md` 的 `TC-SR-001`（「`src` 含 … 且为**绝对 URL**（exp/sig 齐备）」）。两处**均不在** `Q-009` §3.1 给 `Q9-K7` 的落点（只点了本册 `02-core-domains.js`）⇒ 本件**只登记、⛔ 不顺手改**（跨册改动须各自走其域 CHANGELOG）。
- ⛔ **未改内容**：本条为**追加**；`v107` 及更早条目**逐字保留**。本域既有卡（含 7 张 `planned` 卡与 `rest-sleep-report-image` 的**路径/权限/404 语义**）**语义零改**。

## v106 → v107：`T-411` —— 销账 `G-287`／`T-410` 照出的 8 处「卡 ↔ 实现」字段漂移中的 **7 处**；第 8 处（`rest-device-detail`）**停手上报**（2026-09-27）

> 来源＝挂账 **`G-287`** ＋ `T-410`（`69435b90`）新增判据 `scripts/check_rest_response_field_subset.py` 的真树实测读数。
> 🔴 **口径（编排器裁决）**：**以实现 ＋ 前端消费面一致为准 ⇒ 更新契约卡**；⛔ 不得凭空收缩契约（那会破坏消费者）。
> ⚠️ **方向订正（纠正任务书措辞，实测为准）**：判据打出的「`✗ <卡> 缺 <名>`」＝**实现侧缺这些名字**
> （机器读数 `missing = 卡声明名 − 实现顶层键`，脚本 `check_rest_response_field_subset.py` 的 `judge()`）
> ⇒ 方向是「**卡多声明／卡换名**」，⛔ **不是**任务书括号里的「卡漏声明」；「⛔ 不是实现多返回」这一半是对的。
> ⛔ **本件只改设计资产 ＋ 判据登记册（`scripts/**`）＋ 两张同源表（`tests/**`）**，⛔ **一字未动产品代码**（`src/**`）。

| 卡（本目录 `data/rest/…`） | 改前（卡声明） | 改后（＝**实现真实顶层键**） | 实现锚点 | 前端消费面锚点 |
|---|---|---|---|---|
| `rest-device-update`（`01-device-iot.js`） | `{id, name}` | 扁平设备 dict **9** 键：`id`/`device_id`/`product_model`/`firmware_ver`/`hardware_ver`/`status`/`device_group_id`/`last_heartbeat`/`created_at` | `device_iot/api/router.py` 的 `update_device` → `_device_to_dict` | `web-admin/src/api/deviceIot.ts` 的 `deviceApi.update`（调用点 `views/device-iot/DeviceManagement.vue` 的 `handleSave` **不读响应体**） |
| `rest-device-import-preview`（`01-device-iot.js`） | `{total, valid, invalid, errors}` | `{total, valid, invalid, items}`（＋ `items[].{row,valid,issues}`） | `import_preview`（`device_iot/application/services.py` 的 `import_preview` 逐行返回 `{row,valid,issues}`） | `deviceApi.importPreview`（未定型、无调用点） |
| `rest-device-import-execute`（`01-device-iot.js`） | `{created, updated, failed}` | `{success, imported}` | `import_execute`（`import_execute` 返回 `{"success": True, "imported": count}`） | `deviceApi.importExecute`（未定型、无调用点） |
| `rest-device-preference-configs-upsert`（`02-core-domains.js`） | 含 `device_id`；缺 `source`/`created_at` | 8 键：去 `device_id` ＋ 补 `source`/`created_at` | `device_control/api/router.py` 的 `_pref_config_to_dict` | `web-admin/src/api/preferenceConfig.ts` 的 `PreferenceConfig`（**与实现同形**，无 `device_id`） |
| `rest-device-preference-configs-reply-voice-confirm`（`02-core-domains.js`） | 含 `device_id`；缺 `value_type`/`source`/`created_at` | 同上 8 键 | `_pref_config_to_dict`（`confirm_reply_voice` 的返回） | `preferenceConfig.ts` 的 `confirmReplyVoice`（返回型 `PreferenceConfig`） |
| `rest-device-preference-configs-reply-voice-preview`（`02-core-domains.js`） | `{preview_url, voice_id, expires_in}`（TTS 试听**旧口径**） | `{success, pref_key, value, role, resolved_voice, status, played}` | `preview_reply_voice`（`T-401` 后按**角色码**试听） | `preferenceConfig.ts` 的 `ReplyVoicePreviewResponse`（`T-399`/`T-401` 后，**与实现同形**） |
| `rest-bed-user-profile-preference-upsert`（`02-core-domains.js`） | `{device_id, preferences, updated_at}` | `{device_id, key, value, removed, preferences}` | `upsert_profile_preference`（`{"device_id", "key", "value", "removed", "preferences"}`） | `web-admin/src/api/bedUserProfile.ts` 的 `updatePreference` 返回型（**与实现同形**，无 `updated_at`） |

- 🔵 **`rest-device-preference-configs-reply-voice-preview` 多改了三处（同卡自相矛盾的必然面，⛔ 非"顺手美化"）**：
  该卡的 `description`／`business_logic.steps`／`body_params`／纯 400 文案**原本整段按 TTS 试听旧口径书写**
  （「仅返回可播放的 `preview_url`」／「请求体 `voice_id` 或 `text`」）⇒ 只改响应字段会**当场自相矛盾** ⇒
  一并按 `T-401` 后的实现订正为**角色码**口径（`body_params: value` ＋ 叙述只改**被改名概念**所在处）。
  ⚠️ 同族的 `-reply-voice-confirm` 未被 `T-410` 判为"整卡过时"（只判 `device_id` 一个字段）⇒ 本件**只动响应字段面**，
  其请求面 `voice_id` 残留见上条③④。
- 🔴 **停手 1 处（⛔ 未改、⛔ 不属本件权限）：`rest-device-detail`** —— 判据仍登记 `("device","history","modules")`。
  **三条证据**合成「这是**语义裁决**、不是卡滞后」：① 卡 `response.body` 是**复合体** `{device, modules, history}`
  （⚠️ 且 `response.body` 写 `modules`、`responses[].fields` 写 `peripherals` —— **卡内部也不自洽**），
  实现 `_device_to_dict` 是**扁平** 9 键 ⇒ 「更新卡」＝把该端点契约**整体收缩**为扁平；② **前端消费面为空**——
  `web-admin/src/api/deviceIot.ts` 的 `deviceApi.detail()` **无任何调用点**，且
  `design/06-web-admin/data/tree.js` 的「设备详情弹层」`api_ref` 走 `GET /api/v1/devices/{id}/sub-devices`
  ⇒ 本件默认口径「实现 ＋ **前端消费面**一致」**不成立**（前端给不出确认）；③ **设计侧自身分叉**——
  `design/07-tdd/api/01-device-iot-regression-tdd-design.md` 的 TC-R001／TC-R005／TC-R006 仍断言
  `response.body.device.*` ＋ `response.body.peripherals`，改本卡而不同改该件 ⇒ 设计资产互斥；
  该 TDD 件属**另一域**（另一 CHANGELOG）⇒ 命中停手条件②（改变对外契约语义）／④（跨域）⇒ **另立件**。
- 🔵 **销账（两张表同批，⛔ 不同改⇒判红）**：判据 `scripts/check_rest_response_field_subset.py` 的 `KNOWN_DRIFT`
  与 pytest 包装 `tests/design-regression/test_rest_response_field_subset_gate.py` 的 `EXPECTED_KNOWN_DRIFT`
  **同批 8 → 1**；`tests/README.md` §49 的计数与说明同步。
- 🔴 **判据读数（改前 → 改后）**：默认档 `rc=0 → rc=0`（⚠️ **8 条本来就在 `KNOWN_DRIFT` 明账里 ⇒ 默认档从未判红**，
  任务书「F3 转绿」的说法在本仓不适用；F3 在**真仓口径**下不触发，真判面是 **F4 登记册**）；
  `--strict`：点名 **8 张 → 1 张**；声明面 **55**／被判面 **40**／未解析面 **15** **三面读数不变**（棘轮未动）；
  反向读数 22 张／87 键 → 15 张／60 键（**不判红**的读数面，随卡补齐而下降）；`--self-test` **15/15**。
- **判据**：`python scripts/check_rest_response_field_subset.py`（`rc=0`）／`--strict`（`rc=1`，点名 1 张）／
  `--self-test`（15/15 ＋ 「跑完无残留」夹具根条目 0 → 0）／`pytest tests/design-regression`（全绿）。
- **`publish/**` 同步**：本目录 `data/rest/01-device-iot.js`、`data/rest/02-core-domains.js`、`CHANGELOG.md`
  三件按 `scripts/sync_design_to_publish.ps1` 的主映射**定点**拷贝到 `publish/api/…`（⛔ 未用 `-Apply`：
  `publish/**` 存在 `G-148` 假脏面，整体同步会改到与本件无关的字节）；
  `scripts/check_publish_content_parity.py --byte-exact` **rc=0**。
- ⚠️ **未改（明文残留，⛔ 非本件测量面，属另一件）**：本件改动面＝**响应字段声明**（＋ 为免自相矛盾而必须同改的
  `json` 示例／`response.body` 串／**被改名概念所在的那处叙述**）。以下**请求面**漂移**改前已存在**、`T-410` 判据**只判响应面**
  ⇒ 本件**不改**、**具名登记**（⛔ 不夹带）：① `rest-device-import-preview` 与 `-execute` 的 `body_params` 仍写
  `file: multipart` 而实现读 `body["rows"]`；② `rest-device-update` 的 `body_params` 仍声明 `name`
  （设备表**无 name 列**、实现忽略该键）；③ `rest-device-preference-configs-upsert` 的 `body_params` 仍写 `pref_value`
  而实现读 `body["value"]`（前端 `PreferenceConfigUpsertBody` 亦为 `value`）；④ `rest-device-preference-configs-reply-voice-confirm`
  的 `body_params`/叙述仍写 `voice_id` 而 `T-401` 后实现读 `{value}`（角色码）；⑤ `rest-bed-user-profile-preference-upsert`
  的 `body_params` 仍声明 `merge:boolean` 而实现按 `body["value"] is None` 判清除（前端只发 `{value}`）；
  ⑥ 若干 `pref_value` 示例值仍是旧「音色名」形态（如 `{ "voice_id": "zh-CN-Xiaoxiao" }`）。
  ⚠️ 另**待核实**（⛔ 非本件可裁）：`rest-device-preference-configs-reply-voice-preview` 的**实现自述**「试听失败不落库」
  与其**代码注释**「仅持久化 pending」＋ 失败分支的 `upsert(status="pending")` 似有出入 ⇒ 属**实现侧**文档/行为待核，
  ⛔ 本件**不动产品代码**（`src/**` 一字未改）。
- ⛔ **未改内容**：本条目为**追加**；`v106` 及更早条目**逐字保留**。

## v105 → v106：`T-404` —— 音色映射改 **per-provider**（「角色 × 语言 → **该 provider 的**音色名」）＋ **补记** `T-401` 的 `voice_role` 8 值 → 4 值（2026-09-27）

> 来源＝**需求方 2026-09-27 原话**（逐字）：「**音色是每个 provider 不一样的**（我们之前从小米那拿了一些音色去做对应）⇒ **角色到音色的这种对应关系，当然是每个 provider 会有自己的一张表、一张对应关系**；**无论怎么降级都是去找 provider 对应的这样的一个映射关系就好了**」。⇒ 落为 `T-404`（本目录 `data/rest/02-core-domains.js` ＋ 域 03 的候选实体卡）。
> 🔴 **A. 件② 补记（`T-401`，`7038b368`，2026-09-26）**：该件把 `voice_role` 的 **8 值**（年龄段×性别×语气）**取代**为 **4 值**角色（`boy`/`girl`/`young_man`/`young_woman`），改了 `design/03-entity-relationship/data/07-event-infrastructure.js`、本目录 `data/iot/smart-bed.js`（＋ `data/iot/smart-bed-examples.js`），`publish/**` 三件**已逐字节同步**；⚠️ **但本 CHANGELOG 当时漏记**（全文只有一条讲 `play_voice` 语义的旧条目提到 `voice_role`）⇒ **本条补记**。语义要点：⛔ 码名**不带语气后缀**（语气是合成参数 `style`、不是音色）；**音色名**由「语言 ＋ TTS 供应商」经**配置**解析（故切供应商时**无需改设备数据/角色**）。
> 🔴 **B. 件① 本次变更：音色映射的落点由「一处全局」改为「每个候选（provider）一行」** —— `GET/PUT /api/v1/ai/config/models/tts/voice-map` 的语义随之改为「**针对某个 provider 写它那张表**」；映射落在**候选行** `model_fallback_candidates.extra_params.voice_map`（⛔ **不新增表/列**；域 03 的候选实体卡同步登记该键）。

| 落点 | 改前 | 改后 |
|---|---|---|
| 本目录 `data/rest/02-core-domains.js`（TTS 音色映射两条端点） | **未登记**（`T-401` 建了端点、未回填本资产 ⇒ 属漏项） | 新增两条：`GET` ＝ 逐候选一行（含 `allowed_voice_names` ＝ **该 provider 的音色域**）；`PUT` ＝ **定点到某个 provider（候选行）**，含**定址规则**（给 `priority`／只给 `provider_key` 时链上须**恰好一条**／都没给时链上须**恰好一个候选**）与**校验**（每语言恰好 4 角色、音色名 ∈ **该 provider 的音域**，非法 **422**） |
| 域 03 `data/04-ai-agent.js` 的候选实体卡（`model_fallback_candidate`） | `extra_params` ＝ `tts: {voice, style}`、`asr: {language}` | `tts: {voice, style, voice_map}`；并写明 `voice_map` ＝ **该候选（provider）专属**的「角色 × 语言 → 音色名」，解析取**当前生效候选**那一行；`voice` 退回其本义（**没有角色可解析时**的候选默认音色，`T-201` 的候选绑定语义不变） |
| `publish/api/data/rest/02-core-domains.js`／`publish/er/data/04-ai-agent.js`（同源镜像） | 旧口径 | 同上，**逐字节同步**（`raw_equal=True`，见 journal） |
| `CHANGELOG` | v105 | **v106**（本条） |

- 🔴 **为什么不新增表/列**：候选行的 `extra_params` 本来就是「**该候选专属**参数」（ER 卡原文）；需求方要的「每个 provider 一张表」正好是「一行一张表」⇒ 落点天然存在，⛔ 不需要迁移、⛔ 不需要新表。
- 🔴 **`extra_params.voice` 的写入行为按新语义复核（逐条）**：`T-401` 的 `PUT` 曾把 **usage 级**的 `extra_params.voice` 对齐成 `zh.young_man`（当时只有一份全局映射 ⇒ 一个候选默认值尚可代表它）。**改为 per-provider 后该写法不再成立**：① 一个候选级的默认音色**无法**同时对齐 4 个角色（同一 provider 的 4 个角色本就该是 4 个不同音色）② 写 usage 级那份是**跨 provider** 的（会把别家的名字写成全局默认）。⇒ `T-404` 保留「**对齐**」这一意图但**按 provider 落在该候选行**；`V9`（`PUT …/chain`）本就要求 `tts` 候选行 `voice` 非空，故该键**不消失**，只是从「覆盖角色解析值」退回「**没有角色可解析时**的候选默认音色」。
- 🔴 **向后兼容（既有那份全局 `voice_map` 的处置）**：**保留、⛔ 不迁移**。`T-401` 写在 usage 级的那份**降级为「缺省表」**：① 无候选行的 `D-144` 单候选回退档下，链解析出的那个单候选 `extra_params` **逐字取自** usage 级 ⇒ 它就是**那个候选自己的表**（⇒ 单候选部署**零行为变更**）；② 其余候选**没有**自己那张表时兜底，⚠️ 但**按 provider 门控** —— `DEFAULT_VOICE_MAP` 的 9 个名字是**小米 MiMo 官方**音色，**只有 `mimo`** 能用它兜底；别家若拿到 MiMo 的名字 ⇒ 那正是 `T-201` 点名的阿里云百炼 **`411`** 根因。⇒ 别家候选在缺表时按 `T-201` 用它**自己的** `extra_params.voice`。
- ⚠️ **如实登记的限制（未在本件解决）**：**每个 provider 的完整音色目录在仓内仍缺** —— 权威名单 `TTS_VOICE_NAMES` 只覆盖 MiMo 官方 9 个（`T-401` 已声明「名单内容的校准另行处置」）。⇒ 本件对**别家**的校验域取「该 provider **在链上已在用的音色名**」（实测：`bailian` 回填链上的 `longanxiaoxin`/`longanlingxin`/`Cherry`）—— ⛔ 不臆造目录，但也意味着**别家暂时只能在这几个名字里选**；每 provider 的正式音色目录属**待决事项**（需 provider 侧清单）。
- **判据**：`node scripts/check-viewer-data-consistency.mjs` ／ `node scripts/check-design-asset-render.mjs` ／ `file://` api-viewer 与 er-viewer 实渲染 0 page error（原始读数见 `.pipeline/journal/T-404.journal.md`）。
- **publish 同步**：本域 `data/rest/02-core-domains.js` 与域 03 `data/*.js` 均在映射表内 ⇒ **定点**拷贝（⛔ 未用 `-Apply`：`publish/**` 存在 `G-148` 假脏面，整体同步会改到与本件无关的字节）；`scripts/check_publish_content_parity.py --byte-exact` **rc=0**。
- ⛔ **未改内容**：本条目为**追加**；`v105` 及更早条目**逐字保留**。

## v104 → v105：`Q-007` / `M7` —— 过期行号引用改指**条目号锚** `RB-018`（**仅修指针，设计语义零改**）（2026-09-22）

> 来源＝**第 3 层设计工作区** `.pipeline/design/Q-007/`（`M7`，用户 2026-09-22 点头 ⇒ **第 1 层的设计资产变更任务**，走「设计资产变更六步」）。
> 🔴 **病灶**：RB 册已由 `Q-007`/`M4`（`6486b199`）收口为**一张索引表**（实测 **92 行**）：登记表与「`## 🔴 受保护行`」**整段逐字节迁入** `.pipeline/journal/_archived/requirements-backlog-archive-2026-09-23.md`。⇒ 本目录原先指向 RB 册的三处旧行号 **早已越界**（判据 `scripts/check_doc_ref_health.py` 判 `oob`）。
> ✅ **改法（统一口径）**：那三处旧行号 → 条目号锚（`:786`→`RB-010`｜`:1730`→`RB-016`｜`:1838`→`RB-018`，映射自述见归档件「`## 🔴 受保护行`」段），**改指条目号而非行号**。
> ⚠️ **本条刻意不写出「文件名＋行号」的完整形态**（本目录不得再产生可解析的 `file:line` 引用）：否则判据会把**本条说明本身**算成一处新的 `oob`。

| 落点 | 改前 | 改后 |
|---|---|---|
| 本目录 `CHANGELOG.md` 的 `T-278` 历史条目（本文件内那一处指针） | 旧行号引用（RB 册 + `:1838`） | `` `RB-018`「条目号锚 · 原文＝`.pipeline/journal/_archived/requirements-backlog-archive-2026-09-23.md` 的「RB-018｜…」段」 `` |
| `publish/api/CHANGELOG.md`（同源镜像，逐字一致） | 同上（旧指针） | 同上（新指针）—— 本批**定点同步** |
| `CHANGELOG` | v104 | **v105**（本条） |

- ⛔ **本件设计语义零改**：`design/04-platform-api/data/**`（含 `rest/02-core-domains.js`）**一字未动**，只换「指针 token」；实测本目录**再无**指向 RB 册的行号引用。
- 🔵 **括号纪律**：该处原文的 token **正贴着一个 `）`** ⇒ 插入串若自带 `（）` 会立刻产生 `））`。故插入串**括号中性**（只带 `「」`），插入前后整行括号计数**逐字不变**。
- ⛔ **未改内容**：本条为**追加**；`v104` 及更早条目**逐字保留**（`:147` 那处是 `T-278` 的历史指针 ⇒ **只改该指针，其余文字一字不动**）。

## v103 → v104：`T-332` —— 承载 `Q-003` 出口 2a：`copyWriting` 口径由「成品句／唯一建议文本」订正为「**模板串 ＋ 同 VO 参数**，**拼装在我方**」（2026-09-20）

> 来源＝**用户在另一会话的第 3 层设计稿 §4.2**（**出口 2a** ＝ 意图已定、设计资产没落笔 ⇒ **第 1 层的设计资产变更任务**；⚠️ 依规则 0c／`J7a`，⛔ **本目录不得写它的路径**；已由编排器按 **`D-161`** 取件并落为 `T-332`；证据＝**已入库快照** `design/01-raw-input/anyishui-api-docs/snapshot-2026-09-20/`（commit `86d1f5e8`））。⛔ **本件只改设计资产 ＋ `publish/**` 定点同步，⛔ 不写产品代码**（占位符兜底修复 ＝ 另一件 `T-331`）。
> 🔴 **新口径（权威）**：`asleepRespVO.copyWriting` / `bodyFunctionRespVO.copyWriting` ＝ **模板串**（内含 `{key}` 占位符）；参数取自**同一 VO**（`asleepRespVO` ← `reportDays`/`avgAsleepTime`/`stayingUpLate`；`bodyFunctionRespVO` ← `x`/`y`/`z`）；**平台已停止服务端替换**（2026-09-20 实测）⇒ **替换/拼装责任在我方**（残留 `{...}` ⛔ 不外显）。
> 🔴 **实测证据（两路独立）**：① **行为侧** ＝ `Q-003/evidence/copywriting-contrast.json`（同一份 2026-08-30 历史报告：存档读数`A`＝成品句「最近7天您的日均入睡时间是20:44，有1天属于熬夜情况。」／2026-09-20 现场读数`B`＝模板串，除两个 `copyWriting` 与滚动基准 `y` 外 16 项内容字段逐项相同）；② **文档侧** ＝ 归档快照 `design/01-raw-input/anyishui-api-docs/snapshot-2026-09-20/`（`T-333`，commit `86d1f5e8`）—— **新旧两版文档对两个 `copyWriting` 描述都只写「说明文案」、`example` 均为空**（⇒ **2026-07-06 那次文档变更并没有把 `copyWriting` 定义成模板**）；**全 26 条接口正文里「模板」仅出现 1 次**、且语义无关（`abl.device.realtime.query` → `hearPoints` 的「心率模板合成标记点」）；`deepSeekAnalysis` 仍在册（`abl.sleep.formerly.query`）。
> ⚠️ **归因纪律（`Q-003` §2）**：**「文档改版」与「行为变化」是两件独立的事，⛔ 不得合并归因**——**文档侧从未承诺过服务端替换** ⇒ 这是**我方拼装责任**，⛔ **不是对方行为回归**。

| 落点（`sleep-data-pull-protocol-plan.md`） | 前 | 后 |
|---|---|---|
| `:65`（§2.3 实测关键结论） | 「`asleepRespVO.copyWriting` 是**唯一建议文案**（平台未启用 deepSeekAnalysis，恒 None…）」 | 「`asleepRespVO.copyWriting` 是**模板串**（含 `{reportDays}`/`{avgAsleepTime}`/`{stayingUpLate}` 等同 VO 参数占位符；`bodyFunctionRespVO.copyWriting` 同形 ← `x`/`y`/`z`）——⛔ 不是「唯一建议文本」/成品句：**平台不再服务端替换**（2026-09-20 实测）⇒ **拼装在我方**（按同 VO 字段替换、残留 `{...}` 不外显）。**平台未启用 deepSeekAnalysis，恒 None——AGENTS.md 踩坑记录**（🔵 该半句**逐字保留**、属另一件事） |
| `:272` 字段表（`devices[].report.asleep.copywriting`） | 说明＝「入睡建议文案（**唯一建议文本**）」；来源列＝`copyWriting` | 说明＝「入睡建议文案——**模板串 ＋ 同 VO 参数**：占位符 ← 同 VO 的 `reportDays`（本 payload ＝ `report_days`）/`avgAsleepTime`（`avg_asleep_time`）/`stayingUpLate`（`staying_up_late`）；**平台不再服务端替换**（2026-09-20 实测）⇒ **拼装在我方**，⛔ 不得当作成品句/唯一建议文本」；来源列＝`copyWriting`（**模板串**） |
| `:292` 字段表（`devices[].report.body_function.copywriting`） | 说明＝「机能文案」；来源列＝`copyWriting`（**无参数映射**） | 说明＝「机能文案——**模板串 ＋ 同 VO 参数**：占位符 ← 同 VO 的 `x`（当日 rmssd）/`y`（当日 rmssd 基准）/`z`（统计天数）；**拼装在我方**」；来源列＝`copyWriting`（**模板串**） |
| `:358`（§4.3 示例报文 · `asleep.copywriting`） | `"过去7天...平均入睡时间22:57...建议..."`（**成品句形态**） | `"最近{reportDays}天您的日均入睡时间是{avgAsleepTime}，有{stayingUpLate}天属于熬夜情况。"`（**平台原始返回形态 · 逐字实测原文**） |
| `:382`（§4.3 示例报文 · `body_function.copywriting`） | `"您本次夜间睡眠的心率变异性（HRV）为53..."`（**成品句形态**） | `"您本次夜间睡眠的心率变异性（HRV）为{x}，低于您之前{z}天的个人基准值{y}。"`（**逐字实测原文**） |
| `:407+`（§4.3 示例报文后） | —— | **新增一条注记**：两个示例值＝平台**原始返回形态**（暴露占位符与参数来源，占位符键名 ＝ 同 VO 字段名）；**替换（拼装）在我方归一化完成**、**下行 payload ⛔ 不得出现裸 `{...}`**（替换后仍残留 ⇒ 该句不输出）；⛔ **设备固件不解析占位符** |

- 🔵 **判定项 1 结论（示例报文 `:358`/`:382` 改没改）＝ 改**。理由三条：① 原值是**成品句形态**，正是本件要订正的过期口径，留着会在**定义该字段的同一份文档**里反向教回旧口径；② 改成模板串**不涉及臆造** —— 两个串是 `Q-003` 现场读数的**逐字原文**（占位符键名与同 VO 参数名**已双向实测复核**：快照侧 `asleepRespVO`/`bodyFunctionRespVO` 子字段确为 `reportDays`/`avgAsleepTime`/`stayingUpLate` 与 `x`/`y`/`z`，行为侧 `B_live` 串按此映射替换后与 `A` 存档成品句**逐字相同**）；③ 加注记后**语义完整**：示例示上游形态、注记钉死「拼装在我方、下行不得裸露占位符」，与 §4.3 既有的「设备固件直接取用」**不冲突**（取用的是**拼装后**文本）。⚠️ 该 §四 已随 `T-032` 标注**作废**（`sleep_report_data_push` 等三条 push 已删）⇒ 本项属**历史留痕面上的口径订正**，⛔ 不据此重开已删 action。
- 🔵 **判定项 2 结论（同族陈旧表述扫描）**：全仓 grep `成品句` / `唯一建议文本` / `现成文案` / `唯一建议文案` / `copyWriting` ⇒ **本域（`design/04-platform-api`）内只有上表三处**；跨域命中的 `design/05-backend-architecture/**` 两条同族（`anyishui-sleep-integration.md` / `sleep-report-analysis-T-029.md`）与本件同一 commit 一并订正，见 `design/05-backend-architecture/CHANGELOG.md` v58→v59。⛔ **未改**的同类命中已逐条判为「非陈旧口径」或「不得改」，清单见 `.pipeline/journal/T-332.journal.md` §4。
- ⛔ **不改历史**：本条目为**追加**；`v103` 及更早条目**逐字保留**（含 `:44`/`:216`/`:221`/`:223` 的「建议文案」「文案透传」等**中性字段标签** —— 判为**不属**本件要订正的「成品句/唯一建议文本」口径，⛔ 未动，理由见 journal）。
- **判据**：`node scripts/check-viewer-data-consistency.mjs` / `node scripts/check-design-asset-render.mjs` / `file://` api-viewer 逐 type 实渲染 0 page error（原始行见 `.pipeline/journal/T-332.journal.md`）。
- **publish 同步**：`sleep-data-pull-protocol-plan.md` ⛔ **无同源副本**（该域映射表只含 `data/*.js` ＋ viewer 等，不含本 `.md`）⇒ **未去"同步"**；本域**唯一**被映射的 `CHANGELOG.md` 已手工**定点拷贝** `publish/api/CHANGELOG.md`（⛔ 未用 `-Apply`；`raw_equal=True` 证据见 journal）。同步门禁既有基线 `GREEN(0)` **仍为 0**，`scripts/check_publish_content_parity.py --byte-exact` **rc=0**。

## v102 → v103：`T-330` —— 承载 `G-263`：域 04 知识库检索的「embed 故障回退 ILIKE」陈旧口径订正为**读路径三态**（2026-09-20）

> 来源：挂账 **`G-263`**（承担任务 `T-330`；口径＝**代码为权威**，出口 2a）。⛔ **本件只改设计资产、不写产品代码**。
> 🔴 **权威锚点（代码实测）**：`src/server/modules/ai_agent/api/router.py:281-282`（响应新增 `status`（`hit`/`miss`/`unavailable`）
> ＋ `available` ＋ `model`；且明写「`unavailable`（embedding 不可用/超时）**语义上不是"无命中"**」）·
> `:286` 的 `K2` 裁决「**保留显式 `mode=keyword`**」· `infrastructure/repositories.py:231`／`:308`（`RB-010` B3 已删 ILIKE 假兜底）。
> 🔴 **DEPRECATED 开关**：`knowledge_iliike_fallback_on_no_result` ＝ `False` ＋「DEPRECATED（无消费方）」
> （`src/server/config/settings.py:491-497`）⇒ 设计文本**不得**写成"可开启 ILIKE 兜底"。

| 落点（`data/rest/02-core-domains.js`） | 变更 |
|---|---|
| 域 `implementation.note`（`:53`）的「2026-08-26（T-009）」段 | 🔴 **删「embed 故障回退 ILIKE（零破坏）、「语义无结果→再 ILIKE」配置项默认关」** ⇒ 改为「**订正（`RB-010`/`T-256a` B3）：删除了「embed 故障回退 ILIKE」——检索失败**无兜底**、读路径三态 `hit`/`miss`/`unavailable`（`unavailable` 绝不伪装成「无命中」）；旧配置项 **DEPRECATED（无消费方）**，唯一保留的 ILIKE ＝ **显式** `mode=keyword`（`K2`，仅供管理端测试检索）**」。⚠️ 该 `note` 的其余内容**逐字未动**。 |
| `rest-knowledge-search` 卡（`GET /api/v1/knowledge/search`）`description`（`:77`） | 🔴 **删「embed 故障/超时回退 ILIKE 零破坏，「语义无结果→再 ILIKE」配置项默认关」** ⇒ 改为**读路径三态**（`hit`/`miss`/`unavailable`；`unavailable` **语义上不是「无命中」**、⛔ 不得表述为「知识库暂无该内容」）＋ 旧开关 **DEPRECATED（无消费方）** ＋ 唯一保留的 ILIKE ＝ **显式** `mode=keyword`（`K2`）；**同句追加取代注记**：原写的「对话链路经 `needs_kb` 确定性门 + 两段式」**已由 `RB-014`/`T-274`（2026-09-18）整体取代**（门与词表退役，改由 LLM 自主 tool calling）。 |

- **纯口径订正（⛔ 不改契约结构）**：`addressing`（method/path）· `request.query` 4 参数 · `response.status` · 权限码 · 端点数**均未动**；⚠️ `mode` 参数在实现里早已存在（`router.py:273`），本件**未**新增/删除任何参数 —— 只订正 `description` 文本口径。
- ⛔ **不改历史**：本条目为**追加**；`v102` 及更早条目**逐字保留**（含 `:53` 那条「本域残留 …… ⛔ 本件未改」的留痕 —— 该留痕所述残留已由本条目收口，历史行原样保留）。
- **判据**：`node scripts/check-viewer-data-consistency.mjs` / `node scripts/check-design-asset-render.mjs` / `file://` api-viewer 逐 type 实渲染 0 page error（原始行见 `.pipeline/journal/T-330.journal.md`）。
- **publish 同步**：手工**定点拷贝** `publish/api/data/rest/02-core-domains.js` ＋ `publish/api/CHANGELOG.md` 等（⛔ 未用 `-Apply`）；同步门禁既有基线 `GREEN(0)` **仍为 0**。

## v101 → v102：`T-326`（`RB-020` 改动面 ⑥）—— `rest-binding-list` 卡**新增只读可选字段** `stolenDetected`（**纯增量**）（2026-09-20）

> 来源：`RB-020`「三、改动面」第 ⑥ 项（＝ `T-324` 未交的残余之一；成因见 `.pipeline/journal/T-326.taskbook.md` 抬头）。⛔ **本件只改设计资产、不写产品代码**。
> 🔴 **实现面已在 `T-324` 落地（本件只是把设计卡补齐，⛔ 不重做）**：`services.py::_binding_dict` 增 `"stolenDetected"`（`:126`）；`list_bindings` 逐行**只读** `cache.is_stolen(...)`（`:280`，⛔ 不探测/不落库）。

| 项 | 变更 |
|----|------|
| `data/rest/02-core-domains.js` · `rest-binding-list` 卡（`GET /api/v1/sleep-monitor/bindings`）`description` | **末尾追加一句**（⛔ 不改既有句子）：`stolenDetected` ＝**纯增量**只读布尔键，取值＝**进程内缓存最近一次检测（≤60s）** 的结论（TTL 过期 ⇒ 回落 `false`），⛔ **非平台当前真值**；**仅本列表卡**声明（`rest-binding-device` 单床详情卡不带该字段 —— `T-324` 的有意最小面）。 |
| 同卡 · `response` | `{ status: 200 }` ⇒ `{ status: 200, body: { items: "object[]（每项含可选 \`stolenDetected: boolean\`）", total: "int" } }`。⚠️ **表达形态照本文件既有列表卡惯例**（`response.body` ＝ `{ items, total }`，同族 6 处：本文件 `:463`／`:576`／`:810`、`01-device-iot.js:137`／`:442`／`:1028`、`system.js:156`）—— ⛔ **未发明新形态**。 |
| 同卡 · `addressing`／`request`／权限／租户圈定（T-108） | ⛔ **逐字未动**（`request.query` 4 参数原样；⛔ 未加参数）。 |

- **纯增量（⛔ 不改既有字段/值域）**：不删不改任何既有键；`response.body` 由旧卡的**未声明**变为声明（新增信息，非改语义）；`bindingStatus` 值域**仍 3 值**（被抢 ⇒ 标 `stolenDetected`，⛔ 不是第 4 个状态）。实测钉子：`tests/api/sleep_data/test_binding_stolen_visibility.py::test_stolen_flag_is_pure_increment_and_values_unchanged`（DTO 键集合 − 旧 12 命名键 **== `{"stolenDetected"}`**）。
- **计数**：本域端点卡数**不变**（⛔ 未增删卡；`stats` 由文件末 IIFE 自维护）。
- ⛔ **不改历史**：本条目为**追加**；`v101` 及更早条目**逐字保留**。
- **判据**：`node scripts/check-viewer-data-consistency.mjs` / `node scripts/check-design-asset-render.mjs` / `file://` api-viewer 逐 type 实渲染 0 page error（原始行见 `.pipeline/journal/T-326.journal.md`）。
- **publish 同步**：手工**定点拷贝** `publish/api/data/rest/02-core-domains.js`（⛔ 未用 `-Apply` —— `D-150` 明令暂缓、且会破坏 `T-308` 的 index 逐字节对齐）；同步门禁既有基线 `RED(5)` **仍为 5**（⛔ 未变 6）。


## v100 → v101：`T-identity-A` P1 —— `rest-binding-list` 的 `remoteUserId` 派生日径由「由床派生」订正为「**由睡眠监控设备 imei 派生**」（2026-09-19）

> 来源：`.pipeline/journal/T-identity-A.taskbook.md` §2.4-D5（**设计先行**，出口 2a）。⛔ **本件只改设计资产、不写产品代码**。
> 🔴 **为什么必须订正**：身份模型方案 A 下身份**按睡眠监测设备 `imei` 派生**（`zone` 降为槽位/数据标注），「由床派生」已不精确。

| 落点（`data/rest/02-core-domains.js`） | 前 | 后 |
|---|---|---|
| `rest-binding-list`（`GET /api/v1/sleep-monitor/bindings`）description | 「…remoteUserId 为外部平台账号（**由床派生**，内部标识，不体现用户概念）。T-108 租户圈定：…」 | 「…remoteUserId 为外部平台账号（**由睡眠监控设备 imei 派生**，内部标识，不体现用户概念；`zone` 仅槽位/数据标注，不参与身份派生）。T-108 租户圈定：…」 |

**⛔ 未改**：`rest-binding-unbind` 的 `zone?` 语义（`zone_required` ⇒ 409）· **REST DTO 字段集**（不新增 `remotePhone`；`failure_reason` / `remote_phone` 均不上 DTO）· 端点计数 · 协议与 `iot/*` · 本 CHANGELOG 历史条目（v1~v100）。
⚠️ **残留（未确证，⛔ 本件未改）**：同文件 `rest-report-daily` 的 description 仍写「外部账号**由床派生**」——属同一类陈旧措辞，本任务书 §2.4-D5 只点名 `rest-binding-list` 一处，故**留待后续件**统一。

**判据**：规则 0b 三项（`check-viewer-file-render` / `check-viewer-data-consistency` / `check-design-asset-render`）各 `RC=0`；publish `-Apply` 后**独立复跑 `-Check` = `GREEN`**。

## v99 → v100：`T-305` —— `G-258` 收口②：`rest-maintenance-non-event-logs` 的 `log_type` 取值面由 2 值补齐为 **10 值**（2026-09-19）

> 来源：挂账 **`G-258`**（承担任务 `T-305`；口径＝**代码为权威**，出口 2a）。⛔ 本件只改设计资产、不写产品代码。
> 🔴 **代码实测（判据）**：`src/server/modules/device_control/infrastructure/non_event_log_handler.py:47-52`
> 的 `_SUPPORTED_TYPES` ＝ **10 值**（第 10 值 `ai_route_alert` 由 `T-293`／`MF-T14`（commit `27837e0f`）引入）；
> 前端 `NON_EVENT_LOG_TYPES`（`nonEventLog.ts:34-47`）与 i18n `routes.maintenance.non-event-log.types`
> （`zh.json`，10 键）**同序同集**。

| 项 | 变更 |
|----|------|
| `data/rest/02-core-domains.js` · `rest-maintenance-non-event-logs` 卡（`:335`）`query_params` 的 `log_type` **取值列** | 🔴 **`parse_failed/system` ⇒ 全量 10 值**（`parse_failed/processing_failed/system/duplicate/ack/skipped/stream_frame/stream_summary/downlink/ai_route_alert`）；描述列同时标明「全量 10 值，同 ER `NonEventLogType`／后端 `_SUPPORTED_TYPES`」。 |
| 同卡 · `description`（`:333`） | **同一 2 值口径的使用点，一并订正**：「（non_event_logs：协议解析失败原文 + 系统运行日志，无限滚动）」⇒ 写明共 **10 类 `log_type`**。⚠️ 该行不在 `G-258` 点名的四处内，但不同改就会与刚补齐的取值列**在同一张卡内自相矛盾**。 |
| 同卡 · 其余字段（`addressing`／`permission`／`business_logic`／`responses`／`errors`／`caching`） | ⛔ **逐字未动**（⛔ 未增删端点卡）。 |

- 🔴 **裁决留痕：为什么是「补齐到 10 值」而不是「显式声明有意子集」** —— 该参数即按 `enum:NonEventLogType` **全集**校验（后端 `_SUPPORTED_TYPES` 白名单），意图早已确定（**出口 2a**）⇒ 声明子集只会再造「设计有、代码无」的偏差。与 `design/03-entity-relationship/data/CHANGELOG.md` 的 v7.44→v7.45 同批同口径。
- **计数**：本域端点卡数**不变**（⛔ 未增删卡）。
- ⛔ **不改历史**：本条目为**追加**；`v99` 及更早条目**逐字保留**。
- 🔴 **同域残留（⛔ 本件未改，如实上报）**：本文件 `:53`（域 `note`）与 `:77`（`rest-knowledge-search` 卡）仍写「embed 故障回退 ILIKE」—— 那是 **`G-262` 的同族陈旧口径**（知识库检索面），⛔ **不属** `T-305` 的 `G-258` 四处锚点 ⇒ **未动**，建议另立小件统一收口（同族清单见 `design/02-business-workflow/data/CHANGELOG.md` v26→v27 的「同族残留」行）。
- **判据**：`node scripts/check-viewer-data-consistency.mjs` 的 `api-rest` = 0 页错误；`node scripts/check-design-asset-render.mjs` RC=0（原始行见 `.pipeline/journal/T-305.journal.md`）。
- **publish 同步**：`scripts/sync_design_to_publish.ps1 -Apply -Confirm:$false` ⇒ `-Check` 须 `GREEN`（卡面经 `data/rest/*.js` 映射同步；本文件经**扩展映射行**同步到 `publish/api/CHANGELOG.md`）。
- 🔗 **同批落点**：`design/03-entity-relationship/data/CHANGELOG.md` v7.44→v7.45 · `design/06-web-admin/data/CHANGELOG.md` v32→v33 · `design/02-business-workflow/data/CHANGELOG.md` v26→v27。


## v98 → v99：`T-302` —— `G-259` 收口：`rest-ai-model-route-stats` 卡的「13 项指标 + **9 条**告警」订正为「**8 条**」（＋ 订正历史条目 v97 → v98 里的同一处口径）（2026-09-19）

> 来源：挂账 **`G-259`**（承载改判给 `T-302`；`T-302` 的**主体**是兜底链并入，见
> `design/05-backend-architecture/CHANGELOG.md` 的 `v55 → v56` —— 本条是**同批顺带**的设计面，⛔ 不是另一件事）。
> 🔴 **口径依据 ＝ 代码实测，⛔ 不是推测**：`src/server/modules/ai_agent/application/route_alerts.py`
> 的 `ALERT_RULES` 实测 **8 条**（`:84-91`，`A1`~`A8`）；`A9`（检索质量下降）**已退役** ——
> `ALERT_A9_RETIRED` / `ALERT_A9_RETIRED_REASON`（`:96-97`，⛔ **不在** `ALERT_RULES` 里）。
> **退役理由（原文见 `:20-22`）＝ `D-147`**：`ai_fallback_total` 的写入点**只有 `P0/P1/P2`**
> （`conversation_coordinator._fallback_text()` 的 `FALLBACK_LEVELS` 里没有 `P3`）⇒ `A9`「embedding
> `P3` 兜底超阈值」**没有任何可判的量**（恒不触发）⇒ `T-293` 按「⛔ 不为凑数编一条永不触发的告警」删除。

| 项 | 变更 |
|----|------|
| `data/rest/02-core-domains.js` · `rest-ai-model-route-stats` 卡（`:106`） | 🔴 **「13 项指标 + 9 条告警」⇒「13 项指标 + 8 条告警」**，并就地把依据写进卡面（`A9` 退役 ＋ `route_alerts.py:84-91`/`:96-97` 锚点）。⚠️ **只改这一处口径文本** —— 该卡的 `stats.mode` 闭集（`G-246`，`T-294`）等其余内容**逐字保留**。 |
| 本文件 · **历史条目 `v97 → v98`**（`G-259` 立案时的行号 `:37`；⚠️ 本条目追加后该行号顺移） | ⛔ **不改历史条目**（逐字保留）⇒ 该条目里的「**13 指标 ＋ 9 告警**」一处在**本新增条目**里**显式订正**：**应读作 8 条告警**（理由同上）。 |
| `design/design-decisions.js` · `DD-039` 的 ⑧（该文件在 `design/` 根，⛔ 不在 `04-platform-api/` 下） | 同批订正「9 条 ⇒ **8 条**」＋ 依据（`G-259` 的另两处，见 `design/05-backend-architecture/CHANGELOG.md` 的 `v55 → v56`）。 |

- **计数**：本域端点卡数**不变**（⛔ 未增删卡；`stats` 由文件末 IIFE 自维护 ⇒ 自动同步）。
- ⛔ **不改历史**：本条目为**追加**；`v98` 及更早条目**逐字保留**（`git diff --numstat` 的 deletions 仅来自 `:106` 那一行卡面）。
- **判据**：`node scripts/check-viewer-data-consistency.mjs` 的 `api-rest` 等视图 = 0 页错误；`node scripts/check-design-asset-render.mjs` RC=0（原始行见 `.pipeline/journal/T-302.journal.md`）。
- **publish 同步**：`scripts/sync_design_to_publish.ps1 -Apply -Confirm:$false` ⇒ `-Check` 须 `GREEN`（本文件经**扩展映射行**同步到 `publish/api/CHANGELOG.md`；卡面经 `data/rest/*.js` 映射同步）。


## v97 → v98：`T-294` —— `G-246` 收口：`rest-ai-model-route-stats` 卡补写 `stats.mode` 的**取值闭集** `{live, skeleton}` ＋ `modeKnown`（2026-09-18）

> 来源：挂账 **`G-246`**（`T-294` 同批收口；⚠️ 本件的**主体**是事件字段改造，见
> `design/05-backend-architecture/CHANGELOG.md` 的 `v54 → v55` —— `G-246` 是**顺带**的设计面，⛔ 不是另一件事）。
> 🔴 **口径依据 ＝ 后端实测，⛔ 不是推测**：`src/server/modules/ai_agent/api/router.py:793`
> `ROUTE_STATS_MODES: Final[tuple[str, ...]] = (ROUTE_STATS_MODE_LIVE, ROUTE_STATS_MODE_SKELETON)`
> （其上方 `:786-789` 的表格就是三档语义的权威陈述）＋ `:1097` 响应字段 `"modeKnown": list(ROUTE_STATS_MODES)`。
> 前端（`statsModeLabel()`）已按**闭集穷举**、**闭集外一律显示「未知（取值域外，不下结论）」** ⇒ 设计卡此前
> **没有写闭集**，属"契约缺失"（`G-246`）。

| 项 | 变更 |
|----|------|
| `data/rest/02-core-domains.js` · `rest-ai-model-route-stats` 卡（`description`） | **追加**：`stats.mode` 的**闭集** `{live, skeleton}` 逐档语义（`live` = 求值器＋写入点都已落地「运行中」；`skeleton` = 只有名单、值全 `null`「未实现（骨架）」）＋ **闭集外/字段缺失** ⇒ 「**未知**」（⛔ 不是「运行中」、⛔ 不回落成闭集任一项）＋ 后端**随响应回传** `modeKnown`（顺序即闭集顺序）＋ 两处锚点（`router.py:793` / `:1097`）＋ 前端标签口径（`statsModeLabel()`）。⚠️ 既有句**一字未删**（追加式）。 |
| `data/rest/*.js` 其它卡 | ⛔ **未改**（本件只收 `G-246` 一条；⛔ 未顺手夹带 `G-235`/`G-238`/`G-245` —— 各有归属任务）。 |

- **计数**：本域端点卡数 **不变**（⛔ 未加卡、未删卡；`stats` 由文件末 IIFE 自维护 ⇒ 自动同步）。
- ⛔ **不改历史**：本条目为**追加**；`v97` 及更早条目原样保留。
- **同步面**：`publish/api/data/rest/02-core-domains.js`（`scripts/sync_design_to_publish.ps1 -Apply` ⇒ `-Check` GREEN）。
- ⚠️ **属性说明**：`G-246` 的目标卡**运行期仍标「⛔ 未实现」**（本件只补**设计侧**闭集口径，⛔ 不改该卡的实现状态）。


## v96 → v97：`RB-018`（`T-278` 批次①）—— 模型降级：**4 个 REST 端点**入档 ＋ 订正「8 用途」漏改残留（2026-09-18）

> 来源：`RB-018`（**出口 2a / 用户已批准**：用户 2026-09-15 逐字「我审核过了，没有问题，可以升级到 RB」，
> 落档 `RB-018`「条目号锚 · 原文＝`.pipeline/journal/_archived/requirements-backlog-archive-2026-09-23.md` 的「RB-018｜模型降级（多供应商故障转移）」段」）。契约正本＝第 3 层工作区
> `design-workbench/model-failover/contract-2026-09-15.md` §二（稿已按第 3 层工作区晋级协议（`design-workbench/` 的 `README`）§3.1 标
> `status: approved` / `approved-by: user`）。
> ⛔ **本批只并入设计资产，不写任何产品代码**：4 张卡均为**设计侧新增**，实现属 `RB-018` 后续批次（卡面上已逐张标注「⛔ 未实现」）。
> ⚠️ 设计正本 `design.md:10` 曾写「⛔ 不改本文件（该文件当前有他人未提交改动）」—— 那是 **2026-09-15 的时点状态**；
> 本批该文件已干净，且 `RB-018` §三 明确把 API 卡同步列为并入范围 ⇒ **本批照范围落笔**。

| 项 | 变更 |
|----|------|
| `data/rest/02-core-domains.js` · `domain-ai-agent` | **新增 4 张端点卡**（设计侧，⛔ 未实现）：① `GET /api/v1/ai/config/models/usages/{usage_key}/chain`（读候选链：`failover_enabled` 生效值 / `required_traits` / `chain[]`（含 `traits_ok` / `credential` / **进程内** `health` 四态 / `last_switch` / `last_switchback`）/ `effective_priority`；`usage_key ∉ KNOWN_USAGES` ⇒ 404）② `PUT` 同路径（**整体替换**，后写覆盖无乐观锁；**保存期校验 `V1`~`V9` 逐条进 422、⛔ 不静默修正**：`unknown_usage` / `too_few_candidates`（enabled 候选 ≥3）/ `bad_priority` / `unknown_model`（**列出合法候选**，取代旧的静默回落）/ `unknown_provider` / `kind_mismatch` / `traits_missing` / `not_configured` / `bad_extra_params`；🔴 **`V8` 两档**＝迁移回填允许 `not_configured` 入链、**交互式保存强制**）③ `POST /api/v1/ai/config/models/providers/{provider_key}/probe`（**应急**探测，⚠️ 自动回主不依赖它、且**真的会调付费接口**）④ `GET /api/v1/ai/model-route/stats`（13 指标 ＋ 9 告警；形态照抄 `GET /ai/cache/stats`；⛔ 不引 Prometheus）。权限：`GET` ⇒ `ai_agent.read`，`PUT`/`POST` ⇒ `ai_agent.write`（照 `ai_agent/api/router.py:739/752`）。 |
| `data/rest/02-core-domains.js` · `rest-ai-config-model-usages-list` 卡 | 🔴 **订正漏改残留（顺带，非 `RB-018` 本体）**：「**8 用途**」→「**7 用途**」。`T-274`/`RB-014` 已把 `realtime`（联网搜索）**整体退役**（`RB014-D1=C` / `D-141` ①）⇒ 实测 `KNOWN_USAGES` **7 个**（`model_router.py:110-118`）；该卡是 `T-274` 批次⑤ 的**漏改残留**（`git log -S` 显示该串仍是 `T-005` 的旧值），本批一并订正。 |

- **计数**：本域端点卡 **+4**（`stats` 由文件末 IIFE 自维护 ⇒ 自动同步，⛔ 未手改计数）。
- ⛔ **不改历史**：本条目为**追加**；`v96` 及更早条目原样保留（`git diff --numstat` 的 deletions ＝ 0）。
- **同步面**：`design/05-backend-architecture/data/resilience-policy.js`（`modelFailover` v4）· `design/03-entity-relationship/data/04-ai-agent.js`（`model_fallback_candidate` ＋ `failover_enabled`）· `design/design-decisions.js`（`DD-039`）。


## v95 → v96：`RB-016`（`T-276`）—— 协议**承载层可选性声明**：MQTT 明文标为可选链路 ＋ 订正四处「云端同时支持两种链路」（2026-09-18）

> 来源：`RB-016`（**出口 2a**）。`RB016-D4=c`：口径落点＝**协议文档**（`data/iot/protocol.js`）。
> 🔴 **用户 2026-09-16 逐字口径**：「**是否允许选择性实现，是协议规定的范围，而不是项目级的东西**……
> 只有当协议里明确规定了哪些东西可以允许选择实现，那它才有效。否则的话，**所有的协议都是要被实现的**……
> 我们内部的这些接口，不能说写了之后不实现也可以。」
> ⇒ 本条把**协议自己该说的那句话补上**（哪些可选）；⛔ **不是**用「协议与实现不必一一对应」去解释「为什么没实现」。

| 项 | 变更 |
|----|------|
| `data/iot/protocol.js` · **新增顶层 `transportOptionality`** | 协议规范条款（插在 `transports` 与 `transportComparison` 之间）：`implemented`＝**WebSocket**（云端唯一已实现链路，未声明可选 ⇒ 按口径**必须实现**）／`optional`＝**MQTT**（**明文声明的可选链路**，云端当前未实现，**不构成欠账**；若设备选型该链路 ⇒ **另行立项**）／`scope`＝⚠️ **只覆盖承载层这一项**，⛔ 不得推广成项目级挡箭牌。 |
| `data/iot/protocol.js` · `transports[]` | 各补 **`optional`**（mqtt `true` / websocket `false`）与 **`implementationStatus`** 两个字段；⛔ **MQTT 段完整保留**（承载参数 / Topic / JWT 认证 / 心跳 / 链路对比**一段不删**）。 |
| `data/iot/protocol.js` · 四处口径订正 | ① `transportComparison.notes` 新增第 5 条（可选性声明）——原「云端同时维护两套接入点」的口径以本条为准；② 部署模型渲染块「云平台负责 MQTT Broker + REST API + 数据面 WebSocket 统一入口」→ **REST API ＋ 设备接入 WebSocket 统一入口**（MQTT 承载层为可选、未实现）；③ WS 承载层「与 MQTT 关系」的「设备同时维持两路连接」→ **云端仅实现 WS（控制面与数据面共用同一 WSS 连接）**；④ 「链路选择建议」的「设备须支持 MQTT，WebSocket 选配」→ **现阶段设备必须支持 WebSocket**（MQTT 为协议可选链路、未实现）。 |
| `data/iot/protocol.js` · `version` / 文件头 / `changeLog` | `3.1` → **`3.2`**，`changeLog` 前置 v3.2 段（说明四项改动与依据）。 |
| `data/iot/smart-bed.js` · 文件头 | 新增**承载层口径**声明：各 action 的业务语义**与链路无关**；本文件的 MQTT 字样（传输层认证 / 单帧大小 / 遗嘱消息触发离线）均指**协议定义**，⛔ 不代表云端已实现该链路。⚠️ **action 条目零改动**（不改任何 action 名/字段/枚举）。 |

- **计数**：action 数、枚举、字段**一律不变**（本批只加**声明**与**口径订正**，不增删契约条目）。
- **判据**：`node scripts/check-viewer-data-consistency.mjs` 的 `api-iot`（`#/protocol-zyric-v1/smart-bed`）= 0 页错误；
  `node scripts/check-design-asset-render.mjs` RC=0（原始数字见 `.pipeline/journal/T-276.journal.md`）。
- **publish 同步**：`scripts/sync_design_to_publish.ps1 -Apply -Confirm:$false` ⇒ `-Check` 须 `GREEN`。
- 🔗 **同批落点**：`design/05-backend-architecture/CHANGELOG.md` v51→v52（架构侧幻觉清理 ＋ 同源声明）· `design/02-business-workflow/data/CHANGELOG.md` v24→v25。

## v94 → v95：`RB-014`（`T-274`）—— 撤「词表前置门」：两个关键词端点退役 + ai-tools 两节改写（2026-09-18）

> 来源：`RB-014`（**出口 2a**：用户裁决 `RB011-D4=A` / `RB014-D1=C` / `RB014-D2=B` / `RB014-D3=A`
> 已明确 ⇒ 第 1 层「设计资产变更六步」）。用户逐字原话见 `.pipeline/PENDING-DECISIONS.md` 的 `D-141`。

| 项 | 变更 |
|----|------|
| `data/rest/02-core-domains.js` · `rest-ai-config-keywords-list` | ⛔ **端点已退役** —— `GET`/`PUT /api/v1/ai/config/keywords*` 随两张词表（`realtime_keywords`/`needs_kb_keywords`）整体删除（词表前置门已删 ⇒ 词表无消费者）。**卡保留为留痕**，描述改「🔴 RB-014/T-274：已退役」。 |
| `data/ai-tools/tools.js` · 头注「系统级能力：web_search」 | 改为「**已整体退役**」（`RB014-D1=C`：实时信息能力下线）＋ 列出被删的 6 处落点。原文口径保留留痕。 |
| `data/ai-tools/tools.js` · 头注「系统级能力：knowledge_search」 | 改为「**常驻注入、无前置门**」：删 needs_kb 门 + KB 两段式叙事，写入「缺陷形态（词表门把设备控制措辞判进 KB 路径 ⇒ 床不会动）」＋ 现口径（同场在列、LLM 自主调用）＋ **I2 替代可观测性**（`kb_available` 与 `intent_class` 口径）。 |
| `data/ai-tools/tools.js` · `knowledge_search` 工具 description（顶层 + `openai.function`，2 处） | 与实现（`domain/tools.py` 的 `ToolSpec.description`）**逐字同步**：补「⛔ 设备控制指令不要走本工具（本工具不会让床动起来）」＋「本平台无联网搜索工具」。 |

- **计数**：REST 端点 **44 → 42**（退役 `GET /ai/config/keywords` 列表 + `PUT /ai/config/keywords/{config_key}` 更新，共 **2** 个）；AI-Tools 卡数 **38** 不变（`knowledge_search` 是内部工具，本就不进设备表）。
  ⚠️ 本批**未**改 `02-core-domains.js` 的端点数**声明字段**（`rest-ai-config-keywords-list` 卡保留为留痕，其 id 仍占位）—— 端点数声明由后续批次按只读勘察统一订正；此处的 44→42 是**实现侧真实端点数**（`router.routes` 实测少 2 条）。
- **判据**：`node scripts/check-viewer-data-consistency.mjs` 的 `api-api-rest` / `api-api-ai-tools` = 0 页错误；
  `node scripts/check-design-asset-render.mjs` RC=0；`tests/design-regression/test_cacheability_seams_gate.py` RL4b 同源。

## v93 → v94：AI 供应商口径清理（去火山引擎）（2026-09-18）

> 来源：**用户 2026-09-17 口述**（白名单＝DeepSeek／MiMo／dashscope，⛔ 不用火山引擎）。

| 项 | 变更 |
|----|------|
| `IMPLEMENTATION-PLAN.md` | 批次 5 标题「依赖 LangGraph + 火山云」→「依赖 LangGraph + 外部 AI 供应商：DeepSeek／MiMo／dashscope」（**实施计划文档，不是端点契约**） |

- **计数**：REST/ZYRIC/AI-Tools 的**卡数与节点数一律不变**（未动 `data/**`）。
## v92 → v93：`RB-011`（`T-270`）—— 天气独立化：新增 `GetWeather` 工具 + 分组 11→12（2026-09-17）

> 来源：`RB-011`（**出口 2a**：用户裁决 `RB011-D1`~`D7` 已明确 ⇒ 第 1 层「设计资产变更六步」）。
> **契约**：天气＝**独立表 `weather_entries` + AI Tool `GetWeather`**（LLM 自主 tool calling）；词表天气门与 web_search 兜底删除。

| # | 文件 | 变更 |
|---|------|------|
| ① | `data/ai-tools/tools.js` | **新增** `GetWeather` 工具条目（`action=weather_query`、`category=天气查询`、非设备控制类 ⇒ 不发布 PlatformCommand、**不在** `DEVICE_CONTROL_TOOL_NAMES`）；参数 `city`（必填，中文城市名，可带/不带「市」后缀）+ `date`（可选，`YYYY-MM-DD` 或 今天/明天/后天，缺省=今天）。工具头注原「系统级能力：天气知识门（T-024）」整块替换为 `RB-011` 取代注记（旧口径原文保留并标「⛔ 已被 RB-011 取代」）。`AI_TOOLS.categories` 新增 `"天气查询"`（icon ⛅，tools `["GetWeather"]`）。 |
| ② | `data/ai-tools/protocol.js` | 协议文本 `37 个可调用 Tool … 11 组` → **`38 … 12 组`**；Tool 分组表**新增一行** `⛅ 天气查询 / 1 / GetWeather`（平台自取数：先查 `weather_entries` 快路径，未命中工具内实时调 Open-Meteo；⛔ 不经网页搜索）；`categories` 同步新增 `"天气查询"`；注释「11 组」→「12 组」。 |

| ③ | `data/rest/02-core-domains.js` | 4 条 REST 契约描述订正：`rest-ai-cache-config-list`（配置键清单：删 `prefetch_hour_end`/`weather_kb_enabled`，增 `weather_tool_enabled`/`weather_retention_days`，并写明 `generic_result_cache_ttl_seconds` **无 weather**）；`rest-ai-cache-prefetch-config`（字段改 `weatherToolEnabled`/`retentionDays`，删 `hourEnd`）；`rest-ai-cache-prefetch-run`（写入目标 → **独立表 `weather_entries`**，⛔ 不再写 `knowledge_entries`/`generic_result_cache`）；`rest-ai-cache-result-list`（category 枚举**彻底移除 weather**，F5 收口）。 |

**计数**：AI Tools **37 → 38**（Tool 定义卡 38）；分组 **11 → 12**。REST 卡 **243 不变**（只改 4 条描述，不增删端点卡）、ZYRIC 一级节点数 **7 不变**。
- **publish 同步**：`scripts/sync_design_to_publish.ps1 -Apply` ⇒ `publish/api/data/ai-tools/{tools,protocol}.js` + `publish/api/data/rest/02-core-domains.js` + `publish/api/CHANGELOG.md` 逐字节同源。
- **判据**：`node scripts/check-viewer-data-consistency.mjs` = `[OK] 24 个视图 / 144 条断言 渲染器↔数据双向一致 + 0 页错误`；`node scripts/check-design-asset-render.mjs` = **EXIT 0**（本变更同步改了该脚本的**镜像期望**：`11 category / 37 卡` → `12 / 38`，并把 `天气查询` 加进期望数组；该期望表是渲染器↔数据的**镜像**、不是独立判据）；`file://` 实渲染 api-viewer `#/protocol-ai-tools/tools` 卡数 **38**、0 页错误。

## v91 → v92：`T-240`（`G-194`）—— `data/ai-tools/tools.js` 头注的 `annotate_cache` 收窄口径订正（2026-09-16）

> 来源：`T-240`（**出口 2a**：上游意图已由**真模型实测**明确 ⇒ 第 1 层「设计资产变更六步」）。
> **依据**＝`.pipeline/journal/T-226.journal.md` §5.1 的真模型原始行（`G1_stop_massage #10`）。

| # | 文件 | 变更 |
|---|------|------|
| ① | `data/ai-tools/tools.js` | 头注里 `annotate_cache` 的收窄口径：**删**「**只能收窄不能放宽**」＋「**`scope=capability` → 收窄 `global→capability`**」⇒ 改为「**只能收窄到 `none`**」＋「🔴 `T-240`：**`scope` 不参与判定**（层级由确定性层权威，`scope` 仅观测）—— 修复前 `scope=capability` 可把已判 `global` 的动作收窄成 `capability` ⇒ **跨设备复用丢失**（真模型原始行 `G1_stop_massage #10`）」。`annotate_cache` 的 schema 本体（`cacheable` 必填、`scope` 可选）**未改**（schema 唯一真相源＝`src/server/modules/ai_agent/domain/tools.py`）。 |

**计数**：AI Tools **37 不变**、REST 卡 **243 不变**、ZYRIC 一级节点数 **7 不变** —— 本版**只改 1 处头注文本**，
不增删任何端点卡/字段/枚举/schema 键。
- **publish 同步**：`scripts/sync_design_to_publish.ps1 -Apply` ⇒ `publish/api/data/ai-tools/tools.js` 逐字节同源。
- **判据**：`scripts/check_cacheability_seams.py` 的 **RL4/RL4b**（旧口径回归 / 双副本同源）实测 rc=0。

## v91：订正 2 处与协议不一致的动作卡描述（`T-175` 第 5 项，2026-09-15）

> **触发**：`T-173`/`T-174` 普查发现两处设计卡字段名与协议**不一致**，两代（demo/main）**逐字相同**
> ⇒ 客户**照卡调用即静默丢字段**（200 通过但字段到不了设备）。

| # | 文件 | 变更 |
|---|------|------|
| ① | `data/rest/02-core-domains.js` | `rest-massage-control` 描述：`speed+intensity+duration 三维度` ⇒ `intensity+duration+mode 三维度`（`mode` = 按摩手法 `pulse/wave/continuous`）。**原因**：协议 `massage` 的 `envelope.payload` 只有 `sub_device_id`/`intensity`/`duration`/`mode`（`data/iot/smart-bed.js`），**无 `speed`**；代码侧 `speed` 亦无读写点（仅 `router.py` 的 `_UNDECLARED_KEYS` 提及，全仓 0 引用）⇒ 按卡传 `speed` 会被**静默剔除**。 |
| ② | `data/rest/02-core-domains.js` | `rest-play-voice-message` 描述：`level/title/url` ⇒ `level/title/body`。**原因**：协议 `play_voice`（`T-030` 起）载荷字段是 **`body`**（`data/iot/smart-bed.js` 的 `required: ["level","title","body"]`），`url` 不在 envelope 里 ⇒ 按卡传 `url` 会被**静默剔除**。 |

**计数**：REST 卡 **243 不变**、ZYRIC 一级节点数 **7 不变**、AI Tools **37 不变** —— 本版**只改 2 条描述文本**，不增删任何端点卡/字段/枚举。
⚠️ **本版不动的两处（有意，留痕）**：① `data/rest/02-core-domains.js` 的 3 张 stop 卡
（`rest-angle-adjust-stop` / `rest-massage-control-stop` / `rest-lulling-stop`）描述里的
`position`/`zone` —— 协议 43 个 cloud-to-device action 里**只有** `status_notification_trigger`
声明了这两个字段，7 条 stop 类协议载荷只有 `sub_device_id`；**卡与协议的这处不一致未在本版处置**
（不是 `T-175` 任务书点名的 2 处），已在 journal 保留为待裁决项。
② `design/07-tdd/api/02-device-control-tdd-design.md`（TC-013 用例体）仍写 `speed`/`url` ——
**该文档与卡同源**，本版未扩围（避免把"2 处卡订正"变成"TDD 用例重写"）。

## v90：删除 `event-identifier.js` 的不可达 `bluetooth` 派生键（`T-093①b` · `G-67①`，2026-09-15）

> **触发**：`G-67①` —— 代码侧 `T-164b` 已删 `DEVICE_TYPE_DERIVED` 的 `"bluetooth"` 键（`src/server/events/identifier_rules.py:158`），
> 设计侧同键残留在 `data/iot/event-identifier.js`（约 `:167`）⇒ **双侧残留**。本任务**先核实可达性再删**（任务书要求）。

| # | 文件 | 变更 |
|---|------|------|
| ① | `data/iot/event-identifier.js` | **删除** `deviceTypeDerived` 的 `"bluetooth"` 键（**25 → 24** 键），就地留「勿回加」注记（含取证）。**原因 = 该键不可达**（死键）：本表查表键是**经校验的 `DeviceType`**（协议 `device_type` 枚举 **37 值**，`data/iot/smart-bed.js` 的 `device_type` 枚举块；后端 `sub_device_config.py::DeviceType` 同值域 + `_parse_optional_enum` 对未知值直接抛 `InvalidEnvelopeError`），**37 值里没有 `bluetooth`** ⇒ `identifier.py` 的 `if device_type in DEVICE_TYPE_DERIVED` 永不命中。全仓 grep 实测：`bluetooth` 的真实用法只有 `bluetooth_audio_control`（action 名）/ `OutputBluetooth`（事件类，仍在 `events/base.py` 与 ER 07）/ `bluetooth_status`（协议环境状态字段）/ `bluetooth_action`（公共枚举）——**无一处以 `device_type="bluetooth"` 注册子设备**；蓝牙状态由协议 `status_notification.environment.devices.bluetooth_status` 表达。 |
| ② | `data/iot/protocol.js` | **同一事实的计数文本同批更正**（`T-093①b` 主动扩围，1 行）：渲染文本「层 3 …—— **25 条**（含 3 个 sleep_monitor 派生键 + bluetooth 子能力）」⇒ **24 条**（含 3 个 sleep_monitor 派生键；`bluetooth` 已删）。不改会让 `api-viewer` 的 ZYRIC「事件映射表」一节**显示与本资产实际不符的计数**。 |

**计数**：`deviceTypeDerived` **25 → 24**（`node design/04-platform-api/data/iot/event-identifier-check.mjs` 实测输出：
`device_type 映射: 41/37` → **`40/37`**、`subDeviceEvents: 16 | deviceTypeDerived: 24`，脚本 **`[OK]` rc=0**）。
⚠️ 该脚本的判据 ⑥ 是「协议 `device_type` → 必须有映射」（**正向覆盖**），**没有反向判据**（映射键 ⊆ `device_type`），
故删除前后都 `[OK]` ⇒ **本次设计侧删除在门禁上零覆盖**（见 journal 的零覆盖登记与"要加什么判据/为什么不加"）。
**REST 卡数不变（243）**、ZYRIC 一级节点数不变（7）、AI Tools 37 不变 —— 本版**不增删任何端点卡/工具**。

> ⚠️ **与 v89 的关系**：v89 末尾登记了「`bluetooth` 键双侧残留待同步」；**本版即为那次同步**（v89 原文不改，留痕）。

## v89：音乐库上传卡补 D-67 幂等语义注记（`T-164` 阶段b · `G-04`，2026-09-15）

> **触发**：`D-67` 裁决（`T-162` 报回媒体库同内容二次上传 500）——后端已按裁决落地
> （幂等返回既有资产 id + `deduplicated=true`；404/409 语义与 `storage_key` UNIQUE 索引不动），
> 设计卡补一句语义注记（任务书 ③ 明确允许改本卡）。

| # | 文件 | 变更 |
|---|------|------|
| ① | `data/rest/09-media-library.js` | `rest-media-files-create` 卡 description 补 **D-67 幂等语义**：存储为内容寻址（storage_key = sha256）⇒ 同内容二次上传幂等返回既有资产（201 + 同 id + `deduplicated=true`），不是 409/500。**端点卡数不变（REST 243）**。 |

> ⚠️ 未动 `data/iot/event-identifier.js`（其 `deviceTypeDerived` 的 `"bluetooth"` 键与后端
> `identifier_rules.py` 的删除（v116①）形成双侧残留——本任务非 ③ 面不碰该资产，已登记待同步）。

## v88：`T-093①` 设计侧复查 —— 修正 1 处悬空 `related_apis`（`G-53`，2026-09-14）

> 来源：`T-093` 阶段①（仅设计侧；⛔ 未 publish、⛔ 未跑 pytest）。
> **处理**：`G-53` —— `data/rest/10-business-orchestration.js` 的 `rest-business-management-aggregate` 卡
> `related_apis` 原写 `POST /api/v1/business/management/actions — 管理动作统一入口`，而该端点
> **已按 RB-006 裁决 33 / D-1a 删除**（`T-127` 落地，同文件头注与 `warn` 注记均已自陈）
> ⇒ 该引用**指向不存在的端点**（设计侧陈旧引用）。现改为**显式「无同域姊妹端点」＋ 删除依据留痕**，
> **不发明替代端点**（`D-31`）。
> ⚠️ 同文件无其它改动；端点卡数不变（**243**，门禁实测）。

## v87：域 04 **「判定 → 发布」落点落地** —— AI 侧发布 `DomainCommand.SleepDataRequest`（T-130-09d · `D-59` 的 A10，2026-09-14）

> 来源：`T-130-09d`（`D-59` 的 **A10「✅ 先补设计再实现」**：域 07 补字段级契约 ＋ **域 04 补「判定 → 发布」落点**）。
> **裁决依据**：**RB-006 裁决 36 / 36′ / 36.1 / 36.2**（「`DomainCommand.SleepDataRequest` 由 **AI 层发出、睡眠层接收**」/「事件无 callback 无 ack 但会产出结果」/「结果去向由**指令要求的目标**决定」）
> ＋ 领域 04 **意图类型 3（外发/外调类）**（`domain-registry.js`：*"例如睡眠事件：**调用总线发送睡眠数据请求**（`DomainCommand.SleepDataRequest`）让睡眠域响应并下发"*）
> ＋ 领域 03 ⛔「**所有需求必须走事件总线**」＋ `D-31`（不发明语义）。
> **纪律**：**旧句一律留痕**（保留原句 ＋ 取代注记）；设计侧与实现侧**同批**。
> 🔗 **同批落点**：域 07 的字段契约与 `publishes` 补登见 `../05-backend-architecture/CHANGELOG.md` v42；代码侧见 `src/server/CHANGELOG.md` v104。

| # | 项 | 落点 | 变更 |
|---|----|------|------|
| **A10** | **AI 侧「判定 → 发布」落点** | `data/ai-tools/tools.js` · `GetSleepReport` 的 `notes`；`data/rest/02-core-domains.js` · 域 04 块 `description` | **① 落点 = `GetSleepReport`**（与设备侧 `sleep_report_trigger` 同源语义；AI 侧**唯一**承载"用户要睡眠报告"意图的工具）：**每次 LLM 选定本工具即发布** `DomainCommand.SleepDataRequest`（`source="ai-agent"`，字段 `device_id` = 当前会话的床、`date` = 入参日期）⇒ 睡眠域（03）**自主完成 拉取 → 渲染 → 整理 → 下发**（AI 不代其下发、不经域 10 中转，裁决 36.1）。<br>**② 「同一工具的两条通路」被显式写清**（这是本项**必须写下来**的口径，否则读者会把两条通路混为一谈）：**对话侧** = 既有的 anyishui 实时拉取 → tool result 回填 LLM（**用户问话有人答**，`_query_sleep` **未动**）；**报告下发侧** = 事件（**无 callback / 无 ack**，与对话侧**互不等待**）。⇒ 设计卡原描述只讲"实时拉取原始数据"的那句**保留**，另追加取代注记写清第二条通路。<br>**③ 兄弟工具 `GetSleepStatus` 不连带发布**（其设备侧对应事件 `DeviceCommand.*` 的归属**未定** ⇒ 登记待裁决，**不发明**）。<br>**④ 域 04 块 `description`** 的「A10 设计缺口，登记待用户裁决」段**改写为已落地**，并就地保留一条**外部前提更正**（`T-130-09a` 已指出）：「触发语义在设计里根本没定义」**不成立** —— 业务条件早已在裁决 36 ＋ 领域 03 ⛔ ＋ 领域 04 意图类型 3 里定义，真正缺的只有**字段契约**与**落点**，本任务两项同批补齐。 |
| — | **字段依据（引用域 07 的单源口径，本域不另造）** | 同上 | 事件字段集由**域 07** 给（裁决 11：事件定义的唯一集中地）＝ `device_id`（目标床，必给）＋ `date`（报告日期，可选，**纯透传**）。⛔ `ref_id` **不补**（AI 侧无生产者 ＋ 裁决 36.1 无 ack ⇒ 死字段，`D-31`）；`zone` 为 **proposed 未采用**（域 03 下发侧目前**零消费者**）。**本域不新增任何字段定义**。 |

**计数**：REST 卡数 **243 → 243**（不增删卡）· AI Tools **37 → 37**（不增删工具，**只改 `notes` 文案**）⇒ `scripts/check-design-asset-render.mjs` 与 `tests/design-regression/test_rest_domain_migration.py` 的**两处计数常量均未动**（已复核无第三处硬编码）。

## v86：域重画 · **行为/口径收尾**（A2 作废 `rest-set-user-profile` ＋ A9 新增 `sleep_data.write` ＋ A12 摘掉 `AiVoiceStreamEnded`）（T-130-09c，2026-09-14）

> 来源：`T-130-09c`（域重画 · **行为/口径收尾**：`D-59` 的 A2/A9/A12 代码侧 ＋ 死代码 ＋ 架构资产残留）。
> **裁决依据**：`.pipeline/PENDING-DECISIONS.md` 的 **`D-59`**（A2「✅ 作废该卡」/ A9「✅ 新增 `sleep_data.write`」/ A12「✅ 摘掉」）
> ＋ `D-58`（A9 的登记前情）＋ `D-56`。
> **纪律**：**旧句一律留痕**（保留原句 ＋ 取代注记，照 `DD-002`/`T-125` 先例）；设计侧与实现侧**同批**改（避免只改一侧造新漂移）。
> 🔗 **不在本文件的同批落点**：A12 的 ER 半边（`command_push_config` 实体描述）见 `../03-entity-relationship/data/CHANGELOG.md`；
> A9 的权限码 3 处（域 05 的 ER ＋ 工作流 ＋ 权限映射设计）见各自的 CHANGELOG；`modules['sleep-data']` 架构资产见
> `../05-backend-architecture/CHANGELOG.md`；代码侧见 `src/server/CHANGELOG.md`／`src/web-admin/CHANGELOG.md`。

| # | 项 | 落点 | 变更 |
|---|----|------|------|
| **A2** | `rest-set-user-profile` **作废**（`D-59` A2） | `data/rest/02-core-domains.js`（**删除该卡**；域 02 块 `implementation.note` ＋ 动作组注释行） | **卡与实现路由一并删除**。依据：协议 `set_user_profile` 是**上行** action（`iot/smart-bed.js` `direction: "device-to-cloud"` ⇒ `UPLINK` → `DeviceMessage.UserProfileReported`），**`DOWNLINK` 无对应 action ⇒ 平台侧无下行事件类** ⇒ 该卡声明的「平台设置设备用户档案」动作**不可实现**（原实现只能建 URL ＋ 诚实 501）。**为何"卡与路由一起删"而不是"留路由但明确口径"**：① 路由**没有设计卡就不是契约**，留下等于在**已裁定作废**的端点上继续对外暴露一个只会 501 的 URL —— 与"诚实"相反，且 `_action_missing_paths()` 自检（设计卡 ↔ `_ACTION_ENDPOINTS` **双向差集**）会在只有一侧改动时**启动期直接炸**，两边都不改才是唯一自洽态；② 通用口 `POST /control/devices/{id}/{action}` **仍在**（它本就不是任何设计卡的实现），`set_user_profile` 走它仍会被按"非下行 action"拒 **400** ⇒ **不存在"能力被删"**；③ 现役写入路径 `PUT /api/v1/devices/{id}/bed-user-profile`（同域，T-020）**能力零损失**。**计数同步**：域 02 动作端点 **30 → 29**、`/control/*` **32 → 31**、REST 卡总数 **244 → 243**（同步 `scripts/check-design-asset-render.mjs` 的 L2/卡数 ＋ `tests/design-regression/test_rest_domain_migration.py` 的 `EXPECTED_MOUNTS`/`total_endpoint_cards`/`DELETED`）；前端 `CONTROL_ACTION_PATHS` 去掉 `set_user_profile`（27 → 26 键）。 |
| **A9** | 新增权限码 **`sleep_data.write`**（`D-59` A9） | `data/rest/02-core-domains.js`（`rest-binding-unbind` 的 `description`） | 该卡原**未声明 `permission`**（本域 kebab 式紧凑卡通例）⇒ 现按**实现侧唯一强制点**记录：`sleep_data/api/router.py::unbind_binding` 取 `sleep_data.write`。⚠️ **权限码沿革**：`T-130-05` 时全仓不存在该码，实现曾暂复用只读/导出码 `sleep_data.export`（`D-58(a)` 登记）；本任务同批补齐 **设计 4 处 ＋ 实现注册表 ＋ 代码 1 行**。⚠️ 本域 `implementation.note` 里的「权限 `sleep_data.read/sleep_data.export`」为 `T-126` 时点叙述，**未改**（登记给后继设计收口）。 |
| **A12** | 摘掉 `AiVoiceStreamEnded`（`D-59` A12） | 同上（`rest-ai-config-command-push-update` 的 `description`） | 该卡原写「契约枚举 `(DeviceRequirementCommand, AiVoiceStreamEnded)`（**二元组**）」并把它列为**待裁决**项 ⇒ 现改为 **单元组 `("DeviceRequirementCommand",)`**，并把"待裁决"段改写为**已落地**（`DEFAULT_PUSH_EVENT_CLASSES` **2 → 1 项**；`test_command_push_config.py` 的二元组断言 → 单元组 ＋ 反向断言；卡枚举；ER 04 实体描述）。**管理后台 `command-pushes` 可见行随之 2 → 1**（该表 `:data` 绑定 `commandPushes` 数组、纯动态渲染，**无硬编码行数**；`design/03` 与 `.pipeline` 里"断言钉 2 项"指的是那处**二元组集合断言**，已同步）。⚠️ **`AiVoiceStreamEnded` 的事件契约本身一字未动**（仍在 `events/base.py` 的 `EventType` 与 `event-contracts.js` 内）—— 摘的只是「AI 指令推送」这一**配置面**。 |

**本版其余同步（同批、非独立裁决）**：`data/rest/02-core-domains.js` 的域 02 块内 3 处计数/枚举叙述（`implementation.note` 的「30 条」→ 29、动作组注释「30 条」→ 29、`rest-ai-config-command-push-update` 的枚举）逐处更新并留痕。

## v85：契约收口 —— 删除 `rest-admin-regions-list` 的 `include_invalid`（T-130-09b，2026-09-14）

> 来源：`T-130-09b`（**补门禁**）的 **B6**。发现者 `T-130-03`（域重画时登记为「设计有·代码无」的既有缺口）。
> **裁决依据**：`D-31`（语义未定不得实现）。

| # | 项 | 落点 | 变更 |
|---|----|------|------|
| B6 | `rest-admin-regions-list` 的 `include_invalid` 参数**设计声明、实现从未接受** | `data/rest/system.js`（该卡 `description` / `business_logic.steps` / `query_params`） | **删除该参数（设计侧收口）**。事实：该参数自建卡起只存在于设计侧 —— 实现 `src/server/modules/system/api/router.py::list_admin_regions` 只有 `level` / `keyword` / `parent_code` 三个 Query 参数，**从未接受过** `include_invalid`（`T-130-03` 迁移时逐字比对确认：迁移前/后一致）。**二选一的理由（选设计侧）**：补齐实现要改 `system/api/router.py` 并**新造一条未裁决的语义**（"撤并区划"是否该出现在三级级联选择器里、被选中后如何表现）⇒ 按 **`D-31`** 与最小改动原则，改为**让契约与实现一致**：删掉一个从未生效的参数。相应文案改为「`is_valid=false` 的撤并区划**一律不返回**」，`steps` 的 query 解析清单同步去掉它。⚠️ **URL / 权限码（`device.read`）/ 响应字段一字未改 ⇒ 端点计数不变（REST 卡数仍 244）**；`include_invalid` 在本文件**零残留**（改后再 `grep` 零命中）。 |

## v84：域重画 · **设计/文档侧收口（A1–A13，13 项）**（T-130-09a，2026-09-14）

> 来源：`T-130-09a`（域重画 · **设计/文档侧收口**）。任务书主体 = `.pipeline/journal/_t130-09-backlog.md`
> 「A. 设计/文档侧（`130-09a`）」表（A1–A13）。
> **裁决依据（只读）**：`docs/requirements-backlog.md` `RB-006`（裁决 1/3/4/11/12/22/26/28/30/36/37）＋
> `.pipeline/PENDING-DECISIONS.md` 的 `D-56` / `D-57` / `D-58`。
> **纪律**：凡「设计语义该是什么」的判定一律先回 RB-006/`delta` 找依据；**找不到依据的一律登记为
> 「待用户裁决」并给建议选项，绝不自造语义（`D-31`）**；**旧句一律留痕**（保留原句 ＋ 取代注记，照 `DD-002`/`T-125` 先例）。
> ⚠️ 本次**未改 `docs/requirements-backlog.md`**（另一会话所有物，只读）。
> 🔗 **不在本文件的同批落点**：**A7**（`design/domain-design-overview.md` 的「事件契约 109 → 110」＋ 补 `DomainCommand.*` 表行）、**A13**（`data/module-boundaries.js` 域 04 `crossModuleCalls` 三条逐条核活叶子后只留 1 条）记录于 **`design/05-backend-architecture/CHANGELOG.md` v40**；**A12 的 ER 半边**（`command_push_config` 实体描述）记录于 **`design/03-entity-relationship/data/CHANGELOG.md` v7.32**。

| # | 项 | 落点 | 变更 |
|---|----|------|------|
| A1 | `implementation.note` 仍写 DD-007（与 `D-57` 冲突） | `data/rest/02-core-domains.js`（域 02 块 `implementation.note` 及两组 `deferred` 注释行） | **追加取代注记（旧句一字未改）**：DD-007 的「细分端点 deferred、唯一实现路径＝通用 `POST /devices/{id}/control`」**整段作废**，现行以 **`D-57`**（用户选 A：**按设计实施显式动作端点**）为准 —— 30 条 `/control/devices/{id}/<action>` 及 `device-status/tree`、`message-flow` 已按 `T-130-04` 落地；通用口**保留但已不是任何设计卡的实现**（仅「协议里有、设计未覆盖」的兼容逃生口）。 |
| A3 | `rest-device-status-tree` / `rest-message-flow-list` 只有一句描述、无字段级契约 | 同上（两条卡 `description`） | **显式声明「无字段级设计契约 ⇒ 按实现渲染」**并给出**实测字段口径**（状态树 `{tree, group_count, device_count}` ＋ 节点形状/未分组虚拟节点；消息流**复用** `/maintenance/network-messages` 同一取数与方向推导、`{items[]}` 无 total、查询参数与取值域）。**不补造契约**（`D-31`）。**端点卡数不变**。 |
| A2 | `rest-set-user-profile` 与协议不可调和 | 同上（该卡 `description`） | **登记差异 ＋ 待用户裁决**：协议 `set_user_profile` 是**上行** action（`iot/smart-bed.js` `direction: "device-to-cloud"` ⇒ 映射 `DeviceMessage.UserProfileReported`），`DOWNLINK` 无对应 action ⇒ 平台无下行事件类；实现**按卡建 URL 并诚实报 501**（指向现役写入路径 `PUT /devices/{id}/bed-user-profile`）。**未删卡**（删卡会同时动端点数与代码的 `_CONTROL_ACTION_PATHS` 启动校验）。选项 ①作废该卡（30→29）②补设备侧下行契约（需改协议，无 RB-006 依据）。 |
| A4 | `rest-device-deregister` 文案与 ER 01 `nn: true` 冲突 | `data/rest/01-device-iot.js`（该卡 `summary`/`scenario`/`description`/`steps`/`post_effects`/`responses` ＋ T-127 块注释） | 按 **`D-56①`** 改正：**`device_group_id` 不置空**（「摘除归属」由 `status=unregistered` ＋ 准入凭据作废表达），响应 `group_id` **回显实际所属分组**（json 样例与 `fields` 类型 `null → string\\|null` 同步）。旧句**留痕**（T-127 块注释内保留原文）。 |
| A5 | `GET /devices/export` 设计列含不存在的 `name` / 需跨域 JOIN 的 `tenant_name` | 同上（该卡 `description`/`responses` ＋ T-127 块注释） | 按 **`D-56②`** **反向修正为已实现 6 列**：`device_id/product_model/status/group_id/group_name/created_at`（`name` 表里不存在、`tenant_name` 需跨域 JOIN ⇒ 均不实现）；补「`content` 带 UTF-8 BOM」。旧列清单**留痕**。 |
| A6 | `GET /devices/{id}/connection` 不在设计 17 卡内但前端在用 | 同上（**新增 1 张卡** `rest-device-connection`） | 判定**补卡**（非归档）。依据三条：① 实现早已存在（本域代码侧 18 条 vs 设计 17 张卡，唯一差额即本条）；② 设计**已有文字提及**（`rest-device-sub-devices` 的 `related_apis`）；③ **客户端设计在用**（`design/06-web-admin/data/tree.js` 两个组件的 `api_ref`）。⇒ **域 01 卡数 17 → 18、REST 总数 243 → 244**（同步 `scripts/check-design-asset-render.mjs` 的 L2/卡数 ＋ `tests/design-regression/test_rest_domain_migration.py` 的 `EXPECTED_MOUNTS`/`total_endpoint_cards`/`ADDED`）。 |
| A8 | 7 条 `/sleep-monitor/devices*` 卡片被反复读成"缺口" | 同上（本域块 `endpoints` 前新增 planned 组说明 ＋ 7 条 `description` 前缀） | 按 **`D-58`**（维持 `planned`）**只加显式标注、不下架**：新增「⛔ planned 组」说明块（列出"能力已在域 01 URL 下 / ER 03 无独立实体 / 硬做＝`D-31` 发明语义"三条理由），并给 7 条卡 `description` 加 **`⛔ planned（不排期实现，非缺口）——`** 前缀（**viewer 卡片可见**，可 grep）。**端点卡数不变**。 |
| A9 | `unbind` 权限码设计未给 | 同上（`rest-binding-unbind` `description`） | **登记差异 ＋ 待用户裁决**：本域权限清单只有 `sleep_data.read`/`sleep_data.export`（设计 2 处 ＋ 实现注册表 `identity_auth/domain/models.py:66-67`，`RoleService._validate_codes` 按它校验 ⇒ **只改设计会立刻产生新漂移**）；实现暂复用 `sleep_data.export`。选项 ①新增 `sleep_data.write`（设计 4 处 ＋ 实现注册表/种子同改）②维持复用。 |
| A10 | `DomainCommand.SleepDataRequest` 的 AI 侧发布点 | 同上（域 04 块 `description`） | **登记缺口 ＋ 待用户裁决**，并**就地更正一条外部前提**：「触发语义在设计里根本没定义」**不成立** —— RB-006 **裁决 36** ＋ 领域 03 ⛔「所有需求必须走事件总线」已给出**业务触发条件**；**真正缺的**是 ①**事件字段级契约**（`event-contracts.js` 自陈"仅继承基类可空 `device_id`"）②**AI 侧落点**（哪个意图判定/哪个工具去 publish，本域设计零条款、`src/**` 零发布点）。`ref_id` 若需**必须由域 07 作为事件字段补入**（裁决 11）。 |
| A11 | `stats.restCount` 是"第三种口径" | `data/rest/{01-device-iot,02-core-domains,05-identity-auth,09-media-library,10-business-orchestration,system}.js` | **按同口径回填 ＋ 改 IIFE 自维护**（沿用 01/09/system 的既有先例）：唯一口径 = 本域端点**卡片数**（`endpoints` 数组长度；实测 8 域均与「method+path 去重后卡数」逐值相等）；**补 2 个缺失的 `stats` 块**（域 05、域 10）；**修正错值**（`device-control` 64 → 72）。8 域求和 **192 → 244 ＝ 卡数**（改前 `stats` 求和 192，其中 64 是手工错值）。⚠️ 实测该字段**零消费者**（`scripts/**/*.mjs` 0 命中、`**/*.py` 0 命中；`api-viewer` 的 `D[k].stats.totalEndpoints` **只对 `protocol-*` 键**求和）⇒ 卡内注记写明「仅供人读、**不得当端点计数判据**」（`N-052①` 教训）。 |
| A12 | `AiVoiceStreamEnded` 在契约枚举内但无 handler | `data/rest/02-core-domains.js`（`rest-ai-config-command-push-update`）＋ `../03-entity-relationship/data/04-ai-agent.js`（`command_push_config` 实体描述） | **纠正过时枚举口径 ＋ 登记待裁决**：① 旧清单「`TextCommand`/`VoiceStreamCommand`/`AiVoiceStreamEnded`」**作废**（**RB-006 裁决 3**：三者**不是事件**，归并为 `DeviceRequirementCommand`；`T-050` 用户裁决 ②B/③A）⇒ 实测契约枚举 = `(DeviceRequirementCommand, AiVoiceStreamEnded)`，旧清单里两个封包名会**被拒 422**；② 「可选扩展 `DeviceCommand.*`」为**设计意图、非现役行为**；③ `AiVoiceStreamEnded` **无可开关效果**（`_HANDLERS` 只含 `DeviceRequirementCommand`，遍历时静默跳过；其唯一消费者已是 device-control 归一响应器）⇒ **待用户裁决**（摘掉／补 handler／保留但标注），涉及代码与 `test_command_push_config.py:29` 断言 ⇒ **归 `130-09b` 或后续代码任务**。 |

## v83：音乐库事件 producer 迁移落地 ＋「外链前缀」字段形态收口（T-130-07，2026-09-14）

> 来源：`T-130-07`（域重画 · **代码实施 7/11：域 09 事件发布 ＋ 外链前缀**）。
> 本条目登记**设计侧**的两处收口：① 音乐播放事件的 **producer 由 `ai-agent` 改为 `media-library`**
> （RB-006 裁决 17/20/21；代码侧同提交落地，见 `src/server/modules/media_library/events/__init__.py`）；
> ② **「外链前缀」的字段形态**（裁决 18/23 只说"本期要落地"，此前**无字段级定义** —— `T-129` §9-4
> 登记为待定；本轮由实施侧定清并写回设计）。

| 变更 | 内容 |
|------|------|
| `data/ai-tools/tools.js` · `PlayMusic.notes` | **追加取代注记**（旧句一字未改）：实现迁移已完成 —— `action` 即 `media_library_play_request`、`event_class=None`（AI **不再发布**该事件），发布点 = 域 09 `PlaybackRequestService`。**`action` / `description` 字段本身一字未改**（T-127 已按裁决 21 写好，本轮实现追平设计） |
| `data/rest/09-media-library.js` · `link-config` 两卡描述 | 补**字段级口径**：外链前缀 = `domain`（**唯一的可配置前缀**，运营在 `PUT /link-config` 配置）＋ `base_path`（**内部固定**，只读回显）；优先级 `link-config.domain` → `settings.media_link_base_url` → 空（相对路径）。**端点卡数不变**（仍 14） |
| `data/rest/09-media-library.js` 文件头 | 追加 `T-130-07` 注记（producer 迁移 ＋ 外链前缀字段口径） |

**「外链前缀」的落地字段形态（本轮定清，写进设计）**：

| 项 | 值 | 生效点 |
|---|---|---|
| 字段名 | `link-config.domain`（**外链前缀**，唯一可配置项） | `GET/PUT /api/v1/media-library/link-config` |
| 部署级默认 | `settings.media_link_base_url`（`SMARTBED_MEDIA_LINK_BASE_URL`） | ⚠️ **本任务前它是死键**（全仓零读取点）⇒ 本轮接上，`domain` 为空时生效 |
| 固定路径段 | `/api/v1/media-library/files/{id}/file`（`MEDIA_PUBLIC_PATH_PREFIX`，**不可配置**） | `src/server/infrastructure/media_storage.py` |
| 优先级 | `link_config.domain` → `settings.media_link_base_url` → 空（**相对路径**） | `MediaStorageService.resolve_link_prefix()` |
| 生效范围 | ① 播放事件 `PlatformCommand.PlayOnlineMedia` 的 `url`；② `POST /playback/{id}/url`；③ 选曲/检索返回的 `issued_url` | 全部经 `MediaStorageService.build_issued_url()` **同一构建点** |
| ⛔ **不做** | CDN 防盗链签名（`enable_cdn_signer` 保持占位、不实现） | 裁决 18 |

- **未变**（刻意）：REST 端点卡数 **243**（域 09 仍 14 卡，本条目**不增删端点**）；
  域 04 的 44 卡；`ai-tools` 的**卡数与 `action`/`description` 字段值**（本轮只补 notes 注记）。
- 📌 **权威来源**：`scripts/check-design-asset-render.mjs`（`REST 243`）；
  `tests/design-regression/test_rest_domain_migration.py`（域 04 = 44 卡）；
  事件归属见 `design/05-backend-architecture/data/{module-boundaries,event-contracts}.js`。

## v82：域 04 AI 智能体 —— **补 2 张"代码有 · 设计无"的端点卡** ＋ 计数同步（T-130-06，2026-09-14）

> 来源：`T-130-06`（域重画 · **代码实施 6/11：域 04 AI 订阅面与工具迁移**）。
> `T-129` 差异报告 §3.3(b) 判定 **`POST /api/v1/ai/conversations`** 与
> **`POST /api/v1/ai/conversations/{id}/messages`** 属「**设计侧需补卡**」（不是"代码多余"）——
> 两条端点的**实现早已存在**（管理后台调试对话入口），设计资产里却从未建卡
> （`T-127` 域重画时漏补）。本条目即补该缺口。

| 变更 | 内容 |
|------|------|
| `data/rest/02-core-domains.js`（域 04 块） | **新增 2 张端点卡**：`rest-conversation-create`（`POST /api/v1/ai/conversations`，`ai_agent.write`，body `{sessionType?}` 默认 `text`，**201**）· `rest-conversation-message-send`（`POST /api/v1/ai/conversations/{id}/messages`，`ai_agent.write`，body `{content}` 必填、空 → 422、会话不存在 → 404，**200**）。两条均**按代码现状登记**（路径/权限/请求体/状态码逐条对齐 `src/server/modules/ai_agent/api/router.py:115` 与 `:131`），**未改任何实现**。域 04 端点卡 **42 → 44**。 |
| `data/rest/02-core-domains.js` 文件头 | 版本日期 2026-09-13 → **2026-09-14**（v2 → v3）；头部注记追加 `T-130-06` 补卡说明 |
| `data/rest/02-core-domains.js` `implementation.note`（域 04） | 追加 T-130-06 补卡说明（42 → 44） |
| `data/rest/02-core-domains.js` `stats`（域 04） | `{totalEndpoints: 44, restCount: 44}`（原 42） |
| `scripts/check-design-asset-render.mjs` | 计数常量同步：`AI 智能体` L2 `42 → 44`、`REST endpoint 卡数` `241 → 243`（**改一处必须改两处**：本脚本 ＋ `tests/design-regression/test_rest_domain_migration.py` 的 `EXPECTED_MOUNTS` / `total_endpoint_cards`） |
| `tests/design-regression/test_rest_domain_migration.py` | `EXPECTED_MOUNTS["domain-ai-agent"]` 42 → 44；`test_total_endpoint_cards` 241 → 243；`ADDED` 清单 **+2 条**（新增落点断言，含路径）⇒ 该文件用例 **+2** |
| `IMPLEMENTATION-PLAN.md` §一 | 域 04 端点数 `42 → 44`、`REST 领域端点卡数 241 → 243`、闭世界和同步（**同一结论散布多处必须一起改**） |

⚠️ **为什么是"补卡"而不是"删代码"**：两条端点是**管理后台调试对话**的真实入口
（`GET /ai/conversations` ＋ `GET /ai/conversations/{id}/messages` 的设计卡**早已存在**，
只缺写入侧的两条 ⇒ 读/写不对称），删除会让"新建议会/发消息"这条链路失去契约。
⇒ 缺口在设计侧的**端点卡**，不在实现。

- **未变**（刻意）：被**补卡**的两条端点的**实现代码一行未改**；域 04 的其余 42 张卡**一字未动**；
  `02-core-domains.js` 承载的域 02（72）/域 03（14）端点数**不变**。
- 📌 **权威来源**：`scripts/check-design-asset-render.mjs`（`REST 243` / 一级节点 8 常量）；
  `tests/design-regression/test_rest_domain_migration.py`（域 04 = 44 卡）。
- ⚠️ **本域同期另有 2 项非计数类设计改动**（同属 `T-130-06`，不改变端点卡数）：
  ① `design/05-backend-architecture/data/module-boundaries.js` 域 04 的 `events.subscribes`
  **补登运行时真值** `DeviceMessage.OutputStreamStopped`（barge-in 恒订阅，`T-089` 起即存在、
  设计清单此前未列）；② `data/ai-tools/tools.js` 的 `PlayMusic` / `ControlMusic`
  **未改动**——其 `action` 迁移（`play_online_media*` → `media_library_play_request*`）
  与 **producer 迁移（域 09）成对落地**，归 `T-130-07`（详见该任务报告「未做/待核实」）。

## v81：域 08 平台支撑 —— **补 2 张"代码有 · 设计无"的端点卡** ＋ 计数同步（T-130-03，2026-09-14）

> 来源：`T-130-03`（域重画 · **代码实施 3/11：域 08 平台支撑建立**）。
> 其一：把 `admin_regions`（行政区划字典）的表/端点/仓储**从域 01 物理迁到域 08**（代码侧，**本文件不动**——
> 端点卡早在 v78 已迁入 `system.js`，URL 与契约一字未变）。
> 其二（**本条目**）：`T-129` 差异报告 §3.3(b) 判定 **`GET /api/v1/help/locales`** 与
> **`GET /api/v1/locales/additional`** 属「**设计侧需补卡**」（不是"代码多余"）——
> 两条端点的**实现早已存在且前端在用**，设计资产里却从未建卡。

| 变更 | 内容 |
|------|------|
| `data/rest/system.js` | **新增 2 张端点卡**：`rest-help-locales`（`GET /api/v1/help/locales`，`admin.read`，返回启用语言 `[{code,name}]`）· `rest-locales-additional`（`GET /api/v1/locales/additional`，公开，返回附加动态翻译映射，**当前实现恒为 `{}`**）。两条均**按代码现状登记**（路径/权限/响应形状逐条对齐实现），**未改任何实现**。域 08 端点卡 **37 → 39**。 |
| `data/rest/system.js` 文件头 | 版本日期 2026-09-13 → **2026-09-14**；头部注记追加 `T-130-03` 补卡说明 |
| `data/rest/system.js` `implementation.note` | 追加 T-130-03 补卡说明（37 → 39） |
| `scripts/check-design-asset-render.mjs` | 计数常量同步：`平台支撑` L2 `37 → 39`、`REST endpoint 卡数` `239 → 241`（**改一处必须改两处**：本脚本 ＋ `tests/design-regression/test_rest_domain_migration.py` 的 `EXPECTED_MOUNTS` / `total_endpoint_cards`） |
| `tests/design-regression/test_rest_domain_migration.py` | `EXPECTED_MOUNTS["domain-system"]` 37 → 39；`test_total_endpoint_cards` 239 → 241；`ADDED` 清单 **+2 条**（新增落点断言，含路径）⇒ 该文件用例 **+2** |
| `IMPLEMENTATION-PLAN.md` §一 | 域 08 端点数 `37 → 39`、`REST 领域端点卡数 239 → 241`、闭世界和同步（**同一结论散布多处必须一起改**） |

⚠️ **为什么是"补卡"而不是"删代码"**：两条端点都有**真实消费方**（`src/web-admin/src/api/help.ts:54` 的
`helpApi.locales()`；`src/web-admin/src/stores/app.ts:36` 的启动期 `GET /api/v1/locales/additional`），
且 `design/07-tdd/api/system-domain-tdd-design.md:71`（`TC-HELP-006`）与
`design/06-web-admin/_reference/page_level_help.md:72` 早已**以文字声明** `GET /api/help/locales`
⇒ 缺口在设计侧的**端点卡**，不在实现。

- **未变**（刻意）：被**补卡**的两条端点的**实现代码一行未改**（`system/api/router.py` 仅搬迁
  `/admin-regions` 块进来，`help/locales` 与 `locales/additional` 原样）；`rest-admin-regions-list`
  卡**一字未动**（它的 URL `GET /api/v1/admin-regions`、权限 `device.read`、响应形状在本任务**保持不变**，
  只是代码归属模块由 `device_iot` 换成 `system`）。
- ⚠️ **既有差异（本任务只登记不改）**：`rest-admin-regions-list` 卡的 `query_params` 声明了
  **`include_invalid`**，而实现（迁移前后一致）**未接受该参数** ⇒ 属"设计有·代码无"的既有缺口，
  归设计侧/后续任务裁决（扩契约不在本任务范围，见任务报告「未做/待核实」）。
- 📌 **权威来源**：`scripts/check-design-asset-render.mjs`（`REST 241` / 一级节点 8 常量）；
  `tests/design-regression/test_rest_domain_migration.py`（域 08 = 39 卡）。

## v80：事件名收敛 —— `play_online_media` 的事件映射改指规范名（T-130-01，2026-09-14）

> 来源：`T-130-01`（域重画 · **代码实施第 1 步**：事件层落地 ＋ producer/名称收敛 ＋ 修 `alembic/env.py` 漏注册）。
> 依据：`RB-006` 裁决 20（该事件名统一采用设计层规范名 `PlatformCommand.PlayOnlineMedia`；
> 编排器裁决「与 B 项（运行时 `MediaPlayStartCommand` 改名）合并执行」）。

| 变更 | 内容 |
|------|------|
| `data/iot/event-identifier.js` · `downlink.play_online_media.event` | `PlatformCommand.MediaPlayStartCommand` → **`PlatformCommand.PlayOnlineMedia`**（与运行时代码 `src/server/events/base.py` 的改名、`design/03-…/07-event-infrastructure.js` 的 `EventType` 收敛**同批**）。⚠️ **协议 action 键 `play_online_media` 一字未动**（它是设备侧契约，`design/04-platform-api/data/iot/smart-bed.js` 与 `src/server/events/identifier_rules.py` 的 `DOWNLINK` 键都靠它）。 |
| 一致性 | `node design/04-platform-api/data/iot/event-identifier-check.mjs` ⇒ `[OK]`（本脚本第 5 项「映射事件类型在事件体系（`event-contracts.js`）中存在」正是这条的兜底判据）。 |

- **未变**（刻意）：`data/iot/protocol.js` / `smart-bed.js` 的协议 action 名与消息清单；`data/rest/**` 端点数据；其余 42 条 `downlink` 映射。
- ⚠️ **`downlink` 映射条数与 `ackOnly` 清单**未动 ⇒ 逐字输出不变（`downlink 映射: 43/42`）。

## v79：`IMPLEMENTATION-PLAN.md` 现状改正（T-128，2026-09-14）

> 来源：`T-128`（域重画 · 架构资产结构化改写 ＋ 文档卫生）接手 `T-127` 交办的残留项
> （`N-092⑥(a)`：`design/04-platform-api/IMPLEMENTATION-PLAN.md` **已过期**、不在 `T-127` 允许范围）。

| 变更 | 内容 |
|------|------|
| `IMPLEMENTATION-PLAN.md` · §一「现状」 | **全表按 `T-127` 后的实测现状重写**：原「`rest/01-device-iot.js`（含 maintenance 分组）31 · `rest/02-core-domains.js`（5 域合计）67 · `rest/system.js` 22」**均为旧基线**。现行 = `data/rest/` **8 个文件**、**REST 领域端点卡数 239**、一级节点 **8**，逐域 `01=17 / 02=72 / 03=14 / 04=42 / 05=32 / 08=37 / 09=14 / 10=11`（闭世界和 = 239）；并写明「**REST 数据不是"一域一文件"**」（域 02/03/04 三个挂载键都在 `02-core-domains.js` 内）。⚠️ **无法确证的历史批次分解**已明确标注「已过期、不追溯」而**不是猜测** |
| `IMPLEMENTATION-PLAN.md` · §二 批次 4 | 标题与说明由「**dashboard / statistics 域**」改为「**并入域 10**」（RB-006 裁决 33/D-2a·D-2b；`T-127` 已迁数据并**删除伪域挂载**）；**旧标题留痕**于取代注记 |
| `IMPLEMENTATION-PLAN.md` · §四 未排期 | 「事件消费 REST 端点（P-011 遗留）」标 ✅ **已由 `T-127` 落为域 10 的 `GET /api/v1/events`**（`rest-events-query`；旧句留痕） |

- **未变**（刻意）：批次 1/2/3/5 的**历史批次描述**（其"当时要做什么"的语义属历史留痕）与 §三「执行约束」。
- ⚠️ **本条目不改任何 `data/rest/**` 数据**（端点数据已在 `T-127` 的 v78 条目下沉），仅改正**计划文档的现状口径**。
- 📌 **权威来源**：`design/04-platform-api/CHANGELOG.md` v78（`T-127`）；`N-092`（`T-127` 收口账目）；`scripts/check-design-asset-render.mjs`（`REST 239` / 一级节点 8 常量）。

## v78：**域重画 · API 端点改造（全部域）** —— 逐域迁移 `01/02/03/04/08/09/10` ＋ 计数常量同步（T-127，2026-09-13）

> 来源：`T-127`（`docs/requirements-backlog.md` **RB-006** 裁决 **1 / 12 / 20 / 21 / 24 / 26 / 27 / 28 /
> 30' / 33 / 34 / 35 / 36 / 37**；线 B 第 3 步）。⚠️ 本任务**只改设计侧归属与声明**，
> **不动 `src/**` 实现**（实现迁移属后续代码任务）；`publish/` 不动（归 `T-093`）。
>
> **为什么本条目先给"改前/改后逐域表"**：REST 数据文件**不是"一域一文件"**（改前 8 个文件承载
> 10 个域挂载），端点搬家必须给可对账的逐域账目，否则无法判"搬完没搬完"。

### 一、逐域端点计数（改前 → 改后 × 迁入/迁出/新增/删除）

| 域 | 改前 | 改后 | 迁入 | 迁出 | 新增 | 删除 | 依据（RB-006） |
|---|:--:|:--:|:--:|:--:|:--:|:--:|---|
| **01 设备接入和管理**（`device-iot`） | 57 | **17** | 0 | **41** → 02（40）· 08（1） | **+2**（注销 / 导出） | **−1**（`POST /devices/{id}/control`） | 裁决 30'（D-5④）+ 裁决 12 修正 / 26（偏好）· 28（闹钟）· 27/34（行政区划）· 领域 02 裁决（运维） |
| **02 设备控制**（`device-control`） | 42 | **72** | **+41** ← 01（偏好家族 15 / 闹钟 8 / 运维 17 / 床用户 Profile 1 单键 + 3 主 CRUD 见下） | 0 | 0 | **−11**（10 条被完整定义取代的紧凑运维条目 + 其重复路径） | 裁决 12 修正 / 26 / 28 + 领域 02 裁决（"不需要专门的 maintenance 域"） |
| **03 睡眠数据**（`sleep-data`） | 13 | **14** | 0 | 0 | **+1**（手动解绑） | 0 | 裁决 37（"界面上给他提供一个解绑的动作"）；域键/标签对齐 registry slug |
| **04 AI 智能体**（`ai-agent`） | 42 | **42** | 0 | 0 | 0 | 0 | 裁决 20/21：`PlayMusic`/`ControlMusic` **只请求音乐库**（职责迁移，端点数不变） |
| **05 身份权限**（`identity-auth`） | 32 | **32** | 0 | 0 | 0 | 0 | 不在本任务范围（裁决 5–9 已由 `T-125`/`T-126` 落地） |
| **08 平台支撑**（`platform-support`） | —（原「系统管理」36，**无域身份**） | **37** | **+1** ← 01（行政区划字典） | 0 | 0 | 0 | 裁决 27/34/35（"直接建个支撑域……A 进支撑域就好了"） |
| **09 音乐库**（`media-library`） | 14 | **14** | 0 | 0 | 0 | 0 | 裁决 18/23：**外链前缀本期落地 / CDN 不落地**（口径落地，端点数不变） |
| **10 聚合业务逻辑**（`business-orchestration`） | 7 | **11** | **+9** ← 伪域 `dashboard`(5) + `statistics`(4) | 0 | **+1**（`GET /api/v1/events`） | **−6**（`/intents` ×3 + `/capabilities` ×2 + `/management/actions` ×1） | 裁决 24（改名重定位）+ 裁决 1（否掉统一转译）+ 裁决 33 / D-1a·D-2a·D-2b + N-038① |
| **（删除）伪域挂载** `domain-dashboard` / `domain-statistics` | 5 + 4 | **0** | — | 全部 → 10 | — | 挂载删除 | 裁决 33 / D-2a·D-2b |
| **合计（REST 端点卡数）** | **252** | **239** | — | — | — | — | 净 **−13** |

> 账目闭合自检（脚本化，`tmp_t127_verify.cjs` 一次性）：
> `17 + 42 + 72 + 14 + 32 + 14 + 11 + 37 = 239`；01 侧 `56（母本）+2（新增）−41（迁出）= 17`；
> 02 侧 `42（母本）−11（重复条目）+41（迁入）= 72`；02 迁入清单**逐 id 校验无缺漏**。

### 二、关键端点迁移对照（`文件:行` 为**改后**位置；改前位置在括号内）

| 端点 | 改前归属（文件） | 改后归属（文件:行） | 依据 |
|---|---|---|---|
| `GET /api/v1/admin-regions`（`rest-admin-regions-list`） | 01 `01-device-iot.js:1859` | **08 `system.js:80`** | 裁决 27/34 |
| 运维 17 路由（`/maintenance/**`） | 01 `01-device-iot.js:1357-1540` + 02 紧凑 10 条（`02-core-domains.js:211-220`） | **02 `02-core-domains.js:227-500`**（完整定义唯一保留） | 领域 02 裁决 |
| 偏好家族 A（types / type-prefs / merged / user-prefs，7） | 01 `01-device-iot.js:906-1302` | **02 `02-core-domains.js:505-900`** | 裁决 12 修正 / 26 |
| 偏好家族 B（preference-configs 5） | 01 `01-device-iot.js:1633-1806` | **02 `02-core-domains.js:905-1080`** | 裁决 12 修正 / 26 |
| 床用户 Profile（GET/PUT/DELETE + 单键偏好 1） | 01 `01-device-iot.js:1501-1626` / `1853-1886` | **02 `02-core-domains.js:1085-1330`** | 裁决 12 修正 / 26（"人的健康档案……其实都是 02"） |
| 闹钟 8 条（`device-alarm-*`） | 01 `01-device-iot.js:1895-2112` | **02 `02-core-domains.js:1335-1560`** | 裁决 28 |
| `GET /api/v1/dashboard/*`（5） | 02 伪域 `domain-dashboard` | **10 `10-business-orchestration.js`（`domain-business-orchestration.endpoints`，尾部）** | 裁决 33 / D-2a |
| `GET /api/v1/statistics/*`（4） | 02 伪域 `domain-statistics` | **10（同上）** | 裁决 33 / D-2b |
| ~~`POST /api/v1/devices/{id}/control`~~ | 01 `01-device-iot.js:878-939` | **已删除**（不留兼容别名） | 裁决 30' / 领域 01 排除 3 |
| ~~`POST/GET /business/intents`·`/business/devices/{id}/capabilities`·`/business/capabilities`~~ | 10 | **已删除**；`intent_command_map`（14 条）整块删除 | 裁决 1 / 33 D-1a |
| ~~`POST /business/management/actions`~~ | 10 | **已删除** | 裁决 33 / D-1a |
| `GET /api/v1/events`（`rest-events-query`） | **零声明**（实现早在 `modules/dashboard/api/router.py`） | **10（新增声明）** | **N-038①** |

### 三、域键 / 显示名 / 标签变更（viewer 侧可见）

| 项 | 改前 | 改后 | 说明 |
|---|---|---|---|
| 01 显示名 | 设备物联与接入 | **设备接入和管理** | RB-006 领域 01 命名 |
| 03 挂载键 / 域标签 / 显示名 | `domain-sleep-monitor` / `sleep-monitor` / 睡眠监控 | **`domain-sleep-data`** / `03-sleep-data` / **睡眠数据** | 对齐 `domain-registry.js` v2 slug |
| 08 域标签 / 显示名 | `system` / 系统管理（**无域身份**） | **`08-platform-support`** / **平台支撑** | 裁决 27/34/35；⚠️ **挂载键 `domain-system` 保留**（viewer 深链锚点，改名会破坏既有 hash） |
| 10 显示名 | 业务逻辑处理 | **聚合业务逻辑** | 裁决 24（**slug 保留 `business-orchestration`** —— 裁决 33 / D-1c） |
| 伪域挂载 | `domain-dashboard` / `domain-statistics` | **删除**（端点并入域 10） | 裁决 33 / D-2a·D-2b |

### 四、域 02 的"每端点只留一处定义"清理（**重要**）

改前 `01-device-iot.js` 与 `02-core-domains.js` **各有一份运维端点**：01 是完整定义（17 条），
02 是紧凑一行式（10 条，含 `rest-maintenance-network-message-dates` 的**重复路径**）。
本次迁移**只保留 01 迁入的完整定义 17 条**，删除 02 的 10 条紧凑条目
（`rest-diagnostic-*` ×4 · `rest-fault-*` ×2 · `rest-firmware-*` ×2 · `rest-batch-config` ×1 ·
`rest-maintenance-network-message-dates` ×1），
使「同域内 (method, path) 唯一」这条本技能规则 1 成立（新增常驻用例钉住）。
⚠️ `rest-firmware-update`（`POST /control/devices/{id}/firmware/update`）**保留** —— 它是**控制类**端点，
不是运维端点。

### 五、`_trace` 修正（`T-126` 交接的同文件残留）

`design/04-platform-api/data/rest/system.js` 的 `_trace.consumes`：

| | 值 |
|---|---|
| **改前** | `["er:01-device-iot-er", "er:05-identity-auth-er"]` |
| **改后** | `["er:08-platform-support-er", "er:05-identity-auth-er"]` |

理由：`T-126` 已把 system 相关实体（`admin_region` / `system_setting` / `localization_language` /
`localization_entry` / `help_page` / `help_media`）落到 `design/03-entity-relationship/data/08-platform-support.js`。

### 六、计数常量同步（`scripts/check-design-asset-render.mjs`）

| 断言 | 改前 | 改后 |
|---|---|---|
| `AI 智能体` L2 | 42 | 42（不变） |
| `业务逻辑处理` → **`聚合业务逻辑`** | 7 | **11** |
| `仪表盘` | 5 | **（视图删除，并入域 10）** |
| `数据统计` | 4 | **（视图删除，并入域 10）** |
| `设备控制与运维` | 42 | **72** |
| `设备物联与接入` → **`设备接入和管理`** | 57 | **17** |
| `身份认证与权限` | 32 | 32（不变） |
| `音乐库` | 14 | 14（不变） |
| `睡眠监控` → **`睡眠数据`** | 13 | **14** |
| `系统管理` → **`平台支撑`** | 36 | **37** |
| **REST endpoint 卡数** | **252** | **239** |
| 一级节点数 | 10 | **8**（两个伪域视图删除） |

⚠️ **顺序口径**：断言列表的顺序 = **实测渲染顺序**（不是挂载键字典序）——
实测 `domain-system`（平台支撑）渲染在**最后**，`domain-sleep-data`（睡眠数据）在音乐库之后。
原因：`loader.js` 的 `document.write` 按**文件顺序**注入脚本，`window.API_DATA` 的键插入顺序即渲染顺序；
`api-viewer` 的 `.sort()` 只作用于 `buildNav()` 的**副本**，DOM 顺序由 `renderRestDomains()` 决定。
（T-127 实测：改成字典序会误判 2 项。）

### 七、AI 工具职责迁移（裁决 20/21；`data/ai-tools/tools.js`）

| Tool | 改前 `action` | 改后 `action` | 说明 |
|---|---|---|---|
| `PlayMusic` | `play_online_media`（**设备指令**） | **`media_library_play_request`** | AI 只把业务逻辑扔给音乐库；音乐库自己发布/构建外链 → 发 `PlatformCommand.PlayOnlineMedia`（**producer = 域 09**）→ device-control（02）消费 → 影子 → IoT 层（01）下发 |
| `ControlMusic` | `play_online_media_control`（**设备指令**） | **`media_library_play_control_request`** | 同上 |

> ⚠️ 本资产登记的是**设计契约**；`src/server/ai_agent` 的实现迁移与 `action` 字段最终取值属后续代码任务。

### 八、**未变声明**（刻意不动，逐条列出）

- **端点契约本体一字未改**：所有**被搬迁**端点的 `id` / `addressing.method` / `addressing.path` /
  权限 / 请求响应形状 / 错误码 / 六维度 `business_logic` —— **按行原样搬迁**（只改归属，不重写契约）。
- **`02-core-domains.js` 的三个域文件内容**（04 `ai-agent` 42 条 · 03 睡眠数据 14 条 · 02 设备控制 72 条）
  中，**域 01 未涉及的端点**全部保持原样。
- **`05-identity-auth.js`（32 条）完全未动**。
- **被删除的 10 条紧凑运维条目**：其**完整定义在 01 侧一直存在**（本次只是把完整定义搬到 02 并删掉重复的紧凑条目）
  ⇒ 没有丢失任何端点语义；`rest-firmware-list` / `rest-firmware-upgrade` 两条**路径与实现不符**
  （`/maintenance/firmware` vs 实现的 `/maintenance/firmware-packages`）的旧契约随条目一并作废。
- **`data/iot/**`（ZYRIC 协议 / 事件标识 / 示例）、`data/internal/**`、`data/rest/protocol.js`、
  `data/rest/examples.js`、`data/loader.js`、`api-viewer.html`**：本任务**一字未动**
  （loader 不需要改：文件数不变、挂载键由数据文件自身定义）。
- **`publish/**`**：未动（归 `T-093`）。
- **`src/**`**：未动（实现迁移属后续代码任务）。
- **`docs/requirements-backlog.md`**：只读，未动。

### 九、门禁与反证（原始数字）

| 门禁 | 结果 |
|---|---|
| RB-005 一致性 `node scripts/check-viewer-data-consistency.mjs` | **exit 0 · 24 视图 / 140 条断言 / 0 页错误**（与改前逐项一致：视图数与断言数**未变**） |
| 可表达性 `node scripts/check-design-asset-render.mjs` | **exit 0**；`REST 252 → 239`、一级节点 `10 → 8`（解释见 §六） |
| 5 viewer `file://` 实渲染 | **45 页 0 页错误**（wf 8 域 · er 10 域 + all · api 4 协议页 + 9 深链 · arch 10 视图 · web-admin 1） |
| `python -m pytest tests/design-regression -q` | **260 passed**（改前 **182**，`+78` = 新增 `test_rest_domain_migration.py`） |
| `node --check` | 改动的 6 个 REST/工具 JS 文件逐个全 0 |

**反证（全部在 `$env:TEMP` 副本内做，未用 `git checkout --` / `git restore`）**：

| # | 改了什么（副本内） | 结果 | 结论 |
|---|---|---|---|
| ① | 把已删端点 `rest-device-control` 加回域 01 **＋** 把 `rest-maintenance-diag-tree` 搬回域 01（双持） | **6 failed**（`deleted_endpoint_absent_everywhere[rest-device-control]` · `migrated_endpoint_in_target_mount_only[rest-maintenance-diag-tree]` · `device_iot_has_no_control_endpoints` · `no_duplicate_endpoint_ids` · `expected_mounts_present_with_counts` · `total_endpoint_cards`） | **新用例承重** |
| ② | 把 `check-design-asset-render.mjs` 的 REST 卡数断言 `239` 改成 `240`（同仓副本脚本） | **exit 1** · `✗ api: REST 领域端点 卡数=240 — 实际 239` | **计数断言在拦** |
| ③ | 把 08 域 `_trace.consumes` 改回 `er:01-device-iot-er` | **1 failed**（`platform_support_trace_points_to_domain_08`） | **归属配套断言承重** |
| ④ | 有**零覆盖**证据：本任务实测 —— **改前**的既有三条门禁对「端点搬到哪个域」**全部照旧通过**（`T-126` 在 ER 侧同样实证）⇒ 才新增 `test_rest_domain_migration.py`（78 例）把归属钉成常驻断言 | — | 已按 `T-126` 先例补门禁，未硬造反证 |

### 十、新增常驻用例

`tests/design-regression/test_rest_domain_migration.py`（**78 例**）——判据六类：
① 域清单（8 挂载 × 域标签/显示名/端点数 + 伪域挂载已删 + 旧键 `domain-sleep-monitor` 已删 + 总数 239）；
② **搬迁落点 46 例**（在目标域 ∧ 源域零残留 ∧ 全仓唯一持有者）；
③ **删除落点 17 例**（任何域都不得再出现）；
④ **新增落点 4 例**（含路径）；
⑤ 归属配套（08 `_trace` 指向 08 · 域 01 无 `/control` 端点 · `device-types` 留 01 而偏好模板归 02 ·
   端点 id 全局唯一 · 同域 (method,path) 唯一 · 每端点有 `addressing`）；
⑥ AI 工具职责（`PlayMusic`/`ControlMusic` 不再声明 `play_online_media*` 设备指令 action）。
反证逃生口：`REST_GATE_ROOT=<副本根>`（副本里不需要改用例文件）。

## v77：system 域 — `T-084` 后台模板页**已落地** ＋ 渲染口径残留**按现状校正**（T-135，2026-09-13）

> 来源：`T-135`「设计资产与文档卫生 —— 清 `design/` 里被裁决/实现取代的渲染口径残留」。
> **本条目不改任何端点契约**（端点集合/权限/错误码/请求响应形状**一字未动**），只做**两件事**：
>
> **① 登记状态更正（`T-084` 已实现，不再是"尚未实现"）**：`rest/system.js` 的 system 域 `note`
> 原写「T-084 的后台模板页**尚未实现**，不在此登记」⇒ `T-084` 已于 2026-09-12 交付（前端
> `views/system/SleepReportTemplate.vue` ＋ `sleepReportTemplate.ts`），**14 个 sleep-report 端点在前端全部有引用**
> ⇒ 该句改为「T-084 后台模板页**已实现**（前端契约见 `design/06-web-admin/data/tree.js`「功能区：睡眠报告模板」）」。
> ⚠️ **底图管理 UI**（上传/替换/删除/manifest 编辑）**仍未实现** ⇒ 仍属 **`T-137`**（`T-084` 只做只读展示，见 `design/06` CHANGELOG v23）。
>
> **② 渲染口径校正（`v75` 安全措辞）**：`v75` 的「没有 Jinja2 `SandboxedEnvironment`／WeasyPrint `url_fetcher`
> —— 旧稿口径作废」保留原文（**历史不得篡改**），并就地补齐**现架构口径**：模板语言 = **SVG 受限子集**
> ＋ **自建纯 Python 绑定层（零新增依赖）**（`data-field` 小写键路径查表、**无表达式语言** ⇒ Jinja2 沙箱逃逸
> 那一整类风险**结构上不存在**、白名单 = 渲染上下文、非法路径**抛错**、`data-*` 交接前**全部剥离**）
> ＋ 光栅画笔 **`reportlab → PDF → pypdfium2 → PNG`**（`D-51`/`T-124`）。
>
> **端点计数（复核，未变）**：system 域 **36** 个（REST endpoint 卡 **252**）；其中
> `/api/v1/system/sleep-report/**` 共 **14 个** = GET/PUT 设置 2 ＋ 模板 5 ＋ 底图 6 ＋ 预览 1。
> ⚠️ **本条目只改 `rest/system.js` 的 `note` 文本**，**未动任何端点定义行** ⇒
> `scripts/check-design-asset-render.mjs` 的硬编码计数（36/252）**无需同步**（已跑门禁确认 exit 0）。
>
> **未变**（刻意）：`v75`/`v76` 的全部端点定义、`SleepReportPreviewDTO`/`SleepReportTemplateSaveDTO` 等
> 请求契约、权限码（`admin.read`/`admin.write`）、错误契约（`422/404/409 + {code,reason,details[]}`）**一字未改**。

## v76：system 域 — 睡眠报告**模板预览** 1 端点（T-083，2026-09-12）

> 来源：`T-083`（预览 API —— 运营改模板时"**保存前先看效果**"）。前置 `T-082`（模板外置 API/
> 版本/回滚/底图管线，`rest/system.js` 24 → 35 端点）已落地。
> `rest/system.js` 新增 1 端点（系统管理 35 → **36**；REST 端点卡 251 → **252**，已同步
> `scripts/check-design-asset-render.mjs` 的硬编码计数 —— 见 `N-065③` 的纪律）：
>
> - `POST /api/v1/system/sleep-report/preview` —— 把（**未保存的草稿** / 当前生效）模板渲染成 PNG。
>
> **契约要点**：
> - **只读**：`admin.read` 即可（**不落版本、不激活、不落盘**，与 `GET …/template` 同档）；
> - **响应形态选 JSON**（`{page_index, page_count, png_base64, png_sha256, template, data,
>   background, warnings, design}`）而不是 `image/png` 原始字节流：多页版式必须**指明第几页**，
>   运营还需要 `sha256`/尺寸/告警做"预览 ↔ 保存后"对比（理由见 `application/preview.py` docstring）；
> - 🔴 **草稿模板必须走与 `PUT /template` 同一套 fail-fast 校验**
>   （`SqlTemplateStore.validate_draft` ⇒ `validate_bundle`）⇒ **预览不是绕过保存校验的后门**：
>   外链 / 路径穿越 / SVG 子集外元素 / 未知 `data-*` 一律 **422 且拒绝理由与 `PUT` 逐字一致**；
> - **可选参数**：`layout`(single/long/multi) · `locale`(zh/en) · `width`/`height` ·
>   `page_index`（**越界显式 422**，理由给出合法范围）· `background`（`manifest` 当前清单 /
>   `bundled` 随包默认 / `none` 不画底图）· `kind`(day/trend) · `period` · `data`（自带数据）；
> - **数据源默认内置示例、不触 anyishui**（随包 `assets/samples/sample_report.json`，与
>   `tests/fixtures/sleep_report/` **同源同字节**）；响应恒带 `data.real_data = false`，
>   **不提供"指定设备 + 某日"**（会触外部平台 + 需 `D-34` 租户/设备归属校验，属另一件事）；
> - **渲染走生产同一路径**（`svg_report.render_report`）⇒ 非 720 分辨率一律 **`viewBox` 等比缩放**
>   （`T-081` 口径，预览**不另造缩放逻辑**）；同参数下**预览产物与真实渲染逐页 `SHA256` 一致**。
>
> ⚠️ `T-084`（后台模板页的编辑区/预览区）**尚未实现**，故不登记。

## v75：system 域 — 睡眠报告**模板外置** 5 端点 ＋ **底图资产管线** 6 端点（T-082 · RB-007，2026-09-12）

> 来源：`T-082`（模板 CRUD/版本/回滚 ＋ 安全 ＋ 底图资产管线）。前置 `T-081`（`9fb2a33c`：
> SVG 模板 ＋ 自建绑定层 ＋ 每页底图 slot）已落地，故安全口径按**现架构**写
> （**没有** Jinja2 `SandboxedEnvironment`／WeasyPrint `url_fetcher` —— 旧稿口径作废）。
> 现架构口径（`T-135` 核对，2026-09-13）：**模板语言 = SVG 受限子集** ＋ **自建纯 Python 绑定层（零新增依赖）**
> —— `data-field` 小写键路径查表、**无表达式语言** ⇒ **Jinja2 沙箱逃逸那一整类风险在结构上不存在**，
> 而不是"有沙箱但被绕过"；白名单 = 渲染上下文、非法路径**抛错**、`data-*` 交给渲染器前**全部剥离**；
> 光栅画笔 = **`reportlab → PDF → pypdfium2 → PNG`**（`D-51`/`T-124`，纯 wheel、无系统库）。
> ⇒ 本节安全语义的**权威依据**是 `T-081`/`T-082` 的实际实现，**不得照抄任何 Jinja2/WeasyPrint 时代的措辞**。
> `rest/system.js` 新增 11 端点（系统管理 24 → **35**）：
> - **模板外置**：`GET/PUT …/sleep-report/template`（PUT = 保存为新版本并**立即生效**）、
>   `GET …/template/versions`（历史，新→旧）、`POST …/template/rollback`
>   （`{version:N}` 复制旧版为新版；`{version:"default"}` 恢复随包默认）、`GET …/template/default`；
> - **底图资产**（`RB-007`）：`GET/POST …/backgrounds`、`PUT/DELETE …/backgrounds/{name}`、
>   `GET/PUT …/backgrounds/manifest`（**清单编辑入口 = API**，不随模板走）。
>
> **契约要点**：权限读 `admin.read` / 写 `admin.write`；错误 422（校验失败）/ 404（版本或资产不存在）/
> **409（底图被引用，防误删）**，`detail = {code, reason, details[]}` —— **拒绝理由必须可断言**。
> **存储**：模板走 DB 版本表 `sleep_report_template_versions` ＋ `system_settings` KV 激活指针
> （迁移 `c8d9e0f1a2b3`）；底图走托管目录 ＋ `sleep_report_backgrounds` 元数据。
> **`GET …/template` 的 `layout` 段**回答 `G-20` 的"谁来折行、宽度从哪来"
> （绑定层预折行；宽度来自模板内容区 / 每页安全区 / 清单 `wrap.width`，逐页给出来源）。
> ⚠️ `T-083`（预览 `POST …/preview`）与 `T-084`（后台模板页）**尚未实现**，故不登记。
> ⚠️ `publish/` 同步留给 `T-093`。

## v74：域10 `/business/management/aggregate` 接入 aggregate registry ＋ `/management/actions` 接入 action registry（T-102 · D-31，2026-09-12）

> 来源：用户 2026-09-11 裁决 **D-31**（接受「诚实的空壳」现状 ＋ **另立 T-102 补全域10 registry**）。
> 缺陷实质：`AdminManagementService` 的 `aggregate_registry` / `action_registry` **恒为 `{}`**
> （`_admin_services()` 从不注入）⇒ `/management/aggregate` 恒返回承载壳、`/management/actions`
> 除硬编码的 `device.control` 外恒 `not_implemented`。
> **T-102 实际接入 4 个 scope**（`devices`/`alarms`/`events`/`media`，每个 handler 只调**对应业务域
> 自己的读接口**：`DeviceService.list_devices` / `StatsService.alerts` / `EventQueryService.query` /
> `MediaLibraryService.list_files`），**`reports` 未挂载**（睡眠报告为按设备实时外呼 anyishui 的
> 外部数据、两域无可复用读接口，设计也未声明其 filters/summary ⇒ 按 D-31「不得自己发明」）。
> **`action_registry` 只含 `device.control`**（平台里唯一有据的管理动作，行为与既有内建分支逐项一致）。
> 🔴 **本任务报告的设计缺口（未擅自补全，作为待裁决项上报）**：本端点 precondition 提到的
> 「**管理动作清单**」在整个 `design/` 里**只被引用这一次、从未定义**；`module` 枚举开放式，
> `action` 只有「**如** `device.control` / `device.config`」的示例 ⇒ **除 `device.control` 外的动作
> 一律无依据**，不实现、由承载壳如实呈现。
> 另：**N-031 `time_range`** 有了结论 —— **不改端点签名**（避免扩契约），改由既有 `filters.time_range`
> 传入，且**仅 `events` scope 生效**（闭合集合 `last_1h/last_24h/last_7d/last_30d/last_90d`，
> 映射到 `EventStore.query` 既有的 `time_start`/`time_end`）；其余 scope **明确不支持**并在
> 响应 `note` 中**如实回显该键未生效**（不静默忽略、不造假）。
>
> 📌 **执行期间的三条新裁决（本条目收尾时发现，已对齐）**：**D-41**（2026-09-12，P-027）已在
> **裁决层面**给出「管理动作清单」结论（`device`+`alarm`+`report`+`firmware`/`diagnostics`，
> 口径＝批量/跨设备 ＋ 高危），但其**实现计划被 D-42 冻结**、且按 D-41⑤ **必须先把清单写进设计资产
> 再写代码**；**D-42** 进一步**纠正域10 定位**（「它是一个转发层，它是业务逻辑层」，非批量操作层）
> 并启动**全域网功能设计重新审核**；**D-43** 裁定审核范围（全部 10 域、先出域10 样板 = `T-118`）
> 并**放行 `T-102` 跑完**，同时点名域10 的 4 个审核争点（含 **`design/04` 的 `scope` 枚举是否该收窄**）。
> ⇒ 本条目的处置：**action 侧只纳入 `device.control`**（在三条裁决下依然正确，且**不做** `device.control`
> 的批量变体 —— D-42 明确"这次先保持单设备语义"）；**设计资产里的「管理动作清单」缺口按现状如实标注**
> （不把 D-41 的清单写进资产 —— 那正是 D-41⑤ 要求"先行"的那一步，属另一任务）。

| 变更 | 内容 |
|------|------|
| `data/rest/10-business-orchestration.js`（`rest-business-management-aggregate`） | `description` 补 T-102 现状（4 个 scope 已挂载 / `reports` 未挂载 + 理由）；**新增 `business_logic.handler_registry`**：逐 scope 的 `source_domain` / `source_api` / `filters` / `filter_basis` / `summary` / `summary_basis`（**逐个写出"依据是什么"**，因为设计原文未逐项枚举 filters 键）；`query_params` 补 `page`/`size`，`time_range` 条目写明 N-031 的落地口径 |
| `data/rest/10-business-orchestration.js`（`rest-business-management-action`） | `description` 写明 action registry 只含 `device.control` ＋ **「管理动作清单」缺口**；`preconditions[0]` 标注该清单**从未定义**；`steps[④]` 补 registry 命中/未命中的两条路径 |
| `data/rest/10-business-orchestration.js`（文件头） | 版本日期 2026-09-11 → **2026-09-12**，设计来源追加 **T-102（D-31）** |

## v73：09 音乐库 — 删除 `GET /playback-records` ＋ `tracks/select` 补租户圈定语义（T-105 · D-33 ＋ D-34/D-38，2026-09-12）

> 来源：用户 2026-09-11 裁决 **D-33**（媒体侧不做播放记录持久化）＋ **D-35**
> （换曲「不重复」只要求与上一轮那一首不同）。
> `data/rest/09-media-library.js`：**14 → 13 端点**。
> - **删** `rest-media-playback-records`（`GET /api/v1/media-library/playback-records`）；
> - **改** `rest-media-tracks-select`：描述改为**无状态、不写任何记录**，并补齐
>   **租户圈定语义**（body 的 `device_id` 经 `tenancy.ensure_device_in_tenant`：
>   他租户 403 / 设备不存在 404 / 平台级放行；不传 `device_id` ⇒ 纯内容选曲）
>   ＋ **D-35 换曲口径**（排除只认调用方入参）＋ 请求体补 `recent_ids[]`
>   ＋ `errors: {403, 404}`；
> - **保留** `rest-media-playback-url`（`POST /playback/{file_id}/url`）并显式注明它是
>   **内容直链签发**、**不是**播放记录，T-105 **未动**；
> - 实现备注改为 13 端点 / **三表**（`media_files`/`media_tags`/`media_file_tags`）。
> `data/ai-tools/tools.js`（SearchMediaLibrary）：mode 描述去掉"热度"、`next` 改为只认调用方入参、
> **新增 `recent_ids` 参数**、notes 补 T-105 口径。

## v72：dashboard/statistics 8 个聚合端点 + `/business/management/aggregate` 补租户圈定（T-110 · D-38）（2026-09-11）

> 来源：**T-107** 全模块「设备作用域端点」普查把 dashboard/statistics 的 8 个聚合端点
> 与 `GET /business/management/aggregate` 列为**「模糊待问」**（`design/04` 给它们声明的权限码是
> **租户级** `device.read` / `business_orchestration.admin`，**从未声明平台级专用**）
> → 编排器按 D-37③ 上交用户裁决 → 用户 2026-09-11 选 **A** ⇒ **D-38**：这些端点
> **一律按 D-34 租户级圈定，不设「平台级专用」例外**（其中 `POST /management/actions` 已由
> T-111 先行修复 ⇒ 本任务实际施工 9 条）。
> **缺陷实质**：`dashboard/api/router.py` 当时只有 `/events` 有圈定，8 个聚合端点**零圈定** ⇒
> 任何持 `device.read` 的租户成员都能拿到**全租户**的设备总数 / 状态分布 / 心跳趋势 / 活跃告警 /
> 分组与型号聚合；其中 `GET /statistics/operator` 按租户分组，把他租户的**名称与设备数**一并返回。
> **实测复证（反证 A 原始输出）**：把设备集合短路成「平台级不过滤」后，0 台设备租户调
> `GET /dashboard/stats` ⇒ `{"totalDevices":3,"activeAlerts":1,"totalGroups":3,…}`；
> `GET /statistics/operator` ⇒ `[{"name":"运营商甲","deviceCount":2},{"name":"运营商乙","deviceCount":1}]`
> （**他租户名称确实泄露**）⇒ 10 例转红。
> 原设计**未声明**租户圈定 ⇒ 本条是**设计缺口**的补齐，不是措辞错误。

| 变更 | 内容 |
|------|------|
| `data/rest/02-core-domains.js`（`rest-dashboard-stats` / `rest-dashboard-device-status` / `rest-dashboard-usage-trend` / `rest-dashboard-alerts`） | 4 个 `description` 补 **T-110 租户圈定语义**：平台级（`tenant_id is None`）可见全部；租户级只统计本租户设备（含活跃告警数与分组数）；**0 台设备 ⇒ 空聚合**（不得退化为「不过滤」） |
| `data/rest/02-core-domains.js`（`rest-stats-operator` / `rest-stats-deployment` / `rest-stats-zones` / `rest-stats-devices`） | 4 个 `description` 补 T-110 语义：分组 / 型号维度只保留**确实拥有可见设备**的分组；**`/statistics/operator` 对租户级退化为「只有一个分组」= D-38 明确的预期行为** |
| 同域两处 `implementation.note` | 分别追加「批次 7（2026-09-11，T-110 · D-38 租户圈定）」说明缺陷形态与修复口径 |
| `data/rest/10-business-orchestration.js`（`rest-business-management-aggregate`） | `description` + `business_logic.steps` 补租户圈定：租户级只可见本租户设备范围（0 台 ⇒ 空结果）；**handler 契约**为必填 `tenant_id` + 三态 `device_ids`（`None` 平台级不过滤 / `[]` 空结果 / 非空集合）；如实标注当前 registry 为空 ⇒ 返回不含数据的承载壳 |

- **未变**（刻意）：端点路径 / 方法 / `permission`（`device.read` / `business_orchestration.admin`）/
  请求参数 / `stats.totalEndpoints`（dashboard 5、statistics 4 **均未变**，无端点增删）**一字未改**；
  `rest-dashboard-sleep-stats`（501 占位，**不在**缺陷基线内）**未动**。
- **配套实现**：`modules/dashboard/api/router.py`（8 端点全部挂 `Depends(get_current_tenant_id)` +
  新增 `_visible_device_ids` 三态 helper；`_visible_event_device_ids` 复用其尾部）、
  `modules/dashboard/application/services.py`（`StatsService` 8 个方法**必填 kw-only `device_ids`** +
  `_device_scope_condition`（`is not None` 判定））、
  `modules/business_orchestration/{api/router.py, application/services.py}`（aggregate 端点挂依赖 +
  `list_management_aggregate` **必填 kw-only** `tenant_id`/`device_ids` 并下传 handler）、
  门禁 `DEFECT_BASELINE` **11 → 2**（移除 9 条，**未**塞进 `PLATFORM_ONLY`）。

## v71：sleep-monitor 5 端点补租户圈定（T-108 · D-34）（2026-09-11）

> 来源：**T-107** 全模块「设备作用域端点」普查（`DEFECT_BASELINE` 判定为缺陷，标 `T-107 缺陷`）
> → 用户 2026-09-11 裁决「两项安全修复插到 `T-096` 之前」→ 立项 **T-108**；口径沿用 **D-34**。
> **缺陷实质**：`sleep_data/api/router.py` **全文件 `tenant` 命中数 = 0** ⇒ 5 个端点全部零圈定：
> ① 绑定列表返回**全部租户**的床→睡眠监控设备绑定；② 绑定详情无归属校验；
> ③④⑤ daily / trend / export 只要给一个（**可任选**的）`device_id` 就会**真的调用 anyishui 平台**
> 并把他租户的**真实睡眠数据**返回给调用者。
> **实测复证（修复前，独立探针原始输出）**：租户乙用租户甲设备调 daily ⇒ **200**
> `{"deviceId":"P-BED-A","reports":[{"imei":"aa:bb:cc:dd:ee:01",…}]}` ＋ 外部调用流水
> `['report:aa:bb:cc:dd:ee:01']`（**确实发起了外部调用**）；`bindings` 列表 ⇒ 租户乙拿到
> `{P-BED-A, P-BED-B}` 两条（含他租户）。
> 原设计**未声明**租户圈定 ⇒ 本条是**设计缺口**的补齐，不是措辞错误。

| 变更 | 内容 |
|------|------|
| `data/rest/02-core-domains.js`（`rest-binding-list` / `rest-binding-device` / `rest-report-daily` / `rest-report-trend` / `rest-report-export`） | 5 个端点的 `description` 补 **T-108 租户圈定语义**：平台级（`tenant_id is None`）可见全部；租户级只可见本租户床（0 台 ⇒ 空结果）；他租户 ⇒ **403**、床不存在 ⇒ **404**；daily/trend/export 明确"**不发起任何外部调用**"（归属校验早于 `_resolve_bindings` 的被抢检测与 anyishui 查询）；绑定列表 `total` 为**圈定后**总数 |
| 同域 `implementation.note` | 追加「批次 6（2026-09-11，T-108 · D-34 租户圈定）」说明缺陷形态与修复口径 |

- **未变**（刻意）：端点路径 / 方法 / `permission`（`sleep_data.read` × 4 + `sleep_data.export` × 1）/
  请求参数 / `stats.totalEndpoints`（**13 未变**，无端点增删）**一字未改**。
- **配套实现**：`modules/sleep_data/application/services.py`（新增 `_ensure_device_in_tenant`；
  `_resolve_bindings` **首行**做归属校验 = daily/trend/day-raw/trend-raw/`fetch_status` 的**共同咽喉**；
  `list_bindings`/`get_binding` 加**必填 kw-only** `tenant_id`）、
  `modules/sleep_data/api/router.py`（5 端点全部挂 `Depends(get_current_tenant_id)`）、
  `modules/sleep_data/infrastructure/repositories.py`（`device_ids` 过滤，**`is not None`** 判定）、
  门禁 `DEFECT_BASELINE` **16 → 11**（移除 sleep_data 5 条，**未**塞进 `PLATFORM_ONLY`）。

## v70：`/business/management/actions` 服务层下发补租户圈定（T-111 · D-34）（2026-09-11）

> 来源：T-109 §7④ 的过程发现（**同文件内的第二条**跨租户设备控制路径）→ 编排器独立复证 →
> 立项 **T-111**（判定为**最高优先安全项**）；口径沿用 **D-34**，非新决策。
> **缺陷实质**：`POST /api/v1/business/management/actions` 门禁**仅** `BusinessOrchestration:admin`，
> 而 `AdminManagementService._dispatch_action` 把 `payload.device_id` **直达**
> `BusinessOrchestrationService.execute_device_intent` ⇒ **绕过** T-109 只加在 router 层的归属校验。
> 实测（T-111 复证）：租户乙成员（角色**仅**含 `business_orchestration.admin`）对租户甲设备
> ⇒ **200** `{"result":{"status":"ok","command_id":…}}` ＋ `orchestration_requests` / `event_store` 各 1 行。
> ⚠️ **仅凭 `BusinessOrchestration:admin` 权限码无法提供租户隔离**（该码可被自由授予任意租户成员）。
> 原设计**未声明**租户圈定 ⇒ 本条是**设计缺口**的补齐，不是措辞错误。落点选**唯一咽喉**
> `execute_device_intent`（治本：覆盖全部下发路径），而非只堵 `_dispatch_action` 一处。

| 变更 | 内容 |
|------|------|
| `data/rest/10-business-orchestration.js`（`rest-business-management-action`） | `business_logic.steps` 4 步 → **5 步**，新增 **③ 请求形状校验 ＋ 设备归属校验**（`payload.intent.capability` 必填 ⇒ 400；他租户 ⇒ **403**；设备不存在 ⇒ **404**；**均在审计落库与下发之前**）；新增 **`tenant_scoping`** 字段，说明"下发实施点在**唯一咽喉** `execute_device_intent`"且"仅凭 admin 权限码不构成租户隔离"；`preconditions` 补归属要求；`side_effects` 补"被拒的跨租户请求**不产生任何持久化副作用**"；**补 400**（`module/action` 必填 / 缺 `capability` / `device_id` 缺失为空）、**403**（补"device_id 不属于当前租户"）、**404**（设备不存在） |
| 文件头 | `版本：v2 | 日期：2026-09-11` → **`版本：v3 | 日期：2026-09-11`**（T-109 已把本文件同步到 v2，本版**基于 v2**） |

- **未变**（刻意）：端点路径/方法/`permission`/`request_body` 四字段**一字未改**；
  `GET /management/aggregate` **未动**（属 **T-110** 范围，仍留在门禁 `DEFECT_BASELINE`）。
- **配套实现**：`application/services.py`（咽喉 `execute_device_intent` 加**必填 kw-only** `tenant_id`
  ＋ `ensure_device_in_tenant`；`execute_management_action` 穿参并在审计前预校验）、
  `api/router.py`（`/management/actions` 挂 `Depends(get_current_tenant_id)`；注入
  `device_tenant_resolver`；`_to_http` 映射 403/404/400）；门禁 `DEFECT_BASELINE` **17 → 16**
  （移除本条，**未**塞进 `PLATFORM_ONLY`）。

## v69：business-orchestration 4 端点补租户圈定（T-109 · D-34/D-38）（2026-09-11）

> 来源：T-109「安全：`business_orchestration` 4 端点跨租户圈定」（T-107 准入普查判定为**缺陷** →
> 用户 2026-09-11 裁决 **D-37/D-38** 后单独立项，口径沿用 **D-34**，非新决策）。
> **缺陷实质**：`src/server/modules/business_orchestration/api/router.py` 全文件零 `tenant` 字样
> ⇒ 4 个端点**从未做租户圈定**；其中 `POST /intents` 的 `device_id` **直接取自请求体、零归属校验**
> ⇒ **任何持 `business_orchestration.execute` 码的用户可向任意租户的设备下发指令**
> （实测：跨租户 POST 返回 200 + 落 `orchestration_request` 1 行 + 发 `PlatformCommand` 到
> `event_store` 1 行）—— 这是"能动手"而不只是"能看"。
> ⚠️ 本条**不是**设计措辞错误（原设计只是**未声明**租户圈定），是**设计缺口**；本版把口径补齐。

| 变更 | 内容 |
|------|------|
| `data/rest/10-business-orchestration.js`（`rest-business-intent-execute`） | `business_logic.steps` 5 步 → **6 步**，新增 **③ 设备归属校验**：租户级 device_id 必须属于本租户，**否则 403 且不产生任何下行/编排请求**；平台级（全局角色，无 `X-Tenant-Id` ⇒ `tenant_id` 为 None）放行全部租户设备。`preconditions` / `request_body.device_id` 补归属要求；**补 `403`（他租户）** 与 **`404`（设备不存在）** |
| `data/rest/10-business-orchestration.js`（`rest-business-intent-status`） | 新增 **② 租户圈定**：记录不属于本租户 ⇒ **404**（并说明"与 not_found 不区分，避免泄露他租户记录是否存在"）；`preconditions` 补归属要求；`404` 说明同步 |
| `data/rest/10-business-orchestration.js`（`rest-business-device-capability`） | 新增 **① 设备归属校验**（他租户 ⇒ **403**；不存在 ⇒ 404；平台级放行）；`preconditions` 补归属要求；**补 `403`** |
| `data/rest/10-business-orchestration.js`（`rest-business-intent-list`） | `business_logic.steps` 3 步 → **5 步**：留空 `device_id` ⇒ **平台级不按设备过滤（全部租户）/ 租户级仅限本租户设备集合（本租户无设备 ⇒ 空结果，不得退化为不过滤）**；指定 `device_id` ⇒ **先归属校验（他租户 ⇒ 403）**。`query_params.device_id` 补归属要求；**补 `403`**；200 响应说明补可见范围 |
| 文件头 / `last_updated` | `版本：v1 | 日期：2026-09-04` → **`版本：v2 | 日期：2026-09-11`** |

- **未变**（刻意）：4 个端点的路径/方法/`permission`/其余 `query_params`/其余响应字段**一字未改**；
  **端点数量不变**；`/business/capabilities`（代码内静态枚举）与 `management/*`（属 **T-110** 范围）
  未改 —— `management/*` 的 2 条仍留在 T-107 门禁的 `DEFECT_BASELINE` 中。
- **语义边界**：平台级"可见/可操作全部租户"是**有意**（D-34 口径①），**未**登记进门禁的
  `PLATFORM_ONLY` 白名单 —— 它以 `get_current_tenant_id` 的角色分层实现，无需白名单豁免。
- **配套实现**：`business_orchestration/api/router.py`（`get_current_tenant_id` + `_ensure_device_tenant`
  / `_visible_device_ids`，与 `maintenance` 同语义）+ `infrastructure/repositories.py`（`device_ids`
  用 `is not None` 判定，`[]` ⇒ 空结果）；回归用例 `tests/api/business_orchestration/test_tenant_scoping.py`
  （TC-BO-101~108）。

---

## v68：设备消息三端点留空 `device_id` 的可见范围按角色分层（T-106 · D-34）（2026-09-11）

> 来源：T-106「安全：`network-messages` / `export` / `dates` 跨租户日志暴露」（T-103 施工中实测发现，
> 同属用户 2026-09-11 裁决的 **D-34** 口径，非新决策）。
> 原设计 `query_params.device_id` 写「**空 = 全部租户设备**」——**这正是缺陷的措辞来源**；
> 实现为 `device_ids[0] if len(device_ids) == 1 else None` ⇒ **租户设备数 ≠ 1（0 台 / ≥2 台）时
> 归并成 `None` ⇒ 完全不过滤 ⇒ 返回全部租户的设备消息**（`event_store.query` 的 `if device_id:`
> 真值判断叠加）。⚠️「≥2 台设备」是**普通路径**，非边缘情况。

| 变更 | 内容 |
|------|------|
| `data/rest/01-device-iot.js`（`rest-maintenance-network-messages`） | `business_logic.steps` 由 4 步改为 5 步并明确分层：**① 指定 device_id ⇒ 归属校验（越权 403）**；**② 留空 ⇒ 平台级（全局角色，无 `X-Tenant-Id` ⇒ `tenant_id` 为 None）不按设备过滤（全部租户）；租户级仅限本租户设备集合（本租户无设备 ⇒ 空结果）**；③ 设备集合 + 日期/方向过滤；④ 方向推导；⑤ limit/offset 分页 |
| 同上（`query_params.device_id`） | `设备` → **`设备（空=平台级看全部租户/租户级只看本租户；本租户无设备则空结果）`**——旧措辞「空 = 全部租户设备」即缺陷成因 |
| 同上（`preconditions`） | `登录 + X-Tenant-Id` → **`登录（全局角色可不带 X-Tenant-Id）`** |
| `data/rest/01-device-iot.js`（`rest-maintenance-network-message-dates`） | 同上分层（步骤 ①②③④）；`query_params.device_id` 由 **`设备（空 = 全部租户设备）`**（本行就是缺陷措辞）改为分层说明；`preconditions` 同改；**补 `400 INVALID_PARAMETER`**（`limit > 90` 实际返回 400，原设计漏登记） |
| `data/rest/01-device-iot.js`（`rest-maintenance-network-messages-export`） | 步骤①②改为显式分层（原写「① 同列表过滤」过于含糊，掩盖了归并语义）；`query_params.device_id` 同补分层说明；`preconditions` 同改；CSV 无数据时仅表头的语义写入步骤② |

- **未变**（刻意）：三个端点的路径/方法/`permission`/其余 `query_params`/`responses`/`caching`
  **一字未改**；响应契约（`items[]`、**无 `total`**、CSV `filename + content`）保持原样；**端点数量不变**；
  `diagnostics/tree` 等邻近端点**未改**（其留空语义本就是"逐个 iterate 本租户设备"，实测安全）。
- **语义边界**：本次只改**留空 `device_id`** 时的可见范围；**指定 `device_id`** 时的
  `_ensure_device_tenant` 403 行为**保持原样**（平台级放行）。
- **同源登记**：`non-event-logs` 的角色分层见 **v67**（T-103）；本 v68 与其**同一 D-34 口径**，
  差异仅在 `non_event_logs` 额外含 `device_id IS NULL` 的系统级日志，而 `event_store` 记录**都带 device_id**
  （已实测 `EVENT_SEED_SPECS` 全量 13 条设备事件均非 NULL），故无系统级日志特例。

---

## v67：`/maintenance/non-event-logs` 留空 `device_id` 的可见范围按角色分层（T-103 · D-34）（2026-09-11）

> 来源：T-103「安全：non-event-logs 跨租户日志暴露」（用户 2026-09-11 裁决 **D-34**）。
> 原设计与实现把「`device_id` 为空」等同于「系统级日志」，而实现为 `device_ids = None`
> ⇒ 仓储 `if device_ids:` 判真值 ⇒ **完全不过滤** ⇒ **租户管理员可读到其它租户设备的日志**
> （跨租户数据越权）。用户口径：**平台级可以看系统日志；其他（租户级）只能看租户自己的日志；
> 租户管理员不能看系统日志**。

| 变更 | 内容 |
|------|------|
| `data/rest/01-device-iot.js`（`rest-maintenance-non-event-logs`，`business_logic.steps`） | 由「① 按租户过滤设备（device_id 归属校验；系统级日志 device_id 为空）」拆为 4 步并明确分层语义：**① 指定 device_id ⇒ 归属校验（越权 403）**；**② 留空 device_id ⇒ 平台级（全局角色，无 `X-Tenant-Id` ⇒ `tenant_id` 为 None）不按设备过滤（可见全部 + `device_id` 为空的系统级日志）；租户级仅限本租户设备（自然排除系统级日志；本租户无设备 ⇒ 空结果）**；③ log_type/日期过滤；④ limit/offset 分页 |
| 同上（`query_params.device_id` 描述） | `设备（空=系统级日志）` → **`设备（空=平台级看全部/租户级只看本租户）`**——旧措辞正是本次缺陷的语义来源 |
| 同上（`preconditions`） | `登录 + X-Tenant-Id` → **`登录（全局角色可不带 X-Tenant-Id）`**（与 `get_current_tenant_id` 实际契约一致：无 header ⇒ 仅全局角色放行，否则 403） |

- **未变**（刻意）：路径/方法/`permission`/`query_params` 其余 6 项/`responses`/`errors`/`caching` **一字未改**；
  响应契约（`items[]`、**无 `total`**、`deviceId` 可为 null）保持原样；**端点数量不变**。
- **语义边界**：本次只改**留空 `device_id`** 时的可见范围；**指定 `device_id`** 时的 `_ensure_device_tenant`
  403 行为**保持原样**（平台级放行）。
- ⚠️ **同文件邻近 4 端点（`network-messages` / `network-messages/dates` / `network-messages/export` /
  `diagnostics/tree`）的留空语义本次【未改】**——其中前 3 个实测**另存在同类跨租户泄露**（见 `T-103` 报告：
  `list_network_messages` 用 `device_ids[0] if len(device_ids) == 1 else None` ⇒ 租户有 0 台或 ≥2 台设备时
  退化为**不过滤**；`group_by_date` 的 `[]` 同因），属**另一个议题**，只登记不修改。

---

## v66：缓存体系端点补登 + 全新「缓存管理」HTTP 出口（T-074）（2026-09-11）

> 来源：T-074「缓存管理页（前端，D-4 裁定必做）」（缓存体系 v2 批次第 4 个任务）。
> 页面含「命中率与埋点指标 / 能力池分布」，而 T-073 的埋点此前**只在进程内**
> （重启清零、多实例不聚合）——T-073 明确把 HTTP 出口留给本任务；本任务补上出口，
> 并按「设计资产变更六步流程」**同步本资产**（此前 T-019~T-073 期间缓存体系端点**从未登记**，
> 属历史缺口，本次一并补登，避免页面 `api_ref` 指向不存在的端点 → `api_mismatch`）。

| 变更 | 内容 |
|------|------|
| `data/rest/02-core-domains.js`（`domain-ai-agent`，**新增 12 个端点**） | ① `GET /api/v1/ai/cache/stats`（**新**，观测快照=埋点计数+按池分布+DB 池聚合+生效运行参数）；② `POST /api/v1/ai/cache/cleanup`（**新**，分批清理触发）；③ `GET /api/v1/ai/cache/instructions`（补登）；④ `DELETE /api/v1/ai/cache/instructions/{id}`（补登）；⑤ `PUT .../{id}/mark`（补登）；⑥ `PUT .../{id}/ttl`（补登 + **口径更新**）；⑦ `GET /api/v1/ai/config/cache`（补登）；⑧ `PUT /api/v1/ai/config/cache/{config_key}`（补登）；⑨ `GET /api/v1/ai/config/cache/prefetch`（补登）；⑩ `POST /api/v1/ai/config/cache/prefetch/run`（补登）；⑪ `GET /api/v1/ai/cache/results`（补登）；⑫ `DELETE /api/v1/ai/cache/results/{id}`（补登） |
| **权限口径迁移** | 缓存体系端点由 `ai_agent.read`/`ai_agent.write` **整块迁移**为 **`admin.read`/`admin.write`**——缓存是**平台级运行参数**（与「系统设置」同类），若保留 `ai_agent.*`，持 `admin.write` 的运营会看到可写页面却被后端 403（可见不一致）。前端对 `/ai/cache*` 改动前**零引用**（已 grep 确认）⇒ 无破坏 |
| `PUT /api/v1/ai/cache/instructions/{id}/ttl` **语义更新** | 接受**任意整数**：`ttlSeconds ≤ 0` = **不缓存** ⇒ 直接删除条目（响应 `removed=true`）。此前该端点对 `≤0` 返回 422，与 T-073 已落地的领域语义（`ttl ≤ 0 = 不缓存`）**不一致**，管理页无法表达「不缓存」；非整数仍 422 |
| `data/rest/02-core-domains.js`（`stats`） | `totalEndpoints`/`restCount` 27 → **42**（顺带修正：27 为历史陈旧值，原实际端点数已是 30） |

- **未变**（刻意）：协议（`data/iot/smart-bed.js`）与公共枚举值域（device_type 37 / position 13 / zone 3 /
  measure_unit 26 / media_format 2）**一字未改**；`data/ai-tools/*` 的 tool schema 与 role 口径**未动**（T-072 成果保持）；
  缓存**键与内容不含任何 ID**（T-072 硬约束）不变；`admin.*` 权限码本身**不新增**（属既有 17 码之一）。
- **安全边界**：`stats` 只回显计数 / 池签名 / 运行参数，**不回显任何凭证或密钥**；
  条目列表只回显业务内容（指令文本、动作组、回复文本），不含凭证。

---

## v65：AI 工具目标设备参数 sub_device_id → role + device_abilities 改能力清单（T-072）（2026-09-11）

> 来源：T-072「缓存判定 + role/能力签名重构（后端核心）」——缓存体系 v2 的 **role 化**落地：
> 大模型**不再指定子设备 ID**，改输出 role（角色）；解析只在 ToolExecutor 边界发生一次
> （D-6：同 role 多实例 → 全部匹配并下发）。目的：缓存键与内容**不含任何 ID**（可跨设备按能力池复用），
> 且「相近但不等价」不再因向量相似而误命中。

| 变更 | 内容 |
|------|------|
| `data/ai-tools/tools.js`（全部含目标设备的 Tool） | 参数 `sub_device_id:string` → **`role: {device_type, position[], zone[]}`**（三个**协议枚举**；新增文件级 `roleParam()` 构造器，枚举值域内联 = 协议 37/13/3）。`required` 同步改 `role`；`role` 的 description 统一「从能力清单对应三列原样照抄」 |
| `data/ai-tools/tools.js`（GetDeviceStatus / SetDevicePreference / SetDeviceUsageNote） | 目标参数同样改 `role`（可选/必填语义不变；SetDevicePreference 整机级省略 role） |
| `data/ai-tools/tools.js`（`annotate_cache` 注释） | scope 枚举 `global\|device` → `global\|capability`；收窄判据改「输出是否由 LLM 参照设备状态裁量」 |
| `data/ai-tools/tools.js`（提示词模板 / 渲染规则 / 样例 / systemPrompt） | `device_abilities` 由「按 sub_device_id 合并的设备表」改为**能力清单**（每行一个 role：device_type/position/zone 三列 + 能力边界 + 动态状态），**去除设备 ID 列**；`usage_notes`/`user_overrides` 模板键 `{sub_device_id}` → `{role}`；系统指令改「目标设备一律用 role 指定，不要使用任何设备 ID」 |
| `data/ai-tools/protocol.js` | 同口径同步：能力清单说明/列定义/样例渲染/完整 system message 样例/请求样例的 tools schema（`role` 三维枚举） |

- **未变**（刻意）：协议（`data/iot/smart-bed.js`）与公共枚举值域（device_type 37 / position 13 / zone 3 /
  measure_unit 26 / media_format 2）**一字未改**——role 只是把既有协议枚举组合成「能力描述」，
  不新增字段、不新增值；
- **顺带对齐既有漂移**：本资产中 `SetBedMode` / `StartWakeUp` / `StopWakeUp` 的目标参数与
  `StartWakeUp.intensity` 与实现存在 **T-072 之前就有的漂移**（实现：`StartWakeUp` 用
  `mode` gentle/gradual/strong + `duration`、**无目标参数**；`StopWakeUp` 无参数）。
  本次按实现口径改正（`StartWakeUp` / `StopWakeUp` 条目重写；`SetBedMode` 实现确有 role 参数，保留），
  并用脚本比对 **`tool_definitions()`（39 个工具）↔ 本资产逐工具参数/required** → **全对齐、零差异**。
  证据：`tests/test-reports/t072-design-tools-params.json` 与 `t072-ai-facing-after.json`。

---

## v64：connect 组态强校验口径固化 + AI 工具 position 补 enum（T-071）（2026-09-10）

> 来源：T-071「协议对象化与强类型地基」（缓存体系 v2 批次前置任务；用户裁决 D-3
> 「协议值进入系统必须对象化 + 强类型，转不成枚举 = 封包非法」）。**不新增/不删除任何协议字段、
> 不改枚举值域**——本条目只把**既有声明的强校验口径写明**，并补齐一处工具 schema 缺口。

| 变更 | 内容 |
|------|------|
| `data/iot/smart-bed.js`（connect `notes`） | 补明「组态字段类型/枚举强校验（T-071）」：`device_type` ∈ 公共枚举 `device_type`；`position`/`zone` 必须是**数组**（`array[position]`/`array[zone]`，**字符串不是数组**）且元素 ∈ 对应公共枚举；`limits.unit` ∈ `measure_unit`；任一不满足（含历史写法 `side`/`chest`/`zone:["single"]`/`env_*`）→ **`1001 invalid_envelope`**；平台内部一律对象化为强类型 VO，不存在「字符串枚举内部流转」 |
| `data/ai-tools/tools.js`（`GetDeviceStatus.parameters.position`） | 补 `enum`（协议 `position` 13 值）——此前同排 `zone` 有 enum 而 `position` 缺失，与后端 `tools.py` 的工具 schema 缺口同源，本次一并补齐 |

- **未变**（刻意）：`device_type`(37) / `position`(13) / `zone`(3) / `measure_unit`(26) / `media_format`(2)
  的值域与字段类型**一律照旧**——`chest`/`side` 不新增（用户裁决 D-1/D-2），侧边用既有 `surrounded`。
- 实现落点：`src/server/modules/device_iot/domain/sub_device_config.py`（枚举 + `SubDeviceConfig` VO +
  `from_wire()/to_wire()`）、`events/identifier.py`（注册表解析即校验 + D-6 role 唯一性告警）、
  `gateway/iot_gateway.py`（connect 非法 → `1001` 错误回执 + 断开）。
- 门禁：`node --check` ×3 + `check-viewer-data-consistency.mjs` / `check-design-asset-render.mjs` → **[OK]**。

---

## v63：系统管理 +2 端点 — 睡眠报告平台设置（T-068）（2026-09-10）

> 来源：用户裁决 2026-09-10「渲染可配置化 A+B 都做（C 不做）」的 **B（后端）**：睡眠报告版式/语言/
> 页块/主题做成**平台级可配**（后台页面归 T-069）。**不改设备协议**（不动 `smart-bed.js`）。

| 变更 | 内容 |
|------|------|
| 新增端点 | `GET /api/v1/system/sleep-report`（读，合并默认值的完整对象）、`PUT /api/v1/system/sleep-report`（部分更新，字段级校验）——系统管理域 22 → **24 端点** |
| 存储 | `system_settings` **KV 表**（key `sleep_report`，value JSON），无迁移：`default_layout` / `default_locale` / `show{vitals,environment,sides_compare}` / `theme{accent,bg,card}` |
| 校验 | 写入 **422 严格拒绝**（枚举 / 基准语言 / `#RRGGBB` / 严格 bool）；读取**逐字段回落默认**（坏配置不阻断报告） |
| 优先级 | layout：trigger 显式 > 设备软偏好 `report_layout` > 平台 `default_layout` > `single`；locale：硬 `reply_language` > 软 `preferences.locale` > 平台 `default_locale` > `zh`（设备更具体 > 平台默认） |
| 渲染口径 | 页块开关只作用于 `multi`（关闭则页数减少）；`single`/`long` 不受影响；主题色驱动 PNG 背景/卡片/主色 |
| 门禁 | `check-design-asset-render.mjs` 蓝本同步：系统管理 l2 22→24、REST 卡数 227→229 |

---

## v62：补登睡眠报告图片公开直链端点（T-060b 复查）（2026-09-09）

> 来源：T-060b 复查（API 设计资产一致性维度）——已上线的公开直链 `GET /api/v1/sleep-report/images/{image_id}`
> **未进 REST 设计数据**（对照：media-library 的 `/files/{id}/file` 有登记），属文档缺口。

- `data/rest/02-core-domains.js`（`domain-sleep-monitor`）：新增端点节点 `rest-sleep-report-image`
  —— `GET /api/v1/sleep-report/images/{image_id}`，描述含签名/过期/404 语义与 T-062/T-064 结论；
  `stats` 由 `totalEndpoints/restCount = 12/12` → **13/13**。
- `scripts/check-design-asset-render.mjs`：同步蓝本期望值（睡眠监控二级节点 12→13、REST 卡数 226→227）。
- 门禁：`check-viewer-data-consistency.mjs` / `check-design-asset-render.mjs` → **exit 0 / exit 0**；
  `node --check 02-core-domains.js` → exit 0。
- 实现落点（未改）：`src/server/modules/sleep_report/api/router.py`（T-062 前缀对齐）+ `storage.build_url`。

---

## v61：sleep_report_image_push.images[] 的 N 构成定稿 + 多睡眠位渲染（T-062）（2026-09-09）

> 来源：T-061 已把双睡眠位（left+right）各侧原始报告全量落 raw 缓存并给出主报告下标（`main_index`），
> 但协议只写「single/long=1 项、multi=N 项」，**N 的构成未定**；T-062 落地多侧渲染并定稿。

- `smart-bed.js` `sleep_report_image_push.images`：明确 **N 的构成**——
  ① 单睡眠位（both 或仅一侧绑定）：`single`/`long`=1 项、`multi`=3 项（摘要/体征/环境）；
  ② 双睡眠位：`single`/`long`=1 项（**主报告**页，标题标注侧别，不混两侧数据）、`multi`=**N+1** 项
  （每侧 1 张侧别摘要页 + 1 张「多睡眠位汇总」对比页，通常 2→3 项）；**images[] 顺序即展示顺序**
  （前 N 张=各侧摘要，末张=汇总页）。主报告 = 有效睡眠时长最长者（T-061 规则）。
- `smart-bed.js` `sleep_report_trigger.layout` 与 `notes`：同步多睡眠位下的布局语义（multi=N+1）。
- `smart-bed.js` `sleep_report_image_push.notes`：更正 text/voice 去向为 `show_text` / **`play_stream_voice`**
  （T-044 起 voice 走流式通道；原文 `play_voice` 已过时）。
- `protocol.js` `documentHistory` 增 **v3.2** 条目（仅语义/文案澄清，字段类型与结构未变）。
- 实现同步：`src/server/modules/sleep_report/domain/renderer.py`（`render_report(sides=...)` +
  `render_sides_compare` + `summary_title`）、`SleepReportModel.side_models/side_labels/main_index`、
  `SleepDataService.get_daily_report_raw`（带 zone）——详见 `src/server/CHANGELOG.md` v73。
- 门禁：`check-viewer-data-consistency.mjs` / `check-design-asset-render.mjs` exit 0；`node --check` 通过。

---

## v60：睡眠数据拉取报文方案稿作废标注（T-058）（2026-09-09）

> 来源：T-032（2026-08-31 用户裁决）已删除 `sleep_minutes`/`sleep_report_data`/`sleep_trend` 触发/推送全族、
> 并把 `sleep_status_push.devices[].sub_device_id` 定为**真实 MAC**（T-054），但 `sleep-data-pull-protocol-plan.md`
> 仍是 2026-08-20 的「4 Trigger + 4 PushNotify」方案稿、无任何作废标注，易被误当实现依据。

- **处理方式 (a)：保留历史方案稿 + 醒目作废横幅**（不整篇重写——保留 4 Trigger + 4 PushNotify 的字段级设计演进价值，成本低、不丢历史）。
- `sleep-data-pull-protocol-plan.md`：文件头加 **⛔ 已作废（T-032 起）** 横幅 + 指向现行 `design/04-platform-api/data/iot/smart-bed.js`
  「睡眠数据指令（T-032 收敛）」组（`sleep_report_trigger` + `sleep_report_image_push` + `sleep_status_trigger`/`sleep_status_push`）；
  并在 §三（4 组 trigger/push）、§3.2（`devices[].zone`）、§四（3 条已删 push 的 payload）、§八（待确认事项）加**内联作废标注**。
- **引用核查**：全库检索 `sleep-data-pull-protocol-plan` **无 viewer/loader/脚本引用**（仅 `design/01-raw-input/` 有历史提及），
  故不涉及渲染器或数据加载器同步改动。

---

## v59：sleep_status_push.devices[].sub_device_id 语义明确为真实 MAC（T-054）（2026-09-09）

> 来源：用户 2026-09-09 口径「Sub Device ID 本身就是设备的 MAC 地址，用于睡眠平台的设备注册和数据抽取」
> + 审计发现后端 `fetch_status` 实际下发 `left/right/both` 位置别名（实现未跟上 T-032 协议）。
> - smart-bed.js `sleep_status_push.devices[].sub_device_id`：desc 由「睡眠监测设备子设备ID」明确为
>   **值 = 该睡眠监测设备的真实 MAC**（= 绑定表 `cloud_user_bindings.imei`，如 `44:1b:f6:c6:8a:48`），
>   并显式禁止下发 `left/right/both` 位置别名；设备端据此匹配自己的 `sleep_monitor` 子设备。
> - 仅语义澄清（字段类型/必填/结构未变，无新增/删除 action）——协议 T-032 定稿本意如此，本次把口径写死。
> - 实现同步：`src/server/modules/sleep_data/application/services.py::SleepDataService.fetch_status`
>   成功项与 error 项均改为该绑定的真实 `imei`（详见 `src/server/CHANGELOG.md` v69）。

---

## v58：睡眠报告语音走流式通道 + 语义修正（T-044）（2026-09-04）

> 来源：T-044 复核遗留项（T-040 后用户裁决）。
> - smart-bed.js play_voice：语义同步——描述/notes 由「base64 音频内联」修正为「body=语音正文文本（TTS 合成输入）+ voice_role」，设备端合成播放；并注明睡眠报告语音走 play_stream_voice 流式通道（不再 play_voice 直推 base64）。
> - smart-bed.js sleep_report_trigger：enable_voice 分发由 play_voice 改为 **play_stream_voice（流式 TTS + stream 下发）**。
> - smart-bed.js massage：speed 字段描述/notes/errors 修正为 mode（massage_mode：pulse/wave/continuous，默认 wave），intensity（力度）与 mode（手法）语义区分。
> - smart-bed.js wake_up：duration 补语义（**0 = 持续唤醒**，直到 wake_up_stop / 用户打断停，与 lulling 同约定）。

---

## v57：用户指令 → 动作缓存体系（T-019）（2026-08-31）

> 来源：T-019「用户指令 → 动作缓存体系」（2026-08-31）。
> - 新增 REST 端点组 `/api/v1/ai/cache/instructions`（GET 列表 / DELETE /{id} / PUT {id}/ttl / PUT {id}/mark）
>   与 `/api/v1/ai/cache/results`（GET 列表 / DELETE /{id}）；`/api/v1/ai/config/cache`（GET/PUT 运行配置）
>   与 `/api/v1/ai/config/cache/prefetch`（GET 摘要 / POST run 手动触发天气预取）。
> - ai-tools/tools.js：`annotate_cache` 作为系统级工具注记（内部 no-op，schema 见 domain/tools.py；
>   LLM 软信号，只能收窄确定性判定不能放宽）。

---

## v56：知识库使用校验 + 两段式检索（2026-08-29，T-021）

> 来源：T-021「知识库使用校验 + 两段式检索」（2026-08-29）。
> - rest-knowledge-search：更新为 needs_kb 确定性门 + 两段式（确定向量检索注入 + LLM ≤2 次作答；
>   E4 修订：知识库为补充、非唯一知识输入；KB 无命中返回 items=[] 并标记 kb_hit=False）；
>   前端知识库页「测试检索」直接调本端点验证 top-N + similarity。
> - 关键词配置新增 `needs_kb_keywords`（知识库触发词，仿 realtime_keywords，config 端点同组）。
> - process_message（POST /conversations/{id}/messages）响应补 KB 观测：toolsCalled/kbUsed/kbHit/
>   kbSimilarityMax/kbType/intentClass（additive，前端「测试对话」展示是否触发 knowledge_search）。
> - ai-tools/tools.js：knowledge_search 作为系统级工具注记（内部工具，schema 见 domain/tools.py），
>   含 E4 规则 + needs_kb 门 + 两段式 + 可观测埋点说明。

## v55：音乐库域 — 新增 REST 端点（2026-08-29，T-018）

> 来源：T-018「建设并管理音乐库」（2026-08-29）。新增 rest/09-media-library.js：
> 音乐文件 CRUD + 发布 + 直链流式（GET /files/{id}/file）+ 标签词表 + 选曲
> （tracks/select pick/next）+ 播放直链（playback/{file_id}/url）+ 播放审计
> （playback-records）+ link-config。权限 media_library.read/write。

## v54：大模型配置重做——供应商硬编码目录 + 模型用途配置可用化（2026-08-25，T-005）

> 来源：T-005「大模型配置重做（供应商硬编码目录 + 模型用途配置可用化）」（2026-08-25）。
> 供应商目录为代码常量 PROVIDER_CATALOG（仅 deepseek/mimo/mock），base_url/protocol/models 不可编辑、不可新增/删除供应商；模型用途配置可管理（provider/model 按目录校验）。
> 对应 ER 04 v7.15（model_provider 退化 + model_usage_config 精简）。

### 变更内容

| ID | 变更 | 文件 |
|----|------|------|
| `rest-ai-config-model-providers-list` | GET /providers 改为**内置供应商只读目录**（仅 deepseek/mimo/mock，含 kind/baseUrl/protocol/models/掩码 key/enabled） | 02-core-domains.js |
| `rest-ai-config-model-provider-get` | GET /providers/{key} —— 内置供应商详情；key 不在目录 → 404 | 02-core-domains.js |
| `rest-ai-config-model-provider-create` | **删除**（供应商不可新增，目录硬编码） | 02-core-domains.js |
| `rest-ai-config-model-provider-update` | PUT /providers/{key} 改 body **仅 {apiKey?, enabled?}**（apiKey 留空=保留原值；body 含 baseUrl/displayName/model → 400；key 不在目录 → 404） | 02-core-domains.js |
| `rest-ai-config-model-provider-delete` | **删除**（供应商不可删除） | 02-core-domains.js |
| `rest-ai-config-model-usages-list` | GET /usages —— 8 用途，含**默认绑定**（defaultProviderKey/defaultModel）+ 当前生效 + configured | 02-core-domains.js |
| `rest-ai-config-model-usage-update` | PUT /usages/{key} —— provider/model 按内置目录校验（provider 须∈目录且 kind 兼容；model 须∈该供应商固定清单），非法 422 | 02-core-domains.js |
| `rest-ai-config-model-usages-registry` | GET /usages/registry —— 用途定义 + 各 kind 兼容 provider（kindProviders）+ 各 provider 固定模型清单（providerModels/kindModels），作为前端下拉数据源 | 02-core-domains.js |

### 统计

端点总数 29 → **27**（删 POST /providers 创建 + DELETE /providers 删除）。权限仍为读 ai_agent.read / 写 ai_agent.write。

---

## v53：设备使用偏好（真语义）+ 设备类型偏好模板端点（2026-08-25，T-004）

> 来源：T-004「设备使用偏好（真语义）页面 + 现页改『工具调用指引』」（2026-08-25）。
> 真语义：AiConfig.vue 名="设备使用偏好"的第 3 个 Tab 实为**工具描述覆盖表** → 改名「工具调用指引」（只改 i18n/标题/描述 + 设计文档注释；路由/权限 ai_agent.read|write 不动；工具描述覆盖表逻辑不动）。
> DeviceUsagePolicyDrawer（设备管理抽屉，名="设备使用策略"）才是真语义 → 提升为独立「设备使用偏好」页面（`/device-iot/device-preferences`），**废弃该抽屉**。
> 设备类型 = **代码常量** DEVICE_TYPE_CATALOG（协议 device_type 枚举，非 DB 表）；类型偏好模板 = device_type_preferences（跨设备共享，device_type 主键 upsert）。
> 对应 ER 01 v7.14（device_type_preference 实体）。

### 变更内容

| ID | 变更 | 文件 |
|----|------|------|
| 1 | 新增 `GET /api/v1/device-types`：设备类型清单（DEVICE_TYPE_CATALOG 代码常量，按分组，含中文名/单位/类型级硬 limits），权限 device.read | `rest/01-device-iot.js` |
| 2 | 新增 `GET /api/v1/device-type-preferences` + `GET /api/v1/device-type-preferences/{device_type}`：类型偏好模板列表/详情，权限 device.read | `rest/01-device-iot.js` |
| 3 | 新增 `PUT /api/v1/device-type-preferences/{device_type}`：类型模板 upsert（device_type 主键），校验 usage_notes 非空≤2000 字 + pref_min≤pref_max + 不越硬 limits（越界 400），支持单边；权限 device.write | `rest/01-device-iot.js` |
| 4 | 新增 `GET /api/v1/devices/{id}/usage-preference-merged`：每子设备最终生效策略文本（三层来源 type_template/device_override/device_report + 软范围），权限 device.read，供页面「所见即所得」预览 | `rest/01-device-iot.js` |
| 5 | `GET /api/v1/devices/{id}/usage-policies` 改**三层合并视图**：来源由 device_reported/platform_override 改为 **type_template/device_override/device_report**；行新增 pref_min/pref_max/pref_unit 软范围 | `rest/01-device-iot.js` |
| 6 | 设计决策「设备使用策略双层数据源」→「三层合并」（类型模板追加为第二层） | `rest/01-device-iot.js` |

### 端点组速览

| 组 | 端点 | 说明 |
|----|------|------|
| 设备类型 | GET `/api/v1/device-types` | 类型清单（代码常量，分组，含中文名/硬 limits），device.read |
| 类型模板 | GET `/api/v1/device-type-preferences`、GET `/api/v1/device-type-preferences/{device_type}` | 列表/详情，device.read |
| 类型模板 | PUT `/api/v1/device-type-preferences/{device_type}` | upsert（校验软约束不越硬 limits），device.write |
| 设备偏好合并 | GET `/api/v1/devices/{id}/usage-preference-merged` | 每子设备最终生效文本（所见即所得），device.read |

> 注意与 `/ai/config/*` 前缀区分：本组走 **device-iot 路由**（设备物联模块）。

## v52：提示词「坑位 + 候选」端点重做（2026-08-25，T-003）

> 来源：T-003「提示词模板功能重做（坑位+候选模型）」（2026-08-25）。
> 确认方案：取代 prompt_templates（版本/类型/状态）→ 固定 5 坑位（prompt_slots）+ 候选（prompt_candidates，每槽 ≤1 生效）。resolve = 候选生效内容（DB）> 代码默认（零破坏）。
> 缓存机制：任一候选增/删/激活 → invalidate(f"prompt:{slot_key}")（保存即生效）；编辑槽位定义（名称/用途）不失效内容缓存。
> 对应 ER 04 v7.13（prompt_slot + prompt_candidate 实体）。

### 变更内容

| ID | 变更 | 文件 |
|----|------|------|
| 1 | domain-ai-agent 的 rest-ai-config-prompt-* **5 个端点整体重做**：列表→坑位列表（slotKey/name{zh,en}/description{zh,en}/sortOrder/candidates[]/activeCandidateId/useDefault/candidateCount）；新建→`POST /prompts/{slot_key}/candidates`（body {content}，首条自动生效）；激活→`POST /prompts/{slot_key}/candidates/{candidate_id}/activate`；删除→`DELETE /prompts/{slot_key}/candidates/{candidate_id}`（删生效→重选/回落默认）；更新→`PUT /prompts/{slot_key}`（编辑坑位定义，不失效内容缓存）。端点总数不变（29）；与 T-002 rest-ai-config-model-* 零冲突 | `rest/02-core-domains.js` |
| 2 | 移除旧契约：`POST /prompts`、`PUT /prompts/{id}`、`POST /prompts/{id}/activate`、`DELETE /prompts/{id}`、列表 query templateType 筛选；不再造默认回填行 | `rest/02-core-domains.js` |

### 端点组速览

| 组 | 端点 | 说明 |
|----|------|------|
| 提示词坑位 | GET `/api/v1/ai/config/prompts` | 坑位列表（含候选摘要）；不再按 templateType 筛选、不再造默认回填行 |
| 坑位候选 | POST `/prompts/{slot_key}/candidates`、POST `/prompts/{slot_key}/candidates/{id}/activate`、DELETE `/prompts/{slot_key}/candidates/{id}` | 首条自动生效；激活即失效缓存；删生效→重选/回落默认 |
| 坑位定义 | PUT `/prompts/{slot_key}` | 编辑 name/desc 多语言，不失效内容缓存 |

## v51：大模型配置端点组（2026-08-25，T-002）

> 来源：T-002「AI 参数配置·增加大模型配置（Model Usage Config）」（2026-08-25）。
> 确认方案：DB 持久化（model_providers + model_usage_configs）+ 页面 CRUD（保存即生效、无需重启）。
> 缓存机制：进程内 TTL 缓存（惰性加载）+ 写路径主动失效。resolve 优先级：DB > 环境变量 > 代码默认。
> 对应 ER 04 v7.12（model_provider + model_usage_config 实体）。

### 变更内容

| ID | 变更 | 文件 |
|----|------|------|
| 1 | domain-ai-agent 新增 **9 个模型配置端点**（rest-ai-config-model-*）：providers 5（列表/详情/新建/更新/删除）、usages 3（列表/详情/更新）、registry 1（用途注册表）；20 → 29 端点 | `rest/02-core-domains.js` |

### 端点组速览

| 组 | 端点 | 说明 |
|----|------|------|
| 模型供应商 | GET `/api/v1/ai/config/models/providers`、GET `/providers/{provider_key}`、POST `/providers`、PUT `/providers/{provider_key}`、DELETE `/providers/{provider_key}` | api_key 掩码（sk-****尾4）绝不回明文；apiKey 留空=保留原值；非空 AES-GCM 加密落库 |
| 模型用途配置 | GET `/api/v1/ai/config/models/usages`、GET `/usages/{usage_key}`、PUT `/usages/{usage_key}` | 8 用途（default/chat/realtime/knowledge/device_control/report/asr/tts）：provider/model/温度/topP/maxTokens/超时/重试/extraParams/启用；留空=保留原值 |
| 用途注册表 | GET `/api/v1/ai/config/models/usages/registry` | KNOWN_USAGES 注册表（8 用途 + kind + inherits） |

### 设计要点

- **写路径缓存失效**：所有 PUT/POST/DELETE 端点保存后由 ModelRouterService 删除对应进程内缓存 key → 运行时下一轮会话读取即重载（保存即生效）。
- **权限**：沿用 ai_agent.read / ai_agent.write（permissions.ts 已预留 model-route 权限点）。
- **零破坏**：DB 无配置时 resolve 回落环境变量/代码默认，运行时行为与现状完全一致。

---

## v50：AI 预制参数配置化端点组（2026-08-23）

> 用户主动提出（2026-08-23）：AI 测试相关功能的所有预制参数（提示词、关键词等）都应可配置，
> 放到页面便于调试。确认：DB 持久化 + 页面 CRUD（改完即生效、无需重启）。
> 首批范围：提示词类（人设/工具规则/降级文案）+ 关键词类（联网搜索触发词/占位过滤词）
> + 工具描述类（25 Tool description）+ 模型路由类。缓存机制：进程内 TTL 缓存（惰性加载）
> + 写路径主动失效（保存即生效，秒级）。优先级：DB 覆盖 > 环境变量 > 代码默认值。
> 对应 ER 04 v7.10（TemplateType 扩展 + ai_keyword_config + tool_definition.description）。

### 变更内容

| ID | 变更 | 文件 |
|----|------|------|
| 1 | domain-ai-agent 新增 **11 个配置端点**（rest-ai-config-*）：prompts 5（列表/新建/更新/激活/删除）、keywords 2（列表/更新）、tools 2（列表/更新）、model-routes 2（列表/更新）；12 → 23 端点 | `rest/02-core-domains.js` |

### 端点组速览

| 组 | 端点 | 说明 |
|----|------|------|
| 提示词模板 | GET/POST `/api/v1/ai/config/prompts`、PUT/DELETE `/prompts/{id}`、POST `/prompts/{id}/activate` | 版本管理：编辑即新版本，同类型仅一个生效；激活指定版本 |
| 关键词配置 | GET `/api/v1/ai/config/keywords`、PUT `/keywords/{config_key}` | 整表替换 keywords 数组（realtime_keywords/placeholder_markers） |
| 工具描述 | GET `/api/v1/ai/config/tools`、PUT `/tools/{tool_name}` | description 覆盖；空=恢复代码默认 |
| 模型路由 | GET `/api/v1/ai/config/model-routes`、PUT `/model-routes/{complexity_level}` | 主/备模型 + 超时/重试 |

### 设计要点

- **写路径缓存失效**：所有 PUT/POST/DELETE 端点保存后由 AiConfigService 删除对应进程内缓存 key → 下一次会话读取即重载，页面保存即生效。
- **权限**：沿用 ai_agent.read / ai_agent.write（permissions.ts 已预留 prompt / model-route 权限点）。
- **降级文案**：FALLBACK_REPLY / ASR_FAIL_HINT / TTS_FAIL_HINT 归 template_type=fallback 的提示词模板，按 template_name 区分。
- **模型路由**：复用 ER model_route_config（value_object）+ settings 环境变量兜底（DB 覆盖 > 环境变量）。

---

## v49：用户个性化设定层 — 统一更新语义 + 整机级 + 模糊澄清（2026-08-23）

> 用户主动提出（2026-08-23）：用户明确表达的使用要求/取值约束（如「床头最高 45°」）优先级应高于
> 平台覆盖与设备默认，且质疑时系统能说明设定来源并询问是否调整/解除。
> 裁决：软约束（AI 遵守 + 可更新/清除，物理 limits 不变）/ 设备维度（一机一主）/ 先设计后实现。
> 第二轮评审修正：① **整机级设定**也保存（sub_device_id 空，描述性要求）；② **统一更新语义**
> （更新成有/清除/更新成什么样都是更新）；③ 整机级**整合式更新**（AI 结合旧+新输出完整新版）；
> ④ **模糊澄清**（先问后写，多轮上下文闭环）。第三轮选型：更新工具为**单目标 + 多 tool_calls**
> （优于 updates 数组——参数扁平降幻觉率、每条独立执行/结果反馈、执行器零新增，见 raw-input 选型记录）。
> 本次仅设计资产（ER v7.8 实体 + 本 CHANGELOG 的 REST/工具设计），实现（§8.5~§8.8）评审后启动。

### 变更内容

| ID | 变更 | 文件 |
|----|------|------|
| 1 | REST 新增 2 端点：`GET /api/v1/devices/{id}/user-preferences`（设定列表，含整机级 scope=device / 子设备级 scope=sub_device，content 完整目标态）/ `DELETE /api/v1/devices/{id}/user-preferences/{target}`（清除单条，target=子设备ID 或 device，幂等） | `rest/01-device-iot.js` |
| 2 | AI 工具新增 `update_user_preference`（**单目标**：scope=device\|sub_device + sub_device_id + content=完整目标态/null=清除；批量=一个回复输出多个本工具调用——第三轮选型：单目标+多 tool_calls 优于 updates 数组，见 raw-input；内部工具，写 device_user_preferences） | `ai-tools/tools.js` |
| 3 | AI 模板新增 `{user_overrides}` 占位符 + 渲染规则（list：`## 用户设定（最高优先级）`，整机级/子设备级行）+ 样例 + systemPrompt 规则（用户设定 > 默认策略；超出设定说明来源并询问调整/解除；统一经 update_user_preferences 更新；模糊先澄清） | `ai-tools/tools.js` |

### 设计要点

- **四层优先级**：用户实时指令 > 用户设定（整机级 + 子设备级）> 平台覆盖 > 设备上报 > 无策略。
- **统一更新语义**：更新成有 / 清除 / 更新成什么样 = 同一操作；整机级为整合式更新（replace，AI 整合旧+新）。
- **模糊澄清**：prompt 规则驱动（不硬编码场景）；AI 问话落库 → 下一轮结合用户回答做最终决策（标准会话历史机制，settings.llm_max_history_turns=5）。
- **软硬分离**：用户设定也是软约束，物理 limits 校验路径零感知。

### 级联说明
- §8.2 ER：v7.8 新增 device_user_preference 实体（sub_device_id 可空=整机级，content text）
- §8.3b 架构：ai-conversation-pipeline §14 优先级/更新语义/澄清闭环（已同步）
- §8.4 页面：策略抽屉「用户设定」区块（已同步）

---

## v48：睡眠数据拉取报文（Trigger + PushNotify 异步模式）— 8 条新增 action（2026-08-20）

> 用户主动提出（2026-08-20）：设备端需要拉取睡眠监控数据（基于已实测的 anyishui 睡眠接口）。
> 多轮审查定案：**Trigger + PushNotify 异步模式**（设备发 trigger → 平台立即回 ack 确认、设备不等数据 →
> 平台异步拉取 anyishui → 拿到后经独立 push 主动下发）；每个 trigger 独立响应 push；固定全量查询
> （无 sub_device_id）；取消 detail 分级（统一完整数据）；取消限流。

### 变更内容

| ID | 变更 | 文件 |
|----|------|------|
| 1 | 新增 8 endpoints：4 trigger（sleep_status/minutes/report_data/trend_trigger，device→cloud）+ 4 push（sleep_status/minutes/report_data/trend_push，cloud→device），action_name 枚举 41→49 | `iot/smart-bed.js` |
| 2 | 新增 categories「睡眠数据」+ 8 条调用示例（含完整 report/trend payload） | `iot/smart-bed.js` / `iot/smart-bed-examples.js` |
| 3 | event-identifier 新增 8 映射（4 DeviceCommand + 4 PlatformMessage）+ ackOnly 补 8 + 校验通过 | `iot/event-identifier.js` |
| 4 | 协议 changeLog 追加 v2.5（8 action + 6xxx 业务错误段 6001-6004）+ sections 补「睡眠数据」子菜单 | `iot/protocol.js` |
| 5 | 状态码新增 6xxx 睡眠业务段：6001 未绑定 / 6002 无数据 / 6003 上游失败 / 6004 参数非法 | `iot/protocol.js` |

### 设计要点

- **异步时序**：trigger ack 仅确认受理（设备不等数据）；数据经 push 下发，`request_ref` 关联 trigger msg_id。
- **固定全量**：无 sub_device_id，按连接身份 device_id 展开该床全部绑定（both 单条 / left+right 两条），`devices[]` 数组返回。
- **字段完整性**：report push 覆盖 anyishui 顶层 16 字段全量（元数据/基准值 reference/入睡概况 asleep/慢波 slow_wave_data/dateValueList）+ data_list 110 项全量透传；trend push 六维全字段（含 current 汇总 + 状态位）+ overview 18 键全量。
- **单侧失败不阻塞**：双人床一侧拉取失败 → devices[] 该项 error 对象，其余正常。
- **错误分布**：受理时可知（未绑定/参数非法）→ trigger ack；拉取后才知道（无数据/上游失败）→ push devices[].error。
- **现有 sleep_report_trigger（播报）原样保留**，与新增数据拉取系列不混用。

### 级联说明
- §8.2 ER：无新增表（数据不落库，复用 cloud_user_binding 配对）
- §8.3b 架构：iot_gateway 查询类 action 路由 + 异步拉取任务 + push 下行发送路径
- §8.5 TDD：7 个新用例（异步时序/字段完整性/错误码/参数校验）
- §8.6 代码：SleepDataService.fetch_status/minutes/report/trend + iot_gateway trigger 路由 + events 8 事件类
- §8.8：全量回归

---

## v47：设备使用策略（usage_notes）双层数据源 — 协议字段 + AI 模板注入 + 覆盖端点（2026-08-23）

> 用户主动提出（2026-08-23）：设备 connect 上报的 configurations 增加描述性「使用策略」，
> 注入 AI tool-calling 上下文，让 AI 理解结构化 limits 表达不了的取值语义
> （何时用低值/高值、倾向值、应避免的值）。裁决：设备上报 + 平台覆盖双层数据源；
> 本次仅设计资产，实现（§8.5~§8.8）评审后另行启动。

### 变更内容

| ID | 变更 | 文件 |
|----|------|------|
| 1 | `configurations[].usage_notes` 可选字段（协议 v2.4，软建议 ≤200 字，不参与校验） | `iot/smart-bed.js` |
| 2 | 协议版本 bump v2.2 → v2.4 + changeLog + 版本历史 v2.4/v2.3 条目 | `iot/protocol.js` |
| 3 | AI 模板新增 `{usage_notes}` 占位符 + 渲染规则（list：`## 设备使用策略`）+ 样例 + systemPrompt 规则（策略=建议、limits=硬约束） | `ai-tools/tools.js` |
| 4 | connect 调用示例补 usage_notes 演示 | `iot/smart-bed-examples.js` |
| 5 | REST 新增 2 端点：GET/PUT `/api/v1/devices/{id}/usage-policies`（策略总览含来源标注 / 整表替换平台覆盖） | `rest/01-device-iot.js` |

### 设计要点

- **软硬分离**：usage_notes 为软建议（AI 取值倾向），limits 为硬约束（指令合法性校验，tool_executor 预检不变）。
- **匹配优先级**：sub_device_id 精确 > device_type 类型级 > 整机默认。
- **兼容性**：老设备不报 usage_notes 零影响（影子 JSON 无该字段 → 渲染无策略段落）。
- **token 控制**：策略段落仅存在 usage_notes 时注入；每子设备一行，建议 ≤200 字。

### 级联说明
- §8.2 ER：v7.7 新增 device_usage_policy 实体（覆盖层数据模型）
- §8.3b 架构：ai-conversation-pipeline 双层合并/注入决策（下一步骤）
- §8.4 页面：web-admin 设备详情「使用策略」标签（下一步骤）
- §8.5~§8.8：评审后启动（渲染/合并/注入/预检回归测试）

> **2026-08-23 提交前复查补正（2 处文档对齐实现）**：
> ① PUT usage-policies 响应示例/response 由 `{code,message,data:{...}}` 信封改为扁平 `{updated, policies}`（与实现 router.py 返回一致，同文件 GET 也是扁平约定）；
> ② GET 示例 platform_override 行 `overridable` 由 `true` 改为 `false`（对齐 fields 说明「platform_override=false」与实现）。

---

## v46：移除 /iot 兼容别名——设备接入唯一路径为协议 v2.2 主路径（2026-08-22）

> 预发实测「模拟器连接后断开」根因：同设备第二客户端走 /iot 旧路径接入，触发服务端
> 单设备单连接互斥（close 4401 replaced）踢断浏览器连接。定案：彻底移除 /iot 兼容别名，
> 设备接入唯一路径 `/ws/v1/device/{product_model}/{device_id}`（路径承载身份，
> query 仅 timestamp/signature；query 冗余携带身份须与路径一致）。

### 变更内容

| ID | 变更 |
|----|------|
| 1 | `iot/protocol.js` changeLog 追加 v2.3（移除 /iot；v2.2 的 deprecated 表述更新为已移除） |
| 2 | `06-web-admin/data/i18n.js` / `tree.js`：模拟器网关地址占位/描述 `/iot` → 根地址 + 主路径说明 |
| 3 | 服务端联动：main.py 删 /iot 路由；iot_gateway 身份解析收紧（路径唯一来源）；nginx-*.conf 删 /iot location；前端 zreClient 4401 replaced 区分 |

### 级联说明
- §8.6 后端：v49（移除 /iot + connection_replaced 日志）
- §8.7 前端：v10.26（占位符 + 4401 提示）
- §8.5 测试：test_asgi_adapter 删 /iot 用例、test_iot_gateway TC-GW-021/024、10 文件 WS URL 主路径化

---

## v45：环境传感器 env_* 族协议注册（2026-08-22）

> 预发实测问题根因：模拟器环境设备 device_type=env_* 未进入协议 device_type 枚举与
> event-identifier 派生映射 → 上报在识别层被丢弃 → 影子无环境数据 → AI 读不到。
> 依「协议注册即记录」原则把 env_* 六类正式注册（房间级环境传感器同样经设备上报）。

### 变更内容

| ID | 变更 |
|----|------|
| 1 | `iot/smart-bed.js` device_type 枚举新增 6 类：env_temperature / env_humidity / env_noise / env_co2 / env_air_quality / env_illuminance（房间级环境传感器，经设备 status_notification 上报） |
| 2 | `iot/event-identifier.js` deviceTypeDerived 新增对应 6 条派生映射（→ SensorTemperature / SensorHumidity / SensorNoise / SensorCo2 / SensorAirQuality / SensorIlluminance），版本 v1 → v2 |
| 3 | `ai-tools/tools.js` 无需变更——query_environment 契约已覆盖两族命名（env_* 或 *_sensor） |

### 级联说明
- §8.6 后端：identifier_rules.py 同步注册（DEVICE_TYPE_DERIVED + SUBDEVICE_FIELD_MAP）；tool_executor._query_environment 经 configurations 反查 sub_device_id → device_type 识别（不再拿 sub_device_id 当 key）
- §8.5 测试：TC-IDF-016（env_* 派生）+ 影子全链路（上报 → runtime 记录）+ tool_executor 两族场景回归

---

## v44：删除设备型号管理（3 个 device-models 端点）——型号弱化为设备字符串属性

> 用户主动提出（2026-08-20）：设备型号管理无实际业务价值——设备自报型号即权威，无需预定义型号集合。
> 型号概念保留：device.product_model 作为设备自带字符串属性；统计按型号聚合、OTA 按型号下发、设备列表按型号过滤（模糊匹配）全部保留。
> 决策点确认：D1 种子型号能力演示数据保留 / D2 模拟器型号改自由输入 / D3 model 筛选改模糊匹配。

### 变更内容

| ID | 变更 |
|----|------|
| 1 | `rest/01-device-iot.js` 删除 3 个端点块：`rest-device-model-list`（GET /api/v1/device-models）、`rest-device-model-toggle`（PATCH /api/v1/device-models/{id}）、`rest-device-model-batch-update`（POST /api/v1/device-models/batch-update） |
| 2 | 权限映射删除 `DeviceModelManage`；overview 核心实体行删 DeviceModel |
| 3 | `rest-device-create` 契约重构：删 `model_id`（必填校验 + INVALID_MODEL 错误），改为 `product_model` 自由字符串（必填但无枚举校验）；preconditions/steps/related_apis 同步更新 |
| 4 | `rest-device-import-preview` 步骤删「检查 model_id 是否存在」（型号随导入原样存储） |
| 5 | `rest-device-options` 响应删 `models` 字段（仅 groups + statuses） |
| 6 | 设备列表 `model` 筛选参数：精确匹配 → 模糊匹配（ilike contains，D3 决策） |

### 级联说明
- §8.2 ER：删 device_model 实体（v7.6）
- §8.4 页面设计：删「设备型号/协议管理」功能区 + 权限 DeviceModelView；控制台型号筛选下拉 → 手动输入
- §8.6 后端：删 DeviceModelService/ORM/仓储/端点 + 种子 MODEL_SEEDS；import 不再 ensure_model；/devices/options 删 models
- §8.7 前端：删 DeviceModel.vue/路由/权限/API client/i18n；DeviceStatus 型号下拉改输入框；模拟器型号改自由输入
- §8.5 测试：删 TC-API-007 等旧用例 + 新增回归（创建/导入不校验型号、options 无 models、模糊匹配）
- 原输入存档：`design/01-raw-input/01-device-iot.md`（2026-08-20 条目）

---

## v43：故障管理页分组/关键词筛选——device-errors 契约补 group_id + keyword

> 2026-08-22 用户实测报错：故障管理页（DeviceError）选择「设备分组」后点搜索，
> 弹「加载设备错误失败」。根因：前端把 groupId 误映射到 device_id 查询参数，
> 后端将其当单台设备 ID 校验 → KeyError → 404。同时 keyword 从未传参。
> 契约层本就未定义 group_id/keyword——分组筛选与关键词搜索是页面既有功能，属设计遗漏。

### 变更内容

| ID | 变更 |
|----|------|
| 1 | `rest/01-device-iot.js` `rest-maintenance-device-errors` 契约：query_params 新增 `group_id`（与 device_id 互斥）、`keyword`（设备 ID/型号关键词）；business_logic 步骤补分组/关键词过滤；errors 补 device_id 与 group_id 同传 400 |

### 级联说明
- §8.6 后端：`device_errors` 端点新增 `group_id`/`keyword` 参数，无 device_id 时经 `dsvc.list_devices` 过滤设备集合；摘要条目补 `displayName`/`groupName`
- §8.7 前端：`maintenance.ts` 参数映射 `groupId → group_id` 并补传 `keyword`
- §8.5 测试：API 回归（分组/关键词过滤）+ Web UI 集成（故障管理页选分组搜索）
- 原输入存档：`design/01-raw-input/01-device-iot.md`（2026-08-22 条目）

---

## v42：设备消息页两栏重构——新增 network-messages/dates 日期分组端点

> 2026-08-22 用户主动提出（设备消息页仿设备日志两栏布局）：
> 左栏设备展开后按日期平铺消息历史（日期 + 当日条数），点击日期右栏加载消息清单。
> 现有端点（消息列表 network-messages、导出 export）已够用，仅补 1 个日期分组端点。

### 变更内容

| ID | 变更 |
|----|------|
| 1 | `rest/01-device-iot.js` 新增 `rest-maintenance-network-message-dates`：GET `/api/v1/maintenance/network-messages/dates`（query `device_id`（可空=全部租户设备）+ `limit`（最近 N 天，默认 30），按 event_store 分组 `date(timestamp)` 聚合当日条数，日期倒序返回 `[{date, count}]`，权限 device.read） |
| 2 | `rest/02-core-domains.js` domain-device-iot 端点列表补 rest-maintenance-network-message-dates，totalEndpoints 63 → 64 |

### 级联说明
- §8.2 ER：EventStore 实体 timestamp/event_type/category/device_id 字段已具备，无需变更
- §8.1 工作流：设备消息查看为简单运维查询，无状态机/权限点新增
- 原输入存档：`design/01-raw-input/01-device-iot.md`（2026-08-22 条目，若用户确认后补记）

---

## v41：设备日志页简化重构——新增 diagnostics/clear + diagnostics/download

> 2026-08-21 用户主动提出（页面过复杂，要求极简重做）：
> 左栏 = 分组 + 床编号搜索 + 设备列表（每行设备：展开/清理日志/下载日志/触发日志上传）；
> 展开后平铺该设备全部日志文件（无分类概念），点击文件右侧预览内容。
> 现有端点（设备列表 /devices、文件列表 files、文件详情、上传）已够用，仅补 2 个运维动作端点。

### 变更内容

| ID | 变更 |
|----|------|
| 1 | `rest/01-device-iot.js` 新增 `rest-maintenance-diag-clear`：POST `/api/v1/maintenance/diagnostics/clear`（body `device_ids`，删除设备全部 diagnostic_file，幂等，权限 device.write） |
| 2 | `rest/01-device-iot.js` 新增 `rest-maintenance-diag-download`：GET `/api/v1/maintenance/diagnostics/download?device_id=`（合并该设备全部日志文件 content 返回 filename+content，沿用 network-messages/export 模式，权限 device.read） |
| 3 | `rest/02-core-domains.js` domain-device-iot 端点列表补 rest-diagnostic-clear / rest-diagnostic-download，totalEndpoints 61 → 63 |

### 级联说明
- §8.2 ER：diagnostic_files 实体已具备全部所需字段，无需变更
- §8.1 工作流：清理/下载为简单运维动作，无状态机/权限点新增
- 原输入存档：`design/01-raw-input/01-device-iot.md`（2026-08-21 条目）

---

## v40：睡眠监控去用户化（床 = 绑定锚点）

> 用户主动提出（2026-08-20）：睡眠监控云平台（外部平台）的"用户-设备绑定"实为「床」与「睡眠监控设备」的绑定；
> 本系统不体现用户概念，外部平台账号由床派生，永远跟随床。
> 决策：D1 保留 remoteUserId 响应字段（内部外部账号标识，前端不展示，零行为变更）；D2 报告页语义改「按床查看」。

### 变更内容

| ID | 变更 |
|----|------|
| 1 | `rest/02-core-domains.js` domain-sleep-monitor：rest-binding-list 描述改为「睡眠监控绑定关系列表（床 → 睡眠位 zone → 睡眠监控设备 imei）」，remoteUserId 标注为外部平台账号（由床派生，内部标识） |
| 2 | rest-binding-device 描述：「单台设备绑定详情」→「单张床的绑定详情（床 → 睡眠位 → 监控设备 imei）」 |
| 3 | rest-report-daily 描述：「按绑定 imei+userId 拉报告组装」→「按绑定 imei 拉外部平台报告组装（外部账号由床派生）」 |
| 4 | implementation.note 补批次 5（2026-08-20 去用户化）说明 |

---

## v39：睡眠查询 Tool 补齐 + 实时体征接口纠偏（anyishui 联调批次 4 修正）

> 2026-08-19 用户真实设备联调发现：① `abl.device.realtime.query` 是**门店设备**接口，
> 非门店场景恒返回离线/空占位数据（曾误判设备离线）——正确实时体征接口为 `abl.quick.heart.query`；
> ② 4 个睡眠查询 Tool（query_sleep_*）已在代码 `tools.py` 实现但设计数据文件缺失；
> ③ 报告/趋势端点语义确认为「实时拉取 anyishui，不落库」（用户决策）。
> 详见 `design/05-backend-architecture/anyishui-sleep-integration.md` §2.4。

### 变更内容

| ID | 变更 |
|----|------|
| 1 | `rest/02-core-domains.js` domain-sleep-monitor：① 实施状态补批次 4 说明；② rest-report-daily/rest-report-trend 语义修正为「实时拉取 anyishui（查绑定→拉数据→组装），不落库」；③ rest-monitor-device-* 7 端点裁决：**由 device-iot devices API 承载**（前端设备页已改调 deviceIot API），契约保留 planned 不排期实现 |
| 2 | `ai-tools/tools.js` 新增 **4 个睡眠查询 Tool**（分组：😴 睡眠查询）：`query_sleep_report`（abl.sleep.report.query）/ `query_sleep_minutes`（abl.sleep.minute.query）/ `query_sleep_trend`（abl.sleep.analysis/formerly.query）/ `query_realtime_vitals`（**abl.quick.heart.query**，deviceStatus 0离床/1在床/2在线/3离线）——与代码 `src/server/modules/ai_agent/domain/tools.py` 对齐 |
| 3 | `ai-tools/tools.js` categories 注册「睡眠查询」组（4 Tool） |
| 4 | `ai-tools/protocol.js` Tool 分组表 + categories + stats 同步：Tool 25 → 29，分组 10 → 11 |

### 设计决策

- **实时体征必须走 `abl.quick.heart.query`**：`abl.device.realtime.query` 是门店场景接口（养老院/机构），非门店账号调用不报错（code=0）但恒返回 `deviceStatus=3` 离线占位——**禁止用它判断在线状态或取实时体征**（本次误判根因）
- **报告/趋势 = 实时拉取不落库**（用户 2026-08-19 决策）：查 cloud_user_bindings（device↔imei↔userId）→ 调 anyishui → 组装返回；本地 sleep_sessions/sleep_scores 表仅演示/兼容
- **监控设备 CRUD 不重复实现**：设备接入层已由 device-iot devices API 承载，sleep-monitor 侧保留契约（planned）避免双重实现

---

## v38：环境数据查询 Tool（query_environment，P-003 遗留待办闭环）

> 2026-08-20 用户要求：AI 应能回答「当前环境温度/湿度是多少」——这属于设备传感器数据
> （设备上报 → 影子快照），应在 Tool Calling 中暴露。核查发现 `design/review/_archived/
> ai-agent-session-recovery-review-v1.md` P-003（2026-08-20 早）已登记该缺失（📋 planned），
> 本次闭环实现。

### 变更内容

| ID | 变更 |
|----|------|
| 1 | `ai-tools/tools.js` 新增 **`query_environment`** 环境查询 Tool（分组：🌡️ 环境查询）：从影子快照读环境传感器数据（温度/湿度/噪音/空气质量/CO₂/照度），参数 `metrics` 可选数组过滤，不触达物理设备 |
| 2 | `ai-tools/tools.js` systemPrompt 新增「用户询问环境数据时调用 query_environment」指引 |
| 3 | `ai-tools/protocol.js` Tool 分组表 + categories + stats 同步：Tool 24 → 25，分组 9 → 10 |

### 设计决策

- **数据源 = 影子快照**（DD-014 get_shadow 直连）：环境传感器值随设备 status_notification 上报进 `shadow.state.runtime[env_*]`，AI 读影子即得最新上报值——与 query_device_state 同一读通路，不新增影子能力
- **与联网搜索严格化协同**：环境温湿度是设备数据（非外部实时信息），`_REALTIME_KEYWORDS` 严格化后「温度/湿度」不再触发联网——AI 通过 query_environment 从影子回答，双链路一致
- **metrics 过滤**：可选数组让 AI 只取需要的指标（省 token），不填返回全部

---

## v37：AI Tool Calling 重构——统一面向影子设备门面（ai-tools 语义对齐 DD-015）

> 用户架构反思（2026-08-20）：设备状态数据来自影子设备（设备上报 → 影子更新 → 查询影子），
> 而 AI Tool 层原表述为"建立在 ZYRIC 设备协议之上"，形成"设备通信 + 影子状态"两条通路语义割裂。
> 用户确认原则：外部对影子设备的操作基本通过发送事件完成（DD-014/DD-015），
> Tool Calling 作为 AI 侧设备操作适配器必须遵循同一原则——操作（写）走事件、状态（读）走影子快照，
> 不发明 AI 专属影子直调通道。

### 变更内容

| ID | 变更 |
|----|------|
| 1 | `ai-tools/protocol.js` 基础策略重构：AI Tool 层定位从「建立在 ZYRIC 设备协议之上」改为「建立在**影子设备门面**之上」——指令类 Tool = 构造 PlatformCommand 事件发送给影子设备（事件分发器路由 → ShadowDevice 校验 → 协议适配器下发，与 web-admin/device-control 同一链路）；状态读取走影子快照（DD-014 get_shadow 直连）；ZYRIC 设备通信是影子内部实现，对 AI 不可见 |
| 2 | `ai-tools/tools.js` 新增 **`query_device_state`** 状态查询 Tool（分组：📊 状态查询）：从影子快照读取 configurations + runtime（可选 sub_device_id 过滤），AI 可主动查询设备状态（不触达物理设备） |
| 3 | `ai-tools/tools.js` 头部注释 + systemPrompt 更新：明确影子门面架构定位，新增「用户询问设备状态时调用 query_device_state」指引 |
| 4 | Tool 总数 23 → 24，分组 8 → 9 |

### 设计决策

- **写走事件 / 读走快照**：与 DD-014「删除 update_shadow 直连，写状态收敛为事件驱动」、DD-015「影子设备=唯一交互边界」完全对齐——AI 不是例外，是同一协议语义的消费方
- **不新增影子直调 API**：AI 读状态复用既有 `get_device_context`（gateway 公开影子 state 访问），不另建影子查询端点
- **状态查询 Tool 的边界**：只读影子快照（可能滞后于物理设备），不发布 status_notification_trigger 强制设备上报——保持读操作无副作用原则

---

## v36：流数据帧审计事件 + 全帧 /debug 广播契约

> 用户裁决（2026-08-19）：二进制数据帧（上行/下行）与文本重复消息是可解析为事件的内容 → Event Store；
> 流级汇总/坏帧等非事件内容 → non_event_logs；所有真实帧（文本+二进制）实时广播到信息流监控页，
> 二进制帧广播带帧头+帧数据信息+内容摘要。内容展示按摘要方案（hex 前 32B + TS 包数 + MD5 短值）。
> 日期：2026-08-19

### 变更内容

| ID | 变更 |
|----|------|
| 1 | EventType +2（98 种）：`DeviceMessage.StreamDataFrame`（上行流数据帧）/ `PlatformMessage.DownlinkStreamDataFrame`（下行流数据帧）——payload 契约见 internal/07-event-infrastructure.js 附录 StreamDataFrameEvent（stream_id/frame_index/data_len/total_frames/eos/ts_aligned/duplicate/gap/payload_preview/ts_packets/payload_hash，不存原始载荷） |
| 2 | 帧级审计事件直写 `event_store`（EventStore.append），**不投递 EventBus**（审计记录，不触发业务 handler）；文本重复消息（msg_id 去重窗口内）同样以对应事件类型直写（duplicate 标记，不重新执行业务） |
| 3 | `/debug` 广播契约：所有真实帧——文本帧（call/ack/去重/已处理/解析失败，解析失败带原文预览）、二进制帧（帧头全字段 + dup/gap + 内容摘要，坏帧带长度+字节预览）——实时推送信息流监控页 |
| 4 | 流级汇总（EOS/流结束时：总帧/总字节/首末帧号/dup/gap 计数）与坏帧写 `non_event_logs`（log_type=stream_summary / stream_frame） |

### 设计决策

- **审计直写 vs 总线投递**：帧与重复包是运输层事实记录，投递 EventBus 会触发网关自身 PlatformMessage* 前缀回投成指令——直写 EventStore 语义正确且零投递噪音
- **内容摘要**：payload_preview（前 32B hex，TS 帧应以 47 同步字节开头）+ ts_packets（data_len/188）+ payload_hash（MD5 短值），满足"看得到帧内容"且带宽/存储可控（~20 帧/秒/流）
- **方向推导**：运维「网络消息」页 direction 由 category 推导（DeviceMessage→上行 / PlatformMessage→下行），帧事件自动正确展示

---

## v35：下行容器默认 OPUS 帧流（DD-025，格式层澄清）

> 用户裁决（2026-08-19）：设备协议旧、不上报 supported_formats 也应正常收发——
> 下行默认 OPUS；协商优先级 opus > mpeg_ts > 默认 opus。
> 另澄清：嗅探文本文件的 47B 封包为 YT7242 UART 层格式，云端收发标准裸 OPUS 40B 帧，
> 无上行/下行区分，不存在 TOC 非标问题。
> 日期：2026-08-19

### 变更内容

| ID | 变更 |
|----|------|
| 1 | `iot/smart-bed.js` `connect` `supported_formats`：语义由「仅上行设备声明、下行播放设备不声明」改为「上行拾音与下行播放设备均可声明；未声明不阻塞收发」 |
| 2 | `iot/smart-bed.js` `connect` notes：音频格式协商更新——下行容器协商优先级 ① 声明 {opus,PCM} → OPUS 帧流；② 声明 {mpeg_ts,PCM}（且无 opus）→ TS 容器；③ 未声明 → **默认 OPUS 帧流**（原默认 TS+PCM 24kHz 废除）；上行容器由平台自动探测分派，不依赖声明 |

### 设计决策

- **DD-025 核心**：老设备（YT7242 主控，固件旧）零改动即可收 OPUS 下行；数据量从 TS+PCM 的 ~48KB/s 降至 OPUS 16kbps 的 ~2KB/s（24 倍）
- **格式层澄清**：47B 封包（55AA+len+cmd+40B OPUS+check）是 YT7242 模块 UART 接口层私有格式；云端传输为标准裸 OPUS 40B 帧（12B 帧头之外无额外封包头），上传下载格式一致
- **协商保留 TS 分支**：显式声明 {mpeg_ts,PCM} 的设备仍走 TS 容器（R-7 管道能力不废）

---

## v34：设备消息页改查 event_store + network_messages 语义重定义（non-event-logs）

> 用户裁决（2026-08-19）：设备消息页改查 event_store（真实交互日志）；network_messages 表
> 语义重定义为「非事件系统日志」更名 non_event_logs（协议解析失败原文 + 系统运行日志）。
> 日期：2026-08-19

### 变更内容

| ID | 变更 |
|----|------|
| 1 | `rest/01-device-iot.js` `rest-maintenance-network-messages`：语义改为**查 event_store**（device_id + 日期/方向过滤，方向由 category 推导；limit/offset **无限滚动无 total**；响应 items[].{id,deviceId,timestamp,direction,type,category,summary,payload}） |
| 2 | `rest/01-device-iot.js` `rest-maintenance-network-messages-export`：改查 event_store，CSV 含 category；新增 limit 导出上限 |
| 3 | `rest/01-device-iot.js` **新增** `rest-maintenance-non-event-logs`：查 non_event_logs（device_id 可空=系统级日志，log_type 过滤，limit/offset 无限滚动） |

### 设计决策

- **方向推导**：uplink = category ∈ {DeviceMessage, DeviceCommand}；downlink = category ∈ {PlatformCommand, PlatformMessage}
- **无限滚动**：消息/日志列表不再返回 total（event_store 数据量大，页游式加载），前端滚动到底 +offset
- **摘要生成**：summary = event_type 中文名 + payload 关键字段（如 `温度传感上报 value=26.5`）

---

## v33：supported_formats 白名单收敛（容器/编码/单位仅接收枚举）

> 用户在 v32 基础上收敛格式协商白名单：container、codec、unit 三个字段均为枚举，
> 平台只接收枚举值（拒绝自由文本）；音频 container 仅 mpeg_ts/opus，codec 仅 PCM。
> 日期：2026-08-19

### 变更内容

| ID | 变更 |
|----|------|
| 1 | `smart-bed.js` `container_format` 枚举收敛：`mpeg_ts`（TS 容器）+ `opus`（Opus 流）两种，移除 wav/raw——desc 标注"白名单收敛，音频容器仅此二者" |
| 2 | `smart-bed.js` supported_formats 字段：container 仅接收 mpeg_ts/opus、codec 仅接收 PCM（组合限 {mpeg_ts,PCM} / {opus,PCM}）；limits.unit 仅接收 measure_unit 枚举值（媒体设备仅 bps/kbps） |
| 3 | `smart-bed.js` connect notes 补充：非枚举值一律按无效载荷拒绝 |
| 4 | `examples.js`（rest）+ `smart-bed-examples.js`（iot）voice_monitor 示例：supported_formats 第二组合 wav+PCM → opus+PCM |

### 定案要点

- **枚举即白名单**：container_format 枚举本身即平台接收白名单，不再有"枚举外但可接受"的文本值
- **音频格式组合全集**：`{mpeg_ts, PCM}`（TS 容器内 PCM，DD-018 现状链路）+ `{opus, PCM}`（Opus 流内 PCM，编解码实现待下一波）
- **unit 枚举约束**：limits.unit 引用 measure_unit 枚举，媒体设备仅用 bps/kbps

---

## v32：connect 音频格式协商 — voice_monitor 设备类型 + supported_formats 三层声明

> 设备侧音频/媒体上下行已在设备类型层面分离（voice_monitor 纯上行 / audio_output、tts_generator 纯下行），
> connect configurations 中仅上行拾音设备声明支持格式；下行格式由平台决定，不参与协商。
> 日期：2026-08-19

### 变更内容

| ID | 变更 |
|----|------|
| 1 | `smart-bed.js` connect envelope configurations 新增 `supported_formats`（可选，仅上行设备声明）：数组条目 = 容器格式 + 编码格式 + 编码参数三层限定（容器 ⊇ 编码 ⊇ 参数）。编码参数固化：音频 = sample_rate + bit_depth；视频 = frame_rate + resolution（预留）。未声明视为仅支持默认格式（MPEG-TS + PCM，16kHz 上行 / 24kHz 下行） |
| 2 | `smart-bed.js` 新增 `container_format` 公共枚举（mpeg_ts / wav / raw）——supported_formats 第一层限定 |
| 3 | `smart-bed.js` device_type 枚举新增 `voice_monitor`（语音拾取设备 — 专门用于语音指令上行，不接收平台操作） |
| 4 | `smart-bed.js` limits 语义统一：媒体输出/拾音设备（audio_output/tts_generator/voice_output/voice_monitor）的 limits 统一定义为比特率范围（unit: bps），不参与格式描述——格式由 supported_formats 表达；`measure_unit` 新增 bps/kbps |
| 5 | `examples.js`（rest）+ `smart-bed-examples.js`（iot）connect 示例：audio_output limits 从 hz 采样率改为 bps 比特率；新增 voice_monitor 子设备示例（supported_formats: mpeg_ts+PCM 16kHz/16bit + wav+PCM） |
| 6 | `event-identifier.js` deviceTypeDerived 新增 voice_monitor → `DeviceMessage.VoiceMonitorStatus`；`event-identifier-check.mjs` device_type 块截取边界改为「position_control → zone 枚举前」（34 种全覆盖） |

### 定案要点

- **上下行分离**：设备类型层面已定义方向（voice_monitor=上行、audio_output/tts_generator/voice_output=下行/双向），connect 不再重复声明下行格式——下行由平台按 play_stream_voice 实际下发内容决定
- **三层限定**：supported_formats 每个条目 = container + codec + 参数（音频：采样率+采样精度；视频：帧率+分辨率），层层收窄
- **limits 单一职责**：媒体设备 limits 只表达比特率高低（带宽约束），格式参数全部收敛到 supported_formats
- **事件配套**：新增 `DeviceMessage.VoiceMonitorStatus`（拾音状态 idle/recording/fault），同步 ER 07（v7.3，96 事件类）与 src/server/events/base.py

---

## v31：rest-device-control 端点语义备注（PlatformMessage 放行 + command_id 可空）

> 信息流页手动 heartbeat 下发联调修复的实现层备注（设计数据文件不变）。
> 日期：2026-08-18

### 变更内容

| ID | 变更 |
|----|------|
| 1 | `rest/01-device-iot.js` rest-device-control 备注：实现端点 `POST /api/v1/devices/{id}/control` 的 action 白名单为协议**下行表（DOWNLINK）**——30 PlatformCommand + 2 PlatformMessage（heartbeat / stream_voice_command_result）。上行/双向 action（connect、status_notification、stream_stop 等）不属于下发语义，一律 400 |
| 2 | 响应 `command_id` 对 PlatformMessage（消息非指令）为 null；PlatformCommand 仍为 UUID。设计契约 `response.body.command_id: UUID` 追加「PlatformMessage 时为 null」备注 |

### 定案要点

- 设计语义不变：该端点仍是「下发控制指令」端点；heartbeat 手动下发走同一端点仅因协议 DOWNLINK 表包含 2 条 PlatformMessage 下行
- 实现对齐：`src/server/modules/device_iot/api/router.py`（v21，见 src/server/CHANGELOG.md）

---

## v30：流式传输帧格式歧义消除（12 字节帧头 + MPEG-TS 定案）

> 数据面帧格式三版本残留清理：统一为「12 字节极简帧头 + MPEG-TS 容器」，
> 并修正下行语音流 action 名（stream_voice_start → play_stream_voice）。
> 日期：2026-08-17

### 变更内容

| ID | 变更 |
|----|------|
| 1 | `protocol.js` streaming 章节重写：帧头 16B 含 codec → **12B（total_frames/frame_index/data_len，无 codec）**；EOS 统一 data_len=0；新增 `principle`（做管道不做编解码）、`tsContainer`（188B 对齐/不跨帧/PAT-PMT 100~500ms 周期重发/纯音频 PAT→PMT→audio 示例）、`sourceOfTruth`（声明取代 raw-input 规范.md §6.4.2 旧帧格式；「元数据帧」即 TS 的 PAT/PMT） |
| 2 | `protocol.js` 动作名同步：5 处 stream_voice_start → play_stream_voice（对齐 smart-bed.js / CHANGELOG B5 定名） |
| 3 | `smart-bed.js`：stream_voice_command / play_stream_voice 的「元数据帧自描述」→「MPEG-TS 容器 PAT/PMT 自描述」+ 12B 帧头/EOS 表述 |
| 4 | `02-device-control.js` / `ai-conversation-pipeline.js` / `ai-conversation-implementation-plan.md`：stream_voice_start → play_stream_voice；旧帧格式「4 字节长度头+负载+2 字节序号」→「12 字节极简帧头 + MPEG-TS」 |

### 定案要点（唯一真相源 = protocol.js streaming）

- 帧头 12B（total_frames + frame_index + data_len）纯运输字段，无 codec、无 stream_id
- 载荷 = MPEG-TS（188B 包对齐切割，不跨传输帧；PAT/PMT 流首几帧 + 每 100~500ms 重发）
- 编码/采样率/PTS 全部由 TS 的 PAT/PMT/PES 自描述；EOS = 最后一帧 data_len=0

---

## v29：各域实现状态标注 + device-control 双轨收敛决策（批次 3）

> 批次 3（设计层收敛）：为 02-core-domains.js 全部 5 域补 implementation 状态标注
> （implemented / partial / deferred），并记录设备控制双轨收敛决策。
> 日期：2026-08-16

### 各域实现状态（02-core-domains.js implementation 字段）

| 域 | 状态 | 说明 |
|----|------|------|
| domain-dashboard | partial | stats/device-status/usage-trend/alerts 已实现；sleep-stats deferred（DD-008） |
| domain-statistics | implemented | 4 端点全部实现（按租户/分组/区域/型号聚合） |
| domain-ai-agent | implemented | 批次 1：12 端点全部实现（src/server/modules/ai_agent）+ 前端 4 页 |
| domain-device-control | partial | 运维 7 端点已实现；控制细分端点 deferred（DD-007） |
| domain-sleep-monitor | partial | 批次 2：绑定/报告/趋势 5 端点已实现；监控设备 CRUD 7 端点 planned（前端设备页由 device-iot devices API 承载） |

### device-control 双轨收敛决策（2026-08-16 固化）

**背景**：设备控制存在两套承载路径——① REST 细分端点（rest-angle-adjust 等，设计态 39 端点）
与通用 `POST /api/v1/devices/{id}/control`；② WS 网关 `/ws/v1/device/{model}/{id}`（协议 v2.2，
ZYRIC action 下发）。此前设计保留细分端点契约但实现仅走通用 control 端点 + WS action，双轨未明确收敛。

**决策（与 2026-08-15 单端口 443 部署定案一致）**：

1. **REST 层收敛**：控制指令唯一实现路径 = 通用 `POST /api/v1/devices/{id}/control`
   （action 参数化，01-device-iot.js rest-device-control）。细分端点（angle-adjust/height-adjust/
   massage-control/light 等）保留契约（api-viewer 可查）但标记 deferred，不再独立实现。
2. **WS 层收敛**：设备实时指令走 `/ws/v1/device/{product_model}/{device_id}`（协议 v2.2 主路径，
   `/iot` 兼容别名 deprecated）；/debug WS 承载联调消息流。9100 独立 WS 端口已废弃
   （2026-08-15 清理，代码无残留）。
3. **运维域**：7 端点已实现（src/server/modules/maintenance），不受双轨收敛影响。
4. **单端口**：REST `/api/v1/*`、设备 WS、/debug 统一后端 8000 单端口按路径区分；
   线上 zy-iot-api.digifurn.com 443 反代。

---

## v28：睡眠监控域补绑定/报告/趋势端点（批次 2）

> 批次 2（睡眠监控三页实现）：domain-sleep-monitor 从 7 端点扩到 12 端点，对齐 ER 03（cloud_user_binding/sleep_session/sleep_score/daily_report_card）+ tree.js §7 功能区。
> 日期：2026-08-16

| # | 端点 | 说明 |
|---|------|------|
| 1 | GET /api/v1/sleep-monitor/bindings | 设备和用户绑定列表（两层绑定） |
| 2 | GET /api/v1/sleep-monitor/bindings/{device_id} | 单设备绑定详情 |
| 3 | GET /api/v1/sleep-data/reports/daily | 每日报告卡片（单用户） |
| 4 | GET /api/v1/sleep-data/reports/trend | 趋势曲线（7/30/90 天） |
| 5 | POST /api/v1/sleep-data/reports/export | 导出报告 PDF |

权限：binding 读=BindingView（workflow 03）、报告/趋势/导出=SleepDataView/SleepReportExport；身份域权限码 sleep_data.read/sleep_data.export。

---

## v27：ZYRIC WS 握手认证载体双轨化（v2.1，修复设计/实现不一致）

> 背景（2026-08-14 用户确认）：设计文档（§2.3.1 / protocol.js v2.0）定义 WS 握手认证
> 走 `Authorization: Basic` header，但浏览器 `WebSocket` API 无法自定义请求头，
> 实现（批次 2b）实际落地为 URL query 载体且未留决策记录，导致设计与实现脱节。
> 本次修复：服务端双轨兼容 + 设计文档同步，两者统一为"query 优先、header 回退"。
> 日期：2026-08-14

| # | 变更 | 文件 |
|---|------|------|
| E1 | `_verify_handshake` 双载体解析：query（product_model/timestamp/signature）优先，缺省回退 `Authorization: Basic base64({product_model}-{device_id}:{password})`；新增 `_parse_basic_auth`（username 必须与 URL device_id 匹配，防跨设备冒用） | src/server/gateway/iot_gateway.py |
| E2 | asgi_adapter `request` 暴露 `headers`（透传 starlette Headers，大小写不敏感） | src/server/gateway/asgi_adapter.py |
| E3 | 测试：TC-ASGI-001b headers 透传 + TC-ASGI-007/007b/007c/007d（header 握手成功/坏签名拒绝/用户名不匹配拒绝/query 优先） | tests/api/device_access/test_asgi_adapter.py |
| E4 | protocol.js v2.1：WS auth 双载体描述 + carrierNote 决策说明 + 版本历史/changeLog 更新 | data/iot/protocol.js |
| E5 | raw input 协议规范 §2.3.1 加勘误标注（保留原文，指向 v2.1 双载体） | design/01-raw-input/设备通信协议规范.md |
| E6 | zreAuth.ts `buildWsUrl` 注释补充"为何 query 非 header"决策说明 | src/web-admin/src/lib/zre/zreAuth.ts |

---

## v26：IoT 补齐——统计与事件查询（批次 4 IoT 部分，用户裁决 DD-008）

> 用户裁决（2026-08-11）：AI Agent / Sleep Monitor 暂停，优先补齐纯物联网能力。
> dashboard 4 端点 + statistics 4 端点 + GET /api/v1/events 事件历史（P-011）实现于
> `src/server/modules/dashboard`（只读聚合，权限 device.read，无新表）；
> sleep-stats 标注 deferred（睡眠数据域暂停，返回 501）。
> 日期：2026-08-11

| # | 变更 | 文件 |
|---|------|------|
| D1 | 新增 GET /api/v1/events 事件历史查询（复用 EventStore.query，按设备/类型/分类/时间过滤 + 分页） | src/server/modules/dashboard/api/router.py |
| D2 | 新增 dashboard 4 端点：stats / device-status / usage-trend / alerts | 同上 |
| D3 | dashboard/sleep-stats → 501（deferred，睡眠域暂停） | 同上 |
| D4 | 新增 statistics 4 端点：operator / deployment / zones / devices（租户/分组/区域/型号聚合） | 同上 |
| D5 | 前端：api/dashboard.ts 新建；DashboardIndex 统计卡片接真实数据；/statistics 页面新建并挂载路由（tree.js 对齐） | src/web-admin/src/api/dashboard.ts、views/statistics/、router/modules/statistics.ts |
| D6 | 对齐脚本修复：http.get<T>() 泛型调用纳入比对（前端调用 27→92，全匹配） | scripts/validate-frontend-api-alignment.mjs |
| D7 | 设计标注：dashboard partial（sleep-stats deferred）+ statistics implemented；mock 路径同步 /api/v1 | 02-core-domains.js、mock/index.ts |

---

## v25：System 域实现（批次 1，用户裁决 DD-004）

> 用户裁决（2026-08-11）：补后端实现 system 域，消除已挂载页面 404。
> 22 端点全部实现于 `src/server/modules/system`（5 张表：system_settings / localization_languages /
> localization_entries / help_pages / help_media）；前端 3 个 API 模块统一 `/api/v1` 前缀（P-104）；
> mock 拦截路径同步。权限：读=admin.read，写=admin.write。
> 日期：2026-08-11

| # | 变更 | 文件 |
|---|------|------|
| C1 | domain-system 新增 `implementation` 字段（implemented + DD-004 说明） | system.js |
| C2 | 新增 GET/PUT /api/v1/system/general[/basic|storage|auth] 4 端点实现 | src/server/modules/system/api/router.py |
| C3 | 新增 /api/v1/localization/* 9 端点实现（languages CRUD + entries + export/import/preview） | 同上 |
| C4 | 新增 /api/v1/help/* 9 端点实现（pages CRUD/publish + locales + media 上传/删除） | 同上 |
| C5 | 种子：默认语言 zh-CN（is_default）+ general 默认配置 | src/server/modules/system/seeds.py |
| C6 | 前端 api/systemGeneral.ts / localization.ts / help.ts / stores/help.ts 路径统一 /api/v1 | src/web-admin/src/api/* |
| C7 | mock/index.ts 拦截路径同步 /api/v1 | src/web-admin/src/mock/index.ts |

---

## v24：device-control REST 双轨收敛（批次 3，用户裁决）

> 用户裁决（2026-08-11）：61 个控制指令类 REST 细分端点不再独立实现——唯一实现路径 = 通用
> `POST /api/v1/devices/{id}/control`（action 参数化）+ ZYRIC action（iot/smart-bed.js）；
> 细分端点保留契约但标注 deferred。设计决策 DD-007。
> 日期：2026-08-11

| # | 变更 | 文件 |
|---|------|------|
| B1 | domain-device-control 分组新增 `implementation` 字段（partial + DD-007 说明） | 02-core-domains.js |
| B2 | 非动作端点 rest-device-status-tree / rest-message-flow-list 标注 deferred（前端已改用 devices API + /debug WS） | 02-core-domains.js |
| B3 | 控制指令端点（rest-angle-adjust ~ rest-set-user-profile，共 36 个）标注 deferred（唯一实现路径 = 通用 control 端点 + ZYRIC action） | 02-core-domains.js |
| B4 | 运维 7 端点（rest-diagnostic-tree ~ rest-batch-config）标注 implemented（src/server/modules/maintenance） | 02-core-domains.js |

---

## v23：Maintenance 域端点（设备运维数据源修复）

> 用户裁决（2026-08-06）：Maintenance 域从 mock 接入真实后端。新增 13 个 /api/v1/maintenance/* 端点；
> 剔除功率平衡（非智能床业务）；设备列表/选项复用 device-iot 端点（前端适配）
> 日期：2026-08-06

| # | 变更 | 文件 |
|---|------|------|
| A9 | 新增 13 端点：诊断树/文件列表/文件详情/上传（4）、网络消息列表/CSV 导出（2）、设备错误摘要/详情/解决/全部解决（4）、固件包列表/升级任务（2）、批量初始化（1）——全部挂 /api/v1/maintenance/*，X-Tenant-Id 隔离经 device 级联 | 01-device-iot.js |
| A10 | rest-device-options 响应清理：operators 字段移除（改为 groups/models/statuses，按租户过滤） | 01-device-iot.js |
| A11 | 功率平衡端点（power-balance-history/power-balance）不设计（智能床无此业务，BatchConfig 页移除该卡片）| — |
| A12 | options/chargers 前端复用 GET /api/v1/devices/options + GET /api/v1/devices（响应适配），不重复定义 maintenance 端点 | — |

---

## v22：设备/分组端点租户隔离正式化（去"待接入"标注）

> 多租户复查迭代（核心设计原则 3/4）：设备组 tenant_id 归属落地后，X-Tenant-Id 过滤从"约定/待接入"转为正式约束
> 日期：2026-08-06

| # | 变更 | 文件 |
|---|------|------|
| A6 | device-groups GET/POST：X-Tenant-Id 从"UUID 可选/上下文约定"改为"UUID 必填"，服务端校验为岗位租户否则 403；步骤增加"校验 X-Tenant-Id → 按 device_group.tenant_id 过滤/写入"；403/404 错误描述去掉"待隔离迭代接入" | 01-device-iot.js |
| A7 | 全库清理"待隔离迭代接入/待后续迭代"标注（0 残留） | 01-device-iot.js |
| A8 | 子租户端点（tenant 层级/父租户查询）标注"后续扩展"（设计层支持，无端点）——见 05-identity-auth.js tenant 描述 | 05-identity-auth.js |

---

## v21：多运营商（租户）身份选择/切换闭环

> iterate 复查：raw-input 05"管理人员可以在很多个租户拥有不同的身份"→ 需登录后选择/切换租户身份。
> 日期：2026-08-06

| # | 变更 | 文件 |
|---|------|------|
| A1 | `GET /auth/me` 响应增加 `available_tenants`（tenant_id/tenant_name/roles/is_global 数组），供登录后身份路由使用 | 05-identity-auth.js |
| A2 | 新增 `POST /auth/switch-tenant`（校验目标租户为当前用户岗位；返回切换后租户角色与权限；服务端无状态不重签 token） | 05-identity-auth.js |
| A3 | 新增端点级租户上下文约定：需登录端点携带 `X-Tenant-Id`，服务端校验为当前用户岗位租户否则 403；无 header = 全局上下文（对齐 security-policy.tenantContext） | 05-identity-auth.js |
| A4 | device-groups 清理 `operator_id` 残留：GET/POST 均改为按 `X-Tenant-Id` 上下文过滤/归属（分组列表、创建、错误码 403/404、request header） | 01-device-iot.js |
| A5 | 清理全库 `operator_id` 残留（设计层 rest/iot/protocols 已归零） | 全局 |

---

## v20：蓝牙/音乐/消息类命名调整（叠加 v19 执行方案）

> 用户在 v19 方案基础上追加裁决。**执行状态：已记录待执行**（v19+v20 合并后统一执行）。
> 日期：2026-08-05

| # | 裁决 | 内容 |
|---|------|------|
| B1 | 蓝牙 action 新增 `connect_audio` | `bluetooth_audio_control` 指令 action 枚举加 `connect_audio`（特殊动作：将设备默认音频输出连接到已配对蓝牙设备）；协议 + 事件类同步 |
| B2 | **music 指令合并到蓝牙控制** | 播放功能已由信息呈现类指令（media_play_*）承担，`music` 指令删除、其操作（play/pause/resume/stop/prev/next/音量）并入 `bluetooth_audio_control`；事件 `PlatformCommand.MusicCommand` 删除，`action.music` 状态事件保留（music_player 设备状态上报）；D9 随之裁决：media_play_* 4 条 → `MediaPlayStartCommand`/`MediaPlayPauseCommand`/`MediaPlayResumeCommand`/`MediaPlayStopCommand` |
| B3 | 蓝牙配对移除 `duration` 字段 | 配对/连接操作无持续时长概念，`bluetooth_audio_control` payload 移除 duration；事件类字段同步移除 |
| B4 | `all_play_stop` 类名写完整 | 事件类 `AllPlayStopCommand`（非 AllStopCommand） |
| B5 | 消息类指令加起始动词 | `text_message`→`show_text_message`、`voice_message`→`play_voice_message`、`stream_voice`→`play_stream_voice`（协议 message id + ack + 事件类名同步）：`ShowTextMessageCommand`/`PlayVoiceMessageCommand`/`PlayStreamVoiceCommand` |

---

## v19：事件类型组织方式重构（基类名+类名，用户裁决）

> 事件类型由"大基类名 + 叶子类名"组织（如 `PlatformCommand.AdjustAngleCommand`），取代小写 dotted prefix 体系；细节（驼峰格式、stop 拆细独立类）按用户推荐确认。六分类语义：**Command=要求对方执行，Message=告知对方**（见 v18 R5b）。
> 日期：2026-08-05

| 项 | 方案 |
|----|------|
| EventType 格式 | `{Category}.{LeafClass}` 驼峰（`PlatformCommand.AdjustAngleCommand` / `DeviceMessage.SensorTemperature`） |
| stop 拆细 | 独立类：`AngleAdjustStopCommand`/`MassageStopCommand`/`AirflowStopCommand`/`TemperatureAdjustStopCommand`/`SnoreInterventionStopCommand`/`WakeUpStopCommand`/`HeightAdjustStopCommand`/`LullingStopCommand` |
| 类别语义 | PlatformCommand=平台要求设备执行（26+ 条）；PlatformMessage=平台告知（仅 heartbeat 类）；DeviceCommand=设备请求平台（4）；DeviceMessage=设备告知（46）；DomainMessage=模块告知；DomainCommand 预留 |
| 待定项 | D10 heartbeat 事件、D11 stream_voice_command_result 归属——批次 2 前裁决 |

---

## v18：四方一致性复查裁决（协议/API/事件/示例）

> 复查发现 12 项差异（D1-D12），用户裁决如下。**执行状态：✅ 已全部执行（2026-08-05 v19+v20 合并执行完成，协议→API→ER 07→base.py→模拟器→示例→测试七层同步）**。
> 日期：2026-08-05

| # | 裁决 | 内容 |
|---|------|------|
| R1 | D1 高度调节**全链补齐** | 协议新增 `height_adjust` / `height_adjust_stop`（仿 angle_adjust，payload 用 height_value）；API 新增端点；事件新增 `platform.height_adjust`（状态事件 `action.height_change` 已有）；模拟器/示例同步 |
| R2 | D2 哄睡执行器命名 | 采用 **Sleep Looping** 词根（sleep_looping），具体事件名待命名确认（见下） |
| R3 | 事件命名格式 | 用户要求解释 ER 事件命名体系（action.*/platform.* 前缀来历）——解释后确认 |
| R4 | D4 reset_device **全局移除** | `platform.reset_device` 事件 + ResetDeviceCommand 类从事件体系/ER/测试全量删除（协议本就无 reset，以后需要再加） |
| R5 | D5-D8 **命名统一跟协议走，不建映射表** | 事件名直接改用协议指令名：`push_notification`→`text_message`、`push_tts`→`voice_message`、`stream_audio`→`stream_voice`、`start_ota`→`firmware_update`（原则：能命名统一就不建表） |
| R5b | **归属修正（用户业务逻辑）** | 六分类语义按业务主动权定义：**Command=要求对方执行/处理**（主动权方发指令），**Message=告知情况**（接收方自行决定响应）。推论：平台下发的显示/播放/推送类（text_message/voice_message/stream_voice）**均归 PlatformCommand**；PlatformMessage 仅剩纯告知类（heartbeat 等，极少）；设备上报绝大多数归 DeviceMessage（主动权在平台）；设备请求平台处理（text_command/voice_command/sleep_report_trigger）归 DeviceCommand ✓ 与现状一致 |
| R6 | D12 stop **拆细** | 每个指令独立 `*_stop` 事件（angle_adjust_stop/massage_stop/airflow_stop/temperature_adjust_stop/snore_intervention_stop/wake_up_stop/height_adjust_stop/lulling_stop）+ `all_stop` 急停 + `platform.emergency_stop` 保留 |
| — | D9/D10/D11 待定 | media_play_* 与 music 关系、heartbeat、stream_voice_command_result 是否需领域事件——待用户裁决 |

---

## v17：设备类型枚举扩展 + 模拟器协议同步

> device_type 新增 angle_control / height_control（以设备模拟器为准，双向对齐）
> ER 共享枚举 DeviceType 同步扩展，06 域 device_type 字段改为引用共享枚举
> 设备模拟器协议层按 ZYRIC 设计全量重写：信封格式 / connect configurations / status_notification sub_device_events
> 日期：2026-07-29

---

## 变更 1：device_type 枚举新增 2 值

**类型**：枚举扩展
**来源**：Review 发现模拟器有 angle_control/height_control 而设计缺失；用户裁决"这两个以模拟器为准加入设计"

| 枚举 | 新增值 |
|------|--------|
| API sharedEnums.device_type（smart-bed.js） | angle_control（角度控制设备）、height_control（高度控制设备） |
| ER 共享枚举 DeviceType（07-event-infrastructure.js） | 同上，position_control 后插入 |
| 06-device-simulator 域 device_type 字段 | comment 改为引用 enum:DeviceType（原为旧列表 vibration_motor/speaker/heating_pad/angle_motor） |

## 变更 2：设备模拟器协议层按设计重写

**类型**：实现同步（以 API 设计为准）
**来源**：全量 Review 发现模拟器协议过时

| 项 | 之前（模拟器） | 之后（对齐设计） |
|----|--------------|----------------|
| 信封 | {version, msg_id, device_address, action, timestamp} | {ver, msg_id, type, action, ts, ref_id?, code?, payload} |
| connect | {firmware_ver, hardware_ver, capabilities, configuration} | {firmware_ver, hardware_ver, configurations:[{sub_device_id, device_type, position[], zone[], limits}]} |
| status_notification | 扁平 {sub_device_id, value} | {sub_device_events:[{sub_device_id, event, status, value, fault, fault_code}]} |
| 指令定位 | deviceAddress（vd-bed-xxx） | sub_device_id（bed-{dt}-{i} / term-{dk} / env-{dk}） |
| debug 预置 | 含废弃 fault_report | connect/status_notification/text_command/heartbeat/angle_adjust |

## 变更 3：设备状态枚举维持双轨（不统一）

**类型**：裁决记录
**来源**：用户澄清——ER 平台视角状态（imported→online→offline→deactivated）与模拟器设备自身状态（connecting/fault）分属两个域，各自正确

---

# API 设计变更说明

---

## v28：睡眠监控域补绑定/报告/趋势端点（批次 2）

> 批次 2（睡眠监控三页实现）：domain-sleep-monitor 从 7 端点扩到 12 端点，对齐 ER 03（cloud_user_binding/sleep_session/sleep_score/daily_report_card）+ tree.js §7 功能区。
> 日期：2026-08-16

| # | 端点 | 说明 |
|---|------|------|
| 1 | GET /api/v1/sleep-monitor/bindings | 设备和用户绑定列表（两层绑定） |
| 2 | GET /api/v1/sleep-monitor/bindings/{device_id} | 单设备绑定详情 |
| 3 | GET /api/v1/sleep-data/reports/daily | 每日报告卡片（单用户） |
| 4 | GET /api/v1/sleep-data/reports/trend | 趋势曲线（7/30/90 天） |
| 5 | POST /api/v1/sleep-data/reports/export | 导出报告 PDF |

权限：binding 读=BindingView（workflow 03）、报告/趋势/导出=SleepDataView/SleepReportExport；身份域权限码 sleep_data.read/sleep_data.export。

---

## v15 → v16：故障上报机制重构 — fault 下沉至子设备 + 故障类别/码二级结构 + 新设备类型

> fault 从 status_notification 根级移除，改为 sub_device_events[] 子设备级独立上报
> FaultCode 扁平枚举 → fault_category（7 大类）+ fault_code（数值码）
> DeviceType 新增 bed_assembly / smart_terminal / device_global
> 日期：2026-07-11

---

## 变更 1：status_notification fault 下沉

**类型**：payload 结构调整
**来源**：用户反馈——根级 fault 无法标记具体子设备，且非所有通知都有 fault 状态变化

### 变更内容

| 之前 | 之后 |
|------|------|
| `status_notification` payload 根级 `fault: enum` | 移除根级 fault |
| `sub_device_events[].item` 无 fault | 新增 `fault: { category: enum, code: int }` 可选对象 |

### 理由
- 每个子设备独立上报故障，精确定位故障来源
- 正常状态通知不再携带多余的根级 fault 字段

## 变更 2：fault 共享枚举重构

**类型**：枚举替换 + 新增

| 之前 | 之后 |
|------|------|
| `fault` 枚举（5 个值：STALL/OVERHEAT/SENSOR_FAIL/EMERGENCY_STOP/COMM_FAIL） | 移除，替换为 `fault_category` + `fault_code` |
| — | `fault_category`（7 大类）：communication/mechanical/execution/power_supply/protection/user_intervention/sensor_acquisition |
| — | `fault_code`（20 个典型值，按 1xx~7xx 编码段组织） |

## 变更 3：device_type 枚举新增

**类型**：枚举扩展

新增 `bed_assembly`（床体总成）、`smart_terminal`（智能终端）、`device_global`（床整体）

## 变更 4：示例文件更新

`examples.js` + `smart-bed-examples.js`：status_notification 示例反映新 fault 结构（3 个子设备事件，含 1 个 fault 上报）

## 涉及的文件

- `data/zyric-actions.js`：status_notification payload + sharedEnums
- `data/iot/smart-bed.js`：同步镜像
- `data/examples.js` / `data/iot/smart-bed-examples.js`：示例同步

---

## v14 → v15：Locale 枚举全球覆盖（12→30）

> 按地理区域全面扩展 locale 枚举，覆盖亚洲（东亚+东南亚+南亚）、美洲（北美+南美）、欧洲主要国家、大洋洲
> 日期：2026-07-08

### 变更内容
- **`_conventions.js`** — locale 表 12→30 行，按 7 大地理区域分组（东亚/东南亚/南亚/北美/南美/欧洲/大洋洲）
- **`zyric-actions.js`** — locale 公共枚举同步扩展，新增 18 个值：zh-HK, ms-MY, fil-PH, hi-IN, ta-IN, bn-BD, ur-PK, en-CA, fr-CA, es-MX, pt-BR, es-AR, pt-PT, it-IT, nl-NL, pl-PL, ru-RU, tr-TR, sv-SE, en-AU
- **`iot/smart-bed.js`** — 同上同步
- **`01-device-iot.js`（ER）** — 注释 12→30 个值

### 涉及文件：_conventions.js、zyric-actions.js、iot/smart-bed.js、01-device-iot.js（ER）

## v13 → v14：语言/区域字段全网对齐（API + ER）

> 基于 locale 枚举扩展后的全量一致性审计：修复 API 字段类型、示例值、ER 实体缺失字段、sleep_preference 与 API 不对齐等问题
> 日期：2026-07-08

### API 数据文件
1. **`zyric-actions.js` / `iot/smart-bed.js`** — `preferences.language` 类型从 `string` 改为 `enum, ref:locale`；`tts_voice` 描述增加语言关联说明
2. **`examples.js` / `iot/smart-bed-examples.js`** — `preferences` 示例值从 `"SM-001"` 修正为 `"zh-CN"` / `"zh-CN-Xiaoxiao"`

### ER 数据文件
3. **`01-device-iot.js`** — `command_text.locale` 注释补充 BCP 47 引用；`command_voice` 和 `command_voice_stream` 实体各补 `locale` 字段
4. **`04-ai-agent.js`** — `sleep_preference` 新增 `language` 字段（对齐 API preferences）；`asr_dialect` 注释改用 BCP 47 扩展子标签格式（移除不存在的 `zh-yue`）；`tts_voice` 注释增加 language 关联；`conversation_session` 的 `asr_dialect`/`tts_voice` 注释同步更新

### 涉及文件：zyric-actions.js、iot/smart-bed.js、examples.js、smart-bed-examples.js、01-device-iot.js（ER）、04-ai-agent.js（ER）

## v12 → v13：Locale 枚举扩展（BCP 47 标准化）

> 语言区域枚举从 3 个值扩展到 12 个值，明确 BCP 47 标准，新增公共约定章节
> 日期：2026-07-04

### 变更内容

1. **`_conventions.js`** — 新增「语言与区域代码（Locale）」章节，声明 BCP 47 标准，包含完整 12 语言枚举表，区分 locale（请求级）与 preferences.language（用户级）语义
2. **`zyric-actions.js`** — locale 公共枚举 3→12 值；3 处 locale 字段 desc 改为引用公共枚举；preferences.language desc 同步更新
3. **`iot/smart-bed.js`** — 同上，同步扩展

### 新增 locale 值
zh-CN / zh-TW / en-US / en-GB / ja-JP / ko-KR / de-DE / fr-FR / es-ES / th-TH / vi-VN / id-ID

### 涉及文件：_conventions.js、zyric-actions.js、iot/smart-bed.js

## v11 → v12：全局枚举规范化（内嵌→ref）

> 所有 Action payload 内嵌枚举值统一改为引用公共枚举 ref；新增 9 个公共枚举
> 日期：2026-07-04

### 新增 9 个公共枚举
execution_mode / locale / reply_mode / audio_codec / report_period / notification_level / adjust_mode / firmware_stage / operation_result

### zone 统一
7 处内联 values: ["left","right","both"] → ref: "zone"

### 涉及文件：zyric-actions.js

## v10 → v11：音箱控制指令合并重构

> 将 8 个独立 speaker_* Action 合并为 2 个统一 music_control 指令；新增 resume 枚举值
> 日期：2026-07-04

### 变更 1：zyric-actions.js — speaker 8→2 合并

**类型**：Action 合并
**来源**：raw-input v12（音箱/music_control 合并）

**变更内容**：speaker_play/pause/stop/previous/next/volume_up/volume_down/volume_set → music_control（action 枚举 9 值）+ music_control_stop（zone 可选，空=全部）。新增 resume 到 action 枚举。

### 变更 2：REST + capability 同步

**类型**：端点调整
**来源**：同变更 1

**变更内容**：8 REST 端点 → 2；actionCapabilityMap 8→2

### 涉及文件

- zyric-actions.js + 02-core-domains.js + device-capability-taxonomy.js

## v9 → v10：角度调节 + 按摩控制指令合并重构

> 将 14 个细粒度角度/按摩 ZYRIC Action 合并为 4 个统一指令；同步合并 REST 端点；更新 capability map
> 日期：2026-07-04

---

### 变更 1：zyric-actions.js — Action 定义合并

**类型**：字段调整（枚举值替换 + 新增 Action）
**来源**：raw-input 需求变更（设备操作指令大幅迭代 v11）

**变更内容**：

| 之前 | 之后 |
|------|------|
| 14 个 Action（8 角度 + 6 按摩）| 4 个 Action（angle_adjust / angle_adjust_stop / massage_control / massage_control_stop）|
| 角度字段：speed/zone/intensity/duration_ms/angle 分散 | 统一为：position + zone + mode(relative/absolute) + value(int, -180~+180) |
| 按摩字段：intensity/duration_ms/mode/zone/count/height/speed 分散 | 统一为：position → zone → intensity → duration → speed 五字段，intensity/speed 引用公共枚举 |
| sharedEnums 无 speed/intensity | 新增 speed（very_slow~very_fast 5 档）+ intensity（very_low~very_high 5 档）|
| categories actions: 37 个 | 27 个（替换 14 个旧为 4 个新）|
| actionRelationships: set_bed_mode triggers 引用旧 Action | 更新为 angle_adjust + massage_control |

### 变更 2：02-core-domains.js — REST 端点合并

**类型**：端点调整
**来源**：同变更 1

**变更内容**：
- 8 个角度 REST 端点 → 2 个（rest-angle-adjust + rest-angle-adjust-stop）
- 6 个按摩 REST 端点 → 2 个（rest-massage-control + rest-massage-control-stop）

### 变更 3：device-capability-taxonomy.js — actionCapabilityMap 更新

**类型**：映射更新
**来源**：同变更 1

**变更内容**：
- 14 个旧 angle/massage 能力映射 → 4 个新映射
- angle_adjust / angle_adjust_stop → back_angle_ctrl + leg_angle_ctrl
- massage_control / massage_control_stop → back_massage + leg_massage + waist_massage

### 涉及文件

- `data/zyric-actions.js`：14 旧 Action → 4 新 Action + categories + actionRelationships + sharedEnums
- `data/02-core-domains.js`：14 REST 端点 → 4 REST 端点
- `data/device-capability-taxonomy.js`：actionCapabilityMap 更新

---

## v8 → v9：新增打鼾干预 / 唤醒动作 / 急停 ZYRIC Action

> 基于用户需求迭代，设备操作指令组新增 3 个 Action + 2 个能力标签 + 3 个 REST 端点
> 日期：2026-07-04

---

### 变更 1：新增 snore_intervention ZYRIC Action

**类型**：新增 Action
**来源**：用户需求迭代

**变更内容**：zyric-actions.js 新增 `snore_intervention` action（cloud→device, call）
- payload: action(start/stop) + zone(left/right/both) + duration_ms + intensity(1-5)
- 更新逻辑：干预中再次 start 覆盖前次参数
- 左右侧规则：dual_zone 支持分侧，非 dual_zone 默认 both

### 变更 2：新增 wake_up_action ZYRIC Action

**类型**：新增 Action
**来源**：用户需求迭代

**变更内容**：zyric-actions.js 新增 `wake_up_action` action（cloud→device, call）
- payload: action(start/stop) + zone + duration_ms + intensity
- 仅控制机械动作，音乐由音频指令独立下发

### 变更 3：新增 emergency_stop ZYRIC Action

**类型**：新增 Action
**来源**：用户需求迭代

**变更内容**：zyric-actions.js 新增 `emergency_stop` action（cloud→device, call）
- payload: null（无参数）
- 比 all_play_stop 更高级别：停止所有机械动作+媒体+环境
- 绕过安全校验，最高优先级

### 变更 4：能力标签扩展

**类型**：新增标签
**来源**：跟随 Action 新增

**变更内容**：
- device-capability-taxonomy.js 新增 2 个标签：`snore_intervention`（打鼾干预）、`wake_up`（唤醒模式），since v2.2
- allTags 新增 2 个
- actionCapabilityMap 新增 2 条规则
- profiles：standard_bed +snore_intervention；premium_bed +snore_intervention +wake_up

### 变更 5：REST 端点新增

**类型**：新增端点
**来源**：跟随 Action 新增

**变更内容**：02-core-domains.js 新增 3 个 REST 端点：
- POST /api/v1/control/devices/{id}/actions/snore-intervention
- POST /api/v1/control/devices/{id}/actions/wake-up
- POST /api/v1/control/devices/{id}/actions/emergency-stop

### 涉及文件

- `data/zyric-actions.js`：+3 endpoint + categories/actionRelationships 更新
- `data/device-capability-taxonomy.js`：+2 标签 + profiles/actionCapabilityMap 更新
- `data/02-core-domains.js`：+3 REST 端点

---

## v3 — 2026-07-02：Command 协议改造（三种指令类型 + 执行模式）

> 改造设备→平台的指令上传协议：新增 voice_command，重命名 stream_audio_command 为 stream_voice_command，三种 command 均新增 type 字段支持 dry_run 模式。
> 日期：2026-07-02

### 变更 1：新增 voice_command（完整语音上传）

**涉及领域**：zyric-actions（语音与指令）
**类型**：新增端点
**来源**：用户需求——终端已完成录音、持有完整音频数据时，无需建立流式通道，一次性上传即可

| 之前 | 之后 |
|------|------|
| 仅有 text_command 和 stream_audio_command 两种指令类型 | 新增 voice_command，共三种：text / voice / stream_voice |

### 变更 2：stream_audio_command → stream_voice_command

**涉及领域**：zyric-actions、zyric-v1 协议、device-capability-taxonomy
**类型**：重命名
**来源**：统一命名规范——设备→平台的语音上传使用 voice 语义而非 audio（audio 保留给平台→设备的音频流播放）

| 之前 | 之后 |
|------|------|
| `stream_audio_command` | `stream_voice_command` |

### 变更 3：三种 command 新增 type 字段（execute / dry_run）

**涉及领域**：zyric-actions（text_command / voice_command / stream_voice_command）
**类型**：参数新增
**来源**：用户需求——支持 dry_run 模式，终端可在不实际执行的前提下获取平台的指令规划建议

| 之前 | 之后 |
|------|------|
| command 仅有 execute 模式 | 新增 `type` 字段：`"execute"`（默认）/ `"dry_run"` |

**execute 模式**：ack 仅确认收到，后续操作由平台通过独立 action 逐条下发。
**dry_run 模式**：
- text_command / voice_command → ack 中 `planned_actions` 字段返回拟执行的指令集
- stream_voice_command → 流结束后平台通过 `stream_voice_command_result` 消息返回

### 变更 4：新增 stream_voice_command_result（dry_run 结果下发）

**涉及领域**：zyric-actions
**类型**：新增端点
**来源**：stream_voice_command 的 dry_run 模式需要在流结束后返回指令规划结果

| 之前 | 之后 |
|------|------|
| 无 | 新增 `stream_voice_command_result`（cloud→device），携带 recognized_text + planned_actions |

### 涉及的端点

- `text_command`：新增 `type` 字段，ack 新增 `planned_actions`
- `voice_command`：新增端点（完整语音上传）
- `stream_voice_command`：原 `stream_audio_command` 重命名，新增 `type` 字段
- `stream_voice_command_result`：新增端点（dry_run 结果下发）

---

## v4 — 2026-07-02：文本消息改造（text_message + level enum + 文本长度 + UTF-8）

> 改造云端→设备的文本消息推送协议：重命名 show_notification 为 text_message，细化 level 枚举，定义文本长度限制，明确 UTF-8 编码规范。
> 日期：2026-07-02

### 变更 1：show_notification → text_message

**涉及领域**：zyric-actions、zyric-v1 协议、device-capability-taxonomy
**类型**：重命名
**来源**：语义优化——"notification"偏被动通知，"text_message"更准确表达主动推送文本消息的语义

| 之前 | 之后 |
|------|------|
| `show_notification` | `text_message` |

### 变更 2：level enum 细化

**涉及领域**：zyric-actions（text_message）
**类型**：枚举调整
**来源**：用户需求——需要更明确的语义和显示样式区分

| 之前 | 之后 |
|------|------|
| `["info","warn","alert"]` | `["info","warning","error","success"]` |

- `info` — 一般信息（蓝色/默认样式）
- `warning` — 警告（黄色/闪烁提示）
- `error` — 错误（红色/持续高亮）
- `success` — 成功（绿色/短暂显示）

### 变更 3：title 和 body 文本长度限制

**涉及领域**：zyric-actions（text_message）
**类型**：约束新增
**来源**：嵌入式设备屏幕显示能力有限，需要限制文本长度

| 字段 | 之前 | 之后 |
|------|------|------|
| `title` | 无限制 | UTF-8 编码，最长 64 字符（约 21 个汉字） |
| `body` | 无限制 | UTF-8 编码，最长 512 字符（约 170 个汉字） |

### 变更 4：UTF-8 编码规范明确声明

**涉及领域**：zyric-v1 协议（fieldSpec）
**类型**：规范补充
**来源**：确保设备端和平台端统一使用 UTF-8 编码

在 `fieldSpec` 顶层新增 `encoding` 字段：
> "所有文本字段（string 类型）统一使用 UTF-8 编码。JSON 本身基于 UTF-8，设备端和平台端必须使用 UTF-8 编解码所有字符串。"

### 涉及的端点

- `text_message`：原 `show_notification` 重命名，level enum 调整，title/body 新增长度限制和 UTF-8 标注

---

## v5 — 2026-07-02：音频流简化（删除 TTS 指令 + stream_audio_start → stream_voice_start）

> 简化平台→设备的音频流播放协议：删除所有 TTS 控制指令，删除音频流 pause/resume/stop 指令，stream_audio_start 重命名为 stream_voice_start，播放控制由用户打断操作处理。
> 日期：2026-07-02

### 变更 1：删除 stream_tts 指令（4 个端点）

**涉及领域**：zyric-actions、zyric-v1 协议、device-capability-taxonomy
**类型**：端点删除
**来源**：设备端要求——TTS 和音频流统一为一个 stream_voice_start 指令，不再需要单独的 TTS 控制指令

| 之前 | 之后 |
|------|------|
| stream_tts_start/pause/resume/stop | 已删除 |

### 变更 2：删除 stream_audio pause/resume/stop 指令（3 个端点）

**涉及领域**：zyric-actions
**类型**：端点删除
**来源**：设备端要求——播放状态由用户打断操作控制，平台不再控制暂停/恢复/停止

| 之前 | 之后 |
|------|------|
| stream_audio_pause/resume/stop | 已删除 |

### 变更 3：stream_audio_start → stream_voice_start

**涉及领域**：zyric-actions、zyric-v1 协议、device-capability-taxonomy
**类型**：重命名 + 用途扩展
**来源**：统一命名规范——平台→设备的音频流播放使用 stream_voice_start，适用于 TTS 语音播报、提示音、语音回复等场景

| 之前 | 之后 |
|------|------|
| `stream_audio_start`（音频流播放） | `stream_voice_start`（语音流播放，含 TTS/提示音/语音回复） |

### 涉及的端点

- 删除：`stream_tts_start`、`stream_tts_pause`、`stream_tts_resume`、`stream_tts_stop`
- 删除：`stream_audio_pause`、`stream_audio_resume`、`stream_audio_stop`
- 重命名：`stream_audio_start` → `stream_voice_start`
- 更新：`stream_stop` 描述（设备端用户打断播放时发送）

---

## v5b — 2026-07-02：stream_voice_start 打断行为规范补充

> 补充 stream_voice_start 的设备端打断行为规范说明。
> 日期：2026-07-02

### 变更：stream_voice_start notes 补充打断行为规范

**涉及领域**：zyric-actions（stream_voice_start）
**类型**：说明补充
**来源**：设备端需求——明确播放被打断时的行为规范

**补充内容**：
1. 接收指令时，设备端应立即准备接收语音流数据并同步播放给用户
2. 播放过程中一旦被打断（用户开口说话或按下停止键），播放必须立即停止，视为流传输连接断开
3. 设备端需保证：① 不再继续播放该 stream 的后续数据；② 向服务端发送 stream_stop 消息（携带 stream_id），通知平台停止继续发送数据
4. 平台侧收到 stream_stop 后停止发送二进制帧，流传输结束

### 涉及的端点

- `stream_voice_start`：notes 字段补充打断行为规范

---

## v6 — 2026-07-02：API Viewer 协议分页切换 + 协议内部一致性检查

> 改进 API Viewer 支持协议分页切换，并完成 ZYRIC 协议内部一致性检查。
> 日期：2026-07-02

### 变更 1：ZYRIC 协议内部一致性检查

**涉及领域**：zyric-actions、zyric-v1 协议、device-capability-taxonomy
**类型**：一致性检查
**来源**：用户需求——检查多次迭代是否造成协议内部矛盾

**检查结果**：✅ 无矛盾
- 在 `data/` 目录下搜索已删除的 action 名称（stream_tts_*、stream_audio_pause/resume/stop、show_notification），结果为空
- 在 `protocols/zyric-v1.js` 中搜索已删除的 action，仅在 `documentHistory` 变更记录中找到（正常）
- 在 `device-capability-taxonomy.js` 中搜索已删除的 action，结果为空

### 变更 2：API Viewer 协议分页切换

**涉及领域**：api-viewer.html
**类型**：功能增强
**来源**：用户需求——不同协议的内容分页显示，通过下拉列表切换

**新增功能**：
- 在工具栏添加协议选择下拉列表（默认"所有协议"）
- 支持选择特定协议（RESTful HTTP / ZYRIC v1）
- 选择协议后，领域端点页面只显示该协议下的端点
- 切换协议时自动重新渲染当前页面

### 涉及的文件

- `api-viewer.html`：新增协议选择下拉列表和端点过滤逻辑

---

## v6b — 2026-07-02：API Viewer 协议切换逻辑修正

> 修正协议切换行为：选择特定协议时，左侧导航按协议过滤，而非仅过滤端点内容。
> 日期：2026-07-02

### 变更：协议切换逻辑修正

**涉及领域**：api-viewer.html
**类型**：行为修正
**来源**：用户反馈——原逻辑仅过滤端点内容，未过滤左侧导航

**修正内容**：
1. 选择特定协议时，隐藏左侧"协议定义"分类，直接显示该协议定义
2. "领域端点"按协议筛选，只显示包含该协议端点的领域
3. 切换协议时自动重建左侧导航

**行为对比**：

| 之前 | 之后 |
|------|------|
| 切换协议后，左侧导航不变，仅端点内容过滤 | 切换协议后，左侧导航按协议过滤 |
| "协议定义"始终显示所有协议 | 选择特定协议时隐藏"协议定义"分类 |
| "领域端点"显示所有领域 | 选择特定协议时只显示包含该协议端点的领域 |

### 验证结果

- ✅ 选择"所有协议"：显示所有协议和所有领域（10 个领域）
- ✅ 选择"ZYRIC v1"：隐藏协议定义，只显示 ZYRIC 领域（1 个领域）
- ✅ 选择"RESTful HTTP"：隐藏协议定义，只显示 REST 领域（8 个领域）
- ✅ 0 console errors

---

## v6c — 2026-07-02：API Viewer 协议切换逻辑再次修正

> 再次修正协议切换行为：选择特定协议时，左侧显示该协议定义，而非隐藏协议定义。
> 日期：2026-07-02

### 变更：协议切换逻辑再次修正

**涉及领域**：api-viewer.html
**类型**：行为修正
**来源**：用户反馈——选择特定协议时，协议定义消失了

**修正内容**：
1. 选择特定协议时，左侧显示该协议名称作为标题
2. 协议定义始终显示为可点击项
3. "领域端点"按协议筛选，只显示包含该协议端点的领域

**行为对比**：

| 之前 | 之后 |
|------|------|
| 选择特定协议时，隐藏"协议定义"分类 | 选择特定协议时，显示该协议名称作为标题 |
| 协议定义不可点击 | 协议定义始终显示为可点击项 |
| "领域端点"显示所有领域 | 选择特定协议时只显示包含该协议端点的领域 |

### 验证结果

- ✅ 选择"所有协议"：显示"协议定义"标题，2 个协议，10 个领域
- ✅ 选择"ZYRIC v1"：显示"ZYRIC v1"标题，1 个协议定义项，1 个领域
- ✅ 选择"RESTful HTTP"：显示"RESTful HTTP"标题，1 个协议定义项，8 个领域
- ✅ 0 console errors

---

## v7 — 2026-07-02：API Viewer 重大重构（ZYRIC 协议专用菜单结构）

> 重构 API Viewer，为 ZYRIC 协议提供专用菜单结构：协议定义、动作定义、对象定义、枚举定义、状态定义。
> 日期：2026-07-02

### 变更：API Viewer 重大重构

**涉及领域**：api-viewer.html
**类型**：架构重构
**来源**：用户需求——不同协议需要不同的菜单和页面结构

**重构内容**：

1. **移除"所有协议"选项**：必须选择一个特定协议查看
2. **ZYRIC 协议菜单结构**：
   - 📋 协议定义：协议基础定义（承载层、认证、RPC 语义等）
   - ⚡ 动作定义：所有 Action 的详细定义，包含 call/ack 对象
   - 📦 对象定义：所有数据对象的字段结构，支持嵌套
   - 📝 枚举定义：所有枚举类型的可选值
   - 🔄 状态定义：设备生命周期、交互状态、动作状态

3. **REST 协议菜单结构**：
   - 📋 协议定义：REST 协议基础定义
   - 🌐 领域端点：按领域分组的 REST 端点

4. **交叉引用**：
   - 动作定义中的枚举字段可点击跳转到枚举定义
   - 对象定义中的枚举字段可点击跳转到枚举定义

**验证结果**：

- ✅ ZYRIC 协议：5 个菜单项，协议定义 9 个 section，动作定义 16 个 action 卡片
- ✅ 对象定义：32 个 section（call + ack 对象）
- ✅ 枚举定义：6 个 section
- ✅ 状态定义：4 个 section
- ✅ REST 协议：2 个菜单项，正常显示领域端点
- ✅ 0 console errors

---

## v8 — 2026-07-03：新增设备操作指令分组（6 个 Action + 能力标签扩展）

> 为 ZYRIC 协议新增"设备操作指令"Action 分组，包含 6 个 cloud-to-device 方向的结构化指令动作，扩展能力标签体系。
> 日期：2026-07-03

### 变更 1：新增 6 个设备操作指令 Action

**涉及领域**：zyric-actions
**类型**：新增端点（6 个）
**来源**：用户需求——平台解析用户语音/文本指令后，需要具体的结构化 Action 来下发设备操作

| Action | 方向 | 说明 |
|--------|------|------|
| `set_bed_mode` | cloud→device | 一键设置床的模式预设（睡眠/观影/按摩/零重力/平放/阅读/自定义） |
| `posture_control` | cloud→device | 床体姿态控制（角度/档位/左右分区/速度） |
| `environment_control` | cloud→device | 环境设备控制（灯光/通风/加热） |
| `speaker_control` | cloud→device | 蓝牙音箱播放控制（上一首/下一首/音量/播放暂停） |
| `bluetooth_pairing` | cloud→device | 蓝牙配对管理（配对/确认/取消/解除/列表） |
| `massage_control` | cloud→device | 按摩动作控制（顶腰/头颈/背/腿/全身 + 次数/时长/间隔等参数） |

每个 Action 包含完整定义：payload 字段、business_logic（6 维度）、RPC 配置、响应格式、错误码和 notes。

### 变更 2：扩展 status_notification 状态上报

**涉及领域**：zyric-actions
**类型**：参数新增

- **F. 模式状态**（`mode`）：新增 `active_mode`、`mode_started_at`、`mode_source` 字段
- **C. 环境状态扩展**：新增 `light_brightness`、`heating_level`、`ventilation_intensity`、`speaker_playing`、`speaker_volume`、`bluetooth_status` 字段
- `status_notification_trigger`：`categories` 新增 `mode` 选项

### 变更 3：新增能力标签

**涉及领域**：device-capability-taxonomy
**类型**：新增标签（2 个）

| 标签 | 类别 | 说明 |
|------|------|------|
| `bed_mode` | posture_action | 模式预设：支持一键模式切换 |
| `bluetooth_speaker` | media_output | 蓝牙音箱：支持蓝牙配对与播放控制 |

### 变更 4：更新典型设备型号

**涉及领域**：device-capability-taxonomy
**类型**：配置更新

- `standard_bed`：新增 `bed_mode`
- `premium_bed`：新增 `bed_mode`、`bluetooth_speaker`
- `voice_hand_controller`：新增 `bluetooth_speaker`

### 变更 5：新增状态定义

**涉及领域**：zyric-actions
**类型**：状态新增（3 组）

- `mode_state`：模式执行状态（idle→activating→active→interrupted/completed）
- `massage_state`：按摩执行状态（idle→running→paused→completed/interrupted）
- `bluetooth_state`：蓝牙连接状态（disconnected→pairing→connected/error）

### 涉及的端点

- 新增：`set_bed_mode`、`posture_control`、`environment_control`、`speaker_control`、`bluetooth_pairing`、`massage_control`
- 修改：`status_notification`（新增 mode 分类和扩展 environment 字段）
- 修改：`status_notification_trigger`（categories 新增 mode）

> ⚠️ **取代注记（`G-493`⑤，2026-10-13）**：本册 `v99 → v100`（`T-305`）的「判据」行句末原写「（原始行见 `.pipeline/journal/T-305.journal.md`）」—— 🔴 **该件实测不存在**（`Test-Path` ＝ `False`；`Get-ChildItem -Recurse -Filter 'T-305*'` 全仓 **0** 命中）⇒ 该可核性承诺**不可兑现**。**原指针原文一字未改**（⛔ 不改历史条目）；**存续证据改指**：① `.pipeline/journal/_archived/_garchive-closed-2026-09-19.md` 的 **`G-258`** 节（节标题逐字含「关闭于 2026-09-19；关闭依据 ＝ `T-305` `6e089610`」）；② `.pipeline/archive/PIPELINE-readset-history-2026-09.md` 的 **`T-305`** 行（载 `0b` 三项与 `design-regression` 等收口读数）。⚠️ 本注记为**册尾追加**（⛔ 未增删任何既有行）：实测本册存在**指向被引句（第 1220 行）之后各行**的行号入向引用（第 1479／1481／1486／1489／1493 行等）⇒ 就地插入会令这些行号整体漂移。⚠️ **另注（本件面外，如实上报）**：本册在 `scripts/sync_design_to_publish.ps1` 的 `ExtraMappingTable` 内（`design/04-platform-api/CHANGELOG.md → publish/api/CHANGELOG.md`，`Kind=file`）⇒ 本注记落盘后该镜像件将**少这一条**；⛔ 本次任务书**未授权**动 `publish/**` ⇒ **未同步**，交编排器裁决。

> 🔴 **取代注记（`T-863` ＝ `T-852`／`Q-054` §6-B4 的设计面同步，2026-10-20；按 `rule-0d` 追加，⛔ 未改任何历史条目）**：用户 **2026-10-20** 裁决（逐字）：「我们**只认"绑定"或"不绑定"**，那么"**绑定状态**"其实就**没有意义**了……我的记录**要么有绑定，要么没绑定。有就加上去，没有就删掉**」；「因此，我倾向于**完全删掉这个字段**」。
> ⇒ `T-852` 已删 `cloud_user_bindings` 的「绑定状态」与「失败原因」两列（迁移 `f0a1b2c3d4e5`，down `c9d0e1f2a3b5`）；**本域 API 契约同批收口**：`rest-binding-list`（`data/rest/02-core-domains.js`）**删掉「绑定状态」查询参数** —— **行存在 ＝ 绑定存在；无行 ＝ 未绑定**，无状态可筛 ⇒ `request.query` 只剩 `keyword?` / `page` / `size`；`rest-binding-unbind` 卡里「改前语义留痕」提到的三字段复位，改为「`remote_phone` ＋ 已删的两列」。
> **被取代的原句（逐字留痕，原处已改 ⇒ 只在本注里保存）**：① `request: { query: { keyword: "string?", binding_status: "enum?", page: "int", size: "int" } }`；② `rest-binding-unbind` 的 `description` 句「（`Q-010` §1.4 现场原文：只能人工改库复位 `remote_phone`／`failure_reason`／`binding_status` 三字段）」。
> ⚠️ **镜像同步（`T-863` 已获授权，与本注同批落地）**：本册在 `scripts/sync_design_to_publish.ps1` 的 `ExtraMappingTable` 内（`design/04-platform-api/CHANGELOG.md → publish/api/CHANGELOG.md`，`Kind=file`）⇒ 本注落盘后**同批**以 `Copy-Item` 镜像到 `publish/api/CHANGELOG.md`，双侧 sha256 相等，`sync_design_to_publish.ps1 -Check` 判 **GREEN**。
> ⚠️ 摆放理由 ＝ **册尾追加**：本册存在行号入向引用（见上条 `G-493`⑤ 注），⛔ 就地插入会令行号漂移。本注只追加、不改任何既有行。


