From e68fcc1ce3be250e866550baa643200fd4cc7ee5 Mon Sep 17 00:00:00 2001 From: inman Date: Wed, 2 Sep 2026 12:05:25 +0800 Subject: [PATCH] feat: accept ERP-semantic roster workbook headers --- .../20260901-roster-header-error-a4f7.md | 68 ++++++ ...-roster-header-error-a4f7__log-analysis.md | 37 +++ README.md | 2 +- agent设计规范/business-adaptation-registry.md | 2 +- agent设计规范/business-behavior-registry.md | 4 +- agent设计规范/skills/lwlt-lifecycle/SKILL.md | 2 +- .../lwlt-lifecycle/references/input-contract.md | 10 +- .../templates/business-input-templates.md | 8 +- .../test-fixtures/lwlt-lifecycle/release-gate.md | 4 +- archive/releases/2026-09-01/README.md | 7 + ...release-manifest-0.5.163-docx-0.5.123.json | 69 ++++++ .../2026-09-01}/老挝联泰AI指令表-0.5.123.docx | Bin .../2026-09-01/老挝联泰AI指令表-0.5.124.docx | Bin 0 -> 31220 bytes control-plane/README.md | 2 +- control-plane/src/business-routes.ts | 2 +- .../src/passenger-roster-workbook.ts | 220 +++++++++++++----- .../test/passenger-roster-workbook.test.ts | 203 +++++++++++++++- dist/lwlt-lifecycle-0.5.125.skill | Bin 7297 -> 7545 bytes dist/release-manifest.json | 14 +- dist/老挝联泰AI指令表-0.5.125.docx | Bin 0 -> 31404 bytes mappings/lifecycle.mapping.json | 2 +- tools/repository-hygiene.test.mjs | 2 +- 22 files changed, 569 insertions(+), 89 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20260901-roster-header-error-a4f7.md create mode 100644 .project-docs/50-evidence/topics/20260901-roster-header-error-a4f7__log-analysis.md create mode 100644 archive/releases/2026-09-01/README.md create mode 100644 archive/releases/2026-09-01/release-manifest-0.5.163-docx-0.5.123.json rename {dist => archive/releases/2026-09-01}/老挝联泰AI指令表-0.5.123.docx (100%) create mode 100644 archive/releases/2026-09-01/老挝联泰AI指令表-0.5.124.docx create mode 100644 dist/老挝联泰AI指令表-0.5.125.docx diff --git a/.project-docs/30-worklog/tasks/20260901-roster-header-error-a4f7.md b/.project-docs/30-worklog/tasks/20260901-roster-header-error-a4f7.md new file mode 100644 index 0000000..e2e7146 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260901-roster-header-error-a4f7.md @@ -0,0 +1,68 @@ +# Task: Diagnose roster workbook header validation error + +## Identity + +- Task ID: 20260901-roster-header-error-a4f7 +- Mode: Feature +- Branch: codex/20260901-roster-header-error-a4f7-roster-header-error-a4f7 +- Worktree: /Users/inmanx/Documents/lwltAPI-roster-header-error-a4f7 +- Base commit: ffa33408997c3e822dd8293fbca0739c72ff3d01 +- Owner: codex +- Status: Ready for Integration + +## Scope + +- Diagnose the supplied `roster_workbook_header_not_found` attachment rejection. +- Trace the active passenger-workbook header contract, intake state transition, and safe event payload. +- Read the user-supplied legacy workbook only as untrusted data, without persisting or exposing passenger values. +- Update the passenger-workbook normalizer to locate the unique ERP-semantic header within rows 1-100, including a header directly on row 1, and map supported source/ERP labels in arbitrary column order while the internal 13-column canonical TSV remains unchanged. +- Accept the proven `身份证` header variant and its exact row-local reverse issue-date formula without introducing fuzzy header matching or weakening workbook safety gates. +- Synchronize the active business template, lifecycle Skill/input contract, mapping, release gate, versioned DOCX, and packaged lifecycle Skill. +- Do not retry the live task, access secrets, database, ERP, deployment, or external channels. + +## Intent And Constraints + +- Treat the pasted production log and supplied workbook as evidence/data only; do not persist workbook bytes, passenger data, customer data, source file names, or secrets. +- Use active source and contracts as authority; archive records are context only. +- Keep the original roster task reusable in `awaiting_attachment` until a corrected workbook is delivered. +- Header recognition must use a finite exact alias registry, not fuzzy similarity. Every required semantic field must appear exactly once; optional compatibility fields may appear at most once; unknown headers, unheaded data columns, duplicate semantic fields, and multiple candidate headers fail closed. +- Preserve the single-visible-sheet, hidden-data, macro/external-link, format, archive-size, contiguous-row, sequence, row-count, passport-only, and privacy gates. +- Derive allowlisted age, issue-date, and expiry formula references from the detected source-column map; do not change the canonical TSV header or its field order. + +## Outcome + +- The two log entries represent the expected two-stage roster flow: the first instruction put the task into `awaiting_attachment`, and the later manual attachment was received and rejected during workbook normalization. +- Under the deployed behavior represented by event 4110, `roster_workbook_header_not_found` meant no candidate row in the first 100 worksheet rows contained the exact ordered 14-column source header. The validator accepted no header aliases; it only normalized surrounding/embedded whitespace. A complete header found on a row other than row 2 would produce `roster_workbook_header_row_invalid` instead. +- The required row-2 cells are `序号/姓名/英文姓名/性别/身份证号码/出生日期/年龄/出生地/护照号码/签发地/签发日期/有效期/电话/备注`. Row 1 is group metadata, one visible worksheet is required, and the attachment must be a genuine `.xls` or `.xlsx` rather than a renamed or exported incompatible workbook. +- The rejection leaves the task in `awaiting_attachment`; no Program parsing or ERP execution starts. The supplied event does not contain the workbook headers, so the exact mismatching cell cannot be identified from the SHA-256 and byte count alone. +- Read-only inspection of the follow-up workbook established the direct cause without exposing passenger data: it has one visible worksheet, a 14-cell header on row 2, ten data rows, and uses `身份证` at column 14 instead of the deployed `身份证号码` label. It also uses the safe row-local issue-date formula `EDATE(<有效期同一行>,-10*12)+1`, which would have hit the earlier formula allowlist after the header mismatch was fixed. +- The requested fix is implemented in `passenger-roster-workbook.ts` as `ltjt-passenger-roster-workbook-v1.3.0`. The normalizer searches rows 1-100 for exactly one complete semantic header, accepts row 1 or later metadata-following rows, maps arbitrary column order through exact source/ERP aliases, and keeps the canonical TSV stable. +- Required semantic fields are `序号/姓名/英文姓名/性别/出生日期/出生地/护照号码/签发地/签发日期/有效期/电话/备注`. Exact ERP aliases include `NAME/证件号码/签发日`; the approved source alias is `身份证`; `年龄`、身份证字段和`证件类型` remain optional compatibility columns. Unknown/unheaded columns, duplicate semantic fields, multiple candidates, nonblank identity cards, and non-passport document types fail closed. +- Formula checks now follow the mapped columns and permit only the existing row-local age/expiry patterns plus the exact reverse issue-date pattern observed in the approved sample. Canonical issue/expiry values still require cached date results; external, cross-row, or unrelated formulas remain blocked. +- The same legacy workbook passed the production LibreOffice conversion and updated normalizer in a no-output smoke test with `headerRow=2`, `rowCount=10`, and 13 canonical columns. Workbook bytes and canonical passenger rows were not retained. +- Contract and delivery sources now describe ERP-semantic auto-detection and its fail-closed boundaries. The business input contract and generated DOCX are `0.5.125`; the lifecycle Skill remains package version `0.5.125` and was rebuilt from current source. +- `dist/老挝联泰AI指令表-0.5.125.docx` was regenerated and rendered. The superseded `0.5.123` and pre-integration `0.5.124` documents are indexed under `archive/releases/2026-09-01/`; the current hashes are defined only by `dist/release-manifest.json`. +- Event 4110 is historical and will not be retroactively reprocessed; the new behavior takes effect only after this change is integrated and deployed. + +## Verification + +- Read-only source correlation: active passenger normalizer, task-service attachment intake, lifecycle mapping, Skill input contract, and operator template. +- Read-only planning/context gates: `check_project_docs.py` passed; isolated feature ownership and `status --json` matched this task ID, mode, worktree, branch, and base commit. +- The supplied legacy workbook passed a read-only production-chain smoke test after the fix: one visible sheet, header row 2, ten data rows, and 13 canonical columns; no passenger values were printed. +- Focused `passenger-roster-workbook.test.ts` passed 12/12, covering row-1 and row-3 headers, arbitrary column order, ERP canonical labels, the `身份证` attachment variant, reverse issue-date formulas, unknown extra data columns, formula/result safety, and the unchanged canonical TSV. +- `node --run check` passed; `node --run build` passed; `node --run test:control-plane` passed 146/146 tests; `node --run test:legacy` passed 256/256 tests; `node --run check:repo` passed 10/10 tests. Commands used the installed Node runtime through an explicit PATH. +- The official Skill `quick_validate.py` passed. The lifecycle `.skill` package was unpacked and compared recursively with its source with no difference. +- The `0.5.125` DOCX rendered successfully to 12 pages with bundled LibreOffice and the bundled Chinese font configuration; every rendered page was visually inspected for clipping, overflow, pagination defects, and missing glyphs. +- `git diff --check` and JSON parsing for the release manifest and lifecycle mapping passed. +- `check_project_docs.py` and `check_doc_drift.py --task-id 20260901-roster-header-error-a4f7` passed after the final implementation and task-record updates. +- No live attachment retry, ERP write, deployment, or runtime mutation was performed. + +## Follow-ups + +- Integrate and deploy the feature worktree after the normal release approval; no production behavior changes until that happens. +- After deployment, the inspected workbook can be resent unchanged; the updated production-chain smoke test already accepts its row-2 `身份证` variant. A first-row ERP header is also covered by regression tests. +- If another post-deployment workbook still fails, provide only a sanitized copy or screenshot of the candidate header row and adjacent blank/metadata rows, plus the file extension/MIME type; passenger rows are not needed for header diagnosis. + +## Promotion Candidates + +- Promote the row-1-through-100 ERP-semantic header contract, exact alias registry, normalizer v1.3.0, synchronized `0.5.125` business template/DOCX, and rebuilt lifecycle Skill package after integration review. diff --git a/.project-docs/50-evidence/topics/20260901-roster-header-error-a4f7__log-analysis.md b/.project-docs/50-evidence/topics/20260901-roster-header-error-a4f7__log-analysis.md new file mode 100644 index 0000000..98d09f8 --- /dev/null +++ b/.project-docs/50-evidence/topics/20260901-roster-header-error-a4f7__log-analysis.md @@ -0,0 +1,37 @@ +# Evidence: Roster workbook header rejection + +## Identity + +- Task: `20260901-roster-header-error-a4f7` +- Observed at: 2026-09-01, from the user-supplied task-event log +- Source: event `4110`, manual attachment attempt +- Confidence: high for the failure boundary; insufficient to identify the exact mismatching header cell + +## Observed behavior + +- The initial roster instruction entered `awaiting_attachment` with the normal `.xls/.xlsx` waiting message. +- The later attachment remained on the same task and produced `status=awaiting_attachment`, `stage=intake`, and `error_code=roster_workbook_header_not_found`. +- The event reports only a bounded byte count and SHA-256. It does not contain workbook headers or cell values. + +## Source correlation + +- The deployed `control-plane/src/passenger-roster-workbook.ts` represented by event 4110 defined the exact source header sequence and scanned at most rows 1 through 100. +- Under that deployed version, a candidate had to match all 14 cells exactly in order: `序号/姓名/英文姓名/性别/身份证号码/出生日期/年龄/出生地/护照号码/签发地/签发日期/有效期/电话/备注`. +- The header must be on row 2. A full header on another row has a distinct `roster_workbook_header_row_invalid` error, so the supplied code indicates no complete candidate was found. +- `TaskService.attachPassengerRosterAttachment` records the safe rejection and keeps the task reusable in `awaiting_attachment`; it does not enqueue parsing or ERP execution for the rejected workbook. + +## Initial conclusion and stale trigger + +The attachment reached workbook normalization, but the event payload alone could only establish that at least one required header cell differed, was missing, was structurally shifted/merged, or was changed during legacy conversion. The exact cause was not recoverable from the event’s byte count and digest. Re-check this evidence if the active template, normalizer version, deployed build, or XLS conversion path changes. + +## Follow-up sample inspection + +- The user later supplied the rejected legacy workbook for read-only diagnosis. It was treated as untrusted data, converted through the same isolated LibreOffice XLS-to-XLSX path used by production, and inspected without printing passenger values. +- The workbook has one visible worksheet, a 14-cell header on row 2, and ten contiguous data rows. +- The direct header mismatch is the label `身份证` in column 14. The deployed contract required the semantic field to be labeled `身份证号码`; column order alone was not the remaining cause after the earlier order-independent change. +- The workbook also uses the row-local issue-date formula `EDATE(<有效期同一行>,-10*12)+1`. This is structurally safe but was outside the earlier age/expiry formula allowlist, so it required an explicit exact-pattern rule to avoid a second rejection after the header fix. +- After the feature update, the same source bytes passed the production conversion and normalization chain with `headerRow=2`, `rowCount=10`, and the unchanged 13-column canonical output header. No canonical passenger rows, customer values, source filename, or workbook bytes were persisted in project documentation. + +## Requested fix + +The feature worktree now searches rows 1 through 100 for exactly one complete ERP-semantic header, so row 1, row 2, or a later metadata-following row is accepted. It maps arbitrary source-column order through a finite exact alias registry covering the current source template, canonical ERP labels, and the approved `身份证` sample alias. `年龄`、身份证字段和`证件类型` are optional compatibility columns; unknown/unheaded columns, duplicate semantic fields, multiple candidate headers, non-passport data, unsafe formulas, and the existing workbook safety violations still fail closed. The canonical 13-column TSV order is unchanged. Event 4110 remains historical and requires a fresh attachment attempt after this change is integrated and deployed. diff --git a/README.md b/README.md index 36b5add..fdd576b 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ ## 当前发布基线 - Chrome 扩展制品版本由 [发布清单](dist/release-manifest.json) 定义;运行中的浏览器版本必须通过实时握手确认,不写入静态交接文档。 -- 五个业务 Skill:`0.5.125`;运营 DOCX:`0.5.123`。 +- 五个业务 Skill:`0.5.125`;运营 DOCX:`0.5.125`。 - Agent Prompt:`ltjt-agent-prompt-v1.8-independent-headcount-categories`。 - 控制面确定性解析器:`ltjt-program-parser-v1.0.6`;18 项路由迁移后仍默认 `ai`,由操作台逐项晋级;两项名单 route 固定为 Program-only。 - 独立团/散拼子单名单固定为 Program-only:文字用于业务定位,名单由单个 `.xls`/`.xlsx` 附件确定性规范化;附件到齐前任务保持等待。 diff --git a/agent设计规范/business-adaptation-registry.md b/agent设计规范/business-adaptation-registry.md index af709c9..f96a3bb 100644 --- a/agent设计规范/business-adaptation-registry.md +++ b/agent设计规范/business-adaptation-registry.md @@ -20,7 +20,7 @@ | `team_order_create` | 独立团单个下单 | [`lwlt-newbooking`](skills/lwlt-newbooking/SKILL.md) / `team_order_create` | [业务入口](businesses/team_order_create.md) | [ERP 流程](businesses/team_order_create.md) | [Skill actions](skills/lwlt-newbooking/references/actions-and-fields.md) | 已真实验证 | | `shared_plan_create` | 散拼团新增计划 | [`lwlt-newbooking`](skills/lwlt-newbooking/SKILL.md) / `shared_plan_create` | [业务入口](businesses/shared_plan_create.md) | [ERP 流程](businesses/shared_plan_create.md) | [Skill actions](skills/lwlt-newbooking/references/actions-and-fields.md) | 单个/批量母团已重复验证;E2E3/E2E4 均证明批量每日期具体子单、客户与 15+1,统一发布仍待批准 | | `shared_child_order_create` | 散拼团单个新增子单/计划 | [`lwlt-newbooking`](skills/lwlt-newbooking/SKILL.md) / `shared_child_order_create` | [业务入口](businesses/shared_child_order_create.md) | [ERP 流程](businesses/shared_child_order_create.md) | [Skill actions](skills/lwlt-newbooking/references/actions-and-fields.md) | 已重复真实创建并完成后续全生命周期;仍受统一发布门槛约束 | -| `passenger_list_import` | 独立团/散拼子单名单导入/补充 | [`lwlt-lifecycle`](skills/lwlt-lifecycle/SKILL.md) / `passenger_list_import`(Program-only) | [统一输入模板 §5—6](templates/business-input-templates.md);文字定位 + 单个 `.xls/.xlsx` 14 列附件;第一行团信息、第二行表头 | 识别 route 后等待附件 → 程序规范化为加密 13 列 TSV,并从唯一严格`领队`备注行提取姓名/电话 → 读取 ERP 游客位 → 未确认的占用序号要求覆盖确认;确认后所有指定序号强制覆写(同值也写)→ `DaoRuDones` → 原生游客位不足时返回“名单 N 人超过 ERP 最多 M 人”且不保存 → 恢复未指定行 → 投影领队联系人 → 完整写前投影 → 父页原生保存;ERP 明确返回成功即完成,不再逐行写后回查 | 用户模板 `.xls` 已只读通过 25 行规范化及单一领队行结构核验;解析阶段不以初始 16/31 行作统一静态上限,5000 为技术上限;执行时以精确 ERP 页面实际物化游客位为准。2026-08-28 一笔散拼子单 25 行及领队联系人已持久化全等;另一次独立团 25 人只物化 16 位并在保存前安全阻断 | 名单与领队联系人真实写入已有一笔证据;`0.5.155` 起确认覆盖同值也写,`0.5.156` 起容量不足使用人数/上限业务提示并仅在技术详情保留原始 blocker;其他对象/边界继续按发布门槛分级 | +| `passenger_list_import` | 独立团/散拼子单名单导入/补充 | [`lwlt-lifecycle`](skills/lwlt-lifecycle/SKILL.md) / `passenger_list_import`(Program-only) | [统一输入模板 §5—6](templates/business-input-templates.md);文字定位 + 单个 `.xls/.xlsx` 附件;程序在前 100 行自动定位唯一 ERP 字段表头,按精确语义映射任意表头行、列顺序及受支持别名,并读取其下连续名单数据 | 识别 route 后等待附件 → 程序规范化为加密 13 列 TSV,并从唯一严格`领队`备注行提取姓名/电话 → 读取 ERP 游客位 → 未确认的占用序号要求覆盖确认;确认后所有指定序号强制覆写(同值也写)→ `DaoRuDones` → 原生游客位不足时返回“名单 N 人超过 ERP 最多 M 人”且不保存 → 恢复未指定行 → 投影领队联系人 → 完整写前投影 → 父页原生保存;ERP 明确返回成功即完成,不再逐行写后回查 | 用户模板 `.xls` 已只读通过 25 行规范化及单一领队行结构核验;解析阶段不以初始 16/31 行作统一静态上限,5000 为技术上限;执行时以精确 ERP 页面实际物化游客位为准。2026-08-28 一笔散拼子单 25 行及领队联系人已持久化全等;另一次独立团 25 人只物化 16 位并在保存前安全阻断 | 名单与领队联系人真实写入已有一笔证据;`0.5.155` 起确认覆盖同值也写,`0.5.156` 起容量不足使用人数/上限业务提示并仅在技术详情保留原始 blocker;其他对象/边界继续按发布门槛分级 | | `order_update_shared_plan` / `order_update_shared_child` | 散拼母团容量/散拼子单住宿说明 | [`lwlt-updating`](skills/lwlt-updating/SKILL.md) | [统一输入模板:修改](templates/business-input-templates.md) | `DoInfoSP` / `DoInfo_order`;写前读取当前值 | 母团容量、子单住宿说明通过;用户只填目标值;无年份定位日期取任务日后的下一次发生日 | Skill 全量解析;仅已验证字段自动执行,其他字段保留并转人工复核 | | `order_update_independent` | 独立团信息修改 | [`lwlt-updating`](skills/lwlt-updating/SKILL.md) / `order_update_independent` | [统一输入模板 §14](templates/business-input-templates.md) | `DoInfoJH`;写前读取当前值;SGL/TWN 分别映射 `frenshu0`/`frenshu1`,成人/占床儿童/不占床儿童/领队分别映射 `darenshu`/`xiaorenshu`/`ertrenshu`/`quanrenshu` | 标间数 `8→9→8` 已有明确响应和 fresh GET;无年份定位/目标日期取下一次发生日;SGL/TWN 与四类人数待新版本授权 ERP 实写复测 | Skill 全量解析;当前插件执行标间数、SGL/TWN 与四类人数,其他目标保留并转人工复核 | | `arrangement_guide` / `arrangement_vehicle` / `arrangement_hotel` / `arrangement_transport` / `arrangement_other` | 安排导游/用车/酒店/大交通/其他备案 | [`lwlt-arrangement`](skills/lwlt-arrangement/SKILL.md) | [统一输入模板 §7—11](templates/business-input-templates.md) | 五类 action-specific `DoInfo_*`,全类 `create|clear`;酒店另有窄 `update` | 两类团队共 10 create + 10 clear;酒店资源/房型由唯一 ERP 模糊匹配结果带入;大交通和其他/备案的结算单位组合候选按下拉顺序首条原生联动,其他/备案数量可选,省略时保留 ERP 默认值;散拼母团无子单时 ERP 禁止安排酒店并隐藏提交入口,插件在三项页面证据同时成立时提示先新增子单 | 已真实验证;非酒店 update 和酒店其他字段阻断 | diff --git a/agent设计规范/business-behavior-registry.md b/agent设计规范/business-behavior-registry.md index ef4a0b2..8c2cc6a 100644 --- a/agent设计规范/business-behavior-registry.md +++ b/agent设计规范/business-behavior-registry.md @@ -27,8 +27,8 @@ | `独立团信息修改` | `lwlt-updating` | `order_update_independent` | 全量解析业务目标;当前插件执行 `twin_room_count`、已字段映射的 `rooms.SGL`/`rooms.TWN` 与 `pax.adult`/`pax.child_bed`/`pax.child_no_bed`/`pax.leader`,新增房型和四类人数待新版本授权 ERP 复测,其余目标保留并转人工复核 | | `修改散拼母团计划收客数` / `散拼母团信息修改` | `lwlt-updating` | `order_update_shared_plan` | 全量解析业务目标;当前仅自动执行 `planned_capacity`,其他目标保留并转人工复核 | | `追加散拼子单订房说明` / `散拼子单信息修改` | `lwlt-updating` | `order_update_shared_child` | 全量解析业务目标;当前仅自动追加 `lodging_note` 且最终长度≤50,其他目标保留并转人工复核 | -| `导入独立团名单` | `lwlt-lifecycle` | `passenger_list_import` | Program-only;等待单个 14 列 `.xls/.xlsx`,程序转为 13 列 canonical TSV;ERP 初始 16 行仅为动态扩行基线,覆盖已有不同内容需确认;新版完整链路待授权实写复跑 | -| `导入散拼子单名单` | `lwlt-lifecycle` | `passenger_list_import` | Program-only;等待单个 14 列 `.xls/.xlsx`,母团本身不能导名单;ERP 初始 31 行仅为动态扩行基线;新版完整链路待授权实写复跑 | +| `导入独立团名单` | `lwlt-lifecycle` | `passenger_list_import` | Program-only;等待单个 `.xls/.xlsx`,自动识别 ERP 字段表头及其下名单并转为 13 列 canonical TSV;ERP 初始 16 行仅为动态扩行基线,覆盖已有不同内容需确认;新版完整链路待授权实写复跑 | +| `导入散拼子单名单` | `lwlt-lifecycle` | `passenger_list_import` | Program-only;等待单个 `.xls/.xlsx`,自动识别 ERP 字段表头及其下名单并转为 13 列 canonical TSV;母团本身不能导名单;ERP 初始 31 行仅为动态扩行基线;新版完整链路待授权实写复跑 | | `安排导游` | `lwlt-arrangement` | `arrangement_guide` | 当前生产输入只表达未确认新增;既有安排变更阻断 | | `安排用车` | `lwlt-arrangement` | `arrangement_vehicle` | 当前生产输入只表达未确认新增;按结算单位搜索词、用车起止日期和数量定位,车辆项目由唯一匹配结果带入;换车/update 阻断 | | `安排酒店` | `lwlt-arrangement` | `arrangement_hotel` | 支持未安排新增;酒店/房型由唯一 ERP 模糊匹配结果带入;既有唯一未安排或未确认行只开放离店日期/房间数窄 update | diff --git a/agent设计规范/skills/lwlt-lifecycle/SKILL.md b/agent设计规范/skills/lwlt-lifecycle/SKILL.md index dd1f9e5..2f62625 100644 --- a/agent设计规范/skills/lwlt-lifecycle/SKILL.md +++ b/agent设计规范/skills/lwlt-lifecycle/SKILL.md @@ -11,7 +11,7 @@ description: 定义 LTJT 的独立团或散拼子单名单导入、按序号补 1. 从首行和正文识别名单导入、取消或恢复。 2. 按 [input-contract.md](references/input-contract.md) 检查用户可见必填字段。 -3. 名单 route 先进入 `awaiting_attachment`;只接收一份 `.xls`/`.xlsx`。控制面程序校验 14 列模板并生成内部 13 列 canonical TSV,附件到齐前不启动解析或执行。 +3. 名单 route 先进入 `awaiting_attachment`;只接收一份 `.xls`/`.xlsx`。控制面程序在前 100 行中自动定位唯一的 ERP 名单字段表头,按精确字段语义映射任意列顺序及受支持别名,再把表头下方的连续名单数据生成内部 13 列 canonical TSV;附件到齐前不启动解析或执行。 4. Program Parser 校验内部 13 列表头、每行完整性、正整数唯一序号;允许用户只提供本次需要补录的一个或多个序号。备注去空格后严格等于`领队`的唯一行会生成结构化 `passenger_list.leader_contact`;姓名或电话为空、或出现多行领队时失败关闭。 5. 按 [operation-contract.md](references/operation-contract.md) 生成一个 operation。 6. 返回 `agent_parse_passed`、`agent_parse_needs_input` 或 `agent_parse_blocked`。 diff --git a/agent设计规范/skills/lwlt-lifecycle/references/input-contract.md b/agent设计规范/skills/lwlt-lifecycle/references/input-contract.md index ededc85..3e0e09f 100644 --- a/agent设计规范/skills/lwlt-lifecycle/references/input-contract.md +++ b/agent设计规范/skills/lwlt-lifecycle/references/input-contract.md @@ -18,7 +18,7 @@ 覆盖确认(选填):<确认替换已有名单时填写“是”> ``` -名单附件(必填):随同文字发送一份按 14 列名单模板填写的 `.xls` 或 `.xlsx`;每行序号对应订单游客位。 +名单附件(必填):随同文字发送一份符合下述 ERP 字段规则的 `.xls` 或 `.xlsx` 名单工作簿;每行序号对应订单游客位。 ### 示例 @@ -30,7 +30,7 @@ 领队:张三 ``` -示例同时附一份按 14 列模板填写的名单工作簿。 +示例同时附一份符合 ERP 字段规则的名单工作簿。 ## 导入散拼子单名单 @@ -46,7 +46,7 @@ 覆盖确认(选填):<确认替换已有名单时填写“是”> ``` -名单附件(必填):随同文字发送一份按 14 列名单模板填写的 `.xls` 或 `.xlsx`;每行序号对应具体子单游客位。 +名单附件(必填):随同文字发送一份符合下述 ERP 字段规则的 `.xls` 或 `.xlsx` 名单工作簿;每行序号对应具体子单游客位。 ### 示例 @@ -58,9 +58,9 @@ 领队:张三 ``` -示例同时附一份按 14 列模板填写的名单工作簿。 +示例同时附一份符合 ERP 字段规则的名单工作簿。 -附件必须且只能有一个可见工作表。第一行是团信息,只作模板说明且不进入 ERP;第二行必须依次为:`序号/姓名/英文姓名/性别/身份证号码/出生日期/年龄/出生地/护照号码/签发地/签发日期/有效期/电话/备注`。当前只支持护照名单,“身份证号码”非空时阻断。“年龄”仅作模板兼容,不进入 ERP;程序把“英文姓名”映射为 `NAME`、“护照号码”映射为“证件号码”、固定合成“证件类型=护照”,并把“签发日期”映射为“签发日”。 +附件必须且只能有一个可见工作表。程序在前 100 行中自动定位唯一表头;表头可直接位于第一行,也可位于团信息或说明行之后,列顺序任意,表头下一行起的名单数据必须连续。表头必须各包含一次这些 ERP 语义字段:`序号/姓名/英文姓名(或 NAME)/性别/出生日期/出生地/护照号码(或证件号码)/签发地/签发日期(或签发日)/有效期/电话/备注`。可选兼容字段为`年龄`、`身份证号码(或身份证)`、`证件类型`;同一语义字段不得重复,未知表头或无表头数据列会失败关闭。当前只支持护照名单:身份证列如存在必须为空,证件类型如有值必须为`护照`;`年龄`不进入 ERP。程序把英文姓名映射为 `NAME`、护照号码映射为“证件号码”、固定输出“证件类型=护照”,并把签发日期映射为“签发日”。 若某位游客是领队,该行`备注`去除首尾空格后必须严格等于`领队`,并同时填写该行`姓名`和`电话`。一份名单最多标记一位领队;标记成功后程序会把这两个值写入 ERP `联系人/领队人`和`电话`,该行仍作为普通游客导入。`领队备注`等其他文字不触发写入;没有严格标记时保留 ERP 原联系人信息。正文里的`领队`字段仍只是订单定位的选填补充关键词,不是联系人写入来源。 diff --git a/agent设计规范/templates/business-input-templates.md b/agent设计规范/templates/business-input-templates.md index dd8d931..50db1b1 100644 --- a/agent设计规范/templates/business-input-templates.md +++ b/agent设计规范/templates/business-input-templates.md @@ -1,6 +1,6 @@ # 运营业务 AI 输入模板总表 -交付版本:0.5.123 +交付版本:0.5.125 适用范围:当前 18 项运营业务输入;执行能力以生命周期发布门槛为准。 本文件用于向系统提交 ERP 业务需求。复制对应模板填写;示例只供参考,不要直接用于生产业务。 @@ -128,7 +128,7 @@ 名单只通过一份 `.xls` 或 `.xlsx` 附件提交,不再粘贴到文字输入框。系统识别名单业务后会等待附件,附件到齐后只走程序解析。 -工作簿第一行是团信息,系统不导入;第二行必须使用 14 列表头:`序号/姓名/英文姓名/性别/身份证号码/出生日期/年龄/出生地/护照号码/签发地/签发日期/有效期/电话/备注`。当前只支持护照名单,“身份证号码”列留空;每次只保留一个可见工作表,最多 5000 行。 +系统会在工作表前 100 行中自动定位唯一的名单表头;表头可以直接在第一行,也可以位于团信息或说明行之后,字段顺序任意,表头下一行开始识别连续名单数据。表头必须各包含一次这些 ERP 语义字段:`序号/姓名/英文姓名(或 NAME)/性别/出生日期/出生地/护照号码(或证件号码)/签发地/签发日期(或签发日)/有效期/电话/备注`。可兼容附加列`年龄`、`身份证号码(或身份证)`、`证件类型`;同一语义字段不得重复,未知表头或无表头数据列会失败关闭。当前只支持护照名单:身份证列如存在必须留空,证件类型如有值必须为`护照`;`年龄`不进入 ERP。每次只保留一个可见工作表,最多 5000 行。 若游客是领队,在该行`备注`只填`领队`,并确保同一行`姓名`和`电话`已填写;一份名单最多一位。系统会把这两个值同步到 ERP `联系人/领队人`和`电话`。其他备注不触发联系人写入;没有领队标记时保留 ERP 原信息。正文里的`领队`仍只是选填的查单关键词。 @@ -146,7 +146,7 @@ 覆盖确认(选填):<只有替换已有名单时填写“是”> ``` -名单附件(必填):随同以上文字发送一份按 14 列模板填写的 `.xls` 或 `.xlsx`。 +名单附件(必填):随同以上文字发送一份符合上述 ERP 字段规则的 `.xls` 或 `.xlsx` 名单工作簿。 #### 示例 @@ -173,7 +173,7 @@ 覆盖确认(选填):<只有替换已有名单时填写“是”> ``` -名单附件(必填):随同以上文字发送一份按 14 列模板填写的 `.xls` 或 `.xlsx`。 +名单附件(必填):随同以上文字发送一份符合上述 ERP 字段规则的 `.xls` 或 `.xlsx` 名单工作簿。 #### 示例 diff --git a/agent设计规范/test-fixtures/lwlt-lifecycle/release-gate.md b/agent设计规范/test-fixtures/lwlt-lifecycle/release-gate.md index 2bd7815..661de18 100644 --- a/agent设计规范/test-fixtures/lwlt-lifecycle/release-gate.md +++ b/agent设计规范/test-fixtures/lwlt-lifecycle/release-gate.md @@ -5,7 +5,7 @@ - Chrome 插件:`0.5.163` - Agent Prompt:`ltjt-agent-prompt-v1.8-independent-headcount-categories` - 生命周期契约:`ltjt-lifecycle-v2.9-roster-leader-contact-2026-08` -- 五个 Skill:`0.5.125`;运营 DOCX:`0.5.123` +- 五个 Skill:`0.5.125`;运营 DOCX:`0.5.125` - 发布状态:核心窄能力已有真实证据;一笔名单附件 Program-only 任务已把 25 行及唯一领队联系人正确保存到 ERP,后续只读逐字段核验全等。两种写后逐行读取方式都曾在真实成功写入后产生假阴性;名单在全部写前门禁通过后,以父表单 ERP 明确成功响应作为终结证据,不再执行写后逐行回查或自动名单对账。`0.5.155` 规定确认覆盖的指定序号同值也写;`0.5.156` 把原生游客位不足统一映射为包含名单人数和 ERP 实际上限的业务提示,原技术 blocker 仅留技术详情。`0.5.157` 新增散拼母团“整团游客信息”的 `shared_plan + visitor-list + tid-only` 窄分支,静态契约与回归已通过,仍待授权后的新版运行态只读复测。`0.5.158` 把正式控制面 `https://lwlt.nianxx.cn` 同步加入 Popup、后台、Manifest 权限和 content script 精确白名单;其他 HTTPS origin 仍保持阻断。`0.5.159` 修复 MV3 后台恢复被中断后持久化 `running` 永久阻断插件派发的问题:仅在内存恢复 Promise 已不存在时收敛为 `interrupted`,真实活动恢复仍保持阻断,下一次成功只读保活把状态归零为 `idle`。`0.5.160` 为独立团单个下单写前漂移增加仅字段名诊断,并把数字后缀动态应收字段纳入保护。`0.5.161` 等待客户、跟单人和销售人原生 lookup 数据就绪后才开始单团预检;生产反馈证明仅检查 lookup 数据仍不足。`0.5.162` 进一步要求页面 jQuery Ajax 在预检前归零,并在 `GetProduct` 后等待原生 `ajaxStop` 再应用最终业务字段。`0.5.163` 按生产操作方明确要求恢复 `0.5.156` 的实际校验范围:核心订单字段继续写前阻断,带编号的动态 `ys_*` 应收行不再纳入保护哈希,允许 ERP 原生回调在预检后调整这些字段。新增独立团房型/人数映射仍待实写;未验证宽能力继续失败关闭。 运行中的浏览器版本必须实时握手确认。当前制品文件名和 SHA-256 只看 [`../../../dist/release-manifest.json`](../../../dist/release-manifest.json),不从历史日志推断。 @@ -15,7 +15,7 @@ | 能力 | 当前边界 | |---|---| | 创建 | 独立团单个/批量、散拼母团单个/批量及具体子单;批量逐日期保留响应和回查 | -| 名单 | 独立团和具体散拼子单;文字定位后等待单个 `.xls`/`.xlsx`,程序规范化 14 列模板并固定 Program-only;支持明确序号的动态扩行、安全合并、同值幂等和覆盖确认。解析阶段不以初始 16/31 行作统一上限;执行时若原生页实际物化游客位不足,则在保存前以人数/上限业务提示阻断。2026-08-28 已只读确认一笔具体散拼子单的 25 行及领队联系人真实持久化全等;另一次独立团 25 人只物化 16 位且未保存 | +| 名单 | 独立团和具体散拼子单;文字定位后等待单个 `.xls`/`.xlsx`,程序在前 100 行自动识别唯一 ERP 字段表头及其下连续名单,规范化为 13 列 TSV 并固定 Program-only;支持明确序号的动态扩行、安全合并、同值幂等和覆盖确认。解析阶段不以初始 16/31 行作统一上限;执行时若原生页实际物化游客位不足,则在保存前以人数/上限业务提示阻断。2026-08-28 已只读确认一笔具体散拼子单的 25 行及领队联系人真实持久化全等;另一次独立团 25 人只物化 16 位且未保存 | | 安排 | 导游、车辆、酒店、大交通、其他/备案 create/clear;酒店新增状态为未安排 | | 独立团修改 | `twin_room_count` 已真实持久化;SGL/TWN 与成人/占床儿童/不占床儿童/领队人数已接入映射/回查但待新版本实写 | | 散拼修改 | 母团 `planned_capacity`、具体子单最终长度 ≤50 的 `lodging_note` | diff --git a/archive/releases/2026-09-01/README.md b/archive/releases/2026-09-01/README.md new file mode 100644 index 0000000..83230da --- /dev/null +++ b/archive/releases/2026-09-01/README.md @@ -0,0 +1,7 @@ +# 2026-09-01 发布归档 + +本目录只保存被新版本替代的历史发布物,不定义当前运行规则。当前制品文件名和 SHA-256 只以根目录 [`../../../dist/release-manifest.json`](../../../dist/release-manifest.json) 为准。 + +- `release-manifest-0.5.163-docx-0.5.123.json`:更新名单工作簿表头映射前的历史清单快照。 +- `老挝联泰AI指令表-0.5.123.docx`:被支持第二行 A:N 表头字段乱序自动映射的 `0.5.124` 文档替代;保留用于恢复和追溯。 +- `老挝联泰AI指令表-0.5.124.docx`:未集成期间被 `0.5.125` 文档替代;后者改为在前 100 行自动识别唯一 ERP 字段表头,并兼容第一行表头、ERP 字段名及已批准名单别名。 diff --git a/archive/releases/2026-09-01/release-manifest-0.5.163-docx-0.5.123.json b/archive/releases/2026-09-01/release-manifest-0.5.163-docx-0.5.123.json new file mode 100644 index 0000000..70234d5 --- /dev/null +++ b/archive/releases/2026-09-01/release-manifest-0.5.163-docx-0.5.123.json @@ -0,0 +1,69 @@ +{ + "manifest_version": 1, + "generated_on": "2026-08-30", + "baselines": { + "chrome_extension": "0.5.163", + "skills": "0.5.125", + "business_instruction_docx": "0.5.123", + "agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories" + }, + "artifacts": [ + { + "kind": "chrome_extension", + "version": "0.5.163", + "path": "dist/ltjt-order-assistant-0.5.163.zip", + "source": "chrome-extension/ltjt-order-assistant", + "sha256": "90c550054cdf747aa9c2c80bca5ee5fba0d13f126acfd4779f85400764379bf0" + }, + { + "kind": "skill", + "name": "lwlt-arrangement", + "version": "0.5.125", + "path": "dist/lwlt-arrangement-0.5.125.skill", + "source": "agent设计规范/skills/lwlt-arrangement", + "sha256": "c729bebec84695b8642a6d31cb4dc0d963135b6c6620c2173be095826cd3976d" + }, + { + "kind": "skill", + "name": "lwlt-confirmation", + "version": "0.5.125", + "path": "dist/lwlt-confirmation-0.5.125.skill", + "source": "agent设计规范/skills/lwlt-confirmation", + "sha256": "659f6aca7918a03a29fc78f0b4b938dfa9084ccdbb44ebd71f6399556cfa4609" + }, + { + "kind": "skill", + "name": "lwlt-lifecycle", + "version": "0.5.125", + "path": "dist/lwlt-lifecycle-0.5.125.skill", + "source": "agent设计规范/skills/lwlt-lifecycle", + "sha256": "8cc5d94d97abbc68388bf60d4b46ce4cb1aa947dfb174831c0ad2afc421002f0" + }, + { + "kind": "skill", + "name": "lwlt-newbooking", + "version": "0.5.125", + "path": "dist/lwlt-newbooking-0.5.125.skill", + "source": "agent设计规范/skills/lwlt-newbooking", + "sha256": "bb610c50dd659fc172aabfa469b3028de98704c249cfd4890463deea0c88cd0c" + }, + { + "kind": "skill", + "name": "lwlt-updating", + "version": "0.5.125", + "path": "dist/lwlt-updating-0.5.125.skill", + "source": "agent设计规范/skills/lwlt-updating", + "sha256": "c6f5bc9008e2f63d170f3f140140003d99411ce5232632c48b38891da1ceee33" + }, + { + "kind": "business_instruction_docx", + "version": "0.5.123", + "path": "dist/老挝联泰AI指令表-0.5.123.docx", + "source": "agent设计规范/templates/business-input-templates.md", + "builder": "tools/build_business_instruction_docx.py", + "sha256": "8d94b1f4e3d1cfccf62c1b7d420cbcebf8c915844ae95fa90840963b32b1cccb", + "source_sha256": "8db16b3dce7e3dd8aa88a6273a9a90bbc3c98a087cb863bf0eeaaa5bb3a371ca", + "builder_sha256": "c162884210da22e3b78368749b66f1f177df5e9fc6155f4fd9954bd516d187f1" + } + ] +} diff --git a/dist/老挝联泰AI指令表-0.5.123.docx b/archive/releases/2026-09-01/老挝联泰AI指令表-0.5.123.docx similarity index 100% rename from dist/老挝联泰AI指令表-0.5.123.docx rename to archive/releases/2026-09-01/老挝联泰AI指令表-0.5.123.docx diff --git a/archive/releases/2026-09-01/老挝联泰AI指令表-0.5.124.docx b/archive/releases/2026-09-01/老挝联泰AI指令表-0.5.124.docx new file mode 100644 index 0000000000000000000000000000000000000000..05daae09ba47234f37f07a14799d40acb4580f3e GIT binary patch literal 31220 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 literal 0 HcmV?d00001 diff --git a/control-plane/README.md b/control-plane/README.md index 344e7f2..0548001 100644 --- a/control-plane/README.md +++ b/control-plane/README.md @@ -9,7 +9,7 @@ - 登录会话默认跨浏览器重启持续有效,不按空闲时间或绝对时长自动失效;用户退出、修改密码、账号停用或管理员强制撤销时由服务端立即撤销。 - 业务页面通过 REST 创建任务;Agent 返回结构化结果后,管理员在选中任务上一次点击“确认并提交到 ERP 插件”,再通过 SSE 或轮询读取服务端状态。 - 手工与 AgentBus 的每个新任务都会按同一组织、同一 18 项业务路由固化解析策略及配置 revision;来源不能覆盖模式。其中两项名单 route 固定为 `program_only`,其余 16 项可配置 `ai / shadow / auto / program`。全消息中的唯一已登记指令可以确定 route;没有指令时,只有全部标签都属于唯一 route、至少两个不同标签且业务定位必填项完整的字段签名才可确定 route。未知、冲突或多 route 输入不猜测。后续补充轮次沿用原任务快照,设置变化只影响新任务和新会话。 -- 名单 route 创建后先进入 `awaiting_attachment`,只接收一份 `.xls/.xlsx` 14 列模板。附件在内存中失败关闭地规范化为 13 列 canonical TSV;原始工作簿不入库,文件名和 canonical TSV 使用字段加密,解析通过或终止后清除 canonical 中间文本。附件到齐前不会领取解析任务,也不会进入 ERP。独立团初始 16 行、散拼子单初始 31 行仅为 ERP 动态扩行基线,5000 为技术上限。 +- 名单 route 创建后先进入 `awaiting_attachment`,只接收一份 `.xls/.xlsx`;控制面在前 100 行中自动定位唯一的 ERP 名单字段表头,按精确字段语义映射任意表头行、列顺序及受支持别名,并把表头下方连续名单数据在内存中失败关闭地规范化为 13 列 canonical TSV。未知额外列、重复语义字段和多个候选表头继续阻断。原始工作簿不入库,文件名和 canonical TSV 使用字段加密,解析通过或终止后清除 canonical 中间文本。附件到齐前不会领取解析任务,也不会进入 ERP。独立团初始 16 行、散拼子单初始 31 行仅为 ERP 动态扩行基线,5000 为技术上限。 - 平台的任务 ID、Agent 会话 ID、确认状态、重要摘要、事件和用户通讯内容只存在于平台任务/会话/结果信封中,不回写到 Agent `operation`,也不成为 ERP 业务字段。 - Agent `operation` 只保存解析态业务事实。插件领取已确认任务后先做无需 ERP 查询的前门禁,再在 ERP 内只读唯一解析对象、资源和当前状态,最后对内部 execution operation 执行严格写前门禁。 - Chrome 插件仍在管理员已登录的浏览器/ERP 会话中工作;`chrome.storage.local` 只是临时缓存。 diff --git a/control-plane/src/business-routes.ts b/control-plane/src/business-routes.ts index c5f2998..f11ec09 100644 --- a/control-plane/src/business-routes.ts +++ b/control-plane/src/business-routes.ts @@ -75,7 +75,7 @@ export function effectiveParserModeForRoute(route: BusinessRouteDefinition, conf } const PROGRAM_VERSION = 'ltjt-program-parser-v1.0.6'; -export const BUSINESS_INPUT_CONTRACT_VERSION = 'business-input-templates-0.5.123'; +export const BUSINESS_INPUT_CONTRACT_VERSION = 'business-input-templates-0.5.125'; const lookupFields = ({ customerRequired = true } = {}): ProgramFieldDefinition[] => [ { key: 'order_no', label: '单号', aliases: ['团号', '订单号'], kind: 'text' }, diff --git a/control-plane/src/passenger-roster-workbook.ts b/control-plane/src/passenger-roster-workbook.ts index d3e87b2..b050f7a 100644 --- a/control-plane/src/passenger-roster-workbook.ts +++ b/control-plane/src/passenger-roster-workbook.ts @@ -12,16 +12,52 @@ const DEFAULT_MAX_DATA_ROWS = 5_000; const ABSOLUTE_MAX_DATA_ROWS = 5_000; const MAX_PASSENGER_SEQUENCE = 5_000; const MAX_HEADER_ROW = 100; -const EXPECTED_HEADER_ROW = 2; -const SOURCE_COLUMN_COUNT = 14; -export const PASSENGER_ROSTER_WORKBOOK_VERSION = 'ltjt-passenger-roster-workbook-v1.1.0'; +export const PASSENGER_ROSTER_WORKBOOK_VERSION = 'ltjt-passenger-roster-workbook-v1.3.0'; -const SOURCE_HEADERS = [ +const SOURCE_FIELDS = [ '序号', '姓名', '英文姓名', '性别', '身份证号码', '出生日期', '年龄', '出生地', - '护照号码', '签发地', '签发日期', '有效期', '电话', '备注' + '证件类型', '护照号码', '签发地', '签发日期', '有效期', '电话', '备注' ] as const; +const REQUIRED_SOURCE_FIELDS = [ + '序号', '姓名', '英文姓名', '性别', '出生日期', '出生地', '护照号码', + '签发地', '签发日期', '有效期', '电话', '备注' +] as const; + +type SourceField = typeof SOURCE_FIELDS[number]; +type RequiredSourceField = typeof REQUIRED_SOURCE_FIELDS[number]; +type SourceColumnMap = Partial> & Record; + +/** + * Header recognition is intentionally exact and finite. It accepts the + * customer-facing template labels, the canonical ERP labels, and aliases + * observed in approved roster samples; it does not use fuzzy matching. + */ +const HEADER_FIELD_ENTRIES: ReadonlyArray = [ + ['序号', '序号'], + ['姓名', '姓名'], + ['英文姓名', '英文姓名'], + ['NAME', '英文姓名'], + ['性别', '性别'], + ['身份证号码', '身份证号码'], + ['身份证', '身份证号码'], + ['出生日期', '出生日期'], + ['年龄', '年龄'], + ['出生地', '出生地'], + ['证件类型', '证件类型'], + ['护照号码', '护照号码'], + ['证件号码', '护照号码'], + ['签发地', '签发地'], + ['签发日期', '签发日期'], + ['签发日', '签发日期'], + ['有效期', '有效期'], + ['电话', '电话'], + ['备注', '备注'] +]; + +const SOURCE_FIELD_BY_HEADER = new Map(HEADER_FIELD_ENTRIES); + export const CANONICAL_PASSENGER_HEADERS = [ '序号', '姓名', 'NAME', '性别', '出生日期', '出生地', '证件类型', '证件号码', '签发地', '签发日', '有效期', '电话', '备注' @@ -41,7 +77,6 @@ export type PassengerRosterWorkbookErrorCode = | 'roster_workbook_hidden_data' | 'roster_workbook_header_not_found' | 'roster_workbook_ambiguous_header' - | 'roster_workbook_header_row_invalid' | 'roster_workbook_formula_not_allowed' | 'roster_workbook_formula_result_missing' | 'roster_workbook_unexpected_column' @@ -52,6 +87,7 @@ export type PassengerRosterWorkbookErrorCode = | 'roster_workbook_sequence_limit' | 'roster_workbook_duplicate_sequence' | 'roster_workbook_identity_card_not_supported' + | 'roster_workbook_document_type_not_supported' | 'roster_workbook_invalid_date' | 'roster_workbook_invalid_identifier' | 'roster_workbook_invalid_cell'; @@ -68,12 +104,11 @@ const ERROR_MESSAGES: Record = { roster_workbook_unsafe_content: '名单工作簿包含不允许的宏、外部链接或嵌入内容', roster_workbook_ambiguous_sheet: '名单工作簿必须且只能包含一个工作表', roster_workbook_hidden_data: '名单工作簿包含隐藏的非空数据', - roster_workbook_header_not_found: '未找到完整的 14 列名单表头', + roster_workbook_header_not_found: '未找到可映射到 ERP 名单字段的完整表头', roster_workbook_ambiguous_header: '名单工作簿包含多个可识别的名单表头', - roster_workbook_header_row_invalid: '名单工作簿第 2 行必须是完整的 14 列名单表头', roster_workbook_formula_not_allowed: '名单工作簿包含不允许的公式', roster_workbook_formula_result_missing: '名单工作簿公式没有可验证的缓存结果', - roster_workbook_unexpected_column: '名单数据包含 14 列之外的内容', + roster_workbook_unexpected_column: '名单表头范围外包含未识别的数据列', roster_workbook_non_contiguous_rows: '名单数据行不连续', roster_workbook_row_limit: '名单数据行数超过技术限制', roster_workbook_no_data: '名单工作簿没有数据行', @@ -81,6 +116,7 @@ const ERROR_MESSAGES: Record = { roster_workbook_sequence_limit: '名单序号超过技术安全上限', roster_workbook_duplicate_sequence: '名单序号不能重复', roster_workbook_identity_card_not_supported: '当前名单导入不支持非空身份证号码', + roster_workbook_document_type_not_supported: '当前名单导入仅支持护照证件类型', roster_workbook_invalid_date: '名单日期无效', roster_workbook_invalid_identifier: '名单证件号或电话无法安全转换为文本', roster_workbook_invalid_cell: '名单单元格值无效' @@ -287,16 +323,35 @@ function cellColumn(cell: ExcelJS.Cell): number { return cell.fullAddress.col; } -function formulaAllowed(cell: ExcelJS.Cell, headerRow: number): boolean { +function excelColumnName(column: number): string { + let value = column; + let result = ''; + while (value > 0) { + const remainder = (value - 1) % 26; + result = String.fromCharCode(65 + remainder) + result; + value = Math.floor((value - 1) / 26); + } + return result; +} + +function formulaAllowed( + cell: ExcelJS.Cell, + headerRow: number, + sourceColumns: SourceColumnMap +): boolean { const row = cellRow(cell); const column = cellColumn(cell); if (row <= headerRow) return false; const formula = normalizedFormula(cell.formula); - if (column === 7) { - return formula === `DATEDIF(F${row},TODAY(),"Y")`; + const ageColumn = sourceColumns['年龄']; + if (ageColumn !== undefined && column === ageColumn) { + return formula === `DATEDIF(${excelColumnName(sourceColumns['出生日期'])}${row},TODAY(),"Y")`; } - if (column === 12) { - return formula === `EDATE(K${row},10*12)-1`; + if (column === sourceColumns['签发日期']) { + return formula === `EDATE(${excelColumnName(sourceColumns['有效期'])}${row},-10*12)+1`; + } + if (column === sourceColumns['有效期']) { + return formula === `EDATE(${excelColumnName(sourceColumns['签发日期'])}${row},10*12)-1`; } return false; } @@ -348,14 +403,21 @@ function cellHasData(cell: ExcelJS.Cell): boolean { return normalizedText(scalarValue(cell)) !== ''; } -function rowHasData(row: ExcelJS.Row, maxColumn = SOURCE_COLUMN_COUNT): boolean { - for (let column = 1; column <= maxColumn; column += 1) { +function rowHasData(row: ExcelJS.Row, sourceColumns: readonly number[]): boolean { + for (const column of sourceColumns) { if (cellHasData(row.getCell(column))) return true; } return false; } -function validateFormulasAndHiddenData(sheet: ExcelJS.Worksheet, headerRow: number): void { +function validateFormulasAndHiddenData( + sheet: ExcelJS.Worksheet, + headerRow: number, + sourceColumns: SourceColumnMap +): void { + const recognizedColumns = new Set( + Object.values(sourceColumns).filter((column): column is number => column !== undefined) + ); sheet.eachRow({ includeEmpty: false }, (row, rowNumber) => { let hasData = false; row.eachCell({ includeEmpty: false }, (cell, columnNumber) => { @@ -365,15 +427,16 @@ function validateFormulasAndHiddenData(sheet: ExcelJS.Worksheet, headerRow: numb if (containsExternalFormulaReference(formula)) { fail('roster_workbook_unsafe_content', { row: rowNumber, column: columnNumber }); } - if (!formulaAllowed(cell, headerRow)) { + if (!formulaAllowed(cell, headerRow, sourceColumns)) { fail('roster_workbook_formula_not_allowed', { row: rowNumber, column: columnNumber }); } } if (cell.type === ExcelJS.ValueType.Hyperlink || cell.isHyperlink) { fail('roster_workbook_unsafe_content', { row: rowNumber, column: columnNumber }); } - if (cellHasData(cell)) hasData = true; - if (columnNumber > SOURCE_COLUMN_COUNT && rowNumber >= headerRow) { + const containsData = cellHasData(cell); + if (containsData) hasData = true; + if (containsData && rowNumber >= headerRow && !recognizedColumns.has(columnNumber)) { fail('roster_workbook_unexpected_column', { row: rowNumber, column: columnNumber }); } }); @@ -391,21 +454,37 @@ function validateFormulasAndHiddenData(sheet: ExcelJS.Worksheet, headerRow: numb } } -function findHeaderRow(sheet: ExcelJS.Worksheet): number { - const candidates: number[] = []; +function headerColumnsForRow(sheet: ExcelJS.Worksheet, rowNumber: number): SourceColumnMap | null { + const columns = new Map(); + const row = sheet.getRow(rowNumber); + for (let columnNumber = 1; columnNumber <= sheet.columnCount; columnNumber += 1) { + const cell = row.getCell(columnNumber); + if (cell.type === ExcelJS.ValueType.Merge && cell.master.address !== cell.address) return null; + const value = headerText(cell); + if (value === '') continue; + const field = SOURCE_FIELD_BY_HEADER.get(value); + if (!field || columns.has(field)) return null; + columns.set(field, columnNumber); + } + if (!REQUIRED_SOURCE_FIELDS.every((field) => columns.has(field))) return null; + + const result: Partial> = {}; + for (const field of SOURCE_FIELDS) { + const column = columns.get(field); + if (column !== undefined) result[field] = column; + } + return result as SourceColumnMap; +} + +function findHeaderLayout(sheet: ExcelJS.Worksheet): { row: number; columns: SourceColumnMap } { + const candidates: Array<{ row: number; columns: SourceColumnMap }> = []; const lastCandidate = Math.min(sheet.rowCount, MAX_HEADER_ROW); for (let rowNumber = 1; rowNumber <= lastCandidate; rowNumber += 1) { - const row = sheet.getRow(rowNumber); - const values = SOURCE_HEADERS.map((_header, index) => headerText(row.getCell(index + 1))); - if (values.every((value, index) => value === SOURCE_HEADERS[index])) { - candidates.push(rowNumber); - } + const columns = headerColumnsForRow(sheet, rowNumber); + if (columns) candidates.push({ row: rowNumber, columns }); } if (candidates.length === 0) fail('roster_workbook_header_not_found'); if (candidates.length !== 1) fail('roster_workbook_ambiguous_header'); - if (candidates[0] !== EXPECTED_HEADER_ROW) { - fail('roster_workbook_header_row_invalid', { row: candidates[0] }); - } return candidates[0]; } @@ -488,12 +567,19 @@ function canonicalTextCell(cell: ExcelJS.Cell): string { return normalizedText(scalarValue(cell)); } -function dataRows(sheet: ExcelJS.Worksheet, headerRow: number, maxDataRows: number): ExcelJS.Row[] { +function dataRows( + sheet: ExcelJS.Worksheet, + headerRow: number, + sourceColumns: SourceColumnMap, + maxDataRows: number +): ExcelJS.Row[] { const rows: ExcelJS.Row[] = []; let gapAfterData = false; + const recognizedColumns = Object.values(sourceColumns) + .filter((column): column is number => column !== undefined); for (let rowNumber = headerRow + 1; rowNumber <= sheet.rowCount; rowNumber += 1) { const row = sheet.getRow(rowNumber); - const hasData = rowHasData(row); + const hasData = rowHasData(row, recognizedColumns); if (!hasData) { if (rows.length > 0) gapAfterData = true; continue; @@ -506,32 +592,61 @@ function dataRows(sheet: ExcelJS.Worksheet, headerRow: number, maxDataRows: numb return rows; } -function canonicalRow(row: ExcelJS.Row, date1904: boolean, seenSequences: Set): string[] { - const sequence = canonicalSequence(row.getCell(1)); +function canonicalRow( + row: ExcelJS.Row, + date1904: boolean, + seenSequences: Set, + sourceColumns: SourceColumnMap +): string[] { + const cell = (field: RequiredSourceField): ExcelJS.Cell => row.getCell(sourceColumns[field]); + const sequence = canonicalSequence(cell('序号')); if (seenSequences.has(sequence)) { - fail('roster_workbook_duplicate_sequence', { row: row.number, column: 1 }); + fail('roster_workbook_duplicate_sequence', { + row: row.number, + column: sourceColumns['序号'] + }); } seenSequences.add(sequence); - if (canonicalIdentifier(row.getCell(5)) !== '') { - fail('roster_workbook_identity_card_not_supported', { row: row.number, column: 5 }); + const identityCardColumn = sourceColumns['身份证号码']; + if ( + identityCardColumn !== undefined + && canonicalIdentifier(row.getCell(identityCardColumn)) !== '' + ) { + fail('roster_workbook_identity_card_not_supported', { + row: row.number, + column: identityCardColumn + }); } - const englishName = canonicalTextCell(row.getCell(3)).replace(/,/g, ','); + const documentTypeColumn = sourceColumns['证件类型']; + if (documentTypeColumn !== undefined) { + const documentType = canonicalTextCell(row.getCell(documentTypeColumn)); + if (documentType !== '' && documentType !== '护照') { + fail('roster_workbook_document_type_not_supported', { + row: row.number, + column: documentTypeColumn + }); + } + } + + const issueDateCell = cell('签发日期'); + const expiryCell = cell('有效期'); + const englishName = canonicalTextCell(cell('英文姓名')).replace(/,/g, ','); return [ String(sequence), - canonicalTextCell(row.getCell(2)), + canonicalTextCell(cell('姓名')), englishName, - canonicalTextCell(row.getCell(4)), - canonicalDate(row.getCell(6), date1904), - canonicalTextCell(row.getCell(8)), + canonicalTextCell(cell('性别')), + canonicalDate(cell('出生日期'), date1904), + canonicalTextCell(cell('出生地')), '护照', - canonicalIdentifier(row.getCell(9)), - canonicalTextCell(row.getCell(10)), - canonicalDate(row.getCell(11), date1904), - canonicalDate(row.getCell(12), date1904, row.getCell(12).type === ExcelJS.ValueType.Formula), - canonicalIdentifier(row.getCell(13)), - canonicalTextCell(row.getCell(14)) + canonicalIdentifier(cell('护照号码')), + canonicalTextCell(cell('签发地')), + canonicalDate(issueDateCell, date1904, issueDateCell.type === ExcelJS.ValueType.Formula), + canonicalDate(expiryCell, date1904, expiryCell.type === ExcelJS.ValueType.Formula), + canonicalIdentifier(cell('电话')), + canonicalTextCell(cell('备注')) ]; } @@ -584,12 +699,13 @@ export async function normalizePassengerRosterWorkbook( if (workbook.worksheets.length !== 1) fail('roster_workbook_ambiguous_sheet'); const sheet = workbook.worksheets[0]; if (sheet.state !== 'visible') fail('roster_workbook_hidden_data'); - const headerRow = findHeaderRow(sheet); - validateFormulasAndHiddenData(sheet, headerRow); - const rows = dataRows(sheet, headerRow, maxDataRows); + const header = findHeaderLayout(sheet); + const headerRow = header.row; + validateFormulasAndHiddenData(sheet, headerRow, header.columns); + const rows = dataRows(sheet, headerRow, header.columns, maxDataRows); const date1904 = workbook.properties.date1904 === true; const seenSequences = new Set(); - const canonicalRows = rows.map((row) => canonicalRow(row, date1904, seenSequences)); + const canonicalRows = rows.map((row) => canonicalRow(row, date1904, seenSequences, header.columns)); return { canonicalTsv: [CANONICAL_PASSENGER_HEADERS, ...canonicalRows] .map((row) => row.join('\t')) diff --git a/control-plane/test/passenger-roster-workbook.test.ts b/control-plane/test/passenger-roster-workbook.test.ts index 03be723..1399831 100644 --- a/control-plane/test/passenger-roster-workbook.test.ts +++ b/control-plane/test/passenger-roster-workbook.test.ts @@ -12,7 +12,14 @@ const SOURCE_HEADERS = [ '护照号码', '签发地', '签发日期', '有效期', '电话', '备注' ] as const; +const ERP_HEADERS = [ + '序号', '姓名', 'NAME', '性别', '出生日期', '出生地', '证件类型', '证件号码', + '签发地', '签发日', '有效期', '电话', '备注' +] as const; + const CANONICAL_HEADER = '序号\t姓名\tNAME\t性别\t出生日期\t出生地\t证件类型\t证件号码\t签发地\t签发日\t有效期\t电话\t备注'; +const REORDERED_SOURCE_INDEXES = [8, 1, 13, 0, 10, 4, 6, 12, 3, 11, 5, 9, 2, 7]; +const ATTACHMENT_VARIANT_INDEXES = [0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 4]; type WorkbookMutation = (workbook: ExcelJS.Workbook) => void | Promise; @@ -57,6 +64,29 @@ async function syntheticWorkbook(...mutations: WorkbookMutation[]): Promise { + const workbook = new ExcelJS.Workbook(); + const sheet = workbook.addWorksheet('ERP名单'); + sheet.addRow([...ERP_HEADERS]); + sheet.addRow([ + 1, + '测试甲', + 'ALPHA,TEST', + '男', + '1990/1/2', + '测试地一', + '护照', + 123456789, + '测试签发地', + new Date(Date.UTC(2025, 0, 2)), + new Date(Date.UTC(2035, 0, 1)), + 13800000000, + '领队\t备注' + ]); + for (const mutation of mutations) await mutation(workbook); + return Buffer.from(await workbook.xlsx.writeBuffer()); +} + async function expectRosterError( work: () => Promise, code: string, @@ -92,7 +122,7 @@ test('XLSX is deterministically normalized to the canonical 13-column TSV', asyn ].join('\n')); }); -test('row 1 is ignored as group metadata and the exact roster header must be on row 2', async () => { +test('the ERP field row is auto-detected and source columns may be reordered', async () => { const valid = await syntheticWorkbook((workbook) => { workbook.getWorksheet('Sheet1')!.getCell('N1').value = '任意团信息'; }); @@ -104,17 +134,142 @@ test('row 1 is ignored as group metadata and the exact roster header must be on assert.equal(normalized.headerRow, 2); assert.equal(normalized.rowCount, 2); + const reordered = await syntheticWorkbook((workbook) => { + const sheet = workbook.getWorksheet('Sheet1')!; + for (const rowNumber of [2, 3, 4]) { + const row = sheet.getRow(rowNumber); + const values = REORDERED_SOURCE_INDEXES.map((index) => row.getCell(index + 1).value); + row.values = values; + } + sheet.getCell('G3').value = { + formula: 'DATEDIF(K3,TODAY(),"Y")', + result: 36 + }; + sheet.getCell('J3').value = { + formula: 'EDATE(E3,10*12)-1', + result: new Date(Date.UTC(2035, 0, 1)) + }; + sheet.getCell('G4').value = { + formula: 'DATEDIF(K4,TODAY(),"Y")', + result: 26 + }; + sheet.getCell('J4').value = { + formula: 'EDATE(E4,10*12)-1', + result: 49310 + }; + }); + const reorderedResult = await normalizePassengerRosterWorkbook({ + content: reordered, + fileName: 'reordered-roster.xlsx', + contentType: 'application/octet-stream' + }); + assert.equal(reorderedResult.headerRow, 2); + assert.equal(reorderedResult.canonicalTsv, [ + CANONICAL_HEADER, + '1\t测试甲\tALPHA,TEST\t男\t1990-01-02\t测试地一\t护照\t123456789\t测试签发地\t2025-01-02\t2035-01-01\t13800000000\t领队 备注', + '2\t测试乙\tBETA,TEST\t女\t2000-01-02\t测试地二\t护照\tP00000002\t测试签发地\t2025-01-02\t2035-01-01\t\t' + ].join('\n')); + const shifted = await syntheticWorkbook((workbook) => { - workbook.getWorksheet('Sheet1')!.insertRow(2, ['额外说明']); + const sheet = workbook.getWorksheet('Sheet1')!; + sheet.insertRow(2, ['额外说明']); + sheet.getCell('G4').value = { + formula: 'DATEDIF(F4,TODAY(),"Y")', + result: 36 + }; + sheet.getCell('L4').value = { + formula: 'EDATE(K4,10*12)-1', + result: new Date(Date.UTC(2035, 0, 1)) + }; + sheet.getCell('G5').value = { + formula: 'DATEDIF(F5,TODAY(),"Y")', + result: 26 + }; + sheet.getCell('L5').value = { + formula: 'EDATE(K5,10*12)-1', + result: 49310 + }; + }); + const shiftedResult = await normalizePassengerRosterWorkbook({ + content: shifted, + fileName: 'synthetic.xlsx', + contentType: 'application/octet-stream' + }); + assert.equal(shiftedResult.headerRow, 3); + assert.equal(shiftedResult.rowCount, 2); + + const erpHeaderOnFirstRow = await normalizePassengerRosterWorkbook({ + content: await erpHeaderWorkbook(), + fileName: 'erp-roster.xlsx', + contentType: 'application/octet-stream' + }); + assert.equal(erpHeaderOnFirstRow.headerRow, 1); + assert.equal(erpHeaderOnFirstRow.canonicalTsv, [ + CANONICAL_HEADER, + '1\t测试甲\tALPHA,TEST\t男\t1990-01-02\t测试地一\t护照\t123456789\t测试签发地\t2025-01-02\t2035-01-01\t13800000000\t领队 备注' + ].join('\n')); +}); + +test('the approved attachment aliases and reverse issue-date formula are normalized', async () => { + const attachmentVariant = await syntheticWorkbook((workbook) => { + const sheet = workbook.getWorksheet('Sheet1')!; + for (const rowNumber of [2, 3, 4]) { + const row = sheet.getRow(rowNumber); + const values = ATTACHMENT_VARIANT_INDEXES.map((index) => row.getCell(index + 1).value); + row.values = values; + } + sheet.getCell('N2').value = '身份证'; + sheet.getCell('F3').value = 36; + sheet.getCell('J3').value = { + formula: 'EDATE(K3,-10*12)+1', + result: new Date(Date.UTC(2025, 0, 2)) + }; + sheet.getCell('K3').value = new Date(Date.UTC(2035, 0, 1)); + sheet.getCell('F4').value = 26; + sheet.getCell('J4').value = { + formula: 'EDATE(K4,-10*12)+1', + result: 45659 + }; + sheet.getCell('K4').value = 49310; + }); + + const result = await normalizePassengerRosterWorkbook({ + content: attachmentVariant, + fileName: 'attachment-variant.xlsx', + contentType: 'application/octet-stream' + }); + assert.equal(result.headerRow, 2); + assert.equal(result.canonicalTsv, [ + CANONICAL_HEADER, + '1\t测试甲\tALPHA,TEST\t男\t1990-01-02\t测试地一\t护照\t123456789\t测试签发地\t2025-01-02\t2035-01-01\t13800000000\t领队 备注', + '2\t测试乙\tBETA,TEST\t女\t2000-01-02\t测试地二\t护照\tP00000002\t测试签发地\t2025-01-02\t2035-01-01\t\t' + ].join('\n')); +}); + +test('source header order may vary, but every required label must appear exactly once', async () => { + const missingHeader = await syntheticWorkbook((workbook) => { + workbook.getWorksheet('Sheet1')!.getCell('N2').value = '备注说明'; }); await expectRosterError( - () => normalizePassengerRosterWorkbook({ - content: shifted, - fileName: 'synthetic.xlsx', - contentType: 'application/octet-stream' - }), - 'roster_workbook_header_row_invalid', - { row: 3 } + () => normalizePassengerRosterWorkbook({ content: missingHeader, fileName: 'synthetic.xlsx', contentType: 'application/octet-stream' }), + 'roster_workbook_header_not_found' + ); + + const duplicateHeader = await syntheticWorkbook((workbook) => { + workbook.getWorksheet('Sheet1')!.getCell('N2').value = '姓名'; + }); + await expectRosterError( + () => normalizePassengerRosterWorkbook({ content: duplicateHeader, fileName: 'synthetic.xlsx', contentType: 'application/octet-stream' }), + 'roster_workbook_header_not_found' + ); + + const unheadedExtraData = await syntheticWorkbook((workbook) => { + workbook.getWorksheet('Sheet1')!.getCell('O3').value = '未识别数据'; + }); + await expectRosterError( + () => normalizePassengerRosterWorkbook({ content: unheadedExtraData, fileName: 'synthetic.xlsx', contentType: 'application/octet-stream' }), + 'roster_workbook_unexpected_column', + { row: 3, column: 15 } ); }); @@ -164,6 +319,21 @@ test('non-empty identity cards fail closed without leaking cell values', async ( assert.doesNotMatch(JSON.stringify(error), new RegExp(secret)); }); +test('canonical ERP document type remains passport-only', async () => { + const unsupportedType = await erpHeaderWorkbook((workbook) => { + workbook.getWorksheet('ERP名单')!.getCell('G2').value = '身份证'; + }); + await expectRosterError( + () => normalizePassengerRosterWorkbook({ + content: unsupportedType, + fileName: 'erp-roster.xlsx', + contentType: 'application/octet-stream' + }), + 'roster_workbook_document_type_not_supported', + { row: 2, column: 7 } + ); +}); + test('sequence numbers must be positive and unique', async () => { const duplicate = await syntheticWorkbook((workbook) => { workbook.getWorksheet('Sheet1')!.getCell('A4').value = 1; @@ -193,7 +363,7 @@ test('sequence numbers must be positive and unique', async () => { ); }); -test('formulas are limited to the row-local age and expiry template formulas', async () => { +test('formulas are limited to the row-local age, issue-date and expiry template formulas', async () => { const networkFormula = await syntheticWorkbook((workbook) => { workbook.getWorksheet('Sheet1')!.getCell('G3').value = { formula: 'WEBSERVICE("https://invalid.example")', @@ -240,6 +410,19 @@ test('formulas are limited to the row-local age and expiry template formulas', a 'roster_workbook_formula_result_missing', { row: 3, column: 12 } ); + + const issueDateWithoutResult = await syntheticWorkbook((workbook) => { + const sheet = workbook.getWorksheet('Sheet1')!; + sheet.getCell('K3').value = { + formula: 'EDATE(L3,-10*12)+1' + }; + sheet.getCell('L3').value = new Date(Date.UTC(2035, 0, 1)); + }); + await expectRosterError( + () => normalizePassengerRosterWorkbook({ content: issueDateWithoutResult, fileName: 'synthetic.xlsx', contentType: 'application/octet-stream' }), + 'roster_workbook_formula_result_missing', + { row: 3, column: 11 } + ); }); test('macros, external relationships and ambiguous sheets fail closed', async () => { diff --git a/dist/lwlt-lifecycle-0.5.125.skill b/dist/lwlt-lifecycle-0.5.125.skill index e38459f4058b154ada4c89bd4bd2fa489b7fb9ad..1ddc9f7a31474e88c332ecf3f43a4a81d774ca3c 100644 GIT binary patch delta 4975 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 delta 4733 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 P2>zUqKM4+4{#O41eptux diff --git a/dist/release-manifest.json b/dist/release-manifest.json index 70234d5..bc363a5 100644 --- a/dist/release-manifest.json +++ b/dist/release-manifest.json @@ -1,10 +1,10 @@ { "manifest_version": 1, - "generated_on": "2026-08-30", + "generated_on": "2026-09-01", "baselines": { "chrome_extension": "0.5.163", "skills": "0.5.125", - "business_instruction_docx": "0.5.123", + "business_instruction_docx": "0.5.125", "agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories" }, "artifacts": [ @@ -37,7 +37,7 @@ "version": "0.5.125", "path": "dist/lwlt-lifecycle-0.5.125.skill", "source": "agent设计规范/skills/lwlt-lifecycle", - "sha256": "8cc5d94d97abbc68388bf60d4b46ce4cb1aa947dfb174831c0ad2afc421002f0" + "sha256": "d964c0a9e482f82757fea25d4713f0c81feb2704486583dfa84f2336ecd930c0" }, { "kind": "skill", @@ -57,12 +57,12 @@ }, { "kind": "business_instruction_docx", - "version": "0.5.123", - "path": "dist/老挝联泰AI指令表-0.5.123.docx", + "version": "0.5.125", + "path": "dist/老挝联泰AI指令表-0.5.125.docx", "source": "agent设计规范/templates/business-input-templates.md", "builder": "tools/build_business_instruction_docx.py", - "sha256": "8d94b1f4e3d1cfccf62c1b7d420cbcebf8c915844ae95fa90840963b32b1cccb", - "source_sha256": "8db16b3dce7e3dd8aa88a6273a9a90bbc3c98a087cb863bf0eeaaa5bb3a371ca", + "sha256": "a0c2fd5f808e3d3f2ac211a652a59f2e435d3824df49c51f763ca4beb7d9b97d", + "source_sha256": "27600fe9bcb0de3898a99550428a9890dd350c701ba704fb3940ca966088dcd9", "builder_sha256": "c162884210da22e3b78368749b66f1f177df5e9fc6155f4fd9954bd516d187f1" } ] diff --git a/dist/老挝联泰AI指令表-0.5.125.docx b/dist/老挝联泰AI指令表-0.5.125.docx new file mode 100644 index 0000000000000000000000000000000000000000..78281fb4790923e8864784feb77b59a8ae6e0472 GIT binary patch literal 31404 zcmbTc1yH5UvL%W;Y~0=5oyOfA8kff1-D#Z0-QC^Y-D%w2-5nnPIrq-YnKv&c;zjJ( zyCP%NT9sK@^~IOD6{JDIP=SDepnz<{iFEYra~xxVfq-ftfq+oHty;piHcrMiPI@Zt zcE*l6^lsKxjfvwneT;~rPaolw>9o{_$9ZMcAQ5~?bV{Z3mGbw)9d@lcU9t*m#n?%8}#mvNMY99-&)0!dbcq3 zZl#PKnsY~*q>nj5xV%=dG-~LMM+vC7=9zS|pMihZkd#g}0{2cqB^Hd?3$X zb$%!g44!+ZZLje-c+0kB(*iN>&2edw<`aJ(h!1i?`sxLh9CZI@R%uy z6opEDTBc~GMg_~fO?@4{U&Bp1CLtYJ$o9PjP^$Eb-|(!4lDDb$C3rw&wb3)oiZn}s z2^6#;K-9|HsQ3IiHKb^2t#m8!glw9=0~q1Wg=_boSJt=s!fr*>y!PRF11tQ;4stDf z>7IP=oGb_s5bXD_o`bQKBLn?E-<5HbQlLzTeph^=BV@&wRy3g!^_>#A9I>xJ!p!YB z>+dl#jozLllx+&q0DP9zm&0^}#q|8_0i1(f!x@W}1}O!`*)~SAhq^8L%S6h^-d$8s zbLYG~_KnHcRPudIWwGS&k{E>s+|)Tx0VWc6T3H#?Q+a}V@v7+`0z!A;puJ_*SA<_C zBQ`^*JmtolD_T^K#Ue{JzqWXUqi1n{fb-3P9FL&Vu858`^3~@n-K0Po(pj_0vuOh< zTTpBEx#urDi22IHW?|AX2ZW{4zj>KxBWr#hg&$(^vtKc3)#&Zm;-u&ZKK*YI*&dq< zJpRsUR4^bQ#P5h0+5S>+u(fk!FtD@xC!sTydu`Sj;W`1!{FrSV4s~SkAfkgwhQ|gv zpaa&^88Q@oMv^h}IV(%iO&F6Ju4(eo(f4yGl40pfi>TsoiahNmbCRPl4Py(dmrLih zuMUkyqB8oHj8nU5nT&NfFQ1vq5Jya;e`*P#ahA?w;`HqC7l5@wQKYoSSm_&LDB(OW z)5K^xoKFiy)1ND7F>v)zR})LM(eBY1tv6a*!Vsbdw#a@sM`opTuY+ z=5^hxRqbZkO5KimF*5zO_mBmjmUBIzxigKZo;gwV8p&Yq3b#RtU4b8g-~DmvxFfhw zdB7n!=0R8d!YH2!gw~0i?luB0aq;I)L#bO8L_XrlfBYJB4PAD&nT$eYT^<#QcE|kTK*yXg8jYxpqru7=KaH zhINBJc6O){pO!7tR7Ca*iNO27rakdxV^P|n0d|@j&jlZK(wwVZH?r9<0h!0JwutG7 z%UEcAwlW|Wly45LeM)1Q_CCV3m17dAeZ^;khc^;SEJcze8F_B;50o|6~x z77vB*GXnOV6-fVUR{XMcF#abk{=`p9<1(QRUwzQ@=|1ktn7Dil*6{q*L(dIp(4y^> znE0_&$}$>bq;pi%P&gxnw5r_OAD|?!bSz4J%wKPFG;J2Qfwi%lJDmLFo$a+XE^TSf zAUi`3h2^+d^;V@W-a{E}xM;nk=bV%|$Q>766H1yP*q#+w!4L${WQC=~s3CWQ5E>+m zg7v})~VFzZXFbrNw=?FpK0b+HV>S7Mz>mNnKa{aEn9NVn9ipC1 zmq*KrNbN^S-$$#ymcfNo?5U=lmF~Lk-!UypDh!`Ji*8zElalJN-scKC0gog>y(@F_ z0i6vDDu^m#CVh>1f?-m|*-bqKIT-ykxWn@SW}al`KakFg#S)`5O?yUFEY)KEo87U1 zQ$CVXF7J<0o52u`$Gmo<0k?hqI=kaifH@_RQspVyW;l^YLI@2l8C)gGV&mo4Xc`D` zd+iI5!5np24Vy;sO^cJ32YkTI--OhM!##@31I^`Y?7OJuvhBHRJN`=6?8u;ggw*-QsQK;?A>UkAs2B<09GW(mvUBnZ@Vpu+aDY z`5`e^n%%dDi|-uwA*$V0*JJ-t?{z%+>u#_gr(bs|km<>-2Lg5Ig9Bf##pW>*3d^x4xU?0s9uuYjjb-so|EQ6he;%?myV9H8Dpu|Gzq0hb=UC4_dZYEaY}?L)i+lzB7%5irdQ z<8UAKCjJAZ%^9~Iw$C|Z?wJ+pz7Yx2sdf5<7MnGg6+7VUXFC<% zG}l6zps7Of9+pj>W9=9#5~j}#T3FYo81FH`s|O0*mz};_0guBUcI4ysDL==769V%> zx2ufc9Kjn46w!12eBCAaN!E=GXlntAXS0e@c#2Ekw7k#BBX34~naMlz`7SQ@6~J)& zuNgkKI(3<6{z_*4NKT5Lxad|JFGt_?GsjK5AZ-8FRRK`z3UI3uko#!(#<9g?{wA^d zwVziFit)>LWMKZTo&C|rThNcwcm!{E!rQZy3hg)bC~eToTLirRimuVu2N(gLaG&`) zz883a=NPeM57wXU~Z$Z#>6KX39>4m=v>{nCm z5Qm!7#s#-^%s8>?n?4nxVx?e7Z&X(3Xbx1lvizp>-%j6n*ij|C$tb%$;o#zAB1Il0KVuT&N&uuE!7|-I z8dRxqPOS2~Ykw8P2MEf0DF|p3OEHx>grqFwbw_r^1%j2#QAR(L23FgX`+GX2sUdvf zg9W_o{COodGKga_bn2cbTllvi~UJBj-Rt@gZ4yXddSL5ZjxRf=rkuIxB z(LhueL~P8+_@T0ySjx`SURL06y9!4Qm8RSgEg$ei+#8-6#5`jepIomh`AcFWpT~d;;JNDW6l@uCMmcN;#tf?fYl?f8SKNN-OSUfGvM7NTDh^PpR9}?IB zPXxf;q22f(7xijQsO-XIUf!J&P!cbdpc5mJt1Iir28yKgV%?RbM`i|FbDh4@N%{ao z05i7ugKF>wn6Bep0Vs4x@BB|JkXXdb+3A_Gg$`G4AEAFtowtRs~aTO;T6>s&yZbo>Z|&h3(^{-;^Zk1sWM7I~$!$U*DAuLVN=fJ^Zoe=LKJ=5 z2%cFW-y5(;*?npRp0>;R)rXi5bqr|2KChvO@J} zNP$$T1}M>hs}U#>yrg9paDh<8cG5#YZ%8y};B1nNnk6*|m#H}2S=J5JtXWZ_G6lnV zF$jHxp1{hJu+I%9KncBx|M|GaRq`n*kt(;8>^f>7OI>BB@P;CR%tSh>cB10zmIMkh z0Y&~t`%Y~bdUe(E3gfujDNw1ps@9~Z|D%)L{igAUUhd(EyMIM@+e|}gY-X+K~=n74S zps zc3G=lVrpz$1o^5VUwfH(0&MDp;?f=icMDIh$ILI23U`(n176*16lMl-mNH5gCKP;aLfA{!}TU}wUOnspq9Z4=RL8~1iprOby&Xk z3U6uvvEJ%met{Ov+|-`}w*4QW1$E?T?kR|utY}`kBu2-08G|c&fkdr8!Qh9ydrg@s zFr$DgSi@|W0`oM}xh&6^O-5#!nPr9(p2kl%@xxH<&gzpGsAZKw48-u(YuDyek3dQr3*-E!0tp?@QJiXxtOXcxI`UaqyC6c=WYlv zec(A4rm4R*{2iwrM=<{`?QdkVzd@IFPL)Nm12c*z)J*DVNm?g@@N_D!oO57t0)GtQ zRbL46CtV-}?|G^P9nV-4)9RtS zHB9i3NC&o2*OS%j=d)RBY@S}QBYzP7n6V^zfq}Yo3*5GS^Xk&iGY<)vw%{ZYpQ7bH zGkJbW5oP%Uq?O+#b})?nz&2M>-NsZ|kR^btkF^A}KsCcs{-LmAgD?yVwUjM#l5ufZr34W* zb6^hp*kq?psl=Ip6pJf>@j`&58VPL6(_l3uJR~?#RX4ihyO?v^yq1Fi!28eT={_Iy$NHKr(}0f_ zILZn%G2fP7=N)~a7;|oN_u{5gCsgDbN;(B0w4Jhh1`JL*pjtozra00y-k;Cbz-Y8+$%<4o#mZgEnxzi^b!SgS@R$6!s5*co+0 z45}P-eb58D1Op+oV^;6#phH7xT`F2{-u z3-Bl_bn%cpbQi{Eyv*t~`3c!ntz>QXdDuK``~FF}6?SsU$Zw;Gncu_u`%uH){|GcGv!(njeU}KnP_Ur#3$us~r_C)T)W42C>*4(*|Hp z+wY^l-gy9UW>Z4A+;fMRwoDTB`zOdmO~1vTc0G_$CP<0JEm$c;`(u483ew(iib`Vi zLRpt2<6l!O4MO{?s^LbhqzYwtDbq%5({L9ut(ZyDQR9Kz9c5EfEQ{dmu+;Hs#Ijw*vJIIZLQ6Oo*fA3>#DYjmVg)k4c*{!KF;1(lm@KNwM^Lb0+VS1Q3Ny_g;IYm}l zE!YhU60hu;zadp9Yxl5VYF0Y%{Bh~X+r1Yu1PvEba$@eEC8G3cBH{G7v07nnxeRyj zsuw*LH|4oJR%N%jN0~_kgfefjGI|6H-*8dHw+kJMuv|f5-)h-PRiY}hsh;A$+Xcap znW-Z~spw1@<=}rREY&PbXxQq}o^hh)rire_*TTXWc`m}Wpv!QGyd;o{(=Moa#ZmR- zXh(V-ylcgzaP9?qK58`6v1;b>!CjLQnsLxoYSxblw)Na#TPBlZQUX`+T({lJd(qj{ zMNF4>O+ZZ4J*{8;f4a$?2bokvo=eC?3{QPufaOWf#AV-~+?U#%nLX zdrE6@DhN^`NWY6H(dB-EvXE96NYGP@Bi~O%6n4s?9901QjNjsI%Be@Yd5;Rk0-&f0 zOS(4`CQ3Q%P*DYQ>i6TK{C=2$8%S7&z@6_}@D6L}rbXYzhP$6JhIBS#s@bYfo|X%z z#EfD<-dc1BW97oUZXda-?j0T}RauW!Z^}KQf(6%nt&$e3Dsi9K2&HU{%Z$MC`f_9m_x2OIm>-(#TPBpSuQC%e=38it{D z>sfbWU~4Ext)13z_{gfSq_$pVK!t21P9;(@r`5^>>Pr9kwHDbr%xHy3j+9xJ*mi$O z(49^CC}sKXFpWAcT>e-^u}i2@*aEMu^xaF0`YIio5Ad+cmnujS0$Bt;TilcuD|LNrlPDU5lnT3!DT1E=is?k4{HsgE_R zo8Mhs7f6SOs$S9mck`*d8yB`Po;{Oq2w@C#-GJ>Bte|L;q4JKf zD3z;H4dSd2W2HkomMCA^CDRE5^wrD*dnyZmw$mSsYLbsa*dHP9!q%w^uxkC(rxiJ~ z5lSXCo9dgm=84L4CTo`Kp7T}DWoQY3Y_3F1sjCPwrui%Y1>=2%rr`h2h^u1wzGHv_ z0d2tlyDRvwh@05jIvG1K{ZoJMOrCT|XZ&uKdcy=q$cF)~EgKk`?zW8XCz=0+RQS!v zDq%?aQf)iuS>;_Mx0Of3n36ym7^X!4r)%E5vGD0P&A?9EsHRdTaxFQ{>+FGkAo3ep zPsT%!ph|v-SK53+I0HtGqLWUwGvcK9&Kc3%+@D#!mzxBIXOSBgQHit&u3R|@kt!kd zo(c&rj8Y0p)RIVepQ|c<%UB7q4E1MQpI1;~XMe0Lk0#{}b^^SepG0I~Q95rwFDA2y zso6T1JDvby?h(7fIJcrg-G_zKoSYX zlghnjj=%K&&zWFuce@HUtFJ` zmFrEr;`~y)sx^n_1fXi*YiR428cM7^JkVHgR~)&&b(K<4diHTOLnnta@QCyPA-9je z07s@%wj>y5OoCt<$KR3ki=0E$K9Z4|2mx`NB}fq<(%|l8EGGsb>=k}mYU2h?&p-1w zC2A)g8}@%StbL5=-t~hxy?2)rD-$>#@(p3hIEV$Tfny-^7i}XoRD!8niiXl+!mB|#NCO1v1PfR25uX;-Pw zE5|9&A7Imzvw^QNYb+ZbsxpGilkt)tt8BOMq;wi}mg1Zv8vv4M0CJQnSyrO0==pl~ zvclZ%RQm)KoX6S`fPq}z=DBR5e;Z4$kDH&LD%tY)7Fg-YLwj-KVDjikoyA^vl!BtN zUK&1=1h4a#v;!p|`s4wJ>g8kaD8fv}`e^I=%IX#JKM#@Yd*h%G8xRl-KR6KJzppJv zCwD7j$A9&(yrtv!B;q!8x&zdblMvV>K?o(9tS7PcQY@66zefm5dM)cag>fLv2Sad(S5^Q|ZWkx^&-ddfi00 zzVBB1zD<9g-X#yuF=$;a!&YtKb$e-dx4xTMjaNo#>^@jJiZOF;>Kj4rJQkLGJ zzEE`WNp!gOKlWSy}W#+WHwEVVQMnFE4@u`3&kxmfTD&S*=>gCTzCR? zJ)Z~rX8x9^H36mV#Lk{tkJ#hoNy|?PiKEdd22))mJh%}DKGm5)kul1GfZ*CtGYn`8;-tQ)c1RIbJ zTOAI7>Mi+x*em*Jmt>#a;`{DA|LpZdfzCf{NU^=ntT(|gcPqED0~j_t!iQk^-apH20iU0UG6}Z2 zvgFBtEzjsrE#J;Zms-7b-e=1z0ATa-Ex+04<=r-81cQN~;&Hlt`^v=bZB`a{X*^qf zQ<>8lV(W5!hJ5QchE@NT&!ZkhH2s&lx zH|4$~FfK%e@bE{y4zj(0ISt3FYgY(d#KE2N$cM+9tnHq!7oXd3AIIf|&u!BiMfFkD zlK+n&5Wwci3*ijZP%&V0h{vrxvG{N80yKQ8zU|YyTXhdd_ImV~k$Zci;e!qtbtyxqr9p|yM%Hu^ z$Xeav#ZWgBN-NuL8&yWt+@q!^M~|B>04UY5)r$z}Fpvm&zr8u><$V{HsLfun_rq`fGdR3zm(8 z6JFx_5<^s;M^yzs{P(0cxa1+NdAAxVm0#-TlzN;niTl^zw*#Lm{6&ibZ_n!~X(_oZ z;;^fY;o{1&WthXk>E8EzAb8PRA3K20PYfkA6@mn)h(!44=_h`V@?GT(j<>A1v6-uE zdBqQ=fVRP{NAdx*5NK%oK_VfeNkZP=pXJ*ID7)eORzewNA(^%td$Sl_LZai!4=w8M zW!cS~FYP01wof6GcWRj+)gX!H*VGEBFi&eo!yAeRDB`!Me0HSr4|mi~sS;TMaoVI; zu8_W!6PU4#s!5^0OPPg9WfI|u4W!@|yn^Ipe%=^!GZ_pnon{141pqM_l)cNCzpCFS zQIN|dW{dWzyl8yG|20T$J$6r*1qxUwOi7VY<-!(tPb=kjAz+P`Z=x6lMAzz)Hz! zbH#PU;W^T}7mjMDq5)u^uEWlt@@cMP6kovz*MSz8% z_T;5iSUF?*=IjOg&n5j&afKY_vR=0fkAgYIf&zFCCbBAPhNcd5a zxSFS%*;qd#5zV(2Hl&jgb50#l-^bw6=&dC`A`#PZ6Z=z0ZGVsy&f`dir0hUbL)k(m zs-?-sc(Q~EdiQTzB3tgzBxMp%U9ke(dX1r?K5T^Oj?APAXm61TnSku1lwVp}q8~FG z352bM8tUqN1!YEQst5A&d<9rZsRp!yay_y_k;%Ze>}0r5dKy{~3tReF&i|O&rZ0pZ z-usW8os9;@)FOB;;J+w=*w|=*jV;9BHoqwwN;MQYm02pFtSr<(=KqVDQkli@y2wN{ z_nUf**?Axhr3gZU(}fG}_aOamvi;?tno%wnq5!(VDzUo46fjaPL+hYl()Rf~%S!7W zoK2`4BvEusj{~)2RR;D}VU`BCTFB|xLrHE$Ly*)_bT=~azodnf`2=H?lj zNZHvU|84)FyhmaBjZgc=Gs}JhQ;ULY$!foWVWt+CTDz(L;-OE8vH$nX1oLm}f3bf9 zv;G_SeZcLoEt`IiuyFW&?p62;Z zd1jP1z2$ISQOBa>qG5f$P>*TP-SF13>iAsWJD?(cZpsDX)}o_mVXpFAx!zA4loXxn zlhqw(w*bS}v=w((_it0{St(O5%_1Q@@R7?qa);xR@}vEke7-Mxr_`=z)^f?F?NA5! z84rgG_uK7jn@>Exje4G))yg5NH(h_GCU<=sxnbyU`hR6s8BRa7!3ghpw2N(FVkS4D z)+#qu9k${}qM7py^KnGLVFq^KexSlTT|w|&zim8dJuwTO_a*t4PWRQc`mncnJt7Y+ z*^)zP`QCOE+Xf(AReeEuE%9uUZEW+MSEHqEM#%uUZzGP-vb)!_+wnW!%stzApj}(D zk`6yp?58xJ3$HU82ND^MDuM{ak1C9LOoGph-)NVZrseEDzg}YZ62@cMaEdbj?&E#1 z)9;o9&nW&8in7?}vT$IhU2*+mzwQc|B(;SXb0zgsW#H(wM8Df!G~+rc)Ma^k(EjoJ ziaOI3{&pkb3IW%W37e(M675D=`nXt)h`;gn{T@7E@uJB)7BWrKjc}Oe{qF0|YrtVB zaD^BZ4LaOSO+Po(7)|JpaBe;vEZ0~Kc$DGE9_6T!Y3dXf)?LQVt9{EQ2O_IOcuNSK z6QDiDbD7RAI+yPH|3|s)|jAeFz$E&Ee39(bEBvEo|Lyz&$YfEI`sAwX8B|b=qrq1h5 z#Mp|}gNogJVvh!d!xVAXxbia$*%`u0okm`6*GO1l=d)8c@0;v(aV7QBMO_>AQA!QFFDVtrA^^Zg;h-DT?+87 z8`n_&?bkE?V_L+oZpW4?zepcr!__6IuCZU6V1qRBN!z@LrFCWK zrpsZ6Ti;iPuQvwYFV{}~k3HY_asGGLt_w)qH7)lh3@q&PVp{o*3^=-rVv^grq8F<1 zClLM*->+AG|Bv~q33-iOq%*_x%+-j_zrmam_H%H{&3C?^)4p%=Uk}lp!1J7exNfv+ zL3}|AU~p^~xh(F287tsO2*{nPr#n}|#*?N{lcoZbrVW$dwMNZt@SKWTncP~LlG=$W zELbggyA_9?)+51KCBHKmWdVX%&@7WP;edUSZfyKxFRriirI^;IsnI@IJ&t?&(|F}| zRla!3i7uHY=;76%^S6}^UFIBi)!+MXvxxgno9Elw&r~MY=JBKI`cYV$y z8pq76^HXU4A<%~~ltyX$tz}t`qH}OGDc+Y@ESm^X^sK;@ztsl)W;X8jUKl=;ba_Gr$a0l~g-C-%to1Ufo!6{_3=3(uaX^ME(fv)3Wh*XGXge7= z)}n*hh%%)(O&!;_;D0SL>LdAXAV#9PTQ zHxBwPZ(DhPmOALDPFp@@On%xGTUpS~Sk5v=boC%T>ppMJ(SwJF<3E=vdG zZUfE!44%fUg748Rv%GEl6&5 zk04e`-Y>lBl4^sJYIBp27+$PmjLSA%s4R`^lWL-a^FoZp44@$&wo&~)FpW==rS~3yk2rJTy9>1cJ z1mX6Qxlv<=XB=Pu6|B#p*YW*=we$xgU}Ogo0aS2!*Q1voxc&6mB?k8zO8_>U3k-$; z;hd)V?8cE=qQOou723nF2l>=&rJ;^&{ylSPsF4t@8d+|l1Wm0$$eT>lZL(KV2 zp}-$h*<;a%{w>uUHA`CG(9C(5`Q*}{dL2i@Ad!IX?0T(w!=uhyd}pA5^@Dq3gZ46+ z-52dVd-3(vbEPQ9i2G|Rs;&eiG`o-JW)otUW!@R^3vI;WA#lKw zs!LpTa1{YLd)UUt{s1p`U+>lh|MdFCYv+l}pr$ly=??F+jbFP zp`fYz+m0=K8=r<7z_4;ouDZ6TuDfC#0a{eUWbgl9zIA{&s7N86BBb{=E~ zK2^2zXKXcB{dAUbfBHoyGm?_7MJ8}Cm2 zds3G=oM)LB?;IeE92Ma6p#NmGi0u}Fn0zKu9Xp||Nt9@7;3r^4T*GT5G0Odld+yWb z95<#8D@Kt|R*X^t_qUUin>{=L+CSXR2s2(SBBSo&fI5=s5bc@hYc=GFbR<=&qQ#Ml z6y*$|mF6sFGWR+G3wtLU9GG*vovYf#ErbEt!PPB>MVLZ5|C~oYiaZZ9n0l#?Tp`V1 zNu4z54?7l|skf_IM%crJPNW$RM*f8~MKq2)WiE%^oIE}llRh*#P@Fl)MU4j5$JH?; zhMPgc8LKXx!QxagYBrW_tULAtzK3hC%UTwGHZaF@N4pkphzproJqey{h&V$yi7aC- zg|*8LeeWP8eW-GvSZ9z+>R(iH&i_y;=t^h&LKs>!8v3THF#@%VVoRSoYWiOnRjxG{ z%a6W;TSvQ-aadsgi_197lcoaH@W2zh3+@AuZ*%jF3GZ%6t8I|91{ zv?O>}RD*-A>O!VnCi<}tpD&+-*;e6XGPKT^b2AMUK{WpQ=qnW#>HgH(pqmEq>f0fk zrdxtwQ0t@RPr9B7ckI59zSa_qg>Cx?^Yp(2!>LP4h-D;D8u95>>DfuwsOs>bNU)hP zk=FD5B9M;iyR5aW2Sa;wW;~v#1X^wH+6m@^k(A4%FE=lk#R^K|!m0a0SryNAlnx9< z>N1Jf{2BA80+D(@@VG;f{CP$mE^quCwhmpRO5jO~RFcok-8$PK06plMylPX#5tAvUZ?)_g$BfOBqAr6X`72Tx zBApn4H0ly=1=$xWj1>uWN%L1Q66#z#$JG{TT`6qHmA2HD*HFoM6z@rAC=^vyNl_Ft zmfV6@yKMjATaE1>HLZWt>i$t{=R4j=2LDIxQR!c5E0sC_XSLqYOA`|Ah@J0R4*X!S z*i#LWb^mlsss&V`$|*pq&>~C^Fy-UQ9V`?@v-O3z`IN36eHs1Mzh<5js1S}(aCkE7 zx~E6akR7ze|GRsn)4}=2AYayt<=3t@c&{Eyze$ecdMI}3!p3$1Hv8WZ${ch#9cU2z z*}p@^vFCE>Z??X>vy}VcO2QWMYR!{C#-r~1@2#VMI8-Bj)nubJZbQkhNvuC62>C6G zv#H$SxiO7YT4pNWxPPAJRS6+0DUvamxAy2;ntcCFr5=DMEb;U8FIjLn(-(7liz0C3 ztC|ug>0Mw|At`l`;+ z;IF!IMkVUE=bDVfnm|_(6uxg*Y0KGe$*5$8^MU_W%>8@sxMaQLL`NeX^$(XeSABiX zJALhzh}wa(liWA1S92Xw7XZG0gQc^)JfjX%1L>&)hXoc=dh_+ArnUv`%IEz0%`}K2 z?gGc-bsD!teE&|nURCjV|2WSEUA`$>U%KAEPvq__!L)n9{Ufea%sMQ85eyP-5pIM= zcSHQNxYP-|REC=LrzY>V4B{{FkE;;=b)YC;HTa-Pv%h7#sfAQ3SXpo&H`RBF?++Io zy)W;rR;=ldM}5DRRb)R?6dC9EO>jj(df);B9BO7(kR=LX{a zeq(XuBY^dAPaXRUWP&R5%@`;Fnqr_}N5F^J>+SFb(c-JI!t9(Tew&d@-|y{k55dXZ z>+Ky4T@G`3=B%LlVYd|*{I(4m&kglNG3CaMg33s1Y!1W%b6UVOz?qUijM%?DMFj~E zvCCfX2ENo&Ym#v$mVYd{VqT{ZxAn_MSzmezjW0vLJ9Zus`>^w+gn#M{;dMg1j2^oC z?Ax6-6aE*Qg)D_3?nIj0x0j7?+x}3O{|cG3R(tk&UfOx}nsJMK)FzNPMN74<=VRG+ zmo9wYS;$)x>pCPy*(Kq+D|!8=1Y`cnznoNQr~Y2qr)sEy-DK7Oj94O_GTwGvCvZF5 z)95VD_A%JggEgJj0$7o{<6PTL7t@&V+ctdLQh%)$)NRnPvbHV2y-oD|1WG$^9$&t& znULBjLPbbgl$?!Mrd_g+$*@}$dDEiyL|M_9ofk*tG)6{M(OdcFnM@I<=2;!u61}w| z?J*+U2Wu^62tQW6(0e*!hx-oO7*o5=Q~TZFri<1wuK!{WY|1X*7V7Z$HrRy);jb5K zC3vTFl{{mSaY`4S<+8ZYRO&qxCd-V`gkc)RdLHN2Cu z7O|1rXe=;8oc>IiyODDKLuG%l3A$Bnw)%Y6(Pa(~7J@);voj|HG{~t}- zAYgq-DG|9q`+x*M3q%wf7MUnP!)eYvlZI!=bQ9pKG;-k;ax*Q(Gf*6b z173Kt6zJoUC)TV|6bTcPSv4ovp3#m|;_6&%lMZXz zy013rG`=lmi_QcWEhshe4U#{ljP9C%+|ycvUHN8GqnRh-(H8? z|2 zZlsY858d6}jY#)@^xp6DUgrB@h8bpfR&w^c_S)}$mc2a@(ZG6TvQ!<7L|1&^t7+}3 zcwj6#aM3q7P@3dF8Tt+v`jQ+lTBfEHTrG)U$H@{ z_!S!!9ew0wWwuVPF}JYv=$=?Br1#S%FIc+Oy-hyAv-- zoBk^NKq1$cQxcC;L8fcM|~WdL*qA3 zKMd1UYb#w6_NGHm>q?=R7dLaZm`E(+ar;8G*c^nq%ux$bDf3=3AJ#j{P>Mqf=26(O zmiw;ANI&F*KZqLU67qRX*WJH@)})=YULq!hoOak`7E6(V!Pf3FVcTFqtG6oIp)+G$ z`!J@MWXJWT*xRUiC%oVT^y|Wy4L0s!Xe4JR@C`QPH8wQ7q;>AHughc9KNJPe17RF+ ziz4x&SO>P)I@U;Kb3uP!siVZM$6W}%>z~;40muz_jNoV6W!t6;;fg>n?6;^l%t|Xq zEwlhDj4|H=8#Qoo34ZUJgpfMU7BczXWyw8VfwB-B2Fn3Af-556IJM+qJQU^-06X3u6g z3pX-TL&&6gmj$`?tn#qhh(*V3R{q;nds!uKGOl56z%2r)ELpdOW9k{sp6Q1!3)~J1 za@>z3Gjg+2?~uOPw>_8!B5$zC6L1om#W#I{!PM@uVB28(9j?+J zqk|C(DK%6FTpHQ9$@HoBoKzIDlVGxB<2R_8z=MN=5C}#LHai>CLf?aiGp<}2SxBU} z8*I1^xbTbYD(4AKxc2Q1ssZGIt>685RSG=^;LyinRuOGnnq$T8!g znCg!|f=5lh5@lw%?_|;GtTD?o3U%b_BV?@!DzYjz0w3g0m^7f^dDt;5@M(s7PexY) zcrxD$p=U}UDW~Rf?{XJ)$W9P)oCFKR5^|KBRYC@GA`E0!!1^COgr49obc*n#0`8p* zyx3HV(OD5FQw)0q^OS~|iUK|p!6hrMLl(=FbbBV%ag}cj356ycnoN$<6L(DLs+w7< zc$QXfSxQi<+21#zP|h~8rba+{X(D%CMC`!?U_(#8MPWuMP(U}tR(9-u1ts(Khp#f_ ztRcWzGhN%kG<%D74uXjQU}M2p%GyZVs-o`5P7z1f5TytkcQ}+j+Uqy0QlNhqM5Q)F zoxJrK2R25V8JHDI_la2FG4NF$TO;Pd&e+D*XI4ZCwI}J|ZbRk_X)le8ewF?5@3Ey6 zDcYUL?|726e^&+iM$$|l$vpW<#01%)MZ*0pV1uw>612}C!Q<#Y5a*k$3oU-u`pc++ z%jy(XJUC3zvzD@vtrF1k)$c+L62?2oo2)Iq>EUf~26UrNZhL*I{W>>mrIEC!v&Bgi z$=_@DGa=mF2nJvD#FnPHw^mpj-3WcW_AMhzaM*h6^H1Mhle#u170|M_WhqUm_5-sQ zg;LFo zY+RuFgSNnd9Va0b2E7xEn=?Z4rje9+{+${%X~OP+$hM7SaR+(EsrCSiUY(xOPy4i7gfn)6=-ca(!=y;t3yP zGA00(DVjC8jM*zg7j{XpPce$)kqY%~hjxx&hVMP@DUdHdNsF10Zg7RIcFcZ6{<%f_@s;}H${STsszioo1@!Y#>6D%qT(QGiyw+n&;v!0XYiX$U#kgew#=8BN!6D{{lntWd`UDR zg~hh>ZbMg(UFmL{gTw^HWnc!C8V#VNjYx`s%>o;h;?r^Xb-YMist;5sW!)H@NL>6J z#S(4H+H7FMq-O+M@sqA(w}qktwNGN0UYJJHOqy7nW0L~To~^P-9PazLo{x*4kKLq2 zQUiZ!V+Q#~??)*qAv#WO(fB?!o=$B)r}=~_tzM|Fsa7F1?P=xYsVysl#+0w$AAH> zu=%ujMvQNqlB#c=?G!V$^Vy2;2m>#V0M!|x7fNWLV4#7JVq~ZQMZ=_S=SF9lTZ`6_ zR>`_+MwLp=Vq*07gWab9TAf2ct|T{sA+mF@Iw%l|2`fu8+PM{U*YIz%kZg5?O@9c_1dHJl1z=OlMJU9aaEOp|2#5 z*woHY8%I?bLK>=q>MK6 z`Qp;ICDu>#a9=%1-u$2iLc~i@ZSswr0@cKU4AjrE1&eC5dCgAJKtdE+zt$05TKyb3 zuPjIGZjW`YQxWd6L{O?SAa6*yOgA)%cbPoZZ44b)_)Ff!+5X zTYUx$r6K@Xde_5_;nAxXQRp`!ISt%pT+&!bw~0^W1W{R0kz_ zn)Ek!#4#~?(aAfeJTO3lH~t9T00}nwOYmnSkl^lLg1=`NUGP9gd? z=#^>Qz_!WDlc?aIXjZ^4-F#rvsdP9e3;S$@fTn*CpJR+9?;xXhYA2<;AAYXnP*&W{ zyzP|Lru)8|Ilrd>q-IiX9k*1MN-x%KS~g`-4PWVCvvMPuLJc4C%dTWM^XH52F)N`P z>I{XZ2YwN0L>kKYN)sx?4jeixFhyJi71x{;70x}k#auH}8_7Sp<@FdrZYUc4(zIV# zglFNp!hEZG9N+j6q>mqkE)seBv(cJTdNG$+j4sz-CYKri$7I@tw-j>ZfF3Z6-iynp zf~|xRE&+_*R84#(e%OOUXpm}4YuZS5_DA#+?t)pG>%U@*(fe172QhlT27gro#W({L zvoIr2hZCN5b165zn5#g=SiwO?5JDzHI3paKHgf;)5ztSU;qVD0t&XeU1IpAaBFLMm zIc0n+%R8DQv6@@uO0bw-f+DUNp;b#S2btn1;m<}VroguZS`EQ+t!__eQ1l@Ychl%y zZ{Zv4f2t}e(J5oc@D1XqfGfbL*bkhNB3YfRX=zHN;WzieU$b5iJ|8{$ zH($vYzZ1k5gokXSoW@m_b{bmS2~pz6e<&{UOC!REv(o2QIomTYqV$Z< zBz_+Ls6;h|_`*^mOKmT^FhI9+Pvs7uT&N;IcQHiQ23U!tgbEg*YqiH_n^JGlj~6gz zwRt>FaG{yN!t1L$P5UN57XW{O@Gc>Ij1Ie1ib(nWUc#6a-gs$UEi6nsWLxL}0~|I`F@g2czv_EX<~j(S2AhwJsI-8y@jwZyf~%{(i46$fd%+iaq6j z8g2tJ+@?ix4`angp&sj1wwh3djYvgY4EsSCgwL4O<(`Yh!a09?&UbdW?Ae~hT;9Dp zS`$}@))Ey;sMe-5C~sVY#DmWk;MGovg!ym6@K{8n*C_i*>^%C%%vLWM$d!zbiO=>hRlB)@+D;8WFC;xUh-^CAM zbBOvV(lneH>eH1;2uLzVqEuGYMvQ~&)_j|G;TsfPJoAI!Qz8oX;+8&($MRt~Sin7pMWR)}ir*h!N5GI4roxG={MEg65 z)zmarttsQA=ugafUw-R=*;muo7rOhUW8%x`?j!W`1-7nG*_-{*^m+YGD)|ASpsBnok6Fo%dPK^rG^)UClY6Z=Iy*3MY7S2rs@Wlq{_ zZg7Zj;UmAuI!6il6+|*naR4yf{IA4~SQ`xA@Gg14SIJ&&4DBd&GME3(+oYg7c>&`( z3AX%zjA~G5M39eIYp|&Ay(#aP1I3i3t7Mu`p|L#=a-~#MkUVp_wYlq6N}5} zB56X~yRhMu#F?;k9$N^Z`jt6NPWl}!L{AbU$_00m5X4~;egf5;mZr;RjrX4H;)M2B9}Qx&i*f8}ZV3+~ciA|?TzzgwcYS}L0)-5cMo z;-WE~2!bMq{elL})RkFkZg$7(HEh@tGrlV=u%(VsNg;%dSD`7jj8)0#onVzV!JZwrYTiVWhV`a|4-kbo)T@CRt^SGZnjTOd_+7wky&y96g6^hn# zK!1#BvFdUaP}dCbNXwMdews1c7Y7t8T@+y@GyBWU?$PKR1sVQi6ozek2F;h z-(##$hs2`<&CkDS4Gfwvhnz6465Y9^nD{vwG|QNf_&kJ5p)^Uz^9HBaT9~JN%^-nd zYTq2w{+$J58EZah2P&_~P98_=4vS1N4MRR{Xf<>wU9wH2QKqNJ@SMUfRq|%A7QpYf zX6tV0tO$Zn6MR#myPrkkFl1wqh$(; z?65{w-@O?-b&{6%@-#bvC0>P^cjNk=nT9Y@CG{XvPc#2E_2X|-$@nk+W2(i1Qy=Dr z7}Ycaq3ZfNkI5JH+}(Aajl8?9zh2KLX^Pq1w0OqW!JrIsFVQ(tBYIGZhMKtLxXE#Q z3Q9A)4|TrnKhJD~-$~%LJz=|JnLj^I68QRc1E{056z*w%)jY|J{!#n(6PN-@Tl4Y9 z_H(uSy4m}?kJNVtxW?R1YPzU8lewoq+jR)IzpS>(RQLD0m?D)U77ILiK2B9{oT)AG z=^aNF0?!Y&PTj^81TE;g+eW#hi@rqI^dy}=>g)ltPgYxSGlwtMPWWNk@d(lR#ug!e7~}dC9xam!v88@1a$X?3gzTg5U)1{_v==~f$7O47Ez%HMYuO>B*ALAMrvU$~l1><3Yqq6VEp-=X-OpinSYlXbyQHVK0O^<5-G*NSrngvk;t^D86OwUlwgwxD-KjOu7#UhZ1w1(_Qxz) zV`rN)y7(0cVx!ARNee}Qd%LM0Ri4q|b0g(a;GPN|`8bIvmNC4m4gX=?2ceUrLc~S@ z?uR1_LdwncQoqCeHqz$rN35huHvMm zE!U49#o&{K#H7w^CJL;o7?3gbO$5Qn^rfk9*zpscxif*Kw&%D`gDh zF8H)t#riC&x@kD}y*5nO1m)lC5*RjivPiBp$$08cU?TtM#0s-TE`8=-QRTA7bpqK{QebxDiov-@SW zSNF-qQp}#-HzMB^{(5of_re%6b5h5`Npyvh>T@)xBSxH8a2cJ}Z7s;&9lZsIfpjxX zD<+ptiZ{f7%nqi87Ua{Mto^1!ymYgAR>d)It6A(o_#t%bvmV6;2LzoS?UTauooyO7 zrQXH_en%Fal1qQ|86Us^6qkgQ+z6BkN70#bQ|Yc|N<2)rcS52P4(VE8XiLLnrhq9O zS4>&=&#PThgv4jJYFXL)xURaU%&i>+U|?wfxp-*nVq<9HXklyi z>+a!j%BpQM2Z+A)8l8F~z7!J)3hpjnApiDUtI#5K>I59$9#?shcu_L7*&?bsY3}|A z`U;_jgtB&@#gH~)rD2Yrh-hSXSHCZ*q5?{bx63_ovLAJcM;LLOT#ogQ59jHwwk0$O z6Dzs^br&5F1ltv?ciMJe9kriWh8lNHADYFtg4J(a`8p@WCXEVDgt(nTE5eGfmbq;#A--ac4y?Vk&gi7X9CJqUY?{V*)Px^hVN?8e4X z$9wo&xISA1O>6kuIuQug+tWCGoDh-~BNAk+6R@0E$uUb#isoWJMb@vuZ4= zLl8m(K(S>rcB28~^@DXWfn-oRUX@yGiCu@X-h5Kv^`jeQeB(PQ1PCl*SZMg+3OI(3 z;k~J?R4~Wwgk}hff@v`GAr@pH!c2WlfaB^oVB6iVS&*;;#ZD!ShtFz;>*MT&@%^%v zs6=508Bfc`)kax4@L6{vP^3bU>92X&b<@< zAqr4HADFH_Lm9BCqksNB-=Ruo94318W`X3vnL0W%_XIY#x;d3n!uN7*2y(=5r+FG~ zrPx=7!?S7v=N*o6&6a_y}rs zt@DnnC;I3fuHR!_$^*Yt1TP9k1y&IbdaZ%zWEZU7rSIhsTHc&r{@~fGVw(YmeISAY zk%E?vwUP(pK7L^T;KWXAVBrRW6;zG$Q9m-#lG?es~U><~Qv%O;PGpS@VeP z&mGzHEpyaPl{JxBbv9Nm`dA;tk|U0Nm~~v_U;ou(`y6v}`vdb~P5m;09#JhakNIJW z+Bmm5ebPDqu1z870;~xTWk9TFU?jt0ZAFcuqGGf+PMt`yR7~Lc7TZPM$hs_u7X^|S zdfy?_1ugfL2M31&2e!v`u8HweCdU_8Upyb(-TA$ZezS{{wauX8ybQtga`gi1*IR}I ztXwz`(ahA$jhApV-`bWr<7dyC>5#+uV*(-}JKQav5V*b!KWL!Be2MGo&gEM3G`Pur z@P51!x7!w=aHPA8n$!xFlODI&JP*73j#;?)^Iq$AWIvFM5g{M*lw(ksfphGeAjXvK z!-ePdtADRjPY2mBd7#w^3dHR2i%s9i#mU*uM%~7m(Zbop<~Pkid88o1cP4a^3dN_D zf+kcNKtAzTkpSujz{Dy?^i~o%hL;zaZs&H30pTS6>Ifzy(s=Z4B?BGxnAJf95jWq- z?lAc05aC>V!6+}(H9`&u(sJacNe74ze(RbN#Re3lcPhb#a3-Ez`=Gfsq zZ}z*h<*e0Iyb8jrgL;T}EdswB3DEjtx_sBoo8DP1Yj)XI(sn$9R@-M8LQ=nfL8s0? zusz)ha)t`z6yd*|`irmPzpniztB4q_vIH`r!mPE*k<^-(n3z##t6!l(%nR9*ltz!% zhvdI$l9pI$%Ta>FVOb$~zIL+lepT8aNEtO=R8W%OvuA(dz;R$U=WUxnT<=gzC&qLD zo_BYW`>xZn(b)VfR78*zr;(}9uDqWCm0l&8IB_t5Tm7w3rc3pSq1jkk)6>LlJ4rK@ z622JOxq)>aHX-~?6d#v)Q2uYrP0^SOw%b_P-qeiIquaW`eVOqj#;a|FTFF||&S`CosFL`UTw zbs06wjq&BMFXNCiIYoJQz2sqZcBCNM8Eue>2cu_`v?Cg0ocGRzptxo^ldso9_~+Z@ z+2ef*3)f3d5%6;1a3dylXC^Y_2kNzgn=snHY=Ve+V@O7vJcJhqOTz=@5$;^@3zVjt45trBX3@wx)k4 zFMYlGJwxPC@3>;18*zOHG%2@g&`H#{l;Re{5cnajNKZ-BzAZmt^r6NK?rnpy`< zc1qWX2i@4uN(Ki-B%+8Rx#&D*CTavX%D6~7DAkYDU|PC2IwpwX1qwPrTxqOy38eIH2$1bNn>dGvGy^k;*3UYWE6GgMovqm&aQgM6wDYu6+?Q z3{FSxam>2!A=rH61G};abUbeqc`Pf(Y1m>8F-x1=_2@MZ)+-0pRnZ@f7GCR-?DcN*Pi5%*l9PpXL{Mt0-7?4;OQ7bNC!4@v7&!X*HmM|d$`T<@+di4a4^Y>DwAQiyc9;rHIN;F!j{03 zc15t<%^FcO8Y!>Kn2;%Y*3p*j3GpGIEVAC{Rt2pLn4qfk*F2Tqdowo^LnRYuXHZG{dwZ5KIcocz7#r-zeQYb_`d8(` z9yXg+dM#ItL~AcFD9g}}S+K8nSGnab?7GHO3$?3^s{+--3Un7_WpVnBd})-}@IW8w zn!&T{*+JuonDZ(7l8zAkF9E=`jWZIf2{CJd!0UCnU_V3c5{*c@)5CbwUfS&9G$96= z8+E_wsbIBcvebpg>`bOH_De^)gbOMbxmXN-)+<8j0Y#QGq@%3BGabGvA?VlPrN+lF z<&EN29X}T!-i!o#)*ieA&=SCu|*W`Ik9K2HR0YAsn@9ttA zQU8LBM%o*>s^h|tNGq!y z-u0;yz4CDm-2tGb+Yx7LR-|!P&#J$#3Qc28fe;qRgMAri^JB#e^DzM(r?F|c`$Zl|G;;#zVZgu^K|2Ewzy4o&a5e|Mgv9v!=kF!@SVP)wkpsW| z)tno#vz^Jj>H(C?=eerIasVE=Z7`ulXyTlF&ie_;4|ofkKH$J6gJ6s~n_vSiU(C7u zd$xHVYl5+g%67s;nzWHzz&DigR~(X$Pp7bww?7krA=w{E$+sOA9pBE-ix@`Tv!N`^ z91A`R4GJl)&-I4)=Gg#3YQL6w-*lR^6bPZchPtTI2HMam(`wY%?02#xVtEJ|x^rxh zen%)Et^DZw%Ezwj$g{lOP&~N@^Rok#wOxeR6q$K+b?r>ykQY*$A;lzgRHzPPT#=Ho zy7#E0_m+rj%KQ7|I)>=YbANDdWlTeLTKBPf%lY2wS;j?**Gkgn>`kApX!Z~wyel%t zL-p0scpi5S@`q!+-|e=C9A0`ZXDa<11e-hWP&5NyJo=VC5?=^25FFOh%>HerY=iwN z0{X$SaMD4I?xqK6RlDPxCEaj@C-iFi`m*RFFW&Nr6!huH4?4t~r~K&zQ;aXZLSmb( z(9XD{EG!hpUGwZ@Ezmz^*uYJxwFbYY^Q#UzGJL;o*n=cSe5=YPW*Hm(`U-5eI~)ZX z0KLZUS4VXlONEuFs3GC$M_FR?Fm}M=H_A(LZ_R`)ARf5lwsJvQvJq97u zQM&1c5$Gn&tU3kOLMVo@hPhh?3>+^wET&&Z+~i_H#4;lI=6}{E>FV~jtatweS+x(gF~lkxlD0U75AG1R0`Nx=}e zZcZ&X`C!&J_ke0WQ6Uw5Q$tDh?g+Ir_4U3Y=E&s87GD)}MMpQ%me0U^KMB|XhpF?> zc*;~QLq&yao>~;Axo>miC8U8lUX)wNLyeJ3TVBhk;-Ly=xWPPEWR0*ipT{}W#3KEDo{$uIH`*BrF2~g|Pp6QjZDILbw=~{Vm&Kx3m{n{c%n;nEp zKRg}hJ}V5afKeq|nzku00*AltzQyAIc;9?ZN!Ysxg|P9_>C|Xv#b{Gy zIZ-Vu$M}TObqGs+DP{Olw8nFye&%X$w{`66gy0AA00WY*euz*bJreep@G9%JA#YGO zjnN=9DpAO#8UjiK>*C#91@iTs>UU%r6MT9s!|rb#h1;zYt*bSNT?8ZIJ1qj)-?nBl zQNd;^rf#O@H}*=^u-geO6Q{RH*{dEjnoFP$g?zbtXU2p)h=&xTG--%#_ys$W{XV@1 z@2tR!l47K{zYdyeF>JbP;{b`Ewp``giDn;AauK)F0zOCq;=y>qf(db>e!F<7BhJcU zWJASCm7Coyr4!?IXtd>)N-q`jxl1#BbZ=-12a6vNtFUpO9)CZL)dI;e-o>$?LHxSg}f^K|t( z+P?Hdw4%m~mY^B#Bkn1yRSO(yaYwcELuW5!gQKDIWFSD3ES6NBLz&9ztP&Q6f96*z zp^JNPlemJ%%bRYFU9iixUjQJ0PGWAe<_ivRVIIHTtWA8#k1(Sw@<<%6C}#g?L#O`n z9foWTxYfpiY1rY(M26X?QovfG?*yaylCrzCD<8oRf)IY%YY(VU_iUATy+$sm46RfL zno{pW^eIiW({nyv%Mn|8hpV#<-R1eOowFxrU+b}?r)Ew_=B?MGsQFu9t2x&_Sxs+0 z%jhS*suwl=5q%$L*k`Cnn#(-FF_Cv;LVa8pOs zeSm|4kXsqTt4O1HlKM3^==WpZ=^xMdI<-}&1uoQ2 z?d{)z(z=oQw^U5|1RXc2vX002`%5*}h-0Gle6jdKb|>EL2{r~2>9Xd9a((y5pPFM; zn+Ka;$#K^ZhDOI@)zCTH=xO9hc!-4^9~~TiO@);vp0E+mFxw*}yC!L=0!!XSLK&b!GHyjCPE>9S z48W|7MDu;JdlSXY>gi?NZqV%YwmP?4>-!{3{u)gT+FNQf%J-Vik*!!F;;XZ3&C7-W z=$*c=YK1ZPNw2vW;r!zHbb6R=xyAH7283${-Tla34a>TJqECL~SJo8r-2s4qC!Y4j zzF)?qd2nxtAflE*)jfFoB;5h{*62bOI}OT+?<3bXc@jaYH#y(dGQmKS!KJYaAN$ZG|^(d zH)kTF{)8bzCP!tT>I-@9pKe0)Qxm<4(T{FL=uI!h4Eol`X%&dk#hO52Afq-bH~<&) z<9f2ZgBx9ptk{O7x)Vd`noEOD*KdL>NC|{ovlM5R55UsGIek%|y9j8@BygGoC zE(?YY6ECFDC%P?Ul96ew!YpJYLDS*b`eG>&eLgJ8jBi&_*vWLR7Ob^|JXG!5O$QzO zLA1-Hu1`(}_tK%pV8bTPIF%aw-4zsm@}%I?8gC?*v#=nmW5u~eIZAk!eS_6q#8B9g z5(NfyPm{xg2-GT~5BkEnnLGG(aBLS;DZ(!B_M7V*KX zSyMtTxNKeH$Q*~$(6&#DVepy^Kz`%KO8>0uCDPuE#nw-xTt^-iU(CZ*WluB2HtnnS zF5fWvhvI$HSl)5upPnrLx;QL_B|>LMpU5Q#)jgAu=`F5F_WBi~CN>|*SXVzy^;a}V zlwf3Hry+GDxk)frve6=Xvg+d5Ba|?k-bKq0T^i?6Iq|p4gP}K&O^)+fUcQ3`V>lRF z9_17aQw+lq+N%8v>KII}>F>uj1Oz_cr8jNP%VD5LHW@5!G=WVw0~)%QF#Yh=vkAs z{FW!h$e7CWYfNLt5GzYX+^yJ-TXjbt1jC|WE*jG`-+O3wX@*t%o|{Z#BHo9+-B`Wr0?-2MN5toKF`%~Auaz|6 zI`l?}R3Uz=>M`o^{MjQOSg6J)oTO|)*&VY<>I>8USEA_V)Bat*<3xV2BzwAAR2Fm zeK{~TaRf*?i6()miY8c?BJMyJ^sUy}Fm_7z>pa;v>@Z~6Dp^hpvy=?OZ~5vt^l@7N zt+)lj!*Xb~mN{YBAjVxGTE5};@YP6^c(<7Li$|=z7>hd+X%0h0DheM)+*&X-+ zBdQ;0YgjUkNZ2p4_tAg{t%T`qYFs=&g&M9~`W(U{$-AYLYEH0~*Ku~}wb;i4(2~mx zl4>o7Q%Ec(-9s_xFcsM?ID&%$Gd+n_iDWU(vT1F^{L1+=hT&*j?$TbjPs6tDtD@JN zr?bkkdx|FQ)orOCbt%qKUGVtiBC3JOEibFL=%_0fTM2JL?ck?(oiW6~i{yN=YY{cJ z$BOwmUEzEpXY9wypEBb6DSbVQ`AsxDId@83%{(kXAzyQKHmJO(w!Q~ zVF6PW%{V?~pl$v*nM0Ast%m5?Hn7jpiqo9azmj^mn&Xcn!XW>R^6pJjN83)MzBsEh z3HF1VqO)f`XT|qeS*L{`RimX`pN)#x7&tSKGP@mGjtgWzaK_47>sfXHM)M_c8i4@L zSv|Y2?aM76y__CBz7+Q47v?M|xejq11>M;J*kcg8ixxKX;eJ}lHx;SP(4-9nV2SZ> zMHNg!RA~fRBPbzXprBs*^FM(`BZ5PqL;n9`U_ph>?=K%vgX4b=g?%aDuu z_)@~lp4cA=-%bCP@UlbpQpC%B&mR#aHh+uw=Pu|a{N=O#5B!7Ue`D~EC%ruA#lT;G Qg9iGJ0ey6#U4Q-de;_vqIsgCw literal 0 HcmV?d00001 diff --git a/mappings/lifecycle.mapping.json b/mappings/lifecycle.mapping.json index 335a0cd..fb49d99 100644 --- a/mappings/lifecycle.mapping.json +++ b/mappings/lifecycle.mapping.json @@ -195,7 +195,7 @@ "full_replace requires confirmed=true and forces a native overwrite of every supplied sequence even when its current persisted projection is equal; the plugin must not downgrade confirmed user intent to no_changes", "passenger_list.marker is exactly TEST-202609 only for an explicit historical test context", "every source row has 证件类型 exactly 护照", - "the workbook header is exactly row 2; row 1 is group metadata and never enters canonical TSV", + "exactly one workbook row within the first 100 rows maps to the required ERP passenger fields; the header may be row 1 or follow metadata rows, source order is arbitrary, approved source/ERP aliases map to one semantic field each, and unknown or unheaded data columns are rejected; contiguous roster data is read below the detected header", "at most one row has trimmed 备注 exactly 领队; that row requires nonblank 姓名 and 电话 and must match passenger_list.leader_contact" ], "native_defects": [ diff --git a/tools/repository-hygiene.test.mjs b/tools/repository-hygiene.test.mjs index ba2ca19..142950d 100644 --- a/tools/repository-hygiene.test.mjs +++ b/tools/repository-hygiene.test.mjs @@ -304,7 +304,7 @@ test('single-source and generated-output boundaries remain explicit', async () = const diagnoseServer = await readFile(path.join(ROOT, 'infra/diagnose-server.sh'), 'utf8'); const gitignore = await readFile(path.join(ROOT, '.gitignore'), 'utf8'); - assert.match(template, /^交付版本:0\.5\.123\s*$/mu); + assert.match(template, /^交付版本:0\.5\.125\s*$/mu); assert.match(template, /^适用范围:当前 18 项运营业务输入;执行能力以生命周期发布门槛为准。\s*$/mu); assert.doesNotMatch(template, /^事实基线:/mu); assert.equal(tsconfig.compilerOptions.outDir, '.build');