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

17 KiB
Raw Blame History

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 命中后,附件仍必须通过以下模板验证:

  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继续可用。核心结构为:

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 或用户并行任务文件。