From 3ae3fea6bc23470f533b5168c70ce851b87ec529 Mon Sep 17 00:00:00 2001 From: andy Date: Sat, 11 Jul 2026 22:23:19 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=20M002=20V3=20CP6=20P0=20fix?= =?UTF-8?q?tures=20=E5=9B=9E=E5=BD=92=E5=9F=BA=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../任务卡前端字段变更与路由说明_3.0_to_current.md | 306 ++++++++ ...务卡前端字段变更说明_3.0_to_当前版_2026-07-10.xlsx | Bin 0 -> 51947 bytes .../02_AI_Runtime/booking-desk-event.skill | Bin 0 -> 59733 bytes .../02_AI_Runtime/main_agent_prompt.md | 320 +++++++++ .../核心契约_展开阅读/00-output-contract.md | 416 +++++++++++ .../核心契约_展开阅读/02-event-routing-map.md | 82 +++ .../03-current-content-completeness.md | 121 ++++ .../核心契约_展开阅读/04-main-skill-input-contract.md | 144 ++++ .../核心契约_展开阅读/90-manual-review.md | 158 ++++ .../03_P0_Acceptance/fixtures/allotment_scope.json | 145 ++++ .../03_P0_Acceptance/fixtures/candidate_gate.json | 226 ++++++ .../03_P0_Acceptance/fixtures/main_outcomes.json | 175 +++++ .../fixtures/manual_review_resolution.json | 153 ++++ .../fixtures/parent_split_two_children.json | 125 ++++ .../fixtures/row_multiple_derived.json | 146 ++++ .../fixtures/source_identity_errors.json | 106 +++ .../03_P0_Acceptance/validate_p0_contracts.mjs | 674 ++++++++++++++++++ .../prompt_skill_p1_p2_reassessment_2026-07-11.md | 441 ++++++++++++ .../CHECKSUMS.sha256 | 19 + .../README_交付说明.md | 100 +++ docs/project/frontend-backend/README.md | 11 +- .../backend-to-frontend-notes.md | 3 +- .../frontend-to-backend-api-requests.md | 5 +- docs/project/go-live-notes.md | 2 +- .../integrations/superagent-api-contract.md | 23 +- .../M002-order-task-workflow-v3.md | 17 +- ...002-superagent-task-result-api-contract.md | 6 +- ...esultInfrastructureInputErrorResponse.java | 19 + .../SuperAgentTaskResultControllerAdvice.java | 11 +- .../ReservationAiTaskIntakeServiceImpl.java | 109 ++- .../ReservationTaskWorkflowServiceImpl.java | 48 +- .../SuperAgentTaskResultControllerTest.java | 105 +-- ...gentTaskResultP0FixtureRegressionTest.java | 459 ++++++++++++ 33 files changed, 4600 insertions(+), 75 deletions(-) create mode 100644 docs/import/20260711/开发交付_P0冻结基线_2026-07-11/01_Adapter_Frontend/任务卡前端字段变更与路由说明_3.0_to_current.md create mode 100644 docs/import/20260711/开发交付_P0冻结基线_2026-07-11/01_Adapter_Frontend/任务卡前端字段变更说明_3.0_to_当前版_2026-07-10.xlsx create mode 100644 docs/import/20260711/开发交付_P0冻结基线_2026-07-11/02_AI_Runtime/booking-desk-event.skill create mode 100644 docs/import/20260711/开发交付_P0冻结基线_2026-07-11/02_AI_Runtime/main_agent_prompt.md create mode 100644 docs/import/20260711/开发交付_P0冻结基线_2026-07-11/02_AI_Runtime/核心契约_展开阅读/00-output-contract.md create mode 100644 docs/import/20260711/开发交付_P0冻结基线_2026-07-11/02_AI_Runtime/核心契约_展开阅读/02-event-routing-map.md create mode 100644 docs/import/20260711/开发交付_P0冻结基线_2026-07-11/02_AI_Runtime/核心契约_展开阅读/03-current-content-completeness.md create mode 100644 docs/import/20260711/开发交付_P0冻结基线_2026-07-11/02_AI_Runtime/核心契约_展开阅读/04-main-skill-input-contract.md create mode 100644 docs/import/20260711/开发交付_P0冻结基线_2026-07-11/02_AI_Runtime/核心契约_展开阅读/90-manual-review.md create mode 100644 docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/allotment_scope.json create mode 100644 docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/candidate_gate.json create mode 100644 docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/main_outcomes.json create mode 100644 docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/manual_review_resolution.json create mode 100644 docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/parent_split_two_children.json create mode 100644 docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/row_multiple_derived.json create mode 100644 docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/source_identity_errors.json create mode 100644 docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/validate_p0_contracts.mjs create mode 100644 docs/import/20260711/开发交付_P0冻结基线_2026-07-11/04_Known_Issues_非契约/prompt_skill_p1_p2_reassessment_2026-07-11.md create mode 100644 docs/import/20260711/开发交付_P0冻结基线_2026-07-11/CHECKSUMS.sha256 create mode 100644 docs/import/20260711/开发交付_P0冻结基线_2026-07-11/README_交付说明.md create mode 100644 server/src/main/java/cn/nianxx/thhotel/integrations/ai/superagent/common/result/SuperAgentTaskResultInfrastructureInputErrorResponse.java create mode 100644 server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultP0FixtureRegressionTest.java diff --git a/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/01_Adapter_Frontend/任务卡前端字段变更与路由说明_3.0_to_current.md b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/01_Adapter_Frontend/任务卡前端字段变更与路由说明_3.0_to_current.md new file mode 100644 index 0000000..d27a112 --- /dev/null +++ b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/01_Adapter_Frontend/任务卡前端字段变更与路由说明_3.0_to_current.md @@ -0,0 +1,306 @@ +# 任务卡前端字段变更与路由说明(3.0 → 当前版) + +> 对比基线:`任务卡前端展示字段表 3.0.xlsx` +> P0 契约基线:2026-07-11 00:32:56(Asia/Shanghai) +> `booking-desk-event.skill` SHA-256:`64063a6d24c2e70bb0d9d557923fdbecb9e133c1ea5971f3858df03469e48693` +> `归档.zip` SHA-256:`ebf3806cbdf1605d3a023fecff76654ba228c4d5eaf1cf68b609633bb07520cb` + +> 文档定位:本文是生产 Prompt、Skill 和 references 的派生开发交接件,不是覆盖上游的第二份规则源。如本文与生产契约不一致,adapter 必须 fail closed,停止使用冲突路由并修复交接件;不得声称本文自动覆盖 Prompt/Skill。 + +## 1. 本次 P0 收口结论 + +1. Gateway 在调用 Main Agent 前校验 `source_message.source_message_id`;缺失时只返回基础设施错误,不读正文、附件、OCR、历史或系统上下文,不调用 Skill,不建卡或通知。 +2. `S10` 和 `S99` 是两份不同的完整根契约,使用 `route_code` 判别;S10 的 `manual_review=null`,S99 必须带完整 `main_agent_entry_review`。 +3. Main 只产生粗分类 `candidate_events[]`;Skill 决定最终 event type、subtype、目标拆分和派生事件。 +4. Adapter 按每个 `message_events[i]` 派生三元组,不在邮件根对整封邮件只计算一次。 +5. 业务类型和 subtype 已知时,人工复核保留原业务 `task_type + task_subtype`;只有类型或 subtype 本身不可确定时才走 Fallback。 +6. 房型不唯一时保留 `room_type_raw`、置 `pms_room_type_code=null`,并要求用户在当前有效 PMS 房型目录中选择。Parent split 不再存在 generic `SUITE` 普通候选例外。 +7. Parent split 固定产生 N 个 Child Group New Booking 和 1 个 Parent Group Cancel Booking 候选;Parent 始终需要下游硬校验和 Preflight。 +8. 不再支持部分配额维护、部分释放或保留余量继续使用;独立新建 Allotment/Control Block 和明确整块取消仍受支持。 +9. “一行一事件”改为行级隔离:每个当前有效行独立处理,不跨行合并;Parent Cancel、linked Trace、TA Recorder 等明确派生事件可以额外产生。 +10. 复核在同一张业务卡上解阻;不新建第二张 linked normal task,不改写原 Agent payload。 + +## 2. 根结果分流 + +### 2.1 基础设施输入错误 + +`source_message.source_message_id` 为 `null`、空字符串或纯空白时,Gateway 只返回: + +```json +{ + "result_type": "infrastructure_input_error", + "error_code": "missing_source_message_id", + "retryable": true, + "missing_fields": [ + "source_message.source_message_id" + ] +} +``` + +该结果不是业务根、S10 或 S99,也不计入下文 42 条 adapter 路由。Adapter 应进入重新取数/重试流程;Main 若意外收到此类输入,使用同一结构作为防御性终态。 + +### 2.2 S10 和 S99 + +| `route_code` | 固定 `agent_assessment.status` | `manual_review` | Adapter 三元组 | 去向 | +| --- | --- | --- | --- | --- | +| `S10` | `no_booking_action_detected` | `null` | `source_message_review_notification + Message Notification + S10` | 展示源邮件,0 张业务卡 | +| `S99` | `material_package_unavailable` | 完整 `main_agent_entry_review` | `source_message_review_notification + Message Notification + S99` | 展示源邮件和入口复核信息,0 张业务卡 | + +两者的 `result_type` 都是 `source_message_review_notification`,所以必须使用 `route_code` 作为结构判别字段。不得接受 `S10 | S99`、联合 status 或 S99 空复核对象。 + +### 2.3 业务根 + +业务根固定包含: + +```json +{ + "source_message": {}, + "message_events": [], + "case_candidates": [], + "extraction_warnings": [], + "unhandled_current_intents": [] +} +``` + +Agent 不在邮件根输出业务 `result_type`、`task_type` 或 `task_subtype`,因为同一封邮件可同时存在 normal 和 manual-review sibling events。 + +## 3. Main → Skill 输入契约 + +`candidate_events[]` 是粗分类信号,不是最终业务事件。每项固定使用: + +```json +{ + "candidate_id": "C1", + "classification_status": "coarse_supported_signal", + "possible_event_types": [ + "Voucher Received", + "Payment Evidence" + ], + "evidence_refs": [ + { + "source": "current_attachments", + "index": 0 + } + ], + "target_hints": [], + "uncertainty_codes": [ + "document_subtype_unresolved" + ] +} +``` + +- `possible_event_types` 只允许 active event;禁止 S10、S99、Need Manual Review、Note 和 Allotment Maintenance。 +- Skill 在 `context_used.main_candidate_ids[]` 记录所使用的粗候选 ID。 +- Skill 否定全部粗候选时返回内部 `no_supported_event` disposition,由 Main 转成 S10;不伪装成人工复核或契约错误。 +- candidate 结构非法时 fail closed 为内部输入契约错误,不进入业务路由。 + +## 4. 42 条明确 Adapter 路由 + +### 4.1 19 个业务 subtype 的 normal/manual-review 成对路由 + +对下表每一行: + +- `event.manual_review=null` 时使用“普通三元组”; +- `event.manual_review` 是完整 `business_event_review` 时,使用“复核三元组”; +- 复核路由仅更改 `result_type`,保留同一业务 task type/subtype。 + +| # | Agent 判别 | 普通三元组 | 复核三元组 | +| ---: | --- | --- | --- | +| 1 | `New Booking` + `booking_object_type=FIT Reservation` | `normal_task + New Booking + new_fit_reservation` | `manual_review + New Booking + new_fit_reservation` | +| 2 | `New Booking` + `booking_object_type=Group Block` | `normal_task + New Booking + new_group_block` | `manual_review + New Booking + new_group_block` | +| 3 | `New Booking` + `booking_object_type=Allotment / Control Block` | `normal_task + New Booking + new_allotment_control_block` | `manual_review + New Booking + new_allotment_control_block` | +| 4 | `Update Booking / Amendment` | `normal_task + Update Booking + update_booking_amendment` | `manual_review + Update Booking + update_booking_amendment` | +| 5 | `Cancel Booking` + `cancel_object_type=fit_reservation` | `normal_task + Cancel Booking + cancel_fit_reservation` | `manual_review + Cancel Booking + cancel_fit_reservation` | +| 6 | `Cancel Booking` + `cancel_object_type=group_block` | `normal_task + Cancel Booking + cancel_group_block` | `manual_review + Cancel Booking + cancel_group_block` | +| 7 | `Cancel Booking` + `relationship_type=linked_parent_release_after_child_split` | `normal_task + Cancel Booking + linked_parent_release_after_child_split` | `manual_review + Cancel Booking + linked_parent_release_after_child_split` | +| 8 | `Cancel Allotment` | `normal_task + Cancel Allotment + cancel_allotment_control_block` | `manual_review + Cancel Allotment + cancel_allotment_control_block` | +| 9 | `Voucher Received` | `normal_task + Voucher Received + lian_tai_credit_voucher` | `manual_review + Voucher Received + lian_tai_credit_voucher` | +| 10 | `Payment Evidence` | `normal_task + Payment Evidence + payment_evidence` | `manual_review + Payment Evidence + payment_evidence` | +| 11 | `Rooming List` | `normal_task + Rooming List + rooming_list` | `manual_review + Rooming List + rooming_list` | +| 12 | `AMEND GROUP CODE` | `normal_task + Amend Group Code + amend_group_code` | `manual_review + Amend Group Code + amend_group_code` | +| 13 | `Trace` + `trace_subtype=extra_bed` | `normal_task + Trace / Reservation Notes + extra_bed` | `manual_review + Trace / Reservation Notes + extra_bed` | +| 14 | `Trace` + `trace_subtype=general_request` | `normal_task + Trace / Reservation Notes + general_request` | `manual_review + Trace / Reservation Notes + general_request` | +| 15 | `TA RECORDER` | `normal_task + TA Recorder + maintain_ta_recorder` | `manual_review + TA Recorder + maintain_ta_recorder` | +| 16 | `Invoice Generation` | `normal_task + Invoice Generation + invoice_generation` | `manual_review + Invoice Generation + invoice_generation` | +| 17 | `Invoice Received` | `normal_task + Invoice Received + invoice_received` | `manual_review + Invoice Received + invoice_received` | +| 18 | `Payment Notice` | `normal_task + Payment Notice + payment_notice` | `manual_review + Payment Notice + payment_notice` | +| 19 | `Manual RateCode` | `normal_task + Manual RateCode + manual_rate_code` | `manual_review + Manual RateCode + manual_rate_code` | + +上表是 19 条 normal + 19 条 type-known manual review,共 38 条。 + +### 4.2 其余 4 条 + +| # | 上游判别 | Adapter 三元组 | 去向 | +| ---: | --- | --- | --- | +| 39 | 根 `route_code=S10` | `source_message_review_notification + Message Notification + S10` | 源邮件通知,不建卡 | +| 40 | 根 `route_code=S99` | `source_message_review_notification + Message Notification + S99` | 源邮件 + 入口复核信息,不建卡 | +| 41 | `event_type=Need Manual Review` | `manual_review + Fallback + business_event_review` | 类型/subtype 未知的业务复核卡 | +| 42 | 每个 `unhandled_current_intents[i]` | `unhandled_current_intent + Unhandled Current Intent + requires_business_approval_or_unsupported_task_card` | 邮件详情展示块,不建卡 | + +`Note` 和 `Allotment Maintenance` 只是 legacy accepted event,各自为 0 条合法三元组。 + +## 5. 人工复核和同卡解阻 + +### 5.1 Agent 输出规则 + +- 业务类型和 subtype 已知:保留原 `event_type` 和 subtype 判别字段,继续输出可安全抽取值,`manual_review` 为完整对象。 +- 只有类型或 subtype 本身无法确定:`event_type=Need Manual Review`,adapter 走 Fallback。 +- `manual_review.missing_fields[]` 每项是指向当前 event 的 RFC 6901 JSON Pointer,例如 `/extracted_fields/room_items/0/pms_room_type_code`。 +- 不新增 `required_user_inputs[]`。Pointer 无法映射到已知可编辑业务字段时,属于 `adapter_contract_error`。 +- 房型必须从信息系统当前有效 PMS 房型目录选择,不得猜测或自由填写 PMS code。 + +### 5.2 信息系统状态 + +初始: + +```json +{ + "review_status": "pending", + "review_resolution": null +} +``` + +用户确认后: + +```json +{ + "review_status": "resolved", + "review_resolution": { + "field_overrides": [ + { + "field_pointer": "/extracted_fields/room_items/0/pms_room_type_code", + "value": "SU1" + } + ], + "resolved_by": "", + "resolved_at": "" + } +} +``` + +实施要求: + +- 原 Agent payload 和初始 `result_type=manual_review` 不可变;页面当前模式由 `review_status` 控制。 +- 用户覆盖值保存在 `review_resolution.field_overrides[]`,不回写 Agent 原值。 +- 所有缺失字段完成、目录校验和依赖字段重验通过后,才进入现有 Preflight。 +- 不创建第二张 linked normal task。 +- 业务字段可编辑;邮件原文、raw evidence、source ID 和审计字段只读。 + +## 6. Parent split、配额和行级隔离 + +### 6.1 Parent split + +1 Parent 拆成 N Children 时: + +- 每个 Child 输出 `New Booking`,`booking_object_type=Group Block`,`case_keys.group_code=`。 +- Parent 额外输出一个 `Cancel Booking`,`cancel_object_type=group_block`,`relationship_type=linked_parent_release_after_child_split`。 +- `child_group_codes[]` 与 `related_source_event_indices[]` 数量和顺序一一对应;每个 index 必须指向同根中对应 Child Group New Booking。 +- 一个 split cluster 只有一个 Parent 候选;Parent 始终 `requires_downstream_hard_validation=true`。 +- 禁止走 Update、Cancel Allotment、Allotment Maintenance 或单个复合事件。 + +某个 Child 房型不唯一时,该 Child 仍是 `New Booking + new_group_block`,但 adapter 派生该卡的 manual-review mode;其他明确 Child 仍走 normal mode。Parent Cancel 候选仍保留并经下游硬校验,不得因一个 Child 歧义丢弃 sibling events。 + +### 6.2 Allotment 范围 + +| 当前意图 | 结果 | +| --- | --- | +| 独立新建 Allotment/Control Block | `New Booking + new_allotment_control_block` | +| 明确整块取消 | `Cancel Allotment + cancel_allotment_control_block` | +| 明确部分日期/房量维护、部分释放或保留余量,且是唯一意图 | Main 输出 S10 | +| 上述不支持意图与 active event 混合 | active events 正常输出,该意图进入 `unhandled_current_intents[]` | +| 无法判断是整块拆分/取消还是部分保留 | 因 type/subtype 未知,走 `Need Manual Review` Fallback | + +当前 Agent 不得生成 `update_allotment_control_block` 或 `Allotment Maintenance`。 + +### 6.3 行级隔离 + +每个当前有效行独立处理,不跨行合并。普通情况下每行、每目标产生一个主事件;Parent Cancel、linked Trace、TA Recorder 等契约明确的 derived event 可以额外产生。测试不再断言“事件数=行数”,而是检查行/目标未跨行合并、主事件不遗漏、必需派生事件完整和关系引用有效。 + +## 7. 3.0 字段和路由迁移 + +### 7.1 必须新增或修改 + +| 作用域 | 字段/行为 | 开发动作 | +| --- | --- | --- | +| Gateway | `infrastructure_input_error` | 在 Main 前校验 source ID;独立重试,禁止建卡/通知 | +| Main/Skill | `candidate_events[]` 粗候选 | 实现 04 输入契约;不把粗候选当最终类型 | +| 业务根 | 五个固定顶层字段 | 按 `message_events[]` 逐项路由;`unhandled_current_intents[]` 只展示 | +| Event | `manual_review: object|null` | 按是否为 null 派生 normal/manual mode;保留业务 subtype | +| Manual review | `missing_fields[]` | 仅接受 RFC 6901 pointer,映射到业务卡编辑控件 | +| Review state | `review_status/review_resolution` | 由信息系统持有,不改写 Agent payload | +| 房型 | `room_type_raw` / `pms_room_type_code` | 分开原文与 PMS code;歧义时 raw 保留、code 为 null | +| 事件关系 | `source_event_index/related_*` | 仅为本次输出内引用,不是 TaskCard/PMS ID | +| Parent split | `child_group_codes[]` + `related_source_event_indices[]` | 强制数量、顺序一一对应,Parent 仅一个 | +| Trace | `trace_items[]` | 同一目标多条补充合并一张 Trace,不按 item 拆卡 | +| Rooming List | 派生 TA Recorder | 每个已确认 target 产生对应关联事件 | +| Update | 多修改项 | subtype 固定 `update_booking_amendment`,修改动作/前后值另存明细 | + +### 7.2 Legacy/禁止新生成 + +| 旧值或旧行为 | 当前处理 | +| --- | --- | +| `manual_review + 各业务 task_type + manual_review` 旧 subtype | 能稳定推导业务 subtype 时归一到本文对应 type-known 复核三元组;不能推导时才走 Fallback | +| `Note` | 仅历史兼容;当前补充信息统一 Trace | +| `Allotment Maintenance` | 仅历史兼容;当前不新生成 | +| `update_allotment_control_block` | 禁止当前生成;不支持部分配额维护 | +| Parent split 当 Update/Allotment Maintenance/Cancel Allotment | 禁止;固定 Child Group New + Parent Group Cancel | +| Parent generic `SUITE` normal 候选例外 | 删除;保留 New Group 卡型但走该卡 manual-review mode | +| “一行一事件”绝对计数 | 删除;改为行级隔离 + 明确派生事件 | + +## 8. Adapter 顺序和 fail-closed + +```text +0. Gateway 先验 source_message_id;缺失时只返基础设施错误。 +1. 根含 route_code 时,严格校验完整 S10 或 S99 结构。 +2. 否则严格校验五字段业务根。 +3. 遍历 message_events[]:先由 event_type/subtype 得到业务卡型,再由 manual_review 是否为 null 得到 mode。 +4. 遍历 unhandled_current_intents[],只产生展示块。 +5. 将候选卡送入现有 Case / Preflight / 去重 / 锁 / 状态校验。 +``` + +下列情况必须 fail closed: + +- candidate 输入结构非法:内部输入契约错误,不进业务路由。 +- S10/S99 联合值、S10 带复核对象、S99 空复核对象:根契约错误。 +- 已知业务路由缺少 subtype 判别字段,或未知/当前禁用 `event_type`:`adapter_contract_error`,该 event 0 卡,siblings 继续。 +- `manual_review` 九字段不完整,或 `missing_fields[]` 不是有效/可映射 JSON Pointer:`adapter_contract_error`。 +- Parent relationship 字段不完整或一一对应失败:拒绝 Parent event,不猜关系。 +- 业务字段未确认、`review_status!=resolved` 或 Preflight 未通过:禁止写 PMS。 + +契约错误不等于业务人工复核,不得用 Fallback 吞掉接口问题。 + +## 9. P0 最低验收 + +| 场景 | 必须结果 | +| --- | --- | +| S10/S99 合法样例 | 各自通过;三种联合/空复核非法结构被拒绝 | +| 图片付款凭证 | Main 粗候选可包含 Voucher/Payment;Skill 最终判型,并记录 candidate ID | +| Skill 否定全部粗候选 | 内部 `no_supported_event` → Main S10,不是 Fallback/契约错误 | +| 1 Parent → 2 Children | 2 Child New Group + 1 Parent Group Cancel;关系数组一一对应 | +| 两个 split clusters | 每个 cluster 恰好一个 Parent,关系不串组 | +| 一个 Child 房型 `SUITE` 不唯一 | 该 Child:`manual_review + New Booking + new_group_block`;sibling Child 仍 normal;Parent 候选仍在 | +| Update 房型歧义 | `manual_review + Update Booking + update_booking_amendment`,保留安全已知字段 | +| Update 只改日期,附带无关房型文字 | 不因无关文字进人工复核 | +| 业务类型/subtype 未知 | `manual_review + Fallback + business_event_review` | +| 部分配额维护单独出现 | S10 | +| 部分配额维护 + 支持事件 | 支持事件照常输出 + `unhandled_current_intents[]` | +| 独立新建/明确整块取消配额 | New Allotment / Cancel Allotment 仍正常 | +| 同卡解阻 | 目录值、覆盖审计、状态转换通过;Preflight 前 0 次 PMS 写入 | +| 缺 source ID,正文为 Cancel/Thanks/see attached | 全部得到同一 infrastructure error;Skill/附件/历史查询调用数都为 0 | + +## 10. 生产源与部署必读 + +权威顺序: + +1. `prompts/main_agent_prompt.md` +2. `skills/booking-desk-event/SKILL.md` +3. `skills/booking-desk-event/references/00-output-contract.md` +4. `skills/booking-desk-event/references/04-main-skill-input-contract.md` +5. `skills/booking-desk-event/references/01-current-history-boundary.md` +6. `skills/booking-desk-event/references/03-current-content-completeness.md` +7. `skills/booking-desk-event/references/02-event-routing-map.md` +8. 各业务 reference,特别是 `10`–`18`、`30`、`31`、`50`–`54` 和 `90` + +`task_plan.md`、`findings.md`、`progress.md`、审计报告和本文都是过程/派生交付件,不是可以反向覆盖生产契约的规则源。 + +部署时必须同步发布 Main Prompt 和 `.skill` 包,并使用本文页首 SHA-256 校验 skill 包。本仓库没有 adapter/frontend 代码;实际实现必须以上述契约和 42 条路由为验收基线。 diff --git a/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/01_Adapter_Frontend/任务卡前端字段变更说明_3.0_to_当前版_2026-07-10.xlsx b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/01_Adapter_Frontend/任务卡前端字段变更说明_3.0_to_当前版_2026-07-10.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..fcaa76057aa23e67575a31d3d24708088de0f4a8 GIT binary patch literal 51947 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%0A4PX z&+322IgZZ=T@rz}+Ajn(e=eXjwet^ZG@oD1&+YvTLAkRED=Q@4h%IDsf#{JxT5Yg) zlJlYaLPN9uHK6IpYo}+llsKOX?t`!#!Y%D3O2q#0R;I zmF_WO;Kb4_yr1ckcvIJH4zrN!j-c%Y?YayrM=`OKU>g=E4}L*9Oz+w0*O>sv?5>?V zHlg|_vRaE<=S5NHRMFwQ-08BO?6`e`yY2DJB`oH6J`bHkscFi0#W*|Lzr~D)P@{q0 zn@0tY5Hnf&1IR3(sjq*c^OS)3Ro_a5+cm#%;UQ3xnYhf3<-pj#|3EwXzCRh1)a>w1Pa{KCa%)6h@H z-4lc)#-1ZTZ=w{8-#ee)jQJQ%fEg{ngKi=Sf)BHZQCWJz&CS0!4{x*tyO4H0HQ-cA zhV%TfhD!+>JsW=Bc$K^LZrz{J%ZWa?lk8fHXeAxBT#5uj=hy4J;eBAtIq|WMbmvCh zB_PfnBt`0BJuwuM?6xE1jl>&>n+DiZuW)tna=J#1f<*m>#iVpTMM1Zo@Ig+RN&!xx zq#WEQKdYR(ot8Z%U>wk6yMQUBx#<yIoxTLKhp_gIG^?vUc$JZG=!>5Ax(Y(N1SqIfz7zEqyZ zr`O9={m@D1xhm>W51Vvfxyh~mQ z;LPF$_||5bLI7gxW|qAQRQtn+T-+W)rj8?QPio>;dA#^1SGdSX!Bsu+J0i#-x)|Qy>0*LRZfwE6)OE@gXv~raJo}AB(jGt zMJ9Ag#rTL%VzcdyRm+*utmLJRJ7YA~A-dX5k}h=6F&tSF06a+~5l`l!tJNjNd{A5u zv}0Y)2f`Rri=_e7Bmo5KBXK^mQC-cr(}LfY%RKW4xDluDN)y#m#0jN{8`0x|H`iy* zES8Tku=CGP#pQ0lA8^zJ{WdVOMFD-5qah1KHHAf3sVY z9}1AsG&j{Y#URgxLs*Yahs~hM{p?A@cFU(_IWC0tiBU?BO?}>;MK-E@=^SU!;GJoM zb}MHZu6I~dxu_rNV#)5zi$~?@RKcPR$W zQA)A#I26Uv5@hVcj3KxJ2+#{lDGY7l29IWIn3SfA@P$5If)fpO&*ZJIxNvXp^wwX- z5Q;N~#j%h;=yqA;s<|B?FDB<~Pu>DrVuER;Bi{&8KD?D#qhZd>*2WPTTBqgP%jRj8 zhq{)^QbDb)u|z;DZp%h)|Cs6O^NSw_$-y*E#oBrlHrzqL$DX-zDNRHtf?C)?ZCU)P zv-u2l8{<^e3rO1qlIp?_d@cuWk+8e*TgRYA1tG4SUxP`m9?78TVI9TAEG-showI_G ztN{%4BRz@C%Waf|k91~dVZ(TKn|2he@sAC`pzCe}@`cU)h6^>$Tqs?xxbA(O3)-9I z$lKYNZtLVe5@bwW-KK+UKuwudPOC{##A0?&7MfM=3I;2EN^iD-K2SnWqx{gPD;7CE z-xVnEEbY=4V>vU)T-JVhK7Rbe6;r`WHvC_dfMvg-xLv0CLYy{u;IFehy1jcI zI8B1RUPv+y^rT~Kv(}7*rp!TA6VotQ#t$>M95yU_HAKTA-ppxER`AZD@<5P)bH$~f^qNH@WRQ90>36|}aC{i?l#B5k9M85qGiwdbNH>j1n>C6M z$N6c16YeCOc0O1rIw|)gZ2h-Ih*VOFK7|IY{Zz^7VPEktISXOm3q{i{O7>qa*OI6( zRwd3=_)(pWEy>IHlh=Y`x*3ag`&&iutG>7f)s*uL$}Q<$IgQvJCuf^1tL~(N&K|%ZtP?`XAQ(Is-2w}jj$2#M5+gf(h4k5rH9DXi zFWmdU+sMk;Q8gy}QhiTZnlg@)4D@hyVgmcPpjs9Lq zS8uy8TX)XwX~9FtNw= zQwd8=4h4R!9gUIJ9@{xWi{R?&TXaRVEwh>8#zx%Y?sHnfa)c$sNi}KrO0qaZ>HEANz0k#+!>x;r2)ACNjr>{q$}6@PnLuv0jq)|QNW_&_aM7yB?eUC+bJl* z%ucN>kP>5k29WQ)SZLWeHrgPO1K!c+lMk%_2B1Ws3(b)~xd%zVYLp-@008%Y1fai6 zzxtDzHI0LXnWZI-nf3pdb$b;{-hBP{{4YrKU#b+=7>f5w&8hxFqB^Zixx!JWRcA)I zv=LpMmYW^>yc*^JNM4=@kw+PgYFI=oWS|uws2|KRrX}%T7F5L4kk14yrz@C7ewp0u(UyjHyI`$^gz zuS=z8fxHqEygXY?=T zO)5OdP#s(?Qw|}Yrx-fCrOK%3f#h`LDCQ>)Pf0C3n#hFmGzg6P=~W>`Ok>~y4gUtk zR@;+i_w^>E*4zV^M(8Y#-0mugmltH5(o?42rWww8mHj3J^LlBnH_)y(teB*%H7u={ z;N)?#HIDIAUXK)c7-h%%_gz@6pW8LS^dJ1`I(I)D^q&!*`vOeI>vv1H0g22qmcduO z=hrGgtAGT;p9EcO<|(Kb%Jd0Gpz!$6=i!No<M~SVbsWX?VxoG*^)9#Wk^)8jhzmj^mi$WsL;@WsZZ-rWTkZ6r^f3m^3X0>df zgGE&`K81U94hOyXlkLzmqm#ySC7s3bhNgS$OAxwG`?#YtGkeYm;vw$-m*9$O5B)SlW)a6QL5>SE_DpLKGSLA*L^BA7ZtjpaSTnMzR)|RDz=*A z&qKc{Lz;TTafMPo+ICg9S{Pv}BPtSdV%${%l zgnPO{CQ`er@=+7mw|#$d&9Ju=S9vEt;i*Bd0nB;J0jbAl;85rne?6xQKTW-`#aZqx zef>S(6k7EIp+CWq%f;9be|iWYCx`^t-`RrIMX%Abs;HadN*-!Oq0g5aEfR zG6_lAQv7RyB7$kY{PK9A1Elkh;ludn6{-I|?Y3-r!k+QmMC))k)DS!gIZbeLD(4~# zQJ(dT)RqDS*J+8LMb~c!Js9RT5b-{2&zlNS2LO- z5+94xkIBZx;VVi~glwM?7kb9qTg}sU3;w-u)@=NA;q2PmyJZz3$}DFkhu0^%O-L=d zd)JFGF>w-Q!XF5Ugxh2Tsk0Y4F0!1~FN3%T_TlB4|;ON&kz>!h0Yu99RjbCKqT`nD3q zxzb*<)3pHhj+K5rlze$rK&4>fB($WuG+)eFPwn>9i-hD@duZ;|lA^7awQtC2=2(UkP9SSDBLQ6(E#Iw?@l~O|rrh zY>>xt%CfbUA~D0}k!~MN(lst+5w{FSacDAtoqD|Y-C$RKox-k65R$1%lGsw4p&%KA z_HP6!T0XD{HZ>PEt8Xm1ACcIZ5u}>}QLtMVK>f2sHZh6rpDf?%h@uub&&(D&ezMt` zf0{ErkkucA<05^5Abr_MyVR6Jge~CJ$-(i>w5q+<;BE0kadT;aY7k@yYU4KNgiRH4 zr6&>S1C(_}Is#TeieZJ2sBykYjJFiI6hxS7TL3YeWr8OAlQcMOVDKZdTl#-e<*M4HUCL35H>3vhV$vr0 z@>DmK8L090^jZjkk+m-TX74kyUu-hXqc)-Mp) zws|wdVHY`3XfGIZ;e31uE2?$8AU)|LzNS@BzsPwPx{yVh?3>C0x&0h>j?$nzU@j$9 zT>zYXRU%}21oY1g6btk+GSSvB`hhsGqUCe(^`hJvm6V<>$wLc&%4LCf21)*sfJN%f zcNT@_cSIZG!6}(q9ta^uYW=mQ$;1-0h+QW}WqOgw`nKcbhYlM_-&BwSgvd>LF z-_;B=k=dH-l;HKLfj)3tKopc^zOkuGGUqMJ^M9shJV%`fEMp5PMP6OeVc>J7h8#$K z^L14%u-fqZ-me7iQ!4$tj`4QK9|E0>BiiKLw#d?S>#D8@g|k0I-A>^EK+})|nwTcD zm&WJ}TSg*z+Jsw7%GDO%z;>QrgSPzkzAbS&fL?q7tg6#j3!>Zl0{Wi?|?8wl?=`<*U>iz&Cb2w2XuV^SEf-%;195E27NNdJ3E*L#L)c;SIRo!T;<+TzV)tU5y zgzUrP;k1@51AS74Pgmc!W7iGl$41pnx6w`pB9<5tWZbdPeOE-S)+RqY9oYtHC<7rk zl&>U3G_qFgdRUrU45e2ipsE(l#b&p{kw98#LVTVbui17LgMXnwN-onDr3D9W_<;r= zqOvpz7ZV|&4{Y3sp&(4%@XIj%rIMjM6Twofh@U;M^8+wCT$0>sCYadsQLprUIeWUa z0#i#%-Bf7QmR>*fBb&cxg@As;kd3dAIoU!*xs3TIFYTX+dY8&fbZXP&C7-6k)x z6-~Ba^UMi|)B~NGAk#g2VjI{!g&^fVw06h~G9wU!8>{ z6FLM2#9niX4-LSn{K6iN2-HA2x3B7BbMoYN_qGh*Zvra1vFkpG6z3Z$Mju0!a?S-4~YJBR}QH)8}dAUO!HEQ;40)6F7!NvY5Nl=KY9jWi`D*;aA zHmHo4+(`L}>~1N}7kFq8Va~mwY(#Wb)t6df(#!a`UlQ_aStU7>@DTa4|8E{3wARZx zd%axx;T*cGe|zD?Y>dEwM%FFHzNioV+lWs0 z3U69gQj77Igk>6BLS#CaPiKf#f@ql^>q<#|?wIVE(|Mz}gpIChSF+10c2O=#bD$G($DdD9x$^BgS#szlAj!-w-z#oGA4 zfOfu>#tH8 z)^!bnmTJpV6rwULBa3Phe4K*7|j*z#_l>*gn^yb|8+V z1f2`bIRK*kGtuw`@u#pz2?mEWzQO*R3qt)m6NcQ=rlmju0E(dj067067i6TT(X+I) zakTnvfB3)lcWo?b^et@+EdGZk_58cQ`}+T0;!RT3P}~qf{$+xz1Ti=vg<0K>q;L>j zorzMU;wBsmXJY-@{{GZ$@z8o4S%8SWzJ0{>=zl@Pd}o>Jq{VwzHNB-8sXi!t@=RVv zS4j!1bZQH?xLa;GcmC1FT@;&n*qOT(COEDopRAI$>8;Os@28lI(QF||^0GNI@Lv;4 z4Jal`rigjDGL2rp9vQJT<&k?kRW@_u9-{GJ(@;qSf-KYo{00vQgeg;@ZTRqVt#5Mv zBv&1)8T;Lod|wIu<}}3yH+Z^dmK=+z=I9OE_Mvzry$|3dW5Si&ye%Go>>+qo$x7W_ zLdep*mQ?h8B8um7|G1X%#6gtP1)O93yduvVjL5DXM(SkirD4yYy15Hs20hX<^L)gY z$Z%d5Fg~4BgcCMTAT{lS=TIy*i@si$>TKOlEL6IxZEWqfeZh&It@G25Ue_yynX;bY zx9M$s1v|+?NtUqM!{rpTo2;UC3JnN6h6?m@a)gA;dIj=5?PB!Qwk4`5&)U^$7|j%0 z1R-)}r+70-cii}|?Vt#7xc>!Ox^9y#$UcW5F&$woJP<(UNaPI!6n^8KMWCjbtM;O0o>XMmRMu=GC~X`tt7>MJKHi zrRB8%dLjp)y2*va{^UFfPSlMh#oBT`Sb7wzhXF=M7f)6e@oHQ(&U7B3FaliZjy3x) z1((7~(x0-dmeyj#Q~!7$Z>}zP_#N6LYFJ>d0xwT2g}2VhCa|wZU`2t zpSl3Uv3ip$`E0YAl0qsv&9n@^k2^%F+Y>#^(fAc>jJQITmCxz0bEHRbQO0216`NhcUF$V8 zf32klaxIjs*m~_Jhix#O)NRs=B{sJ9&G+?j3#ML?mh-0&H4V!7z(lVs3**lnKZ`ts zu_Vps@6Y<%W)$OSSoJoy>NXzbj#8Y0z?6~KWrlNDPMiuGC4S1) zV}Ywf3ZXzTJv5|++IO=`Jzx!`&q3(Rc$NCNjm5%tNnvdJfFWV9uk<4cJoQxMol<$Y}F4(vSQfN zVsLC|BK8?gKcf{H4*7C@T2MpuqHDSy^S~0a)lCCAoJxYQbaAvy$SXqw&RkytEp^No zF{0}HUW&!sT7_QeY~H5|x@l(Bgf>x>Kva%^?%o@CgM4J z?#+zIbugGvM+bZBq;}cla!4^qxVfEGlm!H!0f@A50 zd^{D2J}sH_<;;HBZAqMy!dv&|1_;OlGN_IdJW3n12Ul9|<*k!-9I)re#WV75k8T{hB&P3T=*l{YxCy@o72ZEmbbAh2|YyxFG;DmbjW zKavI|Unjfy6^?cuO7D8WkKk&B?j1wx(6J7dXlsG66SU+>`UbS{*P|TmLy6id5j+A8 zVg{KWrO}rCEu~3QZ3-EQMWXc4lqCjvr`AZ!PkowDw6#LA?_r|~KEAMOm1n-zMNm3N zzgkKt7R@Xa*V1bN>|aqOf_;quH$411(i<0zZ%#FEr9CuMfRU>k4lSrAl+N~!4>%;I z(7F`en-r~FpU=xd@(AP2Z&p=kCgI8-sc}MA`@X;C_mcg*RproD1-9I*{x)H}a;pPy zxij;?93w$Mxu_&axERZ{6q`%=mITSU+J8qd{tWm3e88?4QeXIgTIlN9Nlb<{>xTp`#;4E{coPGC>1T)4Hfk6nHtw3ZR>x&DoM_B5!F=YE&b?#Rtrpe zg3wLJY;gyP40dsq#KgpY=0_QgL8y>Hq-Gd+GLySCLgh1c4oy9^FRyHPwdU*6(O#b0 zC!IPo5676ZlYiXe3Mw)VzanQ`t}tWmlMA0O1F$KUJ-er84hff58Lo!l8ed_V%rxhT zouq);)$-m7ji}>?!i|V|NWD$KA|PfOTW>nWH?Ss{8l)sD3R^`bJ5-n@Rb{?;CN$7y zu8Npv2gPTLM&3t+>~rbB)rk>7y(lUB)PC=2w8$rS5Kfr;rz{&EG|k5hwEVmEe7J#kmF>g@PIBu;BXaQ9 zcF@2agtU-ojV25EIpU@s0^7(9!8HycRL$6%6j-Q>1JdDm*AJSZZLiag1`~TY#_nI% z)o>bk01%1?uK;28m!PGyiO;8`F%21dC7J?0yI)AK(@vyt!D0Gc~#CNwhyk77=K~})5 ze@1&dSFkRV2HMUnN3NdrCpg$1*IFhkLy(<2#yrb`fer+}*N>r%XUd4p@}%yUM9OFV z)@kFduGfvjjvE$NEs1mfP(LJEXU9#lXwf#&cC74#yuJ{YGoeJ|H`Q_-Qy%Y3r8}ti z&F^O$6{q53Ti5NfM!SAyTUTb<_S=gM|D{Eh4u12yJho&!5D)D|XYNO5_BSW|tMbmd zHUbfX84x$!f$SJ2%3x-n%m=+lYCSO2PtjwAm+zp=kz>Ri+A2IkU==1wz&qy!FMH+;7QY{-U(x2Tf6TEDL13mXSMQ1G@VTT)CzCpkeLF)2Rj!5pr zq}Y@&M?YDHnjT##B|*zzF}18`*Sm(h{Tpn$6ZZP9OXb2Tu8&I4$PClXLQ)>*7e^<4 zG9qqH5$ffB)l-4_kZ14soW1F|E2oLo7g87fs<_en8q4M8F+N$@G<|)!GDd}J{fO8} zKaJ@heYA|tV-~HoLLb_@vYDl&DGw%WEHgZlEw79F+irz)x_*0V(h4M^gZjciecqq` zeX6)eO!2E&iKInT&Qo>uydwkWDcu{K);A+nR!^kQYCKTF`f^FkkDI#MnZZHna55v= zhgIQ1`{PO6yJk^jj3QuTXQfj{AGwEr)^gzjoWV)$H%iOZwQRFlVN~>&Mx1# zMJmeiQnqI*ThLmjZ{YvNYsdIA%`&6Zw-R6gfY@KsLHi#G2QveWv6(B4fvKLoiP8Vy z7@bbC=3T$%fB9|lQW~?~5cvgaMd(9~ECF+0AmJDpxyMDpxa@F${nEMtAwm>$^bK$z zwrVt*3Q-(o6!UajHi=*d{e_djhtf=dTOa0H67k+bWOp zCO-J|w7!5O<)62_6^mMZth_qJ2Y0j|0o1!pGoIO~Ka&p~uPv4mMTW%pSQY_Y%qo_5 zCnkMoqC4D2L(waAFza}{Rh0}UvNfu~f4$G}ep-nM_%7N^a$))cTEHwn=mJ-t5o}Ob zhhV7^1FjXdomZDRDsUUFXeVr5!D#X8WYUx$DnI)jc=>1%*@UzLLOY#@gKj`4@O*f7 z?r9}2b)O#Gfb(qZ?u`lKS|1)ZFJHoh6gqo8_TD+?eH<9W{x$Ve+1T2v41+<%5`qNP zPNbhyH>A*XpFpc%a`_BBOU8zJh8L=zToz8Jhg|Ns)Ysaj)x3E#OBtc!LmH(uoO<;5 z`=HQt85M&^A7JM(Niz)QvRRyAA|Mdfn3I$vALkbXi15rPZ+w@ zkyAlUfc$XHsPYr)yut2pV+hJ}fd0n7c~IG|cPkFSSuN#qXu*!iN<)U44>!ValwaA0 zSUs-u^%rQqko7J;NvGTQKVQ4Eib=r2wiVDMAQ-lQ;M~-L`?Qt#U zl9S@EV6W8V5E}XJ%zz?V%0y}iH_J$V+%t;NNOJatW+rL8Vs#PB@he)4o--OO+^3TM~i@z%cDe=f(?BLY8Uw_ilqy{-|+jFQyKpFVn!L|dlt zI{cU^@dcDk!mVn_z3u`tWq)(Sv(v>uxDhy!tmi@2@R`QAY(HJ&Q=P}Rg%h-@;MS{s z6nbZA^|>J#Bk9zNv!ZsSGuDAGdmLRY7&*qwgRBzW_Gm1hZ5?V7Fa#72hts{fSE6*+ zWp*8FH7UcDrG58K5qYb)lB|0Bl(j~8v6S(j;Tt4PW%_nM)kL72%kDlO}XfL8C{EOPCnQXNoij$R(UA|tbZ@#ad!nJD# zU0A`KQkn*CPksIy4*pAjfb2L^2mgfvv){BxhW`i$^b9lxdcPHszoNtc)kgV0AmM*A zyA`SI$YP1Yd%x(^DP)HsDVHyXa1oc5smC!%%$F-DzePWQR%7$LTuY_1;?uK$8sr0j z8sN)$C$X>`g3&wo`bb&7Ov}EZadb48Kyy8hxooec*>tD99akf$sPB)wO)p2(Hlsvn zb%<~rB=q%n@08Z|8&i|wW;k3vqGZrIlUjIRux)1TiYB;5ouqKI0qL-P+?OFnJjHeF z@RGunNoLV+FK@BQoYRstFLdSYm#TJw zg}bs4!UgxEtXX{ZK$`;X+-uhUJBax8!5}oGIKE9`;FGYK*F9nQ#ndcv9H8JgSb+96 zc|*H{$y;nW9;9BP4A9DBE+4+}Ovt`nVd}6!%*gl;{mUg&)eoWT6D3o9=i&h*>+Bq; zz`BiO?vv~q18hqT+R=tWRTWcr0Rj9zgM0;7_u#*X@>vg#_jzNAAThCE?~xD0yba>W z$$GsvQ|`=88fZ|ZULoc!N@y1)$oGc<40YB%^k`4JHKd3U}`o{KYD~7w(3Fx=_X=&IFRSXWW2Fl69Dcy*r^MrvRdN@l~9^6x47ST`S zv~^J+9u;_{oV-P#PRd+a5|1iPx|kF=c-#hrQQO8jwo;MmsFt%kgh|(Pe1i&zRiXT9 zujQ*lWsrxjr;-umGYV`tE3HusRv2_;^jCO4VA(@^HJWHitS**7EYi9!H7f0~OvHa2${x*}s}zeU&;viz|K8+$fhwN*hIB5_VP4_)2 zs5IvAvhS0aVlRq%r;IO4X#C33q~VA)9qr0L2*ws|@)PTmQIHy>*ciz>zW9`y9)bH! zC!LTyK;-8Ud+Rp0QdY{GWb5LTP!H}~2F6KxSaQ@Y*@X{_80We+O#})$htCtD2nXUT zOf)Ny;9hzZvSP!>PzpXvXx=;Bo;&2cAWUydoOIYx(ie23g^NtEuBM-5@dIE(g#uj) zUdQUAswdP@`g);6USc0oOt1^YWsyR|N*@@pYDA96-$lA?H|SR$Q4OG1$aJAdf(bz8 z8?Mf>gHGp*mi|j$`f*`{*_VE|hMnOvR{t?O2N~Vyh(Jc1pWK&WZ(>DNjw|*-2W*vuxkGp;_B+umn>aaea9{U~^}IJXG7q3tgf;8K;_ zgaCt(xhs;m-|Yy#(%0QkkK^RfXr=@u>8}f)qC|7Vf$#s;YOqmomj~_C<+=`790p67 zhAa`*%)5bMkjin__$@U~CgmCrROI>&ZygcEs@uNJ4zXl*j}FSWCe$$%y8wGq*&ZX5 zpZFfddg2CwBGAW2(OL?;_n??aXz%7WR6|PvuB^Lepu!6g!_wg&d~nm^{5dRDqIqFS zQ-EII1{1kmh0V!6Jhbpn1Mb_}`APZtycC%$Rf575jTe|}3nM=pyG%r-u3ig5#p2Tm z_D|+gah@N42VeXgz{LeCBtQh+(d+%c+AU;@pD-g#zN5Q-U2e^-0kPIF}|_{f$BBH5YaLv)#2p$eH3-M`#^9L-N2V>#B;TXzLh6` z;Q27s`_=B(!OSSw$?*;x?*stJ$Tl;8n6~&4HgKSjiwIstHSGfZ`|9*(HB!PIycQj` z@a8w!H73SXr&(@qYUqbGT}U-koe;^zkO(Xb6Yhy;Xbr;bQx{ebl4Q zIqlpiblz6`VKyJ~);d}MTV6rhPX(9c#pja#L`zTVv3;-beE(09!GDdQLTLE#!@ma1 zcyIs!n*aFbFw@i6|5rG%fsNsRnm~h8E)=npk$<{C$ z4{9at^QgwGH!a`M?Sk?B_`s{;;c@x>k#Gaz8*^ig-9G8JJXqfrefk_;j0b?Cy~nma zuHABW*vYa=by)ts3qcteE}>dF%vB>(KcpthfaEnG#f?*oU#^keM2>Ek(^bz&QFBnjN(tTb@56pH5T zWV0TH&{BaBT972q4jm$dTqdYxuSm|5WSsOX7-%7Th-wgKP;5DOvhNfg|EFH=2gbE~ z0PJB!&kO`?i&xUi139kGJsAuCKTGhDloZM?MAx3NjJZRvh z*(UJwFw;DGbU}EAH=2cXuL+WSd&o$D>9^8Cu#85wiTDrw>_T>vYI5TZ{d^imD@u@<@myzgLKMIa{R?PQ2CJ%F=-*fo4@-(tFc7KUJ>Kw z0@2V~Mi}#Va1g>Hdb`YzAkX*h&-yC=tFlLA3PI1A^L=?uUW^`S z9|(HuibDfbI`X5q--fr($Gm$df>5L*(P=aNB~>ZpBTQpmm(_ZOqN!T6K{UZNqIk;L ze76bnYqfH)pge2{$})ScsDL_CaoOcod5RU%dlwaXG`Xy>n*TewH!G&~6MMk;Kfmdz zwPWPnS?!9rHbl3&-rdA0#>k=>0y$QD8#=}p4bG(zR!aZo5b-{h!AHNYlta>0&%KD} zf6zp$s*itfhUiYrYjmDv^P2cGpAZwGL`N%TI2YtRPF;6;VR*8s7aCpOpj9g z^@)n2dY`B|D&w{w4`mcS8#O359~3HFY;ZVVsaRQw2cToo2oM&;(b)hI!Q)e3+JXW1 z>#AW(vgr~L|MdwfFZB?SeS`YQ;;D-3y|XUA%wJk?c-QQC$a)j{yqaGjq^=&WI<%sq z{hV)B=Zpu-h&nKOa;~koNRSm`QSw%a!kgHy)WJ|nFk=4M-YAWO+)D=PTQa}S(kVMQ zrCTG|RIhs12#89Er^fwsZ(LnM;V5ynWz~|`vVi>-y9pXh3ZdsjaIU%zq)`u__kNs@ z!t(jLes9^Io~ohS?nVsqpSw`~rk!m5_PSe7y3ol`zE#!ibblJQ;%cjMq5V0a{P{Xo zPb6Wz82DbD**mUy6REmPfpk4zT0Xj9znAl_YOCE_lBvCxZgUnW)S8$o6sArZeI%=^ zcy?a8EDg=Zh0RjY$y5=Qg#&xaM<%&pc9=1=lA)~{U3Xl?@+rZ0uopSTwZMZrbG37| zZRla9etSbQYt{i$@eROX!yY$<~Pp-JDGD=G3( zts2L!5=@=|Dr73NO`$4^&|z`2SY6vF$iKsgpoG*L@g-Bq7=ZSoj$#h}K20mj^pA~) z|GoT2XlECF`Ezpsj&sV3J9NA^w>kq)8A_bOD#6Z0hPaZE&J=Il9|_VQ;p($c#Gt7wwtc>Sge8V#ZZKM?`huEVqir#H!;LTfwnX) zr5FeML#qM~K(kC?HCIU^@-yk0@=v9L^dXv3#%@g1twwClv+zeg!fHW6P*);j5_yov z5;7T{kV+x0HItXnjcAmHAkX00uHQzJqhrOIXT)NUB8IM+l78-Hq@I|CXD=~HsOgO3 zcZW9mjZ!QA43*STbiN>W#`1b6!%oVcBKyp>r}F`OBLN`;mG$VywV3&wb952%;Lz@7 zO>1g%PI2TRd%M(w>#ni`tyA*(H0zf+fJg4vHH|Ie7o3<-dW$l7W*X5=efE7Us<_i;eJ@fg6^i0nhFQAN z*kezvWR*zz_c%M0B@bOyRkRF!fO&f@s^yKC)S#PTKp*jno#E$z#D$eCiKn2ZmfF2S z+)&s05pQs|BB%cIW~5b{6qi*cE*xK!>aW=6`D&z_hAnNOdu_U{k_wJZ0j5p@xy^Sb zX!@oP{;LIHOb9bFS3rJ8MU0aG>8gfPS(2^oTcQp7T4C9QLgRGHJ~*^ zx_IkNFT-p&w!2hsk@^psk{M?plE`~UtmpQt4vG<24HvBq{^t=7OJv07xp67Uu%dVu zw+tyy_Q1kfdcD_sDk6rT@`{OEXV|JLv8!F9EN>pF(xBh+Dxv_&$gtBCoxt3_0{Ao> zfv4hwF^`<*7onh@CxI%dv(SQpTDILic@^tRLGmqSSL&2!EYSysl#)(2UO*?I$9}Xt zD4layr@e{UAl|d36EuOGFZ%WB#w!RFG(`EXsCA^C#xUfBkPp44Ofv2jiK#4s*r@IK zZuk1L9_+hhDM5V{DU&bwtq9K3vqj|LmbzU-BQx{verr7#(+At67l^2fMU~G4dF36s z8A(vxCLa@*uX_$$27F0DvgVSg>$OLnue7C&u?HTHt#SfkHB^iPJ!@obpMFbbmE^dn`SNv*&^QC0wZ^+ z?cQnbBgBl~JFLLa_i>iIFT(!*miH)GD8Cv0AB>$-kS$=ArPH=;+qP}nwym4CZQQhN z>!x+nwrx#TPgF-ucT7}0oH(!N<@~Yt+G~AFm9DzE$}T&3>7k(Ijp(c+%qojWV&@6z zp>p1*hs+35yr>5?o8aPON+nbu-Lq+-;Dh!Nd9aI8MnEH!2s+7nD<3YHA z>CAcI)N4HCAIc%(D30Nu|`9#vfKeGxiRQsx|pLjPkg&)lG zl4~x3>7Q&8A}(tDE=4)X3Ndl5AXxCRGzuiIT2oZ4x}UaQ7!rG@fvb+-9dlVZCvGDF zaP&EWj+;&y18xnLzmG$o{i6AZnG3ZnV=vmV3S$TDvm^Mii=h?p1#PXuO1$@heS+v? zFw!#DVl0w~)|3%tFQ(S{P4;4QVjqg}NcsnDD*5NFIe8P_)q-4tcWQOW7o7Cg5n2Me zt5y4AED@^HaSf9MK>B0nzdz8|T6Rf`oX#kBrY2GAMaX1%L5MuIm<=x|fS4(=+t*i+ z`L1OI%Mx`h%tp1wrNe(_jT`Cpy`$#qhzsTsLJ23IlwPm^i)xx*^jV#|T{BHW`8MafT&rRrYpJYpNPv$3DL$-hP zdvKW$rEV)U)=~BdU0OM#^FT%;18E! zCQP7Chq#xUp}nTT)d2o$h<$q#?py^5&q3|{1Lk+$z5QmPRc94{=FKm1!BK(Eo7@X5 zb|)Jc5V2=pC9rmx#O~q&kBep+o|NkLzd&$P2#P3=HeGJOe^A4eAPRaokb02BMO`iy)&jMhliA>s z;5D^J0{3u<=1qM(hi$=7oYM|v59!5}Mttaqs~@w1LFTBL&@-J+lJBL`VVa#i1itPF z;70F2*?R_Ui$G4?5vKUHu$rN{-z4{fLw~+m0puTn*0=v1XmwT9{fBwb_s2YlnTuKKdBSZ#ol!M!BW_h3@Iqs`m`% zvlWD_aTi4;Bnw@si%Ety1r?ptl^jB7r~-eZ*|olgS%%FbNFc4Yc5$aTl$d|DUGsNw ziEX9&>pUg&fCB(@3or?a&|E^q*h^Fz^A=J)YcRGNWYR_jtV}h$ewqBx9R=shE@6C5p@hL zPUAet#9lzqsYd?j9?x6~AfN+zvh*WGgyAxsMVrd;4*_%R^ih`1!JJ`0Ib0v7*q&ck ztDWD%;S(_&mZ;K#ye!{1K}D(nvL2?E_q$5S{9vv-D9X7;G1Ej{#gc`$<3bTfk1u}d zWsAGvCG6?)TgQ7P;ISwQ4W5!j?zRP=ScbVs^(C5K{X)!H1D!ZL3;+8aP#FlS#&pt^ zLR>NfJfHnR+rCvlDbwp_-t2Z6DH&}|aDM&=qkajxa~>o{@(`Dk`E6Bqo~v*v+PT-5 zeVZF!ZL4J~_QjNIw5OI7k zt)ZixB!2gMl9`)MbUmz?`5sQJ=-#Co(j}!Ha9|78RKj=w^)#=a`=s9>_ux5?;%kgD zX$LmVW><}9{-nWC*>b(a)wrea)cs)g`?fwroLYr$tB8 zV+biD`@n?EmQ_AUFGs`c5>#^O&_OVeDq0Dup_l=dwu;};CVH;Vpm<;>Z4${t&No$( zGXzK$2{#;Ha7ITCT6=$wbkxo*h5D|ytdlq0bShKBSp^BcEptXAd09c_0ujDU(-3&M zNZG^Zo4p_e)XhE>yS((}2u{)P((_M~UJbq*lRbR9-}^(2&+#OIUmTbToPj2G-*nDC zs-7qw*P2&i<=_Tc9ToC z@qY5rba4L`snKcD$+Q6rmLFDJc>W?pz8uD`L1{IeE#ji*&mCoFicy%UT4g6VmG4;h zuefVX>hD*L8nHqa%25jgH008PhXJ{g7(u}4xH_gp81j!9iwD0UQKGP7Sg9Mc0IKv+ z8=Iun)V+WMOP&#n8I0$tdq$kgb^;$}J->XY7%4~2su#OZd}aQWB$geJ-h-**+l^QR zw^`#~H0LaJK*m0OTXL5wgFII{`V|^HSCItVHpYbct(tz}DTJ(JRXLks31Hm#0`@zS z6nPCs;8IuzZW`w=%2crXJsKM5rACG_XX!Eb9P=8(lDUiFs|)ZA;<|XETYu3 zfYnJ8kqoEQgog&RTEmK)shZMCqUTu6YCRCv`)+yjb@n_l0P_LhrRdLke(QPz}>bG_}OQLHEwy$rLxDT2DNmlrQsuEZkl+y9IHb$K>#R30u~I29R;x-0S+l61~J~?C-cTDS@{vt{=8}D zyW*(IHrqAhJ=1-hJ<3YvKJ(VRFLAKO$~H#x+@k+F2{y@yvlp!WG?s&vmTV-&*8>3Y?tolgZK5e!D354 zP8%n>+GOh)YYTV$<+>krLMz;Qbq-cEK~d;u3T$?L24`2aFJdcv^+C0)ZsHduJA-K! zM5aSF0UI7VytG0STmDn2N98WhDCDRXWU^9fqwwV#zluKfdxOGYqt`UmP%9>CK69Jz zHp{vQ)Yx)%VC&7_yLOpSC?H?I7lh+r%dA$(*eiX{K7+<@6cy=4oM+5E6mI;_R2tKE zNSt5|2ZtXNeqj^zHVaY99Ru{~$Cn#0Q2wx@7S);h=Y8b2oBY8Y%2b}Hc`cmgW2ZSj zZ_W2z!Wcgw1SR(OW=GkTkfUk$0lPn>11dPJ;`(*3H;tOMOy?l_D(reLdzB_^(9(`rC6NFwX2 zj={*-U_eAj1~V_4nHxQchsK^70ih_vxI(yRq}Z=F@962K&~99OqqwMQQu<11p&2qX zUFPeO03n-9I&owsuciupmfZcATW#KKY)ti^d;*h8v`}_zgUJ0KYt+5Y5S2r_)b;H| zQug5ue)lYsvM$a7JAS;>QOy#;^XM7mzhwIDs|V-%4Z)6p#gLIB&RpDPzrk5weNv|G zNuvw5+YpwBS0~4ld~NXZL#!(N&YQQdKwbf3n&kO2oOpdLq3Uy`PGg}lbmYnM9ZEXN zP(`j(dwYoPYIhM5xNo^jEUrbhoBoWY`Ka?~hvofRLP{)gyJ*#NXyxF*yTEk=Jg- zf#c{T3M;?F(`_vSM{vm2+G#RpeYDdNmVx3h4+=nNsO8p@Fak4*}~)j279H?wKcvnpToiEITfb@vqFFV$~7tU}ji; zikt|MG1k=3U$9#qguje{<6B@7hOj=Q?RJH;U`tIlGXkI!R47V=a4|wQrzrEccd$*! zD?n@ZWj+cJ+!rV&RyDh=yT>Klo;)0W#a*8w2Tntq1z(Fpa&L_Gm8!TSGx_XsbUVd)1*C6&kZf}l7z@z?p$BJe&AXyGSY z|JvsjLW<79Lav5K>VX_epIiDxz4M#R6ZpiY22dxPu;Qo^0PPeVYF-ui4RF!n=0WrN zIMoTJiuI{9b+Nk1ZUiSEMt>0~MVMX~%rmq#cqJ$YG}r6ZZ1Ppu9>Uhp0@42f)w3rlZ7xWY&rps@e(fvU_#A*Y_C#H)7;&Q2*;7%SXMf{5|d8h>nYHv1$RavOVi z-TyVor-k)4wgi3WgiwA4;-akHeZTC+kS|%H4tywRAMv`g#0qHXcu}PxUhr7eJODfN z(^-Hpmc>>iCn0feE~C`}34>*!s>#x7(fj7Zt9Gh}92Px~yT9>e`+E)`U-?Y2y`HM& zXB1@|rbzkh&RV!=?EO5fy{NSB=^4CQ18e&wd{>woxIcswhfab34UGz=-@IyxySS#} zs)WW-vVk*YomzA;fK8W#&A(5404oT;h-C-=1)Dw#h)}vt<^E!?peR#|c?eg-rdBo=%Zo|Q%q1gKNLkq=S?l>} zTDKD_t63o&KZo|@QmEj7%YJhjmV4#SD)pOhif(!Jh^%sP)iIFf?9EzK`zuk39^8RO zpA1Y^!B~|ERhL+gl^HR-Er(X^6`-7sY-bIQ+JE3aZPAN@B*-HT_vQR@*^c#Y;hvn{ zY<%IhU0*LTqkWY;fk5(v>na~?y|rML<#jr&E0dT%9-sB?T_}D^2*sE$@*}U30ZExW zZglh{vY2*3GHUGh%XLK#_Ohs86K8FC(9O?cIqYe7Wo3Fd!#mU<9(RfBOKhe#=lzp! zPBjC<%7e+|4YB`$jCKrxy4CO)*QBrTW$L=+URGWw!ll6UAuGv7 zGMZ}SH@&(4NGVbh!+U#%=T^|1A=r2@yJp>*dw`!mx)YpF6~j4}AvEcA9-7XTcTD{h zZg!o~TIX2b)AsC)0X$GF<2NJ|Fcse2g!S}RTTa8aR3j2qCH&|y3DOd4(w89}39?Eo zNJIEeC84M+6+I25N(yp9=b}z0$>J#Sz!NCcM6+^@FJu40vK^u_r1Kq!qDRE4CLE*e zN&%~sY6MLNi_M7_4Gw!U1uQ+eTr3AOJO(QP0!Q_R4Lg)Ld&5` z+@h~xF$9Tlx(C{P4bIWnZ&b*^Jkh&4!HrkA)ytHw+>qCO1Beyyjoauv$~9}MBy|cz z9f|E2whI*Tb2Rm43LXSx&0;3B8ysacX$mn?)NhhO1vSzXi;nUe2unIBB=!>|R*y-C zpv>$_mJPoVFhYDE`At{Vxl|WovL|#UqD9zM?bcTA3Ui;ztL4o2sXZd2Q?$wr19Oe~ z9bC-|8up4jfuf7coQ98Sjll{E8YGnw*0_=#^4&Pc0!35uW=WZqabax3I6Mq#bvt_k z5`b&A?gg?D*Y(oER)!PLcUh3$^Zwwzl?>QlF}KO!TSYpMSfmLh1|NPXxZrkKFNpNF zmIvq3Rpm9nG13J}vHXT2u^bm}6ZWnn_ZT!y z4$)+=HVK$`^3Zgh#bnYRFQHHWg1*-TKG$ifMDdLJ@sC_$000R5+s%Z5nbz3V$?2c* z^8fp7>tJK*VrpmV?EFu=w_l|KRy!T(f{I&Y4ML#8FNdfD92V_#OO+@ zvN^8$NtciiPBlOY4G>2>s{K*jBO(-FcLRhZp&QtMi2~^a2?i_pHj;mi&iz1llFQ4T z>uZoJxl8Wqv-th~(C%~0ef*#u?VfkvF63WfiFM4AwL2xfH5qlt>`iaIq~_Dpx@Git zUXZ{0;9fK4_p0`@9mD!)p(@k*so%cR;_>7YR-S!$MxrODdG%C$+m{PEUxXE6%Y9-0 zL=^w;(n+^KeRU7p2^;s9Vd)8q+YeKIWdKF9cLkbe5x>OZj2uvolhR5ae$K-rSH~{f zRv0}k#P*c0H>>Tk%u8E8)~N|kmBBeE`I+pj5hzP&s@pZz?DPAGH)wDF&|@{xX3>f2 zX}+24%p77bc<~H%)!gxn9HJSW^v&a9poM1f%$i{#tg+oGO9sJ7-x>EO*+Bqa&jLev z_g5d-XMMZ9y|=SIGYO@5>LC+MHV-F`BNIQ>&?CY@@(kkC$@(?(Vn!!Tgc; zJJ4Xr@vOCw{5}u+(J6Wy30i6d(ls(dsy~mH3)`fr;yq1Mh$9qOFfbQf z!2W3Fq&7R7_KMKHI)*g84ywa{gKSYXIBH;EOIdA`tPb+?eI$bxNy>stCP2~#UNvct zhF@!8Gk{AynL{Jc*6Qk)?n%;4CdSS2JG>ONeUX%f?W*!%nHl#>prjD^G>j&QFxBwm zMvfNBuL?xQG4)^B1_)Dlb=~~Ac)ExJ?r9v6?sT)pEij{qE1OIZ_&%*LbIT13Js@NM z>PkChhpCT0RJWWgn{ZCZE23DbmQ=KO!p7aA6in2fsHA)0aS5>0u6Qz+ju)?<-;})W zr^+#JN%J^GNF2Oc@-SOw0YHE*t}*37$98=$9IiDt0od-I35P%d9h(H^1+3;G-N(8Lh!&uCZ|q_rO_e9eRI8?JF(cF;H$6cr$0-wyZPe~0xoPo(feC~l z%Biu}*mmFXW=Mq+`|MytoXeHq-A?loqFR~4_0u_g1QypmJWi1|P*57LWINffh(THA zL`tE(waRc2KC`U}41IcZ=7R>84*6(%bNBz-rwm_0bOEgtEQqse-f!W;fq`WTO!v0r zD+Yjk8NH$lj9Rq^DI^dINjX^{ly;|}9PZC4Z)x{jQMZ-rJ1mc8@6c`Z%Bm=@Efx!^ zR6p9U!BG){OLi}AB>9pSulM$79SI=vI$pe&m5gmA1hX_G)?)C;ypX}{tp4)4A`lRi{oevgjEd@Ck~9X6g?pT4PcKEbqUU%R)6-7ESF0Ln>-E5(gkBOq z0;r@vz1Fs}F3fXpvUq-xwjz41j+w z<_jfImz0`Q{GDGBAh>xJ;W>H_i>vo8Fi^q{ymPy5z1TXZF(ZG&T7&4VEm z`y`|oApwu-(ue00B*gcECL{rV0K=yY34)yBui%yKV`_rZi#fSnY;*RbL5=B=z3z_} zNA#tk0K}4xfwg{{0?jY^E#LL}?!bbF^#~QZye&l=G;YH1-~q#l!1wuRe|K0Up>p3D zL+N|t8^stP(B>t4m;jnyc+iZTketz!Weekg7`Y*B`mF?=&wrw|puVC!CQ>S&S`Tqb zM|CWKR|jSV1D)R?;jKcLa0n3EfY$HaYb9{achV-GR-F?o?JWScGU(FK3Dace z#$|tazLy6rhlLy+VJzwzl{XoXFnIeyQP-!SAsr3J>6-eM{pwi=BG1KS;}MxvF=JU; z&>Gx3`(jL-qrcmZd`- z2rzd6Vi;4&Hcw9!>6OqEH&Pv!|fn!G|cwSra9 zqEyS6R3Ly_jZXd~2-azvGdLDZX3Pt$J%QzI7v2SndhBMHR_zaN^lXlGB2zK+Y+m$H zvUADI7ZhS`;LksPV=;KrGdU=E1Jn3P)E0HS?*mQNxEjFMLBF6NASpDOpDHx-=1QWM zGQB1lW`hCqJK!e}(<*OST?#y`VUswH#%wgI#``yb-WCdWV{>4Qa8IOTh9|+&PEW_^=8h|Ti?))@CJMn* z>=VMb^W*t&iJ1bL`5Gkwu+26wD(mLq5Ky#tHJrR?$0|s*b);&!O~ezE>+#M80JR^9 zt#!*6j=(4PkRl`p{t@p9EQGkE)|*xM@d6Y+`3G=FEe>fu!xGIF2`WV7b~_e8GXzRpD%TOOYsh(nY+v2)B)I-!Q9dB z@*M0d1RLIva#inurCYm0d+2%fs0$y|M>*He$lkIC;m{C%nlTZn18*~W)Vqu#opxpl zI@=IoBlE%f+`X#ndeMK*T#BByGPcFFk#X5|&m;yE>NEy`g-3O46&o_A5lLCmS%Ljb z=J5`y0vAOZ^u0L-_q2&sYHHLl^Q$eHbfhTZfLpJ3S5>fClubDwXK0%@LMl`Q{tx8w z$VoYuc*mGSM##&!q5$X*mG=FV6P#nVFmoB2@3}^a^y-4qAn7wmFa>yf5ZfkdLIZBn z4zdX(y&9ZbckBnJ zosdIWq6_)stOIr6u2ZBt2&t7b^sK+rY9U;jJLcI4Vc;_MnTe)Fy=2wQtr;+cvxDiS zB2*P{Q_LSY9mjmd;%<~>?#Z`*IZpumoF`aG80R>CoFOjo002DyehFaw?-=m0IlLOFUmyRC0;*%cx zs4BwEA}sK-fM;`Xi$H`!()95lUin~>MRtLB%n%fWd}dUQ6>9g;e44HK(ha49*pL-W z_FLZf8_sa_82I-0kBQy$tPeem6!%WC>UPzm1?}1ouXKfPg z5l7;`%WG9-V?b5SO4S`Gp6YrjcQM`vyRf90{QRjS&uMNBua62nxY9h3V?~Yz>W5XF z=rH0)SxM7Bh_ImYbCnZnwXEGgycRHu6C&E32ZJbOlMEM29L6|BE*?Y}jq$27TU~Rv zS{KfJR$6PecTJwEqo;0S*~N`|!%)nlq6H-{9z~dK&&sTTb?dgwJ+7|O!oYZkAn&pd z?h4oaoPdG5Z(L+r4dvfVRJ$CIAxi4aVlrws(CL`4Qba7cCE_D$PHv;@Ii^NS|m{ z)b$B967XxrCFGPXNS)WNHZTh@I_#p|OS^sB5#&hr*O-d-0gy3`5}UQT2ngh~o=oTX zc$obA03$r4pew%0G9@VP&(P(2-v$c;E?x)2TkS=9LUE72G=8(M?#2+KL5%8IOA(cq z{EYXZhe#lPstbFWlUS7Zqa8~No+V;qBE%4=E@2RsvRD?#!V7wZYNdDrpO9_Tyu#NS z$L3uPlFdaX$K9{{5gb)_Mrt-bABVer$63aK>bJ8E+n2%sy4^q?BA07tn^9S)8A-9w zbYr)Fwy^hY-(8l@a__cgJ1jF|0B5!gyO+*xY|!%eHiS>-K zAf%d+C_~i``W)Cv){LQ)=#g^MlR{CU19X4{|v& z#sAlQQxG^^Sb6lr=a>GH=rg(FO1&4x7(9gCmfh{#>Nggo<~fIW08+?6smRE>LC*{< zPt63bhp#pct2u=8O?aW_QNRlltwXM-;&6}jaFSIxDLRWPB;nb4+mqUykOxX!Fvg3# z9(7>sUyzmP&|yX@?i^fhZbaMEU;Ly4^35CDnO-3lf$IGWq-O&<>4rsN~ic zF&)w1fu6LvS`Xm+gsOUsE6=VPQB>m&JR?TsC)BEg%CfR$Yx9j^UEWsFVjn(P>e3hN zGK<`Wo-!CJg;q2uu1td`i!+{YfqFDlay`=-7o^HW2_EqSK)E9Zzz~mx>7US2`9{0? zN>)VUJ&L`~UlaLLO{}ZMQWoGVfAy$m`8r_Bl4G=aHAGK+ng-pS+a$Q5eHQbwkC;c0 z;|tesz4V!IGqK~Piv98)N|7qMk~Vs9$i)RpAs85?)K3ONH?jUqBelVFPwKp7M~xEg zD9r!$`E?$jUz5XkV}vJJFF@6jKO}_6%0h~VV2JdM1XyvkOmy7P#a%omfk8On#PSIj zjSLqE7iS)Cj_9ffN#`vvebJ0`q$D%HC%%ahUFI8HLJq#4@{4OWV#zithi^t-`c0F! zrAD5Vup7Uw_WM(vlZt1Z{Q+w{H;vG|k%EcW^VcU2A41>eUwk)@9{W=HT*d(!XcqmT zh){x0gPg-jG26_BCcq|d2S@L>js`@ zvR_r)FnDwXs|U6MC-WG#h8dNdxrz?jd--sJBF6%62u|?aye|~W+rhe?4a56y#SIA@ zT+kj@f4e3|Mc;HEZxjc5g= z$wC|-U!+>a@rHu1S0^J3_v^bjA*Z{yn)0KmpV4bG!2q-Q(!+<(r9iaQ4yy z87~LTp93?R|+@IWpD=?uEM+J3d;yusY1Pj^K+*W&)hf$G@1L!fc&lLIz-7 zOa_|=c1}vu#fgD`TgZFo6)R z3uKXM!*2As{1Z%Eqxb5;rkRW|?4IILD-IUj=J|y1U=-`+(+D6i5i{+fv1!e>mGd@V zTsT!J$?vi*kH^j6Txo)XQ*t*U_m&du?{(T6*7~~y>wy`D_BkwoQo~L);tkzE^g~)o zY}hJ< z9j{zasS>GzhIF^X=OPo4Uej*NHsXH+hY`4x=$rS-0M-KNtV1-nx* z|0dr$4h2{bgXyurK_x~s9SJy}^nu34f%C;7p_J7>$kmU}tN{E2va{qose%phjg*(z z_&%L*^3J2aKu39;aukoHfANLAz_t4C2xrfL?$Mj#V(Y##$2qJ{=nZZ>(z(KI+u%$P4ez$Dot# z)yhblzuY=C+^^Z!fXLvDS9jUZKM{|sb<%L<=hXs@bYpfvcjY%G7!Awc1*75jRpQo# zd>Vhp`H`{BYg~_L@Bk!QR8dPrk4+I}TGzF&&bXw<%9uKv;ly>%o2|PWwQV6dvFH{M zc!&!3L3cJy+nry9CfRwxcb8ZpRA$QFBP%j=lvyH!8@e9||{hf-v#3u>EnMN?{}i>SFD8wmpbO49AkjXZ8{h&YJ+QsOt2i zsI*8%hy|}D6tqCc!}5t*MY*Bp=Q;myglG}$R4sR$MoQGWFK~J_cV_&cPpu&!e+Psi zo|Yhqc_|0HDCfh@j}Eaguae}6DGWiy6JFXaW^Z~fd(l~;uG!tBS40nUAh_$Wztl0= zIRV2f=wO&svh`~QXGLP&&zI!>Y6~=WgC0EZX%K0QCNFLP_aQFy6{nWrwtGp6HaL_f{pghs+b}>@8sp0hZh}Sa7=Y+h-T#_65=HZ~JG$Rq( zeW}z&gbI#$!r?jhuqHda$azi&5MZP|kaG-zC{Gt#1?0r(w&r5dPpoGy*D0qBe9{E* zGa{8m&}a{4E255)^tYIVz0Enec@b;_-Ih8f|EI@xqJ{AEhDaAMpdwe$`HfygIE)Jg z%9}fq)VMp~NR6UL5EIYx*GNw&=SWXdZyi_7od(G7TFTIZAV8&y+RuLd--B&_tjF7n z?1z;J*(piegV*eV*K;U!x5jJjrMBZT`!{gyQt2FNe?iQXzxMM@4bk`?0! z4VOzWOEmm9&nmWw;^``X1$JWfnRm|K9T+)aia68>mYDQtGJa=KrG~?#nx~#1x?xb+ zQU&G&!n3E!_cjGEb|8}DJ!OXKBm0yMQ8-sZM?wt}*}NFh=+8x|a1T{~x&qM;Y)I-L%D9iT)3O}hB6%e0qN41dswFQAXN1?q(M-APBveYm|B`N#)gd_s+e zg5@$z;hNEMfrh@Nq?vpf+xB0uqVau{pCqcSrb4N*YyZ3HF9{L82lrE1C@LCIL2gSr ziYPfQYjQ|VpwLc#rO?RyjwM=pZWe4qr2Ce7Jw!KpgJ<;Y|m+Gdk zWn`Zf3u6MPM?1XQDTl^K#!6m8fe^+8=&406l*GFEpX*BZe zkvXT^M}F)=tZ})XsH8ud{KgBVN5kl4iAX=Juu{&68OrxfBw0~~6c$lV5`Z4;PE95> z4hcUC{SW-ArC}S4=_Z$j1c`gG&zu0wbUGK-^$niyX1#0~dMkSEv-q?HV#7Q25#Y5#jke8{j^eB?K~YG5$%HiOIKmlrP*6 zH}Q)^5ZCF&^&h+q*c)Z+%{(x84u~h;QzV~tDJiC*S$QTg&k()Of0--(!5tydo(ft8 z1OO-p`cM2Btjs@vQae{eoBtR;;;JNTzaW6n{jOe>%zVJ#*A5ZLK_1lzO8phihTS~M zvM3AWNHr&rxuX`7j8>^+tE#HKAOA4wVt$$Z0)0WIQ7y0`)_K1*>1_{X0lW@tQ>dPv za4Z*Mp;Zu&I;F-1;lUFchb;T+(|On3&<{YDKFTc^J#x^G3$pPo^!2fuGq@y#@tVoOW9%l_)*y51I9 zSq<&C>VQeTk)et9l09YrlB|xoXp}*tgk;Vx*l^2RCA$^t0s%_@@s{!Ea^t*x0f(jV z194~en5gK56mY{ZU=wmKiGw$kvV8VXy9M5JO35%k+wXl{huZndjOd3d(%4U)dRLY` zhBtixr1AZuJ7Itv-qThnn-Z)Kf+#u9yNDS(6L>1=K$P=n6{##RM(W2>l>zBL-HFMo z44Rq)X+@`ZCmiafb{4*duUt^~FA8c-e*Z#HH2JxxSKa1K-2BjP8BhTLnE$Pq#>h9#5$RtQgCgZLDam!f)5LFP zL)7gYxM)@14CThJVq`Uru13iv$OOgov$-JwVc_(EK%{|S14&1FzSC!&=4Slx7}PhH zjNwOGy*tT88L!gL&MHcCz1GynavUc8^ zQWqk;|9~Uo$ESJcu@=#!ntsrdIx~4%{zfn-BuHC~WGjQIie2=JmmNzVXcu3&zYa+O z_#hWeMN*`@RP04?$C?(FZK~V2BQZ7XH9HHg+)vfJ49v&JLE&wM+V`P<>ns#ies(BV z(fj!6M%iaThwjIteE)(;OFwsvsg?UX!uEWML>84V&m?-GSaGUh7;4Hq+G?Y$N0hw} z5F%bz0zv;-!G1=TzMa{j^F3>e5Z~}#x$0q!%UiJ2ik9cYr@^L|qe%+${Gr>mZkRWH zv$-^P4mX!Fyk2qLAd3Fv4l2uUU!&cnME;gMhQ^$*-Ny0nn`Pp<)CvaZz-R_c09g;-wQ072xaN)+tq?>lR3

=11ro=pPB0IJQ`GTr>hzkwaZM?I{=p2g;!vHL9D!~ zhY=NiW zpDdAFCV5xbOG-rNB)l_lfts##rnU*RGPNc@nj{3)=dqaA@)tL6VUz<%?vSTR3)^#t z6AbvA=WKE^xlG~cidA#OjfWsedofOcfX69Dp>dy&kz~#r!ja$+B7=J-WPfnMMHo1u zqG1l?9WEF(eYy*{k~2_F&?D*F;8FbGGDyqkHPCdni`!6YLO8`b4C6{>_qn7JXV~-_ zP>Y03bJM$zg61y)RdhIkOVzF9+U-7PiW*S$kOetP*v=PXR;k`J^x6B^Cvj)g;qoVy*8DVr$Q@2CFTpu;ptaaOKnjdQN80{_& zh!n@@L?TByfx?GPHI$@yzSc3d5RFYiB}?lL9o)mDpBL4plx^KBZZPtMzy_P;6{{5d zq6$dERUHQeLiwx+rlet-%XdB*#Ks9!2YbFqv77+41+hfyH` zjKG4d3`FiR7Dmd8VN3%Y(!wJCvFI@iK-Ve(dJtAd3mj8{MF=Vm563UmTcLCN?at2T zxvA1NZxFtNLP4N0&<|m*u$$tvL(txEN{NT6V7T+Od%KUur*sl&m#L#2*t`a^p=ARYMAf&YBeT2bF4+9##0w@(9-r7{8ZudWWT>@kYYMr~}5`{a6p=Bjf%Yea6M> z8R-(WIf1JUo@{qJsmg{vM*An}J1)ur(vG+&!NCS6SWp0U5m~Xw>VOa`82pS_4hUO% zBi|Z#E3#x)O>!3mqnOz>K!H?`2aV4Cy$4dEB_() zHyH3-Y&e$6i@KpfFn0JG$-;sYm=_*;=c~%5`}i}__TB3)56PL4smPq`uz%iY#V_`w zgtwwm^4fGzi~0=A!srlVLflz20P-E-4XxYvLkbX`2j8#p(sMTn3N|-&8DDM^A>QAy zA&^;sIT_++La1+&JaC$4#4Ic=9hPVd|EICDj*6=7+W3H^be9ZBNevw$-8poNgml9& zG}0|d34)X~f`o*0N_QhAAtene;Q)Ta^Stl(;m7mJJG0iTnLpz--Zx8irl7|mjv|@+VO9@U&t8Sae(2|PfN?K#bC8T=e-U3B zm3x>SZF8R$aR2r4CI!qpDai_T3Tl+>*5& z_jaLrClf5u6_nF96&D!POkFJoEyrh=K~q<}0wVevVUzpdIbdQoRJh^Cv}W107P%<#`t z0(n${K@gXUp+x!SH_X%*F1+jx&l?fakty5C)EhAuHW`czTfCyrT|M4pVulLjE?FnC ziiz4sI&~)#M?*eQ5Bi=6b7lHSRL2LlC?b7`M1c-zKZF!Zx^tORAoyKUy+`9Z^otAq zK{cI=3l(KLvt{p2(DuV#VTlzr@cl80${xIUzQPSx^fe^EA97D!^m^gZNuifg06l9x zJfGDvECwG<3i%?~kd>O{@oyLK1EHjDxnXwb?GByXd((OwA>GL%NP{t0nv`Ja&sz`T ze_bnA6@)J?iy+UT1_S^A9}56L@z0A(4Mlk+rGGCX@!<3w@ZXHbz zLu5nN-Vcp}6r8kk6d9y5K=}rCY}66N*EC7g{?>qaQVIeDbtLr9OwV)W0twkyr1Lvc z=@O`0`@1oVC_IkXcY6D_WC(I|ojaOiqxZIo2Hp)MJg9s`iiv_RQ*)MA`%1~J6aOSg zjeCKJhblQj3GfP!C8@6?K7zf|FAr=2HB(YFdjxjw52x?Q+%tW-$uqVRfEObj9Ysd; z)ody}Noo_ESOIMRLqEJrHqObhmQ%`uUbuKqe3WqeEtzI%hac8y#%~OKX#)8%5v(r}(B z-t4c+;~hc!j-QE>Qii&mqtB{>M?$-*!W+`xSIN0|t~lqZ$+DVsgv1R(8A^2XW$CEI zTxjDXggHVgUi5s=EmwZz9mCEQ>bf{!-0Q)b5a6#PC6wgPSE4rFuBhhA@=SUr$PnF) zJ|2ev@mqeSb#nsvpvij%%8GDCy&sf(Rxa&>5oFH&){bGfvJW)1WXoIjnveYBkqi#B zpOo}zYu{mpAHn;maE8GuIXZ3{MEk?NC$tN$Yx}+7S!qm1U-+CZa>^zP+;c6PD;yWQCz2d!&!{7Y9iYP zA_2}XCxqTmxnNM`$11Cu@e=##z)vkws?j)I_#^D3@(f(WJM&|dSQy?v)42uw->&s) z2=Hj{&jkR68qB?&)nBap2?9$Tqo>@w)A@(Z97;1N+9^Gu#_X$U3gX05J1M)fGimeN zYb4u2Imhj>KJ{845_YZK%(`YVu|1(A+lxlKY95Q>J#EE08*4@4eu!nW{@d7Q7}1#z zQt>1tN2v9&l=99LFtcYYw5e7(a6d`{-H}(|yuz!3+7DP#@~+>O2%XeQ24>4t)|U7t@?Z(3LjE$uI%^&GOZVFy7%qM#)Y{9$ z6W>z-A9m>Z$xC+g@3!mN3X5tRJqg~K?5j^B{g4Tob1YaJiXCV%YudF$&Go8d#hG54 zO>Qy>d{UI>oFe)zL&#ifbx+NnYR)kJtcLVmHB(Ga`UYgj{>Q44vRxa;QEz!=m9i}* zsEtE+G)2DxBS9kK=bZECUS@2kM#IaR>5bwCcZN=ZZat2s#KXWxmEDN~-%uy}FujBB z_4KFHdM1^XX>Qe>&Ca|zjycMo&vcvWgTPXnzS;h;8%@d3cszFi?in_OaORpYQI6LJ zn$GnH9itie`n!qOMWnH|_LoPzlC3S5o$4LV?_sxL_gZUx?0v|IbRdnWiD$wdFM0k{ z*Qyw6b(z`8<{^q((m=s^EWHKUr~F!@xy|YmSkE2zn1ktzCrREHgR{zZlggQWZCtVu zXs|e1rPBB1T63gMk+T(zEOt_E%15+6*eIl+DZ<}p576(u9E{3z0(;w`x;Zne>~35@ zpk4b?5FdHsSNF*fF(LgDYTA86?f3>=lC;`d*aN(+;f?|>B)*6nd(ahcvS?u^)`q@9ozfyUR# z?=pz-k^{t4gNH3Ya4giniaHL466a8m=db@HO8(v+J~Tqj78`YNvMr0p{oa1g4U01ALJ@rrXFl8^9cGEet zM{J93_A14(ib7}%7@CXJ2x!67QKZ#97>gU)#$tuRaDPH%8Rj+(P|8Y^KBT{W)?y;S zaOjZmg%b!dPS`v^ZvE=e`HfZJplfE!_2&!3fcy)qIx!(!<+~Jsj_Nj0p~EYM$uZ4p z%~Ey|cV8w`oYVFc!2lddn9Wx#jtK3LQCX55BiSX^>IGMCPvi&`JjOkSao z`LJ|t38)8isqun|o)YWrlW_V+tQ;&X6uo93Qhz=cGkxu`yDabm-%KYIwCaN+F5Dul z=0tCmSfjO77UgGbY6PWU(7kP@fxGl#+P+)###UxH(UZqOSPDQJry%|-bjQ%344=ls zyfMejQz&|QUrUXtn}d?8LA*3--Lu@<`CBvIR}-WzAcrH(5TU#qLF}TnBSG*dcZYdg zh;!_qp&*O;c|$u3%OS(*QGgfN66rq9B6Ze%4NnG>>F?^uyDiVdqB@a;y_{GWEf}nS z06bo#StGkL@3-4T>4(doQUb#6AjN)NFL07v^3-KqMW8<7pczi(Z}u z=sC`#HIOaYJRgbLm^R#4lcNTR^k;p>JPu~Ca|PS@k;vG@Z;fe7ptSU>(bStwDFz`( zY-;xW{7|u2W8+tr6uQ-ku=2n-S;17fvua?n|CKdPy;ic@&{C5bA0F?!jBnd@m&#JA z2=@d&szkE_?^+`G9G_a+5V(sQ(Oieb*_DmRl+_r$x=BvRwTqA zLR9N}7M}?@Bd;lseBRp(gHJ2)hgR3YR{*UiaTyOpdK;& z39W*P8KJqr0GwhDu5?LxlpG7vY> zW)@@-sJrZ7a;!DmIXX~P5!o5F>G2kl5{A`&EHa@&_u`MMwM)+nWuYGk6JKa|+OToN zvCydMCIR}8OL?JKHy$k69EfvKeB~)Ij%0d&vP%9^!7V}E*=m2G8OX!IO5TW*s1eI! z;}s)<5kFzU5jT*W;k%oO%8ha_pkMCCfcJOg?^;#*EL&c}VzL^fT9kbu6~Z;!Mc67A zuqMV}E_^atVxQ!y`bR6hVSBkcb3rmf83WLU(3<#>M;TGjLmI9YBT&mzS@(j*&-qZS zBD-La(itnXN!Th*`r@_WFP=|$-h@$wy5AfOe>7gUpW2GwZFw_GPU)-v`iqDjb6j&X zVmHxt#Y6QWtXhb+A@|cUxxsdy60GRAF)B8x6voMnj+z83LaKFXnY{@bo`FcaaX7G< z`~f_Oho%uX#>l(()|RW)8J-TP^xVS(E*b2~O-O5HLUD>Vg62iD6p8dYi$SKiiR1Ig zr9GaNYEDMl)&$)vG^zEp&Fy;I{m}J~bgH#jq0d^RdxMJz>G1n{xy+X6y^lMx5GeZO?G%+4 zHRHcvk8U7;YRtBL}bAX2#eA$5lok~ZT;%i`12Yv zyiTKl@SpaPoKeY?I&|@`tuq9ulc&ur42XL+s*)V)M_(F=B3eayk3{uJR3m3=om)2JAW<9$;Unkd3hjJO ze!X&Hi8XfuSZz0JQy2m9gIEDdKs$%Et4kQt@u)D55x+{!K9mCT{TZduFx<${c$AdDAVXPM?_DZ)@XRIP7%abjDntB~Vu4I8qi zxpI=EMu&1?*LAz$kr(YEfS69AwUgo>mV!IhrCBmsbCO^4NUMYdo%5%E*@MJWh=(xWn~OgJ4%T%CtpBpWZB9f#3$BbVc%IH>j}Q_3ZZPb2gfpr zOa+X>vT}oDIY0B)Xuh6fybN4xYPYR-&SJ{OAaii8J8<&^1*PBXGr^Am0-(uMlT?|F z1vnB~>2o_UUBxzA*r6W6vN~frI4?e_+g6T%y&{W+GUv^n21~6Y?`uCM+A-;9Z1+T& zFXj6B#cLerQv242lwt&JjMElFXD5f+yV8axJnoznoB8?zHUwHQX=Ap!Uyb^}lLX~S z7O$*XA`Ax%zZP*GPdyU4NN|&qYQ)K$_=K|IoN00JDz4Q2i(huZ4f1S@KXmMU2E^Vt zkYEewZ08f$)xza*4;S^~NM9KJfV%Z)|p`P(4E{^FFa;e$kO071x_z9XX-9B#FIN^O-C%_KwTMre>F`;x2qw zRReQv;4|R#K_Bw|lFa$q(8(hDPP%8 zqCakCnq$GUqr5i_Bzs{`)r5YoE`s1U#m!}YeIbbY*-iAim9k9;Kgai6ej%pS;{I^- zGaR;n*{+W#3`o3m2RQg5xP`km35O|Z^BH*wm>p@utia-NVwYh&A)vyJ$0iBsf}zOR zy^uo#y%@4PHr2B;K#G<)Iql5;W%Ea;YMMM!8)r~$lD+Uw@P1(Pf~u&P>{Cir-3#iG z=m*hOjJBO0v4*yVsAB(Dc|$HT7Y6!Z|f_c!b-YXl~ss7IY5Y5gcC(0h(>hKP1lSD&5#nygsYqHE{a&*uSebfz}fkm!?EdHwqf z8JIBaSDsF+c)V1HR)IBam0>ZOAZ`Z{4vtnyNk+@OiP3KY-$wQ=z7&29e<|7*Tw$H? z*npfKFO!m!8@Q%67;Kf%EBHX}f-EWqgZP5Xh|)T#t{FcX7|Dk?TsSto0g~8QIHcE0 zzB3z0#Nn#3i(FT;e@qrxzr5nDV6M>o=4(3h*TxDRzqnQ3ELzk}q02FL{0RqjS}~V8 z7q4Gx*0N0;EyqC4H&cBbbgB`t;i6lfBH1`N^TUa!A~jNPkonOY3oack(6Zb^0pQQZ zPppd9Gu#P2hjOV=g%$@n5C3eQ^)E(zo%40T$fVK6Gi}O}1+sRwFL1DWFYkH45d%po z(m-P3Y};dS4a@5_fX(?hq^Z@)MPQq_dci$6{}I&*wyK^&!G5^%rOG#@XY+j0hv$T# zh4(zXR@Jn>P6@Ukp*3@h^MsiTR<;{Xm-pW;H95=u%pg$Z=+Hl#%nN%%CD=W(INOR) ztCkm@cBvmhfl~*DNe|^W_X&;UHbYkFjb3Iwiul>$tPUJ1n7owGHwUa2W0h%>9bXWY z>7+SpAdVJrA@iPY+J+v{CFuLJt-27)*wm?6W=*FUo`_5?DWp8>u3_U1XO!1yQR{xw z&(EuXJ&@JtCVFSfe%5xsAA8n)5_`E<@9xj80T@b$aQnKua0my8Q`4OOqIdMO2qp1T zqJpt2h4=C4$V%U2H+-Zt(qIb}db0z#V0e8x8cXAD9fTgZ%qIf4>5MJ23d44-7H4 zFo)M}o0_})&A`8@e*Bw32>(7B00!7LfPI!YzcQ^d0RjN2EuU;zLa|xCHVCG^FVWO{XJpfUa_vwE3a61!`{E{JHyep zOszDyC9N+ceT)EZhq4Tp^78zav!2RrXik0*)C2@EH+6)V!IcwNXZqjdE0fwkjP^^! zZG&#)H(wIrNlU^fweWA`S&iGs96azetsOuvw$^rbAZv$z8uw2We>~w=hH#x;DMLzd zADDbh06_4!5Y|m@3xS&xWMXIM2)z=Ez)!!IdfHAI9J+fZzKv zU!QRWR<{N4AGc30%_l_{vk*L16A^+4L=JtfIuIk^7rmxf4B5$MrlkGH|ddn4k zHJg8TalKBJjk%TjyQk~zY2Gw+ybdf$xD|NQ-tl(mO#`Xx(2CSsq5r{73QoSVllsHu zbJHc}I$8G3ZRG#PGX@^Tm1oQ!qPSV6dOZrIceh4yOVKJkf~%s{KSXe|^yGR3KdWww zfb$;", + "subject": null, + "from": null, + "cc": [], + "received_at": null, + "source_channel": "Email" + }, + "body_current": "", + "body_thread_evidence": null, + "current_attachments": [], + "current_tables": [], + "current_ocr": [], + "parent_child_split_evidence": [], + "history_lookup": { + "performed": false, + "reason": null, + "evidence_summary": null + }, + "system_context": { + "queried": false, + "summary": null + }, + "candidate_events": [], + "unknowns": [] +} +``` + +以上根结构展示素材盘点的初始化 shape;其中 `candidate_events=[]` 不是合法的 Skill 调用包。实际调用前该数组必须非空。信息不可得时用 `null`、空数组或明确状态,不要猜。`candidate_events` 的唯一 item schema、合法 evidence refs、调用门槛和内部回传遵循 `04-main-skill-input-contract.md`。一个合法粗候选形如: + +```json +{ + "candidate_id": "C1", + "classification_status": "coarse_supported_signal", + "possible_event_types": ["Voucher Received", "Payment Evidence"], + "evidence_refs": [ + { + "source": "current_attachments", + "index": 0 + } + ], + "target_hints": [], + "uncertainty_codes": ["document_subtype_unresolved"] +} +``` + +Main Agent 只保证这是 current evidence 支持的 active 粗候选;不保证一个 candidate 对应一个最终事件,也不裁决最终 event type、subtype 或目标拆分。 + +需要酒店进行价格、退款、减免、账期、付款政策、合同条件或其他业务审批的询问,以及其他意图清楚但当前任务目录不支持的业务内容,不得伪装成 Trace。只要同邮件已经形成至少一个 active 粗候选,就在 `unknowns` 中逐项保留: + +```json +{ + "category": "unhandled_current_business_content", + "current_or_history": "current", + "reason_code": "requires_business_approval_or_unsupported_task_card", + "text_raw": "<当前原文>", + "case_keys": { + "group_code": null, + "confirmation_number": null, + "reservation_number": null, + "block_code": null + }, + "attachments": [], + "file_references": [] +} +``` + +`text_raw` 必须保留当前证据原文。`case_keys` 只能在当前证据或允许的历史证据唯一支持时填写;不唯一时保持全 `null`。附件字段只保留当前附件或当前 file reference。 + +调用 `booking-desk-event` 前,`candidate_events` 必须至少包含一个符合 `04-main-skill-input-contract.md` 的粗候选,且 `possible_event_types` 只能引用 `active_emittable_event_types`。输入可理解但没有 active 粗候选时输出 `S10`;输入不足、无法判断是否存在 active 粗候选时输出 `S99`。 + +当 `candidate_events` 非空且 `unknowns` 同时非空时,必须把两者一起交给 `booking-desk-event`。Skill 将最终 active 事件输出到 `message_events`,并把 `unhandled_current_business_content` 一对一规范化到最终 `unhandled_current_intents`;Main Agent 不得删减或自行改写最终业务事件。 + +Skill 返回内部 `internal_route=no_supported_event` 时,Main Agent 必须将其转换成正常 S10;不得把内部结构直接对外,也不得改成 S99 或业务复核。Skill 返回 `internal_route=candidate_package_contract_error` 时,报告给编排层并停止处理;不得把接口错误映射成 S10、S99 或 `Need Manual Review`。 + +当当前素材包含 QBD/LianTai table evidence 时,`current_tables` 必须尽量保留: + +- attachment/file name +- workbook/sheet +- row index / row label +- cell fill / highlight / yellow / red text / strikethrough evidence +- group code、人数、行程列原文、酒店列原文、备注列原文、酒店状态列原文 +- `hotel_date_raw`、`tour_date_raw`、`action_date_raw`、sheet month/year,如可读 +- current-row selection 的不确定点 + +当前附件内业务列被 yellow/highlight 的行都要作为 current effective row 交给 `booking-desk-event`;Main Agent 不用邮件标题日期过滤标黄行。 + +当当前素材显示 parent-to-child allocation creation 时,`parent_child_split_evidence` 必须保留: + +- parent group code +- parent original room summary,如当前证据可读 +- child group code 列表 +- `AMEND TO` / `AMED TO` / allocation / allotment / control block 等 split raw evidence +- parent-child 关系来源和不可读点 + +Main Agent 不裁决 parent 已释放或已取消,只保留 current evidence 并交给 `booking-desk-event` 输出候选事件。 + +## 6. 事件边界提示 + +Main Agent 只保留粗粒度信号、行级证据、目标提示和不确定点;以下最终拆分、合并和派生规则由 `booking-desk-event` 裁决: + +- 一个最终事件只对应一个主要目标对象;同一目标可以同时有主事件和一个按目标合并后的 linked Trace。 +- 多个 `group_code` 不得放进数组型 `case_keys.group_code`。 +- QBD/LianTai 每个 current effective row 必须独立处理,不得跨行合并主事件。 +- “一行”是隔离边界,不是绝对事件数量。普通情况下每行 × 每目标一个主事件;同一行可以按目标拆分,并可以产生契约明确要求的 linked/derived events。 +- QBD/LianTai 当前附件中业务列标黄/高亮的行全部作为 current effective row;只有序号列、标题、说明区或装饰单元格上色,不单独形成粗候选。 +- Parent-to-child allocation creation 按 child `group_code` 拆分,并保留 parent group、parent original room summary 和 child group 列表,用于额外输出 linked parent `Cancel Booking` candidate。 +- Rooming List 按目标对象拆分,并按业务规则派生 TA Recorder。 +- Extra bed、Meeting、meal、arrival notice、Guest Request 或其他预订补充信息与新订、改单或改团号同现时,按目标拆成 linked Trace 事件。 +- 同一封邮件、同一目标对象的多条补充信息合并成一个 Trace;完整原文按出现顺序写入 `trace_text`,每条信息分别写入 `trace_items`。 +- 多个目标对象必须分别生成 Trace,不得合并多个 `group_code`。 +- `notify_departments` 不清时使用空数组,不得仅因此进入人工复核。 +- 一个独立业务意图只能有一个主要覆盖路径;同一证据可以被契约要求的 linked/derived events 引用,但不得因此重复进入 `unknowns`。HTML/plain MIME alternatives 和相同 OCR 内容必须去重。 +- 一个连续请求跨多句话时保持为一个意图;互相独立的请求按当前证据顺序分别进入粗候选或 `unknowns`。 + +active 业务方向已知但无法安全拆分或字段不安全时,将当前原文、候选边界和不确定点写入素材包。最终 event type 和 subtype 已知时,Skill 保留原业务 event 并附非空 `manual_review`;只有 type 或 subtype 无法确定时才输出 `Need Manual Review`。只有调用 Skill 前输入不足、无法判断是否存在任何 active 信号时才输出 `S99`。 + +## 7. 内置结果 + +输入可理解但未匹配 current active 业务事件时输出 `S10`。`S10` 不表示邮件没有业务价值,也不表示用户无需查看、回复或进行其他处理: + +```json +{ + "source_message": { + "source_message_id": "", + "subject": null, + "from": null, + "cc": [], + "received_at": null, + "source_channel": "Email" + }, + "route_code": "S10", + "handler_type": "main_agent_outcome", + "result_type": "source_message_review_notification", + "current_or_history": "current", + "agent_assessment": { + "status": "no_booking_action_detected", + "reason_code": "no_booking_action_detected", + "automation_action": "none" + }, + "notification": { + "required": true, + "notification_type": "source_message_review", + "show_source_message": true, + "requires_user_decision": true, + "visible_message": "未匹配到当前 Agent 支持的业务事件类型,请查看原邮件并决定是否需要回复或进行其他处理。" + }, + "manual_review": null +} +``` + +当前输入不足,无法判断是否存在 current active 业务信号时输出 `S99`: + +```json +{ + "source_message": { + "source_message_id": "", + "subject": null, + "from": null, + "cc": [], + "received_at": null, + "source_channel": "Email" + }, + "route_code": "S99", + "handler_type": "main_agent_outcome", + "result_type": "source_message_review_notification", + "current_or_history": "current", + "agent_assessment": { + "status": "material_package_unavailable", + "reason_code": "material_package_unavailable", + "automation_action": "none" + }, + "notification": { + "required": true, + "notification_type": "source_message_review", + "show_source_message": true, + "requires_user_decision": true, + "visible_message": "当前输入不足,无法判断是否匹配当前 Agent 支持的业务事件类型,请查看原邮件并决定后续处理。" + }, + "manual_review": { + "reason_code": "material_package_unavailable", + "visible_reason": "当前输入不足,无法完成支持业务事件范围分类。", + "review_record_type": "main_agent_entry_review", + "missing_fields": [], + "blocking_points": [], + "conflicting_points": [], + "suggested_human_actions": ["review_source_message"], + "evidence_to_check": ["source_message"], + "known_fields": {} + } +} +``` + +`S10` 和 `S99` 共用源邮件查看通知通道,但 route code 和 `agent_assessment.status` 必须保持不同。两者都不得使用 `action_required` 替用户裁决是否介入;统一使用 `requires_user_decision=true`。`source_message_id` 只放在顶层 `source_message` 中,不在 notification 内重复。 + +## 8. 业务处理 + +所有预订部业务处理统一交给: + +```text +booking-desk-event +``` + +该 skill 以 `00-output-contract.md` 的 `active_emittable_event_types` 为唯一当前事件目录,负责最终 event type、subtype、目标拆分和派生事件,并把混合邮件中的未覆盖当前意图规范化到业务输出展示字段。`legacy_accepted_event_types` 只读兼容;`business_review_outcomes` 不属于粗候选。 + +之后由信息系统负责 Case 匹配、Preflight、真实任务创建、状态机、外部写入和 Receipt。 + +## 9. 最终输出交付 + +- 最终结果必须作为当前调用的结构化 JSON 参数对象直接返回,不得作为 JSON 字符串、Markdown 代码块或文件返回。 +- Step 0 失败时直接返回 `infrastructure_input_error`;它不是 S10、S99 或业务根。 +- 业务结果的根对象必须直接使用 `booking-desk-event` 的输出,不得增加 `booking_data`、`file`、`filename`、`artifact`、`download_url` 或其他文件包装层。 +- `S10`、`S99` 等 Main Agent 内置结果也必须直接返回对应的 JSON 参数对象。 +- 禁止创建、写入、上传、附加或返回任何结果 JSON 文件,包括 `booking_data.json`。 +- 禁止用结果文件名、文件路径、下载链接、artifact 或文件引用代替最终 JSON 参数对象。 +- 禁止在最终 JSON 前后增加解释性文字。 +- 收到 `booking-desk-event` 的业务结果后,必须将该 JSON 对象原样作为最终参数返回,不得二次序列化、转存或包装成文件。 +- 内部 `no_supported_event` 必须转换为 S10;内部 `candidate_package_contract_error` 必须报告给编排层并停止。这两个 `internal_route` 都不得作为最终结果直接返回,也不得映射成业务人工复核。 +- `candidate_events` 只属于 Main→Skill 内部素材包,不得出现在最终业务根、S10、S99 或 infrastructure error 中。 +- 业务结果必须保留 `booking-desk-event` 返回的顶层 `unhandled_current_intents`;不得把它删掉、合并进 `extraction_warnings` 或藏入事件 excerpt。 + +以上限制只针对最终处理结果,不限制输入附件处理。Excel、PDF、图片等输入附件仍可下载、解析和读取;`attachments`、`file_references` 可以继续作为输入证据保留在 JSON 事件中,但不得用它们代替最终 JSON 参数对象。即使事件很多或 JSON 很长,也不得主动将结果改为文件输出。 diff --git a/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/02_AI_Runtime/核心契约_展开阅读/00-output-contract.md b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/02_AI_Runtime/核心契约_展开阅读/00-output-contract.md new file mode 100644 index 0000000..0d66516 --- /dev/null +++ b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/02_AI_Runtime/核心契约_展开阅读/00-output-contract.md @@ -0,0 +1,416 @@ +# 输出契约 + +## 用途 + +定义 `booking-desk-event` 的统一 JSON 输出,以及 Main Agent 的 `S10/S99` 源邮件查看通知结果。所有业务事件都先是候选 `MessageEvent`,不是系统事实,不是真实 TaskCard。 + +## 输出交付形式 + +- 最终输出必须是当前调用直接返回的结构化 JSON 参数对象,不是 JSON 字符串或结果文件。 +- JSON 根对象必须直接使用本契约定义的顶层结构,不得增加 `booking_data`、`file`、`filename`、`artifact`、`download_url` 或其他文件包装层。 +- 不得创建、写入、上传、附加或返回 `booking_data.json` 或任何其他结果 JSON 文件。 +- 不得用结果文件名、文件路径、下载链接、artifact 或文件引用代替最终 JSON 参数对象。 +- 最终返回值不得使用 Markdown 代码块,也不得在 JSON 前后附加解释性文字。 +- 不得为交付形式增加 `output_mode`、`filename` 等非业务字段。 +- 输入附件引用仍可保留在事件的 `attachments` 或 `file_references` 中,但只能作为输入证据,不能替代最终 JSON 参数对象。 + +## Source Message Identity + +- 所有合规的业务输出、`S10` 和 `S99` 都必须包含 `source_message`。 +- `source_message.source_message_id` 必须是上游提供的非空值,用于通知系统关联和展示原邮件;不得猜测、生成或从其他编号替代。 +- subject、from、cc、received_at 等其他元数据不可得时使用 `null` 或空数组。 +- `null`、空字符串和纯空白字符串都视为缺失 `source_message_id`。 + +## Infrastructure Input Error + +Gateway 必须在读取正文、附件、OCR、历史或系统上下文,以及调用 Main Agent 或业务 skill 之前校验 `source_message_id`。缺失时只返回: + +```json +{ + "result_type": "infrastructure_input_error", + "error_code": "missing_source_message_id", + "retryable": true, + "missing_fields": [ + "source_message.source_message_id" + ] +} +``` + +该结果属于调用层,不属于业务输出、S10 或 S99,并明确豁免“结果必须包含 `source_message`”的业务规则。不得继续读取材料、调用 skill、生成通知、`message_events`、`unhandled_current_intents` 或 TaskCard。Main Agent 若仍收到该输入,必须使用相同结构作为防御性终态。 + +## Main Agent Notification Outcome + +`route_code` 是 S10/S99 结构的 discriminator。以下示例都是可直接返回的完整成品,不得在最终值中使用 `A | B` 表示二选一。 + +### S10 + +```json +{ + "source_message": { + "source_message_id": "", + "subject": null, + "from": null, + "cc": [], + "received_at": null, + "source_channel": "Email" + }, + "route_code": "S10", + "handler_type": "main_agent_outcome", + "result_type": "source_message_review_notification", + "current_or_history": "current", + "agent_assessment": { + "status": "no_booking_action_detected", + "reason_code": "no_booking_action_detected", + "automation_action": "none" + }, + "notification": { + "required": true, + "notification_type": "source_message_review", + "show_source_message": true, + "requires_user_decision": true, + "visible_message": "未匹配到当前 Agent 支持的业务事件类型,请查看原邮件并决定是否需要回复或进行其他处理。" + }, + "manual_review": null +} +``` + +### S99 + +```json +{ + "source_message": { + "source_message_id": "", + "subject": null, + "from": null, + "cc": [], + "received_at": null, + "source_channel": "Email" + }, + "route_code": "S99", + "handler_type": "main_agent_outcome", + "result_type": "source_message_review_notification", + "current_or_history": "current", + "agent_assessment": { + "status": "material_package_unavailable", + "reason_code": "material_package_unavailable", + "automation_action": "none" + }, + "notification": { + "required": true, + "notification_type": "source_message_review", + "show_source_message": true, + "requires_user_decision": true, + "visible_message": "当前输入不足,无法判断是否匹配当前 Agent 支持的业务事件类型,请查看原邮件并决定后续处理。" + }, + "manual_review": { + "reason_code": "material_package_unavailable", + "visible_reason": "当前输入不足,无法完成支持业务事件范围分类。", + "review_record_type": "main_agent_entry_review", + "missing_fields": [], + "blocking_points": [], + "conflicting_points": [], + "suggested_human_actions": ["review_source_message"], + "evidence_to_check": ["source_message"], + "known_fields": {} + } +} +``` + +- `S10` 固定使用 `status=no_booking_action_detected`、`reason_code=no_booking_action_detected`、`manual_review=null`。它表示输入可理解但未匹配 active event,不表示用户无需查看、回复或处理源邮件。 +- `S99` 固定使用 `status=material_package_unavailable`;`agent_assessment.reason_code` 必须与 `manual_review.reason_code` 一致,`manual_review.review_record_type=main_agent_entry_review`。 +- 两者均使用 `requires_user_decision=true`,不得增加 `action_required` 替用户判断是否介入。 +- `S99` 只用于输入不足、无法完成 active-event 范围分类。业务类型或 subtype 已知但字段、目标或证据不安全时,保留该业务 `event_type` 并使用非空 `manual_review`;只有类型或 subtype 无法确定时才使用 `Need Manual Review`。 + +## 业务事件顶层结构 + +```json +{ + "source_message": { + "source_message_id": "", + "subject": null, + "from": null, + "cc": [], + "received_at": null, + "source_channel": "Email" + }, + "message_events": [], + "case_candidates": [], + "extraction_warnings": [], + "unhandled_current_intents": [] +} +``` + +所有业务输出都必须固定包含以上五个顶层字段。没有未覆盖当前意图时,`unhandled_current_intents` 使用空数组。该字段只属于业务输出;`S10` 和 `S99` 保持既有入口通知结构,不增加该字段。 + +## Unhandled Current Intents + +`unhandled_current_intents` 用于展示同一邮件中意图清楚、具有当前业务意义,但现有事件目录或任务卡无法承接的内容。它不是 `MessageEvent`,信息系统不得据此自动创建 TaskCard。 + +每项固定使用: + +```json +{ + "category": "unhandled_current_business_content", + "current_or_history": "current", + "reason_code": "requires_business_approval_or_unsupported_task_card", + "text_raw": "<当前证据原文>", + "visible_message": "当前邮件包含未被现有任务类型覆盖的业务意图:<忠实中文概述>。请查看原邮件并决定后续处理。", + "requires_user_decision": true, + "case_keys": { + "group_code": null, + "confirmation_number": null, + "reservation_number": null, + "block_code": null + }, + "attachments": [], + "file_references": [] +} +``` + +固定规则: + +- 仅由当前正文、当前附件、当前 OCR、当前表格或当前继续处理指令产生;history-only 内容不得进入。 +- `text_raw` 原样保留;`visible_message` 提供忠实中文说明,不得增加批准、拒绝、执行或业务结论。无法安全翻译时使用“当前邮件包含未被现有任务类型覆盖的业务意图,请查看原文并决定后续处理。” +- `requires_user_decision` 固定为 `true`。 +- `case_keys` 始终包含四个键。仅在当前证据或允许的历史证据唯一支持时填写;否则使用 `null`,不得猜测。 +- `attachments` 和 `file_references` 只保留当前证据引用;历史附件不得带入。 +- 一个独立意图一个 item,按当前证据顺序输出;连续多句组成同一请求时保持一个 item。 +- HTML/plain MIME alternatives、重复 OCR、quoted thread 和已被事件完整承接的内容不得重复输出。 +- 清楚但不支持的业务意图使用本字段;业务类型和 subtype 已知但参数或目标不安全时,保留业务事件并附非空 `manual_review`;只有业务类型或 subtype 无法确定时才使用 `Need Manual Review`。内容不可读或抽取失败时使用 `extraction_warnings` 或相应业务复核。 +- `extraction_warnings` 只承载解析、OCR、抽取和证据质量问题,不得用来承载未覆盖业务意图。 +- `source_message_id` 只保留在根 `source_message`,不得在 item 内重复。 + +详细覆盖顺序遵循 `03-current-content-completeness.md`。 + +## Active Emittable Event Types + +- `New Booking` +- `Update Booking / Amendment` +- `Cancel Booking` +- `Cancel Allotment` +- `Voucher Received` +- `Payment Evidence` +- `Rooming List` +- `AMEND GROUP CODE` +- `Invoice Generation` +- `Invoice Received` +- `Payment Notice` +- `Trace` +- `Manual RateCode` +- `TA RECORDER` + +只有本列表中的值可以由当前 Agent 新生成,也只有本列表中的值可以进入 Main Agent 的粗候选 `possible_event_types`。 + +## Legacy Accepted Event Types + +- `Allotment Maintenance` +- `Note` + +以上值只允许读取历史旧数据,当前 Agent 不得新生成。普通预订补充信息统一输出 `Trace`;部分配额维护不再路由为 Update 或 Allotment Maintenance。 + +## Business Review Outcomes + +- `Need Manual Review` + +`Need Manual Review` 不是 active event,也不能放入 Main Agent 的 `possible_event_types`。它只用于业务类型或 subtype 本身无法确定的 Fallback 复核;业务类型和 subtype 已知时保留原 `event_type`。 + +## 单个事件 + +```json +{ + "event_type": "", + "event_role": "travel_agent_request", + "current_or_history": "current", + "source_event_index": "E1", + "case_keys": { + "group_code": null, + "confirmation_number": null, + "reservation_number": null, + "block_code": null + }, + "relevant_message_excerpt": "", + "attachments": [], + "file_references": [], + "context_used": {}, + "extracted_fields": {}, + "manual_review": null +} +``` + +`manual_review=null` 表示普通候选。`manual_review` 为完整 `business_event_review` 对象时,该事件仍保留原 `event_type`,但 adapter 必须把它作为对应业务卡的 manual-review mode;不得自动执行或写 PMS。 + +## Case Key + +- `case_keys.group_code` 必须是单值或 `null`。 +- 多个 group code 必须拆成多个事件。 +- `U-` 是价格或 Rate Code marker,绝不能写入 confirmation、reservation、booking reference 或 target key。 +- Parent allocation group code 是证据,不是 child event 的 `case_keys.group_code`。 + +## Date Evidence + +涉及入住、离店或晚数时,`extracted_fields` 应保留: + +```json +{ + "arrival_date": "2026-05-14", + "departure_date": "2026-05-15", + "nights": 1, + "date_evidence": { + "hotel_date_raw": "14-15", + "tour_date_raw": "2026/05/11\n2026/05/16", + "action_date_raw": "11/05 AMD", + "sheet_month_year": "BOOKING 05-2026", + "date_inference_basis": "hotel_date_range_with_tour_date_context" + } +} +``` + +日期识别必须遵循 `54-stay-date-parsing.md`。`action_date_raw` 是动作日期证据,不是入住日期。 + +## Trace Contract + +Trace 用于当前邮件中能绑定具体预订对象、但不属于主任务核心参数的补充业务信息。要求、安排、备注和包含具体预订事实的单纯告知都可以触发 Trace;不要判断发件人是否明确要求酒店记录、执行或转交。 + +同一封邮件、同一目标对象只输出一个 Trace。多条补充信息按当前证据顺序合并: + +```json +{ + "event_type": "Trace", + "extracted_fields": { + "trace_subtype": "extra_bed | general_request", + "trace_text": "<按当前证据顺序合并的完整补充信息原文>", + "trace_items": [ + { + "category": "extra_bed | room_preference | room_setup | meeting | function | meal | transport | payment_information | general_information", + "text_raw": "<单条原文>", + "service_date": "YYYY-MM-DD | null", + "service_period_raw": "<原始时段或 null>", + "pax": null, + "notify_departments": [] + } + ], + "notify_departments": [] + } +} +``` + +- `trace_text` 必须保留完整原文,结构化字段不能替代原文。 +- `trace_items` 的顺序必须与 `trace_text` 一致。 +- `category` 必须使用固定枚举;banquet 归入 `function`,无法归入更具体类别时使用 `general_information`。 +- `service_date`、`service_period_raw`、`pax` 只有在证据明确时填写,否则为 `null`。 +- item 的 `notify_departments` 只保留明确或规则唯一支持的部门;事件级 `notify_departments` 是 item 已知部门的去重合集。 +- 部门不清时使用空数组,不得仅因此输出人工复核。 +- 全部 item 都是 extra bed 时使用 `trace_subtype=extra_bed`;其他情况使用 `general_request`。 +- Trace 包含 extra bed item 时,继续在事件级 `extracted_fields` 保留原有 `occupancy_update`、`requires_rate_update` 和 `rate_adjustment_formula`,不得移动到 item 或删除。 +- 主事件已完整承接的核心参数不得重复生成 Trace。 +- 不同目标对象必须拆成不同 Trace;`case_keys.group_code` 仍为单值。 +- `payment_information` 只表示已经确定、需要随预订保留的补充付款安排;付款凭证、到账结果、Payment Notice、Invoice、催款或付款条件审批询问不得改名为 Trace。 + +`FYI guide will arrive at 20:00` 等具体预订告知可以触发 Trace;只有 `Thanks`、`Noted`、`Received`、裸 `FYI` 等没有具体业务信息的文字不能触发 Trace。需要酒店进行价格、退款、账期、付款政策、合同条件或其他业务审批的询问也不能伪装成 Trace。 + +不属于 Trace 但具有当前业务意义的内容必须原样保留在 Main Agent 素材包的 `unknowns` 中。只要同邮件存在至少一个支持事件,业务 skill 就按 `03-current-content-completeness.md` 将其一对一输出到顶层 `unhandled_current_intents`;不得静默忽略或改成 Trace。 + +## 人工复核 + +```json +{ + "manual_review": { + "reason_code": "", + "visible_reason": "", + "review_record_type": "business_event_review", + "missing_fields": [], + "blocking_points": [], + "conflicting_points": [], + "suggested_human_actions": [], + "evidence_to_check": [], + "known_fields": {} + } +} +``` + +固定规则: + +- 九个字段必须全部存在;`reason_code` 和 `visible_reason` 必须是非空字符串,`review_record_type` 固定为 `business_event_review`。 +- `missing_fields`、`blocking_points`、`conflicting_points`、`suggested_human_actions` 和 `evidence_to_check` 固定为数组;`known_fields` 固定为对象。 +- `missing_fields[]` 每项必须是指向当前 event 的 RFC 6901 JSON Pointer,例如 `/extracted_fields/room_items/0/pms_room_type_code`。不得放人类说明文字。 +- adapter 必须先按业务 `event_type` 和 subtype 判别字段得到原业务 task type/subtype,再依据 `manual_review` 是否为空派生 `normal_task` 或 `manual_review`。 +- 只有业务类型或 subtype 无法确定时,才使用 `event_type=Need Manual Review` 并派生 `manual_review + Fallback + business_event_review`。 +- `requires_downstream_hard_validation` 只用于契约明确的系统级候选校验,不得替代必需房型、Rate Code 或其他业务字段的人工确认。 +- 原 Agent event 保持不可变;用户覆盖值、`review_status`、解决人和解决时间由信息系统持有,全部缺失字段解决并重新校验后才进入 Preflight。 + +## 派生事件 + +派生或关联事件使用: + +```json +{ + "related_source_event_index": "", + "related_source_event_indices": [], + "related_event_type": "", + "relationship_type": "", + "requires_downstream_hard_validation": true +} +``` + +`related_source_event_index` 用于单一关联事件;一个派生事件关联多个来源事件时使用 `related_source_event_indices`。 + +典型场景: + +- New/Update/Amend Group Code 同事件出现 extra bed、Meeting、meal、arrival notice 或其他预订补充信息,按目标派生一个 linked `Trace`。 +- Rooming List 按每个目标 group 派生 `TA RECORDER`。 +- Parent-to-child allocation creation 必须派生独立的 parent release/cancel 候选 `message_event`,不得只放进 child `New Booking.extracted_fields`。 + +## Parent-To-Child Parent Candidate + +当 current evidence 确认 parent group 拆成 child group codes,且 parent group code 清楚时,必须额外输出一个 parent release/cancel candidate: + +```json +{ + "event_type": "Cancel Booking", + "event_role": "travel_agent_request", + "current_or_history": "current", + "source_event_index": "E_PARENT_RELEASE", + "case_keys": { + "group_code": "", + "confirmation_number": null, + "reservation_number": null, + "block_code": null + }, + "relevant_message_excerpt": "", + "attachments": [], + "file_references": [], + "context_used": { + "source": "current parent-to-child allocation evidence" + }, + "extracted_fields": { + "cancel_object_type": "group_block", + "parent_release_or_cancel_candidate": true, + "release_reason": "parent_to_child_allocation_split", + "parent_group_code": "", + "child_group_codes": [""], + "allocation_split_from_parent": true, + "parent_original_room_summary": null + }, + "related_source_event_indices": ["E1"], + "related_event_type": "New Booking", + "relationship_type": "linked_parent_release_after_child_split", + "requires_downstream_hard_validation": true, + "manual_review": null +} +``` + +该事件是候选事件,不代表 PMS 已取消成功,不创建真实 TaskCard,不写外部系统。 + +关系约束: + +- 一个 split cluster 只能有一个 parent candidate。 +- `child_group_codes[]` 不得重复,并与 `related_source_event_indices[]` 数量和顺序一一对应。 +- 每个相关 index 必须指向同一 `message_events[]` 中的 `New Booking`,其 `booking_object_type=Group Block` 且 `case_keys.group_code` 等于对应 child code。 +- Parent 始终使用复数 `related_source_event_indices[]`,即使只有一个 child。 +- Parent code 不得写入 child event 的 `case_keys.group_code`。 + +## 禁止输出 + +不得输出真实 TaskCard ID、最终 Case 状态、Payment 确认、Block Status 自动转换、Receipt、Invoice 文件、Opera/PMS 写入结果。 + +不得生成或返回结果 JSON 文件、结果文件引用、结果下载链接或 artifact。该限制只针对最终处理结果,不禁止下载、读取和解析输入附件,也不禁止在事件中保留输入附件证据。 diff --git a/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/02_AI_Runtime/核心契约_展开阅读/02-event-routing-map.md b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/02_AI_Runtime/核心契约_展开阅读/02-event-routing-map.md new file mode 100644 index 0000000..5ba4721 --- /dev/null +++ b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/02_AI_Runtime/核心契约_展开阅读/02-event-routing-map.md @@ -0,0 +1,82 @@ +# 事件路由地图 + +## 用途 + +从 current evidence 建立粗候选,再由 Skill 选择最终业务 event type、subtype、目标拆分和派生事件。Main Agent 与 Skill 的内部接口遵循 `04-main-skill-input-contract.md`。 + +`00-output-contract.md` 中的三个集合具有不同用途: + +- `active_emittable_event_types`:可以进入 `candidate_events` 并由当前邮件新生成。 +- `legacy_accepted_event_types`:只读历史兼容,不得作为当前 producer 或粗候选。 +- `business_review_outcomes`:业务类型未知时的复核 outcome,不得作为粗候选或 Skill 调用门槛。 + +## 路由顺序 + +0. Main Agent 先校验 `source_message.source_message_id`。缺失或空白时直接返回 `infrastructure_input_error`;不得读取正文、附件、历史或系统上下文,不得进入 S10/S99,也不得调用 Skill。 +1. Main Agent 按 `03-current-content-completeness.md` 盘点全部 current 业务内容,不得识别一个信号后停止。 +2. Main Agent 按 `04-main-skill-input-contract.md` 只形成 current evidence 支持的粗候选。它可以取得附件、OCR、表格和原始字段,但不作最终文件 subtype、event type 或拆分裁决。 +3. 输入可理解且没有 active 业务信号时,输出 `S10`,不调用 Skill。输入不足、无法判断是否存在任何 active 业务信号时,输出 `S99`,不调用 Skill。 +4. `candidate_events` 至少包含一个合法粗候选时才调用 Skill;同时存在的清楚但不受支持内容保留在 `unknowns`。 +5. Skill 重新验证 current evidence,并对最终 event type、subtype、目标拆分、事件合并和 linked/derived events 作唯一裁决。 +6. event type 和 subtype 已知但目标、字段、映射或证据不安全时,保留原 active `event_type`,并附非空 `manual_review`。只有 active 业务方向已确认但 event type 或 subtype 仍无法确定时,才输出 type-unknown `Need Manual Review`。 +7. Skill 确认所有粗候选均没有 active 事件时,返回内部 `no_supported_event`,由 Main Agent 转换为 S10;该内部结构不得直接对外。素材包结构非法时返回内部 `candidate_package_contract_error`,不得映射成 S10、S99 或业务复核。 +8. 能安全拆分时按目标独立路由,并把未覆盖当前意图一对一输出到顶层 `unhandled_current_intents`。 + +只有 Thank you、裸 FYI、acknowledgement、Noted、Received 等没有具体预订业务信息的文字不匹配 Trace。FYI 或单纯告知只要包含与明确预订对象相关的具体补充信息,就路由为 `Trace`。一般咨询、Booking Confirmation Request、部分配额维护和当前不支持的业务请求不得伪装成 Trace。 + +当一封邮件同时包含 active 事件和当前不支持的业务请求时,不得因为 active 事件已命中而忽略剩余请求,也不得把整封邮件降级为 S10。支持部分正常路由,未覆盖部分进入 `unhandled_current_intents`。 + +## Active 路由表 + +下表只能产生 `active_emittable_event_types` 中的精确 event type: + +| 当前信号 | 最终事件类型 | 读取 | +| --- | --- | --- | +| 新建 FIT、Group Block、Allotment、Control Block | `New Booking` | `10-new-booking.md` | +| Parent-to-child allocation creation | child `New Booking` + linked parent `Cancel Booking` candidate | `31-allotment-control-block.md` | +| 修改已有 FIT/Group 订单的日期、晚数、房型、房量、人数、价格或其他主订单字段;不含独立部分配额维护 | `Update Booking / Amendment` | `11-update-booking.md` | +| 整单取消、CXL、release/cancel reservation 或 group block | `Cancel Booking` | `12-cancel-booking.md` | +| 明确整块取消 Allotment / Control Block | `Cancel Allotment` | `12-cancel-booking.md` | +| 当前 credit voucher 文件 | `Voucher Received` | `13-voucher-payment.md` | +| 当前 bank transfer、cash deposit 或 transaction receipt 文件 | `Payment Evidence` | `13-voucher-payment.md` | +| 当前名单、分房表、guest list | `Rooming List` | `14-rooming-list.md` | +| 旧 Group Code 改新 Group Code | `AMEND GROUP CODE` | `15-amend-group-code.md` | +| 与具体预订对象相关、但不属于主任务核心参数的当前补充信息 | `Trace` | `16-trace-notes.md` | +| Rooming List 目标需要 TA Recorder | `TA RECORDER` | `17-ta-recorder-note.md` | +| 明确请求生成 Proforma Invoice | `Invoice Generation` | `18-invoice.md` | +| 当前收到已有或 revised invoice | `Invoice Received` | `18-invoice.md` | +| 当前清楚的付款通知,不是付款政策审批 | `Payment Notice` | `18-invoice.md` | +| 单独手工价格或 Rate Code 维护 | `Manual RateCode` | `53-manual-rate-code.md` | + +## Legacy 与范围外内容 + +- `Note` 和 `Allotment Maintenance` 属于 `legacy_accepted_event_types`,不得进入 `candidate_events`,不得由当前邮件新生成。 +- 部分配额维护、减少但保留余量、部分日期或部分房量 release 当前不受支持。单独出现时由 Main Agent 输出 S10;与 active 事件同现时原文进入 `unknowns`,最终展示在 `unhandled_current_intents`。 +- 独立新建 Allotment / Control Block 与明确整块 `Cancel Allotment` 仍属于 active 范围。 +- `Need Manual Review` 属于 `business_review_outcomes`,不是 active event type,不能独立满足 Skill 调用门槛。 + +## 冲突优先级 + +先按独立 current 意图拆分,再只对同一意图的多个候选应用以下优先级: + +- 当前只有 voucher/payment proof,即使标题像 NEW,也优先 voucher/payment。 +- Rooming List 文件不得当成 booking update 表。 +- `AMEND GROUP CODE TO` 且 old/new 清楚时,优先 `AMEND GROUP CODE`。 +- Parent-to-child allocation creation 不因出现 `AMEND` 字样就当普通改单。 +- Extra bed alone 是 Trace,不是房量修改。 +- 先识别主任务完整承接的核心参数,再把剩余的具体预订补充信息按目标生成 Trace;不得为同一核心参数重复生成 Trace。 +- 已确定、需要随预订保留的补充付款安排可以是 Trace;付款凭证、到账结果、Payment Notice、Invoice、催款或需要酒店批准的价格、退款、减免、账期、付款政策、合同条件询问不是 Trace。 +- Cancel 行不会因为历史上有 guest request 就自动生成 Trace。 + +## 行级隔离与拆分 + +- QBD/LianTai 每个 current effective row 必须独立处理,不得跨行合并主事件。 +- “一行”是隔离边界,不是绝对事件数量。普通情况下每行 × 每目标一个主事件;同一行可以按多个目标拆分,也可以产生契约明确的 linked/derived events。 +- Parent-to-child allocation 按 child group code 拆,并额外输出 parent linked `Cancel Booking` candidate。 +- Extra bed、Meeting、meal、arrival notice、guest request 或其他预订补充信息与主业务同现时,按目标拆 linked Trace。 +- 同一封邮件、同一目标对象的多条补充信息合并成一个 Trace;多个目标对象分别生成 Trace。 +- Rooming List 按目标 group 拆,并按业务规则派生 TA Recorder。 + +拆分后业务类型已知但字段或目标不安全时,保留对应业务 event 并附 `manual_review`。只有 event type 或 subtype 本身无法确定时才使用 `Need Manual Review`。 + +不符合 Trace 但具有当前业务意义的内容必须以 `unknowns[].category=unhandled_current_business_content` 保留原文。同邮件存在至少一个 active 事件时输出到顶层 `unhandled_current_intents`;整封邮件没有 active 事件时由 Main Agent 输出 S10。不得静默忽略。 diff --git a/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/02_AI_Runtime/核心契约_展开阅读/03-current-content-completeness.md b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/02_AI_Runtime/核心契约_展开阅读/03-current-content-completeness.md new file mode 100644 index 0000000..ea7b11d --- /dev/null +++ b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/02_AI_Runtime/核心契约_展开阅读/03-current-content-completeness.md @@ -0,0 +1,121 @@ +# 当前邮件内容完整覆盖 + +## 用途 + +确保当前邮件中每一项有业务意义的内容都有明确去向。不得因为已经匹配一个支持业务事件,就停止读取或静默丢弃同一邮件中的其他当前意图。 + +本规则是邮件级共享规则,优先于具体事件 reference。它不扩大任务卡能力,也不把当前不支持的内容伪装成 `Trace` 或业务人工复核。 + +## 当前内容盘点 + +盘点范围包括: + +- `body_current` 中的请求、询问、安排、事实和告知。 +- 当前附件、inline image、PDF、spreadsheet、OCR、表格和文件链接中的业务内容。 +- 当前邮件明确继续处理的上文对象。 + +不作为独立业务内容: + +- greeting、signature、disclaimer 和纯礼貌文字。 +- HTML 与 plain-text MIME alternatives 中语义相同的重复内容。 +- quoted thread、forwarded old mail 和其他 history-only 内容。 + +一个连续请求即使跨多句话,仍作为一个意图;互相独立的请求必须拆开,并按当前证据顺序保留。 + +## 完整覆盖不变量 + +每项有业务意义的当前内容必须且只能进入以下一个结果路径: + +1. 匹配支持事件并安全处理:普通 `message_event`,包括 linked `Trace`。 +2. 已匹配 active event 且业务类型和 subtype 已知,但参数、目标或证据不安全:保留该业务 `message_event` 并使用非空 `manual_review`。 +3. 已确认存在 active 业务方向,但业务类型或 subtype 本身无法安全确定:输出 `event_type=Need Manual Review` 的 Fallback 复核。 +4. 意图清楚但现有事件或任务卡不支持,且同邮件还有至少一个支持事件:最终 `unhandled_current_intents`。 +5. 意图清楚但整封邮件没有任何支持事件:Main Agent 输出 `S10`。 +6. 输入不足,无法判断是否匹配支持事件:Main Agent 输出 `S99`。 + +`Need Manual Review` 不是 active event,也不能用于满足 Main→Skill 调用门槛。路径 2 与路径 3 的区别是:路径 2 已知业务卡型,因此保留原业务 event;路径 3 连业务类型或 subtype 都不能确定,因此才使用 Fallback。 + +不得用 `relevant_message_excerpt`、源邮件仍可查看或 `extraction_warnings` 代替上述覆盖结果。 + +## Main Agent 内部 unknowns + +当同邮件已经匹配至少一个支持事件,以下当前内容进入素材包 `unknowns`: + +- 需要酒店批准的价格、退款、减免、豁免、账期、付款政策或合同条件询问。 +- 意图清楚、具有业务意义,但当前支持事件目录或任务卡无法承接的其他内容。 + +内部结构使用: + +```json +{ + "category": "unhandled_current_business_content", + "current_or_history": "current", + "reason_code": "requires_business_approval_or_unsupported_task_card", + "text_raw": "<当前证据原文>", + "case_keys": { + "group_code": null, + "confirmation_number": null, + "reservation_number": null, + "block_code": null + }, + "attachments": [], + "file_references": [] +} +``` + +`case_keys` 只能使用当前证据或允许的历史证据唯一支持的值;不能唯一绑定时保持全 `null`。目标不清本身不阻止邮件级展示,也不得为了填写 key 而猜测。 + +## 业务输出映射 + +`booking-desk-event` 必须把上述 `unknowns` 按原顺序一对一规范化到业务输出顶层 `unhandled_current_intents`: + +```json +{ + "category": "unhandled_current_business_content", + "current_or_history": "current", + "reason_code": "requires_business_approval_or_unsupported_task_card", + "text_raw": "<当前证据原文>", + "visible_message": "当前邮件包含未被现有任务类型覆盖的业务意图:<忠实中文概述>。请查看原邮件并决定后续处理。", + "requires_user_decision": true, + "case_keys": { + "group_code": null, + "confirmation_number": null, + "reservation_number": null, + "block_code": null + }, + "attachments": [], + "file_references": [] +} +``` + +规则: + +- `text_raw` 必须原样保留,不得只留翻译或摘要。 +- `visible_message` 必须忠实说明原意,不得增加批准、拒绝、执行或业务结论。无法安全翻译时使用“当前邮件包含未被现有任务类型覆盖的业务意图,请查看原文并决定后续处理。” +- `requires_user_decision` 固定为 `true`。 +- 只保留当前附件和当前 file reference;历史附件不得带入。 +- `source_message_id` 只使用业务输出根对象中的值,不在 item 内重复。 +- 该 item 不是 `MessageEvent`,没有 `event_type`,不得创建 TaskCard 或触发外部写入。 + +## 去重与边界 + +- HTML/plain MIME 重复、相同 OCR 重复和签名引用不得生成重复 item。 +- 每个独立未覆盖意图一个 item;不得把不同问题压成模糊摘要。 +- 已由主事件或 Trace 完整承接的内容不得再次进入该数组。 +- 已确定的补充付款安排可以是 `Trace.payment_information`;询问酒店是否批准付款安排进入未覆盖意图。 +- 内容不可读或语义不足时,不得伪装成清楚的未覆盖意图;按事件上下文使用 `extraction_warnings`、type-known business review、type-unknown Fallback review 或 `S99`。 +- `extraction_warnings` 只承载解析、OCR、抽取和证据质量问题,不承载清楚但不受支持的业务意图。 + +## 当前不支持的配额维护 + +- 独立新建 Allotment/Control Block 仍属于 active `New Booking`。 +- 明确整块取消仍属于 active `Cancel Allotment`。 +- 明确减少部分房量、修改部分日期或保留剩余配额继续使用,不再属于 active event;整封邮件只有该意图时输出 S10,与其他 active event 同现时进入 `unhandled_current_intents`。 +- 无法判断是完整 Parent split/整块取消还是部分维护时,业务类型或 subtype 不明确,使用 Fallback 业务复核;不得生成 Parent Cancel 候选。 + +## 示例 + +- 当前付款凭证 + “余款能否入住时支付”:输出 `Payment Evidence`,并输出一个未覆盖意图。 +- “余款将在入住时支付”且目标唯一:输出 `Trace.payment_information`,不输出未覆盖意图。 +- 当前只有清楚的付款政策询问,没有任何支持事件:Main Agent 输出 `S10`,不调用业务 skill。 +- 历史中有审批询问、当前只有 `Thanks`:不得从历史生成未覆盖意图。 diff --git a/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/02_AI_Runtime/核心契约_展开阅读/04-main-skill-input-contract.md b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/02_AI_Runtime/核心契约_展开阅读/04-main-skill-input-contract.md new file mode 100644 index 0000000..97a3238 --- /dev/null +++ b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/02_AI_Runtime/核心契约_展开阅读/04-main-skill-input-contract.md @@ -0,0 +1,144 @@ +# Main Agent 到 Skill 的内部输入契约 + +## 用途 + +定义 Main Agent 交给 `booking-desk-event` 的素材包边界、粗候选结构、调用门槛和内部回传。这个接口只存在于 Main Agent 与 Skill 之间;`candidate_events` 和 `internal_route` 都不是最终对外输出。 + +Main Agent 负责入口校验、取得当前素材、OCR/表格原始抽取、current/history 分离、粗候选和未覆盖当前意图。Skill 对最终 event type、subtype、目标拆分、事件合并以及 linked/derived events 拥有唯一裁决权。 + +## 目录 + +- Step 0:Source Message Identity +- 事件集合 +- Candidate Event Item +- 调用门槛 +- Skill 最终裁决与内部回传 +- 行级与目标级边界 + +## Step 0:Source Message Identity + +在读取正文、附件、OCR、历史或系统上下文前,先校验 `source_message.source_message_id`。缺失、`null`、空字符串或只包含空白时,不得建立素材包或调用 Skill,固定返回输出契约定义的: + +```json +{ + "result_type": "infrastructure_input_error", + "error_code": "missing_source_message_id", + "retryable": true, + "missing_fields": ["source_message.source_message_id"] +} +``` + +该结果不是 S10、S99、业务事件或人工复核,不展示源邮件通知,不创建 TaskCard。不得猜测、生成、修剪后替换或从 confirmation/group/reservation number 推导 `source_message_id`。 + +## 事件集合 + +事件集合的唯一权威定义在 `00-output-contract.md`: + +- `active_emittable_event_types`:当前可以由新邮件生成的普通业务 event type。 +- `legacy_accepted_event_types`:只用于读取历史兼容数据;不得进入 `candidate_events`,不得由当前邮件新生成。 +- `business_review_outcomes`:业务复核 outcome;不得进入 `candidate_events`,也不能独立满足 Skill 调用门槛。 + +`possible_event_types` 只能使用 `active_emittable_event_types` 中的精确值。S10、S99、`Need Manual Review`、`Note`、`Allotment Maintenance` 或其他 legacy/review/入口值都不是合法粗候选类型。 + +## Candidate Event Item + +每个相互独立、由 current evidence 支持的业务信号形成一个粗候选: + +```json +{ + "candidate_id": "C1", + "classification_status": "coarse_supported_signal", + "possible_event_types": ["Voucher Received", "Payment Evidence"], + "evidence_refs": [ + { + "source": "current_attachments", + "index": 0 + } + ], + "target_hints": [ + { + "case_keys": { + "group_code": null, + "confirmation_number": null, + "reservation_number": null, + "block_code": null + }, + "target_locator_raw": null + } + ], + "uncertainty_codes": ["document_subtype_unresolved"] +} +``` + +固定规则: + +- `candidate_id` 必须匹配 `^C[1-9][0-9]*$`,并在本素材包中唯一。 +- `classification_status` 固定为 `coarse_supported_signal`,表示 Main Agent 没有作最终业务裁决。 +- `possible_event_types` 必须非空、去重,只能使用 active 集合精确值;可以保留多个候选类型。 +- `evidence_refs` 必须非空,只能引用 current 素材。`source` 只能是 `body_current`、`current_attachments`、`current_tables`、`current_ocr` 或 `parent_child_split_evidence`;`index` 必须是存在于对应素材中的非负整数索引,`body_current` 固定使用 `0`。 +- `target_hints` 是非权威提示,可以为空或包含多个 item。每个 item 固定包含四键 `case_keys` 和原始 locator;不唯一时保持 `null`,不得猜测。 +- `uncertainty_codes` 可以为空;有不确定性时使用稳定 reason code,不用自然语言结论替代。 +- history-only 内容不得创建粗候选。历史只能在 current 粗候选已经存在后绑定目标、旧值或上下文。 +- 每个相互独立的 current 信号都必须有自己的粗候选;同一行或同一目标中的主业务信号和补充 Trace 信号也分别建 candidate。Skill 再按最终目标和业务规则合并、拆分或建立 linked relationship。 +- 一个粗候选不等于一个最终事件。Skill 可以拆成多个目标事件、合并同目标信号,或生成契约明确要求的 linked/derived events。 +- 最终事件在 `context_used.main_candidate_ids` 中记录所使用的 `candidate_id`;一个事件可关联多个 candidate,一个 candidate 也可关联多个最终事件。最终输出只允许保留这些 ID 作为 provenance,不得复制 `candidate_events` item、`possible_event_types`、`uncertainty_codes` 或任何 `internal_route`。 + +## 调用门槛 + +完成全部 current 内容盘点后: + +1. 输入可理解,且没有任何 active 业务信号:Main Agent 输出 S10,不调用 Skill。 +2. 输入不足,无法判断是否存在任何 active 业务信号:Main Agent 输出 S99,不调用 Skill。 +3. `candidate_events` 至少有一个满足本契约的粗候选:Main Agent 调用 Skill,并同时传递 `unknowns`。 + +仅有 unsupported/legacy 内容、`unknowns` 或 review outcome 不能满足调用门槛。正文只有泛化 `see attached` 且附件无法取得、方向不可识别时走 S99;动作方向已明确为 active 事件但必要附件不可读时,仍形成粗候选,由 Skill 生成业务复核结果。 + +## Skill 最终裁决 + +- Skill 必须重新验证 current evidence,不得把 `possible_event_types` 当作最终结论。 +- event type 和 subtype 已知但字段、目标、映射或证据不安全时,保留该 active `event_type`,并附非空 `manual_review`;`manual_review.missing_fields[]` 使用 RFC 6901 JSON Pointer。 +- 只有 active 业务方向已确认,但最终 event type 或 subtype 仍无法确定时,才使用 `business_review_outcomes` 中的 `Need Manual Review`。 +- 业务类型已知的 manual-review event 和 type-unknown `Need Manual Review` 都是业务输出,不得改成 S99。 + +如果 Skill 验证后确认所有粗候选都没有 active 事件,返回以下内部结果: + +```json +{ + "internal_route": "no_supported_event", + "reason_code": "skill_validation_found_no_active_event", + "candidate_ids": ["C1"] +} +``` + +Main Agent 必须把该内部结果转换成正常 S10;不得把 `internal_route` 直接对外返回,也不得把它转换成 S99 或业务人工复核。 + +`no_supported_event.candidate_ids` 必须非空,列出本次已完成验证且均被否定的全部粗候选 ID。 + +如果素材包或 candidate item 违反本契约,Skill 返回内部 contract error: + +```json +{ + "internal_route": "candidate_package_contract_error", + "reason_code": "invalid_candidate_package", + "candidate_ids": ["C1"], + "contract_errors": [ + { + "path": "/candidate_events/0/possible_event_types/0", + "code": "not_active_emittable_event_type" + } + ] +} +``` + +`contract_errors[].path` 使用 RFC 6901 JSON Pointer。该内部错误报告给编排层并停止本次业务处理;不得直接对外,不得映射为 S10、S99 或 `Need Manual Review`。 + +`candidate_package_contract_error.contract_errors` 必须非空。`candidate_ids` 保留仍可安全读取的候选 ID;如果连 ID 都不合法,使用空数组。 + +## 行级与目标级边界 + +QBD/LianTai 当前有效行必须独立处理,不能把不同 current rows 合并成一个主事件。这里的“一行”是隔离边界,不是绝对事件数量: + +- 普通情况下,一个 current row × 一个目标产生一个主事件。 +- 同一行含多个目标时由 Skill 按目标拆分。 +- Parent-to-child split、linked parent cancel、linked Trace、TA Recorder 和其他契约明确的 derived event 可以在同一行主事件之外额外产生。 +- Main Agent 只保留行、highlight、目标提示和不确定点;最终拆分由 Skill 完成。 diff --git a/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/02_AI_Runtime/核心契约_展开阅读/90-manual-review.md b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/02_AI_Runtime/核心契约_展开阅读/90-manual-review.md new file mode 100644 index 0000000..c561cbe --- /dev/null +++ b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/02_AI_Runtime/核心契约_展开阅读/90-manual-review.md @@ -0,0 +1,158 @@ +# 人工复核 + +## 入口结果边界 + +Gateway 必须先校验 `source_message.source_message_id`。该值为 `null`、空字符串或纯空白时,直接返回 `00-output-contract.md` 定义的 `infrastructure_input_error`;不得读取正文、附件、OCR、历史或系统上下文,也不得调用 Main Agent 或 `booking-desk-event` skill。 + +source message identity 合法后,Main Agent 才完成支持范围分类: + +- 当前输入可理解,但没有匹配 active event 时,输出 `S10`。 +- 当前输入不足,无法判断是否匹配 active event 时,输出 `S99`。 +- `S10` 和 `S99` 都展示源邮件并由用户自行决定是否回复或进行其他处理。 +- `S10` 和 `S99` 不由 `booking-desk-event` skill 输出。 + +Thank you、裸 FYI、acknowledgement、Noted、Received 等没有具体预订业务信息的文字,以及一般咨询和当前不支持的 Booking Confirmation Request,都不匹配 Trace。FYI 或单纯告知中包含与明确预订对象相关的具体补充信息时,匹配 `Trace`。正文只有 `see attached` 且附件无法取得、无法识别业务方向时走 `S99`。 + +需要酒店批准的价格、退款、减免、账期、付款政策或合同条件询问不是 Trace。此类具有当前业务意义但没有任务卡承接的内容必须保留在 `unknowns`:同邮件存在支持事件时输出到 `unhandled_current_intents`,整封邮件没有支持事件时走 S10。意图清楚但不受支持不等于业务判断不安全,不得仅因此输出业务人工复核。 + +## 两种业务人工复核 + +### 已知业务卡型的人工复核 + +只要业务 `event_type` 和 subtype 已经能够确定,就必须保留该业务事件,继续输出能够安全抽取的字段,并令该 event 的 `manual_review` 为完整 `business_event_review` 对象。不得仅因为房型、Rate Code、目标对象或某项证据存在歧义,就把事件改成 `Need Manual Review`。 + +常见情形包括: + +- target object 不明确或不唯一,但业务卡型已确定; +- current/history 边界不明确,但业务卡型已确定; +- 已确定业务卡型,但其附件、OCR、表格或必要证据不可读; +- required room type、Rate Code、settlement price 或其他必需字段无法唯一确定; +- QBD/LianTai row、highlight 或 current-row evidence 缺失或歧义; +- system context 显示 duplicate、pending/open task、active workflow、lock 或 conflict。 + +例如,正文明确 `please update the booking as attached`,因此业务卡型已经确定为 Update;附件不可读时仍输出对应 Update event,把安全取得的订单号、客人名等保留下来,并附非空 `manual_review`。Adapter 据此派生 `manual_review + 原业务 task_type + 原业务 subtype`,而不是 Fallback。 + +### 业务类型或 subtype 未知的 Fallback + +只有业务 `event_type` 或 subtype 本身无法确定时,才输出 `event_type=Need Manual Review`。Adapter 将其派生为: + +```text +manual_review + Fallback + business_event_review +``` + +例如,同一段 current action 同时可能表示 New Booking 或 Update,且没有足够证据裁决,业务类型无法确定,此时才走 Fallback。 + +不得为了满足 Main Agent 的 skill-call gate 而制造 `Need Manual Review`。如果 Main 给出的所有粗候选都被 skill 否定,skill 返回内部 `no_supported_event` disposition,由 Main 转成 S10。 + +## 必要结构和字段类型 + +业务级 `manual_review` 必须完整包含九个字段: + +```json +{ + "reason_code": "room_type_mapping_multiple_candidates", + "visible_reason": "房型原文可识别,但无法唯一映射到当前有效 PMS 房型代码。", + "review_record_type": "business_event_review", + "missing_fields": [ + "/extracted_fields/room_items/0/pms_room_type_code" + ], + "blocking_points": [ + "pms_room_type_code is required before Preflight" + ], + "conflicting_points": [], + "suggested_human_actions": [ + "select_room_type_from_active_pms_catalog" + ], + "evidence_to_check": [ + "current_body", + "current_attachments" + ], + "known_fields": { + "room_type_raw": "SUITE" + } +} +``` + +固定规则: + +- 九个字段必须全部存在。 +- `reason_code` 和 `visible_reason` 必须是非空字符串。 +- `review_record_type` 固定为 `business_event_review`。 +- `missing_fields`、`blocking_points`、`conflicting_points`、`suggested_human_actions`、`evidence_to_check` 固定为字符串数组;无内容时使用空数组。 +- `known_fields` 固定为对象;没有已知值时使用空对象。 +- `missing_fields[]` 每一项必须是指向当前 event 的 RFC 6901 JSON Pointer,例如 `/extracted_fields/room_items/0/pms_room_type_code`。不得填写人类说明、点号路径或根邮件路径。 +- 不新增 `required_user_inputs`;前端业务卡字段注册表负责将 pointer 映射到可编辑控件。 +- pointer 无法映射到该业务卡的已知可编辑字段时,属于 adapter contract error,不能展示一个无法完成的复核卡。 + +## 房型和其他必需字段 + +房型原文可以识别但映射不唯一时: + +- 保留 `room_type_raw`; +- `pms_room_type_code=null`; +- 在 `manual_review.missing_fields` 中登记对应的 JSON Pointer; +- 用户只能从信息系统提供的当前有效 PMS 房型目录中选择,不得由 Agent 猜测,也不得允许用户自由填写 PMS code。 + +Rate Code、价格或其他业务必需字段遵循相同原则:保留安全抽取值,待用户确认的目标字段置为 `null` 并用 pointer 标记。`requires_downstream_hard_validation` 不能替代这些字段的人工确认;也不得写成“人工复核或 downstream validation 二选一”。 + +一个 event 进入人工复核时,不得清空同邮件中的 sibling events。每个 event 独立派生路由:确定且完整的 sibling 仍为 normal task,需要确认的 sibling 保留其业务 task type/subtype 并进入 manual-review mode。 + +## 同卡解阻状态 + +Agent 输出只描述初始业务判断,保持不可变。信息系统在同一张业务卡上维护复核状态,初始为: + +```json +{ + "review_status": "pending", + "review_resolution": null +} +``` + +用户确认后保存: + +```json +{ + "review_status": "resolved", + "review_resolution": { + "field_overrides": [ + { + "field_pointer": "/extracted_fields/room_items/0/pms_room_type_code", + "value": "SU1" + } + ], + "resolved_by": "", + "resolved_at": "" + } +} +``` + +固定规则: + +- 原 Agent payload 和初始 `result_type=manual_review` 不得改写;当前页面模式由 `review_status` 控制。 +- `field_overrides[].field_pointer` 必须与待解决字段 pointer 对应,覆盖值和审计信息由信息系统持有。 +- 只有所有缺失字段完成、目录值校验通过、依赖字段重新校验通过后,才允许进入现有 Preflight。 +- 在 Preflight 完成前不得写 PMS。 +- 不创建第二张 linked normal task;解决后仍是同一张业务卡。 +- 业务字段可以按权限编辑;邮件原文、raw evidence、source ID 和审计字段只读。 + +## 常见 Reason Code + +- `current_history_boundary_unclear` +- `target_object_unclear` +- `multiple_target_candidates` +- `attachment_or_ocr_unreadable` +- `event_type_conflict_unclear` +- `subtype_unresolved` +- `room_type_mapping_unconfirmed` +- `room_type_mapping_multiple_candidates` +- `rate_code_unconfirmed` +- `rate_code_rule_not_covered` +- `settlement_price_required` +- `manual_price_unconfirmed` +- `composite_unit_price_unconfirmed` +- `fix_charge_unconfirmed` +- `qbd_liantai_row_evidence_unreadable` +- `parent_child_split_evidence_unclear` +- `stay_date_inference_unclear` +- `existing_order_record_found` +- `pending_or_active_workflow_found` diff --git a/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/allotment_scope.json b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/allotment_scope.json new file mode 100644 index 0000000..d056a5f --- /dev/null +++ b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/allotment_scope.json @@ -0,0 +1,145 @@ +{ + "standalone_partial_allotment": { + "body_current": "Please reduce block BLK-2608-A from 20 rooms to 15 rooms and keep the balance active.", + "candidate_events": [], + "input_is_understood": true, + "expected_route": "S10", + "forbidden_event_types": [ + "Allotment Maintenance", + "Update Booking / Amendment", + "Cancel Allotment" + ] + }, + "mixed_partial_allotment": { + "output": { + "source_message": { + "source_message_id": "MSG-ALLOT-MIXED-001", + "subject": "New group and allotment reduction", + "from": "agent@example.com", + "cc": [], + "received_at": "2026-07-11T10:10:00+08:00", + "source_channel": "Email" + }, + "message_events": [ + { + "event_type": "New Booking", + "event_role": "travel_agent_request", + "current_or_history": "current", + "source_event_index": "E1", + "case_keys": { + "group_code": "NEW-GROUP-01", + "confirmation_number": null, + "reservation_number": null, + "block_code": null + }, + "relevant_message_excerpt": "Please create NEW-GROUP-01 for 10-12 Aug, 2 DBL.", + "attachments": [], + "file_references": [], + "context_used": {}, + "extracted_fields": { + "booking_object_type": "Group Block", + "arrival_date": "2026-08-10", + "departure_date": "2026-08-12", + "room_items": [ + { + "room_type_raw": "DBL", + "pms_room_type_code": "RM2", + "room_quantity": 2 + } + ] + }, + "manual_review": null + } + ], + "case_candidates": [], + "extraction_warnings": [], + "unhandled_current_intents": [ + { + "category": "unhandled_current_business_content", + "current_or_history": "current", + "reason_code": "requires_business_approval_or_unsupported_task_card", + "text_raw": "Please reduce block BLK-2608-A from 20 rooms to 15 rooms and keep the balance active.", + "visible_message": "当前邮件还要求减少 BLK-2608-A 的部分配额并保留剩余房量;该事项不在当前支持范围,请查看原文并决定后续处理。", + "requires_user_decision": true, + "case_keys": { + "group_code": null, + "confirmation_number": null, + "reservation_number": null, + "block_code": "BLK-2608-A" + }, + "attachments": [], + "file_references": [] + } + ] + }, + "expected_unhandled_text": "Please reduce block BLK-2608-A from 20 rooms to 15 rooms and keep the balance active.", + "forbidden_event_types": [ + "Allotment Maintenance", + "Update Booking / Amendment", + "Cancel Allotment" + ] + }, + "new_allotment_control_block": { + "event": { + "event_type": "New Booking", + "event_role": "travel_agent_request", + "current_or_history": "current", + "source_event_index": "E_NEW_ALLOTMENT", + "case_keys": { + "group_code": null, + "confirmation_number": null, + "reservation_number": null, + "block_code": "BLK-NEW-01" + }, + "relevant_message_excerpt": "Please create a new control block BLK-NEW-01 for 20 DBL rooms.", + "attachments": [], + "file_references": [], + "context_used": {}, + "extracted_fields": { + "booking_object_type": "Allotment / Control Block", + "arrival_date": "2026-09-01", + "departure_date": "2026-09-05", + "room_items": [ + { + "room_type_raw": "DBL", + "pms_room_type_code": "RM2", + "room_quantity": 20 + } + ] + }, + "manual_review": null + }, + "expected_adapter_triplet": { + "result_type": "normal_task", + "task_type": "New Booking", + "task_subtype": "new_allotment_control_block" + } + }, + "cancel_entire_allotment": { + "event": { + "event_type": "Cancel Allotment", + "event_role": "travel_agent_request", + "current_or_history": "current", + "source_event_index": "E_CANCEL_ALLOTMENT", + "case_keys": { + "group_code": null, + "confirmation_number": null, + "reservation_number": null, + "block_code": "BLK-CANCEL-01" + }, + "relevant_message_excerpt": "Cancel the entire control block BLK-CANCEL-01 and release all remaining rooms.", + "attachments": [], + "file_references": [], + "context_used": {}, + "extracted_fields": { + "cancel_scope": "entire_allotment_control_block" + }, + "manual_review": null + }, + "expected_adapter_triplet": { + "result_type": "normal_task", + "task_type": "Cancel Allotment", + "task_subtype": "cancel_allotment_control_block" + } + } +} diff --git a/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/candidate_gate.json b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/candidate_gate.json new file mode 100644 index 0000000..aa58827 --- /dev/null +++ b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/candidate_gate.json @@ -0,0 +1,226 @@ +{ + "cases": [ + { + "case_id": "valid_coarse_attachment_signal", + "expect_valid": true, + "candidate_events": [ + { + "candidate_id": "C1", + "classification_status": "coarse_supported_signal", + "possible_event_types": [ + "Voucher Received", + "Payment Evidence" + ], + "evidence_refs": [ + { + "source": "current_attachments", + "index": 0 + } + ], + "target_hints": [], + "uncertainty_codes": [ + "visual_document_type_not_decided" + ] + } + ], + "expected_action": "invoke_booking_desk_event" + }, + { + "case_id": "valid_single_supported_direction", + "expect_valid": true, + "candidate_events": [ + { + "candidate_id": "C1", + "classification_status": "coarse_supported_signal", + "possible_event_types": [ + "Update Booking / Amendment" + ], + "evidence_refs": [ + { + "source": "body_current", + "index": 0 + } + ], + "target_hints": [ + { + "case_keys": { + "group_code": null, + "confirmation_number": null, + "reservation_number": "344055", + "block_code": null + }, + "target_locator_raw": "reservation 344055" + } + ], + "uncertainty_codes": [] + } + ], + "expected_action": "invoke_booking_desk_event" + }, + { + "case_id": "valid_main_no_active_signal_routes_s10", + "expect_valid": true, + "candidate_events": [], + "input_is_understood": true, + "expected_action": "return_S10" + }, + { + "case_id": "valid_skill_no_supported_event_routes_s10", + "expect_valid": true, + "candidate_events": [ + { + "candidate_id": "C1", + "classification_status": "coarse_supported_signal", + "possible_event_types": [ + "Voucher Received", + "Payment Evidence" + ], + "evidence_refs": [ + { + "source": "current_attachments", + "index": 0 + } + ], + "target_hints": [], + "uncertainty_codes": [ + "document_subtype_unresolved" + ] + } + ], + "skill_disposition": { + "internal_route": "no_supported_event", + "reason_code": "skill_validation_found_no_active_event", + "candidate_ids": [ + "C1" + ] + }, + "expected_action": "convert_internal_no_supported_event_to_S10" + }, + { + "case_id": "invalid_candidate_missing_target_hints", + "expect_valid": false, + "candidate_events": [ + { + "candidate_id": "C1", + "classification_status": "coarse_supported_signal", + "possible_event_types": [ + "New Booking" + ], + "evidence_refs": [ + { + "source": "body_current", + "index": 0 + } + ], + "uncertainty_codes": [] + } + ] + }, + { + "case_id": "invalid_candidate_wrong_status", + "expect_valid": false, + "candidate_events": [ + { + "candidate_id": "C1", + "classification_status": "coarse_unresolved", + "possible_event_types": [ + "New Booking" + ], + "evidence_refs": [ + { + "source": "body_current", + "index": 0 + } + ], + "target_hints": [], + "uncertainty_codes": [] + } + ] + }, + { + "case_id": "invalid_candidate_empty_possible_types", + "expect_valid": false, + "candidate_events": [ + { + "candidate_id": "C1", + "classification_status": "coarse_supported_signal", + "possible_event_types": [], + "evidence_refs": [ + { + "source": "current_attachments", + "index": 0 + } + ], + "target_hints": [], + "uncertainty_codes": [ + "business_direction_not_identified" + ] + } + ] + }, + { + "case_id": "invalid_review_outcome_used_as_candidate", + "expect_valid": false, + "candidate_events": [ + { + "candidate_id": "C1", + "classification_status": "coarse_supported_signal", + "possible_event_types": [ + "Need Manual Review" + ], + "evidence_refs": [ + { + "source": "body_current", + "index": 0 + } + ], + "target_hints": [], + "uncertainty_codes": [ + "event_type_unclear" + ] + } + ] + }, + { + "case_id": "invalid_evidence_refs_string_array", + "expect_valid": false, + "candidate_events": [ + { + "candidate_id": "C1", + "classification_status": "coarse_supported_signal", + "possible_event_types": [ + "New Booking" + ], + "evidence_refs": [ + "body_current:new booking" + ], + "target_hints": [], + "uncertainty_codes": [] + } + ] + }, + { + "case_id": "invalid_target_hints_object", + "expect_valid": false, + "candidate_events": [ + { + "candidate_id": "C1", + "classification_status": "coarse_supported_signal", + "possible_event_types": [ + "Update Booking / Amendment" + ], + "evidence_refs": [ + { + "source": "body_current", + "index": 0 + } + ], + "target_hints": { + "reservation_number": "344055" + }, + "uncertainty_codes": [] + } + ] + } + ] +} diff --git a/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/main_outcomes.json b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/main_outcomes.json new file mode 100644 index 0000000..5686379 --- /dev/null +++ b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/main_outcomes.json @@ -0,0 +1,175 @@ +{ + "cases": [ + { + "case_id": "legal_s10_no_supported_match", + "expect_valid": true, + "value": { + "source_message": { + "source_message_id": "MSG-S10-001", + "subject": "Please confirm booking details", + "from": "agent@example.com", + "cc": [], + "received_at": "2026-07-11T09:00:00+08:00", + "source_channel": "Email" + }, + "route_code": "S10", + "handler_type": "main_agent_outcome", + "result_type": "source_message_review_notification", + "current_or_history": "current", + "agent_assessment": { + "status": "no_booking_action_detected", + "reason_code": "no_booking_action_detected", + "automation_action": "none" + }, + "notification": { + "required": true, + "notification_type": "source_message_review", + "show_source_message": true, + "requires_user_decision": true, + "visible_message": "未匹配到当前 Agent 支持的业务事件类型,请查看原邮件并决定是否需要回复或进行其他处理。" + }, + "manual_review": null + } + }, + { + "case_id": "illegal_s10_with_review_object", + "expect_valid": false, + "value": { + "source_message": { + "source_message_id": "MSG-S10-002", + "subject": null, + "from": null, + "cc": [], + "received_at": null, + "source_channel": "Email" + }, + "route_code": "S10", + "handler_type": "main_agent_outcome", + "result_type": "source_message_review_notification", + "current_or_history": "current", + "agent_assessment": { + "status": "no_booking_action_detected", + "reason_code": "no_booking_action_detected", + "automation_action": "none" + }, + "notification": { + "required": true, + "notification_type": "source_message_review", + "show_source_message": true, + "requires_user_decision": true, + "visible_message": "查看原邮件。" + }, + "manual_review": { + "reason_code": "material_package_unavailable" + } + } + }, + { + "case_id": "legal_s99_material_unavailable", + "expect_valid": true, + "value": { + "source_message": { + "source_message_id": "MSG-S99-001", + "subject": "See attached", + "from": "agent@example.com", + "cc": [], + "received_at": "2026-07-11T09:05:00+08:00", + "source_channel": "Email" + }, + "route_code": "S99", + "handler_type": "main_agent_outcome", + "result_type": "source_message_review_notification", + "current_or_history": "current", + "agent_assessment": { + "status": "material_package_unavailable", + "reason_code": "material_package_unavailable", + "automation_action": "none" + }, + "notification": { + "required": true, + "notification_type": "source_message_review", + "show_source_message": true, + "requires_user_decision": true, + "visible_message": "当前输入不足,无法判断是否匹配当前 Agent 支持的业务事件类型,请查看原邮件并决定后续处理。" + }, + "manual_review": { + "reason_code": "material_package_unavailable", + "visible_reason": "当前输入不足,无法完成支持业务事件范围分类。", + "review_record_type": "main_agent_entry_review", + "missing_fields": [], + "blocking_points": [], + "conflicting_points": [], + "suggested_human_actions": [ + "review_source_message" + ], + "evidence_to_check": [ + "source_message" + ], + "known_fields": {} + } + } + }, + { + "case_id": "illegal_s99_with_null_review", + "expect_valid": false, + "value": { + "source_message": { + "source_message_id": "MSG-S99-002", + "subject": null, + "from": null, + "cc": [], + "received_at": null, + "source_channel": "Email" + }, + "route_code": "S99", + "handler_type": "main_agent_outcome", + "result_type": "source_message_review_notification", + "current_or_history": "current", + "agent_assessment": { + "status": "material_package_unavailable", + "reason_code": "material_package_unavailable", + "automation_action": "none" + }, + "notification": { + "required": true, + "notification_type": "source_message_review", + "show_source_message": true, + "requires_user_decision": true, + "visible_message": "查看原邮件。" + }, + "manual_review": null + } + }, + { + "case_id": "illegal_union_template_value", + "expect_valid": false, + "value": { + "source_message": { + "source_message_id": "MSG-UNION-001", + "subject": null, + "from": null, + "cc": [], + "received_at": null, + "source_channel": "Email" + }, + "route_code": "S10 | S99", + "handler_type": "main_agent_outcome", + "result_type": "source_message_review_notification", + "current_or_history": "current", + "agent_assessment": { + "status": "no_booking_action_detected | material_package_unavailable", + "reason_code": "", + "automation_action": "none" + }, + "notification": { + "required": true, + "notification_type": "source_message_review", + "show_source_message": true, + "requires_user_decision": true, + "visible_message": "" + }, + "manual_review": null + } + } + ] +} diff --git a/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/manual_review_resolution.json b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/manual_review_resolution.json new file mode 100644 index 0000000..d865aba --- /dev/null +++ b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/manual_review_resolution.json @@ -0,0 +1,153 @@ +{ + "known_subtype_manual_review": { + "event": { + "event_type": "New Booking", + "event_role": "travel_agent_request", + "current_or_history": "current", + "source_event_index": "E_SUITE_REVIEW", + "case_keys": { + "group_code": "CHILD-SUITE-01", + "confirmation_number": null, + "reservation_number": null, + "block_code": null + }, + "relevant_message_excerpt": "CHILD-SUITE-01 10-12 AUG 2 SUITE", + "attachments": [ + "booking-update.xlsx" + ], + "file_references": [], + "context_used": { + "parent_group_code": "PARENT-SUITE-01" + }, + "extracted_fields": { + "booking_object_type": "Group Block", + "arrival_date": "2026-08-10", + "departure_date": "2026-08-12", + "room_items": [ + { + "room_type_raw": "SUITE", + "room_type_normalized": "SUITE", + "pms_room_type_code": null, + "room_quantity": 2, + "mapping_candidates": [ + "SU1", + "SU6" + ] + } + ] + }, + "manual_review": { + "reason_code": "room_type_mapping_multiple_candidates", + "visible_reason": "房型原文 SUITE 无法唯一映射到 PMS 房型,请在当前 New Booking 卡确认。", + "review_record_type": "business_event_review", + "missing_fields": [ + "/extracted_fields/room_items/0/pms_room_type_code" + ], + "blocking_points": [ + "PMS 房型未确认,当前事件不可进入 Preflight。" + ], + "conflicting_points": [ + "SU1", + "SU6" + ], + "suggested_human_actions": [ + "select_and_confirm_pms_room_type" + ], + "evidence_to_check": [ + "room_type_raw", + "current_attachment" + ], + "known_fields": { + "group_code": "CHILD-SUITE-01", + "booking_object_type": "Group Block", + "arrival_date": "2026-08-10", + "departure_date": "2026-08-12", + "room_quantity": 2 + } + } + }, + "expected_adapter_triplet": { + "result_type": "manual_review", + "task_type": "New Booking", + "task_subtype": "new_group_block" + } + }, + "type_unknown_fallback": { + "event": { + "event_type": "Need Manual Review", + "event_role": "travel_agent_request", + "current_or_history": "current", + "source_event_index": "E_TYPE_UNKNOWN", + "case_keys": { + "group_code": null, + "confirmation_number": null, + "reservation_number": null, + "block_code": null + }, + "relevant_message_excerpt": "Please process this as new or amend; the action is unclear.", + "attachments": [], + "file_references": [], + "context_used": {}, + "extracted_fields": {}, + "manual_review": { + "reason_code": "event_type_conflict_unclear", + "visible_reason": "无法确定当前请求是新订还是改单。", + "review_record_type": "business_event_review", + "missing_fields": [], + "blocking_points": [ + "业务类型无法确定。" + ], + "conflicting_points": [ + "New Booking", + "Update Booking / Amendment" + ], + "suggested_human_actions": [ + "select_business_event_type" + ], + "evidence_to_check": [ + "source_message" + ], + "known_fields": {} + } + }, + "expected_adapter_triplet": { + "result_type": "manual_review", + "task_type": "Fallback", + "task_subtype": "business_event_review" + } + }, + "same_card_resolution": { + "source_case": "known_subtype_manual_review", + "task_identity": { + "task_id": "TASK-SUITE-REVIEW-001", + "result_type": "manual_review", + "task_type": "New Booking", + "task_subtype": "new_group_block" + }, + "initial_state": { + "review_status": "pending", + "review_resolution": null + }, + "resolved_state": { + "review_status": "resolved", + "review_resolution": { + "field_overrides": [ + { + "field_pointer": "/extracted_fields/room_items/0/pms_room_type_code", + "value": "SU1" + } + ], + "resolved_by": "user-123", + "resolved_at": "2026-07-11T10:30:00+08:00" + } + }, + "expected": { + "task_id": "TASK-SUITE-REVIEW-001", + "initial_result_type": "manual_review", + "review_status": "resolved", + "next_state": "preflight", + "creates_second_card": false, + "agent_payload_immutable": true + } + } +} diff --git a/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/parent_split_two_children.json b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/parent_split_two_children.json new file mode 100644 index 0000000..0d84d70 --- /dev/null +++ b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/parent_split_two_children.json @@ -0,0 +1,125 @@ +{ + "source_message": { + "source_message_id": "MSG-PARENT-SPLIT-001", + "subject": "AMEND FINAL BOOKING", + "from": "agent@example.com", + "cc": [], + "received_at": "2026-07-11T10:00:00+08:00", + "source_channel": "Email" + }, + "message_events": [ + { + "event_type": "New Booking", + "event_role": "travel_agent_request", + "current_or_history": "current", + "source_event_index": "E_CHILD_1", + "case_keys": { + "group_code": "CHILD-2608-A", + "confirmation_number": null, + "reservation_number": null, + "block_code": null + }, + "relevant_message_excerpt": "PARENT-2608 AMEND TO CHILD-2608-A 10-12 AUG 2 DBL", + "attachments": [ + "booking-update.xlsx" + ], + "file_references": [], + "context_used": { + "parent_child_split_evidence": "current" + }, + "extracted_fields": { + "booking_object_type": "Group Block", + "allocation_split_from_parent": true, + "parent_group_code": "PARENT-2608", + "arrival_date": "2026-08-10", + "departure_date": "2026-08-12", + "room_items": [ + { + "room_type_raw": "DBL", + "pms_room_type_code": "RM2", + "room_quantity": 2 + } + ] + }, + "manual_review": null + }, + { + "event_type": "New Booking", + "event_role": "travel_agent_request", + "current_or_history": "current", + "source_event_index": "E_CHILD_2", + "case_keys": { + "group_code": "CHILD-2608-B", + "confirmation_number": null, + "reservation_number": null, + "block_code": null + }, + "relevant_message_excerpt": "PARENT-2608 AMEND TO CHILD-2608-B 12-14 AUG 3 TWN", + "attachments": [ + "booking-update.xlsx" + ], + "file_references": [], + "context_used": { + "parent_child_split_evidence": "current" + }, + "extracted_fields": { + "booking_object_type": "Group Block", + "allocation_split_from_parent": true, + "parent_group_code": "PARENT-2608", + "arrival_date": "2026-08-12", + "departure_date": "2026-08-14", + "room_items": [ + { + "room_type_raw": "TWN", + "pms_room_type_code": "RM3", + "room_quantity": 3 + } + ] + }, + "manual_review": null + }, + { + "event_type": "Cancel Booking", + "event_role": "travel_agent_request", + "current_or_history": "current", + "source_event_index": "E_PARENT_CANCEL", + "case_keys": { + "group_code": "PARENT-2608", + "confirmation_number": null, + "reservation_number": null, + "block_code": null + }, + "relevant_message_excerpt": "PARENT-2608 AMEND TO CHILD-2608-A / CHILD-2608-B", + "attachments": [ + "booking-update.xlsx" + ], + "file_references": [], + "context_used": { + "source": "current parent-to-child split evidence" + }, + "extracted_fields": { + "cancel_object_type": "group_block", + "parent_release_or_cancel_candidate": true, + "release_reason": "parent_to_child_allocation_split", + "parent_group_code": "PARENT-2608", + "child_group_codes": [ + "CHILD-2608-A", + "CHILD-2608-B" + ], + "allocation_split_from_parent": true, + "parent_original_room_summary": "5 rooms" + }, + "related_source_event_indices": [ + "E_CHILD_1", + "E_CHILD_2" + ], + "related_event_type": "New Booking", + "relationship_type": "linked_parent_release_after_child_split", + "requires_downstream_hard_validation": true, + "manual_review": null + } + ], + "case_candidates": [], + "extraction_warnings": [], + "unhandled_current_intents": [] +} diff --git a/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/row_multiple_derived.json b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/row_multiple_derived.json new file mode 100644 index 0000000..281f450 --- /dev/null +++ b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/row_multiple_derived.json @@ -0,0 +1,146 @@ +{ + "source_message": { + "source_message_id": "MSG-ROW-DERIVED-001", + "subject": "BOOKING UPDATE 08-2026", + "from": "agent@example.com", + "cc": [], + "received_at": "2026-07-11T10:05:00+08:00", + "source_channel": "Email" + }, + "message_events": [ + { + "event_type": "New Booking", + "event_role": "travel_agent_request", + "current_or_history": "current", + "source_event_index": "E_ROW_CHILD", + "case_keys": { + "group_code": "CHILD-ROW-01", + "confirmation_number": null, + "reservation_number": null, + "block_code": null + }, + "relevant_message_excerpt": "PARENT-ROW-01 AMEND TO CHILD-ROW-01, 10-12 AUG, 2 DBL, add one extra bed", + "attachments": [ + "booking-update.xlsx" + ], + "file_references": [], + "context_used": { + "row_evidence": { + "row_reference": "booking-update.xlsx#BOOKING 08-2026#42", + "sheet": "BOOKING 08-2026", + "row_index": 42, + "highlighted": true + } + }, + "extracted_fields": { + "booking_object_type": "Group Block", + "parent_group_code": "PARENT-ROW-01", + "allocation_split_from_parent": true, + "arrival_date": "2026-08-10", + "departure_date": "2026-08-12", + "room_items": [ + { + "room_type_raw": "DBL", + "pms_room_type_code": "RM2", + "room_quantity": 2 + } + ] + }, + "manual_review": null + }, + { + "event_type": "Cancel Booking", + "event_role": "travel_agent_request", + "current_or_history": "current", + "source_event_index": "E_ROW_PARENT", + "case_keys": { + "group_code": "PARENT-ROW-01", + "confirmation_number": null, + "reservation_number": null, + "block_code": null + }, + "relevant_message_excerpt": "PARENT-ROW-01 AMEND TO CHILD-ROW-01", + "attachments": [ + "booking-update.xlsx" + ], + "file_references": [], + "context_used": { + "row_evidence": { + "row_reference": "booking-update.xlsx#BOOKING 08-2026#42", + "sheet": "BOOKING 08-2026", + "row_index": 42, + "highlighted": true + } + }, + "extracted_fields": { + "cancel_object_type": "group_block", + "parent_release_or_cancel_candidate": true, + "parent_group_code": "PARENT-ROW-01", + "child_group_codes": [ + "CHILD-ROW-01" + ] + }, + "related_source_event_indices": [ + "E_ROW_CHILD" + ], + "related_event_type": "New Booking", + "relationship_type": "linked_parent_release_after_child_split", + "requires_downstream_hard_validation": true, + "manual_review": null + }, + { + "event_type": "Trace", + "event_role": "travel_agent_request", + "current_or_history": "current", + "source_event_index": "E_ROW_TRACE", + "case_keys": { + "group_code": "CHILD-ROW-01", + "confirmation_number": null, + "reservation_number": null, + "block_code": null + }, + "relevant_message_excerpt": "add one extra bed", + "attachments": [ + "booking-update.xlsx" + ], + "file_references": [], + "context_used": { + "row_evidence": { + "row_reference": "booking-update.xlsx#BOOKING 08-2026#42", + "sheet": "BOOKING 08-2026", + "row_index": 42, + "highlighted": true + } + }, + "extracted_fields": { + "trace_subtype": "extra_bed", + "trace_text": "add one extra bed", + "trace_items": [ + { + "category": "extra_bed", + "text_raw": "add one extra bed", + "service_date": null, + "service_period_raw": null, + "pax": null, + "notify_departments": [ + "FO", + "HSK" + ] + } + ], + "notify_departments": [ + "FO", + "HSK" + ] + }, + "related_source_event_index": "E_ROW_CHILD", + "related_event_type": "New Booking", + "relationship_type": "linked_trace", + "requires_downstream_hard_validation": true, + "manual_review": null + } + ], + "case_candidates": [], + "extraction_warnings": [], + "unhandled_current_intents": [] +} diff --git a/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/source_identity_errors.json b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/source_identity_errors.json new file mode 100644 index 0000000..0ffcd63 --- /dev/null +++ b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/fixtures/source_identity_errors.json @@ -0,0 +1,106 @@ +{ + "cases": [ + { + "case_id": "missing_source_message_id_cancel", + "message_class": "Cancel", + "input": { + "source_message": {}, + "body_current": "Please cancel group HD260710A" + }, + "expected_error": { + "result_type": "infrastructure_input_error", + "error_code": "missing_source_message_id", + "retryable": true, + "missing_fields": [ + "source_message.source_message_id" + ] + }, + "expected_processing": { + "material_reads": 0, + "skill_calls": 0 + } + }, + { + "case_id": "null_source_message_id_cancel", + "message_class": "Cancel", + "input": { + "source_message": { + "source_message_id": null + }, + "body_current": "Please cancel group HD260710A" + }, + "expected_error": { + "result_type": "infrastructure_input_error", + "error_code": "missing_source_message_id", + "retryable": true, + "missing_fields": [ + "source_message.source_message_id" + ] + }, + "expected_processing": { + "material_reads": 0, + "skill_calls": 0 + } + }, + { + "case_id": "empty_source_message_id_thanks", + "message_class": "Thanks", + "input": { + "source_message": { + "source_message_id": "" + }, + "body_current": "Thanks" + }, + "expected_error": { + "result_type": "infrastructure_input_error", + "error_code": "missing_source_message_id", + "retryable": true, + "missing_fields": [ + "source_message.source_message_id" + ] + }, + "expected_processing": { + "material_reads": 0, + "skill_calls": 0 + } + }, + { + "case_id": "whitespace_source_message_id_see_attached", + "message_class": "See Attached", + "input": { + "source_message": { + "source_message_id": " " + }, + "body_current": "Please see attached", + "current_attachments": [ + { + "filename": "unknown.pdf" + } + ] + }, + "expected_error": { + "result_type": "infrastructure_input_error", + "error_code": "missing_source_message_id", + "retryable": true, + "missing_fields": [ + "source_message.source_message_id" + ] + }, + "expected_processing": { + "material_reads": 0, + "skill_calls": 0 + } + }, + { + "case_id": "nonblank_source_message_id_passes_guard", + "message_class": "Cancel", + "input": { + "source_message": { + "source_message_id": "MSG-VALID-001" + }, + "body_current": "Please cancel group HD260710A" + }, + "expected_error": null + } + ] +} diff --git a/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/validate_p0_contracts.mjs b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/validate_p0_contracts.mjs new file mode 100644 index 0000000..58601aa --- /dev/null +++ b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/03_P0_Acceptance/validate_p0_contracts.mjs @@ -0,0 +1,674 @@ +import assert from "node:assert/strict"; +import { readFileSync, readdirSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); +const fixturesDir = join(here, "fixtures"); + +const ACTIVE_EVENT_TYPES = new Set([ + "New Booking", + "Update Booking / Amendment", + "Cancel Booking", + "Cancel Allotment", + "Voucher Received", + "Payment Evidence", + "Rooming List", + "AMEND GROUP CODE", + "Invoice Generation", + "Invoice Received", + "Payment Notice", + "Trace", + "Manual RateCode", + "TA RECORDER" +]); + +const SOURCE_KEYS = [ + "source_message_id", + "subject", + "from", + "cc", + "received_at", + "source_channel" +]; + +const CASE_KEY_KEYS = [ + "group_code", + "confirmation_number", + "reservation_number", + "block_code" +]; + +const REVIEW_KEYS = [ + "reason_code", + "visible_reason", + "review_record_type", + "missing_fields", + "blocking_points", + "conflicting_points", + "suggested_human_actions", + "evidence_to_check", + "known_fields" +]; + +const CANDIDATE_KEYS = [ + "candidate_id", + "classification_status", + "possible_event_types", + "evidence_refs", + "target_hints", + "uncertainty_codes" +]; + +const EVIDENCE_SOURCES = new Set([ + "body_current", + "current_attachments", + "current_tables", + "current_ocr", + "parent_child_split_evidence" +]); + +const failures = []; +let passed = 0; +let negativeFixturesRejected = 0; + +function runCheck(name, fn) { + try { + fn(); + passed += 1; + process.stdout.write(`PASS ${name}\n`); + } catch (error) { + failures.push({ name, error }); + process.stderr.write(`FAIL ${name}: ${error.message}\n`); + } +} + +function loadFixture(name) { + return JSON.parse(readFileSync(join(fixturesDir, name), "utf8")); +} + +function isPlainObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function assertExactKeys(value, expected, label) { + assert.ok(isPlainObject(value), `${label} must be an object`); + assert.deepEqual(Object.keys(value).sort(), [...expected].sort(), `${label} key set`); +} + +function assertNonblank(value, label) { + assert.equal(typeof value, "string", `${label} must be a string`); + assert.ok(value.trim().length > 0, `${label} must be nonblank`); +} + +function assertStringOrNull(value, label) { + assert.ok(value === null || typeof value === "string", `${label} must be string|null`); +} + +function assertStringArray(value, label, { nonempty = false } = {}) { + assert.ok(Array.isArray(value), `${label} must be an array`); + if (nonempty) assert.ok(value.length > 0, `${label} must not be empty`); + for (const item of value) assert.equal(typeof item, "string", `${label} items must be strings`); +} + +function validateSourceMessage(source) { + assertExactKeys(source, SOURCE_KEYS, "source_message"); + assertNonblank(source.source_message_id, "source_message.source_message_id"); + assertStringOrNull(source.subject, "source_message.subject"); + assertStringOrNull(source.from, "source_message.from"); + assertStringArray(source.cc, "source_message.cc"); + assertStringOrNull(source.received_at, "source_message.received_at"); + assert.equal(source.source_channel, "Email", "source_message.source_channel"); +} + +function validateCaseKeys(caseKeys) { + assertExactKeys(caseKeys, CASE_KEY_KEYS, "case_keys"); + for (const key of CASE_KEY_KEYS) assertStringOrNull(caseKeys[key], `case_keys.${key}`); +} + +function isRfc6901Pointer(pointer) { + if (typeof pointer !== "string" || !pointer.startsWith("/")) return false; + return pointer + .slice(1) + .split("/") + .every((token) => !/~(?![01])/u.test(token)); +} + +function pointerTokens(pointer) { + assert.ok(isRfc6901Pointer(pointer), `invalid RFC 6901 JSON Pointer: ${pointer}`); + return pointer + .slice(1) + .split("/") + .map((token) => token.replaceAll("~1", "/").replaceAll("~0", "~")); +} + +function resolvePointer(document, pointer) { + let current = document; + for (const token of pointerTokens(pointer)) { + assert.ok(current !== null && typeof current === "object", `${pointer} traverses a scalar`); + assert.ok(Object.prototype.hasOwnProperty.call(current, token), `${pointer} target does not exist`); + current = current[token]; + } + return current; +} + +function setPointer(document, pointer, value) { + const tokens = pointerTokens(pointer); + const finalToken = tokens.pop(); + let current = document; + for (const token of tokens) { + assert.ok(current !== null && typeof current === "object", `${pointer} traverses a scalar`); + assert.ok(Object.prototype.hasOwnProperty.call(current, token), `${pointer} target does not exist`); + current = current[token]; + } + assert.ok(Object.prototype.hasOwnProperty.call(current, finalToken), `${pointer} target does not exist`); + current[finalToken] = value; +} + +function validateReview(review, recordType, { businessEvent } = {}) { + assertExactKeys(review, REVIEW_KEYS, "manual_review"); + assertNonblank(review.reason_code, "manual_review.reason_code"); + assertNonblank(review.visible_reason, "manual_review.visible_reason"); + assert.equal(review.review_record_type, recordType, "manual_review.review_record_type"); + for (const key of [ + "missing_fields", + "blocking_points", + "conflicting_points", + "suggested_human_actions", + "evidence_to_check" + ]) { + assertStringArray(review[key], `manual_review.${key}`); + } + assert.ok(isPlainObject(review.known_fields), "manual_review.known_fields must be an object"); + if (businessEvent) { + for (const pointer of review.missing_fields) { + assert.ok(isRfc6901Pointer(pointer), `${pointer} must be RFC 6901`); + resolvePointer(businessEvent, pointer); + } + } +} + +function validateMainOutcome(value) { + assertExactKeys(value, [ + "source_message", + "route_code", + "handler_type", + "result_type", + "current_or_history", + "agent_assessment", + "notification", + "manual_review" + ], "main outcome"); + validateSourceMessage(value.source_message); + assert.ok(value.route_code === "S10" || value.route_code === "S99", "route_code must be S10|S99"); + assert.equal(value.handler_type, "main_agent_outcome"); + assert.equal(value.result_type, "source_message_review_notification"); + assert.equal(value.current_or_history, "current"); + assertExactKeys(value.agent_assessment, ["status", "reason_code", "automation_action"], "agent_assessment"); + assert.equal(value.agent_assessment.automation_action, "none"); + assertExactKeys(value.notification, [ + "required", + "notification_type", + "show_source_message", + "requires_user_decision", + "visible_message" + ], "notification"); + assert.equal(value.notification.required, true); + assert.equal(value.notification.notification_type, "source_message_review"); + assert.equal(value.notification.show_source_message, true); + assert.equal(value.notification.requires_user_decision, true); + assertNonblank(value.notification.visible_message, "notification.visible_message"); + assert.ok(!Object.prototype.hasOwnProperty.call(value.notification, "action_required")); + + if (value.route_code === "S10") { + assert.equal(value.agent_assessment.status, "no_booking_action_detected"); + assert.equal(value.agent_assessment.reason_code, "no_booking_action_detected"); + assert.equal(value.manual_review, null, "S10.manual_review must be null"); + } else { + assert.equal(value.agent_assessment.status, "material_package_unavailable"); + assert.equal(value.agent_assessment.reason_code, "material_package_unavailable"); + validateReview(value.manual_review, "main_agent_entry_review"); + assert.equal(value.manual_review.reason_code, value.agent_assessment.reason_code); + } +} + +function validateCandidateEvents(candidateEvents) { + assert.ok(Array.isArray(candidateEvents), "candidate_events must be an array"); + const ids = new Set(); + for (const candidate of candidateEvents) { + assertExactKeys(candidate, CANDIDATE_KEYS, "candidate_event"); + assert.match(candidate.candidate_id, /^C[1-9][0-9]*$/u, "candidate_id format"); + assert.ok(!ids.has(candidate.candidate_id), `duplicate candidate_id ${candidate.candidate_id}`); + ids.add(candidate.candidate_id); + assert.equal(candidate.classification_status, "coarse_supported_signal"); + assertStringArray(candidate.possible_event_types, "possible_event_types", { nonempty: true }); + assert.equal(new Set(candidate.possible_event_types).size, candidate.possible_event_types.length, "possible_event_types must be unique"); + for (const eventType of candidate.possible_event_types) { + assert.ok(ACTIVE_EVENT_TYPES.has(eventType), `${eventType} is not an active event type`); + } + assert.ok(Array.isArray(candidate.evidence_refs) && candidate.evidence_refs.length > 0, "evidence_refs must be a nonempty array"); + for (const evidenceRef of candidate.evidence_refs) { + assertExactKeys(evidenceRef, ["source", "index"], "candidate evidence_ref"); + assert.ok(EVIDENCE_SOURCES.has(evidenceRef.source), `invalid evidence source ${evidenceRef.source}`); + assert.ok(Number.isInteger(evidenceRef.index) && evidenceRef.index >= 0, "evidence_ref.index must be a nonnegative integer"); + if (evidenceRef.source === "body_current") assert.equal(evidenceRef.index, 0, "body_current index must be 0"); + } + assert.ok(Array.isArray(candidate.target_hints), "target_hints must be an array"); + for (const targetHint of candidate.target_hints) { + assertExactKeys(targetHint, ["case_keys", "target_locator_raw"], "target_hint"); + validateCaseKeys(targetHint.case_keys); + assertStringOrNull(targetHint.target_locator_raw, "target_hint.target_locator_raw"); + } + assertStringArray(candidate.uncertainty_codes, "uncertainty_codes"); + } +} + +function validateNoSupportedEventDisposition(disposition, candidateEvents) { + assertExactKeys(disposition, ["internal_route", "reason_code", "candidate_ids"], "no_supported_event"); + assert.equal(disposition.internal_route, "no_supported_event"); + assert.equal(disposition.reason_code, "skill_validation_found_no_active_event"); + assertStringArray(disposition.candidate_ids, "no_supported_event.candidate_ids", { nonempty: true }); + const knownCandidateIds = new Set(candidateEvents.map((candidate) => candidate.candidate_id)); + for (const candidateId of disposition.candidate_ids) { + assert.ok(knownCandidateIds.has(candidateId), `no_supported_event references unknown ${candidateId}`); + } + assert.deepEqual([...new Set(disposition.candidate_ids)].sort(), [...knownCandidateIds].sort()); +} + +function validateEvent(event) { + for (const key of [ + "event_type", + "event_role", + "current_or_history", + "source_event_index", + "case_keys", + "relevant_message_excerpt", + "attachments", + "file_references", + "context_used", + "extracted_fields", + "manual_review" + ]) { + assert.ok(Object.prototype.hasOwnProperty.call(event, key), `event missing ${key}`); + } + assert.ok(ACTIVE_EVENT_TYPES.has(event.event_type) || event.event_type === "Need Manual Review", `invalid event_type ${event.event_type}`); + assertNonblank(event.event_role, "event_role"); + assert.equal(event.current_or_history, "current"); + assertNonblank(event.source_event_index, "source_event_index"); + validateCaseKeys(event.case_keys); + assert.equal(typeof event.relevant_message_excerpt, "string"); + assert.ok(Array.isArray(event.attachments)); + assert.ok(Array.isArray(event.file_references)); + assert.ok(isPlainObject(event.context_used)); + assert.ok(isPlainObject(event.extracted_fields)); + if (event.manual_review !== null) validateReview(event.manual_review, "business_event_review", { businessEvent: event }); + if (event.event_type === "Need Manual Review") assert.notEqual(event.manual_review, null, "type-unknown Fallback needs manual_review"); +} + +function validateUnhandledIntent(item) { + assertExactKeys(item, [ + "category", + "current_or_history", + "reason_code", + "text_raw", + "visible_message", + "requires_user_decision", + "case_keys", + "attachments", + "file_references" + ], "unhandled_current_intent"); + assert.equal(item.category, "unhandled_current_business_content"); + assert.equal(item.current_or_history, "current"); + assert.equal(item.reason_code, "requires_business_approval_or_unsupported_task_card"); + assertNonblank(item.text_raw, "unhandled text_raw"); + assertNonblank(item.visible_message, "unhandled visible_message"); + assert.equal(item.requires_user_decision, true); + validateCaseKeys(item.case_keys); + assert.ok(Array.isArray(item.attachments)); + assert.ok(Array.isArray(item.file_references)); +} + +function validateBusinessRoot(root) { + assertExactKeys(root, [ + "source_message", + "message_events", + "case_candidates", + "extraction_warnings", + "unhandled_current_intents" + ], "business root"); + validateSourceMessage(root.source_message); + assert.ok(Array.isArray(root.message_events)); + assert.ok(Array.isArray(root.case_candidates)); + assert.ok(Array.isArray(root.extraction_warnings)); + assert.ok(Array.isArray(root.unhandled_current_intents)); + const indexes = new Set(); + for (const event of root.message_events) { + validateEvent(event); + assert.ok(!indexes.has(event.source_event_index), `duplicate source_event_index ${event.source_event_index}`); + indexes.add(event.source_event_index); + } + for (const item of root.unhandled_current_intents) validateUnhandledIntent(item); +} + +function deriveAdapterTriplet(event) { + validateEvent(event); + if (event.event_type === "Need Manual Review") { + return { + result_type: "manual_review", + task_type: "Fallback", + task_subtype: "business_event_review" + }; + } + + const resultType = event.manual_review === null ? "normal_task" : "manual_review"; + if (event.event_type === "New Booking") { + const subtypeByObject = { + "FIT Reservation": "new_fit_reservation", + "Group Block": "new_group_block", + "Allotment / Control Block": "new_allotment_control_block" + }; + const subtype = subtypeByObject[event.extracted_fields.booking_object_type]; + assert.ok(subtype, "New Booking requires known booking_object_type"); + return { result_type: resultType, task_type: "New Booking", task_subtype: subtype }; + } + if (event.event_type === "Update Booking / Amendment") { + return { result_type: resultType, task_type: "Update Booking", task_subtype: "update_booking_amendment" }; + } + if (event.event_type === "Cancel Booking") { + if (event.relationship_type === "linked_parent_release_after_child_split") { + return { + result_type: resultType, + task_type: "Cancel Booking", + task_subtype: "linked_parent_release_after_child_split" + }; + } + const subtypeByObject = { + fit_reservation: "cancel_fit_reservation", + group_block: "cancel_group_block" + }; + const subtype = subtypeByObject[event.extracted_fields.cancel_object_type]; + assert.ok(subtype, "Cancel Booking requires known cancel_object_type"); + return { result_type: resultType, task_type: "Cancel Booking", task_subtype: subtype }; + } + if (event.event_type === "Cancel Allotment") { + return { + result_type: resultType, + task_type: "Cancel Allotment", + task_subtype: "cancel_allotment_control_block" + }; + } + if (event.event_type === "Trace") { + assert.ok(event.extracted_fields.trace_subtype === "extra_bed" || event.extracted_fields.trace_subtype === "general_request"); + return { + result_type: resultType, + task_type: "Trace / Reservation Notes", + task_subtype: event.extracted_fields.trace_subtype + }; + } + throw new Error(`triplet derivation not implemented for ${event.event_type}`); +} + +function infrastructureGuard(input) { + const sourceId = input?.source_message?.source_message_id; + if (typeof sourceId !== "string" || sourceId.trim() === "") { + return { + result_type: "infrastructure_input_error", + error_code: "missing_source_message_id", + retryable: true, + missing_fields: ["source_message.source_message_id"] + }; + } + return null; +} + +function processSourceIdentityStep(input) { + const processing = { + material_reads: 0, + skill_calls: 0 + }; + const outcome = infrastructureGuard(input); + return { outcome, processing }; +} + +function resolveSameCard(taskIdentity, initialState, resolvedState, agentEvent) { + assertExactKeys(initialState, ["review_status", "review_resolution"], "initial review state"); + assert.equal(initialState.review_status, "pending"); + assert.equal(initialState.review_resolution, null); + assertExactKeys(resolvedState, ["review_status", "review_resolution"], "resolved review state"); + assert.equal(resolvedState.review_status, "resolved"); + assertExactKeys(resolvedState.review_resolution, ["field_overrides", "resolved_by", "resolved_at"], "review_resolution"); + assert.ok(Array.isArray(resolvedState.review_resolution.field_overrides)); + assert.ok(resolvedState.review_resolution.field_overrides.length > 0, "field_overrides must not be empty"); + assertNonblank(resolvedState.review_resolution.resolved_by, "review_resolution.resolved_by"); + assertNonblank(resolvedState.review_resolution.resolved_at, "review_resolution.resolved_at"); + const originalSnapshot = JSON.stringify(agentEvent); + const effectivePayload = structuredClone(agentEvent); + for (const override of resolvedState.review_resolution.field_overrides) { + assertExactKeys(override, ["field_pointer", "value"], "field_override"); + assert.ok(agentEvent.manual_review.missing_fields.includes(override.field_pointer), `${override.field_pointer} is not a declared missing field`); + setPointer(effectivePayload, override.field_pointer, override.value); + } + for (const pointer of agentEvent.manual_review.missing_fields) { + const value = resolvePointer(effectivePayload, pointer); + assert.ok(value !== null && value !== "", `${pointer} remains unresolved`); + } + assert.equal(JSON.stringify(agentEvent), originalSnapshot, "Agent payload mutated"); + return { + task_id: taskIdentity.task_id, + initial_result_type: taskIdentity.result_type, + review_status: resolvedState.review_status, + next_state: "preflight", + creates_second_card: false, + agent_payload_immutable: true + }; +} + +runCheck("all JSON fixtures parse", () => { + const files = readdirSync(fixturesDir).filter((name) => name.endsWith(".json")); + assert.deepEqual(files.sort(), [ + "allotment_scope.json", + "candidate_gate.json", + "main_outcomes.json", + "manual_review_resolution.json", + "parent_split_two_children.json", + "row_multiple_derived.json", + "source_identity_errors.json" + ]); + for (const file of files) loadFixture(file); +}); + +const mainOutcomes = loadFixture("main_outcomes.json"); +for (const fixture of mainOutcomes.cases) { + runCheck(`main outcome ${fixture.case_id}`, () => { + if (fixture.expect_valid) { + validateMainOutcome(fixture.value); + } else { + assert.throws(() => validateMainOutcome(fixture.value)); + negativeFixturesRejected += 1; + } + }); +} + +const candidateGate = loadFixture("candidate_gate.json"); +for (const fixture of candidateGate.cases) { + runCheck(`candidate gate ${fixture.case_id}`, () => { + if (fixture.expect_valid) { + validateCandidateEvents(fixture.candidate_events); + if (fixture.candidate_events.length === 0) { + assert.equal(fixture.input_is_understood, true); + assert.equal(fixture.expected_action, "return_S10"); + assert.ok(!Object.prototype.hasOwnProperty.call(fixture, "skill_disposition"), "Main no-candidate route does not come from Skill"); + } else if (fixture.skill_disposition) { + validateNoSupportedEventDisposition(fixture.skill_disposition, fixture.candidate_events); + assert.equal(fixture.expected_action, "convert_internal_no_supported_event_to_S10"); + } else { + assert.equal(fixture.expected_action, "invoke_booking_desk_event"); + } + } else { + assert.throws(() => validateCandidateEvents(fixture.candidate_events)); + negativeFixturesRejected += 1; + } + }); +} + +const parentSplit = loadFixture("parent_split_two_children.json"); +runCheck("Parent split has two Child New events and one linked Parent Cancel", () => { + validateBusinessRoot(parentSplit); + const children = parentSplit.message_events.filter((event) => event.event_type === "New Booking"); + const parents = parentSplit.message_events.filter((event) => event.relationship_type === "linked_parent_release_after_child_split"); + assert.equal(children.length, 2); + assert.equal(parents.length, 1); + for (const child of children) { + assert.equal(child.extracted_fields.booking_object_type, "Group Block"); + assert.notEqual(child.case_keys.group_code, "PARENT-2608"); + } + const parent = parents[0]; + assert.equal(parent.event_type, "Cancel Booking"); + assert.equal(parent.extracted_fields.cancel_object_type, "group_block"); + assert.equal(parent.case_keys.group_code, "PARENT-2608"); + assert.equal(parent.requires_downstream_hard_validation, true); + assert.deepEqual( + [...parent.related_source_event_indices].sort(), + children.map((child) => child.source_event_index).sort() + ); + assert.deepEqual( + [...parent.extracted_fields.child_group_codes].sort(), + children.map((child) => child.case_keys.group_code).sort() + ); + assert.deepEqual(deriveAdapterTriplet(parent), { + result_type: "normal_task", + task_type: "Cancel Booking", + task_subtype: "linked_parent_release_after_child_split" + }); + assert.ok(parentSplit.message_events.every((event) => event.event_type !== "Allotment Maintenance")); +}); + +const rowDerived = loadFixture("row_multiple_derived.json"); +runCheck("one current row may produce a main event plus linked derived events", () => { + validateBusinessRoot(rowDerived); + assert.ok(rowDerived.message_events.length > 1); + const rowReferences = new Set( + rowDerived.message_events.map((event) => event.context_used.row_evidence.row_reference) + ); + assert.deepEqual([...rowReferences], ["booking-update.xlsx#BOOKING 08-2026#42"]); + const main = rowDerived.message_events.find((event) => event.source_event_index === "E_ROW_CHILD"); + const parent = rowDerived.message_events.find((event) => event.relationship_type === "linked_parent_release_after_child_split"); + const trace = rowDerived.message_events.find((event) => event.relationship_type === "linked_trace"); + assert.equal(main.event_type, "New Booking"); + assert.deepEqual(parent.related_source_event_indices, [main.source_event_index]); + assert.equal(trace.related_source_event_index, main.source_event_index); + assert.equal(trace.case_keys.group_code, main.case_keys.group_code); + assert.equal(trace.extracted_fields.trace_subtype, "extra_bed"); +}); + +const allotmentScope = loadFixture("allotment_scope.json"); +runCheck("standalone partial allotment maintenance is a Main no-candidate S10", () => { + const fixture = allotmentScope.standalone_partial_allotment; + validateCandidateEvents(fixture.candidate_events); + assert.equal(fixture.candidate_events.length, 0); + assert.equal(fixture.input_is_understood, true); + assert.equal(fixture.expected_route, "S10"); +}); + +runCheck("mixed partial allotment maintenance is preserved as unhandled content", () => { + const fixture = allotmentScope.mixed_partial_allotment; + validateBusinessRoot(fixture.output); + assert.equal(fixture.output.message_events.length, 1); + assert.equal(fixture.output.unhandled_current_intents.length, 1); + assert.equal(fixture.output.unhandled_current_intents[0].text_raw, fixture.expected_unhandled_text); + for (const forbidden of fixture.forbidden_event_types) { + assert.ok(!fixture.output.message_events.some((event) => event.event_type === forbidden)); + } +}); + +runCheck("new Allotment or Control Block remains supported", () => { + const fixture = allotmentScope.new_allotment_control_block; + validateEvent(fixture.event); + assert.deepEqual(deriveAdapterTriplet(fixture.event), fixture.expected_adapter_triplet); +}); + +runCheck("entire Allotment or Control Block cancellation remains supported", () => { + const fixture = allotmentScope.cancel_entire_allotment; + validateEvent(fixture.event); + assert.deepEqual(deriveAdapterTriplet(fixture.event), fixture.expected_adapter_triplet); +}); + +const manualReview = loadFixture("manual_review_resolution.json"); +runCheck("generic SUITE keeps known New Booking subtype in manual-review mode", () => { + const fixture = manualReview.known_subtype_manual_review; + validateEvent(fixture.event); + assert.equal(fixture.event.event_type, "New Booking"); + assert.notEqual(fixture.event.manual_review, null); + assert.equal(fixture.event.extracted_fields.booking_object_type, "Group Block"); + assert.equal(fixture.event.extracted_fields.room_items[0].room_type_raw, "SUITE"); + assert.equal(fixture.event.extracted_fields.room_items[0].pms_room_type_code, null); + assert.notEqual(fixture.event.requires_downstream_hard_validation, true); + assert.ok(!Object.prototype.hasOwnProperty.call(fixture.event.manual_review, "required_user_inputs")); + assert.deepEqual(deriveAdapterTriplet(fixture.event), fixture.expected_adapter_triplet); + assert.equal(fixture.expected_adapter_triplet.task_type, "New Booking"); + assert.equal(fixture.expected_adapter_triplet.task_subtype, "new_group_block"); +}); + +runCheck("business manual_review.missing_fields uses resolvable RFC 6901 pointers", () => { + const event = manualReview.known_subtype_manual_review.event; + assert.ok(event.manual_review.missing_fields.length > 0); + for (const pointer of event.manual_review.missing_fields) { + assert.ok(isRfc6901Pointer(pointer)); + assert.equal(resolvePointer(event, pointer), null); + } +}); + +runCheck("unknown business type alone routes to Fallback", () => { + const fixture = manualReview.type_unknown_fallback; + validateEvent(fixture.event); + assert.equal(fixture.event.event_type, "Need Manual Review"); + assert.deepEqual(deriveAdapterTriplet(fixture.event), fixture.expected_adapter_triplet); +}); + +runCheck("same-card resolution keeps Agent payload and initial result immutable, then enters Preflight", () => { + const resolution = manualReview.same_card_resolution; + const source = manualReview[resolution.source_case].event; + const actual = resolveSameCard( + resolution.task_identity, + resolution.initial_state, + resolution.resolved_state, + source + ); + assert.deepEqual(actual, resolution.expected); +}); + +const sourceIdentity = loadFixture("source_identity_errors.json"); +const observedSourceErrors = []; +const rejectedMessageClasses = new Set(); +for (const fixture of sourceIdentity.cases) { + runCheck(`source identity ${fixture.case_id}`, () => { + const actual = processSourceIdentityStep(fixture.input); + assert.deepEqual(actual.outcome, fixture.expected_error); + if (actual.outcome !== null) { + assert.deepEqual(actual.processing, fixture.expected_processing); + assert.equal(actual.processing.material_reads, 0); + assert.equal(actual.processing.skill_calls, 0); + assertExactKeys(actual.outcome, ["result_type", "error_code", "retryable", "missing_fields"], "typed infrastructure error"); + assert.equal(actual.outcome.result_type, "infrastructure_input_error"); + assert.equal(actual.outcome.error_code, "missing_source_message_id"); + assert.deepEqual(actual.outcome.missing_fields, ["source_message.source_message_id"]); + assert.ok(!Object.prototype.hasOwnProperty.call(actual.outcome, "route_code")); + assert.ok(!Object.prototype.hasOwnProperty.call(actual.outcome, "message_events")); + observedSourceErrors.push(actual.outcome); + rejectedMessageClasses.add(fixture.message_class); + } + }); +} + +runCheck("Cancel, Thanks, and See Attached inputs share the same Step-0 typed error", () => { + assert.deepEqual([...rejectedMessageClasses].sort(), ["Cancel", "See Attached", "Thanks"]); + assert.ok(observedSourceErrors.length >= 3); + for (const outcome of observedSourceErrors.slice(1)) assert.deepEqual(outcome, observedSourceErrors[0]); +}); + +if (failures.length > 0) { + process.stderr.write(`\n${failures.length} contract check(s) failed; ${passed} passed.\n`); + process.exitCode = 1; +} else { + process.stdout.write(`\nP0 contract fixtures valid: ${passed} checks passed; ${negativeFixturesRejected} negative fixtures rejected as expected.\n`); +} diff --git a/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/04_Known_Issues_非契约/prompt_skill_p1_p2_reassessment_2026-07-11.md b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/04_Known_Issues_非契约/prompt_skill_p1_p2_reassessment_2026-07-11.md new file mode 100644 index 0000000..7ce38e9 --- /dev/null +++ b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/04_Known_Issues_非契约/prompt_skill_p1_p2_reassessment_2026-07-11.md @@ -0,0 +1,441 @@ +# Prompt / Skill P1、P2 重新评估(P0 后现状) + +评估日期:2026-07-11 +评估对象:当前 Main Prompt、`booking-desk-event` Skill、全部生产 references、README、P0 fixtures/validator、开发交接 MD/Excel 与 ADR-001。 +评估性质:只读复核;本报告没有修改生产规则。 + +## 1. 结论先说 + +旧报告中的“12 组 P1 + 8 组 P2”不能原样继续使用,因为 P0 已解决一部分问题,也让一部分旧问题合并或改了性质。 + +按当前文件去重和重新定级后,建议形成: + +- **P1:12 组**。数字碰巧仍是 12,但成员已经变化:有旧项解决、合并或降级,也有 P0 后新暴露的 package、adapter 和同卡解阻缺口。这些问题会让同一输入走不同事件/卡型、产生不同数量的事件、让严格消费者接收到不兼容结构,或让开发没有唯一权威映射。 +- **P2:8 组**。数字也碰巧仍是 8,但同样不是原来的 8 项;这些问题主要集中在较窄的领域语义、示例、组合场景和测试覆盖,仍可能判错,但通常不是入口契约立即失效。 +- **已解决的旧问题:2 个明确结论**:旧 P1-6 的 Allotment/Parent 边界已解决;旧 P2-3 的 filename-only Voucher 分流已解决。不过 Voucher 又暴露出一个相邻的新 P1 冲突,不能据此认为 Voucher 全部完成。 +- **P3/文档卫生:1 项**:测试仍引用个人桌面绝对路径,不是生产业务冲突,但会破坏测试可移植性。 + +最需要先修的不是所有 P1 一起铺开,而是下面四项: + +1. 42 条 adapter 路由没有生产级单一权威源,当前只完整存在于“派生交接件”。 +2. Fix Charge 的动作与 Schema 没闭合。 +3. Preflight/系统状态的两层门控与同卡解阻模型没有闭合。 +4. Rooming List 专项 reference 仍能生产现行目录禁止的 `Note`。 + +## 2. 旧 20 项如何变化 + +| 旧编号 | 旧问题 | 当前结论 | 当前归属 | +| --- | --- | --- | --- | +| 旧 P1-1 | 历史查询 gate 太窄 | 仍存在 | 新 P1-6 | +| 旧 P1-2 | 日期规则适用范围不一致 | 仍存在 | 新 P1-5 | +| 旧 P1-3 | Fix Charge 触发/Schema 不闭合 | 仍完整存在 | 新 P1-1 | +| 旧 P1-4 | Update / Trace 重叠 | 大部分缓解,但剩余仍会直接改变 event type | 新 P1-12(靠后处理) | +| 旧 P1-5 | Update / Manual RateCode 重叠 | 仍存在 | 新 P1-4 | +| 旧 P1-6 | Allotment 部分释放/Parent Cancel | 已由 ADR-001 解决 | 移除 | +| 旧 P1-7 | `extraction_warnings` 阻塞语义 | 部分解决,剩余并入公共 Schema | 新 P1-7 | +| 旧 P1-8 | NMR 事件模型 | 九字段已解决,目标候选模型并入公共 Schema | 新 P1-7 | +| 旧 P1-9 | source/event requiredness 两版本 | event 已解决,source 示例仍残留 | 新 P2-2 | +| 旧 P1-10 | 根数组/locator 无 Schema | 仍存在 | 新 P1-7 | +| 旧 P1-11 | Invoice/Trace/payment 措辞 | Trace 主体已修,Invoice/payment 仍有冲突 | 新 P1-2 | +| 旧 P1-12 | Preflight 所有权不清 | 仍存在,并发现非字段阻塞无法稳定解阻 | 新 P1-3 | +| 旧 P2-1 | TA Recorder 可选/必选 | 仍存在,属于派生条件歧义 | 新 P2-1 | +| 旧 P2-2 | Rooming List Note/Trace | 仍存在,且违反 P0 active/legacy 目录 | 新 P1-8 | +| 旧 P2-3 | Voucher filename-only | 原冲突已解决;相邻的“明确卡型但文件对象缺失”仍冲突 | 新 P1-9 | +| 旧 P2-4 | HNM/Honeymoon 双重语义 | 仍存在 | 新 P2-3 | +| 旧 P2-5 | Rate Code 金额未标渠道 | 仍存在 | 新 P2-4 | +| 旧 P2-6 | Trace 日期无依据补全年月 | 仍存在且测试固化错误期望 | 新 P2-5 | +| 旧 P2-7 | Amend Group Code + 普通修改 | 通用拆意图规则有所帮助,但组合/目标绑定未定义 | 新 P2-6 | +| 旧 P2-8 | 测试治理 | P0 有了可执行测试,但 P1/P2 和部分 P0 语义仍无有效断言 | 新 P2-8;绝对路径另列 P3 | + +本轮还新增发现:42 条 adapter 三元组没有生产级机器可读权威源(新 P1-10);Main→Skill 只冻结 candidate item、没有冻结完整 package(新 P1-11);FIT/Group 的“通常按房数”没有冲突优先级(新 P2-7)。 + +## 3. 当前 P1:12 组 + +### P1-1:Fix Charge 的触发、动作和输出 Schema 没闭合 + +**当前证据** + +- `references/52-fix-charge.md:9-15` 只允许复合单价/附加费组件触发。 +- `references/11-update-booking.md:75-77` 和 `52-fix-charge.md:59` 又要求支持 add、update、cancel。 +- `52-fix-charge.md:41` 仍含 `create_or_update_fix_charge / TBD`。 +- `52-fix-charge.md:46` 的 `operation_type` 只能表达 `create_or_update_fix_charge`,不能表达 cancel。 + +**会造成什么影响** + +同一条明确的 Fix Charge 维护请求可能完全不触发,也可能触发后无法生成下游可消费的动作值。`TBD` 进入生产 payload 时,严格 Schema 不能接受,宽松实现则会各自解释。 + +**例子** + +邮件:`Please remove the existing fixed charge from reservation 344055.` + +- 没有 `2000+500` 之类复合价格,所以按触发规则它不是 Fix Charge。 +- 但 Update reference 明确说 cancel Fix Charge 是支持动作。 +- 即使识别出来,当前 `create_or_update_fix_charge` 也表达不了 `cancel`。 + +**建议** + +冻结 `operation=add|update|cancel`;允许明确的维护动词触发,不再只依赖复合金额;删除 `TBD` 和混合工具名。工具调用名称应由下游能力注册表维护,不要作为未决枚举塞进业务事件。 + +### P1-2:Invoice 与 payment 的前置措辞仍会误导 Main/Skill 路由 + +**当前证据** + +- `prompts/main_agent_prompt.md:5` 仍笼统写“不要判断 … Invoice 或 Receipt”。 +- `SKILL.md:16` 仍笼统写“不生成 Invoice 或 Receipt”。 +- `00-output-contract.md:185-202` 却把 `Invoice Generation`、`Invoice Received`、`Payment Notice` 列为 active event。 +- `18-invoice.md:22-28` 真正禁止的是生成实际文件、确认付款和执行会计动作。 +- Main `:60` 仍把没有限定词的 `payment information` 列为 Trace 信号;详细 Trace 规则才补充“已确定、非审批、非凭证、非 Notice/Invoice”。 + +**会造成什么影响** + +模型可能在第一层就把正式支持的 Invoice 邮件排除,或者把付款政策询问错放成 Trace 粗候选。 + +**例子** + +- `Please issue a proforma invoice.` 可能因“不要判断 Invoice”走 S10。 +- `Can the balance be paid on arrival?` 可能因早期广义 `payment information` 被当成 Trace;它其实是当前不支持的付款政策审批询问。 + +**建议** + +统一改成:“可以识别 Invoice/Payment 业务候选,但不得生成实际 Invoice/Receipt 文件、确认到账或执行会计动作。”Main 所有 `payment information` 文案统一限定为“已确定、非审批、非凭证、非 Payment Notice/Invoice”。 + +### P1-3:Preflight 与早期系统状态门控重复,非字段型复核又没有稳定解阻方式 + +**当前证据** + +- README `:22`、Main `:304` 和开发交接 `:258` 都把正式 Case/Preflight/去重/锁校验交给信息系统。 +- `SKILL.md:156`、`10-new-booking.md:37,67`、`11-update-booking.md:40,84` 和 `90-manual-review.md:31` 又规定 pending task、workflow、lock、duplicate 可直接把候选切成业务 manual review。 +- `90-manual-review.md:100-136` 的同卡解阻只具体定义了 `missing_fields[]` 对应 `field_overrides[]` 的字段确认路径。 +- pending/lock 这类阻塞可能 `missing_fields=[]`,没有字段可以让用户填写;当前规则没有说明它应等待重查、由谁解除、是否需要用户覆盖值。 +- type-unknown Fallback 也可能 `missing_fields=[]`,需要用户在 New/Update 等候选路由中作选择;现有 `field_overrides[]` 既不能表达 route selection,也不能改写不可变的原 Agent event。 + +**会造成什么影响** + +同一封 Update 邮件仅因系统查询是否可用,就可能进入 normal 卡或 manual-review 卡;随后 manual-review 卡又可能没有可填写字段,用户看得到卡却无法完成它。 + +**例子** + +订单修改信息全部明确,但系统里已有 pending Update task: + +- 查询成功:Skill 可能输出 Update manual-review。 +- 查询暂时失败:Skill 可能输出普通 Update,稍后由正式 Preflight 拦截。 +- 如果已经进入 manual-review,`missing_fields=[]`,前端没有 field override 可以提交。 +- 如果是“无法确定 New 还是 Update”的 Fallback,用户即使已经选了 Update,当前 resolution 也没有合法字段保存“有效路由选择”。 + +**建议** + +优先选择单一所有者:duplicate/pending/lock 只由下游正式 Preflight 裁决,Skill 仅保留 `context_used`。如果业务坚持保留早期门控,就明确命名 `advisory_preflight`,并定义查询未执行时的稳定结果及“非字段阻塞”的解阻模型,例如系统重查通过后自动 resolved。Fallback 还需要独立的 `route_selection` resolution;它应保存在信息系统的 resolution 中,原 Agent event 继续不可变,不能强迫用户填写不存在的字段或回写 Agent payload。 + +### P1-4:只修改 Rate Code 时仍同时匹配 Update 和 Manual RateCode + +**当前证据** + +- `11-update-booking.md:11-18` 把 manual Rate Code / settlement price 纳入 Update。 +- `53-manual-rate-code.md:3-16` 又说“不能作为 New/Update 内嵌时”或“只有 manual rate maintenance”时输出 Manual RateCode。 +- `02-event-routing-map.md:37,49` 同时保留两条 active 路由,但没有唯一优先级。 + +**会造成什么影响** + +同一邮件可生成 Update 卡或 Manual RateCode 卡。两张卡的 task type、subtype、字段和下游流程不同,统计也会分裂。 + +**例子** + +`For reservation 344055, change the Rate Code to CORP1.` + +这既是“修改已有订单字段”,也是“只有一项 manual rate maintenance”。 + +**建议** + +建议冻结:明确 reservation/group 的 Rate Code 或价格字段修改一律走 Update;`Manual RateCode` 只用于不直接修改某个既有订单的独立 rate-maintenance 工作流,并补充该工作流的强触发信号和最小字段。 + +### P1-5:通用日期规则与 QBD/LianTai 专项规则的适用范围冲突 + +**当前证据** + +- `54-stay-date-parsing.md:5` 把用途限定为 QBD/LianTai 表格日期。 +- `10-new-booking.md:32-38`、`11-update-booking.md:34-41` 和 `00-output-contract.md:249-268` 又要求一般 New/Update 涉及日期时遵循 54。 +- 当前高风险日期用例集中在供应商表格,没有普通正文/PDF 的正式反例。 + +**会造成什么影响** + +普通 FIT 邮件、PDF 或其他供应商材料没有正式日期规则;实现可能错误套用 sheet/酒店列逻辑,也可能把完整日期无故送人工复核。 + +**例子** + +`Arrival 12 Aug 2026, departure 14 Aug 2026.` + +它没有酒店列、行程列或 sheet month/year,但 New Booking 又被要求遵循以这些证据为核心的 54。 + +**建议** + +建立“通用 stay date”规则,定义完整日期、短日期、跨月、时区/locale 和冲突处理;54 只保留 QBD/LianTai 特例。也可以把 54 改成“通用规则 + 供应商子章节”,但测试必须同时覆盖正文、PDF 和 QBD/LianTai。 + +### P1-6:历史查询 gate 仍挡住必要旧值和关系证据 + +**当前证据** + +- Main `:66-70` 只在“已有 active 粗候选且目标无法唯一绑定”时允许主动查历史。 +- 同一 prompt `:71-78` 和 `01-current-history-boundary.md:23-29` 又允许历史补 amendment 旧值、parent allocation 和供应商上下文。 + +**会造成什么影响** + +目标已经唯一,但 before/after 旧值或 parent 关系只在历史里时,Main 被 gate 禁止查询,只能输出不完整候选或不必要的人工复核。 + +**例子** + +`Confirmation 344055, change stay to 2 nights.` + +confirmation 已唯一,但旧 arrival/departure 只在历史邮件。当前 gate 因“目标已唯一”不允许查历史。 + +**建议** + +把查询条件改成:“目标绑定不足,或该 active 候选 reference 明确要求的旧值/关系证据不足。”同时区分输入中已有的 quoted-thread evidence 和主动外部 history lookup。 + +### P1-7:公共候选、warning、目标歧义和 `event_role` Schema 仍不闭合 + +**当前证据** + +- `00-output-contract.md:124-143` 固定要求 `case_candidates[]`、`extraction_warnings[]`,却没有 item schema。 +- `04-main-skill-input-contract.md:43-84` 只定义了内部 `target_hints`,不能替代最终业务根的 public candidate。 +- `00-output-contract.md:217-240` 的 `event_role` 只有示例值,没有 enum。 +- `90-manual-review.md:20-33` 允许目标不唯一时保留已知卡型,但没有定义“一组歧义一个 event”还是“每个候选一个 event”。 +- `03-current-content-completeness.md:106`、`00-output-contract.md:179-180` 仍用 warning / review / S99 的选择式措辞,没有明确“warning 只允许非阻塞质量问题”。 +- `04-main-skill-input-contract.md:84` 要求最终 event 使用 `context_used.main_candidate_ids[]` 记录 provenance,但现有 golden event 常用空 `context_used`,输出契约也没有把该子字段设为条件必填。 +- relationship 字段只给了 prose 约束,没有统一 item/引用 Schema;通用根校验不会拒绝 dangling index 或错误 event type。 + +**会造成什么影响** + +不同实现会自行发明数组 item、关联键和去重规则;两个候选目标可能生成一张或两张人工卡;warning 也可能被误当成终局路径。 + +**例子** + +邮件只写 `John Smith, arrival 15 Aug`,历史找到 `R100`、`R200` 两个候选: + +- 实现 A 输出一张 Update manual-review,四个 case key 全 null。 +- 实现 B 输出两张 Update manual-review,看起来像两项真实修改。 +- 实现 C 私自在 `case_keys` 增加 `guest_name`。 + +另一个例子:Update 关键信息完整,但附件第二页无关页脚 OCR 失败;一个实现 normal + warning,另一个实现整卡人工复核。 + +**建议** + +补一份正式公共输出 Schema: + +- `case_candidates[]`:candidate ID、`target_locator`、四键 canonical candidates、evidence refs、related event index。 +- `extraction_warnings[]`:稳定 code、证据位置、related event index,并明确其不能单独承接阻塞问题;阻塞入口走 S99,阻塞已知卡型必需字段走业务复核。 +- 固定“一项 ambiguity cluster 一个 review event”,候选只表示候选,不能变成多项真实事件。 +- 冻结 `event_role` enum 和 relationship index 类型/去重规则。 +- 明确 `context_used.main_candidate_ids[]` 在最终业务 event 中的 requiredness,并校验每个 ID 来自本次 Main candidate package。 + +### P1-8:Rooming List 专项 reference 仍能生产目录禁止的 Note + +**当前证据** + +- `14-rooming-list.md:42-50` 仍写 Rooming List 可以派生 Note。 +- `17-ta-recorder-note.md:7-10,41-50` 仍把 Note 列为事件并提供 payer code、charge code、breakfast、note text 等 producer。 +- `00-output-contract.md:204-209`、`02-event-routing-map.md:51-56` 和 ADR-001 明确 `Note` 只能读历史,当前 producer 必须为 0。 +- 开发交接要求每个已确认 target 派生 TA Recorder,并给 Note 0 条合法三元组。 + +**会造成什么影响** + +同一名单可能多出 adapter 无法路由的 Note;早餐、布置等内容还可能同时生成 Note 和 Trace。 + +**例子** + +workbook 的备注写 `Breakfast for 20; honeymoon setup`。按旧专项 reference 可能生成 Note;按 Trace 规则应生成 meal/room_setup Trace;按现行目录 Note 又必须被拒绝。相同内容因此可能重复,或在 adapter 层丢失。 + +**建议** + +删除 `14/17` 中的当前 Note producer,并把 `17-ta-recorder-note.md` 改成 TA Recorder-only 或将 Note 章节明确移入 legacy reader/migration。breakfast/meal/setup 等开放文本走 Trace;当前不支持的 PMS 维护或审批内容走 `unhandled_current_intents`。TA Recorder 的“必选/可选”问题单列为 P2-1,不与非法 event producer 混为一项。 + +### P1-9:Voucher 的 filename-only 已解决,但“明确卡型、文件对象缺失”仍有硬冲突 + +**当前证据** + +- `13-voucher-payment.md:43-57,100-111` 已正确区分被动 filename-only、已知卡型但附件缺失/不可读、type-unknown 和方向未知。 +- 但 `SKILL.md:97` 仍绝对写“Voucher 必须有当前图片、PDF 或文件证据”。 +- 专项 reference `:55` 则允许 current 正文已经明确 `Voucher Received` 或 `Payment Evidence`,只是必要附件缺失/无法取得时,保留已知业务卡并进入 manual review。 + +**会造成什么影响** + +正文明确要求处理本次 Voucher、但上传过程中附件对象丢失时,一个实现会输出 Voucher manual-review,另一个会因硬边界拒绝产生事件并走 S10/S99。 + +**例子** + +- 只有文件名 `voucher_HD123.pdf`,无文件对象、无明确当前处理语义:S10。这一条已经清楚。 +- 正文:`Please process the attached CREDIT VOUCHER for HD123`,但附件上传丢失:专项 reference 要保留 Voucher 卡;Skill 硬边界说没有文件证据不能生成。 + +**建议** + +把“normal Voucher 必须有真实文件证据”和“明确卡型但证据丢失时允许 type-known review”分开写。保持 filename-only 的 S10 结论,不要重新打开旧冲突。 + +### P1-10:42 条 adapter 路由没有生产级单一权威源 + +**当前证据** + +- 开发交接 MD `:8` 明确自称生产契约的派生件,不能覆盖上游。 +- 完整 42 条 `result_type + task_type + task_subtype` 映射只出现在该交接 MD/Excel 的 `:97-140`。 +- 生产 `00-output-contract.md` 只规定“保留原业务 task type/subtype”的原则,没有给出 19 个 subtype 到具体三元组字符串的完整表。 +- P0 validator 的 `deriveAdapterTriplet()`(`validate_p0_contracts.mjs:353-409`)只实现 New、Update、Cancel、Cancel Allotment、Trace;其他 active event 会直接 `throw`,没有自动验证全部 42 条。 +- Parent split 的 Parent event 同时具有 `cancel_object_type=group_block` 和 `relationship_type=linked_parent_release_after_child_split`;交接表中的普通 group cancel 与 linked parent cancel 两行都可命中,但生产 contract 没有冻结 adapter 判别优先级。 + +**会造成什么影响** + +开发必须依赖一份自称“非规则源”的文档才能知道具体 task subtype;未来生产 event 字段改动时,没有单一机器可读源可以生成交接件并阻止漂移。 + +**例子** + +`Voucher Received` 应映射到 `lian_tai_credit_voucher`,`TA RECORDER` 应映射到 `maintain_ta_recorder`。这些精确 subtype 字符串无法从当前生产 reference 唯一推导,只在交接表里完整出现。Parent event 则可能被 Adapter A 先按 object type 路由成 `cancel_group_block`,被 Adapter B 先按 relationship 路由成 `linked_parent_release_after_child_split`。 + +**建议** + +新增生产级 `05-adapter-routing-contract`(推荐机器可读 JSON/YAML,或带可执行表的 reference),把 42 条映射和判别优先级作为唯一源;例如 Parent relationship 判别必须先于普通 cancel object 判别。开发 MD、Excel 和测试都由它生成,并完整校验 42 条唯一性、19 个业务 subtype 的 normal/manual 成对关系和禁用 legacy 事件。 + +### P1-11:Main→Skill 只冻结 candidate item,没有冻结完整 package + +**当前证据** + +- Main `:93-127` 给了素材包初始化 shape,但没有冻结各 material item、history/system context 和 unknowns 的完整类型。 +- `04-main-skill-input-contract.md:73-84` 要求 `evidence_refs[].index` 必须真实指向对应 current material,却没有一份完整 package schema 可用于验证引用。 +- `candidate_gate.json` 和 validator 只传入/校验 `candidate_events` 数组;没有同时传对应 attachments/tables/OCR,因此 `index=999` 也无法判断是否越界。 + +**会造成什么影响** + +candidate item 自身合法,不代表它引用的材料存在。Skill 可能在运行时读错附件、错行或直接报 contract error;不同编排实现也会自行决定 material item shape。 + +**例子** + +```json +{ + "candidate_id": "C1", + "evidence_refs": [{"source": "current_attachments", "index": 999}] +} +``` + +如果实际 `current_attachments=[]`,这应该在调用 Skill 前失败;现有 candidate-only validator 看不到附件数组,不能拒绝它。 + +**建议** + +把 `04-main-skill-input-contract.md` 扩成完整 package schema:冻结 source/body/material arrays、attachment/table/OCR item、history/system context、candidate events、unknowns 和错误返回;validator 必须接收整个 package,校验引用存在、索引范围、current-only 来源和 candidate provenance。 + +### P1-12:Update / Trace 的“修改既有备注”仍可产生不同 event type + +P0 已明确主修改与 linked Trace 分开,不能把补充信息吞进 Update;但 `11-update-booking.md:5-19` 仍把“备注或其他订单细节”纳入 Update,Trace 又把 high floor、quiet room 等偏好定义为补充信息。 + +**会造成什么影响** + +同一输入可能生成 Update、Trace 或两张重复卡。虽然场景比旧版窄,但它仍是直接的 event-type 路由漂移,因此保留为靠后的 P1,而不是只当措辞问题。 + +**例子** + +`Please change the existing note from high floor to quiet room.` + +**建议** + +冻结语义边界:修改/删除一个可定位的已存字段或备注,且 before/after 清楚时走 Update;新增偏好、要求、安排或事实走 Trace,不受 `update/add note` 动词影响;同一内容不得同时作为 Update 核心字段和 Trace。 + +## 4. 当前 P2:8 组 + +### P2-1:TA Recorder 仍在“可能/通常派生”和“每个目标必须派生”之间摇摆 + +`14-rooming-list.md:42-50` 写 Rooming List “可能派生”,`17-ta-recorder-note.md:5` 写“通常由”,但同文件 `:21-29` 又要求每个已确认 group 派生一个 TA Recorder;开发交接和高风险清单也按必派生理解。 + +例子:一个 workbook 有两个已确认团号。可选解释只出 2 个 Rooming List;必选解释还要出 2 个 TA Recorder,共 4 个事件。 + +建议:若当前产品要求已确认 target 必派生,就统一改成“每个已确认 target 必须且只能派生一个 TA Recorder”;目标未确认时先保留 Rooming List 卡的人工确认,不提前生成无目标 TA Recorder。补事件数和关系的可执行断言。 + +### P2-2:Skill 的 `source_message` 示例仍比正式固定 shape 少字段 + +event 的 `attachments`、`file_references`、`manual_review` requiredness 已解决;只剩 `SKILL.md:118-127` 示例仅含 `source_message_id`,而 `00-output-contract.md:124-143` 要求六个 metadata key 固定存在。 + +例子:严格 validator 接受完整 source shape,但照抄 Skill 示例会缺 `subject/from/cc/received_at/source_channel`。 + +建议:把 Skill 和交接示例同步为完整 shape;不可得值用 `null` 或 `[]`。由于 00 已是明确权威、剩余主要是示例误导,降为 P2。 + +### P2-3:`HNM/Honeymoon` 没有区分结构化房型代码和自然语言布置要求 + +`50-room-type-mapping.md:9-20` 把 `HNM, Honeymoon` 默认走 DBL;`16-trace-notes.md:67-74,139-143` 又把 honeymoon 作为 room setup Trace。 + +例子:`Room Type: TWN; Remark: honeymoon decoration.` 正确应是 RM3 + room-setup Trace;宽泛规则可能把房型改成 DBL 或制造无谓冲突。 + +建议:只有已知供应商的结构化房型字段中正式 `HNM` code 才能参与房型映射;正文/备注的 honeymoon 只能作为 Trace,不能决定 PMS 房型。 + +### P2-4:Rate Code 金额示例仍未绑定 supplier/channel + +`51-rate-code.md:9-15` 区分 QBD/LianTai,但 `:17-22` 又写无渠道的 `850/900/1000/1200 -> code`;`:24` 才补“渠道优先”。 + +例子:QBD 900 应是 `GRPA1-900`,LianTai 900 是 `GRP1-900`;无渠道的 `900 -> GRP1-900` 会误导模型。 + +建议:每个金额示例绑定 supplier/channel;渠道未知时只保留 price evidence,`rate_code=null` 并在原业务卡人工确认。 + +### P2-5:Trace 示例仍无依据地从 group code 补全年月 + +`16-trace-notes.md:55-60` 要求日期只能由可靠上下文唯一支持;`:82-100` 却仅用 `HD260510A + 12/5` 输出 `2026-05-12`。项目没有 group-code 日期 codec,`high_risk_cases.md:25` 还把该结果写成期望。 + +例子:只有 `HD260510A - 12/5 FULL DAY Meeting` 时,安全值应是 `service_date=null` 并保留 raw `12/5`;只有邮件住期或 sheet context 明确 2026-05 时才可补全。 + +建议:给示例补可靠年月证据,否则将期望改为 null。若业务确有 group-code 编码,另建供应商专属 codec,明确格式、适用范围和冲突处理。 + +### P2-6:Amend Group Code 与日期/房量组合的事件数量和目标绑定未定义 + +通用规则已要求独立 current 信号先拆分,这比旧版更好;但 `15-amend-group-code.md:24-50` 只定义 old/new 两字段和 Amend + Trace,没有定义 Amend + Update 的关系、Update 绑定旧团号还是新团号。 + +例子:`Change OLD123 to NEW123, move arrival to 12 May, add 2 TWN.` 可能只出 Amend,也可能出 Amend + Update,但 Update 的 key 不一致。 + +建议:明确输出两个相关主事件;优先用稳定 confirmation/reservation number 共同绑定。只有 group code 时,定义 before/after target 和 relationship;无法确认时保留已知 Update 卡型并进入人工确认,不能把日期/房量静默塞进 Amend context。 + +### P2-7:FIT / Group Block 的“通常按房数”缺少冲突优先级 + +`10-new-booking.md:13-18` 写“小于 5 间通常按 FIT,5 间及以上通常按 Group Block”,但没有说明显式对象词、渠道规则、团队名称和房数冲突时谁优先;`booking_object_type` 又是 adapter 三元组的 subtype 判别字段。 + +例子:正文明确写 `group booking` 但只有 4 间,或一个家庭 FIT 请求 5 间。两个实现可能分别只看房数或只看显式业务词,生成不同 task subtype。 + +建议:冻结判别优先级,例如“受支持供应商结构化对象类型/显式 group or FIT 指令 > 已知渠道规则 > 房数阈值”;冲突且 subtype 无法确定时才走 type-unknown Fallback。补 4-room explicit group、5-room explicit FIT、无显式词的 4/5-room 四组用例。 + +### P2-8:P0 有可执行测试,但当前绿色结果不能覆盖 P1/P2,也没有发现部分 P0 测试文字回退 + +**已经改善** + +- 7 份 JSON fixture 和 `validate_p0_contracts.mjs` 当前运行结果为 32 checks passed、9 个负例被拒绝。 + +**仍有问题** + +- `deriveAdapterTriplet()` 只实现 5 类 active event 分支,没有覆盖 Voucher、Payment Evidence、Rooming List、Amend Group Code、TA Recorder、Invoice、Payment Notice、Manual RateCode 的 42 路由。 +- `validateBusinessRoot()` 只检查 `case_candidates`、`extraction_warnings` 是数组,不检查 item,因为生产本身也没有 item schema。 +- Parent 测试对 `child_group_codes[]` 和 `related_source_event_indices[]` 分别排序后比较,不能证明二者按原顺序一一对应;交换两个 index 仍可能通过。 +- 没有验证最终 event 的 `context_used.main_candidate_ids[]` provenance。 +- `high_risk_cases.md:19,45,49,50` 仍把 type-known 日期、Trace 目标、Rooming List 目标和 Voucher 团号歧义写成 `Need Manual Review`,与 P0 当前“保留原业务 event + 非空 manual_review”冲突。 +- P1/P2 领域场景仍主要是 Markdown,自然语言清单不会让 CI 失败。 + +**建议** + +先修正过时的 Markdown 期望,再将本报告每一项做成正反 golden fixture。测试至少要覆盖:全部 42 路由、public root item schema、candidate provenance、关系数组按位置配对、type-known review 不回退 Fallback,以及本报告列出的领域边界。 + +## 5. P3:测试绝对路径 + +`tests/booking-desk-event/high_risk_cases.md:38` 仍引用 `/Users/chillishark/Desktop/.../53.eml`。这不会直接改变生产路由,所以不再占一个 P2 业务冲突名额;但换机器无法复现。 + +建议把脱敏样本放入仓库 `tests/booking-desk-event/fixtures/` 并用相对路径引用;若原始邮件不能入库,就制作最小脱敏 JSON/EML fixture。 + +## 6. 推荐修改顺序 + +建议分四批,不要一次把所有 reference 平铺修改: + +1. **先封闭机器契约**:完整 42 路由单一权威源、Main→Skill 完整 package、公共 root/relationship/provenance schema、Fix Charge 和 source shape。 +2. **再封闭路由边界**:Rooming List/TA/Note、Voucher 文件证据、Invoice/payment、Update/Manual RateCode。 +3. **再封闭流程状态**:Preflight/advisory gate、非字段阻塞解阻、历史查询 gate。 +4. **最后修领域规则与测试**:通用日期、HNM、Rate Code、Trace 日期、Amend 组合,并把每项转为可执行 fixture。 + +在这四批中,ADR-001 已确认的 P0 结论必须保持不变: + +- 不恢复部分配额 Update 或 Allotment Maintenance producer。 +- 不恢复当前 Note producer。 +- type-known 字段/目标歧义保留原业务 event,不能重新 blanket Fallback。 +- Parent split 继续是 Child Group New + Parent Group Cancel candidate。 +- 房型不唯一继续使用同业务卡 manual-review mode,不改回普通候选或第二张卡。 + +## 7. 验证说明 + +- 本轮重新运行 `node tests/booking-desk-event/validate_p0_contracts.mjs`:32 项通过,9 个非法负例按预期拒绝。 +- `.project-docs` 完整性检查通过。 +- 上述通过结果只能证明现有 P0 fixture 自洽,不能证明本报告列出的 P1/P2 已关闭;原因见 P2-8。 diff --git a/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/CHECKSUMS.sha256 b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/CHECKSUMS.sha256 new file mode 100644 index 0000000..0fdb0fc --- /dev/null +++ b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/CHECKSUMS.sha256 @@ -0,0 +1,19 @@ +5311be08abeaca493933caf646f47f93ac90fd88dc5652e2bd29751d9147b993 ./01_Adapter_Frontend/任务卡前端字段变更与路由说明_3.0_to_current.md +e8c02672807b61a1f62de604a004e5b0f5758f55c0c4dc3542f0b1a31b532943 ./01_Adapter_Frontend/任务卡前端字段变更说明_3.0_to_当前版_2026-07-10.xlsx +64063a6d24c2e70bb0d9d557923fdbecb9e133c1ea5971f3858df03469e48693 ./02_AI_Runtime/booking-desk-event.skill +7ce8a5beab05638fa7f3088b179ef86ee3dca6bfa11447bbed14d8b0017fddf9 ./02_AI_Runtime/main_agent_prompt.md +ff1155706ee818bc973c8dac3405aecbcda711fe41b22850b66e4a1c75f85377 ./02_AI_Runtime/核心契约_展开阅读/00-output-contract.md +b9e2a6d2964de8e4a2691508ca36297b8efaab80d713c5cead0c382b748662a7 ./02_AI_Runtime/核心契约_展开阅读/02-event-routing-map.md +9fab9a398a3320e41c701918174442d4269ac5407a56e091e29da33a00084165 ./02_AI_Runtime/核心契约_展开阅读/03-current-content-completeness.md +c8f8503be23ce8e28fd35cf633cc80d2850fa0308fe9ed4a9232f6ee8ab8cbfb ./02_AI_Runtime/核心契约_展开阅读/04-main-skill-input-contract.md +3c6257c81e83a5feb7fb5125242dcd356b00d2519f2071ab07fb04337d364532 ./02_AI_Runtime/核心契约_展开阅读/90-manual-review.md +e7382dcb77b0161250e48d3ec3f287a446b0cfad2473a06cf7cfbd10b8def232 ./03_P0_Acceptance/fixtures/allotment_scope.json +c849f0a677ccc97a040502ed78d7d00152793b0ca45c127ea1d4b911a900b4a8 ./03_P0_Acceptance/fixtures/candidate_gate.json +0eea30534fd6aa2a19eb275630f533ac99c06b2401d4d0d66744042cbcc603dd ./03_P0_Acceptance/fixtures/main_outcomes.json +781b3d87657b91bb732836937487b1f1c8cec53db2540c56e149ca757e116a15 ./03_P0_Acceptance/fixtures/manual_review_resolution.json +e3f62c503f7702225601fd5fb51d69e79369bf8cb3c308c59fc99b8481cfce8e ./03_P0_Acceptance/fixtures/parent_split_two_children.json +96aefe4ecea57de43a44f7b6dd358298c324bb1bdc414668d27cb4d1fedbc54a ./03_P0_Acceptance/fixtures/row_multiple_derived.json +dcf71127f54f895c076046644a73714db061a1886b900ec8a3ebfe992029520e ./03_P0_Acceptance/fixtures/source_identity_errors.json +1a1ba1a5185a1eecb76ccc92ad13647a7bbe93040354d52afac4f1a4ee9971e3 ./03_P0_Acceptance/validate_p0_contracts.mjs +0728302a1d0fdf1728acd1be2f7f0e44b21f448bec59123100ff0db2c7a428a0 ./04_Known_Issues_非契约/prompt_skill_p1_p2_reassessment_2026-07-11.md +96c1882b9494c399f944b62ddb07763c0fc51ae16ce323c18c02c8e1d23f37ff ./README_交付说明.md diff --git a/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/README_交付说明.md b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/README_交付说明.md new file mode 100644 index 0000000..da1155e --- /dev/null +++ b/docs/import/20260711/开发交付_P0冻结基线_2026-07-11/README_交付说明.md @@ -0,0 +1,100 @@ +# 开发交付说明:P0 冻结基线(2026-07-11) + +## 先看结论 + +这是当前可以交给信息系统 adapter/frontend 与 AI runtime 开发的 **P0 冻结基线包**,不是最终完整契约。 + +- P0-1 至 P0-6 已完成规则、交接件和样例同步。 +- P1/P2 已重新评估,但尚未修改生产规则;它们仍是 OPEN 风险。 +- 开发可以先完成 P0 稳定骨架和已冻结行为,但 P1 影响的字段、路由和解阻流程不得自行补规则或猜测。 +- 如果本包中的开发 MD/Excel 与 AI runtime 输出发生差异,必须 fail closed,停止该事件建卡并反馈契约问题。 + +## 不同开发角色拿什么 + +### Adapter / Frontend 必读 + +1. `README_交付说明.md`:交付边界和暂缓项。 +2. `01_Adapter_Frontend/任务卡前端字段变更与路由说明_3.0_to_current.md`:P0 数据流、42 路由、人工复核、Parent split、fail-closed 规则。 +3. `01_Adapter_Frontend/任务卡前端字段变更说明_3.0_to_当前版_2026-07-10.xlsx`:字段、42 条三元组、迁移、非法组合和验收查表件。文件名保留 7 月 10 日,内部 P0 基线和最后验证时间为 7 月 11 日。 +4. `03_P0_Acceptance/`:P0 executable fixtures 与轻量 validator。 +5. `04_Known_Issues_非契约/prompt_skill_p1_p2_reassessment_2026-07-11.md`:当前 12 个 P1、8 个 P2;只用于识别暂缓范围,不是实现 Schema 的规则源。 + +### AI Runtime / Agent 部署开发 + +1. `02_AI_Runtime/main_agent_prompt.md` +2. `02_AI_Runtime/booking-desk-event.skill` +3. `02_AI_Runtime/核心契约_展开阅读/`:从 `.skill` 中额外展开的 5 份核心契约,方便 adapter/frontend 直接评审;内容仍以同版 `.skill` 为准。 + +这两个文件必须作为同一版本发布。`.skill` 已包含 SKILL 和全部 production references,但不包含 Main Prompt。 + +## 当前可以先实现的 P0 范围 + +- Gateway 在读取正文、附件、OCR、历史、系统上下文或调用 Skill 前校验 `source_message.source_message_id`。 +- 缺 ID 时只返回 `infrastructure_input_error + missing_source_message_id`,材料读取与 Skill 调用均为 0。 +- S10/S99 使用两个完整结构,并以 `route_code` 判别;S10 的 `manual_review=null`,S99 必须是完整 `main_agent_entry_review`。 +- 业务邮件按 `message_events[]` 逐 event 处理,不能在邮件根只派生一个业务三元组。 +- type-known review 保留原业务 task type/subtype;字段型复核使用 RFC 6901 pointer 和同卡 `field_overrides`。 +- Parent split、行级隔离、partial-allotment S10/unhandled/Fallback 边界按开发 MD/Excel 的 P0 基线实现。 +- 当前新生成的 `Note`、`Allotment Maintenance` 和 `update_allotment_control_block` 必须拒绝。 +- Agent payload、邮件原文、raw evidence、source ID 和审计字段保持只读;Preflight 通过前不得写 PMS。 + +## 暂缓定型或必须 fail closed 的范围 + +以下内容属于未修复 P1/P2;开发不得自行发明字段或把某个实现当成最终契约: + +- 生产级 42 路由单一权威源与重叠判别优先级。 +- 完整 Main→Skill package schema,以及 `evidence_refs` 的真实索引校验。 +- public `case_candidates[]`、`extraction_warnings[]`、relationship、provenance 和 `event_role` schema。 +- Fallback route selection、pending/lock 等非字段型阻塞的同卡 resolution。 +- Fix Charge 的 add/update/cancel 与 `TBD` schema。 +- Update 与 Manual RateCode、Update 与 Trace 的窄边界。 +- Voucher 业务卡型明确但文件对象丢失的边界。 +- Rooming List reference 中残留的当前 Note producer;当前仍以“Note 不得新生成”为准。 +- 通用日期、HNM/Honeymoon、Rate Code 渠道、Trace 短日期、Amend Group Code 组合和 FIT/Group 判别。 + +完整证据和例子见 `04_Known_Issues_非契约/`。如果开发输入命中上述范围,应 feature flag、阻断或进入明确的契约错误流程,不得由 adapter 复刻 AI 业务判断。 + +## 42 路由的使用方式 + +- MD/Excel 中的 42 条路由是 **版本锁定的 P0 实现和验收基线**。 +- 它们当前完整存在于派生交接件中;项目已将“缺少生产级机器可读单一权威源”登记为 P1。 +- 因此可以据此开发当前 P0 版本,但不能宣称它已经是长期 SSOT;未来 P1 修复后需要重新同步生成交接包。 +- 任一 event 缺少明确判别字段、同时命中多条互斥路由,或与 runtime contract 不一致时,该 event 0 卡并报告 `adapter_contract_error`;siblings 继续独立处理。 + +## P0 样例怎么运行 + +在本交付目录执行: + +```bash +node 03_P0_Acceptance/validate_p0_contracts.mjs +``` + +当前预期:32 项检查通过,9 个非法负例按预期拒绝。 + +该 validator 只覆盖 P0 已冻结子集,不验证全部 42 路由、public root item schema、全部 relationship/provenance 或 P1/P2 领域问题。它通过不代表 P1/P2 已关闭。 + +## 权威和用途 + +| 文件组 | 用途 | 是否可作为生产规则源 | +| --- | --- | --- | +| `02_AI_Runtime/main_agent_prompt.md` + `booking-desk-event.skill` | 当前 AI runtime 行为和部署版本 | 是;两者必须同版 | +| `02_AI_Runtime/核心契约_展开阅读/` | 方便开发查阅的 00/02/03/04/90 核心 reference 副本 | 是同版 runtime 契约的展开副本;不得单独改写 | +| `01_Adapter_Frontend/*.md` / `*.xlsx` | P0 adapter/frontend 派生实现、字段和验收基线 | 否;不得反向覆盖 runtime contract | +| `03_P0_Acceptance/` | P0 executable examples | 否;只验证已覆盖断言 | +| `04_Known_Issues_非契约/` | OPEN 风险、影响和未来修改建议 | 否;不得据此直接发明 Schema | +| `CHECKSUMS.sha256` | 接收和发布前文件完整性 | 校验用途 | + +## 未放入本包的文件 + +- `归档.zip`:与 Main Prompt + `.skill` 内容重复,不放入最小开发包。 +- `high_risk_cases.md`:仍含与 P0 type-known review 不一致的旧 Fallback 期望,不能作为当前验收源。 +- 旧冲突审计、白话报告、`task_plan.md`、`findings.md`、`progress.md`、`.project-docs`:历史/过程证据,不是开发契约。 +- scratch builder、validator、PNG render 和 QA JSON:内部制作/验证材料,不交开发。 + +## 接收检查 + +1. 运行 `shasum -a 256 -c CHECKSUMS.sha256`。 +2. 确认 Main Prompt 和 `.skill` 同时存在。 +3. 打开 Excel,确认 8 个 sheet 和 42 条唯一路由。 +4. 运行 P0 validator。 +5. 开发前阅读 P1/P2 已知问题;命中暂缓范围时先反馈,不自行补规则。 diff --git a/docs/project/frontend-backend/README.md b/docs/project/frontend-backend/README.md index b733320..75bc537 100644 --- a/docs/project/frontend-backend/README.md +++ b/docs/project/frontend-backend/README.md @@ -19,13 +19,15 @@ | 来源 | 当前用途 | | --- | --- | -| `docs/import/20260708/任务卡前端展示字段表 3.0.xlsx` | 前端任务卡展示 / 编辑白名单。前端页面优先按该表决定哪些字段展示、哪些字段可编辑。 | +| `docs/import/20260711/开发交付_P0冻结基线_2026-07-11/01_Adapter_Frontend/任务卡前端字段变更与路由说明_3.0_to_current.md` | 0711 P0 前端 / Adapter 路由说明。前端后续按 42 路由、S10/S99、type-known manual review 和 fail-closed 口径调整页面。 | +| `docs/import/20260711/开发交付_P0冻结基线_2026-07-11/01_Adapter_Frontend/任务卡前端字段变更说明_3.0_to_当前版_2026-07-10.xlsx` | 最新前端任务卡展示 / 编辑白名单和三元组路由表。文件名保留 7 月 10 日,内部基线为 7 月 11 日。 | +| `docs/import/20260708/任务卡前端展示字段表 3.0.xlsx` | 历史前端字段白名单,已被 0711 P0 冻结基线承接。 | | `docs/import/20260706/任务卡展示编辑矩阵.xlsx` | 后端完整规则来源。用于后端校验、最终确认写入、OPERA 映射、展示条件和任务卡完整约束。 | | `docs/import/20260706/AI输出参数并集字典.xlsx` | AI 输出字段路径、字段含义、建议存储方式和索引参考。 | 约束说明: -- 最新前端 Excel 只作为展示 / 编辑白名单,不替代后端完整规则矩阵。 +- 最新前端 Excel 和 0711 路由说明只作为展示 / 编辑白名单、P0 路由和前后端验收基线,不替代后端完整规则矩阵。 - 后端不应因为前端白名单缺少字段而自动放宽必填、枚举、校验或 OPERA 映射规则。 - 如果前端白名单与后端完整矩阵冲突,应先记录到 `frontend-to-backend-api-requests.md` 的待确认问题,再由产品 / 后端 / 前端一起确认。 @@ -37,13 +39,16 @@ | SuperAgent MCP tools | `docs/project/integrations/superagent-mcp/README.md` | MCP 对外交付资料包,tools 字段语义应跟随 SuperAgent HTTP 对外总契约。 | | SuperAgent 任务结果入站接口 | `docs/project/requirements/M002-superagent-task-result-api-contract.md` | 阶段记录,用于理解 M002 接收 AI 结果的落地细节;如与总契约冲突,以总契约为准。 | | SuperAgent 查询上下文接口 1、2 | `docs/project/requirements/M002-ai-query-minimal-fields.md` | 阶段记录,用于理解接口 1、2 的最小字段实现;如与总契约冲突,以总契约为准。 | -| 订单任务主流程 | `docs/project/requirements/M002-order-task-workflow-v2.md` | 当前有效需求,M002 V1 只作为历史参考。 | +| 订单任务主流程 V3 | `docs/project/requirements/M002-order-task-workflow-v3.md` | 当前开发基线,基于 0711 P0 冻结基线,覆盖 S10/S99、42 路由、方案 C、type-known manual review 同卡解阻和 fail-closed。 | +| 订单任务主流程 V2 | `docs/project/requirements/M002-order-task-workflow-v2.md` | 已实现阶段记录,保留用于理解当前代码中的 S000/S999、订单任务流转和 OPERA 模拟骨架。 | | 后端 checkpoint | `docs/project/requirements/M002-backend-checkpoint-plan.md` | 阶段记录,用于理解后端拆分和验收。 | | 前端可用接口与待补接口 | `docs/project/frontend-backend/frontend-to-backend-api-requests.md` | 前后端协作清单,已区分可用、后置和历史候选路径,不替代后端权威契约。 | ## 5. 当前已明确后置事项 - 普通任务切换订单接口后置。 +- M002 V3 已确认采用 0711 P0:新数据迁移到结构化 `S10/S99`;旧 `S000/S999` 继续在任务列表可见;后端已完成 P0 fixtures 回归基线。 +- type-known manual review 已按同一张业务卡解阻,不再统一做成 Fallback;复核场景第一版支持确认当前订单归属,但普通任务任意切换订单仍后置。 - 任务详情 / 任务写操作是否需要显式 `hotel_id` 已确认后置;第一版先按 ID 定位,后续多酒店隔离 / 权限方案统一补齐。 - 邮件会话详情已返回 `html_body_sanitized` 和 `html_render_mode`;前端展示 HTML 时优先使用清洗字段,`html_body` 只作为原始内容兼容字段。 - 用户 / 权限底座后端 CP1 已完成;前端登录页、动态菜单、管理后台和业务审计 actor 全量迁移仍后置。 diff --git a/docs/project/frontend-backend/backend-to-frontend-notes.md b/docs/project/frontend-backend/backend-to-frontend-notes.md index 1852fc9..cfec430 100644 --- a/docs/project/frontend-backend/backend-to-frontend-notes.md +++ b/docs/project/frontend-backend/backend-to-frontend-notes.md @@ -168,6 +168,7 @@ POST /api/auth/logout ``` - `field_pointer` 必须是 RFC 6901 JSON Pointer,并且只能指向当前任务卡可编辑字段;后端会映射到矩阵 `field_path`。非法或只读字段会返回 `TASK_REVIEW_POINTER_INVALID`。 +- 0711 P0 的房型复核可传 `/extracted_fields/room_items/{index}/pms_room_type_code`;当前后端矩阵仍是扁平字段,响应会映射为 `field_path=extracted_fields.pms_room_type_code`。前端展示时保留原始 `field_pointer`,业务字段写入和确认 payload 以返回的 `field_path` 为准。 - 同一次请求不能重复提交同一字段;重复 `field_pointer` 或重复映射到同一 `field_path` 会返回 `TASK_REVIEW_POINTER_DUPLICATE`。 - `confirmed_order_id` 第一版必须等于当前任务的 `order_id`;如果前端需要选择其他订单,仍属于后续“复核场景订单归属选择”细化,不要复用普通任务切换订单能力。 - 解阻成功后返回 `task_status=READY`、`review_status=RESOLVED`、`review_resolution.field_overrides[]`、`confirmed_payload` 和两条 `opera_operations[]`。前端应刷新任务详情并显示 OPERA 模拟操作入口。 @@ -299,7 +300,7 @@ run_label: 可选调试标签 ## 7. 需要持续提醒的后置事项 - 普通任务切换订单接口继续后置。 -- M002 V3 的结构化 `S10/S99` 入站、42 路由枚举 / 稳定配置、`UNHANDLED_CURRENT_INTENT`、`adapter_contract_error` transition 最小落库、任务列表 / 订单时间线 / 任务详情 V3 路由字段和只读诊断块透出、type-known manual review 同卡解阻第一版均已完成。 +- M002 V3 的结构化 `S10/S99` 入站、42 路由枚举 / 稳定配置、`UNHANDLED_CURRENT_INTENT`、`adapter_contract_error` transition 最小落库、任务列表 / 订单时间线 / 任务详情 V3 路由字段和只读诊断块透出、type-known manual review 同卡解阻第一版、typed infrastructure error 和 P0 fixtures 回归基线均已完成。 - 系统管理后台 V1 已完成;后续若要做用户搜索更多筛选、批量操作、密码策略增强、MFA、登录设备管理,应单独开需求。 - 现有 Reservation / SourceMessage 业务接口的强制登录、强制权限和业务审计 actor 全量迁移仍后置。 - 真实 OPERA / OHIP 接入继续后置。 diff --git a/docs/project/frontend-backend/frontend-to-backend-api-requests.md b/docs/project/frontend-backend/frontend-to-backend-api-requests.md index 731686e..297e3e8 100644 --- a/docs/project/frontend-backend/frontend-to-backend-api-requests.md +++ b/docs/project/frontend-backend/frontend-to-backend-api-requests.md @@ -315,7 +315,7 @@ POST /api/system/reservation/demo-data | `tasks[]` | 生成的任务 ID、任务类型、任务 subtype 和任务状态。 | | `entrypoints` | 可直接访问的任务列表、订单列表、订单详情、任务详情、邮件会话详情 URL。 | -第一版 seed 覆盖:队列阻塞、已完成 OPERA 模拟、OPERA 失败可重试、Fallback 人工复核、历史 Message Notification 只读任务、邮件会话完整 HTML / 附件 / 内联图片。旧 S000/S999 特殊只读任务可通过 SuperAgent 回调或后续专用夹具补充;0711 P0 新入口的 S10/S99 需要后端后续 checkpoint 补充结构化 fixture。 +第一版 seed 覆盖:队列阻塞、已完成 OPERA 模拟、OPERA 失败可重试、Fallback 人工复核、历史 Message Notification 只读任务、邮件会话完整 HTML / 附件 / 内联图片。旧 S000/S999 特殊只读任务可通过 SuperAgent 回调补充;0711 P0 新入口的 S10/S99 已纳入后端 P0 fixtures 回归测试参考。 ## 6.1 M002 V3 当前状态和后续待补能力 @@ -330,6 +330,7 @@ POST /api/system/reservation/demo-data | `adapter_contract_error` | 任务详情、错误提示 | 已完成第一版 | 命中 P1/P2 未闭合或路由冲突时,任务详情 `adapter_contract_errors[]` 返回稳定错误 code 和原始片段,不转成 Fallback。 | | type-known manual review 同卡解阻 | 任务详情复核 | 已完成第一版 | `manual_review` 不再全部等同 Fallback;已知业务卡型返回原业务卡信息、`review_status`、`review_resolution` 和可编辑 pointer 字段,解阻后进入 `READY`。 | | 复核场景订单归属确认 | 任务详情复核 | 已完成第一版 | 后端提供复核确认时的订单归属确认;当前第一版只能确认当前任务所属订单,后续如要选择其他订单需另行细化。 | +| P0 fixtures 回归基线 | 联调回归 | 已完成第一版 | 后端已将 0711 P0 fixtures 纳入测试参考;前端对 S10/S99、同卡复核和只读诊断块的展示应继续按本节稳定字段接入。 | 前端注意:不要把访问口令写入前端仓库、浏览器环境变量或构建产物;该接口只能由本地联调人员手动调用或由受控测试脚本调用。 @@ -524,6 +525,8 @@ Content-Type: application/json } ``` +0711 P0 房型复核说明:前端可以提交 `/extracted_fields/room_items/{index}/pms_room_type_code`,后端第一版会兼容映射到当前矩阵字段 `extracted_fields.pms_room_type_code`。响应里的 `review_resolution.field_overrides[].field_pointer` 保留前端原始 pointer,`field_path` 和 `confirmed_payload.field_values` 使用当前后端矩阵字段。 + 返回示例: ```json diff --git a/docs/project/go-live-notes.md b/docs/project/go-live-notes.md index dc8bf11..14668a2 100644 --- a/docs/project/go-live-notes.md +++ b/docs/project/go-live-notes.md @@ -29,7 +29,7 @@ - 业务前端页面展示邮件原文。 - OHIP / OPERA 或其他业务系统真实写操作。 - 普通任务切换订单接口。 -- M002 V3 type-known manual review 同卡解阻、`missing_fields[]` 到任务卡字段白名单映射、typed `infrastructure_input_error` 响应、复核场景订单归属确认、任务列表 / 详情完整透出 V3 路由字段。 +- M002 V3 字段矩阵从当前扁平结构整体迁移到 0711 P0 新结构。 - 现有业务接口强制登录和强制权限拦截。 - 业务审计 actor 全量迁移到当前登录用户。 - Debug EML 上传链路不属于生产普通业务页面能力,生产默认关闭;即使已有登录权限,也不要开放给普通用户。 diff --git a/docs/project/integrations/superagent-api-contract.md b/docs/project/integrations/superagent-api-contract.md index 8a76483..8f419c9 100644 --- a/docs/project/integrations/superagent-api-contract.md +++ b/docs/project/integrations/superagent-api-contract.md @@ -114,15 +114,16 @@ SuperAgent 不应知道或依赖内部 SourceMessage Inbox ID。任务结果通 - 后端会完整保存 AI 三元组、`route_code` 和系统处理分类;`S10/S99` 仍以只读源邮件通知卡展示,任务列表可见,订单列表不可见。 - 旧 `S000/S999` 数据继续兼容展示,语义上分别映射到 `S10/S99`。 -当前后端已完成 M002 V3 CP1-CP2: +当前后端已完成 M002 V3 CP1-CP6: - 已建立 42 条 P0 路由枚举 / 稳定配置。 - 已支持结构化 `S10/S99` 入站,创建只读 `SOURCE_MESSAGE_ONLY` 任务。 - 已支持 V3 业务根 `source_message + message_events[]` 的基础解析;可派生到现有任务模型的 event 会创建业务任务,无法派生、显式契约错误或基础 manual_review / parent split 结构不完整的 event 只落 `adapter_contract_error` transition,不创建业务任务。 - 已支持 `unhandled_current_intents[]` 最小落库:只写 `UNHANDLED_CURRENT_INTENT` transition,不创建业务任务,也不按 adapter 契约错误返回。 - 已在 `workflow_reservation_ai_transition` 保存 `route_code`、`system_process_category`、`adapter_error_code` 和 `adapter_error_message`。 +- 已支持 type-known manual review 同卡解阻、当前订单归属确认、P0 fixtures 回归测试和 V3 typed `infrastructure_input_error` 响应。 -尚未完成:type-known manual review 同卡解阻、`manual_review.missing_fields[]` 到任务卡可编辑字段白名单的完整映射校验、typed `infrastructure_input_error` 响应、复核场景订单归属确认、真实 OPERA / OHIP、普通任务切换订单、任务列表 / 详情接口完整透出所有 V3 路由字段。 +尚未完成:真实 OPERA / OHIP、普通任务切换订单、字段矩阵从当前扁平结构整体迁移到 0711 P0 新结构。 ## 4. 接口 1:查询订单上下文 @@ -481,7 +482,8 @@ S10 示例: "required": true, "notification_type": "source_message_review", "show_source_message": true, - "requires_user_decision": true + "requires_user_decision": true, + "visible_message": "未匹配到当前 Agent 支持的业务事件类型,请查看原邮件并决定是否需要回复或进行其他处理。" }, "manual_review": null } @@ -544,6 +546,20 @@ V3 字段说明: - 业务 event 能派生到稳定路由时,复用现有订单 / 任务 / 任务卡创建链路。 - event 判别字段不完整、显式携带 `contract_errors`、根 `missing_fields`、不完整 `manual_review` 或不完整 parent split 候选时,写入 `adapter_contract_error` transition,不创建订单和任务;同一邮件其他 sibling event 继续处理。 - `unhandled_current_intents[]` 写入 `UNHANDLED_CURRENT_INTENT` transition,不返回 `adapter_error_code`。 +- type-known manual review 第一版在同一业务任务卡解阻;当前字段矩阵仍使用扁平字段,`/extracted_fields/room_items/{index}/pms_room_type_code` 会兼容映射到 `extracted_fields.pms_room_type_code`。 + +`source_message.source_message_id` 缺失时返回 `HTTP 400`,响应体不使用通用错误包装: + +```json +{ + "result_type": "infrastructure_input_error", + "error_code": "missing_source_message_id", + "retryable": true, + "missing_fields": [ + "source_message.source_message_id" + ] +} +``` ### 8.4 V2 JSON 兼容请求体 @@ -755,6 +771,7 @@ V3 `adapter_contract_error` 响应中的 `items[]` 不会包含 `order_id` / `ta | `SYSTEM_HOTEL_AMBIGUOUS` | 409 | 单酒店阶段平台酒店表存在多家 `ACTIVE` 酒店 | | `HOTEL_ACCESS_DENIED` | 403 | 显式传入的 `hotel_id` 与系统酒店或当前用户授权酒店不一致 | | `SOURCE_MESSAGE_NOT_FOUND` | 404 | 任务结果通知或会话锚点引用的外部来源消息尚未写入 SourceMessage Inbox | +| `missing_source_message_id` | 400 | V3 请求缺少 `source_message.source_message_id`,响应体为 typed `infrastructure_input_error` | ## 10. HMAC 上线配置 diff --git a/docs/project/requirements/M002-order-task-workflow-v3.md b/docs/project/requirements/M002-order-task-workflow-v3.md index ce655d9..d8d6988 100644 --- a/docs/project/requirements/M002-order-task-workflow-v3.md +++ b/docs/project/requirements/M002-order-task-workflow-v3.md @@ -38,7 +38,7 @@ V3 以以下资料和决策为输入: - M002 V3 正式采用 0711 P0 基线。 - 旧数据 `S000/S999` 继续在任务列表可见;新数据迁移为 `S10/S99`。 - `S10/S99` 继续复用隐藏技术订单 + 任务列表只读卡,不进入订单列表和订单执行队列。 -- 缺少 `source_message.source_message_id` 时,目标契约采用 `HTTP 400 + infrastructure_input_error + retryable=true` 的技术错误响应;当前 CP1-CP2 后端暂返回标准错误包装,typed 响应仍在后续 checkpoint。 +- 缺少 `source_message.source_message_id` 时,后端已按 `HTTP 400 + infrastructure_input_error + retryable=true` 的技术错误响应返回,不创建 SourceMessage、AI transition、订单、任务或通知卡。 - 内部任务模型采用“方案 C”:完整保存 AI 三元组,系统处理分类和前端展示分类单独维护。 - type-known manual review 使用同一张业务卡复核解阻,不生成第二张 normal task。 - 第一版保存全部 42 条路由 / 枚举,先支持保存和列表展示。 @@ -91,7 +91,7 @@ Gateway 必须在读取正文、附件、OCR、历史、系统上下文或调用 } ``` -当前 CP1-CP2 实现说明:后端已识别缺失并返回 `HTTP 400`,但响应体仍使用系统标准错误包装,尚未切换为上方 typed `infrastructure_input_error` 结构。 +当前 CP6 实现说明:后端已识别 V3 `source_message.source_message_id` 缺失,并返回上方 typed `infrastructure_input_error` 结构。 处理要求: @@ -266,7 +266,7 @@ Agent payload 不可变。本系统在同一张卡上维护复核状态: "field_overrides": [ { "field_pointer": "/extracted_fields/room_items/0/pms_room_type_code", - "field_path": "extracted_fields.room_items[].pms_room_type_code", + "field_path": "extracted_fields.pms_room_type_code", "value": "SU1" } ], @@ -288,6 +288,8 @@ Agent payload 不可变。本系统在同一张卡上维护复核状态: - 订单归属确认可作为复核解阻的一部分保存;当前第一版只允许确认当前任务所属订单,不开放普通任务随意切换订单能力。 - 全部缺失字段、订单归属、目录值和依赖校验通过后,才进入 Preflight / READY。 +当前 CP6 兼容说明:0711 P0 fixtures 中 `room_items[]` 已经是新结构,但后端第一版字段矩阵仍使用 `extracted_fields.room_type`、`extracted_fields.room_quantity`、`extracted_fields.pms_room_type_code` 等扁平字段。入站 payload 保留原始 `room_items[]`;后端在字段读取、展示、校验和确认 payload 组装时,从第一条 `room_items[]` 做扁平字段别名解析。解阻接口接受 `/extracted_fields/room_items/{index}/pms_room_type_code`,响应中的矩阵 `field_path` 仍返回当前后端字段 `extracted_fields.pms_room_type_code`。后续字段矩阵整体迁移时再取消这层兼容。 + ### 9.3 后端第一版接口 ```text @@ -364,7 +366,7 @@ V3 建议拆成以下 checkpoint,避免一次性重构过大: | M002-V3-CP3 | 路由持久化 | 已完成第一版:已保存 AI 原始三元组、route_code、system_process_category、unhandled_current_intents 和 adapter_contract_error | | M002-V3-CP4 | 列表 / 详情展示 | 已完成第一版:任务列表、订单任务时间线和任务详情透出 V3 路由字段;任务详情支持 S10/S99 入口通知结构、unhandled intent 展示块和 adapter contract error 展示块 | | M002-V3-CP5 | 同卡复核解阻 | 已完成第一版:支持 review_status、review_resolution.field_overrides[]、复核场景订单归属确认、JSON Pointer 校验和 READY 流转 | -| M002-V3-CP6 | P0 fixtures 回归 | 引入 0711 P0 fixtures / validator 作为后端适配测试参考,补充项目级测试 | +| M002-V3-CP6 | P0 fixtures 回归 | 已完成第一版:引入 0711 P0 fixtures / validator 作为后端适配测试参考,覆盖 main_outcomes、candidate_gate、manual_review_resolution、source_identity_errors、parent_split_two_children、row_multiple_derived、allotment_scope;其中 candidate_gate 是 Main Agent 调 Skill 前契约,后端以 validator 和 fixture reference 固化,不作为任务结果回调直接建任务 | ## 13. 明确不做 @@ -380,7 +382,7 @@ V3 P0 不做以下事项: ## 14. 当前代码现状提醒 -截至 M002 V3 CP5 落地后,当前后端已经实现: +截至 M002 V3 CP6 落地后,当前后端已经实现: - `S000/S999` 文本结果兼容处理。 - 结构化 `S10/S99` 入站处理,复用 `SOURCE_MESSAGE_ONLY` 只读特殊任务。 @@ -393,10 +395,11 @@ V3 P0 不做以下事项: - 业务任务详情按同一 AI 批次透出 `adapter_contract_errors[]` 与 `unhandled_intents[]` 只读展示块。 - type-known manual review 创建在原业务任务卡上,任务详情返回 `review_status`、`review_resolution` 和 `manual_review`。 - `POST /api/reservation/tasks/{taskId}/manual-review-resolutions` 支持字段修正、当前订单归属确认、JSON Pointer 到可编辑字段校验、READY 流转、confirmed payload 写入、两条 OPERA 模拟操作创建和审计记录。 +- `source_message.source_message_id` 缺失时按 V3 typed `infrastructure_input_error` 结构响应。 +- 0711 P0 fixtures 已纳入后端回归测试参考,并补齐 S10/S99 严格契约、P0 type-known manual review 解阻、candidate_gate reference 和当前扁平字段矩阵兼容。 - 订单 / 任务列表、任务详情、草稿保存、最终确认、OPERA 模拟骨架和审计列表。 - SuperAgent 查询上下文接口 1、2,以及邮件会话相关查询。 仍需后续 checkpoint 实现: -- `source_message.source_message_id` 缺失时按 V3 typed `infrastructure_input_error` 结构响应。 -- 真实 OPERA / OHIP、普通任务任意切换订单、P0 fixtures / validator 全量回归。 +- 真实 OPERA / OHIP、普通任务任意切换订单、字段矩阵从当前扁平结构整体迁移到 0711 P0 新结构。 diff --git a/docs/project/requirements/M002-superagent-task-result-api-contract.md b/docs/project/requirements/M002-superagent-task-result-api-contract.md index 54cfd7b..448c86c 100644 --- a/docs/project/requirements/M002-superagent-task-result-api-contract.md +++ b/docs/project/requirements/M002-superagent-task-result-api-contract.md @@ -10,7 +10,7 @@ | --- | --- | | 文档版本 | 0.5 | | 日期 | 2026-07-11 | -| 状态 | V2 兼容 + M002 V3 CP1-CP2 入站解析基线;完整 V3 展示和同卡复核仍看 `M002-order-task-workflow-v3.md` 后续 checkpoint | +| 状态 | V2 兼容 + M002 V3 CP1-CP6 入站解析、同卡复核和 P0 fixtures 回归基线;后续业务流程以 `M002-order-task-workflow-v3.md` 为准 | | 适用范围 | SuperAgent / Main Agent 调用本系统提交 AI 任务结果 | | 主要读者 | 后端、SuperAgent 对接方、测试、后续协作 agent | @@ -20,7 +20,7 @@ 本文承接 `M002-order-task-workflow-v2.md`,只定义本系统入站接口、鉴权、幂等、请求响应和技术校验边界,不定义 SuperAgent 内部 prompt、Skill 实现、OPERA 真实接口或前端展示细节。 -2026-07-11 后,M002 后续开发基线已迁移到 `M002-order-task-workflow-v3.md`。当前后端已完成 CP1-CP2:结构化 `S10/S99` 入站、V3 业务根基础解析、42 路由枚举 / 稳定配置、`UNHANDLED_CURRENT_INTENT` 和 route 相关字段最小落库。旧 `S000/S999` 和 `ai_task_results[]` 仍作为兼容路径保留。对外联调以 `docs/project/integrations/superagent-api-contract.md` 为准。完整 type-known manual review 解阻、`missing_fields[]` 到任务卡字段白名单映射和 typed `infrastructure_input_error` 响应仍在后续 checkpoint。 +2026-07-11 后,M002 后续开发基线已迁移到 `M002-order-task-workflow-v3.md`。当前后端已完成 CP1-CP6:结构化 `S10/S99` 入站、V3 业务根基础解析、42 路由枚举 / 稳定配置、`UNHANDLED_CURRENT_INTENT` 和 route 相关字段最小落库、type-known manual review 同卡解阻第一版、typed `infrastructure_input_error` 响应和 P0 fixtures 回归基线。旧 `S000/S999` 和 `ai_task_results[]` 仍作为兼容路径保留。对外联调以 `docs/project/integrations/superagent-api-contract.md` 为准。 ## 2. 接口概览 @@ -404,7 +404,7 @@ S000 / S999 文本结果创建成功时,同样返回 `201 Created`。这类结 } ``` -前端通过任务详情查看该类任务时,字段列表和 OPERA 操作列表为空,按钮应全部只读;`source_message_only_result` 会返回 `entry_result_code`、`entry_result_meaning`、`entry_result_description`、`entry_result_source_message_id` 和 `raw_answer`。 +前端通过任务详情查看该类任务时,字段列表和 OPERA 操作列表为空,按钮应全部只读;`source_message_only_result` 会返回 `entry_result_code`、`entry_result_meaning`、`entry_result_description`、`entry_result_source_message_id`、`result_type`、`route_code`、`agent_assessment`、`notification`、`manual_review` 和 `raw_answer`。 ### 8.2 幂等重放 diff --git a/server/src/main/java/cn/nianxx/thhotel/integrations/ai/superagent/common/result/SuperAgentTaskResultInfrastructureInputErrorResponse.java b/server/src/main/java/cn/nianxx/thhotel/integrations/ai/superagent/common/result/SuperAgentTaskResultInfrastructureInputErrorResponse.java new file mode 100644 index 0000000..7573680 --- /dev/null +++ b/server/src/main/java/cn/nianxx/thhotel/integrations/ai/superagent/common/result/SuperAgentTaskResultInfrastructureInputErrorResponse.java @@ -0,0 +1,19 @@ +package cn.nianxx.thhotel.integrations.ai.superagent.common.result; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +/** + * SuperAgent V3 基础设施输入错误响应。用于 source_message.source_message_id 缺失等调用层错误, + * 不使用通用错误包装,保持 0711 P0 契约的扁平结构。 + */ +public record SuperAgentTaskResultInfrastructureInputErrorResponse( + @JsonProperty("result_type") + String resultType, + @JsonProperty("error_code") + String errorCode, + Boolean retryable, + @JsonProperty("missing_fields") + List missingFields +) { +} diff --git a/server/src/main/java/cn/nianxx/thhotel/integrations/ai/superagent/control/SuperAgentTaskResultControllerAdvice.java b/server/src/main/java/cn/nianxx/thhotel/integrations/ai/superagent/control/SuperAgentTaskResultControllerAdvice.java index 8de7623..82bad71 100644 --- a/server/src/main/java/cn/nianxx/thhotel/integrations/ai/superagent/control/SuperAgentTaskResultControllerAdvice.java +++ b/server/src/main/java/cn/nianxx/thhotel/integrations/ai/superagent/control/SuperAgentTaskResultControllerAdvice.java @@ -1,6 +1,7 @@ package cn.nianxx.thhotel.integrations.ai.superagent.control; import cn.nianxx.thhotel.integrations.ai.superagent.common.result.SuperAgentTaskResultErrorResponse; +import cn.nianxx.thhotel.integrations.ai.superagent.common.result.SuperAgentTaskResultInfrastructureInputErrorResponse; import cn.nianxx.thhotel.integrations.ai.superagent.service.impl.SuperAgentTaskResultException; import cn.nianxx.thhotel.platform.hotel.service.HotelContextException; import cn.nianxx.thhotel.workflows.reservation.service.impl.ReservationAiTaskIntakeException; @@ -29,8 +30,16 @@ public class SuperAgentTaskResultControllerAdvice { * 处理 Reservation 接收阶段的 SourceMessage、幂等和 AI item 技术校验异常。 */ @ExceptionHandler(ReservationAiTaskIntakeException.class) - public ResponseEntity handleIntakeException( + public ResponseEntity handleIntakeException( ReservationAiTaskIntakeException exception) { + if ("MISSING_SOURCE_MESSAGE_ID".equals(exception.getErrorCode())) { + return ResponseEntity.status(exception.getStatus()) + .body(new SuperAgentTaskResultInfrastructureInputErrorResponse( + "infrastructure_input_error", + "missing_source_message_id", + true, + List.of("source_message.source_message_id"))); + } return ResponseEntity.status(exception.getStatus()) .body(error(exception.getErrorCode(), exception.getMessage())); } diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationAiTaskIntakeServiceImpl.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationAiTaskIntakeServiceImpl.java index 71e13e1..e3479d8 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationAiTaskIntakeServiceImpl.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationAiTaskIntakeServiceImpl.java @@ -108,6 +108,12 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta requestBody); } JsonNode root = parseJson(rawBody); + if (isV3SourceMessageIdentityMissing(root)) { + throw infrastructureInputError(); + } + if (isMissingSourceMessageInfrastructureInputError(root)) { + throw infrastructureInputError(); + } if (isInfrastructureInputError(root)) { throw error(HttpStatus.BAD_REQUEST, "INFRASTRUCTURE_INPUT_ERROR", "SuperAgent 返回基础设施输入错误。"); } @@ -334,18 +340,62 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta * 校验结构化 S10/S99 的最小形态,避免把联合值或空复核误收为正常通知。 */ private void validateV3SourceMessageNotificationShape(JsonNode root, ReservationAiRouteDefinition route) { + if (!"main_agent_outcome".equals(textAt(root, "handler_type")) + || !AiResultType.SOURCE_MESSAGE_REVIEW_NOTIFICATION.code().equals(textAt(root, "result_type")) + || !"current".equals(textAt(root, "current_or_history"))) { + throw error(HttpStatus.BAD_REQUEST, "ADAPTER_CONTRACT_ERROR", "S10/S99 根结构字段无效。"); + } + JsonNode assessment = root.path("agent_assessment"); + JsonNode notification = root.path("notification"); + if (!"none".equals(textAt(assessment, "automation_action")) + || !isBooleanTrue(notification.path("required")) + || !"source_message_review".equals(textAt(notification, "notification_type")) + || !isBooleanTrue(notification.path("show_source_message")) + || !isBooleanTrue(notification.path("requires_user_decision")) + || trimToNull(textAt(notification, "visible_message")) == null) { + throw error(HttpStatus.BAD_REQUEST, "ADAPTER_CONTRACT_ERROR", "S10/S99 通知结构无效。"); + } JsonNode manualReview = root.get("manual_review"); - if (route == ReservationAiRouteDefinition.SOURCE_MESSAGE_S10 - && manualReview != null - && !manualReview.isNull()) { - throw error(HttpStatus.BAD_REQUEST, "ADAPTER_CONTRACT_ERROR", "S10 manual_review 必须为空。"); + if (route == ReservationAiRouteDefinition.SOURCE_MESSAGE_S10) { + if (!"no_booking_action_detected".equals(textAt(assessment, "status")) + || !"no_booking_action_detected".equals(textAt(assessment, "reason_code")) + || (manualReview != null && !manualReview.isNull())) { + throw error(HttpStatus.BAD_REQUEST, "ADAPTER_CONTRACT_ERROR", "S10 结构不符合 P0 契约。"); + } + return; } - if (route == ReservationAiRouteDefinition.SOURCE_MESSAGE_S99 - && (manualReview == null || manualReview.isNull() || !manualReview.isObject())) { - throw error(HttpStatus.BAD_REQUEST, "ADAPTER_CONTRACT_ERROR", "S99 manual_review 必须为入口复核对象。"); + if (!"material_package_unavailable".equals(textAt(assessment, "status")) + || !"material_package_unavailable".equals(textAt(assessment, "reason_code")) + || manualReview == null + || manualReview.isNull() + || !manualReview.isObject() + || inspectV3MainAgentReviewIssue(manualReview) != null + || !"material_package_unavailable".equals(textAt(manualReview, "reason_code"))) { + throw error(HttpStatus.BAD_REQUEST, "ADAPTER_CONTRACT_ERROR", "S99 结构不符合 P0 契约。"); } } + /** + * 校验 S99 main_agent_entry_review 的 P0 九字段结构。 + */ + private V3EventContractIssue inspectV3MainAgentReviewIssue(JsonNode manualReview) { + if (!manualReview.isObject()) { + return new V3EventContractIssue("MAIN_AGENT_REVIEW_INVALID", "manual_review 必须是对象。"); + } + if (!"main_agent_entry_review".equals(textAt(manualReview, "review_record_type")) + || trimToNull(textAt(manualReview, "reason_code")) == null + || trimToNull(textAt(manualReview, "visible_reason")) == null + || !manualReview.path("known_fields").isObject() + || !manualReview.path("missing_fields").isArray() + || !manualReview.path("blocking_points").isArray() + || !manualReview.path("conflicting_points").isArray() + || !manualReview.path("suggested_human_actions").isArray() + || !manualReview.path("evidence_to_check").isArray()) { + return new V3EventContractIssue("MAIN_AGENT_REVIEW_INCOMPLETE", "manual_review 九字段不完整。"); + } + return null; + } + /** * 将 V3 业务 event 转为旧任务结果 item 形态,复用现有订单、任务和任务卡创建逻辑。 */ @@ -1320,6 +1370,13 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta return node.get(fieldName).asBoolean(false); } + /** + * 判断 JSON 节点是否为布尔 true。S10/S99 P0 契约不接受字符串 true 这类宽松输入。 + */ + private boolean isBooleanTrue(JsonNode node) { + return node != null && node.isBoolean() && node.booleanValue(); + } + /** * 将 JSON 节点序列化为字符串,缺失时返回 null。 */ @@ -1400,6 +1457,42 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta return "infrastructure_input_error".equals(textAt(root, "result_type")); } + /** + * 判断是否为 P0 明确约定的缺失 source_message_id 基础设施错误;其他 infra 错误不得被改写成该错误。 + */ + private boolean isMissingSourceMessageInfrastructureInputError(JsonNode root) { + return isInfrastructureInputError(root) + && "missing_source_message_id".equals(textAt(root, "error_code")); + } + + /** + * V3 或入口网关形态输入必须先有非空 source_message_id;纯 V2 兼容结构不受该预检查影响。 + */ + private boolean isV3SourceMessageIdentityMissing(JsonNode root) { + if (root == null || !root.has("source_message") || !isV3OrGatewayInputShape(root)) { + return false; + } + return trimToNull(textAt(root.path("source_message"), "source_message_id")) == null; + } + + /** + * 构建 0711 P0 基础设施输入错误,ControllerAdvice 会转换为扁平 typed 响应。 + */ + private ReservationAiTaskIntakeException infrastructureInputError() { + return error(HttpStatus.BAD_REQUEST, "MISSING_SOURCE_MESSAGE_ID", "source_message.source_message_id 缺失。"); + } + + /** + * 判断输入是否属于 V3 回调或 0711 P0 入口网关形态,避免误伤旧 V2 兼容 JSON。 + */ + private boolean isV3OrGatewayInputShape(JsonNode root) { + return root.path("message_events").isArray() + || root.path("unhandled_current_intents").isArray() + || root.path("candidate_events").isArray() + || root.has("body_current") + || isV3SourceMessageNotification(root); + } + /** * 判断是否为 V3 结构化 S10/S99 来源邮件通知根。 */ @@ -1426,7 +1519,7 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta JsonNode sourceMessageNode = root.path("source_message"); String externalSourceMessageId = trimToNull(textAt(sourceMessageNode, "source_message_id")); if (externalSourceMessageId == null) { - throw error(HttpStatus.BAD_REQUEST, "INFRASTRUCTURE_INPUT_ERROR", "source_message.source_message_id 缺失。"); + throw infrastructureInputError(); } validateLength(externalSourceMessageId, "source_message.source_message_id", LENGTH_256); String hotelId = requireText(defaultHotelId, "default_hotel_id", LENGTH_64); diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationTaskWorkflowServiceImpl.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationTaskWorkflowServiceImpl.java index f04ae46..60c58b2 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationTaskWorkflowServiceImpl.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationTaskWorkflowServiceImpl.java @@ -898,9 +898,23 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl if (!collapsed.equals(candidates.get(0))) { candidates.add(collapsed); } + String legacyAlias = p0ReviewPointerLegacyAlias(collapsed); + if (legacyAlias != null && !candidates.contains(legacyAlias)) { + candidates.add(legacyAlias); + } return candidates; } + /** + * 兼容 0711 P0 fixtures 中已迁移为数组结构、但当前后端矩阵仍是扁平字段的复核 pointer。 + */ + private String p0ReviewPointerLegacyAlias(String collapsedFieldPath) { + if ("extracted_fields.room_items[].pms_room_type_code".equals(collapsedFieldPath)) { + return "extracted_fields.pms_room_type_code"; + } + return null; + } + /** * 解析 JSON Pointer token,拒绝非法的 ~ 转义。 */ @@ -1717,7 +1731,33 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl if (editedValues.containsKey(fieldPath)) { return editedValues.get(fieldPath); } - return valueAt(aiPayload, fieldPath); + Object value = valueAt(aiPayload, fieldPath); + if (value != null) { + return value; + } + for (String legacyPath : p0LegacyValuePathCandidates(fieldPath)) { + Object legacyValue = valueAt(aiPayload, legacyPath); + if (legacyValue != null) { + return legacyValue; + } + } + return null; + } + + /** + * 0711 P0 已将房型字段迁移到 room_items[];当前矩阵仍使用扁平字段,读取时做最小别名兼容。 + */ + private List p0LegacyValuePathCandidates(String fieldPath) { + String safeFieldPath = fieldPath == null ? "" : fieldPath; + return switch (safeFieldPath) { + case "extracted_fields.room_type" -> List.of( + "extracted_fields.room_items.0.room_type_normalized", + "extracted_fields.room_items.0.room_type_raw"); + case "extracted_fields.room_quantity" -> List.of("extracted_fields.room_items.0.room_quantity"); + case "extracted_fields.pms_room_type_code" -> List.of( + "extracted_fields.room_items.0.pms_room_type_code"); + default -> List.of(); + }; } /** @@ -1880,7 +1920,7 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl definition.operaWriteParticipation(), definition.operaParameterMapping(), definition.notes(), - valueAt(aiPayload, definition.fieldPath())); + valueForField(aiPayload, Map.of(), definition.fieldPath())); } /** @@ -1904,7 +1944,9 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl JsonNode current = root; for (String rawPart : fieldPath.split("\\.")) { String part = rawPart.replace("[]", ""); - current = current.path(part); + current = current.isArray() && isNumericToken(part) + ? current.path(Integer.parseInt(part)) + : current.path(part); if (current.isMissingNode() || current.isNull()) { return null; } diff --git a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultControllerTest.java b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultControllerTest.java index 2987e11..57c4fec 100644 --- a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultControllerTest.java +++ b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultControllerTest.java @@ -638,14 +638,15 @@ class SuperAgentTaskResultControllerTest { "reason_code": "no_booking_action_detected", "automation_action": "none" }, - "notification": { - "required": true, - "notification_type": "source_message_review", - "show_source_message": true, - "requires_user_decision": true - }, - "manual_review": null - } + "notification": { + "required": true, + "notification_type": "source_message_review", + "show_source_message": true, + "requires_user_decision": true, + "visible_message": "未匹配到当前 Agent 支持的业务事件类型,请查看原邮件并决定是否需要回复或进行其他处理。" + }, + "manual_review": null + } """; MvcResult result = mockMvc.perform(signedPost(body, "nonce-v3-s10-entry-result-001")) @@ -695,23 +696,31 @@ class SuperAgentTaskResultControllerTest { "route_code": "S99", "handler_type": "main_agent_outcome", "result_type": "source_message_review_notification", - "current_or_history": "current", - "agent_assessment": { - "status": "insufficient_business_material", - "reason_code": "cannot_form_business_material_package", - "automation_action": "none" - }, - "notification": { - "required": true, - "notification_type": "source_message_review", - "show_source_message": true, - "requires_user_decision": true - }, - "manual_review": { - "reason_code": "cannot_form_business_material_package", - "review_notes": "需要人工查看原邮件。" - } - } + "current_or_history": "current", + "agent_assessment": { + "status": "material_package_unavailable", + "reason_code": "material_package_unavailable", + "automation_action": "none" + }, + "notification": { + "required": true, + "notification_type": "source_message_review", + "show_source_message": true, + "requires_user_decision": true, + "visible_message": "当前输入不足,无法判断是否匹配当前 Agent 支持的业务事件类型,请查看原邮件并决定后续处理。" + }, + "manual_review": { + "reason_code": "material_package_unavailable", + "visible_reason": "当前输入不足,无法完成支持业务事件范围分类。", + "review_record_type": "main_agent_entry_review", + "missing_fields": [], + "blocking_points": [], + "conflicting_points": [], + "suggested_human_actions": ["review_source_message"], + "evidence_to_check": ["source_message"], + "known_fields": {} + } + } """; MvcResult result = mockMvc.perform(signedPost(body, "nonce-v3-s99-entry-result-001")) @@ -761,23 +770,31 @@ class SuperAgentTaskResultControllerTest { "route_code": "S99", "handler_type": "main_agent_outcome", "result_type": "source_message_review_notification", - "current_or_history": "current", - "agent_assessment": { - "status": "insufficient_business_material", - "reason_code": "cannot_form_business_material_package", - "automation_action": "none" - }, - "notification": { - "required": true, - "notification_type": "source_message_review", - "show_source_message": true, - "requires_user_decision": true - }, - "manual_review": { - "reason_code": "cannot_form_business_material_package", - "review_notes": "需要人工查看原邮件。" - } - } + "current_or_history": "current", + "agent_assessment": { + "status": "material_package_unavailable", + "reason_code": "material_package_unavailable", + "automation_action": "none" + }, + "notification": { + "required": true, + "notification_type": "source_message_review", + "show_source_message": true, + "requires_user_decision": true, + "visible_message": "当前输入不足,无法判断是否匹配当前 Agent 支持的业务事件类型,请查看原邮件并决定后续处理。" + }, + "manual_review": { + "reason_code": "material_package_unavailable", + "visible_reason": "当前输入不足,无法完成支持业务事件范围分类。", + "review_record_type": "main_agent_entry_review", + "missing_fields": [], + "blocking_points": [], + "conflicting_points": [], + "suggested_human_actions": ["review_source_message"], + "evidence_to_check": ["source_message"], + "known_fields": {} + } + } """; MvcResult result = mockMvc.perform(signedPost(body, "nonce-v3-s99-frontend-visible-001")) @@ -810,11 +827,11 @@ class SuperAgentTaskResultControllerTest { .value("source_message_review_notification")) .andExpect(jsonPath("$.source_message_only_result.route_code").value("S99")) .andExpect(jsonPath("$.source_message_only_result.agent_assessment.status") - .value("insufficient_business_material")) + .value("material_package_unavailable")) .andExpect(jsonPath("$.source_message_only_result.notification.notification_type") .value("source_message_review")) .andExpect(jsonPath("$.source_message_only_result.manual_review.reason_code") - .value("cannot_form_business_material_package")); + .value("material_package_unavailable")); } @Test diff --git a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultP0FixtureRegressionTest.java b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultP0FixtureRegressionTest.java new file mode 100644 index 0000000..082c93a --- /dev/null +++ b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultP0FixtureRegressionTest.java @@ -0,0 +1,459 @@ +package cn.nianxx.thhotel.workflows.reservation.control; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import cn.nianxx.thhotel.ThHotelApplication; +import cn.nianxx.thhotel.platform.message.common.request.CaptureSourceMessageCommand; +import cn.nianxx.thhotel.platform.message.common.result.SourceMessageCaptureResult; +import cn.nianxx.thhotel.platform.message.service.SourceMessageCaptureService; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.time.Instant; +import java.util.HexFormat; +import java.util.List; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +@SpringBootTest( + classes = ThHotelApplication.class, + properties = { + "superagent.task-result.hmac-secret=test-superagent-secret", + "superagent.task-result.clock-skew-seconds=300", + "superagent.task-result.nonce-ttl-seconds=600", + "superagent.task-result.max-body-bytes=20000" + }) +@AutoConfigureMockMvc +@ActiveProfiles("test") +class SuperAgentTaskResultP0FixtureRegressionTest { + + private static final String ENDPOINT = "/api/integrations/superagent/task-results"; + private static final String CLIENT_ID = "superagent-test-client"; + private static final String SECRET = "test-superagent-secret"; + private static final Path FIXTURES_DIR = Path.of( + "..", + "docs", + "import", + "20260711", + "开发交付_P0冻结基线_2026-07-11", + "03_P0_Acceptance", + "fixtures"); + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private SourceMessageCaptureService captureService; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Test + void shouldAcceptLegalS10AndS99MainOutcomeFixtures() throws Exception { + JsonNode cases = fixture("main_outcomes.json").path("cases"); + JsonNode s10 = caseValue(cases, "legal_s10_no_supported_match"); + JsonNode s99 = caseValue(cases, "legal_s99_material_unavailable"); + String s10ExternalId = "p0-main-outcome-s10-001"; + String s99ExternalId = "p0-main-outcome-s99-001"; + captureSourceMessage(s10ExternalId); + captureSourceMessage(s99ExternalId); + + MvcResult s10Result = mockMvc.perform(signedPost(withSourceMessageId(s10, s10ExternalId), "nonce-p0-s10-001")) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.items[0].route_code").value("S10")) + .andExpect(jsonPath("$.items[0].system_process_category").value("SOURCE_MESSAGE_NOTIFICATION")) + .andExpect(jsonPath("$.items[0].system_task_type").value("SOURCE_MESSAGE_ONLY")) + .andReturn(); + MvcResult s99Result = mockMvc.perform(signedPost(withSourceMessageId(s99, s99ExternalId), "nonce-p0-s99-001")) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.items[0].route_code").value("S99")) + .andExpect(jsonPath("$.items[0].system_process_category").value("SOURCE_MESSAGE_NOTIFICATION")) + .andExpect(jsonPath("$.items[0].system_task_type").value("SOURCE_MESSAGE_ONLY")) + .andReturn(); + + assertSourceMessageOnlyTaskCount(s10Result, "S10"); + assertSourceMessageOnlyTaskCount(s99Result, "S99"); + } + + @Test + void shouldRejectIllegalMainOutcomeFixturesWithoutCreatingTasks() throws Exception { + JsonNode cases = fixture("main_outcomes.json").path("cases"); + for (String caseId : List.of( + "illegal_s10_with_review_object", + "illegal_s99_with_null_review", + "illegal_union_template_value")) { + JsonNode value = caseValue(cases, caseId); + String externalId = "p0-" + caseId.replace('_', '-'); + SourceMessageCaptureResult source = captureSourceMessage(externalId); + + mockMvc.perform(signedPost(withSourceMessageId(value, externalId), "nonce-" + externalId)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error_code").value("ADAPTER_CONTRACT_ERROR")) + .andExpect(content().string(notContainsSecret())); + + Long taskCount = jdbcTemplate.queryForObject(""" + SELECT COUNT(*) + FROM workflow_reservation_task + WHERE source_message_id = ? + """, Long.class, source.inboxId()); + assertThat(taskCount).isZero(); + } + } + + @Test + void shouldReturnTypedInfrastructureInputErrorForMissingSourceIdentityFixtures() throws Exception { + JsonNode cases = fixture("source_identity_errors.json").path("cases"); + for (JsonNode fixtureCase : cases) { + if (fixtureCase.path("expected_error").isNull()) { + continue; + } + String caseId = fixtureCase.path("case_id").asText(); + String body = objectMapper.writeValueAsString(fixtureCase.path("input")); + + mockMvc.perform(signedPost(body, "nonce-" + caseId)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.result_type").value("infrastructure_input_error")) + .andExpect(jsonPath("$.error_code").value("missing_source_message_id")) + .andExpect(jsonPath("$.retryable").value(true)) + .andExpect(jsonPath("$.missing_fields[0]").value("source_message.source_message_id")) + .andExpect(jsonPath("$.route_code").doesNotExist()) + .andExpect(jsonPath("$.message_events").doesNotExist()); + } + } + + @Test + void shouldNotRewriteOtherInfrastructureInputErrorsToMissingSourceMessageId() throws Exception { + mockMvc.perform(signedPost(""" + { + "result_type": "infrastructure_input_error", + "error_code": "object_storage_unavailable", + "retryable": true, + "missing_fields": [] + } + """, "nonce-p0-infra-other-001")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error_code").value("INFRASTRUCTURE_INPUT_ERROR")) + .andExpect(jsonPath("$.result_type").doesNotExist()); + } + + @Test + void shouldLetNonBlankSourceIdentityPassP0GuardWithoutTypedMissingError() throws Exception { + JsonNode validInput = caseItem( + fixture("source_identity_errors.json").path("cases"), + "nonblank_source_message_id_passes_guard").path("input"); + + mockMvc.perform(signedPost(objectMapper.writeValueAsString(validInput), "nonce-p0-source-valid-guard-001")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error_code").value("SOURCE_MESSAGE_REQUIRED")) + .andExpect(jsonPath("$.result_type").doesNotExist()); + } + + @Test + void shouldKeepCandidateGateFixtureAsP0RegressionReference() throws Exception { + JsonNode cases = fixture("candidate_gate.json").path("cases"); + + assertThat(caseIdsByValidity(cases, true)).containsExactly( + "valid_coarse_attachment_signal", + "valid_single_supported_direction", + "valid_main_no_active_signal_routes_s10", + "valid_skill_no_supported_event_routes_s10"); + assertThat(caseIdsByValidity(cases, false)).containsExactly( + "invalid_candidate_missing_target_hints", + "invalid_candidate_wrong_status", + "invalid_candidate_empty_possible_types", + "invalid_review_outcome_used_as_candidate", + "invalid_evidence_refs_string_array", + "invalid_target_hints_object"); + + JsonNode supportedSignal = caseItem(cases, "valid_coarse_attachment_signal") + .path("candidate_events").get(0); + assertThat(supportedSignal.path("classification_status").asText()).isEqualTo("coarse_supported_signal"); + assertThat(supportedSignal.path("possible_event_types")).hasSize(2); + assertThat(supportedSignal.path("evidence_refs").get(0).isObject()).isTrue(); + assertThat(caseItem(cases, "invalid_review_outcome_used_as_candidate") + .path("candidate_events").get(0).path("possible_event_types").get(0).asText()) + .isEqualTo("Need Manual Review"); + } + + @Test + void shouldCreateParentSplitTasksFromP0FixtureInEventOrder() throws Exception { + ObjectNode root = fixture("parent_split_two_children.json").deepCopy(); + String externalId = "p0-parent-split-001"; + SourceMessageCaptureResult source = captureSourceMessage(externalId); + + MvcResult result = mockMvc.perform(signedPost(withSourceMessageId(root, externalId), "nonce-p0-parent-split-001")) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.accepted_count").value(3)) + .andExpect(jsonPath("$.items[0].route_code").value("R02_NEW_GROUP_BLOCK_NORMAL")) + .andExpect(jsonPath("$.items[0].execution_order").value(1)) + .andExpect(jsonPath("$.items[1].route_code").value("R02_NEW_GROUP_BLOCK_NORMAL")) + .andExpect(jsonPath("$.items[1].execution_order").value(1)) + .andExpect(jsonPath("$.items[2].route_code").value("R07_LINKED_PARENT_RELEASE_AFTER_CHILD_SPLIT_NORMAL")) + .andReturn(); + + String parentOrderId = com.jayway.jsonpath.JsonPath.read( + result.getResponse().getContentAsString(), + "$.items[2].order_id"); + Integer parentExecutionOrder = com.jayway.jsonpath.JsonPath.read( + result.getResponse().getContentAsString(), + "$.items[2].execution_order"); + assertThat(parentOrderId).isNotBlank(); + assertThat(parentExecutionOrder).isEqualTo(1); + + Long transitionCount = jdbcTemplate.queryForObject(""" + SELECT COUNT(*) + FROM workflow_reservation_ai_transition + WHERE source_message_id = ? + AND route_code IN ( + 'R02_NEW_GROUP_BLOCK_NORMAL', + 'R07_LINKED_PARENT_RELEASE_AFTER_CHILD_SPLIT_NORMAL' + ) + """, Long.class, source.inboxId()); + assertThat(transitionCount).isEqualTo(3L); + } + + @Test + void shouldCreateRowDerivedMainParentAndTraceTasksFromP0Fixture() throws Exception { + ObjectNode root = fixture("row_multiple_derived.json").deepCopy(); + String externalId = "p0-row-derived-001"; + SourceMessageCaptureResult source = captureSourceMessage(externalId); + + mockMvc.perform(signedPost(withSourceMessageId(root, externalId), "nonce-p0-row-derived-001")) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.accepted_count").value(3)) + .andExpect(jsonPath("$.items[0].route_code").value("R02_NEW_GROUP_BLOCK_NORMAL")) + .andExpect(jsonPath("$.items[1].route_code").value("R07_LINKED_PARENT_RELEASE_AFTER_CHILD_SPLIT_NORMAL")) + .andExpect(jsonPath("$.items[2].route_code").value("R13_EXTRA_BED_NORMAL")); + + Long taskCount = jdbcTemplate.queryForObject(""" + SELECT COUNT(*) + FROM workflow_reservation_task + WHERE source_message_id = ? + """, Long.class, source.inboxId()); + assertThat(taskCount).isEqualTo(3L); + } + + @Test + void shouldPreserveMixedAllotmentUnhandledIntentAndRejectPartialAllotmentAsBusinessTask() throws Exception { + JsonNode fixture = fixture("allotment_scope.json"); + ObjectNode mixedOutput = fixture.path("mixed_partial_allotment").path("output").deepCopy(); + String externalId = "p0-allotment-mixed-001"; + SourceMessageCaptureResult source = captureSourceMessage(externalId); + + mockMvc.perform(signedPost(withSourceMessageId(mixedOutput, externalId), "nonce-p0-allotment-mixed-001")) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.accepted_count").value(2)) + .andExpect(jsonPath("$.items[0].route_code").value("R02_NEW_GROUP_BLOCK_NORMAL")) + .andExpect(jsonPath("$.items[1].route_code").value("R42_UNHANDLED_CURRENT_INTENT")) + .andExpect(jsonPath("$.items[1].task_id").doesNotExist()); + + Long unsupportedTaskCount = jdbcTemplate.queryForObject(""" + SELECT COUNT(*) + FROM workflow_reservation_ai_transition + WHERE source_message_id = ? + AND ai_task_type IN ( + 'Allotment Maintenance', + 'Update Booking / Amendment', + 'Cancel Allotment' + ) + """, Long.class, source.inboxId()); + assertThat(unsupportedTaskCount).isZero(); + } + + @Test + void shouldCreateTypeKnownManualReviewAndResolveSameCardFromP0Fixture() throws Exception { + JsonNode manualReview = fixture("manual_review_resolution.json"); + ObjectNode event = manualReview.path("known_subtype_manual_review").path("event").deepCopy(); + String externalId = "p0-manual-review-same-card-001"; + SourceMessageCaptureResult source = captureSourceMessage(externalId); + String body = businessRoot(externalId, event); + + MvcResult result = mockMvc.perform(signedPost(body, "nonce-p0-manual-review-create-001")) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.items[0].route_code").value("R02_NEW_GROUP_BLOCK_REVIEW")) + .andExpect(jsonPath("$.items[0].system_task_type").value("NEW_BOOKING")) + .andReturn(); + String taskId = com.jayway.jsonpath.JsonPath.read( + result.getResponse().getContentAsString(), + "$.items[0].task_id"); + String orderId = com.jayway.jsonpath.JsonPath.read( + result.getResponse().getContentAsString(), + "$.items[0].order_id"); + + mockMvc.perform(post("/api/reservation/tasks/{taskId}/manual-review-resolutions", taskId) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "confirmed_order_id": "%s", + "field_overrides": [ + { + "field_pointer": "/extracted_fields/room_items/0/pms_room_type_code", + "value": "SU1" + } + ] + } + """.formatted(orderId))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.task_status").value("READY")) + .andExpect(jsonPath("$.review_status").value("RESOLVED")) + .andExpect(jsonPath("$.review_resolution.field_overrides[0].field_pointer") + .value("/extracted_fields/room_items/0/pms_room_type_code")) + .andExpect(jsonPath("$.review_resolution.field_overrides[0].field_path") + .value("extracted_fields.pms_room_type_code")) + .andExpect(jsonPath("$.confirmed_payload.field_values['extracted_fields.room_type']") + .value("SUITE")) + .andExpect(jsonPath("$.confirmed_payload.field_values['extracted_fields.room_quantity']") + .value(2)) + .andExpect(jsonPath("$.confirmed_payload.field_values['extracted_fields.pms_room_type_code']") + .value("SU1")); + + Long sourceTaskCount = jdbcTemplate.queryForObject(""" + SELECT COUNT(*) + FROM workflow_reservation_task + WHERE source_message_id = ? + """, Long.class, source.inboxId()); + String aiPayloadJson = jdbcTemplate.queryForObject(""" + SELECT ai_payload_json + FROM workflow_reservation_task_card + WHERE task_id = ? + """, String.class, Long.valueOf(taskId)); + JsonNode aiPayload = objectMapper.readTree(aiPayloadJson); + assertThat(sourceTaskCount).isEqualTo(1L); + assertThat(aiPayload.path("extracted_fields").path("room_items").get(0) + .path("pms_room_type_code").isNull()).isTrue(); + assertThat(aiPayload.path("extracted_fields").has("room_type")).isFalse(); + assertThat(aiPayloadJson).doesNotContain("\"pms_room_type_code\":\"SU1\""); + } + + private JsonNode fixture(String fileName) throws Exception { + return objectMapper.readTree(FIXTURES_DIR.resolve(fileName).toFile()); + } + + private JsonNode caseValue(JsonNode cases, String caseId) { + return caseItem(cases, caseId).path("value"); + } + + private JsonNode caseItem(JsonNode cases, String caseId) { + for (JsonNode item : cases) { + if (caseId.equals(item.path("case_id").asText())) { + return item; + } + } + throw new IllegalArgumentException("fixture case not found: " + caseId); + } + + private List caseIdsByValidity(JsonNode cases, boolean expectValid) { + List ids = new java.util.ArrayList<>(); + for (JsonNode item : cases) { + if (item.path("expect_valid").asBoolean(false) == expectValid) { + ids.add(item.path("case_id").asText()); + } + } + return ids; + } + + private String withSourceMessageId(JsonNode root, String externalId) throws Exception { + ObjectNode copy = root.deepCopy(); + ((ObjectNode) copy.path("source_message")).put("source_message_id", externalId); + return objectMapper.writeValueAsString(copy); + } + + private String businessRoot(String externalId, JsonNode event) throws Exception { + ObjectNode root = objectMapper.createObjectNode(); + ObjectNode sourceMessage = root.putObject("source_message"); + sourceMessage.put("source_message_id", externalId); + sourceMessage.put("subject", "P0 fixture"); + sourceMessage.put("from", "agent@example.test"); + sourceMessage.putArray("cc"); + sourceMessage.put("received_at", "2026-07-11T10:00:00+08:00"); + sourceMessage.put("source_channel", "Email"); + ArrayNode events = root.putArray("message_events"); + events.add(event); + root.putArray("case_candidates"); + root.putArray("extraction_warnings"); + root.putArray("unhandled_current_intents"); + return objectMapper.writeValueAsString(root); + } + + private SourceMessageCaptureResult captureSourceMessage(String externalMessageId) { + return captureService.capture(new CaptureSourceMessageCommand( + "HOTEL-TEST", + "AGENTBUS", + "EMAIL", + externalMessageId, + "thread-" + externalMessageId, + "frame-" + externalMessageId, + "session-m002-p0", + Instant.parse("2026-07-11T02:00:00Z"), + "agent@example.test", + "M002 P0 fixture", + "P0 fixture source message.", + "P0 fixture source message.", + "{\"source\":{\"external_message_id\":\"" + externalMessageId + "\"}}", + "agentbus-outlook-v1", + List.of() + )); + } + + private void assertSourceMessageOnlyTaskCount(MvcResult result, String routeCode) throws Exception { + String taskId = com.jayway.jsonpath.JsonPath.read( + result.getResponse().getContentAsString(), + "$.items[0].task_id"); + Long taskCount = jdbcTemplate.queryForObject(""" + SELECT COUNT(*) + FROM workflow_reservation_task task + JOIN workflow_reservation_ai_transition transition + ON transition.id = task.ai_transition_id + WHERE task.id = ? + AND transition.route_code = ? + AND task.queue_participation = 0 + """, Long.class, Long.valueOf(taskId), routeCode); + assertThat(taskCount).isEqualTo(1L); + } + + private MockHttpServletRequestBuilder signedPost(String body, String nonce) throws Exception { + String timestamp = Instant.now().toString(); + return post(ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(body) + .header("X-TH-Hotel-SuperAgent-Client-Id", CLIENT_ID) + .header("X-TH-Hotel-SuperAgent-Timestamp", timestamp) + .header("X-TH-Hotel-SuperAgent-Nonce", nonce) + .header("X-TH-Hotel-SuperAgent-Signature", signature(body, nonce, timestamp)); + } + + private String signature(String body, String nonce, String timestamp) throws Exception { + String bodyHash = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256") + .digest(body.getBytes(StandardCharsets.UTF_8))); + String canonical = "POST\n" + ENDPOINT + "\n" + timestamp + "\n" + nonce + "\n" + CLIENT_ID + "\n" + bodyHash; + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); + return "sha256=" + HexFormat.of().formatHex(mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8))); + } + + private org.hamcrest.Matcher notContainsSecret() { + return org.hamcrest.Matchers.not(containsString(SECRET)); + } +}