Files
th-hotel-simple/docs/project/requirements/M012-qbd-deterministic-parser-change-request-v1.md
T
鲨鱼辣椒 694c4317a3 checkpoint: complete recoverable V2 pre-separation baseline
Complete the selective V2 checkpoint with its minimal AgentBus, object-storage, replay persistence, and validated-workbench shared dependency closure.
2026-08-20 17:09:00 +08:00

247 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# M012 / Layer 3 QBD 确定性 Parser Change Request v1
> **Migration notice(2026-08-11)**:本文记录已验证的旧 QBD Parser 实现。其工作簿业务规则和
> 真实样本证据继续有效,但公共 `FieldObservation` 状态、reason code、`GROUP_IN`、
> `SOURCE_RATE_CODE`、`ROOM_QUALIFIERS` 与字段 Recovery 输出边界已被 ADR-008 的冻结目标契约取代。
> 代码迁移完成前不得把本文的“Approved”误读为新 Parser 契约已实现。
| 项 | 内容 |
| --- | --- |
| 状态 | Approved — implementation verified locally |
| 日期 | 2026-08-10 |
| 提出人 | 用户确认的 QBD 程序解析规则与 sender 映射 |
| 关联契约 | Parser 历史实现见 `booking-email-architecture-v0.4.md`;当前跨层边界见 `booking-email-architecture-v0.5.md`、`booking-email-contracts-v2.md`;Parser/Recovery 细节见原讨论稿 |
| Checkpoint | `M012-qbd-deterministic-parser-v1` |
| 影响范围 | Backend / Contracts / Parser / Test / Docs |
## 1. 变更背景
现有固定渠道 Parser 主要按附件文件名、Sheet 和表头评分识别渠道,并把行内容直接聚合为当前事实。它无法完整表达 QBD 月表已经确认的 `GROUP IN`、CXL 日期补全、富文本划线 BEFORE/AFTER、原始房型/价格、数量默认来源、字段状态和稳定 evidence;现有 C03 还把 `StayFact.nights` 固定写为 null。
本变更先只实现 QBD。普通LianTai在后续独立CR中接入,不能复用或猜测其模板规则。确定性 Parser 只输出不可变 `ParserObservationSet` 与 AFTER/CURRENT 初始 facts;完整 Layer 3 仍按共用契约经过 RecoveryRequestBuilder、字段 Recovery、本地 Validator/Assembler 后形成 `EffectiveFactView`。Parser 不执行 Agent、TaskCard、PMS、Opera、付款、库存扣减或部门流转。
## 2. 权威输入与版本
### 2.1 Sender registry
| 精确 sender | Profile | 本 checkpoint 状态 | 模板处理 |
| --- | --- | --- | --- |
| `op.qbdtravel@gmail.com` | `QBD` | ACTIVE | 启用 QBD v1 Parser |
| `op.liantaitravel@gmail.com` | `LIANTAI` | REGISTERED_PENDING | fail-closed,不解析附件 |
sender 先经既有 `BookingSenderNormalizer` 规范化,再做精确唯一匹配。不得按前缀、显示名或相似域名猜 Profile。
新邮件存在非空 sender 时必须走 registry:未知 sender 为 `NOT_APPLICABLE`;两个 pending Profile 返回明确 `CHANNEL_PROFILE_IMPLEMENTATION_PENDING`。为兼容早期 contracts-v1 重放,只有 sender 缺失的历史/测试输入暂时保留 legacy Parser 路径;该兼容路径不得产生 QBD v1 observations。
### 2.2 真实样本
真实 XLSX 不进入仓库,只保留哈希和 opt-in 验收参数。
| 月份 | SHA-256 | 主业务 Sheet | 已确认橙色行 |
| --- | --- | --- | ---: |
| 2026-08 | `b81bd95df6acaafa1c3a457f7047f4d15966aafafe3989d10744b0d8ad65d918` | `2026年8月|AUGUST_26` | 8 |
| 2026-07 | `f2bb739a4f511348155c47cdf540aac1fdd608dca71f32a2a23833cbb3a08210` | `2026年7月|JULY_26` | 1 |
两份样本合计 9 个 row unit、16 个 detail segment:7 对 BEFORE/AFTER,2 个 CURRENT。
### 2.3 子契约版本
- observation:`fixed-channel-observation-v1`
- sender registry:`fixed-channel-sender-profile-v3`
- QBD profile:`qbd-profile-v1`
- QBD rule bundle:`qbd-rules-v1`
- QBD parser:`booking-email-parser-qbd-v1`
稳定 ID 必须包含来源消息 ID/revision、attachment id/hash、Sheet、行号、segment/item span/order 和 observation version;不得包含当前时间、标准化结果或 Agent 答案。
## 3. 范围与非目标
### 3.1 本次实现
- sender registry 与 ACTIVE/PENDING/UNKNOWN fail-closed 路由;
- QBD 标题、唯一业务 Sheet、唯一语义表头和必需列二次验证;
- 真实橙色变更行选择;
- C/E/H/J 的无损来源观察;QBD F 完全忽略;
- E 富文本字符 span、BEFORE/AFTER/CURRENT、房型/早餐/Q10/数量/价格;
- J/E/H 日期规则、CXL 和 `nights=departure-arrival`;
- AFTER/CURRENT 到 `facts[]` 的当前事实投影;
- additive `parser_observations`、稳定 ID、field status/origin/patchability/evidence;
- synthetic 测试和两份真实 XLSX 的本地 opt-in 验收。
### 3.2 明确不做
- 不在本CR中实现普通LianTai模板;
- 不实现 Recovery Agent、RecoveryRequest/Patch/Overlay;
- 不让现有 Booking Agent 读取 raw/BEFORE observations;
- 不决定最终 Allotment source/actual、库存或扣减;
- 不从 QBD F 列读取 Rate Code;
- 不新增 Controller、权限、数据库表、前端流程或用户可编辑字段;
- 不调用 PMS、Opera、OHIP、外部 Agent 或邮件发送。
## 4. Profile 与模板双门禁
QBD sender 命中后,附件仍必须通过以下模板验证:
1. 当前材料中存在可读取的 `.xlsx`;
2. 唯一业务 Sheet 名符合 `2026年<月>月 | <英文月>_<两位年>`,兼容半角/全角竖线;
3. A1 标题同时包含 `QBD`、月份年份和 `WYNDHAM JOMTIEN PATTAYA`;
4. 前 10 行只有一个表头行能唯一绑定 C/E/H/J 语义;
5. 必需列为 Tour Code、Raw Hotel Detail、操作备注、GROUP IN;F 无论表头或值是否存在均完全忽略,不参与模板门禁、observation、issue、Recovery、Rate Code 或 nights;
6. 损坏、加密、超限、多 QBD 模板附件、多业务 Sheet、重复/歧义表头均 fail-closed。
sender 命中但模板不匹配输出 `PROFILE_TEMPLATE_MISMATCH`,不能回退用文件名强套旧 QBD Profile。
## 5. QBD 行与字段规则
### 5.1 选行
- 只选择核心语义单元格 C/E/H/J 使用同一 QBD 变更橙色的行;
- 真实橙色 OOXML 为 solid `theme=9,tint=0.8`;允许测试 fixture 使用受控等价 ARGB;
- F/G/I/K 不要求填色;
- 任意其他非白色不等于橙色。真实 August 第 22 行是 solid theme 0,必须排除;
- 表头、普通历史行、其他状态色行不生成 row unit。
### 5.2 团号与操作
- Tour Code 完整去除首尾空白后写入 `group_code`,不得拆成客人姓名;
- H 同时保存 `cell_raw`、`operation_date`、`operation_target_raw`、`operation_kind`;
- 受控动作包括 `NEW BOOKING`、`AMD BOOKING`、`AMD ALLOTMENT`、`CANCEL`、`CANCEL BOOKING`、`AMD GROUP CODE`;
- C03 `action_hint` 只投影 `NEW_BOOKING / UPDATE_BOOKING / CANCEL_BOOKING`,原动作继续保存在 observation;
- J 为 CXL/CXL: 时 operation kind 强制 CANCEL,但不丢失 H 原文。
### 5.3 日期与 nights
- J 为真实日期:arrival=J;E 首部 start day 必须交叉验证;end>=start 时离店为同月,end<start 时为下月;
- J 为 `CXL/CXL:`:从每个 E segment 的 start/end day 与 H 行首操作日期补全年月;无法安全补全时字段为 `UNRESOLVED`,不得用 D、Sheet 月份或系统当前时间猜;
- departure 必须晚于 arrival;`nights` 使用 `ChronoUnit.DAYS.between(arrival, departure)`;
- BEFORE/AFTER/CURRENT 每段分别保存自己的日期与 nights;
- F 无论空或非空均不读取、不保留、不告警、不复核;每个 segment 的 nights 只按 `departure-arrival` 计算。
### 5.4 富文本与 segment
- E 按物理换行和 XSSF rich-text run/span 拆成有序 segment;
- 一行非空字符全部划线为 BEFORE;全部未划线且存在 sibling BEFORE 为 AFTER;普通单行/无变更对为 CURRENT;
- 换行符本身的字体不决定下一行角色;必须按该行非空字符覆盖判断;
- 同行 room item 的划线不同则 item 级保存 role,segment 标记 MIXED;只投影活动 item;
- BEFORE 完整保存 stay、room items、quantity、price 和 evidence,但永不进入 `facts[]`;
- 多条均未划线的活动 segment 全部保序,`merge_status=BUSINESS_MERGE_REQUIRED`,不累加、不覆盖、不去重。
### 5.5 房型、早餐、数量和价格
- 每个 `【...】` 形成一个稳定 room item;
- 保留 `source_label`,另输出去除 BF 和价格 token 后的规范房型文本;
- `BF` 为早餐,`Q10` 为 qualifier;`BF(Q10)` 需保留 `(Q10)`;
- `】` 后、同一外层内容中的整数为 explicit quantity;缺失时 deterministic `1`,规则引用 `QBD_QUANTITY_DEFAULT_ONE@1`;
- 仅当同一订单行只有一个可直接投影的活动 fact 时,汇总该 fact 全部 AFTER/CURRENT 活动 room items 的有效 quantity:1–4 间为 `FIT`、5 间及以上为 `GROUP`;QBD 渠道名、Tour Code 和 segment 文本均不得覆盖房量结果。多活动 segment 保持 `booking_type=null + BUSINESS_MERGE_REQUIRED`,待业务合并为独立订单后再按总房量判断;任一有效数量未解决时也保持 null;
- `8.5`(含数值等价写法)→ 850;其他紧贴房型末尾的小数 token ×1000,使用 `BigDecimal`;
- 原始 token 与规则引用不得丢失;空间/异常 token 无法唯一解释时为 `UNRESOLVED`,不得跨 room item 借值;
- 来源未写价格为 `MISSING+LOCKED`,不得交给 Agent 猜;
- QBD `source_rate_code=NOT_APPLICABLE+LOCKED`。
## 6. contracts-v1 additive extension
`ParsedFactSet` 在末尾新增 optional `parser_observations`,旧 Java 构造器和缺失字段 JSON继续可用。核心结构为:
```text
ParserObservationSet
├── observation_contract_version / profile_version / sender_registry_version
├── parser_version / rule_bundle_version / parser_normalization_catalog_version
├── observation_hash
└── row_units[]
├── row_unit_id / source_scope / selection
├── group_code / operation / source_rate_code / group_in
└── detail_segments[]
├── segment_id / change_role / raw_text / raw_text_span / stay
└── room_items[]
├── room_item_id
└── source_label / source_room_type / breakfast / qualifiers
/ quantity / source_price_token / source_price
```
每个 `FieldObservation` 必须携带按 observation version + owner stable ID + canonical semantic role 计算的稳定 `field_id`,以及 `semantic_role / field_path / status / raw_value / normalized_value / origin / patchability / reason_code / rule_ref / evidence_refs`。`reason_code` 只使用共用 `field-reason-v1` 枚举;`UNRESOLVED/CONFLICT` 必填,其余状态显式为 null。统一字段状态:`RESOLVED / MISSING / UNRESOLVED / CONFLICT / NOT_APPLICABLE`;来源:`SOURCE_EXPLICIT / PROFILE_DERIVED`;权限:`LOCKED / RECOVERABLE / REVIEW_ONLY`。Parser observation 不允许出现 `AGENT_RECOVERED`。
`observation_hash` 对排除自身后的完整 observation graph 使用 RFC 8785 JCS + SHA-256;rich-text strike/active signature 必须进入 evidence content hash。共享 canonicalizer 对 candidate identity 的 number/string/boolean/list 也必须返回 value 本身的 canonical JSON;底层 object-root 限制只允许在该唯一实现内部用固定 wrapper 兼容,不得由 Recovery 复制 JCS。`unresolved_fields[]` 是持久 `status=UNRESOLVED` fields 的严格投影并复用同一个 `field_id/reason_code`;结构解析失败、mixed strike 和 `BUSINESS_MERGE_REQUIRED` 只能使用带稳定 `ObservationTargetRef` 的 issue/review gate。
`facts[]` 保持当前有效确定性事实:
- AFTER/CURRENT 或 MIXED 中活动 room items 可进入 facts;
- BEFORE 不进入;
- fact id 基于 source segment 稳定生成,并显式携带 `source_row_unit_id/source_segment_id/source_field_ids/evidence_refs`;
- room item fact 使用 `source_room_type`,并携带 `source_room_item_id/source_label/breakfast/source_price/source_field_ids`;`room_type_code` 只允许 Layer 4 RateRoomResolver 输出;
- `StayFact.nights` 必须填入;
- QBD `rate_hint` 固定为 null;`source_price` 是 Layer 3 唯一权威来源价格,legacy `rate_hint` 不得被 Recovery/Resolver 消费或伪装成 Rate Code。
Parser 与 Recovery 共用的 Java 入口固定为:
- `FixedChannelObservationContracts`:semantic role、field reason、target ref 与 field ID 算法;
- `FixedChannelObservationCanonicalizer`:RFC 8785 observation hash;
- `FixedChannelObservationIndex`:field ID/owner/ancestry 唯一 registry;
- `QbdStayDateRules`:普通 J+E、CXL H+E、跨月和 nights 的同版纯规则;
- `QbdEffectiveDependencyResolver.resolve(...)`:accepted effective fields 的确定性 dependency 重算;
- `QbdCurrentFactProjector.project(...)`:Parser 初始 facts 与 EffectiveFactView 重投影共用的唯一 AFTER/CURRENT 纯函数。
完整 observations 只保存在内部 Layer 3 artifact/重放数据;现有 Booking Agent 的安全副本继续不包含该字段。逐 fact 数据库投影只保存 `facts[]`,不会把 BEFORE 建成当前事实。
## 7. 失败与 issue 语义
| 场景 | applicability / issue |
| --- | --- |
| sender 非空但未知 | `NOT_APPLICABLE / CHANNEL_PROFILE_UNKNOWN` |
| 本checkpoint中普通LIANTAI已登记未实现 | `NOT_APPLICABLE / CHANNEL_PROFILE_IMPLEMENTATION_PENDING` |
| QBD sender + 模板不匹配 | `AMBIGUOUS / PROFILE_TEMPLATE_MISMATCH` |
| workbook 损坏/加密/超限 | `FAILED / QBD_WORKBOOK_PARSE_FAILED` |
| 没有真实橙色业务行 | `APPLICABLE + facts=[] / NO_CURRENT_STRICT_ROWS` |
| 字段来源缺失 | `MISSING+LOCKED` |
| token 存在但程序不能唯一解释 | `UNRESOLVED+RECOVERABLE` 或 `REVIEW_ONLY` |
| 同权冲突/混合格式无法安全投影 | `CONFLICT+REVIEW_ONLY` |
| 多活动 segment | `BUSINESS_MERGE_REQUIRED` |
## 8. 安全、兼容与回滚
- 不新增外部 API 或权限;所有字节只在本次内存解析,Artifact 不保存附件 bytes/URL;
- evidence locator 只包含 attachment logical id/hash、Sheet、行/列/span,不含签名 URL;
- raw detail 仅在项目内部 retention artifact 中保存,不进入普通 Booking Agent 安全投影;
- 旧 contracts-v1 JSON 缺少 `parser_observations` 时按空处理;未知 future contract version 仍 fail-closed;
- 回滚只需停止 QBD sender ACTIVE 路由并恢复旧 Assembler;不涉及数据库 migration 或历史数据重写。
## 9. 验收标准
1. 三个 sender 精确、大小写规范化和相似/未知地址路由结果正确;pending Profile 不读附件。
2. QBD sender 与标题/Sheet/表头不一致时不产生确定性 facts。
3. 只选真实橙色,明确排除任意其他非白填充。
4. synthetic BEFORE/AFTER 的日期、nights、房型、早餐、quantity、price 与 span 完整;facts 只含 AFTER。
5. mixed item strike 按 item 投影;两条未划线明细不合并并产生 merge review。
6. explicit/default quantity、订单级 4/5 间 `FIT/GROUP` 边界与 `8.5→850`、`1.2→1200`、缺价格 MISSING 的状态和 rule ref 正确;QBD 渠道名不构成 Group 例外,多活动 segment 在业务合并前不得提前分类。
7. GROUP IN、跨月、CXL/H anchor、日期冲突正确;F 空/非空得到相同业务 observation/facts,且不产生 F issue/review。
8. 相同 bytes/meta 重放得到完全相同 row/segment/item/field/fact/evidence IDs 与 observation hash。
9. 旧 ParsedFactSet JSON 可反序列化,新 JSON snake_case 且 observations 随 artifact 重放;Layer 3 JSON 使用 `source_room_type`,Booking Agent 安全副本不含 raw observations。
10. opt-in 真实样本得到 August 8 + July 1 row units、合计 16 segments;仓库无真实 XLSX/原文。
11. 价格 MISSING、单非法 token UNRESOLVED、同权多值 CONFLICT、未冻结位置 REVIEW_ONLY 分别有 fixture;F 缺表头/有任意值均完全忽略。
12. focused tests、适度全量回归和 `git diff --check` 通过;若并行工作树已有失败,必须按文件/测试归属明确区分。
## 10. 需求追踪表
| 需求项 | 后端状态 | 测试状态 | 文档位置 | 当前状态 |
| --- | --- | --- | --- | --- |
| sender registry / pending fail-closed | Implemented | Passed | 本 Change Request | Complete |
| additive parser observations | Implemented | Passed | contracts-v1 + 本 Change Request | Complete |
| stable field/index/JCS hash | Implemented | Passed | 共用契约 + shared DTO tests | Complete |
| QBD workbook/row/rich-text Parser | Implemented | Passed | 本 Change Request | Complete |
| AFTER/CURRENT facts projector | Implemented | Passed | QBD projector fixtures | Complete |
| effective dependency resolver | Implemented | Passed | GROUP IN/CXL/cross-month fixtures | Complete |
| synthetic + opt-in real samples | Implemented | Passed | tests + 本 Change Request | Complete |
| 项目状态与兜底任务同步 | In Progress | N/A | Project State / 共用契约 | In Progress |
## 11. 预计修改文件
- `BookingContracts.java`:additive observation/fact link DTO;
- `FixedChannelObservationContracts.java`、`FixedChannelObservationCanonicalizer.java`、`FixedChannelObservationIndex.java`:Parser/Recovery 共用身份、JCS 与 registry;
- `FixedChannelProfileResolver.java`:sender registry 与状态;
- `QbdDeterministicWorkbookParser.java`:QBD 独立纯解析组件;
- `QbdStayDateRules.java`、`QbdEffectiveDependencyResolver.java`、`QbdCurrentFactProjector.java`:同版日期依赖与 current facts 纯投影;
- `BookingContractAssembler.java`:QBD 路由与 C03 接线;
- 对应 Contracts/Profile/QBD/Assembler tests;
- 本 Change Request、contracts-v1 additive 说明、Project State 和文档索引。
不修改前端、数据库 migration、Rate/Room 目录实现、外部 Agent adapter、PMS/OHIP 或用户并行任务文件。