Files
th-hotel-simple/docs/project/requirements/M012-qbd-liantai-parser-output-contract-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

371 lines
15 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 / QBD+LianTai Parser 输出冻结契约 v1
| 项 | 内容 |
|---|---|
| 状态 | **FROZEN — USER CONFIRMED;PARSER IMPLEMENTED;REAL-SAMPLE CORRECTIONS ACCEPTED** |
| 日期 | 2026-08-11 |
| 契约名称 | `fixed-channel-parser-fact-material-v1` |
| 适用渠道 | QBD、LianTai |
| 不在范围 | 其他 sender、Parsing Agent 完整输出 schema、Rate/Room、TaskCard、PMS/Opera |
| 变更规则 | 冻结后只能通过新 ADR 和新 contract version 修改 |
## 1. 给业务人员看的结论
Parser 每次处理一份工作簿,只交付:
1. 本次处理是否正常完成;
2. 程序已经确认的业务信息;
3. 程序看到了、应该继续处理、但无法确认的原文。
程序不再输出“缺失、未解决、歧义、冲突、原因代码”等分类。
## 2. 顶层输出
```json
{
"meta": {},
"status": "SUCCESS",
"facts": [],
"agent_materials": []
}
```
| 字段 | 必填 | 业务含义 |
|---|---:|---|
| `meta` | 是 | 来源文件、渠道 Profile 和版本等追踪信息 |
| `status` | 是 | `SUCCESS` 或 `FAILED` |
| `facts` | 是 | 已经形成唯一确定值的事实,允许空数组 |
| `agent_materials` | 是 | 需要继续处理的原始材料,允许空数组 |
业务结果只有 `facts` 和 `agent_materials` 两类。`meta/status` 只是运行外壳。
## 3. `meta` 冻结字段
```json
{
"parser_result_id": "pr_xxx",
"source_message_id": "msg_xxx",
"attachment_id": "att_xxx",
"attachment_content_hash": "sha256:...",
"file_name": "QBD月表.xlsx",
"profile_code": "QBD",
"contract_version": "fixed-channel-parser-fact-material-v1",
"parser_version": "1.0.0",
"profile_version": "1.0.0",
"rule_bundle_version": "1.0.0"
}
```
`profile_code` v1 只允许:
```text
QBD
LIANTAI
```
文件名只用于描述和审计,不参与渠道业务判断。
## 4. `facts[]` 冻结字段
```json
{
"fact_id": "fact_xxx",
"fact_key": "target_1/room_1/SOURCE_ROOM_NAME/CURRENT",
"target_ref": {
"target_id": "target_1",
"row_unit_id": "row_12",
"segment_id": "segment_1",
"room_item_id": "room_1"
},
"semantic_role": "SOURCE_ROOM_NAME",
"material_state": "CURRENT",
"value_type": "STRING",
"raw_value": "ONE-BEDROOM-SUITE TWN BF(Q10)",
"normalized_value": "ONE-BEDROOM-SUITE TWN BF(Q10)",
"source_locations": [],
"rule_ref": "shared-source-room-name-v1"
}
```
| 字段 | 必填 | 说明 |
|---|---:|---|
| `fact_id` | 是 | 本条事实的稳定唯一 ID |
| `fact_key` | 是 | 合并时识别同一事实槽位的稳定键;由 target、role、state 构成 |
| `target_ref` | 是 | 事实属于哪一业务行、住宿段和房型项;不适用的下级 ID 为 `null` |
| `semantic_role` | 是 | 本契约允许的业务事实类型 |
| `material_state` | 是 | `CURRENT`、`BEFORE` 或 `AFTER`;fact 不允许 `MIXED` |
| `value_type` | 是 | `STRING / INTEGER / DECIMAL / DATE / BOOLEAN / JSON` |
| `raw_value` | 否 | 来源中的原始值;确定性默认值允许为 `null` |
| `normalized_value` | 是 | Parser 依据冻结规则形成的唯一规范值 |
| `source_locations` | 是 | 一个或多个可复核来源位置,不允许空 |
| `rule_ref` | 是 | 形成该事实的确定性规则引用;不是失败原因代码 |
`facts` 内不存在独立的状态、缺失标志或 reason code。fact 存在本身就表示该值已确定。
## 5. 冻结的十类业务事实
| `semantic_role` | 给业务人员的含义 | v1 规则摘要 |
|---|---|---|
| `GROUP_CODE` | 团号/预订目标代码 | 统一承接 Tour Code、Group Code 等已确认来源别名 |
| `SOURCE_ACTION_DATE` | 来源动作指令日期 | 只表示表格指令写出的日期,不是处理时间或入住日期 |
| `SOURCE_ACTION_KIND` | 来源动作类型 | 规范值只允许 `NEW_BOOKING / UPDATE_BOOKING / CANCEL_BOOKING`;原始细分文字保存在 `raw_value` |
| `ARRIVAL_DATE` | 入住日期 | 必须由当前材料和 Profile 日期规则唯一确定 |
| `DEPARTURE_DATE` | 离店日期 | 必须由当前材料和 Profile 日期规则唯一确定 |
| `NIGHTS` | 晚数 | 入住、离店均确定后按日期差确定;不跨住宿段累加 |
| `SOURCE_ROOM_NAME` | 完整来源房型名称 | 保留 BF、Q10、FAM/PAX、床型、景观等来源含义 |
| `BREAKFAST_INCLUDED` | 来源明确声明的早餐条件 | 独立 BF=`true`;明确 RO/不含早=`false`;未声明时不生成该事实 |
| `ROOM_QUANTITY` | 房间数量 | 明写数量按原文;Profile 已冻结的“缺数量默认 1”可生成事实并保留 rule ref |
| `SOURCE_PRICE` | 来源价格 | 只有来源价格存在且能按冻结规则唯一规范化时生成 |
以下内容不属于 Parser v1 公共事实:
```text
GROUP_IN
SOURCE_RATE_CODE
ROOM_QUALIFIERS
BOOKING_TYPE
canonical Room / Rate Code
Allotment source/actual relationship
最终业务事件
Trace / 普通备注的最终业务处置
TaskCard / 部门 / PMS或Opera执行状态
```
## 6. 完整来源房型名称、BF 与 Q10
### 6.1 不拆 Q10
原文:
```text
ONE-BEDROOM-SUITE TWN BF(Q10)
```
正确输出:
```text
SOURCE_ROOM_NAME = ONE-BEDROOM-SUITE TWN BF(Q10)
BREAKFAST_INCLUDED = true
```
错误输出:
```text
SOURCE_ROOM_NAME = ONE-BEDROOM-SUITE TWN
ROOM_QUALIFIERS = Q10
```
### 6.2 允许的规范化
`raw_value` 保留准确来源文本。`normalized_value` 可以统一大小写、重复空格和无业务含义的连字符差异,
但不能删除或拆分 `BF`、`Q10`、ONE/TWO-BEDROOM、DBL/TWN/TRP、FAM/PAX、U、GARDEN/POOL/VIEW
等业务区分项。
若数量或价格与房型名称紧邻,只有在 Profile 规则能唯一证明它们分别属于数量/价格时才允许拆出;
拆出后仍不得破坏完整来源房型名称中的 BF/Q10 语义。
### 6.3 早餐
- 出现独立 `BF` 标记:`BREAKFAST_INCLUDED = true`。
- 出现明确 `RO` 或“不含早”:`BREAKFAST_INCLUDED = false`。
- 两者均未出现:不生成 `BREAKFAST_INCLUDED`,不得把未声明解释为不含早。
- `Q10` 与早餐判断无关。
- 输出早餐布尔值时,不能从 `SOURCE_ROOM_NAME` 中删除 `BF`。
- 公共字段结构仍为 v1;本次语义通过 `shared-breakfast-explicit-token-v2` 规则引用版本化,不新增字段。
## 7. `agent_materials[]` 冻结字段
```json
{
"material_id": "material_xxx",
"target_ref": {
"target_id": "target_1",
"row_unit_id": "row_12",
"segment_id": "segment_1",
"room_item_id": "room_1"
},
"expected_fact_keys": [
"target_1/room_1/SOURCE_PRICE/CURRENT"
],
"material_state": "CURRENT",
"material_kind": "WORKBOOK_TEXT_SPAN",
"raw_content": "【U-DBL1.2/1.3】",
"context": {
"header": "房型及价格",
"neighboring_cells": ["2间", "含早"]
},
"source_locations": []
}
```
| 字段 | 必填 | 说明 |
|---|---:|---|
| `material_id` | 是 | 待处理材料的稳定唯一 ID |
| `target_ref` | 否 | 能安全定位所属目标时填写;不能猜测 |
| `expected_fact_keys` | 是 | 已知待解决事实槽位时填写;整表兜底时允许空数组 |
| `material_state` | 是 | `CURRENT / BEFORE / AFTER / MIXED` |
| `material_kind` | 是 | `ATTACHMENT / SHEET / ROW / CELL / TEXT_SPAN` |
| `raw_content` | 是 | 原文、结构化单元格内容或受控附件引用 |
| `context` | 是 | Agent 理解该材料所需的最小表头、相邻值和格式上下文;允许空对象 |
| `source_locations` | 是 | 一个或多个准确来源位置,不允许空 |
`agent_materials` 不包含:
```text
MISSING
UNRESOLVED
AMBIGUOUS
CONFLICT
reason_code
```
每个 material 应尽量对应一个待决事实问题;可以带更宽的只读上下文,但不能成为无边界原文垃圾桶。
## 8. 共用来源位置
```json
{
"attachment_id": "att_xxx",
"sheet_name": "2026.08",
"row_number": 12,
"cell_ref": "F12",
"text_start": 0,
"text_end": 15,
"formatting_signature": "fill:#FFFF00",
"content_hash": "sha256:..."
}
```
不适用的细粒度位置字段可为 `null`,但 attachment ID、content hash 和能够达到的最精确位置必须存在。
不得在结果中嵌入附件 URL、Secret 或附件二进制/Base64。
## 9. 唯一判断规则
Parser 按事实槽位判断,不按整行或整格一刀切:
1. 唯一规范值成立:输出 fact。
2. 有原文但不能唯一确定:不输出该 fact;输出 agent material。
3. 来源中完全没有该内容:fact/material 都不输出。
4. 同槽位多个来源给出相同规范值:输出一个 fact,并保留全部来源位置。
5. 同槽位存在不同规范值:不选择任何一个作为 Parser fact;保留全部相关原文为 material。
例如 `【U-DBL1.2/1.3】2`:若房型和数量可以唯一确认,Parser 输出
`SOURCE_ROOM_NAME = U-DBL` 与 `ROOM_QUANTITY = 2`;价格原文进入 material,不生成 `SOURCE_PRICE`。
## 10. SUCCESS / FAILED
### SUCCESS
表示 Parser 正常结束且结果可信,不表示“所有字段齐全”。以下均合法:
```text
facts有值,agent_materials为空
facts有值,agent_materials有值
facts为空,agent_materials有值
facts为空,agent_materials为空(工作簿中没有本期业务行)
```
能安全读取但不支持的版式、多个候选业务表或无法唯一识别的业务区域,使用 `SUCCESS` 并把必要的
Sheet/Row/Attachment 材料交给 Agent,不改成歧义状态。
### FAILED
只用于结果无法被信任的技术失败,例如:
- 文件损坏、加密或无法安全读取;
- 文件大小/安全限制;
- Parser 未处理异常;
- 结果身份、来源定位或序列化契约无法成立。
`FAILED` 时 `facts` 必须为空,不发布部分事实。原始附件仍由外层 SourceMessage/Attachment 记录保留,
由外层流程进入失败复核或受控兜底;Parser 不伪造材料内容。
## 11. 合并结果边界
本契约同时冻结合并结果的两类顶层业务内容:
```json
{
"effective_facts": [],
"review_items": []
}
```
- Parser facts 不可被 Agent 覆盖。
- Agent 只能基于获准的 agent material 补充候选事实。
- 同一事实键同值可以合并来源;不同值不得静默覆盖,进入 review。
- 每个 agent material 最终必须被接受事实覆盖,或进入 review,不能静默消失。
- 必需事实完全缺失时,由合并后的完整性校验产生 review,不由 Parser 伪造材料。
Parsing Agent 自身的完整输入/输出字段和 review item 的展示分类不在本 Parser 契约中冻结,后续单独确认。
## 12. Core 与 Profile 边界
### 共享 Parser Core
- 输出结构、版本和运行状态;
- fact/material 稳定 ID、事实键和 target 结构;
- “唯一值才形成事实”的门禁;
- 来源位置与上下文封装;
- 结果 schema 校验。
Core 提供一个只供 Profile 使用的原始材料入口。Profile 交入业务行、整段原文、上下文和证据位置,Core
统一生成正式 `agent_materials`。备注类材料固定为 `CURRENT`,`expected_fact_keys=[]`,上下文固定包含
`material_purpose=USER_REMARK`、`source_column`,能取得表头时还包含 `source_header`。Core 必须验证材料
引用的业务行、来源消息、附件、Sheet、行号和单元格都与已登记证据一致;任一不一致时 Parser 整体
`FAILED`,不发布部分结果。
### QBD / LianTai Profile
- 工作簿、Sheet、表头和业务行识别;
- 渠道列位、底色、rich text 和模板噪音;
- 日期、来源动作、房型、数量和价格语法;
- Profile 确定性默认规则。
Profile 允许内部临时拆分 BF、Q10、价格 token 或 QBD `GROUP IN` 单元格,但公共输出必须重新符合本契约;
内部字段不得泄漏成第十一类公共事实。
QBD Profile 补充规则:
- `WAITING 等待确认 / อักษรดำ` 是可选备注列,按表头文字识别,不固定为 K 列;只在核心表头行及其紧邻
上一行寻找。没有该列不影响旧模板;同一表头带出现多个候选时不得猜测,走既有附件级兜底材料;
- WAITING 不参与业务行选中。只对已经选中的有色业务行读取非空单元格,每格整段原文形成一条备注材料;
空格不生成任何输出,`2300 THB` 等内容不得拆数字、解释为价格或生成 `SOURCE_PRICE`;
- 每个物理明细行先按完整“房型+数量”块判定状态。所有块均划线时,该行日期、晚数、房型、数量都属于
`BEFORE`;所有块均未划线且同一业务行已有修改前内容时都属于 `AFTER`;没有修改前内容时仍为
`CURRENT`。不同房型块状态不一致或单块内部混合时保持 `MIXED`,不强行生成整行日期事实;没有房型块时
沿用整行格式判断。
LianTai Profile 补充规则:
- G、I 列完整原文及 D 列完成确定性字段提取后的残余原文,均通过上述共享入口形成备注材料;
- G 列已经唯一形成的 `SOURCE_ACTION_DATE`、`SOURCE_ACTION_KIND` facts 继续保留,完整 G 原文可以同时
作为补充材料保留;旧内部 `classification_status`、reason code 等状态不得进入公共结果。
## 13. 迁移与验收门禁
本契约冻结时,现有代码仍使用旧 `FieldObservation`、多状态和 Recovery Patch 模型。2026-08-11 已完成
Parser-only 迁移切片,实施状态如下:
1. 已以本契约替换 QBD、LianTai 和共享入口的默认 `parse` 公共 DTO;
2. 目标输出已移除旧公共状态、reason code、`GROUP_IN`、`SOURCE_RATE_CODE`、`ROOM_QUALIFIERS`;
3. 完整来源房型名称与 BF/Q10 规则已进入双渠道共享契约测试;
4. 十类 facts、部分确定、完全缺失、材料冲突、SUCCESS/FAILED 已有契约测试;
5. 真实样本缺口已修正并回放通过:QBD 为 114 facts、4 条 WAITING materials,rows 118/121 的
BEFORE/AFTER 日期、晚数、房型和数量完整;LianTai AI 样板为 70 facts、6 materials,其中 G 列 5 条、
I 列 1 条。公共 DTO 与十类 fact 目录未改变;
6. Parsing Agent v1.0 已由独立 ADR-009 冻结;CP4 已接通默认关闭的 runtime/merge/fake链。旧
observation/Recovery只保留在显式 `parseLegacy` 兼容桥,尚未删除;
7. 本次没有启用 Provider、AgentBus Recovery、PMS、Opera 或生产功能。
## 14. 权威性
对 QBD/LianTai Parser 公共输出,本文件与 ADR-008 的优先级高于:
- ADR-001 的字段 Recovery 公共边界;
- ADR-007 的旧 observation/Field Recovery 兼容描述;
- `C03-v2` Draft 的多状态 contribution core;
- 旧 QBD/LianTai Parser CR 中的 observation status、reason code 和内部拆分字段。
历史文档继续保留用于说明当前代码和迁移来源,不得再作为新 Parser 输出的实现依据。