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.
This commit is contained in:
鲨鱼辣椒 committed 2026-08-20 17:09:00 +08:00
1 parent 7330ac853b
commit 694c4317a3
627 files changed
+130467 -885

No files matched your search

@@ -0,0 +1,246 @@
# 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 或用户并行任务文件。