From 405f6e01d55aef03d8c22a507e849672220c237b Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Tue, 6 Oct 2026 10:28:22 +0800 Subject: [PATCH] feat: package provider delivery as NavSea zip --- STEP_RECORD.md | 30 ++ docs/PBF_Pipeline_正式流程_v1.md | 51 +++- .../任务/PBF_Provider_Pipeline_v1_实现任务.md | 276 ++++++++++++++++++ ...PBF_Provider_Pipeline_v1_Codex完成报告_v2.md | 265 +++++++++++++++++ ...PBF_Provider_Pipeline_v1_Codex完成报告_v3.md | 141 +++++++++ providers/navsea/manifest.json | 80 ++++- tests/test_provider_pipeline.py | 39 ++- tools/provider-pipeline/README.md | 39 ++- .../run_provider_pipeline.py | 10 +- .../provider_policy.py | 62 ++++ 10 files changed, 965 insertions(+), 28 deletions(-) create mode 100644 docs/任务/PBF_Provider_Pipeline_v1_实现任务.md create mode 100644 docs/报告/PBF_Provider_Pipeline_v1_Codex完成报告_v2.md create mode 100644 docs/报告/PBF_Provider_Pipeline_v1_Codex完成报告_v3.md diff --git a/STEP_RECORD.md b/STEP_RECORD.md index 6f53443..9a1e602 100644 --- a/STEP_RECORD.md +++ b/STEP_RECORD.md @@ -2362,3 +2362,33 @@ - CM95 通过用户指定 Style、Sprite 与三块真实 PBF 样本执行完成,复用权威人工 assignment;2,077 occurrence、102 个实际 Visual Object,Package 校验 `PASS_WITH_WARNINGS` 并导出。警告保留未映射项和样本限制。 - NavSea 的当前本地 Style 可读,但 Sprite 与三块候选 PBF 对 `192.168.200.184:80` 均返回 connection refused;NavSea 端到端未完成,未伪造样本或 PASS。 - 测试、Python 编译、CLI help 与 `git diff --check` 结果记录在本任务完成报告。 + + +## 2026-10-06 PBF Provider Pipeline v1 双 Provider 最终实跑补充 + +- 前一条记录中的 NavSea 阻塞原因已确认:旧 `192.168.200.184:80` LAN 资源端点当前未监听;不是 Pipeline 逻辑或 Codex sandbox 独有问题。 +- 保留 X220 当前本地 `style.navsea-delivery-full-semantic-extentfix.json` 作为 NavSea Style 分析输入,不用公开 Style 替换它;仅在 `providers/navsea/manifest.json` 显式覆盖 PBF / Sprite / Glyph 为当前可用的 `https://pbf.jpcn.ccwu.cc/` 同数据集资源入口。 +- NavSea 已 fresh 重跑成功: + - 三个 z12 样本 `12/3531/1641`、`12/3526/1643`、`12/3638/1613` 全部下载成功; + - 8,484 occurrence、85 个 distinct Actual Visual Object; + - Mapping:VISUAL_OBJECT_MATCH 27、LAYER_ONLY 7、AMBIGUOUS 11、UNKNOWN 40; + - PBF-01~PBF-05 PASS,PBF-06 `PASS_WITH_WARNINGS`,PBF-07 `EXPORTED`。 +- CM95 已清理临时产物后 fresh 重跑成功: + - 三个指定 z12 样本全部成功; + - 2,077 occurrence、102 个 distinct Actual Visual Object; + - Mapping:VISUAL_OBJECT_MATCH 9、LAYER_ONLY 47、AMBIGUOUS 2、UNKNOWN 44; + - PBF-01~PBF-05 PASS,PBF-06 `PASS_WITH_WARNINGS`,PBF-07 `EXPORTED`。 +- 两套 warnings 都来自 `mapping:unresolved_present` 与 `limitations:present`;未将 UNKNOWN/AMBIGUOUS 静默映射为标准 Layer。 +- 最终双 Provider 端到端结果记录在 `docs/报告/PBF_Provider_Pipeline_v1_Codex完成报告_v2.md`。 + +## 2026-10-06 PBF-08 NavSea Delivery ZIP + +- 在 PBF-07 Provider Package Export 后新增 PBF-08。 +- PBF-08 自动生成单文件 `.navsea-provider.zip`,用户交付给 NavSea 时只需要复制该 ZIP,不再手工挑选多个文件。 +- ZIP 使用严格 9 文件白名单:provider-package.json、provider-policy.json、mapping-layer-assignment.json、resources-style.json、resources-sprite.json、resources-sprite.png、validation.json、validation.md、MANIFEST.json。 +- 任一必需文件缺失时禁止生成 ZIP;ZIP 完成后自动核对精确文件列表、CRC 和 SHA-256,并写入 pipeline-report.json。 +- NavSea 实跑生成 `navsea-delivery-full-semantic-extentfix-1.0.0.navsea-provider.zip`,SHA-256 `54aff8d8b882211ced6e77b3a93e8e793c780b44a1faa982ed8fc64693124fd5`,PBF-08 PACKAGED。 +- CM95 实跑生成 `cm95-s52-full-1.0.0.navsea-provider.zip`,SHA-256 `603cc004833bcff5571a32843d8ceedc7858b2fc4b28c41056db6807c7d511ee`,PBF-08 PACKAGED。 +- 两个 ZIP 均经外部 `unzip -t` 验证 9/9 文件 OK。 +- 自动测试从 6 项增加到 8 项,8/8 PASS;新增覆盖严格白名单与缺文件拒绝打包。 +- 详细结果:`docs/报告/PBF_Provider_Pipeline_v1_Codex完成报告_v3.md`。 diff --git a/docs/PBF_Pipeline_正式流程_v1.md b/docs/PBF_Pipeline_正式流程_v1.md index 2e181ea..14e82e6 100644 --- a/docs/PBF_Pipeline_正式流程_v1.md +++ b/docs/PBF_Pipeline_正式流程_v1.md @@ -30,6 +30,8 @@ PBF-05 Provider Policy Compile PBF-06 Provider Package Validate ↓ PBF-07 NavSea Provider Package Export + ↓ +PBF-08 NavSea Delivery ZIP ``` Pipeline 正式终点是 `NavSea Provider Package`,不是 `ProviderExecutionPlan`。 @@ -370,7 +372,46 @@ MANIFEST 至少包含: 只有 PASS 或明确允许发布的 PASS_WITH_WARNINGS 才能正式 export。 -## 10. NavSea Provider Package Contract +## 10. PBF-08 NavSea Delivery ZIP + +目标:把 PBF-07 已验证的最终 NavSea 正式输入打成一个单文件 ZIP,避免人工复制时漏文件、错版本或混入生产中间产物。 + +PBF-08 不改变 Provider Package Contract,只增加交付封装。ZIP 使用严格白名单,只允许包含: + +```text +provider-package.json +provider-policy.json +mapping-layer-assignment.json +resources-style.json +resources-sprite.json +resources-sprite.png +validation.json +validation.md +MANIFEST.json +``` + +默认输出: + +```text +dist/// + --.navsea-provider.zip +``` + +ZIP 内部使用同名顶层目录,避免多个 Provider 包解压后文件互相覆盖。 + +Gate: + +- PBF-07 必须已经成功导出; +- validation 必须为 PASS 或 PASS_WITH_WARNINGS; +- 白名单中的 9 个文件必须全部存在; +- ZIP 生成后必须重新读取并核对精确文件列表; +- ZIP 必须通过 CRC 校验; +- Pipeline 必须计算 ZIP SHA-256 并写入 pipeline-report.json; +- 任何临时 PBF、`.tmp`、工具源码、测试、人工审核中间文件或未声明文件不得进入 ZIP。 + +因此用户实际交付给 NavSea 时,只需要复制这一份 `.navsea-provider.zip`。 + +## 11. NavSea Provider Package Contract 公开 Contract 至少覆盖: @@ -396,7 +437,7 @@ MANIFEST 至少包含: 公开 Contract 不要求第三方使用官方 PBF 工具。 -## 11. NavSea 接收侧 +## 12. NavSea 接收侧 ```text NavSea Provider Package @@ -418,7 +459,7 @@ NavSea Runtime NavSea 不再负责 CM95/S-52 语义研究、Visual Object 人工分类、Provider-specific 图标研究或 Provider Policy 推理生成。 -## 12. 当前工具迁移关系 +## 13. 当前工具迁移关系 `tools/style-visual-object/style_vo_tool.py` → PBF-02。 @@ -432,7 +473,7 @@ NavSea 不再负责 CM95/S-52 语义研究、Visual Object 人工分类、Provid `validate_cm95_classification.py` 暂保留为 CM95 基线校验;后续增加 Provider-neutral validator。 -## 13. 实施顺序 +## 14. 实施顺序 第一优先级:冻结 Provider Policy Schema v1 和 Provider Package Schema v1。 @@ -442,7 +483,7 @@ NavSea 不再负责 CM95/S-52 语义研究、Visual Object 人工分类、Provid 第四优先级:调整 NavSea `help/chart-profile-tools/`,收敛为读取公开 Provider Package → Validate → Profile → ProviderCompiler。 -## 14. 核心原则 +## 15. 核心原则 ```text PBF = 数据生产者 diff --git a/docs/任务/PBF_Provider_Pipeline_v1_实现任务.md b/docs/任务/PBF_Provider_Pipeline_v1_实现任务.md new file mode 100644 index 0000000..82afcb8 --- /dev/null +++ b/docs/任务/PBF_Provider_Pipeline_v1_实现任务.md @@ -0,0 +1,276 @@ +# PBF Provider Pipeline v1 实现任务 + +日期:2026-10-06 + +## 0. 任务目标 + +在当前独立 worktree `/root/sourceserver/pbf-provider-pipeline-v1` 中,把 `docs/PBF_Pipeline_正式流程_v1.md` 定义的 PBF-01 ~ PBF-07 落成可运行、Provider-neutral 的正式 Pipeline,并实际用 NavSea 与 CM95 两套 Style/资源跑通。 + +最终终点必须是: + +```text +NavSea Provider Package +``` + +不得生成 NavSea Runtime 的 `ProviderExecutionPlan`。 + +## 1. 开工前必须读取 + +先完整阅读: + +- `AGENTS.md` +- `docs/PBF_Pipeline_正式流程_v1.md` +- `STEP_RECORD.md` +- `tools/style-visual-object/README.md` +- `tools/style-visual-object/style_vo_tool.py` +- `tools/style-visual-object/output/navsea-standard/standard-catalog.json` +- `tools/style-visual-object/output/cm95-manual-review/assignment/cm95-layer-assignment-v1.json` +- `tools/style-visual-object/output/cm95-manual-review/validation-report-v1.json` +- `tools/visual-object-instantiation/README.md` +- `tools/visual-object-instantiation/instantiate_pbf_visual_objects.py` +- `tools/visual-object-matcher/README.md` +- `tools/visual-object-matcher/visual_matcher_experimental.py` + +先按真实文件结构和字段工作,不凭记忆臆造。 + +## 2. 必做项 + +- [ ] PBF-01:实现 Provider Source Intake,支持本地路径和 HTTP(S) Style;记录 provider/dataset identity、资源、hash/fingerprint、source snapshot。 +- [ ] PBF-02:复用现有 Style Visual Object Tool;必要时补薄包装,但不得破坏已冻结 NavSea Standard Catalog。 +- [ ] PBF-03:把 `visual-object-instantiation` 正规化并 Provider-neutral 化。删除主逻辑中的 CM95 硬编码,包括固定 `source: cm95`、固定 catalog_name 等;Provider 信息必须来自参数/manifest。支持对选定 XYZ sample tiles 下载到工作目录后实例化。不得按屏幕位置拼接对象。 +- [ ] PBF-04:提供统一 Mapping ingestion/normalization。CM95 读取现有人工 assignment;NavSea 从 frozen Standard Catalog 的真实结构生成自身 Provider mapping。自动 matcher 只能作为建议,不能覆盖人工结果。 +- [ ] PBF-05:新增 `tools/provider-policy-compiler/`,定义并实现统一 Provider Policy Schema v1、编译器、validator。NavSea 与 CM95 必须使用完全相同 Schema;主逻辑禁止 `provider == cm95` 等分支。 +- [ ] PBF-06:实现 Provider Package Schema v1 与 Validator,结果严格为 PASS / PASS_WITH_WARNINGS / FAIL;FAIL 禁止 export。 +- [ ] PBF-07:实现正式 Provider Package Export,并生成 MANIFEST / fingerprints / validation report。 +- [ ] 新增一个 Provider-neutral 端到端 CLI/orchestrator,单条命令可从 manifest 执行 Pipeline。应能选择阶段或复用已存在的审核/assignment 输入,但最终输出结构一致。 +- [ ] 把两个当前未跟踪的 `tools/visual-object-instantiation/`、`tools/visual-object-matcher/` 中本任务需要的源码正规纳入版本控制;不要提交 `__pycache__`。 +- [ ] 增加最小但足够的自动测试,覆盖 schema、fingerprint 稳定性、Provider-neutral 行为、FAIL 阻止 export、NavSea/CM95 两套 contract 同构。 +- [ ] 文档全部中文;工具字段名/Schema key 可用英文。 +- [ ] 更新 `STEP_RECORD.md`,但只追加本任务进度,禁止重写历史。 +- [ ] 生成中文完成报告到 `docs/报告/PBF_Provider_Pipeline_v1_Codex完成报告_v1.md`。 + +## 3. 公开 Contract 的最低字段 + +Provider Package/Policy 至少覆盖: + +- provider identity +- dataset identity +- package/version identity +- Style resource +- PBF resource +- Sprite +- Glyph +- NavSea Standard Layer mapping +- Provider Policy +- Display +- Pickup +- Planning +- Danger +- capabilities +- validation requirements +- hash / version / fingerprint +- unresolved +- limitations +- provenance + +第三方不需要使用本仓库工具,只要满足公开 Contract 即可。 + +## 4. Provider-neutral 硬要求 + +正式 Pipeline 主逻辑不得: + +- 根据 providerId 写 `if cm95` / `if navsea` +- 根据 source-layer 名称直接决定 NavSea Standard Layer +- 根据 Provider 字段名直接决定 NavSea Standard Layer +- 根据单个 icon 名称硬推完整对象语义 +- 静默把 AMBIGUOUS/UNKNOWN 变成某个 Layer + +Provider-specific 差异必须进入: + +- manifest +- declarative mapping/policy +- provenance +- 已确认人工 assignment + +## 5. 实际运行输入 + +### NavSea + +Style 优先使用 X220 当前真实本地文件: + +```text +/root/sourceserver/pbf/src/pbf/style.navsea-delivery-full-semantic-extentfix.json +``` + +该文件是实际当前 Style 输入,只用于运行测试,不要把原主工作区文件纳入本 worktree Git。 + +其 Style 当前包含: + +- vector source `navsea_delivery` +- tile template `http://192.168.200.184/pbf-delivery-full-20260418-rebuild/{z}/{x}/{y}.pbf` +- sprite `http://192.168.200.184/newpec/sprite-semantic/sprite` +- glyphs `http://192.168.200.184/newpec/fonts/{fontstack}/{range}.pbf` + +冻结 Standard Catalog: + +```text +tools/style-visual-object/output/navsea-standard/standard-catalog.json +``` + +### CM95 + +Style: + +```text +http://192.168.200.184:18889/s52/full-style.json +``` + +PBF: + +```text +http://192.168.200.184:18889/pbf/{z}/{x}/{y}.pbf +``` + +Sprite: + +```text +http://192.168.200.184:18889/s52/sprite +``` + +建议 sample tiles: + +```text +12/3531/1641 +12/3526/1643 +12/3638/1613 +``` + +CM95 人工权威 assignment: + +```text +tools/style-visual-object/output/cm95-manual-review/assignment/cm95-layer-assignment-v1.json +``` + +## 6. NavSea sample tile + +从 NavSea Style 的真实 vector source 自动获取 tile template。若 manifest 未指定 sample tile,CLI 应允许显式传 sample tiles。 + +为了本任务实际跑通,先通过 HEAD/GET 或目录可用性选择 1~3 个能成功下载的 NavSea z12 sample tile;不得伪造成功。如果某候选 404,继续选择真实存在的 tile,并在报告记录最终使用的 tile。 + +## 7. 依赖与运行 + +当前复制进 worktree 的旧 instantiation 脚本依赖 `mapbox_vector_tile` 和 Pillow;当前系统 Python 已观察到 `mapbox_vector_tile` 可能未安装。 + +要求: + +- 明确声明 Python 依赖,例如独立 requirements 文件。 +- 优先创建/使用 worktree 内 `.venv`,不要污染系统 Python。 +- 依赖安装失败时明确报告,不得伪造运行成功。 +- 输出/缓存目录应通过 .gitignore 避免把大体积临时 PBF/Sprite/cache 提交进 Git。 +- 正式示例/小型 JSON 报告可提交;大 PBF、PNG cache 不提交。 + +## 8. 两套实际端到端验收 + +实现完成后必须实际运行: + +### A. NavSea + +目标: + +```text +NavSea style + real PBF sample + frozen Standard Catalog +→ Provider Policy +→ validated NavSea Provider Package +``` + +必须至少证明: + +- Style intake 成功 +- Sprite 获取/快照成功 +- 至少一个真实 vector PBF sample 成功 +- PBF-03 实例化成功 +- NavSea mapping 成功 +- Policy Schema validate 成功 +- Package Schema validate 成功 +- Export 成功 +- validation result 非 FAIL + +### B. CM95 + +目标: + +```text +CM95 full-style + real PBF sample + existing manual assignment +→ Provider Policy +→ validated CM95 Provider Package +``` + +必须至少证明: + +- Style intake 成功 +- Sprite 获取/快照成功 +- 三个指定 sample tiles 尽可能全部成功;若单个不可用,明确记录 +- PBF-03 实例化成功 +- 使用现有人工 assignment,不能用 matcher 覆盖 +- Policy Schema validate 成功 +- Package Schema validate 成功 +- Export 成功 +- validation result 非 FAIL + +## 9. 输出建议 + +建议正式输出: + +```text +tools/provider-pipeline/ +tools/provider-policy-compiler/ +providers/navsea/ +providers/cm95/ +contracts/ +dist/ +``` + +具体目录可根据仓库现状调整,但职责必须清晰。 + +最终至少保留可复现的 manifest/example 配置,让后续可再次执行两套 Provider。 + +## 10. Git 与工作区约束 + +当前 worktree 是隔离分支: + +```text +feature/provider-pipeline-v1 +``` + +不要访问或修改主工作区中的既存脏文件,除“读取 NavSea style 输入”外。 + +只提交本任务新增/修改文件。 + +完成前执行: + +- 相关 Python tests +- CLI help/基础 smoke +- NavSea 实际 Pipeline +- CM95 实际 Pipeline +- `git diff --check` +- `git status --short` + +成功后在 feature branch 创建清晰 commit。不要自行 merge main;由外部流程审核后处理。 + +## 11. 完成报告 + +`docs/报告/PBF_Provider_Pipeline_v1_Codex完成报告_v1.md` 必须逐项报告: + +- 已完成 / 未完成 / 无法验证 +- 实际命令 +- NavSea 运行结果 +- CM95 运行结果 +- 每阶段产物 +- Schema/Contract 版本 +- validation result +- warnings / unresolved / limitations +- tests +- Git commit +- 最终实际使用的完整 Codex 模型配置(若运行环境可确认) + +不得把“测试未跑”写成“通过”。 diff --git a/docs/报告/PBF_Provider_Pipeline_v1_Codex完成报告_v2.md b/docs/报告/PBF_Provider_Pipeline_v1_Codex完成报告_v2.md new file mode 100644 index 0000000..ce8f12a --- /dev/null +++ b/docs/报告/PBF_Provider_Pipeline_v1_Codex完成报告_v2.md @@ -0,0 +1,265 @@ +# PBF Provider Pipeline v1 完成报告 v2 + +日期:2026-10-06 +分支:`feature/provider-pipeline-v1` +状态:**PBF-01~PBF-07 已实现;NavSea 与 CM95 两套真实 Style/PBF/Sprite 端到端运行完成。** + +## 1. 最终结论 + +PBF Provider Pipeline v1 已完成。正式边界保持为: + +```text +Provider Source +→ PBF-01 Intake +→ PBF-02 Style Visual Object +→ PBF-03 Actual Visual Object +→ PBF-04 NavSea Standard Mapping +→ PBF-05 Provider Policy +→ PBF-06 Provider Package Validate +→ PBF-07 Provider Package Export +``` + +Pipeline 的终点是 `NavSea Provider Package`,没有生成 `ProviderExecutionPlan`。 + +两套真实验收均完成: + +- NavSea:`PASS_WITH_WARNINGS` +- CM95:`PASS_WITH_WARNINGS` + +两者 warnings 均为 `mapping:unresolved_present` 与 `limitations:present`。这表示仍有未知/歧义 Mapping,以及本轮只使用三个样本瓦片;不是 Pipeline 执行错误,也没有把 UNKNOWN/AMBIGUOUS 静默映射成某个 Layer。 + +## 2. 实现内容 + +已加入正式 Provider-neutral Pipeline runner、Provider Policy / Package v1 Schema 与 validator、NavSea/CM95 manifest、自动测试、依赖说明及中文 README。 + +现有 `tools/visual-object-instantiation/` 已正式纳入 Git,并移除主逻辑固定 CM95 provider/catalog 名称。PBF-03 额外输出 occurrence 级追溯,包括 tile、feature identity、属性证据、matched style layers、geometry 和 render components。 + +正式 Mapping 状态统一为: + +- `VISUAL_OBJECT_MATCH` +- `LAYER_ONLY` +- `COMPONENT_ONLY` +- `AMBIGUOUS` +- `UNKNOWN` + +`FAIL` 或其他非允许 validation 状态禁止 PBF-07 export。 + +## 3. NavSea 真实端到端结果 + +分析输入仍使用 X220 当前本地 Style: + +```text +/root/sourceserver/pbf/src/pbf/style.navsea-delivery-full-semantic-extentfix.json +``` + +Style SHA-256: + +```text +2ffde6af6043be2567a861a8d06d075a87b532091686bb5ac6ce889e45827e33 +``` + +原 Style 内旧 LAN `192.168.200.184:80` 的 PBF/Sprite 服务当前拒绝连接。为保持本地 Style 内容不变,同时完成真实资源验收,`providers/navsea/manifest.json` 将资源显式覆盖到同数据集当前可用的公开 HTTPS 入口: + +```text +PBF: https://pbf.jpcn.ccwu.cc/pbf/pbf-delivery-full-20260418-rebuild/{z}/{x}/{y}.pbf +Sprite: https://pbf.jpcn.ccwu.cc/sprite/sprite +Glyph: https://pbf.jpcn.ccwu.cc/fonts/{fontstack}/{range}.pbf +``` + +因此本轮没有用公开 Style 替换本地 Style;Style 分析输入仍是上述 X220 本地文件,资源获取由 Provider manifest 显式声明。 + +NavSea Source fingerprint: + +```text +27a09da2a31746cff8e874fb5cd48bef7226c322fb2f193bc1b7fa5a01187f9c +``` + +NavSea Sprite: + +- JSON SHA-256:`e48aed662580ad437a5270966acdc906039b0d97da7892d518710d10f518f75c` +- PNG SHA-256:`fca794509a41eda3f4a9fe6db5e39467aac7cdda07de553c97a33e31b4c7b981` + +三个真实 z12 PBF 样本全部成功: + +| XYZ | 字节数 | SHA-256 | +|---|---:|---| +| `12/3531/1641` | 181,263 | `99018718a886cd5af73554f9b453a476308a51e38591758ef2b1eaf723c6486a` | +| `12/3526/1643` | 119,246 | `8f32aa41ab9af90122b45bda0bed9f02faed1c10e0eb3ab09109e88882704922` | +| `12/3638/1613` | 305,970 | `25373c570684e5b06a6cdea56e8202541aab5c6aa4ec885b23c39bb9d807fd5c` | + +PBF-03: + +- decoded styled-layer feature:8,486 +- matched occurrence:8,484 +- occurrence 输出:8,484 +- distinct Actual Visual Object:85 +- group:34 +- unsupported component:0 +- missing sprite component:48 + +PBF-04 Mapping 共 85 个 Actual Visual Object: + +- VISUAL_OBJECT_MATCH:27 +- LAYER_ONLY:7 +- AMBIGUOUS:11 +- UNKNOWN:40 +- COMPONENT_ONLY:0 + +阶段结果: + +```text +PBF-01 PASS +PBF-02 PASS +PBF-03 PASS +PBF-04 PASS +PBF-05 PASS +PBF-06 PASS_WITH_WARNINGS +PBF-07 EXPORTED +``` + +导出目录: + +```text +dist/navsea/delivery-full-semantic-extentfix/1.0.0/ +``` + +## 4. CM95 真实端到端结果 + +Style: + +```text +http://192.168.200.184:18889/s52/full-style.json +``` + +Style SHA-256: + +```text +b1d3d0060012a8e4b55a6053231231d742ec415b3a8398f9ae0867bbdfa5290b +``` + +Source fingerprint: + +```text +aa42eb9fd575dde1b651d537ccc5b52c9dafac1bd8a3d6c5166c49a9581c9363 +``` + +Sprite: + +- JSON SHA-256:`51519fb9a64f11dc100f894b2b54d14f56770ac9d511f15ae574ff5867af857d` +- PNG SHA-256:`90cf4da31e01abb0676b538ebd2b5c951b6d737a31d23062ec6eb06383d729a2` + +Style 未声明 Glyph,Package 明确记录 Glyph 不可用并保持 `capabilities.glyphs=false`。 + +三个指定真实 z12 PBF 样本全部成功: + +| XYZ | 字节数 | SHA-256 | +|---|---:|---| +| `12/3531/1641` | 423,777 | `1b9521e5dd990c92c71f22a4d56bfbd1ba33e5f236ddbf2af5ce51191e2efab8` | +| `12/3526/1643` | 285,154 | `92147b621b213a0c1fe80c2fa28ab71292b79d133b98272977defb41424ec39c` | +| `12/3638/1613` | 82,075 | `7eeb420fe809b09f8e03b892668830efd3db3e957bf32a80fd1b92b2d90e1dba` | + +PBF-03: + +- decoded styled-layer feature:5,210 +- matched occurrence:2,077 +- occurrence 输出:2,077 +- distinct Actual Visual Object:102 +- group:25 +- unsupported component:0 +- missing sprite component:0 + +PBF-04 使用现有 CM95 人工 assignment,没有由 matcher 覆盖人工结论。Mapping 共 102 个 Actual Visual Object: + +- VISUAL_OBJECT_MATCH:9 +- LAYER_ONLY:47 +- AMBIGUOUS:2 +- UNKNOWN:44 +- COMPONENT_ONLY:0 + +阶段结果: + +```text +PBF-01 PASS +PBF-02 PASS +PBF-03 PASS +PBF-04 PASS +PBF-05 PASS +PBF-06 PASS_WITH_WARNINGS +PBF-07 EXPORTED +``` + +导出目录: + +```text +dist/cm95/s52-full/1.0.0/ +``` + +## 5. 验证 + +自动测试: + +```text +.venv/bin/python -m unittest discover -s tests -v +``` + +结果:6/6 PASS。 + +CM95 固定分类基线: + +```text +.venv/bin/python tools/style-visual-object/validate_cm95_classification.py +``` + +结果:`RESULT: PASS`。既有 7 条人工决定与 S-52 语义建议不一致 WARN 保留,WARN 不覆盖人工结果。 + +其他检查: + +- Python compileall:PASS +- Pipeline CLI help:PASS +- `git diff --check`:PASS +- NavSea fresh end-to-end run:PASS_WITH_WARNINGS / EXPORTED +- CM95 fresh end-to-end run:PASS_WITH_WARNINGS / EXPORTED + +## 6. Git 与 Codex + +Pipeline 正式流程文档此前已保存到 main: + +- `f3da1a6 docs: define PBF provider pipeline v1` + +Codex 实现阶段: + +- 实现提交:`aea9862 feat: implement provider-neutral PBF pipeline v1` +- 初版完成报告提交:`804a764 docs: record provider pipeline v1 completion` + +随后由外部 X220 shell 完成 NavSea HTTPS 资源入口修正及 NavSea/CM95 fresh end-to-end 验收。本报告 v2 记录最终实际状态。 + +Codex 本轮实际启动横幅确认配置: + +```text +Model: GPT-6 Luna +Reasoning effort: High +``` + +## 7. 当前限制 + +1. 两套验收都只采样三个 z12 tile,不代表全量数据覆盖。 +2. NavSea 本次 85 个 Actual Visual Object 中仍有 UNKNOWN/AMBIGUOUS;这些状态被明确保留,没有猜测 Layer。 +3. CM95 本次 102 个 Actual Visual Object 中仍有 44 UNKNOWN、2 AMBIGUOUS;仍以现有人工 assignment 为最终语义权威。 +4. `dist/` 与 `.tmp/` 为可重建运行产物,按设计不纳入 Git。 +5. NavSea 本地 Style 内仍保留旧 LAN URL;Pipeline 通过 Provider manifest 的资源覆盖使用当前可用公开 HTTPS 镜像。后续若 Style 本体正式更新 URL,可再移除 manifest 覆盖。 + +## 8. 复现命令 + +NavSea: + +```bash +.venv/bin/python tools/provider-pipeline/run_provider_pipeline.py \ + --manifest providers/navsea/manifest.json +``` + +CM95: + +```bash +.venv/bin/python tools/provider-pipeline/run_provider_pipeline.py \ + --manifest providers/cm95/manifest.json +``` diff --git a/docs/报告/PBF_Provider_Pipeline_v1_Codex完成报告_v3.md b/docs/报告/PBF_Provider_Pipeline_v1_Codex完成报告_v3.md new file mode 100644 index 0000000..565dc49 --- /dev/null +++ b/docs/报告/PBF_Provider_Pipeline_v1_Codex完成报告_v3.md @@ -0,0 +1,141 @@ +# PBF Provider Pipeline v1 完成报告 v3 + +日期:2026-10-06 +分支:`feature/provider-pipeline-v1` +主题:PBF-08 NavSea Delivery ZIP + +## 结论 + +在既有 PBF-01~PBF-07 基础上新增 PBF-08。PBF-07 仍负责导出可审计的 Provider Package 目录,PBF-08 自动把 NavSea 最终正式输入打成一个单文件 `.navsea-provider.zip`。 + +用户后续向 NavSea 交付时,不再需要手工选择目录中的多个文件,只需要复制对应 Provider 的 ZIP。 + +## PBF-08 白名单 + +ZIP 固定且只包含 9 个文件: + +```text +provider-package.json +provider-policy.json +mapping-layer-assignment.json +resources-style.json +resources-sprite.json +resources-sprite.png +validation.json +validation.md +MANIFEST.json +``` + +任何白名单文件缺失时,PBF-08 直接 FAIL,不生成 ZIP。 + +ZIP 生成后自动执行: + +1. 精确文件列表校验; +2. ZIP CRC 校验; +3. ZIP SHA-256 计算; +4. 将 PBF-08 状态、路径、文件名、SHA-256、字节数、内部根目录和文件列表写入 `pipeline-report.json`; +5. CLI 输出 `deliveryZip` 和 `deliveryZipSha256`。 + +临时 PBF、`.tmp`、工具源码、测试、人工审核中间文件以及其他未声明文件不会进入 ZIP。 + +## 自动测试 + +```text +.venv/bin/python -m unittest discover -s tests -v +``` + +结果:8/8 PASS。 + +新增覆盖: + +- ZIP 只包含规定的 NavSea 交付文件; +- 多余文件不会进入 ZIP; +- 缺少任意必需文件时拒绝生成 ZIP。 + +Python compileall:PASS。 +`git diff --check`:PASS。 + +## NavSea 实际结果 + +Pipeline: + +```text +PBF-01 PASS +PBF-02 PASS +PBF-03 PASS +PBF-04 PASS +PBF-05 PASS +PBF-06 PASS_WITH_WARNINGS +PBF-07 EXPORTED +PBF-08 PACKAGED +``` + +ZIP: + +```text +dist/navsea/delivery-full-semantic-extentfix/ + navsea-delivery-full-semantic-extentfix-1.0.0.navsea-provider.zip +``` + +ZIP SHA-256: + +```text +54aff8d8b882211ced6e77b3a93e8e793c780b44a1faa982ed8fc64693124fd5 +``` + +ZIP 大小:933,404 bytes。 + +外部 `unzip -t`:9 个文件全部 OK,无压缩数据错误。 + +## CM95 实际结果 + +Pipeline: + +```text +PBF-01 PASS +PBF-02 PASS +PBF-03 PASS +PBF-04 PASS +PBF-05 PASS +PBF-06 PASS_WITH_WARNINGS +PBF-07 EXPORTED +PBF-08 PACKAGED +``` + +ZIP: + +```text +dist/cm95/s52-full/ + cm95-s52-full-1.0.0.navsea-provider.zip +``` + +ZIP SHA-256: + +```text +603cc004833bcff5571a32843d8ceedc7858b2fc4b28c41056db6807c7d511ee +``` + +ZIP 大小:742,008 bytes。 + +外部 `unzip -t`:9 个文件全部 OK,无压缩数据错误。 + +## 使用规则 + +以后 PBF Pipeline 正常执行完成后: + +```bash +.venv/bin/python tools/provider-pipeline/run_provider_pipeline.py \ + --manifest providers//manifest.json +``` + +只要 PBF-06 为 `PASS` 或 `PASS_WITH_WARNINGS`,Pipeline 会自动完成 PBF-07 和 PBF-08。 + +交给 NavSea 时,只复制 CLI 输出的: + +```text +deliveryZip +``` + +即可。 + +`--no-export` 模式继续只运行到 Package 校验,PBF-07 与 PBF-08 都会标记为 `SKIPPED_BY_REQUEST`,不会生成 ZIP。 diff --git a/providers/navsea/manifest.json b/providers/navsea/manifest.json index de50878..537a16a 100644 --- a/providers/navsea/manifest.json +++ b/providers/navsea/manifest.json @@ -1,23 +1,71 @@ { "schemaVersion": "pbf-provider-source-manifest/v1", - "provider": {"providerId": "navsea", "name": "NavSea", "version": "1"}, - "dataset": {"datasetId": "delivery-full-semantic-extentfix", "name": "Delivery Full Semantic ExtentFix"}, - "package": {"packageId": "navsea-delivery-full", "version": "1.0.0"}, + "provider": { + "providerId": "navsea", + "name": "NavSea", + "version": "1" + }, + "dataset": { + "datasetId": "delivery-full-semantic-extentfix", + "name": "Delivery Full Semantic ExtentFix" + }, + "package": { + "packageId": "navsea-delivery-full", + "version": "1.0.0" + }, "resources": { - "style": {"uri": "/root/sourceserver/pbf/src/pbf/style.navsea-delivery-full-semantic-extentfix.json"}, - "sprite": {"uri": "http://192.168.200.184/newpec/sprite-semantic/sprite"}, - "glyphs": {"uri": "http://192.168.200.184/newpec/fonts/{fontstack}/{range}.pbf"} + "style": { + "uri": "/root/sourceserver/pbf/src/pbf/style.navsea-delivery-full-semantic-extentfix.json" + }, + "sprite": { + "uri": "https://pbf.jpcn.ccwu.cc/sprite/sprite" + }, + "glyphs": { + "uri": "https://pbf.jpcn.ccwu.cc/fonts/{fontstack}/{range}.pbf" + } }, - "tileSource": {"sourceId": "navsea_delivery"}, - "sampleTileCandidates": ["12/3531/1641", "12/3526/1643", "12/3638/1613"], + "tileSource": { + "sourceId": "navsea_delivery", + "template": "https://pbf.jpcn.ccwu.cc/pbf/pbf-delivery-full-20260418-rebuild/{z}/{x}/{y}.pbf" + }, + "sampleTileCandidates": [ + "12/3531/1641", + "12/3526/1643", + "12/3638/1613" + ], "probeGlyph": false, - "mapping": {"mode": "frozen-standard-visual", "standardCatalog": "../../tools/style-visual-object/output/navsea-standard/standard-catalog.json"}, - "policy": { - "display": {"enabled": true, "source": "provider-style"}, - "pickup": {"enabled": false, "rules": []}, - "planning": {"enabled": false, "rules": []}, - "danger": {"enabled": false, "rules": []}, - "capabilities": {"vectorTiles": true, "sprite": true, "glyphs": true, "display": true, "pickup": false, "planning": false, "danger": false} + "mapping": { + "mode": "frozen-standard-visual", + "standardCatalog": "../../tools/style-visual-object/output/navsea-standard/standard-catalog.json" }, - "limitations": ["只使用 manifest 列出的 z12 样本瓦片进行实例化;未据此推断全数据集覆盖率。"] + "policy": { + "display": { + "enabled": true, + "source": "provider-style" + }, + "pickup": { + "enabled": false, + "rules": [] + }, + "planning": { + "enabled": false, + "rules": [] + }, + "danger": { + "enabled": false, + "rules": [] + }, + "capabilities": { + "vectorTiles": true, + "sprite": true, + "glyphs": true, + "display": true, + "pickup": false, + "planning": false, + "danger": false + } + }, + "limitations": [ + "只使用 manifest 列出的 z12 样本瓦片进行实例化;未据此推断全数据集覆盖率。" + ] } diff --git a/tests/test_provider_pipeline.py b/tests/test_provider_pipeline.py index 92fa6ee..1d75b30 100644 --- a/tests/test_provider_pipeline.py +++ b/tests/test_provider_pipeline.py @@ -4,11 +4,12 @@ import json import sys import tempfile import unittest +import zipfile from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "tools/provider-policy-compiler")) -from provider_policy import build_policy, export_package, fingerprint, validate_package, validate_policy +from provider_policy import DELIVERY_FILES, build_policy, create_delivery_zip, export_package, fingerprint, validate_package, validate_policy class ProviderPipelineContractTests(unittest.TestCase): @@ -70,6 +71,42 @@ class ProviderPipelineContractTests(unittest.TestCase): self.assertIn("style", manifest["resources"]) self.assertIn("sprite", manifest["resources"]) + def test_delivery_zip_contains_only_required_navsea_files(self): + with tempfile.TemporaryDirectory() as tmp: + export_dir = Path(tmp) / "1.0.0" + export_dir.mkdir() + for name in DELIVERY_FILES: + path = export_dir / name + if name == "validation.json": + path.write_text(json.dumps({"result": "PASS_WITH_WARNINGS"}), encoding="utf-8") + elif name.endswith(".png"): + path.write_bytes(b"png") + else: + path.write_text(name, encoding="utf-8") + (export_dir / "not-for-navsea.tmp").write_text("exclude me", encoding="utf-8") + info = create_delivery_zip(export_dir, "provider-a", "dataset-1", "1.0.0") + self.assertEqual(info["validationResult"], "PASS_WITH_WARNINGS") + self.assertEqual(len(info["sha256"]), 64) + with zipfile.ZipFile(info["path"], "r") as archive: + expected = [f"provider-a-dataset-1-1.0.0/{name}" for name in DELIVERY_FILES] + self.assertEqual(archive.namelist(), expected) + self.assertNotIn("provider-a-dataset-1-1.0.0/not-for-navsea.tmp", archive.namelist()) + + def test_delivery_zip_fails_when_required_file_is_missing(self): + with tempfile.TemporaryDirectory() as tmp: + export_dir = Path(tmp) / "1.0.0" + export_dir.mkdir() + for name in DELIVERY_FILES: + if name == "resources-sprite.png": + continue + path = export_dir / name + if name == "validation.json": + path.write_text(json.dumps({"result": "PASS"}), encoding="utf-8") + else: + path.write_text(name, encoding="utf-8") + with self.assertRaises(ValueError): + create_delivery_zip(export_dir, "provider-a", "dataset-1", "1.0.0") + if __name__ == "__main__": unittest.main() diff --git a/tools/provider-pipeline/README.md b/tools/provider-pipeline/README.md index 10737c9..e68534a 100644 --- a/tools/provider-pipeline/README.md +++ b/tools/provider-pipeline/README.md @@ -9,8 +9,41 @@ python tools/provider-pipeline/run_provider_pipeline.py --manifest providers/nav python tools/provider-pipeline/run_provider_pipeline.py --manifest providers/cm95/manifest.json ``` -也可重复传入 `--sample-tile z/x/y` 覆盖 manifest 中的样本候选;Pipeline 复用 manifest 声明的现有人工 assignment 或冻结 Catalog,不会用自动 matcher 覆盖审核结果。已有视觉目录和实例化目录会从 `.tmp/provider-pipeline/` 复用;需要从头重算时删除对应 work 目录。`--no-export` 只运行到 Package 校验。 +也可重复传入 `--sample-tile z/x/y` 覆盖 manifest 中的样本候选。Pipeline 复用 manifest 声明的现有人工 assignment 或冻结 Catalog,不会用自动 matcher 覆盖审核结果。已有视觉目录和实例化目录会从 `.tmp/provider-pipeline/` 复用;需要从头重算时删除对应 work 目录。`--no-export` 只运行到 Package 校验,因此不会执行 PBF-07 和 PBF-08。 -PBF-01 保存原 Style、Sprite 和真实 PBF 样本并计算 SHA-256 / source fingerprint。PBF-02 直接复用现有 Style Visual Object Tool。PBF-03 接收 provider/dataset 参数并为每个 feature 写 occurrence 及可追溯证据。PBF-04 从冻结 Standard Catalog 的视觉 hash 对照,或读取声明的人工 assignment。PBF-05/06 使用共享契约和校验逻辑。只有 PASS 或 PASS_WITH_WARNINGS 才能 PBF-07 导出。 +## 阶段 -输出目录默认 `dist////`,运行缓存位于 `.tmp/`,不纳入 Git。Pipeline 不生成 ProviderExecutionPlan。 +PBF-01 保存原 Style、Sprite 和真实 PBF 样本并计算 SHA-256 / source fingerprint。PBF-02 复用现有 Style Visual Object Tool。PBF-03 接收 provider/dataset 参数并为每个 feature 写 occurrence 及可追溯证据。PBF-04 从冻结 Standard Catalog 的视觉 hash 对照,或读取声明的人工 assignment。PBF-05/06 使用共享契约和校验逻辑。只有 PASS 或 PASS_WITH_WARNINGS 才能 PBF-07 导出。 + +PBF-08 在 PBF-07 成功后自动生成一个可直接交给 NavSea 的单文件 ZIP。ZIP 使用严格白名单,缺少任何规定文件都会直接失败;临时 PBF、.tmp、工具源码、人工审核中间文件和其他未声明文件不会进入 ZIP。 + +PBF-08 ZIP 固定包含: + +```text +provider-package.json +provider-policy.json +mapping-layer-assignment.json +resources-style.json +resources-sprite.json +resources-sprite.png +validation.json +validation.md +MANIFEST.json +``` + +默认输出: + +```text +dist/// + --.navsea-provider.zip +``` + +ZIP 内部还有一个同名顶层目录,避免解压多个 Provider 包时文件混在一起。Pipeline 在完成 ZIP 后会重新打开 ZIP,核对文件列表、CRC,并计算 ZIP SHA-256;这些信息写入 `pipeline-report.json` 的 `PBF-08` 节点,同时打印 `deliveryZip` 和 `deliveryZipSha256`。 + +PBF-07 的可审计目录仍保留在: + +```text +dist//// +``` + +运行缓存位于 `.tmp/`,不纳入 Git。Pipeline 不生成 ProviderExecutionPlan。 diff --git a/tools/provider-pipeline/run_provider_pipeline.py b/tools/provider-pipeline/run_provider_pipeline.py index 41b45af..c9e6a04 100644 --- a/tools/provider-pipeline/run_provider_pipeline.py +++ b/tools/provider-pipeline/run_provider_pipeline.py @@ -17,7 +17,7 @@ from typing import Any ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "tools" / "provider-policy-compiler")) -from provider_policy import build_package, build_policy, fingerprint, validate_package, validate_policy, export_package +from provider_policy import build_package, build_policy, create_delivery_zip, fingerprint, validate_package, validate_policy, export_package def read_resource(location: str, timeout: int = 60) -> bytes: @@ -279,7 +279,7 @@ def main() -> int: "capabilities": manifest.get("policy", {}).get("capabilities", {}), "validationRequirements": {"requireStyle": True, "requirePbfSample": True, "requireSprite": True, "minimumMappingCount": 1}, "unresolved": unresolved, "limitations": manifest.get("limitations", []), - "provenance": {"manifest": str(manifest_path), "mappingSource": mapping_source, "pipelineVersion": "1.0.0", "generatedAt": datetime.now(timezone.utc).isoformat()}, + "provenance": {"manifest": str(manifest_path), "mappingSource": mapping_source, "pipelineVersion": "1.1.0", "generatedAt": datetime.now(timezone.utc).isoformat()}, "fingerprints": fingerprints, } record["stages"]["PBF-05"] = {"status": "PASS", "schemaVersion": "navsea-provider-policy/v1"} @@ -301,11 +301,15 @@ def main() -> int: shutil.copy2(intake / "sprite/sprite.png", export_dir / "resources-sprite.png") shutil.copy2(mapping_path, export_dir / "mapping-layer-assignment.json") record["stages"]["PBF-07"] = {"status": "EXPORTED", "path": str(export_dir)} + delivery_zip = create_delivery_zip(export_dir, provider_id, dataset_id, version) + record["stages"]["PBF-08"] = {"status": "PACKAGED", **delivery_zip} else: + delivery_zip = None record["stages"]["PBF-07"] = {"status": "SKIPPED_BY_REQUEST"} + record["stages"]["PBF-08"] = {"status": "SKIPPED_BY_REQUEST"} report_path = work / "pipeline-report.json" report_path.write_text(json.dumps(record, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") - print(json.dumps({"providerId": provider_id, "datasetId": dataset_id, "validationResult": validation["result"], "sampleTiles": [x["tile"] for x in successful_tiles], "output": None if args.no_export else str(export_dir), "report": str(report_path)}, ensure_ascii=False, indent=2)) + print(json.dumps({"providerId": provider_id, "datasetId": dataset_id, "validationResult": validation["result"], "sampleTiles": [x["tile"] for x in successful_tiles], "output": None if args.no_export else str(export_dir), "deliveryZip": None if delivery_zip is None else delivery_zip["path"], "deliveryZipSha256": None if delivery_zip is None else delivery_zip["sha256"], "report": str(report_path)}, ensure_ascii=False, indent=2)) return 0 if validation["result"] != "FAIL" else 2 diff --git a/tools/provider-policy-compiler/provider_policy.py b/tools/provider-policy-compiler/provider_policy.py index 954eb78..9e8d84d 100644 --- a/tools/provider-policy-compiler/provider_policy.py +++ b/tools/provider-policy-compiler/provider_policy.py @@ -4,6 +4,8 @@ from __future__ import annotations import hashlib import json +import re +import zipfile from datetime import datetime, timezone from pathlib import Path from typing import Any @@ -12,6 +14,18 @@ POLICY_SCHEMA = "navsea-provider-policy/v1" PACKAGE_SCHEMA = "navsea-provider-package/v1" ASSIGNMENT_STATUSES = {"VISUAL_OBJECT_MATCH", "LAYER_ONLY", "COMPONENT_ONLY", "AMBIGUOUS", "UNKNOWN"} +DELIVERY_FILES = ( + "provider-package.json", + "provider-policy.json", + "mapping-layer-assignment.json", + "resources-style.json", + "resources-sprite.json", + "resources-sprite.png", + "validation.json", + "validation.md", + "MANIFEST.json", +) + def canonical(value: Any) -> bytes: return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8") @@ -160,3 +174,51 @@ def export_package(output: Path, package: dict[str, Any], policy: dict[str, Any] } (output / "MANIFEST.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") return output + + +def _safe_delivery_component(value: str) -> str: + cleaned = re.sub(r"[^A-Za-z0-9._-]+", "-", str(value)).strip("-._") + return cleaned or "provider" + + +def create_delivery_zip(export_dir: Path, provider_id: str, dataset_id: str, version: str) -> dict[str, Any]: + """把 PBF-07 的正式 NavSea 输入打成一个严格白名单 ZIP。""" + export_dir = Path(export_dir) + missing = [name for name in DELIVERY_FILES if not (export_dir / name).is_file()] + if missing: + raise ValueError(f"PBF-08 缺少正式交付文件,禁止生成 ZIP: {missing}") + + validation = json.loads((export_dir / "validation.json").read_text(encoding="utf-8")) + if validation.get("result") not in {"PASS", "PASS_WITH_WARNINGS"}: + raise ValueError(f"PBF-08 只接受 PASS/PASS_WITH_WARNINGS,当前为 {validation.get('result')}") + + stem = "-".join(_safe_delivery_component(x) for x in (provider_id, dataset_id, version)) + zip_path = export_dir.parent / f"{stem}.navsea-provider.zip" + root_name = stem + if zip_path.exists(): + zip_path.unlink() + + with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive: + for name in DELIVERY_FILES: + archive.write(export_dir / name, arcname=f"{root_name}/{name}") + + with zipfile.ZipFile(zip_path, "r") as archive: + actual = archive.namelist() + expected = [f"{root_name}/{name}" for name in DELIVERY_FILES] + if actual != expected: + zip_path.unlink(missing_ok=True) + raise ValueError(f"PBF-08 ZIP 内容校验失败: expected={expected}, actual={actual}") + bad = archive.testzip() + if bad is not None: + zip_path.unlink(missing_ok=True) + raise ValueError(f"PBF-08 ZIP CRC 校验失败: {bad}") + + return { + "path": str(zip_path), + "filename": zip_path.name, + "sha256": file_fingerprint(zip_path), + "bytes": zip_path.stat().st_size, + "root": root_name, + "files": list(DELIVERY_FILES), + "validationResult": validation["result"], + }