Complete the selective V2 checkpoint with its minimal AgentBus, object-storage, replay persistence, and validated-workbench shared dependency closure.
17 KiB
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` |
两份样本合计 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 命中后,附件仍必须通过以下模板验证:
- 当前材料中存在可读取的
.xlsx; - 唯一业务 Sheet 名符合
2026年<月>月 | <英文月>_<两位年>,兼容半角/全角竖线; - A1 标题同时包含
QBD、月份年份和WYNDHAM JOMTIEN PATTAYA; - 前 10 行只有一个表头行能唯一绑定 C/E/H/J 语义;
- 必需列为 Tour Code、Raw Hotel Detail、操作备注、GROUP IN;F 无论表头或值是否存在均完全忽略,不参与模板门禁、observation、issue、Recovery、Rate Code 或 nights;
- 损坏、加密、超限、多 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;缺失时 deterministic1,规则引用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继续可用。核心结构为:
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 唯一权威来源价格,legacyrate_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. 验收标准
- 三个 sender 精确、大小写规范化和相似/未知地址路由结果正确;pending Profile 不读附件。
- QBD sender 与标题/Sheet/表头不一致时不产生确定性 facts。
- 只选真实橙色,明确排除任意其他非白填充。
- synthetic BEFORE/AFTER 的日期、nights、房型、早餐、quantity、price 与 span 完整;facts 只含 AFTER。
- mixed item strike 按 item 投影;两条未划线明细不合并并产生 merge review。
- explicit/default quantity、订单级 4/5 间
FIT/GROUP边界与8.5→850、1.2→1200、缺价格 MISSING 的状态和 rule ref 正确;QBD 渠道名不构成 Group 例外,多活动 segment 在业务合并前不得提前分类。 - GROUP IN、跨月、CXL/H anchor、日期冲突正确;F 空/非空得到相同业务 observation/facts,且不产生 F issue/review。
- 相同 bytes/meta 重放得到完全相同 row/segment/item/field/fact/evidence IDs 与 observation hash。
- 旧 ParsedFactSet JSON 可反序列化,新 JSON snake_case 且 observations 随 artifact 重放;Layer 3 JSON 使用
source_room_type,Booking Agent 安全副本不含 raw observations。 - opt-in 真实样本得到 August 8 + July 1 row units、合计 16 segments;仓库无真实 XLSX/原文。
- 价格 MISSING、单非法 token UNRESOLVED、同权多值 CONFLICT、未冻结位置 REVIEW_ONLY 分别有 fixture;F 缺表头/有任意值均完全忽略。
- 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 或用户并行任务文件。