Complete the selective V2 checkpoint with its minimal AgentBus, object-storage, replay persistence, and validated-workbench shared dependency closure.
247 lines
17 KiB
Markdown
247 lines
17 KiB
Markdown
# 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 或用户并行任务文件。
|