archive(provider-pipeline): preserve final B2 audit and mapping evidence

This commit is contained in:
OpenAI Codex
2026-10-13 08:22:44 +08:00
parent d046d7ee0d
commit 7c64299c22
35 changed files with 104658 additions and 7 deletions

View File

@@ -2407,3 +2407,11 @@
- 正式 CM95 102 条 assignment 均有 renderEvidence,status/standardLayer 与旧 1.0.0 逐条一致;Style 原文件、intake 与 Package SHA-256 对齐。 - 正式 CM95 102 条 assignment 均有 renderEvidence,status/standardLayer 与旧 1.0.0 逐条一致;Style 原文件、intake 与 Package SHA-256 对齐。
- 新 ZIP `dist/cm95/s52-full/cm95-s52-full-1.1.0.navsea-provider.zip` 固定 9 文件,重复运行 SHA-256 稳定为 `fb5bc2606ab61662c16eccd22225532779830779e931f623bac4cf049e6b737f`;旧 1.0.0 保留。 - 新 ZIP `dist/cm95/s52-full/cm95-s52-full-1.1.0.navsea-provider.zip` 固定 9 文件,重复运行 SHA-256 稳定为 `fb5bc2606ab61662c16eccd22225532779830779e931f623bac4cf049e6b737f`;旧 1.0.0 保留。
- Validation 为 PASS_WITH_WARNINGS,警告为原有 unresolved 和 limitations;审计记录在 `docs/报告/NAVSEA-PBF-MULTISOURCE-004A_CM95_MappingV2审计_v1.json`。本任务止于 004A,不进入 004B。 - Validation 为 PASS_WITH_WARNINGS,警告为原有 unresolved 和 limitations;审计记录在 `docs/报告/NAVSEA-PBF-MULTISOURCE-004A_CM95_MappingV2审计_v1.json`。本任务止于 004A,不进入 004B。
## 2026-10-13 NavSea 现有 CM95 1.1.0 ZIP 接收审计与上游任务交接
- 直接审计正式 cm95-s52-full-1.1.0.navsea-provider.zip:九文件、CRC、Style/Sprite SHA、Manifest 规范化映射指纹及 Package/Policy/Mapping 语义校验全部 PASS;旧 ZIP SHA 未变。
- 包内映射 102/475:56 已分类、46 UNKNOWN/AMBIGUOUS、373 未纳入;最新上游 475 条审计映射完全保留旧 102 条分类,新增 273 已分类候选、100 未解决。
- CARBON7i 导入同一 SHA ZIP 为 PASS_WITH_WARNINGS;Adapter 输出 19 EXECUTABLE、32 VERIFIED_NOT_ENABLED、51 RESIDUAL(含 5 个已知冲突),相关 7/7 Python 测试 PASS。
- 上游项目新增正式 ZIP 接收审计 JSON、问题报告以及 docs/任务 中的上游新版本重生成任务 MD,要求维持九文件、复用历史审核、完整映射及逐层执行证据。
- 本次仅审计及任务交接,未新发行 ZIP、未改 PBF/Style/Sprite/NavSea Runtime、未 Commit/Push。下一步由上游按任务执行,正式 Contract 冲突先请用户批准。

View File

@@ -0,0 +1,55 @@
# 上游 PBF Provider Pipeline 任务:按 NavSea ZIP 接收审计补全映射并重生成 CM95 新版本 Package v1
## 0. 任务性质与权责
- 责任仓库:`/root/sourceserver/pbf-provider-pipeline-v1`,当前分支 `feature/provider-pipeline-v1`。
- 责任端:**PBF Provider Pipeline 上游制作端**,不是 NavSea Runtime。NavSea 只解析、验证、编译已制作 ZIP;不得让客户端重新执行 PBF/Style 语义识别。
- 依据:`docs/报告/NAVSEA-PBF-MULTISOURCE_CM95-1.1.0正式ZIP接收审计与问题清单_v1.md` 与 `docs/报告/NAVSEA-PBF-MULTISOURCE_正式ZIP-1.1.0_内容审计证据_v1.json`。
- 正式基线 ZIP:`dist/cm95/s52-full/cm95-s52-full-1.1.0.navsea-provider.zip`,SHA-256 `fb5bc2606ab61662c16eccd22225532779830779e931f623bac4cf049e6b737f`,只读保留,禁止覆盖、删除或冒用原版本号。
- 上位原则:Windows CARBON7i `D:\React\NavSea-PBF-Multisource\docs\设计基线\NAVSEA-PBF-MULTISOURCE_通用解释器与标准层映射强制原则_v1.md`;还要核对最新 MULTISOURCE-002/002B/004 和实际当前 Provider ZIP Contract。正式 Contract 发生冲突时暂停影响该 Contract 的步骤并将冲突提请用户裁决,禁止静默改 Schema。
- 不更改原始 PBF、Style、Sprite、Glyph;不改变 Style Layer ID、顺序、filter、symbol 图形内容和 Z-order;不新增第十个 ZIP 文件;禁止基于图标名猜来源语义。
- 保护所有用户现存 dirty/untracked 改动,不执行 git reset --hard、git clean、git add -A;不擅自 Commit/Push/Tag/切分支。
## 1. 先核实并完整复用既有分类成果
1. 直接读取旧正式 ZIP 九文件与现有 `mapping-layer-assignment.json`,保留其中 102 个 Visual Object ID、现有状态和 standardLayer 分类。
2. 读取完整真实 `tools/visual-object-instantiation/output/cm95-current/actual-anonymous-catalog.json`:已确认 475 个 Visual Object 类型、177383 张 Tile 的 Catalog 范围、976534 次 occurrence 声明。需要校对真实输入的来源锁;若重新拉取来源改变,应更新输入指纹与记录,不能沿用旧 475 的数值冒充。
3. 复用已有 Visual Object 图形匹配器与人工审核记录;将现有上游 B2.1 的 475 条全量映射构建步骤应用于新的独立**构建目录**,而不是重新做 475 次人工分类。临时 `/tmp/navsea-b23-audit-475-mapping.json` 仅是审计材料,不是持久发行源;不能以其存在为必要执行前提。
4. 对相同的原始 Catalog 版本,执行 ID 集合对照:正式旧 ZIP 102 ID 必须全部保留,新增 373 类必须显式出现在新 Mapping;旧 102 条 `status/standardLayer` 与当前审核结论应 byte-independent semantic equal,未经证据/批准禁止修改。
5. 当前审计中 475 条分布为 LAYER_ONLY 303、VISUAL_OBJECT_MATCH 26、UNKNOWN 135、AMBIGUOUS 11;这个分布为**对照事实**,不强迫把 146 个未解决类别改成有意义的分类,仍保留原值或明确降级。
## 2. 验证补齐执行证据,不能用数量充当业务 PASS
1. 对每个映射保留 source/source-layer、原始 Style Layer ID/index、原 filter、原绘制角色、资源 expression/key/hash 和可达范围;缺项要给出可诊断的 Reason。
2. 根据同一原始 PBF Feature 的真实复合绘制证据复用 PRIMARY/COMPONENT 关联;图形相似是分类线索,不能凭距离、图标名、Tile 局部 ID 推断跨瓦片实体身份。当前 `fidScope=unknown`,无证据不得升级。
3. 从原 Style 表达式派生逐 Style Layer 的精准 selector 或现行 Contract 允许的同义证据;29 个混合已知标准层/残余的原始 Style Layer 不得整层隐藏,UNKNOWN/AMBIGUOUS 必须默认 PRESERVE。
4. 第一版本实际 Compiler 只放行已证明导航标记的 19 条,另有 32 VERIFIED_NOT_ENABLED、51 RESIDUAL(包括 5 个已知冲突)。新包必须诚实声明哪类对象可被当前客户端执行,不得擅自标注 Pickup/Danger/Planning READY。
5. 如需新的正式属性来表达跨 Layer COMPONENT 继承/PRIMARY/cross-standard-display-group 契约,先提交 `CONTRACT_DIFF_AND_BLOCKERS.md` 并征得明确批准;在未获批准前不能生成声称正式兼容的新 ZIP。当前 ZIP 仍固定 v1 九文件。
## 3. 生成新 Package 候选并跑自动验证
1. 新包须采用独立递增版本目录与 ZIP 名称 `<provider>-<dataset>-<new-version>.navsea-provider.zip`,不得覆盖旧 1.1.0;所有 9 文件、Manifest/Package/Policy/Mapping 的身份、来源锁和校验值必须相互一致。
2. 在重新发行前逐 ID 对比 Actual Catalog、Mapping、Package/Policy 中的映射集合,禁止漏掉 UNKNOWN/AMBIGUOUS;统计 Missing/Extra/Duplicate 必须全部为 0。
3. 调用现有 Pipeline PBF-01~PBF-08、strict ZIP/Manifest 检查;执行测试(当前 X220 上游 50 项 Python 回归基线)和真实 Style/Sprite Source Lock 检查。
4. 复核 B2.3/B2.4/B2.5/B2.7 的全量证据/261 Tile 抽样:保证错误分类、残余与跨组误隐藏不因新增 Mapping 恶化;不把抽样 0 collision 当作全 Provider 证明。
5. 在 NavSea CARBON7i 环境仅用 `import_provider_package`、`adapt_provider_package` 及 `test_visual_bindings.py` 校验**新 ZIP**。如本次上游执行没有当前 NavSea Worktree 访问权限,把此项标为 **无法验证**并提供真实新 ZIP SHA 与路径,待 NavSea 接收端执行,禁止声称跨端 PASS。
6. 若资源仍指向 `http://192.168.200.184:18889/...`,发行报告须将该包标为局域网测试发行,不宣称公网用户 URL 安装成功;原资源表现和指纹不因主观换域名而失真。
## 4. 强制交付文件(本任务开工前已列明,禁止执行后临时追加)
至少实际生成以下完整文件,不得只在聊天里汇报:
- `docs/报告/NAVSEA-PBF-MULTISOURCE_CM95重生成_上游完成报告_v1.md`:逐项 `已完成 / 未完成 / 无法验证`、原因、真实代码/文件、版本、命令与结果;必须列出仍关闭的能力。
- `docs/报告/NAVSEA-PBF-MULTISOURCE_CM95重生成_新旧映射差异_v1.json`:逐 Visual Object ID old/new 的 status/standardLayer/PRIMARY/组件证据与遗漏列表、明示 102 条旧记录是否保持语义。
- `docs/报告/NAVSEA-PBF-MULTISOURCE_CM95重生成_新ZIP结构与来源校验_v1.json`:实际文件 CRC、SHA、Schema、Style/PBF/Sprite/GSRL Source Lock、Manifest、九文件、真实 Catalog IDs 的覆盖及能力结果。
- `docs/报告/NAVSEA-PBF-MULTISOURCE_CM95重生成_能力与Residual矩阵_v1.md`:四档 Display、Pickup、Danger、Planning 分开列为已验证/待验证/不支持,列出混合 Layer、未知分类和冲突影响。
- `docs/报告/NAVSEA-PBF-MULTISOURCE_CM95重生成_Contract差异与阻塞_v1.md`:说明沿用正式 Contract 的字段与任何冲突;无冲突也必须写明 `无须修改`。
- `docs/报告/NAVSEA-PBF-MULTISOURCE_CM95重生成_测试与运行记录_v1.md`:原生真实来源审计/测试、NavSea 消费端能否运行、未完成的 Android/iOS 项及原因。
- 新版本 `dist/cm95/s52-full/<new-version>.navsea-provider.zip`(只有契约和必备证据均满足时才输出正式候选;若阻塞,产物列为未完成且留下已生成的所有报告)。
- 进展应以中文追加至仓库 `STEP_RECORD.md`,遵循当前工作区 AGENTS 规则;必要时附完成本任务所需的可重复生成命令或脚本,但不要复制旧 1.1.0 伪装新品。
## 5. 阶段完成判定
- 审计修复完成不等于实机可见性 PASS;必须分别报告 Zip Contents/Contract/Mapping/Compiler/Android/iOS/URL。
- 没有经批准的多源通用 Contract/标准层显示组语义时不得用 Renderer 专属硬编码补救。人工审核只针对确实缺证据或已知冲突,不重复让人工从零逐项审核已经认可的旧映射。
- 出现需要用户裁决的正式 Contract 分歧时,先提供完整 Blocking Report 而非修改已批准标准。任务以可验证交付物为终点,不自动 Commit/Push。

View File

@@ -0,0 +1,76 @@
# NavSea 多源 PBF:CM95 1.1.0 正式 Provider ZIP 接收审计与问题清单 v1
- 审计日期:2026-10-13(日本时间;事件来源为 2026-10-12 的多源开发会话)
- 审计对象:`dist/cm95/s52-full/cm95-s52-full-1.1.0.navsea-provider.zip`
- ZIP SHA-256:`fb5bc2606ab61662c16eccd22225532779830779e931f623bac4cf049e6b737f`
- 精确结论:**ZIP 格式/来源指纹/Importer 接收 PASS;发行 Mapping 覆盖不足,能力不足以宣称通用分类 COMPLETE;退回上游完善并重生成新版本 ZIP。**
- 架构边界:NavSea 只负责导入、验证和执行既有 ZIP;不在手机生成 ZIP、补造分类、猜 Provider 专属属性。上游 Pipeline 负责调整映射和重生成。
## 一、证据来源(不能混淆发行物与后续草稿)
1. **正式发行物**:在 X220 原始 ZIP 中直接解压/比对九个文件;`mapping-layer-assignment.json` 是本报告判定当前可用程度的首要依据。
2. **NavSea 实际消费者**:CARBON7i Windows NavSea `help/chart-profile-tools/navsea_chart_profile_tools/provider_package.py` 的 `import_provider_package` 成功读取同一 SHA 的 ZIP;`provider_package_adapter` 生成 visual bindings;其 `test_visual_bindings.py` 实际 **7/7 PASS**。
3. **上游后续开发材料**:`tools/visual-object-instantiation/output/cm95-current/actual-anonymous-catalog.json`(475 类)及 `/tmp/navsea-b23-audit-475-mapping.json`(本机审计工作文件,非已发行 ZIP);关联 `docs/报告/` 下 B2.1~B2.7 报告。后续资料只用于问题定位、提供修复基线,不能当成新版正式 Package 已通过。
4. **上位 Contract**:CARBON7i `docs/设计基线/NAVSEA-PBF-MULTISOURCE_通用解释器与标准层映射强制原则_v1.md`,以及 `MULTISOURCE-002B/004` 设计;不擅自增第十个文件或改正式 ZIP Schema。
5. 对应机器可重算 JSON:`docs/报告/NAVSEA-PBF-MULTISOURCE_正式ZIP-1.1.0_内容审计证据_v1.json`,内含 373 个遗漏类型 ID 的完整清单、按文件哈希、映射状态和消费者实测结果。
## 二、ZIP 格式与完整性:PASS,禁止误报为 ZIP 损坏
- ZIP 仅有一个顶层目录 `cm95-s52-full-1.1.0`;九个要求文件齐全且无重复文件名、CRC 正常。
- Style、Sprite JSON、Sprite PNG 与 Package 声明的 SHA-256 一致;MANIFEST 声明的四个原始文件 SHA-256 全部匹配。
- `MANIFEST.fingerprints.mapping` 使用项目 `provider_policy.fingerprint(mapping)` 的**规范化 JSON 指纹**计算,等于 `ec938f7be8c3b833f5dea520cf5033a653b73369c603a9f0f5106af8145e9f1a`;不应拿 Mapping 文件的字节级 SHA-256 `e1a8f218...` 与之比较并误判哈希错误。
- Package / Policy / Mapping 的 102 条语义 assignments 一致;Style `b1d3d0060012a8e4b55a6053231231d742ec415b3a8398f9ae0867bbdfa5290b` 与 mapping 的 Source Lock 一致。
- 本轮所有格式/来源/指纹静态检查全部 PASS。NavSea Importer 返回 **ACCEPTED / PASS_WITH_WARNINGS**,不表示已达到所有业务需求。
## 三、上游需修复或说明的问题(按优先级)
### ISSUE P0-01:正式 ZIP 覆盖 102/475,遗漏 373 个已发现的 Visual Object 类型
- ZIP `mapping-layer-assignment.json` 有 102 条;`renderEvidenceSource.distinctVisualObjectCount=475`,总计有 373 个类型**连 UNKNOWN 记录都没有随 ZIP 下发**。
- 当前 102 条:`LAYER_ONLY=47`、`VISUAL_OBJECT_MATCH=9`、`UNKNOWN=44`、`AMBIGUOUS=2`;**56 已分类,46 未解决**。
- 最新上游工作材料 475 条:`LAYER_ONLY=303`、`VISUAL_OBJECT_MATCH=26`、`UNKNOWN=135`、`AMBIGUOUS=11`;**329 已有分类候选,146 未解决**。新增 373 条中 273 条已有分类候选、100 条尚未分类,均不应凭空丢弃。
- 旧 ZIP 的全部 102 个 VO ID 均属于真实 Catalog,且在 475 条上游草稿中这 102 个的 status/standardLayer **逐条保持完全不变**,可复用历史人工判断;不允许要求再从零审核原有 102 类。
- 上游动作:以真实 Catalog **ID 集合相等**为硬门槛,生成每类一条 assignment,包括 UNKNOWN/AMBIGUOUS/RESIDUAL,不能只做数量相等;重复 VisualObject ID、遗漏、过多均拒绝发行。
### ISSUE P0-02:精准显示/PRIMARY 证据尚未满足通用 Runtime 的完整业务要求
- 正式 ZIP 的 Mapping v2 已带 102 条 `renderEvidence.components`(包括 styleIndex、styleLayerId、role、resourceKey、resourceVisualHash);这部分不可误认为完全没有证据。但没有明确的已确认 PRIMARY/COMPONENT 业务身份和可在各混合 Style Layer 上证明的整体 display control。
- NavSea 的**已有** Adapter 能从原 Style 的可验证资源表达式保守派生 selector;实测 102 条 Binding 中 **19 EXECUTABLE(全部 navigation_marks)**、32 VERIFIED_NOT_ENABLED、51 RESIDUAL。说明缺口不是“客户端不认识 ZIP”,而是上游语义/逐层可执行证据不足,外加 Runtime 仍是受控小范围启用。
- 最近完整 Catalog 的静态证据:170 条原 Style Layer 中 **29 条 MIXED_OR_RESIDUAL**,39 条只见 unresolved,33 条仅有一个已分类 standardLayer 候选,69 条无观测类型。不得把混合层整体关闭,UNKNOWN 要保持 Provider 原绘制。
- 最新上游 261 张真实 Tile 样本抽查得到 595 条逐层候选 selector 在样本中未见跨组/残余误命中,225 条缺候选;这不代表全 PBF、PRIMARY 审批或 GPU 渲染已通过。不得把 B2.7 离线候选直接塞进 ZIP 冒充已审核 Binding。
- 上游动作:结合现有 Visual Object 图形匹配、历史审核、原始 Feature 实例/组件关联证据,补齐逐对象和逐 Style Layer 的可验证定义;只对证明充分的条目附执行资格,不得把 component 擅自变成独立 Pickup/Danger 对象。正式 Contract 若需新增字段/语义,先提交设计冲突和批准申请,不得私下扩写 Schema。
### ISSUE P1-03:语义未解决及分类冲突需要继续 fail-closed
- 当前 ZIP `validation.json` 是 `PASS_WITH_WARNINGS`,warning 为 `limitations:present`、`mapping:unresolved_present`,不是格式错误。
- 当前 `capabilities`: Display=true;Pickup=false、Danger=false、Planning=false。另有 5 个有已知资源分类冲突的 Binding 在 NavSea Adapter 中被强制保留为 RESIDUAL(没有扩大执行)。
- 上游动作:复用已经做过的图形匹配/人工分类成果;仅将尚不明确的对象留为显式 UNKNOWN/AMBIGUOUS 并列出审核缺口。未获得证据的 Pickup/Danger/Planning 继续关闭,不能伪造“无危险”。
### ISSUE P1-04:发行溯源的路径不可移植(应改为可验证描述,不可增文件破协议)
- 当前 `mapping.source` 暴露 X220 的绝对路径 `/root/sourceserver/...`;`renderEvidenceSource.catalog` 是指向上游工作树的相对路径。ZIP 内没有该 Catalog 文件,这是正常九文件结构,但若消费者被要求据此在终端打开 Catalog,则不可实现。
- 上游动作:确认两个路径只是非执行诊断字段,不成为 NavSea 安装/编译的外部依赖;用现有 Contract 允许的 Source Lock、hash/版本/Provider provenance 表达必要权威证据,移除不必要的机器本地路径;如果现行 Contract 无法表达则先报告,不能私自新增第十文件。
### ISSUE P1-05:正式资源引用当前为局域网 HTTP;公网分发可用性未证明
- ZIP 指向 `http://192.168.200.184:18889/...` 的 Style、Tile 和 Sprite。对于不在该 LAN 的用户,这些地址不能作为公网可访问资源。**不能将此推断为当前 LAN 测试失败**,本轮未执行公网和手机连通性测试。
- 上游动作:若新 ZIP 用于公网 URL 加入海图源,先确认可访问资源与部署策略,并验证 Style/Tile/Sprite 指纹;如仅为封闭测试 ZIP,则明确声明测试作用域,不宣称公网分发完成。不得为让 URL 通而改写原始 Style 图形/层顺序。
## 四、发行重新制作的边界
- **上游要做**:修复 Catalog→Mapping 覆盖、保持旧 102 条语义不变、补全受审可执行证据、重新运行 GSRL Source Lock/Package Validator、生成新版本 `.navsea-provider.zip`、提交差异及新审核报告。
- **NavSea 只做**:校验新包中的版本/来源/资源/映射证据,转成内部 immutable ExecutionPlan;按能力证据受控显示/查询;没有新增自主分类逻辑。
- **明确不做**:不动原始 PBF、Style/Sprite/Glyph;不自动覆盖旧 `1.1.0`;不新增 CM95-specific Runtime 特例;不在没有授权时修改正式 Contract;不因缺证据而重审全部 475 项或自动改写所有 UNKNOWN。
## 五、发行前最低回归门槛
1. 新 ZIP 有独立递增版本和不同文件名,九文件完整,CRC/SHA/Manifest 指纹一致,老 ZIP 原地保留。
2. `Catalog IDs == Mapping IDs == Policy Mapping IDs == Package Mapping IDs`,且每个 ID 唯一,额外验证新增的 373 ID 全部有显式状态。旧 102 条现有分类 status/standardLayer 未经重新批准不得变化。
3. 真正的逐 Style Layer Selector 能表达混合 Style Layer 中明确对象的隐藏,UNKNOWN/AMBIGUOUS/残余不会误隐藏;所有显示开关恢复时精确回归 originalFilter,原 Layer 数量/ID/顺序不变化。
4. PRIMARY/COMPONENT 从属证据和任何能力授权均逐项可追溯;证据不够时保守禁用,禁止靠视觉近邻合并 Feature。
5. 上游 Python 自动测试、跨样本静态反例检查、NavSea 的 `import_provider_package` 与 Adapter 编译诊断复跑通过;Android/iOS 视觉、Pickup/Danger/Planning 另列为设备分项,不得被 Python 通过冒名代替。
## 六、审计停点
- 本审计已经完成 ZIP 读取、格式完整性、Mapping 覆盖、旧新成果对照、真实消费者导入测试;仍未真正重生成或批准下一版正式 ZIP。
- 本报告和后续上游任务 MD 均写入实际 X220 Pipeline 工作树;正式 Contract 的修改和重发之前需要履行既定人工/版本授权门槛。

View File

@@ -0,0 +1,466 @@
{
"schemaVersion": "navsea-existing-provider-zip-audit/v1",
"sourceZip": "dist/cm95/s52-full/cm95-s52-full-1.1.0.navsea-provider.zip",
"sourceZipSha256": "fb5bc2606ab61662c16eccd22225532779830779e931f623bac4cf049e6b737f",
"fileIntegrityChecks": {
"zipSHA": true,
"singleDirectory": true,
"nineFiles": true,
"CRC": true,
"manifestFileHashes": true,
"styleResource": true,
"spriteJsonResource": true,
"spritePngResource": true,
"mappingCanonicalDigest": true,
"packageAssignments": true,
"policyAssignments": true,
"styleMappingBinding": true,
"oldIdsInActualCatalog": true,
"stagedIdsMatchActualCatalog": true,
"oldSemanticAssignmentUnchanged": true
},
"package": {
"provider": "cm95",
"dataset": "s52-full",
"version": "1.1.0",
"mappingSchema": "navsea-standard-layer-mapping/v2",
"mappingAssignments": 102,
"mappingStatuses": {
"LAYER_ONLY": 47,
"VISUAL_OBJECT_MATCH": 9,
"UNKNOWN": 44,
"AMBIGUOUS": 2
},
"classifiedAssignments": 56,
"unresolvedAssignments": 46,
"declaredCatalogTypeCount": 475,
"validation": {
"schemaVersion": "navsea-provider-validation-report/v1",
"result": "PASS_WITH_WARNINGS",
"errors": [],
"warnings": [
"limitations:present",
"mapping:unresolved_present"
]
},
"capabilities": {
"vectorTiles": true,
"sprite": true,
"glyphs": false,
"display": true,
"pickup": false,
"planning": false,
"danger": false
},
"explicitPrimaryCount": 0,
"explicitSelectorCount": 0,
"styleUrl": "http://192.168.200.184:18889/s52/full-style.json",
"pbfUrl": "http://192.168.200.184:18889/pbf/{z}/{x}/{y}.pbf",
"spriteUrl": "http://192.168.200.184:18889/s52/sprite",
"sourceMappingPath": "/root/sourceserver/pbf-provider-pipeline-v1/tools/style-visual-object/output/cm95-manual-review/assignment/cm95-layer-assignment-v1.json",
"catalogEvidencePointer": "../../tools/visual-object-instantiation/output/cm95-current/actual-anonymous-catalog.json"
},
"fullCatalogComparison": {
"actualTypes": 475,
"oldZipTypes": 102,
"missingTypesInZip": 373,
"newStagedTypes": 373,
"stagedAssignmentStatuses": {
"LAYER_ONLY": 303,
"UNKNOWN": 135,
"VISUAL_OBJECT_MATCH": 26,
"AMBIGUOUS": 11
},
"oldClassificationsUnchanged": 102,
"newTypeIds": [
"VO-019539c788703729",
"VO-01e305d8405ac611",
"VO-020fbfda3e5b1746",
"VO-022956714ace8dbc",
"VO-0249f905c170305b",
"VO-03818c8f0b9661fe",
"VO-04a33d7b15a3222b",
"VO-04d322734a8ca239",
"VO-050689b162403e6f",
"VO-06b184370ec9f3cd",
"VO-07cc881a430d36a6",
"VO-0833d4d3888887b1",
"VO-0921acf2ccfe79b8",
"VO-09d1641d8e625e8a",
"VO-0a99121a73947f27",
"VO-0a99e3b8fb17625d",
"VO-0ba121b78c676422",
"VO-0e70729abbe6c9cd",
"VO-0eba83cdde810f7e",
"VO-0f3d0f9b4766659e",
"VO-1155eafa9fe9b5c1",
"VO-12055cc4fbfb8bc5",
"VO-121d80ea22b243ce",
"VO-12ede0aa86b84220",
"VO-1302f166f1f64ce7",
"VO-131ebd9bb3270372",
"VO-13347ad099b7c3d0",
"VO-136137192572b3ed",
"VO-143b277afd085f6d",
"VO-149da62fe3f482d4",
"VO-14e6f3a332cf15e5",
"VO-14f05d236de80a67",
"VO-152422762c0ccddd",
"VO-15d056ee7a1f6d27",
"VO-162470a441e39273",
"VO-162ecdd423dcf93f",
"VO-16317338ee4bc676",
"VO-174615e9709a257b",
"VO-17f8209f27ed58cc",
"VO-1815fd22c82311fb",
"VO-185d9e557d158c5c",
"VO-18c8596124a39bce",
"VO-1b61a71ef59bc18b",
"VO-1c13a43a7d6e2059",
"VO-1c3aeb8c9006ce07",
"VO-1da4d955a244b6f5",
"VO-1dd11e028fb0d7aa",
"VO-1e0654b86462de13",
"VO-1e4425c50159c40c",
"VO-1e5bca05559eb97f",
"VO-21499305b3ed0268",
"VO-21ac408ccbfcfd17",
"VO-21d33110bf272ab2",
"VO-22afa694231a214d",
"VO-22ee0680c2bfc0a7",
"VO-23d5a8342ba0470f",
"VO-245e16b3e1d0420c",
"VO-24b7481004302c8d",
"VO-2544a5ee488f8c78",
"VO-256e51465a4438f1",
"VO-27244e64f2b59452",
"VO-272ee9c7251155ce",
"VO-275f232819563b5e",
"VO-27af8f22867f2029",
"VO-28c346b42e6f060e",
"VO-28dfab113bc00c5e",
"VO-2946a51caecf8694",
"VO-2984c0807905d55c",
"VO-29f4f0bd90d9093b",
"VO-2a09c9713360d0e4",
"VO-2ae4791265561d28",
"VO-2b2fd7ba30421281",
"VO-2b9b24ef22ae4203",
"VO-2cb7117b4555dff8",
"VO-2e44d866c93658c2",
"VO-2f2815e2e5d6d3a0",
"VO-2f40b4c3b65694d2",
"VO-2fc08020fb1f89f2",
"VO-2fe9228b360f7adf",
"VO-304fa4a471e6cfc6",
"VO-30ba5f7366c49d70",
"VO-3191a5567cb0d321",
"VO-31dec9fc9a2e1ea6",
"VO-32a594efe434d757",
"VO-3379d23c914877ef",
"VO-33a359ad91101d1f",
"VO-34d016dafee96ded",
"VO-34d3033e1e3a067f",
"VO-35b7ddc67848f9e6",
"VO-368b363f01eaa346",
"VO-3696bd3501d4ea64",
"VO-37260839e48d16a5",
"VO-37b0efab635c95ee",
"VO-39489de3d61a9ad2",
"VO-39c6164faf0431bb",
"VO-3a3017eb9bc2a40e",
"VO-3a820f9f155b9111",
"VO-3ae148d954850c58",
"VO-3d1cead2137a18ea",
"VO-3eb657f1b312d79c",
"VO-40f952efacf6f12d",
"VO-411bb29f682b99cf",
"VO-41f6964be0c475f5",
"VO-4206463b3db83401",
"VO-424510e40e3b690f",
"VO-43004d0c0a33a198",
"VO-4313bf07fda9a003",
"VO-4322ad3736f68669",
"VO-4415ca8c5c2c31ea",
"VO-445051b7e9af8610",
"VO-4488c717cc88a958",
"VO-4492939cd47824ff",
"VO-44ed5cbf4eebabac",
"VO-451460af8848b4f8",
"VO-45d0e536f105523d",
"VO-4733199b096d8b0d",
"VO-47c31195ab520803",
"VO-47f784dd30901421",
"VO-48fe1911d11d618b",
"VO-4a4f6e785a4d9a8c",
"VO-4a694130c46e6ff1",
"VO-4ab8e34d8c063fca",
"VO-4b7a69710090a6ed",
"VO-4b87550b0aaf47bf",
"VO-4bb74edeb6f29dea",
"VO-4e3ae3347d42d6ec",
"VO-4e8fd421d55621b4",
"VO-4f256d2e6ce10702",
"VO-4f38e69dcdb5f149",
"VO-4f68a793fef22a50",
"VO-4f82b496b8558100",
"VO-502108ab5fce38fc",
"VO-5039c1bac33e07a3",
"VO-522bf09e89c7420a",
"VO-5233836a5d218277",
"VO-541376af6a5c7fcc",
"VO-542b8e8da34e10d5",
"VO-545f4cbc7b9f98bc",
"VO-552b065aebfcc61e",
"VO-56a5ba198953c894",
"VO-56aafa5d0bd15a34",
"VO-57a7ed643a2e8a4f",
"VO-599014868e0cc2df",
"VO-5a66e616ecc37c35",
"VO-5a7ffb67711c5d61",
"VO-5aed9a9ede5f7992",
"VO-5b41c92d1129c285",
"VO-5c26bce02631516e",
"VO-5ccdb5ddbf55da6e",
"VO-5edbc070a1a92f17",
"VO-5ef1f3b84499727c",
"VO-5f062057d921fbef",
"VO-5f2d46b6a5be4d8a",
"VO-609434a34bb9465f",
"VO-6127bcfa6f4552cb",
"VO-61acb4e9b0095fc4",
"VO-6226bd74a2677f36",
"VO-6240c47f31e7de2a",
"VO-6346a6df6ef02338",
"VO-639f394c2160c4f0",
"VO-64eeaa93f7b0b864",
"VO-65169002e8e66846",
"VO-664e519da54946d0",
"VO-667eeebd5ae9e611",
"VO-6682de84b216c795",
"VO-66955be464c8f54f",
"VO-66a7d25fcfca00c4",
"VO-6744292c85fef2f1",
"VO-689424baf29ec2e1",
"VO-68ce4a8207ba8bc9",
"VO-6928dbddb8959af7",
"VO-6b43e268f3a2161f",
"VO-6d2b3f101ca7d6dd",
"VO-6e649b5a6f84baa8",
"VO-6eae70ee5dd79880",
"VO-7013c2ff85cc2734",
"VO-712b88b340ee808e",
"VO-713a7b5809da2afe",
"VO-7307261cfcc85080",
"VO-737ce64aa69df5ba",
"VO-738def0b0d853ee6",
"VO-7504f6e138aacf6f",
"VO-76b2e78748663724",
"VO-779675373bddbb91",
"VO-77bdc9de8a8202af",
"VO-77db8a66eec05306",
"VO-7926b28c043b995f",
"VO-7a49f0eebb11cb31",
"VO-7b19828b2b5ab5cf",
"VO-7bd65cdf3b6b573a",
"VO-7c33ecd42fa5c2be",
"VO-7c58d21a5a283b16",
"VO-7d5635c63521ef9c",
"VO-7db160f4de7e0835",
"VO-7e9c8777ad653f78",
"VO-7f093a200d98113d",
"VO-7f30fa5265ffcd7f",
"VO-8166223b0399c1a9",
"VO-827707bc71db3f09",
"VO-82fd3c88aae90c5d",
"VO-8369af1b3ac334b6",
"VO-850bce0f29096537",
"VO-85ade400859178d0",
"VO-8687eadc92a85a81",
"VO-86ae7d5818fdd886",
"VO-86b3635322ecc435",
"VO-8736fc0260650b00",
"VO-8768e2535bef915c",
"VO-878c1d145fae3da6",
"VO-87c6f0b68bba1f57",
"VO-89123e18e7987549",
"VO-897c6bc29121ebd3",
"VO-89f4338670486a38",
"VO-8adf8c39f611b13d",
"VO-8b281a3f6956f570",
"VO-8b29884796b69b30",
"VO-8b966cca1055d1c2",
"VO-8caea6434f7f27b3",
"VO-8db3cad79a6d1ba1",
"VO-8de63f1f75271ed7",
"VO-8e45c9b75d0d6a24",
"VO-901546bb9b8d2611",
"VO-91a1a397893cc2f0",
"VO-94cff8da6d3677e7",
"VO-95a52b8f744f629a",
"VO-95ca352b53fe9665",
"VO-96d5e043b87a450d",
"VO-97a48b2701fab9e3",
"VO-97b0971552ee047a",
"VO-97c5d22f1c940d48",
"VO-97d48e96cdbb649f",
"VO-97e884522ea81815",
"VO-98d7d06bc357ccb6",
"VO-9abee435d9232345",
"VO-9ad2ad97f3766a06",
"VO-9bfe8f3aed094b8b",
"VO-9cd99036ac94c77c",
"VO-9d5e763f5669d0b8",
"VO-9d7ec2c2a3373b09",
"VO-9dcefa9859f61f49",
"VO-9f7f8e71da42e4ab",
"VO-a022c28490931003",
"VO-a1a3b1fc7f30f7b2",
"VO-a1a7df00c5fab371",
"VO-a1b2c84290fc03fb",
"VO-a265938adbb39873",
"VO-a29c996e917ee138",
"VO-a3138f7df69a59bd",
"VO-a34c52e9bd00c9ad",
"VO-a3e0e64173b93917",
"VO-a434923d73ede0a6",
"VO-a529db40623450b9",
"VO-a7a90dc0e17a0328",
"VO-a9dc398d38b0cf05",
"VO-aa4edf3de504afeb",
"VO-aa79cc4b4eb1b964",
"VO-acef02ee1351b503",
"VO-ad21c2817473a4cb",
"VO-b0022d3a65d61fab",
"VO-b15ecf120bba04e9",
"VO-b17622c1f26de89b",
"VO-b1a7bc056f8178d3",
"VO-b1bfcf1f443000ab",
"VO-b35c2feb98d9b679",
"VO-b37b3e5198045a1d",
"VO-b63776f0a806cb96",
"VO-b7d6aeac74071c89",
"VO-b80bf0f564936403",
"VO-b84f7e31fe4c7870",
"VO-b8aef198ab6d8285",
"VO-b8d4b137e537adbd",
"VO-b8e40139e0139967",
"VO-b9e2237dc15e7f8a",
"VO-ba9a066b657d8666",
"VO-bb4fed77f08e0589",
"VO-bcd35f92ef7fd26a",
"VO-bde4eedd97fc0b76",
"VO-be60f0fe9c380088",
"VO-bf92faabd57d32aa",
"VO-bfddc8100fff0897",
"VO-bff61699a48f2859",
"VO-c00e800abdc56818",
"VO-c0f480e8c58b6068",
"VO-c12d02dc11f938ae",
"VO-c2b3cb3d9a13589a",
"VO-c311dedf301786e7",
"VO-c37c1e4b9bcf9c1f",
"VO-c4517cbf20212aed",
"VO-c481e51ab901694a",
"VO-c49db434d441307f",
"VO-c49e9e5a376a3d73",
"VO-c55dd3f4a34c3903",
"VO-c6140e311f210273",
"VO-c6a3fd9b946824a1",
"VO-c6c2b6a8babcb799",
"VO-c6d9d13876d192de",
"VO-c79939b18f48847d",
"VO-c7ccf7e53e75cdc7",
"VO-c84c69da3060a7de",
"VO-c93b1534c60fb6a5",
"VO-c955b7dae3714f15",
"VO-cad642c2b03b7ad8",
"VO-cb09a4eb996982bc",
"VO-cd1b59f2bcc6a4dd",
"VO-cfab2b34de39b298",
"VO-cfd6e4238305128f",
"VO-d0a22e6d73be5f99",
"VO-d0a3da6548cbdcf8",
"VO-d1c9fb214faf69f9",
"VO-d1e36abc63cc26b9",
"VO-d2521425512119ad",
"VO-d2ec5de8c10639c4",
"VO-d2ffbdd471d7905e",
"VO-d302f65a65e0e0ad",
"VO-d324ba3842834562",
"VO-d49e8f67d940cbea",
"VO-d544034acbffd135",
"VO-d5afcc9334085e6b",
"VO-d5d46511d547ba16",
"VO-d6571acab70fe3c8",
"VO-d6abb8634497b816",
"VO-d6b8c5d04af292b1",
"VO-d7da645770f43078",
"VO-d7fbc3cbaa395805",
"VO-d81f08881729d717",
"VO-d8ab7e378e6eb360",
"VO-d97ee93132eb459a",
"VO-daaa38aef6408f06",
"VO-db7e8edc34147873",
"VO-dbb4dabe47464b37",
"VO-dbe2d93a59ea20e7",
"VO-dc11a3e3c4d5bd66",
"VO-ddcee7ac91fe5f8e",
"VO-dde39c7aead71538",
"VO-de8336dc02a6bf82",
"VO-defd9ede35d5b631",
"VO-df05b840b6491111",
"VO-dfe1661fcc01e3ce",
"VO-e0d89bdde61d0f8a",
"VO-e0e61bc69352c78a",
"VO-e19f10ce119b10e1",
"VO-e2c29a47fa25b752",
"VO-e3ce76aa8e8983a0",
"VO-e421f2ccda22cf43",
"VO-e492e592aa779d68",
"VO-e4c0e1f1e1a52dfc",
"VO-e7a69c6099bf65ec",
"VO-e8bc6850a719d2ec",
"VO-e8f727819c4bf3de",
"VO-e8fb46268abf0e2d",
"VO-ea0c35eb4cb6ba7b",
"VO-eab9968e73227189",
"VO-eaf00d9cf77ae308",
"VO-eb62daf75452c536",
"VO-eb995e07133aaef5",
"VO-eec33f0c9879b321",
"VO-eeca079d6ec7e2df",
"VO-eee79ae004365bf5",
"VO-eee81af3c817e091",
"VO-f1f512804ca1a8bd",
"VO-f2873c11dce2e856",
"VO-f3e136c6bcbe7b85",
"VO-f5b84f82b90cfbd1",
"VO-f6e640646aafb224",
"VO-f71d35c50da85ef5",
"VO-f7f34d9acb97ea29",
"VO-f886f8387fd96268",
"VO-f8aaca3bf941e287",
"VO-f90db4cbdbf08d3e",
"VO-f92348ccffbdfa19",
"VO-fa4f648e7b28e38f",
"VO-fe5fe87da25f3fab",
"VO-fea789adff45e0e1"
]
},
"navseaImportVerified": {
"device": "CARBON7i Windows",
"importer": "provider_package.import_provider_package",
"importResult": "PASS_WITH_WARNINGS",
"mappingCoverage": "COUNT_SHORTFALL",
"materialized": 102,
"bindingStatus": {
"EXECUTABLE": 19,
"VERIFIED_NOT_ENABLED": 32,
"RESIDUAL": 51
},
"knownConflictCount": 5,
"onlyExecutableGroup": "navigation_marks",
"consumerPythonTests": "7/7 PASS"
}
}

View File

@@ -0,0 +1,84 @@
# NAVSEA-PBF-MULTISOURCE 阶段 B2.3:标准层与组件证据审计 RDC 完成报告 v1
日期:2026-10-12
状态:**证据审计器实现 PASS;执行契约尚未冻结;全部标准层执行、NavSea 实机、ZIP 新发行均 NOT DONE。**
用户上位原则:NavSea《通用解释器与标准层映射强制原则 v1》(2026-10-12)。
## 一、实际工作环境和文件
- RDC 设备:tei-ThinkPad-X220(Linux)。仓库:`/root/sourceserver/pbf-provider-pipeline-v1`,分支:`feature/provider-pipeline-v1`,开始时 HEAD:`d046d7ee0d9f2030d974d0e5435ae6571b0b4489`。
- 参照 RDC 设备:CARBON7i(Windows)。NavSea Worktree:`D:\React\NavSea-PBF-Multisource`,分支 `feature/pbf-multisource`,HEAD `78d7ffeccc95469ef3be62701b7b805b5a50c1f8`。
- 正式旧 CM95 1.1.0 ZIP 不变;Style 取其 `resources-style.json`,全量 Actual Catalog 来自 `tools/visual-object-instantiation/output/cm95-current/actual-anonymous-catalog.json`。
- 以上 Worktree 均有各自实际情况;本轮没有切换 Git 分支或 Worktree,不执行 Android 编译、安装、ADB、Jest、Codex、Git Commit/Push。
- X220 先前 B2.1 的代码/报告修改保持,不做清理;CARBON7i 的 B1/B2.2 修改与大量既有未跟踪文件不触碰。
## 二、何以不能直接从 475 条 Mapping 升级成全部 EXECUTABLE
1. 现行 PBF Mapping v2 `renderEvidence.components[].role` 为绘图角色 icon/text/line/fill,**不是**唯一 PRIMARY 和从属 COMPONENT 的权威关系。
2. `visualObjectId` 表示视觉组合/类别签名,不能凭名称代替每次真实 raw Feature 实例的稳定身份;多绘制 Layer 的同 Feature 关系仍需可信证据,不能通过屏幕位置邻近拼接。
3. 原始 Style 的部分表达式含单个 `["get", field]`,可以生成**候选资源字段**,但其他表达式或没有 resource key 的 text/line/fill 不能被直接推断为可执行 selector。
4. 现有 NavSea Profile 显示逻辑仍含 `provider_display`,前端 `pbf_group.json` 有 13 个用户组 + `base_map`,但 Package 的人工标准层没有交付一份**明确版本化、可校验的标准层 → Host 显示组**权威映射;不能默认标准层名与 UI 组名相等。
5. 2026-10-12 原始 Provider Style 不可改写原则高于仅靠整条 Style Layer visibility 的旧默认方案。混合 Layer 不许整体隐藏,残余不得自动转为已知;Pickup/Danger/Planning 不可从绘制资源直接推出安全判定。
## 三、本阶段真实新增代码
- `tools/provider-pipeline/mapping_execution_evidence_audit.py`:**只读、通用**逐 Style Layer 证据审计器。输入经过验证的 Style JSON 和 Mapping JSON;输出每个 Visual Object 的候选表达式、分组件可证明情况、原始 Layer 绑定和阻塞原因,内部始终 `executionAuthorized=false`。审计器不改 Provider Package、PBF、Style,不产生 `VerifiedVisualBindingSet`,不成为运行时分类规则。
- `tests/test_mapping_execution_evidence_audit.py`:两种结构不同的合成 Provider(字段分别为 `symbol`、`pictogram`),检验混合 Layer/UNKNOWN 保留、text component 需身份继承、相同字段相互矛盾、重复 Visual ID、错误 Style 索引,以及声明的 Style SHA-256 与实际快照不一致时 fail-closed。
- `docs/报告/NAVSEA-PBF-MULTISOURCE_阶段B2-3_全量Mapping逐层证据审计_v1.json`:以**暂存内存生成的 475 条 Mapping**和既有原始 Style 快照计算出的诊断数据;它不是正式新 ZIP,也没有产品执行授权。
- 本报告:`docs/报告/NAVSEA-PBF-MULTISOURCE_阶段B2-3_标准层与组件证据审计_RDC完成报告_v1.md`。
## 四、实证数据与检查结论
上游完整 CM95 Actual Catalog:177,383 tiles、475 类 Visual Object、976,534 次 matched visual occurrence。使用原有人工赋值构造的 475 条 Mapping 中,329 条有 standardLayer、146 条为 UNKNOWN/AMBIGUOUS;此 Mapping 仅用于审计输入,**正式旧 ZIP 仍只有 102 条 Mapping**。
本阶段审计 475 个对象,总绘制组件数 820:
- 510 个组件具备可静态构造的**候选**直接资源 selector;310 个组件在当前严格 `role + resource property` 规则下无法证明候选。
- 29 个 Style Layer 在**完整 475 类目录**中出现多个分类或 residual;旧 102 类子集曾得出 6 个混合 Layer,是不同覆盖范围,不构成冲突。
- 275 个 Visual Object 含多个绘制组件,需逐实例证明主体与组件关系。274 个对象至少有一个组件的候选 selector 在严格检查下尚不可证明(注意该数量不等同于旧统计的 259 类无 resourceKey)。
- 329 条已分类对象仍缺标准层 → 显示组的**正式发布与绑定版本**;146 条属于 UNKNOWN/AMBIGUOUS 等未确认归属。
- 静态检查未在当前全量对象中检测到“同一字段被要求同时等于不同资源值”的显式矛盾;但不代表所有组合可达。
- 原 Style filter 命中、任意两组件来自同一真实原始 Feature、Pickup 的对象 Identity 和用户四档效果都**未经当前审计器证明**。
- 本轮执行授权新增量为 **0**;Audit 不是 Compiler。
- X220 最终运行 `python3 -m unittest discover -s tests -p 'test_*.py' -v`:**19/19 PASS**、退出码 0(B2.3 新增审计测试 5、B2.1 既有 Pipeline 测试 14)。没有 Jest。审计 CLI 在校验原 Style SHA-256 后成功重建 475 条诊断 JSON;旧正式 ZIP SHA-256 没有变化。
- B2.2 NavSea 导入侧测试在上一阶段 7/7 PASS;不是本轮重新执行的结果。
## 五、拟定通用执行 Contract 结构(候选,未经用户确认,不得视为冻结)
下面仅是**待批准的接口方向**,不作为现有 Package Schema 或任何 EXECUTABLE 授权:
### 5.1 NavSea 所有的标准显示 Catalog
- 独立定义 `standardCatalogId`、`schemaVersion`、`catalogSha256`。
- 明确、逐条声明 `standardLayerId → allowedDisplayGroupIds` 的审核关联;可把多个标准层对应到同一显示组,但必须显式说明是否允许一个标准层拆分成多个 UI 组,以及需怎样区分对象。
- 该 Catalog 由 NavSea 标准层/显示策略维护,不依赖 CM95、source-layer、Sprite 或某个 PBF 的层名;至少校验 Catalog 指纹与被引用版本。
- 未绑定的 standardLayer 只保留 Provider 原图形显示,并返回该组控制/语义能力未可用,禁止默默猜测。
### 5.2 Package 中的 Composite 对象/Component 身份与 selector 证据
每个 Visual Object 至少需要可审核的:已确认 `standardLayer`、对象身份规则(原始 Feature/有效范围)、唯一 PRIMARY 证明、COMPONENT 的父关系/同 Feature 关联,以及每个被控制的 Style Layer 独立 `selector`、资源输出表达式和原 Style Filter/Layer Index/Source 绑定。
- 同一原始 Feature 的证明不能只用屏幕距离或抽象 `visualObjectId`。
- 若其他 Component 不具自己的资源键,可以继承已经证明可在该 Layer 读取到、且来自同一 Feature 的 selector;必须先证明跨层数据字段和 filter 匹配。
- 审查者需明确每条证明的来源、版本、覆盖范围及其对 Display/Pickup/Danger/Planning 的能力授予范围。
- ANY/ALL、不同 Style Layer selector 的合成必须根据每层真实可达证据决定,不能简单把全对象条件照搬到所有 Layer。
- Residual/AMBIGUOUS/UNKNOWN/冲突对象 **PRESERVE** 原渲染,不自动授权查询或安全判断。
### 5.3 ProviderCompiler 验收门槛
Compiler 只接收并校验已冻结、版本/指纹匹配的 Package、Style、Standard Display Catalog,生成不可变 Plan;Host 的 B1 通用 selector 开关仅消费执行计划,不重新解释源数据。缺关键身份、逐层 selector、Catalog 映射、覆盖和冲突证明就降级,不允许“整体隐藏混合 Layer”这种表面通过方式。
**现行 v1 Package 九文件不变**。是否在 Mapping v3 或 v2 的受控扩展字段中表达组件绑定,需先按正式 ADR/RFC 比较旧消费者兼容性,再由用户确认。此处不修改 ZIP 格式/Parser/Compiler。
## 六、后续 B2.4 推荐顺序
1. 将本阶段 475 条逐组件静态证据与上游原始 raw Feature 样本关联,调查什么字段、何种 raw identity 可证明 PRIMARY/COMPONENT。先只读,不自动指定父组。
2. 单独审核并批准标准层 → NavSea 产品显示组的权威 Catalog、版本/指纹和覆盖政策;需确认像 `facility_boundary_outline`、`fixed_fishing_gear_area` 等标准层的 Host 归属,不能按名字臆测。
3. 在**上游先做一个纵向切片**:原始 Style 层的逐层精确 selector + 组件身份 + residual 不变,验收原 Style 全可见等价,再推广至完整 Catalog。
4. 完成另一种结构不同的真实 Provider(非合成测试)同 Contract 验收,再考虑打开其它 EXECUTABLE;原有 19 条及 CM95 专用鱼礁/LIGHTS05 过渡兼容路径继续维持,不做未经实机复验的清除。
5. 完整新 ZIP 发行、真实 URL 安装与 Android/iOS 实机回归应成为独立后续门槛,不得将本阶段测试标记为产品完成。
## 七、边界与待裁决项
正式设计存在“旧 Profile 以整个原 Style Layer 列出 logicalGroups”和“新混合 Layer 必须按对象级 selector”的粒度差异。本阶段未替用户确认新的公开 Schema,也没有在生产侧引入双套解释规则,建议冻结新 Contract 前制定明确的迁移/优先级规则。
无 Git 发布或 Source Lock 行为。重要限制:审计器本身没有验证 177,383 瓦片来源的不可变全量清单、原始 Feature 实例稳定 ID、跨层 Style Filter 语义或真实 GPU 输出;所有相关能力均为待证事实。

View File

@@ -0,0 +1,77 @@
# NAVSEA-PBF-MULTISOURCE 阶段 B2.4:原始 Feature 身份重放与组件关联 RDC 完成报告 v1
- 日期:2026-10-12
- 阶段结论:**原始 PBF 特征实例重放与 Composite 组件同源性样本验证 PASS;PRIMARY、跨瓦片全局身份与业务执行契约尚未完成。**
- 最高优先级依据:NavSea《多源 PBF:通用解释器与标准层映射强制原则 v1》及现行 Chart Provider Contract v3、004 Style Visual Object 契约候选。
- **重要状态差异**:本报告中的“475/475”指全量 Catalog 的**每个不同 Visual Object 各取一条已记录实际 Feature 样本**。不是全量 976,534 次 Visual Occurrence 的逐项检查。
## 1. 实际环境与边界
- 执行机器:RDC / tei-ThinkPad-X220(Linux),代码仓库:`/root/sourceserver/pbf-provider-pipeline-v1`,分支 `feature/provider-pipeline-v1`。
- HEAD 基线:`d046d7ee0d9f2030d974d0e5435ae6571b0b4489`。当前 Worktree 继承 B2.1、B2.3 修改及未跟踪报告;没有创建或切换分支/Worktree。
- NavSea 当前参考仓库为 CARBON7i:`D:\React\NavSea-PBF-Multisource`,分支 `feature/pbf-multisource`,HEAD `78d7ffeccc95469ef3be62701b7b805b5a50c1f8`;本轮未修改 NavSea Runtime。
- 使用原始 PBF:`/root/sourceserver/cm95/build/releases-s52-v2/cm95-d1-62a85bd40d3a-ae61b8866e80/pbf/delivery`。
- 使用原始 Style/Sprite 的同 SHA 快照:`dist/cm95/s52-full/1.1.0/resources-style.json` 等;Style SHA 为 `b1d3d0060012a8e4b55a6053231231d742ec415b3a8398f9ae0867bbdfa5290b`。
- 原 CM95 1.1.0 ZIP SHA 为 `fb5bc2606ab61662c16eccd22225532779830779e931f623bac4cf049e6b737f`,本轮结束仍不变。**未重建正式 ZIP、未重写原 PBF/Style/Sprite、未 Tag/Commit/Push、未进行 Android/iOS 编译安装或 Jest/Codex。**
## 2. 重新核实的原有算法事实
`tools/visual-object-instantiation/instantiate_pbf_visual_objects.py` 通过如下方式制作原始证据:
1. 遍历每个 PBF Tile 的每个原始 tile Layer 与 Feature;
2. **对单个 raw Feature 的原始属性**依 Style 层顺序判断其 filter 与 zoom,并由同一 Feature 的属性解析所有绘制组件;
3. 对单个 Feature 形成 Composite Render Signature,计算 Visual Object 类型标识 `visual_object_id`;
4. 记录该原始 Feature 的 tile、source-layer、feature_id、相关组件和 matched Style Layer。
因此单个 Visual Object 的绘制组件不是通过地理位置靠近而被拼接。但是类型 ID 并不等于物理海上实体的跨瓦片身份,更不等于 PRIMARY 的业务审查结论。
- 全量 Catalog:475 类视觉组合、976,534 次 matched visual occurrence、177,383 个 Tile。
- 原有全量 Catalog 仅包含每类一条 `example.tile/feature_id` 与计数;实际 **不含完整 occurrences 数组**。不能把它误认为 976,534 条逐实例证据已被本轮重新验证。
- 工作目录中的三张样本瓦片对应 2,077 次 Occurrence;样本中含 574 次多绘制组件,同一 `tile+source-layer+feature_id` 没有重复。三张样本之间未发现同一 `source-layer+feature_id` 跨 Tile 重用,但不能据此证明它是 Provider 全域唯一的实体 ID。
## 3. 实际实施
新文件:`tools/provider-pipeline/feature_lineage_replay_audit.py`。
- 使用现有 Visual Object 实例化器的解码、原始 Style 表达式评价、组件构造与签名算法(不另造 CM95 语义分类器)。
- 对 Catalog 中每个 Visual Object 的一个真实 example,根据 `tile+source-layer+feature_id` 在**原始 PBF 文件**精确找回 Feature。Feature ID 缺失、重复,Tile/原始 source-layer 不存在,均 fail-closed。
- 重新执行 Style 原始 filter、zoom 判断、icon/text/line/fill 的资源和视觉签名组合,生成同一 Feature 的 Composite Visual Object 类型 ID,与 Catalog 记录比较。
- Style 两个不同 Source 同名 source-layer 时拒绝假定唯一 source;同一 PBF Tile 有重复的 source-layer 名称时拒绝接受,不把歧义变成可执行绑定。
- 只输出静态证据数据。每条结果 `primaryComponentAuthorized=false`、`displaySelectorAuthorized=false`;不生产 Runtime Binding、不会指定 PRIMARY,也不向 NavSea 执行计划自动增权。
- CLI 允许单独指定原 Catalog、Style、Sprite JSON/PNG、PBF 根目录及输出报告,不硬编码 CM95 资源名。执行时核对 Catalog 所声明原 Style 快照与当前 Style 文件的 SHA-256。
- 真实运行须用**仓库既有** `.venv/bin/python`。系统 `python3` 未安装 `mapbox_vector_tile`,初次执行因此退出;改用已有虚拟环境后可正常执行,没有临时安装新依赖。
新增测试:`tests/test_feature_lineage_replay_audit.py`。以不同 Provider 的属性字段 `symbol` 与 `providerIconCode` 构造两套合成原始 PBF,验证单组件、多组件、Feature 不存在/重复、Composite 改动、同名 source-layer 的跨 Source 歧义、缺失 Tile 等保护。
## 4. 实际数据与结果
真实 CM95 全量 Catalog 示例重放:
- **475/475 个不同 Visual Object 类型**各抽取其 Catalog 中记录的一个原始 Feature example;
- 使用原始 Style、Sprite 和 PBF 原始瓦片重算后,**475 个均为 `SAME_RAW_FEATURE_REPLAY_CONFIRMED`**;
- 475 类所用原始 Feature example 分布于 **261 个不同 Tile**;
- `primaryComponentAuthorizedCount=0`,`displaySelectorAuthorizedCount=0`;
- 报告位置:`docs/报告/NAVSEA-PBF-MULTISOURCE_阶段B2-4_原始Feature实例重放审计_v1.json`。
- 三张旧样本的同一 `tile+source-layer+feature_id` 重复数为 0,Feature ID 缺失数为 0;这是三张样本的事实,不能扩大为全区域保证。
执行与自动检查:
- `./.venv/bin/python -m unittest discover -s tests -p 'test_*.py' -v`,**24/24 PASS**(新增 B2.4 测试 5,原 B2.3 / B2.1 测试 19),退出码 0。
- 原始 475 类实地证据重放 CLI:返回退出 0,所有 475/475 匹配。
- B2.3 既有静态证据审计的 29 个混合 Style Layer 与 146 个 UNKNOWN/AMBIGUOUS 对象不因为本轮组件同源性验证而转为已确认业务类别。
- `git diff --check` 及受本轮写入的文件完整性检查在末尾另行记录。
## 5. 仍然缺失的正式契约
1. **PRIMARY**:本轮只证明“一条原始 Feature 被原始 Style 的多个 Layer 命中并重算成同一复合视觉类型”,未证明其中哪一个渲染 component 应成为 PRIMARY,更不能把 Composite `visual_object_id` 当成真实实体 ID。
2. **全域 Feature identity**:`tile + source-layer + feature_id` 只用于当前原始 Tile 内的单次重放定位,不等于跨瓦片、跨更新、跨设备稳定对象身份。真实 Pickup 多层去重需要显式的 Provider 身份规则和相应作用域;不能按显示位置或未经验证的 `feature_id` 去重。
3. **同源和 filter 全量性**:475 类各验证一个 example,并不保证同类中其他全部 occurrence 的 Style/filter、组件组合在后续数据版本中继续一致。未来可靠 Package 必须描述适用样本/全量覆盖证明和校验指纹。
4. **标准显示组**:还没有由 NavSea 正式批准的、对全部 `standardLayer` 可审核的产品显示组映射。不能根据名字自动将 `fixed_fishing_gear_area` 分配给 `fishery_gear`,尽管名称看似关联。
5. **逐层 selector**:即使同源证明通过,也必须证明用于隐藏该 Feature 的 selector 在每一条原始 Style Layer 可达且不会误隐藏 UNKNOWN/其他类别,不能把多个 component selector 全部 AND 后一律应用到每层。
6. **能力分层**:Display、Pickup、Danger、Planning 各有独立的可信证据与覆盖门槛;无证据时保留/降级,不能把视觉判断当成航行安全结论。
## 6. 后续 B2.5 推荐
先确定真正的身份范围,建议区分 `CompositeVisualTypeId`、`TileLocalFeatureKey` 和 Provider 可承诺的 `StableEntityId` 三种互不混同的概念;对多组件以真实原始 Feature 命中作为同源基础,由上游人审或正式受控规则标记唯一 PRIMARY 与从属 COMPONENT。再结合 NavSea 固定显示组 Catalog 的正式映射,独立做逐 Style Layer selector 证明;一个纵向切片通过 UNKNOWN 保留和原 Style 恢复检查后,才扩大 Compiler 实际 EXECUTABLE 授权。
正式 Provider ZIP 九文件架构不修改。历史 CM95 Fishery/LIGHTS05 过渡兼容不动,直到新通用路径通过实机对比。没有源数据和正式 Contract 不能凭记忆自动生成此授权。

View File

@@ -0,0 +1,67 @@
# NAVSEA-PBF-MULTISOURCE 阶段 B2.5:身份作用域与 PRIMARY 审核清单 RDC 完成报告 v1
日期:2026-10-12
阶段结论:**只读身份作用域审计器及 475 条 PRIMARY 审核清单已完成,工程测试通过;跨 Tile 稳定实体身份、PRIMARY 审核、标准层→显示组权威映射及产品执行资格仍然未完成。**
## 1. 本阶段目标、依据与实际环境
按 2026-10-12 NavSea《多源 PBF:通用解释器与标准层映射强制原则 v1》,不得在 NavSea Runtime 基于 CM95 特有字段自行推断业务分类。参考以下实际设计与代码:
- NavSea `docs/设计基线/NAVSEA-PBF-MULTISOURCE-002B_FeatureScope与ProviderCompilerExecutionPlan_v2_设计.md` 第 6 节:可信 rawId 的稳定键至少含 providerId/datasetId/sourceId/sourceLayer/fidScope/rawId;当 fidScope=tile 时必须附 z/x/y;身份不足时仅可使用有界且保守的 operation-local 去重。
- NavSea `docs/设计基线/NAVSEA-PBF-MULTISOURCE-004_StyleVisualObject可执行适配与共享StyleLayerSelector_v1_设计.md`:唯一 PRIMARY 与 COMPONENT 从属应有独立经确认的证据。
- CARBON7i 当前真实 Chart Profile `src/assets/pbf/providers/bundled/cm95-s52-full-1.1.0.navsea-chart.json`:`identity.fidScope="unknown"`,`dataVersion=null`,实际代码不能因旧扫描样本通过就自动提高到 sourceLayer/global。
执行设备:RDC X220(Linux)。实际工作路径:`/root/sourceserver/pbf-provider-pipeline-v1`,分支 `feature/provider-pipeline-v1`,既有 HEAD `d046d7ee0d9f2030d974d0e5435ae6571b0b4489`。没有运行 Codex、没有切换分支或 Worktree、没有删除、重置、覆盖原有 B2.1~B2.4 的 dirty/untracked 文件。NavSea Windows CARBON7i 参考仓库仍为 `D:\React\NavSea-PBF-Multisource`(分支 `feature/pbf-multisource`、HEAD `78d7ffeccc95469ef3be62701b7b805b5a50c1f8`),本轮 NavSea Runtime 未改动。
## 2. 本阶段实际代码与测试
在上游项目中新增:
- `tools/provider-pipeline/feature_identity_scope_audit.py`:只读对比原始 PBF Tile / source-layer / feature_id、Style source 所有者、Catalog 示例归属、Mapping 当前状态;区分视觉类型 ID、真实样本 Tile 内定位与没有获得证明的稳定实体身份;保留可供人工审查的全部组件,不自动指定 PRIMARY。
- `tests/test_feature_identity_scope_audit.py`:针对两个不同 Source / source-layer 的合成 Provider、同一 ID 跨两个 Tile 的差异、Tile 内重复 ID、缺失 ID、跨 Style Source 歧义、非法 Tile 路径和 Visual Object ID 重复,验证 fail-closed;额外验证多个 Visual Object 类型若抢占同一个 Tile 内原始 Feature,则**先后两个都失去确认资格**,避免只拒绝后者。
- `docs/报告/NAVSEA-PBF-MULTISOURCE_阶段B2-5_身份作用域与PRIMARY审查队列_v1.json`:以真正的原始 PBF、全量 Actual Catalog、原 Style 和 B2.1 生成的 475 条**暂存 Mapping**计算;每条包括 Visual Object 类型 ID、是否能定位样本 Feature、结构化 Tile 局部 Locator、审查前的 Mapping 状态、各绘制组件候选和 `primaryReviewStatus=PENDING_REVIEW`。
脚本仅创建项目内审计输出,不修改原始 PBF、Style、Sprite、正式 Package、NavSea Profile 或 Compiler。没有新增 `VerifiedVisualBindingSet` 或任何业务授权。
## 3. 三种必须隔离的身份含义
| 层级 | 证据/作用域 | B2.5 结论 |
|---|---|---|
| `CompositeVisualTypeId` | 原始 Style + Sprite 决定的视觉组合类别签名 `visual_object_id` | 475 种;**不是**现实实体 ID |
| `TileLocalFeatureLocator` | 明确的 `providerId,datasetId,sourceId,sourceLayer,z,x,y,rawFeatureId`;在该瓦片实际唯一 | 475 个**记录中的示例**确认,可用于本次样本回溯;不是跨更新稳定承诺 |
| `StableEntityId` | 按 Provider 明示且已检验的 rawId、fidScope、dataVersion、跨瓦片重复/拼接与对象身份规则 | **0 条获得授权**,仍为 unknown |
避免把 `visualObjectId`、`occurrence_id`、`feature.id`、Style Render Layer ID 或屏幕位置混成实体身份。
## 4. 真实数据统计
使用 CM95 Japan S52 Full 实际 Catalog,采样了该 Catalog 475 种视觉类型的各一个原始 Feature 示例,示例位于 261 张真实 PBF 瓦片。对**这 261 张瓦片所有 Source Layer 的全部 247,257 条原始 Feature**核验:
- 可定位的 Visual Object 类型示例:**475/475**;无位置缺失;261 个 distinct tile。
- 261 张瓦片的原始 Feature:**247,257**;本次遍历里 `feature.id` 缺失 **0**,单个 `tile+source-layer` 内 ID 重复的取值 **0**。
- 以 `(source-layer,feature.id)` 对比不同 Tile 时,存在 **10,985** 个跨瓦片复用的组合;其中 **240** 个跨瓦片复用组合的完整属性签名不同。属性不同**不等于**不同现实实体:切片、属性派生、数据更新或不完整源身份均可能造成变化;不能凭这项统计自动推断“碰撞/不安全”。
- 旧 3 张样本瓦片的 2,077 个 occurrence 记录中没有观察到同一 `source-layer+feature.id` 跨样本 Tile 重用;3 样本结果不能推翻新的 261 瓦片结果。
- 使用原有审查资料与完整 Catalog 形成的审计用 Mapping:329 个类型已指派 `standardLayer`,146 个 UNKNOWN 或 AMBIGUOUS。此数据只属于**暂存审计输入**,不是当前发行 ZIP 的覆盖度。旧正式 CM95 `1.1.0` ZIP 仍只有 **102 条 Mapping**。
- 全部 **475 条**待 PRIMARY 审核;`primaryStyleComponentIndex=null`、`stableEntityId=null`、`fidScopePromotionAuthorized=false`、`pickupDeduplicationAuthorized=false`。
- `fidScope` 审计输出保持 **unknown**,与 NavSea 真实 Profile 相符。
## 5. 验证记录
- 仓库虚拟环境 `.venv/bin/python -m unittest discover -s tests -p 'test_*.py' -v`:**30/30 PASS**。新增 B2.5 6 项,原有 B2.1/B2.3/B2.4 24 项仍通过。一次初始回归因 Counter 归零残留未清理而失败,已修复并复跑 30/30 PASS;不能省略该修复历史。
- 原始 PBF 261 Tile 审计重新执行,正式 JSON 输出含 `reusedRawFeatureIdWithDifferingProperties=240`、`reusedRawFeatureIdAcrossExaminedTiles=10985`。
- 两个设备及 Git 分支保持原环境;最终 Git/Style/ZIP 完整性检查见本报告收尾命令记录。
- 本次没有 Android/iOS 实机运行、Jest、Git Commit、Git Push、Tag 或正式 ZIP 新发行,也没有执行第二个**真实** Provider 的发行包验收;合成测试不等于两个真实 Provider。
## 6. 与前期证明的边界
B2.4 的 475/475 是对每个视觉类型选取一个真实 Feature 验证其 Composite 组件全部从同一原始 Feature 重放;B2.5 的 475/475 是对每个视觉类型选取的示例验证其 Tile 内 Feature 身份唯一。两者均不覆盖全量 976,534 次 visual occurrence,也不证明跨版本的实体稳定身份。
B2.3 已发现 29 个含多分类或 residual 的原始 Style Layer、146 个未解决对象以及多组件继承的问题。本轮不解除其阻塞,也不假设 MapLibre 动态过滤已通过真机等价验收。保留 CM95 TEST fishery/LIGHTS05 现存过渡兼容。
## 7. 下一阶段 B2.6 的受控方向
1. **按正式 002B 规则审查 fidScope**:确认 Provider 能否提供跨瓦片稳定对象 ID 的文档证据、适用 Source Layer、更新周期和 Feature 实例去重条件。无法确认就继续 `unknown`,Pickup 仅按 operation-local 可信同源证据做有界去重。
2. **人工 PRIMARY 审查**:根据本次 475 条队列、原始 Style Composite Signature、Provider 语义资料与原始 Feature 对照,明确主图形/附属 Component。可维护版本化审核记录及反例,不能选 `icon` 为 PRIMARY 的自动通用默认。
3. **标准层显示组 Contract**:NavSea 需正式声明 `standardLayer→logicalGroupId` 的审核人、版本与指纹,以及一对多归属时如何限定对象。现有 `pbf_group.json` 的 13 用户组只是当前产品显示入口,不能仅按字符串名称自动推断归属。
4. **安全可执行纵向切片**:先证明一类对象在其涉及的每个原始 Style Layer 的 selector 全部可达、隐藏不误伤其他标准层/UNKNOWN,全部显示时原 Style 严格恢复。通过后才考虑 Compiler 扩大 EXECUTABLE。
5. 新正式 ZIP 必须增版本并通过 GSRL Source Lock、完整 Package 验证、至少两个结构不同的真实 Provider 及 Android/iOS 实机门槛,不覆盖现有 1.1.0。
此报告为阶段性工程证据;不构成新的冻结 RFC/Contract,也不申请用户自动确认跨瓦片实体 ID 或 PRIMARY 语义。

View File

@@ -0,0 +1,26 @@
{
"schemaVersion": "navsea-observed-host-group-candidates/v1",
"scope": "OBSERVED_HOST_SETTINGS_ONLY_NOT_STANDARD_MAPPING",
"approvalStatus": "OBSERVED_NOT_APPROVED",
"source": {
"repository": "D:\\React\\NavSea-PBF-Multisource",
"path": "src/assets/pbf/pbf_group.json",
"sha256": "0a6856f4ff59fa6ba804d1aa6763f3fc44fde23280edd1f1e007654ce405c0ac"
},
"notes": "这些是当前 NavSea UI 的候选组 ID,不是标准层正式归属,不允许按名字推断分类,也不代表可执行映射。",
"groupIds": [
"safety_hazards",
"navigation_marks",
"navigation_mark_labels",
"light_arcs",
"anchorage_services",
"port_facilities",
"bridge_clearance",
"land_area",
"depth_contours",
"bathymetry_detail",
"fishery_gear",
"seabed_material",
"place_labels"
]
}

View File

@@ -0,0 +1,15 @@
{
"schemaVersion": "navsea-standard-display-review-gate-report/v1",
"scope": "STRUCTURAL_REVIEW_ONLY_NOT_RUNTIME_AUTHORIZATION",
"status": "PASS_PENDING_REVIEW",
"standardLayerStatusCounts": {
"PENDING": 15
},
"visualObjectStatusCounts": {
"PENDING": 475
},
"reviewedObjectCount": 0,
"runtimeExecutionAuthorizedCount": 0,
"globalIdentityAuthorizedCount": 0,
"validationErrors": []
}

View File

@@ -0,0 +1,45 @@
# NAVSEA-PBF-MULTISOURCE 阶段 B2.6:标准显示组与 PRIMARY 人工审核门禁 RDC 完成报告 v1
- 日期:2026-10-12
- 状态:**离线审核工具 PASS;人工语义审批 NOT DONE;正式 Contract 未冻结;新增 Runtime 执行授权 0。**
- 上位依据:2026-10-12《NavSea 多源 PBF:通用解释器与标准层映射强制原则 v1》及 MULTISOURCE-002B/004。
## 1. 环境与保护边界
- X220 Linux RDC,仓库 /root/sourceserver/pbf-provider-pipeline-v1,分支 feature/provider-pipeline-v1;原 HEAD d046d7ee0d9f2030d974d0e5435ae6571b0b4489。
- Windows CARBON7i RDC 只读核对 D:\React\NavSea-PBF-Multisource 中的正式文档及 pbf_group.json,NavSea 代码未改。
- 不更改原始 PBF/Style/Sprite、九文件 Provider ZIP、当前 CM95 1.1.0 的 102 条映射、客户端已有 19 条 EXECUTABLE、Fishery/LIGHTS05 兼容。
- 没有进行 Codex、Android/iOS 编译、安装/ADB、Jest、Git reset/clean/commit/push/tag,也未切换工作树。
## 2. 现状事实
- 全量身份审核队列 475 条;329 条已赋类、146 条 UNKNOWN/AMBIGUOUS,275 条多绘制组件。
- 现有审查结果有 15 种非空 standardLayer;但 upstream 的 style-visual-object-standard/v1 文件是特定 Style 的视觉目录,不是标准层至产品显示组权威 Catalog。
- CARBON7i 的 pbf_group.json 有 13 个普通用户组、另有 base_map。其已有 layer_ids 属旧数据/Style 配置;不能根据字段名/图标/相似名称自动分配新来源对象。
## 3. 实际新增代码与数据
- tools/provider-pipeline/standard_display_review_gate.py:生成完整待审草稿;强制分开审 standardLayer→displayGroup 和 VisualObject→PRIMARY/COMPONENT;绑定 provider/dataset、源队列、候选 Host 组、原 Style 的 SHA-256。
- 人审审核状态 PENDING/REVIEWED/REJECTED;REVIEWED 必须附审核人和证据引用。逐组件必须指定唯一 PRIMARY 及所有其它组件指向它的父关联;拒绝重复/缺失/越权字段。
- 未批准 Host 组快照(OBSERVED_NOT_APPROVED)时禁止审核记录通过;UNKNOWN/AMBIGUOUS 不允许在此工具中被强行晋级,需返回上游 Mapping 审核。
- 即使离线审核记录结构符合要求,门禁也永远报告 runtimeExecutionAuthorizedCount=0、globalIdentityAuthorizedCount=0;不是生产 Contract Validator,更不能作为 Compiler 执行授权。
- 草稿已存在时 --generate 拒绝覆盖,防止人工改动被清空。
- tools/provider-pipeline/standard_display_review_gate.py;tests/test_standard_display_review_gate.py(8 个新增测试)。
- docs/报告/NAVSEA-PBF-MULTISOURCE_阶段B2-6_NavSea现有Host显示组候选快照_v1.json:实际 pbf_group.json SHA 0a6856f4ff59fa6ba804d1aa6763f3fc44fde23280edd1f1e007654ce405c0ac;明确未批准。
- docs/报告/NAVSEA-PBF-MULTISOURCE_阶段B2-6_标准层与PRIMARY待审草稿_v1.json:15 条标准层决策、475 条对象决策,全部 PENDING。
- docs/报告/NAVSEA-PBF-MULTISOURCE_阶段B2-6_审核门禁结果_v1.json:PASS_PENDING_REVIEW,0 个已审核对象,0 个新增执行授权。
## 4. 验收与局限
- Python:.venv/bin/python -m unittest discover -s tests -p test_*.py -v,38/38 PASS(B2.1 至 B2.5 原有 30 + B2.6 新增 8)。
- 合成 Provider 不等于两个真实不同 PBF 的实际发行/设备验证;当前仍未打开新的 Pickup/Danger/Planning。
- 显示组候选不是正式批准的、跨 Provider 通用 Catalog;本阶段审核工具不是已冻结 Schema,也不允许靠把候选清单的 approvalStatus 改成 APPROVED 代替真正审核。
- 当前模板一条 standardLayer 暂限单个 displayGroupId;若正式产品允许标准层拆分成多个 UI 组,需另行版本化设计裁决和 selector 范围证明,不能用多个字符串绕过。
## 5. 下一步 B2.7
先正式审定 15 个标准层与 NavSea 显示组的关系,并逐对象审定唯一 PRIMARY 和所有组件从属;UNKNOWN 返回上游语义审核。再做单一已审核类型的原始 Style 逐层精确 selector 纵向切片,验证 residual-preserve、全显恢复 originalFilter、混合层不误隐藏。通过两个真实 Provider 与 Android/iOS 分项门槛后,才考虑扩大 Compiler EXECUTABLE 或重发正式 ZIP。
## 6. 归档结论
工具生成与结构回归完成,不等于人的审核完成。当前实际 result 是 PASS_PENDING_REVIEW,不是 Package/Compiler/Runtime 的 PASS;不作自动授权、Commit/Push 或正式发布。

View File

@@ -0,0 +1,88 @@
# NAVSEA-PBF-MULTISOURCE 阶段 B2.7:人工确认总清单 v1
日期:2026-10-12
**唯一当前阻塞:必须有人审定标准层对应的产品显示组、每类视觉对象唯一 PRIMARY 与 COMPONENT 父关系,并复审 UNKNOWN/AMBIGUOUS。尚未取得任何新运行时授权。**
## 已经自动完成,不需要重复人工核验
- 使用真实 CM95 原始 PBF/Style/Sprite 重放 475 种视觉类型各一个 Feature;475/475 原样复合签名匹配。
- 在 261 张真实 PBF Tile 内对 247257 个原始 Feature 做身份作用域核验;475/475 Catalog 原始示例定位确认;fidScope=unknown,仍无跨瓦片稳定实体 ID 授权。
- 对相关 34287 个原始 Feature 的原始 Style 层进行候选 selector 重放;17148 个 Feature 实际有相关 Style 命中。
- 得到 595 条 Style Layer 级候选 selector,样本内零次跨已知其他 standardLayer、UNKNOWN/AMBIGUOUS 或目录外类别的误命中。另有 225 条逐层记录不能构建候选。
- B2.6 人审门禁已绑定原审核队列、候选 Host 组与原 Style SHA,不会自动提升 EXECUTABLE;旧 Provider ZIP 不变。
- **注意**:以上并非 177383 张 Tile 的全量碰撞证明,亦非真实 MapLibre GPU 比对、法律授权、人工 PRIMARY 或经批准的正式 Standard Display Catalog。
## 第一步:需要人工审定的 15 种 standardLayer
不能凭原始 source-layer、Style layer ID、符号名或字段名直接决定下面的产品组。请对照业务含义、原始 Provider 语义资料以及现有 NavSea 产品显示策略给出明确决策。未获得证据时保持 PENDING。
| 当前 standardLayer | 对象数 | 多组件 | 样本无误归对象数 | 仍缺候选对象数 | displayGroupId(人工确认) | 审核结论 |
|---|---:|---:|---:|---:|---|---|
| navigation_marks | 186 | 141 | 185 | 1 | **待审** | PENDING |
| onshore_structure_point | 80 | 58 | 80 | 0 | **待审** | PENDING |
| anchorage_point | 11 | 5 | 11 | 0 | **待审** | PENDING |
| anchor_caution_hazard_point | 10 | 4 | 9 | 1 | **待审** | PENDING |
| facility_boundary_point | 9 | 2 | 9 | 0 | **待审** | PENDING |
| fixed_fishing_gear_area | 8 | 5 | 2 | 6 | **待审** | PENDING |
| clip_outline_754 | 6 | 4 | 2 | 4 | **待审** | PENDING |
| navigation_hazard_point | 3 | 2 | 3 | 0 | **待审** | PENDING |
| bridge_structure | 3 | 3 | 3 | 0 | **待审** | PENDING |
| pilot_station_point | 3 | 2 | 3 | 0 | **待审** | PENDING |
| facility_boundary_outline | 3 | 0 | 3 | 0 | **待审** | PENDING |
| seabed_line | 3 | 0 | 0 | 3 | **待审** | PENDING |
| land_area | 2 | 1 | 0 | 2 | **待审** | PENDING |
| baseline_line | 1 | 0 | 0 | 1 | **待审** | PENDING |
| hazard_boundary_outline | 1 | 1 | 0 | 1 | **待审** | PENDING |
NavSea 当前仅有以下 13 个普通产品显示组 ID 的真实候选快照,并**没有**把任何 standardLayer 批准给这些组:
`safety_hazards`, `navigation_marks`, `navigation_mark_labels`, `light_arcs`, `anchorage_services`, `port_facilities`, `bridge_clearance`, `land_area`, `depth_contours`, `bathymetry_detail`, `fishery_gear`, `seabed_material`, `place_labels`。
另有基础 `base_map`;这不是普通用户可关的显示组。若 standardLayer 需要一对多归属,现有 B2.6 最小草稿只能表示一个显示组,必须先裁决 Contract 扩展,不能强行写多值。
## 第二步:每个 Visual Object 的 PRIMARY/COMPONENT 审核
- 475/475 项在 B2.6 审核草稿中都为 PENDING。已分类 329 项不等于已确认 PRIMARY。多组件 275 项需要指定唯一 PRIMARY 和所有其他组件的父关联。
- 186 项属于 `navigation_marks`、80 项属于 `onshore_structure_point`;可按这些分类和组件结构分批审核,不能因为图标/标签相似就机器批量批准。
- 146 项 UNKNOWN/AMBIGUOUS 不允许在 PRIMARY 草稿中绕过上游语义审查。应回 PBF-04 审核其对象语义、冲突原因和证据,必要时新版本 Mapping。
- 310 类对象在本轮涉及的全部逐层候选中均为样本无禁忌命中;165 类对象至少一层仍没有候选或分类未决。二者都不能绕过人工 PRIMARY 审查,也不是可运行对象计数。
### 代表性待审实例(只做定位,不构成推荐语义)
| Visual Object 类型 | 当前 standardLayer | 样本 tile | PBF feature ID | 绘图组件数量 | 候选 selector 状态 |
|---|---|---|---|---:|---|
| `VO-01e305d8405ac611` | navigation_marks | 10/859/423 | 2548848 | 3 | 抽样无禁忌命中 |
| `VO-12055cc4fbfb8bc5` | onshore_structure_point | 11/1765/830 | 3936400 | 3 | 抽样无禁忌命中 |
| `VO-502108ab5fce38fc` | anchorage_point | 11/1741/812 | 10319648 | 3 | 抽样无禁忌命中 |
| `VO-0e70729abbe6c9cd` | anchor_caution_hazard_point | 10/872/408 | 2177328 | 2 | 抽样无禁忌命中 |
| `VO-60c34a31d299f02d` | facility_boundary_point | 11/1719/844 | 7804704 | 2 | 抽样无禁忌命中 |
| `VO-0847136d5f3f360b` | fixed_fishing_gear_area | 11/1718/840 | 7923504 | 2 | 缺候选/证据 |
| `VO-3379d23c914877ef` | clip_outline_754 | 10/859/418 | 557408 | 2 | 抽样无禁忌命中 |
| `VO-020fbfda3e5b1746` | navigation_hazard_point | 11/1762/825 | 8456912 | 2 | 抽样无禁忌命中 |
| `VO-0ba121b78c676422` | bridge_structure | 12/3588/1627 | 14001600 | 3 | 抽样无禁忌命中 |
| `VO-18c8596124a39bce` | pilot_station_point | 11/1751/812 | 10452704 | 2 | 抽样无禁忌命中 |
| `VO-1302f166f1f64ce7` | facility_boundary_outline | 11/1718/797 | 11280656 | 1 | 抽样无禁忌命中 |
| `VO-7c58d21a5a283b16` | seabed_line | 10/859/419 | 1752576 | 1 | 缺候选/证据 |
| `VO-0a99121a73947f27` | land_area | 11/1767/754 | 16555296 | 2 | 缺候选/证据 |
| `VO-68ce4a8207ba8bc9` | baseline_line | 10/859/420 | 2085488 | 1 | 缺候选/证据 |
| `VO-76b2e78748663724` | hazard_boundary_outline | 11/1751/812 | 10448240 | 2 | 缺候选/证据 |
## 最终人工确认前后的严格顺序
1. 人工批准标准层→显示组 Catalog 的完整对应关系、证据、版本和签名策略(必须显式说明未分配的标准层如何 fail-closed)。
2. 在 `NAVSEA-PBF-MULTISOURCE_阶段B2-6_标准层与PRIMARY待审草稿_v1.json` 的完整 475 条清单内逐条审核,或者另经人审核并冻结安全批量规则;每条要有 reviewer + evidenceRefs,不能让工具自动填。
3. 对 UNKNOWN/AMBIGUOUS 使用上游 PBF-04 审核通路重定分类;已知冲突要显式处理并保留反例。评审后重新跑 B2.3/B2.4/B2.5/B2.6/B2.7,以新 Mapping 源锁保证无修改遗漏。
4. 在正式 Contract 审批前,NavSea Compiler/Runtime **不得**接入本轮新候选 selector;新 Package 必须另起版本,不覆盖 1.1.0。
5. 人审和正式契约完成后才可进行小范围通用 Compiler 实施,然后做 Android/iOS 实机核对四档、混合图层、残余保留、原 Style/碰撞顺序、Pickup(再单独检查 Danger/Planning)。
## 机器留档入口
- `docs/报告/NAVSEA-PBF-MULTISOURCE_阶段B2-7_逐层候选Selector真实样本冲突审计_v2.json`:逐 Style Layer 820 条记录、样本命中与反例字段。
- `docs/报告/NAVSEA-PBF-MULTISOURCE_阶段B2-5_身份作用域与PRIMARY审查队列_v1.json`:475 条 Tile 内真实证据、组件候选。
- `docs/报告/NAVSEA-PBF-MULTISOURCE_阶段B2-6_标准层与PRIMARY待审草稿_v1.json`:独立的 15 + 475 条正式人工审查草稿。
- `docs/报告/NAVSEA-PBF-MULTISOURCE_阶段B2-6_审核门禁结果_v1.json`:当前 PASS_PENDING_REVIEW,不是正式人审通过。
## 停止条件
**当前已进入必须人工进行设计/语义验证的门槛。继续自动推导 displayGroup 或 PRIMARY 会违反用户确认的通用解释器原则,故新业务权限继续为零。**

View File

@@ -0,0 +1,56 @@
# NAVSEA-PBF-MULTISOURCE 阶段 B2.7:持续自动验证至人工门槛 RDC 完成报告 v1
日期:2026-10-12
结论:**上游自动审计实现及离线回归 PASS;停在必须由人批准的标准层显示组、PRIMARY/COMPONENT 及 UNKNOWN 语义门槛。** 新增可执行 Provider Binding 0、正式 Package 0、NavSea Runtime 修改 0。
## 一、实际工作环境与上位规定
- Windows CARBON7i 的 NavSea Worktree 为 D:\React\NavSea-PBF-Multisource,branch feature/pbf-multisource、HEAD 78d7ffeccc95469ef3be62701b7b805b5a50c1f8。本轮只读核对 2026-10-12 的《通用解释器与标准层映射强制原则》及 002/002B/004 契约,不改动 NavSea 源码。
- RDC Linux X220 上游仓库 /root/sourceserver/pbf-provider-pipeline-v1,branch feature/provider-pipeline-v1,开始时 HEAD d046d7ee0d9f2030d974d0e5435ae6571b0b4489;本轮代码与本报告均写入该仓库,不切换分支/worktree,不做 Git reset/clean/add/commit/push。
- 最优先原则:原始 PBF、Style、Sprite、Glyph 都是视觉/数据事实;不能在 NavSea Runtime 认识 CM95 专属字段,不能重绘、复制大量符号层或将未知对象擅自归类。保持既有 Fishery/LIGHTS05 过渡性修复;没有 Android/iOS 编译、安装、ADB、Jest、Codex。
- 所有阶段数据均使用旧正式 CM95 1.1.0 原始 Style SHA b1d3d0060012a8e4b55a6053231231d742ec415b3a8398f9ae0867bbdfa5290b;旧发行 ZIP SHA256 fb5bc2606ab61662c16eccd22225532779830779e931f623bac4cf049e6b737f,禁止覆盖。
- 真实当前正式 ZIP 的 Mapping 是 102 条。此次演算输入为前阶段上游全量 Catalog + 原人工分类构造出的**仅供审计的 475 条临时 Mapping**,不等于已发布新 ZIP 或经批准的正式合同。
## 二、B2.7 新增自动证据
### 1. 逐 Style Layer Selector 冲突审计
- 新工具 tools/provider-pipeline/selector_collision_evidence_audit.py;对应 tests/test_selector_collision_evidence_audit.py(7 项)。
- 工具只利用原始 Style 的直接 get 资源表达式生成候选;支持同原始 Feature 多组件跨 Layer 的候选组合/继承,但绝不把结果作为 PRIMARY 或 Runtime 授权。
- 逐 Feature 运行原来的 PBF-03 Style 实例化评估器,检验候选是否命中不同标准层、UNKNOWN/AMBIGUOUS、Catalog 外对象;不依赖 CM95 语义字段名。
- 根据 Catalog 中 475 个 Visual Object 的原始例子确定 261 张真实 PBF Tile,扫描其中与候选相关的 34287 个原始 Feature;原始 Style 实际命中 17148 个。
- 共分析 820 条原始组件级 Style Layer 绑定:595 条有候选、样本中未发现跨标准层或 UNKNOWN/RESIDUAL 误命中;225 条没有候选。累计同类之外但同 standardLayer 的其它视觉类型命中 18090 次,不代表它们是独立对象或已完成 PRIMARY 审核。
- 从对象层面看,310 种 Visual Object 所有涉及的候选层均获得本样本的零禁忌误命中观察;165 种至少一层缺候选/分类未决,其中 146 种属于 UNKNOWN/AMBIGUOUS,其余 19 种虽有人工标准层名称仍缺机器可证明 selector。
- 这个结论**仅**适用于这 261 张瓦片的静态可执行表达式重放。全部 177383 张 Tile 的逐 Feature 碰撞检查、真正 GPU 的 Style Filter/Placement 行为、未来新增数据和安全业务行为并未验证,不能宣称实际通用完整显示已 PASS。
- 增加 Mapping/Catalog VO ID 必须一致、相同 source-layer 多 Style Source 歧义拒绝等 fail-closed 测试,保护输入来源。
- 最终留档:docs/报告/NAVSEA-PBF-MULTISOURCE_阶段B2-7_逐层候选Selector真实样本冲突审计_v2.json。v1 是先前同主题的历史版本,不可替代 v2 最终结构门禁结果。
### 2. 完整视觉类型目录对原 Style Layer 的隔离审计
- 新工具 tools/provider-pipeline/style_layer_isolation_audit.py;对应 tests/test_style_layer_isolation_audit.py(5 项)。
- 真实原始 Style 170 层;全量 475 类 Visual Object 的 Mapping renderEvidence 静态分布:29 层混合多个标准层或残余对象;39 层只出现未解决/残余对象;33 层只出现一个已分类 standardLayer(仅同数据版本的整层控制候选);69 层没有 Catalog 里对应可绘制对象。
- 33 层的“单类候选”**不是**整层 visibility 的自动授权,仍可能有源数据新增、原 Filter 数据覆盖、动态 Style/MapLibre 行为等问题;29 层必须保持对象级控制的要求。
- 最终留档:docs/报告/NAVSEA-PBF-MULTISOURCE_阶段B2-7_StyleLayer全量归属隔离审计_v2.json;历史 v1 仅用于追踪早期分类口径,v2 细分纯 residual。
### 3. 统一人工交接清单
- docs/报告/NAVSEA-PBF-MULTISOURCE_阶段B2-7_人工确认总清单_v1.md:逐标准层 15 行,含分类数量、多组件数量、候选证据现状、各组代表性真实 Feature ID/Tile,以及必须人工确认的位置和顺序。
- B2.6 先前已建具 SHA 绑定的待审草稿:docs/报告/NAVSEA-PBF-MULTISOURCE_阶段B2-6_标准层与PRIMARY待审草稿_v1.json,15 种标准层全部 PENDING、475 个 Visual Object 全部 PENDING。B2.6 严格结构门禁 PASS_PENDING_REVIEW≠语义批准;不能自动修改 approvalStatus 冒充用户审核。
- NavSea UI 已知组 ID 仅为 13 个普通显示组 + base_map 基础组。上游既有冻结的 style-visual-object-standard/v1 是特定 Style 的视觉 Catalog,不是已批准的跨 Provider 标准层归组契约。
## 三、必须人确认,机器不可再推断的项目
1. 标准层→NavSea 产品显示组(15 类)的语义对应关系、边界、审查证据和版本化 Catalog。不能按标准层名字相像就匹配组名;标准层跨多个显示组的方案需先裁决是否可表达、如何避免双重控制。
2. 每类对象唯一 PRIMARY 与其所有 COMPONENT 父子关系(475 条草稿;275 条多组件特别需要关注)。绘图的 icon/text/line/fill 不等于 PRIMARY/COMPONENT。
3. UNKNOWN/AMBIGUOUS 146 类和已知分类冲突需上游人工依据 Provider 资料审查后才能更新 Mapping,不可由工具自动提升。
4. 以上合同得到人工批准后,才进入少量已审核对象的通用 Compiler/Importer 最小闭环,再安排真实 Android/iOS 实机四档、原 Style 恢复、混合层 residual 保留、Pickup 和 Danger/Planning 分项验收。此步骤尚未实施,不可声称产品已通过。
## 四、验收收尾
- X220 仓库虚拟环境运行 .venv/bin/python -m unittest discover -s tests -p 'test_*.py' -v:最终 **50/50 PASS**、退出 0(既有 38 项 + 新增逐层 Selector 7 项 + Style 隔离 5 项)。
- 静态 checker:候选产生不能擅自授予 displayRuntimeAuthorizedCount / primaryAuthorizedCount,Style Layer 隔离的 runtimeAuthorizationCount 恒为 0。
- git diff --check PASS;全部 8 个本阶段关键代码、测试与报告文件 UTF-8/非空/无尾随空格自检 PASS;B2.7 两份最终 JSON 的计数和 0 授权断言 PASS。
- 原发行 CM95 1.1.0 ZIP SHA-256 为 fb5bc2606ab61662c16eccd22225532779830779e931f623bac4cf049e6b737f,与原值一致。
- 最终分支 feature/provider-pipeline-v1,最终 HEAD d046d7ee0d9f2030d974d0e5435ae6571b0b4489(没有 Commit/Push)。工作区原 B2.1~B2.6 未提交修改和未跟踪文档均保留,不清理或覆盖。
**准确的停点:所有可以独立完成的当前上游只读/候选生成及样本证明已交付;正式标准层显示组与 PRIMARY 语义必须由人工确认,此处 STOP / AWAIT HUMAN SEMANTIC APPROVAL。**

View File

@@ -0,0 +1,63 @@
# NAVSEA-PBF-MULTISOURCE:阶段 B2 全量 Catalog 映射修正 RDC 阶段报告 v1
日期:2026-10-12
结论:上游 **Pipeline 代码修复和静态/内存回归 PASS**;正式 ZIP 再生产、发行、NavSea 全链路与实机验收 **NOT DONE**。
## 一、目标与现有问题
按 NavSea 多源 PBF《通用解释器与标准层映射强制原则 v1》,上游应交付已经审查、可验证的 Visual Object → NavSea 标准层映射,并明确未确认/冲突对象;客户端不能为了 CM95 特例改写原始 PBF 或 Style。
当前正式 CM95 `1.1.0` 包引用全日本 Actual Visual Object Catalog:共扫描 177,383 个 tile,含 475 类不同 Visual Object、976,534 次 matched visual occurrence。但 ZIP 的 Mapping 只有 102 类。经代码核对,`run_provider_pipeline.py` 的 PBF-04 仍使用 3 张样本瓦片得到的 `actual` 做 `manual_assignment / standard_assignment`;声明的全量 `executionEvidenceCatalog` 仅被用来给这 102 类补 `renderEvidence`。因此缺失 373 类、73,745 次出现,并非全部自动作为明确 UNKNOWN 交付。
## 二、真实环境与仓库
- 操作设备:RDC / tei-ThinkPad-X220(Linux)。
- 仓库:`/root/sourceserver/pbf-provider-pipeline-v1`。
- 当前 Worktree 分支:`feature/provider-pipeline-v1`;实施前后 HEAD 均为 `d046d7ee0d9f2030d974d0e5435ae6571b0b4489`。
- 上游原始 Catalog:`tools/visual-object-instantiation/output/cm95-current/actual-anonymous-catalog.json`。
- 人工审查来源:`tools/style-visual-object/output/cm95-manual-review/assignment/cm95-layer-assignment-v1.json`。
- 正式旧 ZIP:`dist/cm95/s52-full/cm95-s52-full-1.1.0.navsea-provider.zip`;SHA-256 `fb5bc2606ab61662c16eccd22225532779830779e931f623bac4cf049e6b737f`。
- 没有改 PBF、原始 Style、Sprite、正式 ZIP、已发布目录或对应 manifest;没有新建/切换分支与 Worktree,没有 Git Commit/Push/Tag。
## 三、实际源码修改
1. `tools/provider-pipeline/run_provider_pipeline.py`:新增 `select_mapping_catalog(sample_catalog, execution_catalog)`,当 manifest 声明全量 Execution Catalog 且已通过 `load_execution_catalog` 的 Style SHA-256 校验时,用该 Catalog 作为 Mapping 分类全集;拒绝无效、重复 ID、或没有覆盖全部样本 Visual Object 的全量 Catalog。
2. PBF-04 的 manual/frozen-standard 两种模式统一从 `mapping_actual` 生成 Assignments,样本 `occurrenceIds` 仍从样本实际采集结果提取;`attach_render_evidence` 绑定全量 Catalog,不增加新 ZIP 文件、不变更九文件布局或 Mapping v2 schema。
3. pipeline-report 的 PBF-04 状态增加 `catalogScope`、`sampleVisualObjects`、`mappingVisualObjects`,明确是声明的全量 Catalog 还是样本集合;若仍有 UNKNOWN、AMBIGUOUS、COMPONENT_ONLY 或缺失证据,PBF-04 返回 `PASS_WITH_WARNINGS`,不得以未缺少 renderEvidence 就报告完全 PASS。
4. `tests/test_provider_pipeline.py`:新增全量 Catalog 选择/拒绝规则和真实 CM95 Catalog 回归检查,确保原有 102 条状态及 standardLayer 不被擅自改写。
## 四、模拟重算与验证结果
原始正式 Mapping:102 条,其中 LAYER_ONLY=47、VISUAL_OBJECT_MATCH=9、UNKNOWN=44、AMBIGUOUS=2;覆盖 902,789 次 occurrence。
本轮使用**原有审核资料 + 实际完整 Catalog**进行内存重算,得到:
- Visual Object:475/475。
- LAYER_ONLY:303;VISUAL_OBJECT_MATCH:26;UNKNOWN:135;AMBIGUOUS:11。
- 已指派 standardLayer:329;待确认/歧义:146;每条均能找到 renderEvidence,缺失 0。
- 976,534/976,534 次 visual occurrence 纳入统计;历史遗漏的 373 类、73,745 次被纳入新的 Mapping 候选,但其中不能自动提升业务能力。
- 原有 102 条的 `status + standardLayer` 比对一致(102/102)。
- 475 条候选提交给现有 `provider_policy.validate_mapping()`:错误 0。
- 全量对象中含多绘制组件 275 类;含至少一个无资源键组件 259 类;有人工 `COMPONENT` 标签 191 类,均需后续更严格的 PRIMARY/COMPONENT 和逐层 selector 审查。
- `python3 -m unittest discover -s tests -p test_provider_pipeline.py -v`:14/14 PASS,退出码 0。
- `git diff --check`:PASS,退出码 0。
注意:以上是**内存重算与代码逻辑验证**,没有重新下载 PBF 或生成/覆盖正式 ZIP,没有完成两套不同 Provider 实装的对照验收。
## 五、确认事实与未确认事项
- 已确认:根因在 Package 制作端选择 Mapping 数据全集时误用样本;不应在 NavSea Runtime 修补这 373 类。
- 已确认:全量 Catalog 数据包含原始 Style 组件与数量信息,但 `role` 是 icon/text/line/fill 等绘制角色,并不能直接证明 PRIMARY/COMPONENT 父子身份。
- 尚未确认:全量 Catalog 的 tile 覆盖事实是否可通过当前 Package Manifest 唯一、不可变地证明;当前 `load_execution_catalog` 主要校验 Schema 与 Style SHA,而不是完整数据发布范围的封闭证明。
- 尚未完成:NavSea 固定标准层到显示组的版本化权威映射;多组件逐层 selector,可执行身份、residual/conflict 处理的审查结果。
- 尚未完成:Provider ZIP 重新制作、GSRL Source Lock、Git 发行 Tag、正式发布、NavSea 导入、Android/iOS 实机、Pickup/Danger/Planning/URL 完整闭环。
- 既有 CM95 1.1.0 包必须按**旧 102 条事实**报告,不得宣称它已经变成 475 条。
## 六、推荐下一步
B2.2 先形成单一可核验的 NavSea 标准层 → Host 显示组 Catalog/Contract,明确 Profile/Package 中的权威来源、版本、指纹和审查责任,不允许按 source-layer、Symbol key 或同名字面相似作默认映射。为每个 Visual Object 建立唯一 PRIMARY 与可信组件从属、各 Style Layer 的 selector 可达证据;无证明对象必须 residual-preserve,精准开关与 Pickup/Danger/Planning 均按证据降级。
在上述规则获得正式确认以前,不扩张客户端 `EXECUTABLE`,不删除 CM95 TEST 鱼礁/LIGHTS05 兼容方案。全量 475 条 Mapping 的正式新 ZIP 必须获得**新的版本和完整发行验收**,不能覆盖目前的 1.1.0。
## 七、Git 与交付
本任务未 Commit、Push、Tag 或做正式发行;本报告自身为新增未跟踪文档。首次运行的静态回归、重算结果均为真实检查结果,不能作为 Android/iOS 或整个 Product COMPLETE 的替代。

View File

@@ -0,0 +1,144 @@
"""B2.5:不同 Provider 形态、Tile ID 作用域和 PRIMARY 审核门禁。"""
from __future__ import annotations
import importlib.util
from pathlib import Path
import tempfile
import unittest
from mapbox_vector_tile.Mapbox import vector_tile_pb2
BASE = Path(__file__).resolve().parents[1]
MODULE_FILE = BASE / "tools/provider-pipeline/feature_identity_scope_audit.py"
spec = importlib.util.spec_from_file_location("feature_identity_scope_audit", MODULE_FILE)
module = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(module)
def write_tile(root: Path, tile: str, source_layer: str, feature_ids: list[int | None], *, property_value: str | None = None):
obj = vector_tile_pb2.tile()
layer = obj.layers.add()
layer.name = source_layer
layer.version = 2
layer.extent = 4096
if property_value is not None:
layer.keys.append("sampleLabel")
prop = layer.values.add()
prop.string_value = property_value
for fid in feature_ids:
feature = layer.features.add()
if fid is not None:
feature.id = fid
feature.type = 1
feature.geometry.extend([9, 0, 0])
if property_value is not None:
feature.tags.extend([0, 0])
target = root / f"{tile}.pbf"
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(obj.SerializeToString())
def visual_object(name: str, tile: str, source_layer: str, fid: int | None):
return {
"visual_object_id": name,
"provenance": {"source_layer": source_layer},
"example": {"tile": tile, "feature_id": fid},
"components": [{"style_layer_id": "shape", "style_index": 0, "role": "icon", "resource_key": "X"}],
}
class FeatureIdentityScopeTests(unittest.TestCase):
def test_two_provider_field_independent_tile_scoped_locator(self):
for source, layer, label in (("alpha", "points", "VO-A"), ("other-provider", "objects", "VO-B")):
with self.subTest(source=source), tempfile.TemporaryDirectory() as folder:
root = Path(folder)
write_tile(root, "1/0/0", layer, [7, 8])
style = {"layers": [{"id": "shape", "source": source, "source-layer": layer}]}
mapping = {"assignments": [{"visualObjectId": label, "status": "LAYER_ONLY", "standardLayer": "navigation_marks"}]}
audit = module.audit_scope(
{"visual_objects": [visual_object(label, "1/0/0", layer, 7)]},
style, root, provider_id=source, dataset_id="d1", mapping=mapping,
)
self.assertEqual(audit["exampleLocatorStatusCounts"], {"TILE_LOCAL_LOCATOR_CONFIRMED": 1})
self.assertEqual(audit["fidScope"], "unknown")
row = audit["reviewQueue"][0]
self.assertEqual(row["tileLocalFeatureLocator"]["sourceId"], source)
self.assertIsNone(row["stableEntityId"])
self.assertIsNone(row["primaryStyleComponentIndex"])
self.assertEqual(row["primaryReviewStatus"], "PENDING_REVIEW")
self.assertFalse(row["pickupDeduplicationAuthorized"])
self.assertEqual(row["assignmentStatus"], "LAYER_ONLY")
def test_reused_raw_id_across_tiles_not_global_claim(self):
with tempfile.TemporaryDirectory() as folder:
root = Path(folder)
write_tile(root, "1/0/0", "objects", [33], property_value="north")
write_tile(root, "1/1/0", "objects", [33], property_value="south")
style = {"layers": [{"id": "shape", "source": "beta", "source-layer": "objects"}]}
catalog = {"visual_objects": [
visual_object("VO-A", "1/0/0", "objects", 33),
visual_object("VO-B", "1/1/0", "objects", 33),
]}
out = module.audit_scope(catalog, style, root, provider_id="beta", dataset_id="d")
self.assertEqual(out["reusedRawFeatureIdAcrossExaminedTiles"], 1)
self.assertEqual(out["reusedRawFeatureIdWithDifferingProperties"], 1)
self.assertEqual(out["exampleLocatorStatusCounts"], {"TILE_LOCAL_LOCATOR_CONFIRMED": 2})
self.assertEqual(out["globalIdentityAuthorizedCount"], 0)
self.assertNotEqual(
out["reviewQueue"][0]["tileLocalFeatureLocator"]["x"],
out["reviewQueue"][1]["tileLocalFeatureLocator"]["x"],
)
def test_duplicate_raw_id_same_tile_blocked(self):
with tempfile.TemporaryDirectory() as folder:
root = Path(folder)
write_tile(root, "1/0/0", "objects", [12, 12])
catalog = {"visual_objects": [visual_object("VO-A", "1/0/0", "objects", 12)]}
style = {"layers": [{"id": "shape", "source": "beta", "source-layer": "objects"}]}
out = module.audit_scope(catalog, style, root, provider_id="beta", dataset_id="d")
self.assertEqual(out["exampleLocatorStatusCounts"], {"FEATURE_ID_AMBIGUOUS": 1})
self.assertEqual(out["examinedTileRawFeatureStats"]["duplicateFeatureIdValues"], 1)
self.assertIsNone(out["reviewQueue"][0]["tileLocalFeatureLocator"])
def test_missing_id_or_ambiguous_source_remains_unknown(self):
with tempfile.TemporaryDirectory() as folder:
root = Path(folder)
write_tile(root, "1/0/0", "objects", [None])
obj = visual_object("VO-A", "1/0/0", "objects", None)
style = {"layers": [{"id": "shape", "source": "beta", "source-layer": "objects"}]}
out = module.audit_scope({"visual_objects": [obj]}, style, root, provider_id="beta", dataset_id="d")
self.assertEqual(out["exampleLocatorStatusCounts"], {"FEATURE_ID_UNAVAILABLE": 1})
obj["example"]["feature_id"] = 7
style["layers"].append({"id": "other", "source": "gamma", "source-layer": "objects"})
out = module.audit_scope({"visual_objects": [obj]}, style, root, provider_id="beta", dataset_id="d")
self.assertEqual(out["exampleLocatorStatusCounts"], {"STYLE_SOURCE_SCOPE_UNPROVEN": 1})
def test_two_visual_types_claiming_same_raw_feature_block_both(self):
with tempfile.TemporaryDirectory() as folder:
root = Path(folder)
write_tile(root, "1/0/0", "objects", [42])
style = {"layers": [{"id": "shape", "source": "beta", "source-layer": "objects"}]}
catalog = {"visual_objects": [
visual_object("VO-A", "1/0/0", "objects", 42),
visual_object("VO-B", "1/0/0", "objects", 42),
]}
out = module.audit_scope(catalog, style, root, provider_id="beta", dataset_id="d")
self.assertEqual(
out["exampleLocatorStatusCounts"],
{"EXAMPLE_LOCATOR_SHARED_BY_VISUAL_TYPES": 2},
)
self.assertTrue(all(row["tileLocalFeatureLocator"] is None for row in out["reviewQueue"]))
self.assertTrue(all(row["identityScope"] == "UNVERIFIED" for row in out["reviewQueue"]))
def test_invalid_tile_and_duplicate_visual_type_rejected(self):
obj = visual_object("VO-A", "../1/0", "objects", 10)
with self.assertRaisesRegex(ValueError, "路径"):
module.audit_scope({"visual_objects": [obj]}, {"layers": []}, Path("/tmp"), provider_id="p", dataset_id="d")
obj["example"]["tile"] = "1/0/0"
with self.assertRaisesRegex(ValueError, "重复"):
module.audit_scope({"visual_objects": [obj, obj]}, {"layers": []}, Path("/tmp"), provider_id="p", dataset_id="d")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,119 @@
"""按原始 Feature 重放,覆盖两种 Provider 字段结构、身份歧义及跨层继承。"""
from __future__ import annotations
import importlib.util
import json
from pathlib import Path
import tempfile
import unittest
from mapbox_vector_tile.Mapbox import vector_tile_pb2
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "tools/provider-pipeline/feature_lineage_replay_audit.py"
spec = importlib.util.spec_from_file_location("lineage_audit_module", SCRIPT)
audit = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(audit)
engine = audit.load_instantiator()
def make_provider(root: Path, field: str, value: str, multi: bool, *, duplicates: bool = False):
tile = vector_tile_pb2.tile()
layer = tile.layers.add()
layer.name = "objects"
layer.version = 2
layer.extent = 4096
layer.keys.extend([field, "text"])
string1 = layer.values.add()
string1.string_value = value
string2 = layer.values.add()
string2.string_value = "A"
for _ in range(2 if duplicates else 1):
feature = layer.features.add()
feature.id = 101
feature.type = 1
feature.tags.extend([0, 0, 1, 1])
feature.geometry.extend([9, 0, 0])
tile_path = root / "1/0/0.pbf"
tile_path.parent.mkdir(parents=True, exist_ok=True)
tile_path.write_bytes(tile.SerializeToString())
layers = [{"id": "picture", "type": "symbol", "source": "provider-a",
"source-layer": "objects", "layout": {"icon-image": ["get", field]}}]
if multi:
layers.append({"id": "label", "type": "symbol", "source": "provider-a",
"source-layer": "objects", "layout": {"text-field": ["get", "text"]}})
style = {"layers": layers}
sprites = {value: {"x": 0, "y": 0, "width": 1, "height": 1}}
hashes = {value: "visual-hash-for-test"}
props = {field: value, "text": "A"}
components = []
for index, part in enumerate(layers):
components.extend(engine.component_from_layer(part, index, props, 1, sprites, hashes))
visual_id = engine.stable_id("VO", {
"geometry_role": "Point",
"components": engine.signature_components(components),
})
obj = {
"visual_object_id": visual_id,
"provenance": {"source_layer": "objects"},
"example": {"tile": "1/0/0", "feature_id": 101},
"components": components,
}
return {"visual_objects": [obj]}, style, sprites, hashes
class FeatureLineageTests(unittest.TestCase):
def test_two_provider_field_shapes_and_same_feature_components(self):
for field, value, multi in (("symbol", "A", False), ("providerIconCode", "ALT", True)):
with self.subTest(field=field), tempfile.TemporaryDirectory() as folder:
cat, style, sprites, hashes = make_provider(Path(folder), field, value, multi)
out = audit.replay_catalog(cat, style, sprites, hashes, Path(folder), instantiator=engine)
self.assertEqual(out["statusCounts"], {"SAME_RAW_FEATURE_REPLAY_CONFIRMED": 1})
self.assertEqual(out["results"][0]["replayedComponentCount"], 2 if multi else 1)
self.assertTrue(out["results"][0]["sameRawFeature"])
self.assertEqual(out["primaryComponentAuthorizedCount"], 0)
self.assertEqual(out["displaySelectorAuthorizedCount"], 0)
def test_feature_id_not_found_and_duplicate_identity_fail_closed(self):
with tempfile.TemporaryDirectory() as folder:
root = Path(folder)
cat, style, sprites, hashes = make_provider(root, "mark", "ICON", True)
cat["visual_objects"][0]["example"]["feature_id"] = 998
out = audit.replay_catalog(cat, style, sprites, hashes, root, instantiator=engine)
self.assertEqual(out["results"][0]["status"], "FEATURE_ID_NOT_FOUND")
with tempfile.TemporaryDirectory() as folder:
root = Path(folder)
cat, style, sprites, hashes = make_provider(root, "mark", "ICON", True, duplicates=True)
out = audit.replay_catalog(cat, style, sprites, hashes, root, instantiator=engine)
self.assertEqual(out["results"][0]["status"], "FEATURE_ID_NOT_UNIQUE")
def test_modified_composite_hash_is_not_accepted(self):
with tempfile.TemporaryDirectory() as folder:
root = Path(folder)
cat, style, sprites, hashes = make_provider(root, "symbol", "REAL", True)
cat["visual_objects"][0]["components"][0]["resource_visual_hash"] = "bad"
out = audit.replay_catalog(cat, style, sprites, hashes, root, instantiator=engine)
self.assertEqual(out["results"][0]["status"], "CATALOG_COMPONENT_MISMATCH")
def test_source_layer_collision_fails_closed(self):
with tempfile.TemporaryDirectory() as folder:
root = Path(folder)
cat, style, sprites, hashes = make_provider(root, "symbol", "REAL", True)
style["layers"].append({**style["layers"][0], "id": "second-provider",
"source": "provider-b"})
out = audit.replay_catalog(cat, style, sprites, hashes, root, instantiator=engine)
self.assertEqual(out["results"][0]["status"], "AMBIGUOUS_STYLE_SOURCE")
def test_missing_tile_fails_closed(self):
with tempfile.TemporaryDirectory() as folder:
root = Path(folder)
cat, style, sprites, hashes = make_provider(root, "symbol", "REAL", True)
(root / "1/0/0.pbf").unlink()
out = audit.replay_catalog(cat, style, sprites, hashes, root, instantiator=engine)
self.assertEqual(out["results"][0]["status"], "PBF_TILE_OR_SOURCE_LAYER_UNAVAILABLE")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,107 @@
"""通用 Mapping 证据审计:两种不同 Provider 结构及 fail-closed 规则。"""
import importlib.util
import json
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[1]
path = ROOT / "tools/provider-pipeline/mapping_execution_evidence_audit.py"
spec = importlib.util.spec_from_file_location("audit_mapping_module", path)
audit = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(audit)
def item(identity, standard, status, layer_indexes, style, keys):
return {
"visualObjectId": identity, "status": status, "standardLayer": standard,
"renderEvidence": {
"sourceLayer": style["layers"][layer_indexes[0]]["source-layer"],
"geometryRole": "Point", "occurrenceCount": 1,
"components": [
{"styleIndex": index, "styleLayerId": style["layers"][index]["id"],
"role": "icon" if style["layers"][index]["layout"].get("icon-image") else "text",
"resourceKey": key}
for index, key in zip(layer_indexes, keys)
],
},
}
class EvidenceAuditTests(unittest.TestCase):
def test_provider_a_direct_and_unknown_mixed_layer_preserved(self):
style = {"layers": [{"id": "icons", "source": "alpha", "source-layer": "features",
"layout": {"icon-image": ["get", "symbol"]}}]}
mapping = {"assignments": [
item("a", "navigation_marks", "LAYER_ONLY", [0], style, ["BOY"]),
item("b", None, "UNKNOWN", [0], style, ["ALIEN"]),
]}
result = audit.audit_mapping(style, mapping)
self.assertEqual(result["mappingObjectCount"], 2)
self.assertEqual(result["executionAuthorizedCount"], 0)
self.assertEqual(result["mixedStyleLayerIds"], ["icons"])
a = result["records"][0]
self.assertEqual(a["renderComponents"][0]["candidateSelector"], ["==", ["get", "symbol"], "BOY"])
self.assertIn("STANDARD_TO_DISPLAY_GROUP_CONTRACT_UNBOUND", a["blockingReasons"])
self.assertIn("UNRESOLVED_CLASSIFICATION", result["records"][1]["blockingReasons"])
def test_provider_b_other_field_multiple_components_and_inheritance(self):
style = {"layers": [
{"id": "mark-draw", "source": "beta", "source-layer": "objects",
"layout": {"icon-image": ["get", "pictogram"]}},
{"id": "mark-label", "source": "beta", "source-layer": "objects",
"layout": {"text-field": ["get", "label"]}},
]}
mapping = {"assignments": [
item("a", "facility_boundary_point", "VISUAL_OBJECT_MATCH", [0, 1],
style, ["P", None]),
]}
result = audit.audit_mapping(style, mapping)
entry = result["records"][0]
self.assertEqual(entry["renderComponents"][0]["candidateSelector"],
["==", ["get", "pictogram"], "P"])
self.assertEqual(entry["renderComponents"][1]["selectorEvidence"], "UNPROVED")
self.assertIn("MULTI_COMPONENT_IDENTITY_UNVERIFIED", entry["blockingReasons"])
self.assertIn("INHERITANCE_REQUIRES_SAME_FEATURE_PROOF", entry["blockingReasons"])
self.assertFalse(entry["executionAuthorized"])
def test_cross_layer_contradiction_and_invalid_style_index_fail_closed(self):
style = {"layers": [
{"id": "one", "source": "beta", "source-layer": "objects",
"layout": {"icon-image": ["get", "icon"]}},
{"id": "two", "source": "beta", "source-layer": "objects",
"layout": {"icon-image": ["get", "icon"]}},
]}
mapping = {"assignments": [
item("a", "mark", "LAYER_ONLY", [0, 1], style, ["A", "B"]),
]}
result = audit.audit_mapping(style, mapping)
self.assertIn("CONTRADICTORY_RESOURCE_FIELDS",
result["records"][0]["blockingReasons"])
mapping["assignments"][0]["renderEvidence"]["components"][0]["styleIndex"] = 99
with self.assertRaisesRegex(ValueError, "索引无效"):
audit.audit_mapping(style, mapping)
def test_style_sha256_binding_fails_closed(self):
import hashlib
sample = b'{"layers":[]}'
digest = hashlib.sha256(sample).hexdigest()
mapping = {"renderEvidenceSource": {"styleSha256": digest}}
audit.verify_mapping_style_fingerprint(mapping, sample)
with self.assertRaisesRegex(ValueError, "不匹配"):
audit.verify_mapping_style_fingerprint(mapping, b'{"layers":[1]}')
with self.assertRaisesRegex(ValueError, "缺失"):
audit.verify_mapping_style_fingerprint({"renderEvidenceSource": {}}, sample)
def test_duplicate_visual_id_fails_closed(self):
style = {"layers": []}
mapping = {"assignments": [
{"visualObjectId": "same", "status": "UNKNOWN"},
{"visualObjectId": "same", "status": "UNKNOWN"},
]}
with self.assertRaisesRegex(ValueError, "重复"):
audit.audit_mapping(style, mapping)
if __name__ == "__main__":
unittest.main()

View File

@@ -20,6 +20,43 @@ spec.loader.exec_module(pipeline)
class ProviderPipelineContractTests(unittest.TestCase): class ProviderPipelineContractTests(unittest.TestCase):
def test_mapping_catalog_uses_declared_full_universe_and_fails_closed(self):
sample = {"visual_objects": [{"visual_object_id": "VO-a"}]}
full = {"visual_objects": [{"visual_object_id": "VO-a"}, {"visual_object_id": "VO-b"}]}
self.assertIs(pipeline.select_mapping_catalog(sample, None), sample)
self.assertIs(pipeline.select_mapping_catalog(sample, full), full)
with self.assertRaisesRegex(ValueError, "未覆盖样本"):
pipeline.select_mapping_catalog(sample, {"visual_objects": [{"visual_object_id": "VO-b"}]})
with self.assertRaisesRegex(ValueError, "无效或重复"):
pipeline.select_mapping_catalog(sample, {"visual_objects": [{"visual_object_id": "VO-a"}] * 2})
with self.assertRaisesRegex(ValueError, "缺少 visual_objects"):
pipeline.select_mapping_catalog(sample, {"visual_objects": None})
def test_cm95_full_mapping_covers_all_actual_objects_without_reclassifying_sample(self):
root = ROOT / "tools/visual-object-instantiation/output/cm95-current"
catalog_file = root / "actual-anonymous-catalog.json"
assignment_file = ROOT / "tools/style-visual-object/output/cm95-manual-review/assignment/cm95-layer-assignment-v1.json"
previous_file = ROOT / "dist/cm95/s52-full/1.1.0/mapping-layer-assignment.json"
if not all(path.exists() for path in (catalog_file, assignment_file, previous_file)):
self.skipTest("未提供 CM95 已验证的完整 Catalog / 人工审核 / 原 Package")
full = json.loads(catalog_file.read_text(encoding="utf-8"))
review = json.loads(assignment_file.read_text(encoding="utf-8"))
before = json.loads(previous_file.read_text(encoding="utf-8"))["assignments"]
sample = {"visual_objects": [{"visual_object_id": row["visualObjectId"]} for row in before]}
selected = pipeline.select_mapping_catalog(sample, full)
rows = pipeline.manual_assignment(selected, review)
self.assertEqual(len(rows), len(full["visual_objects"]))
self.assertEqual(len(rows), 475)
self.assertEqual(pipeline.attach_render_evidence(rows, full), [])
by_id = {row["visualObjectId"]: row for row in rows}
self.assertEqual(len(by_id), 475)
for row in before:
current = by_id[row["visualObjectId"]]
self.assertEqual((current["status"], current.get("standardLayer")),
(row["status"], row.get("standardLayer")))
self.assertEqual(sum(row["renderEvidence"]["occurrenceCount"] for row in rows), 976534)
self.assertTrue(any(row["status"] == "UNKNOWN" for row in rows))
def test_v2_full_catalog_evidence_and_fail_closed(self): def test_v2_full_catalog_evidence_and_fail_closed(self):
style = {"layers": [{"id": "layer-a", "source-layer": "points"}, {"id": "layer-b", "source-layer": "points"}]} style = {"layers": [{"id": "layer-a", "source-layer": "points"}, {"id": "layer-b", "source-layer": "points"}]}
components = [ components = [

View File

@@ -0,0 +1,142 @@
"""B2.7 真实 Style 逐层候选 Selector:混合层不误归残余的离线证据测试。"""
from __future__ import annotations
import copy
import importlib.util
from pathlib import Path
import sys
import unittest
ROOT = Path(__file__).resolve().parents[1]
DIR = ROOT / "tools/provider-pipeline"
sys.path.insert(0, str(DIR))
spec = importlib.util.spec_from_file_location("collision_audit", DIR / "selector_collision_evidence_audit.py")
audit = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(audit)
def fixtures(*, provider="a", source_layer="raw", field="icon"):
style = {"layers": [
{"id": "original-picture", "source": provider, "source-layer": source_layer, "type": "symbol",
"filter": ["has", field], "layout": {"icon-image": ["get", field]}},
{"id": "original-caption", "source": provider, "source-layer": source_layer, "type": "symbol",
"filter": ["has", "label"], "layout": {"text-field": ["get", "label"]}},
]}
mapping = {"assignments": [
{"visualObjectId": "V-BOY", "status": "VISUAL_OBJECT_MATCH", "standardLayer": "navigation_marks",
"renderEvidence": {"sourceLayer": source_layer, "components": [
{"styleIndex": 0, "styleLayerId": "original-picture", "role": "icon", "resourceKey": "B"},
{"styleIndex": 1, "styleLayerId": "original-caption", "role": "text", "resourceKey": None},
]}},
{"visualObjectId": "V-UNKNOWN", "status": "UNKNOWN", "standardLayer": None,
"renderEvidence": {"sourceLayer": source_layer, "components": [
{"styleIndex": 0, "styleLayerId": "original-picture", "role": "icon", "resourceKey": "B"},
]}},
]}
return style, mapping
def evaluate_simple(expr, props, zoom):
if expr[0] == "all":
return all(evaluate_simple(x, props, zoom) for x in expr[1:])
if expr[0] == "==":
return props.get(expr[1][1]) == expr[2]
raise AssertionError(expr)
def obs(vo, values, layer_ids=("original-picture", "original-caption")):
return {"visualObjectTypeId": vo, "properties": values, "zoom": 12,
"matchedStyleLayerIds": list(layer_ids), "tile": "1/0/0", "featureId": 7}
class SelectorCollisionTests(unittest.TestCase):
def test_two_different_provider_property_names_and_original_style_preserved(self):
for provider, field in (("alpha", "icon"), ("beta", "providerSpecificCode")):
with self.subTest(provider=provider):
style, mapping = fixtures(provider=provider, field=field)
before = copy.deepcopy(style)
candidates = audit.build_candidates(mapping, style)
self.assertEqual(style, before)
self.assertEqual(len(candidates), 3)
target = [x for x in candidates if x["visualObjectTypeId"] == "V-BOY"]
self.assertEqual(len(target), 2)
self.assertTrue(all(x["selector"] == ["==", ["get", field], "B"] for x in target))
self.assertEqual(target[1]["selectorOrigin"], "INHERIT_SAME_RAW_FEATURE_CANDIDATE")
self.assertTrue(all(not x["runtimeAuthorized"] for x in target))
def test_unknown_same_resource_blocks_even_with_replayed_components(self):
style, mapping = fixtures()
rows = audit.build_candidates(mapping, style)
assignments = {x["visualObjectId"]: x for x in mapping["assignments"]}
outcome = audit.evaluate_examples(rows, [
obs("V-BOY", {"icon": "B", "label": "Known"}),
obs("V-UNKNOWN", {"icon": "B", "label": "Different"}),
], assignments, eval_selector=evaluate_simple)
target = [x for x in rows if x["visualObjectTypeId"] == "V-BOY"]
self.assertTrue(all(x["sampleStatus"] == "CROSS_CLASS_OR_RESIDUAL_COLLISION" for x in target))
self.assertTrue(all(x["unresolvedOrUnknownHits"] == 1 for x in target))
self.assertEqual(outcome["candidateStatusCounts"]["CROSS_CLASS_OR_RESIDUAL_COLLISION"], 2)
self.assertIsNone(next(x for x in rows if x["visualObjectTypeId"] == "V-UNKNOWN")["selector"])
def test_cross_standard_classification_blocks(self):
style, mapping = fixtures()
mapping["assignments"][1]["status"] = "LAYER_ONLY"
mapping["assignments"][1]["standardLayer"] = "facility_point"
rows = audit.build_candidates(mapping, style)
assignments = {x["visualObjectId"]: x for x in mapping["assignments"]}
audit.evaluate_examples(rows, [
obs("V-BOY", {"icon": "B", "label": "Known"}),
obs("V-UNKNOWN", {"icon": "B", "label": "Other"}),
], assignments, eval_selector=evaluate_simple)
target = [x for x in rows if x["visualObjectTypeId"] == "V-BOY"]
self.assertTrue(all(x["crossStandardHits"] == 1 for x in target))
def test_two_resource_fields_compile_conjunction_but_not_primary(self):
style, mapping = fixtures()
style["layers"][1]["layout"] = {"icon-image": ["get", "light"]}
mapping["assignments"][0]["renderEvidence"]["components"][1] = {
"styleIndex": 1, "styleLayerId": "original-caption", "role": "icon", "resourceKey": "L",
}
rows = audit.build_candidates(mapping, style)
target = [x for x in rows if x["visualObjectTypeId"] == "V-BOY"]
expected = ["all", ["==", ["get", "icon"], "B"], ["==", ["get", "light"], "L"]]
self.assertTrue(all(x["selector"] == expected for x in target))
self.assertTrue(all(not x["runtimeAuthorized"] for x in target))
def test_conflicting_direct_resource_values_and_invalid_style_fail_closed(self):
style, mapping = fixtures()
style["layers"][1]["layout"] = {"icon-image": ["get", "icon"]}
mapping["assignments"][0]["renderEvidence"]["components"][1] = {
"styleIndex": 1, "styleLayerId": "original-caption", "role": "icon", "resourceKey": "DIFFERENT",
}
rows = audit.build_candidates(mapping, style)
self.assertTrue(all(x["selector"] is None for x in rows if x["visualObjectTypeId"] == "V-BOY"))
mapping["assignments"][0]["renderEvidence"]["components"][0]["styleIndex"] = 999
with self.assertRaisesRegex(ValueError, "Style Index"):
audit.build_candidates(mapping, style)
def test_catalog_mismatch_and_multisource_same_layer_fail_closed(self):
style, mapping = fixtures()
catalog = {"visual_objects": [{"visual_object_id": "V-BOY", "example": {"tile": "1/0/0"}}]}
with self.assertRaisesRegex(ValueError, "覆盖不一致"):
audit.run_audit(style, mapping, catalog, Path("/tmp"), {}, {})
style["layers"].append({
"id": "other-source", "source": "second", "source-layer": "raw",
"type": "symbol", "layout": {"icon-image": ["get", "icon"]},
})
catalog["visual_objects"].append({"visual_object_id": "V-UNKNOWN", "example": {"tile": "1/0/0"}})
with self.assertRaisesRegex(ValueError, "多个 Style Source"):
audit.run_audit(style, mapping, catalog, Path("/tmp"), {}, {})
def test_unseen_target_is_not_claimed_safe(self):
style, mapping = fixtures()
rows = audit.build_candidates(mapping, style)
assignments = {x["visualObjectId"]: x for x in mapping["assignments"]}
audit.evaluate_examples(rows, [], assignments, eval_selector=evaluate_simple)
target = [x for x in rows if x["visualObjectTypeId"] == "V-BOY"]
self.assertTrue(all(x["sampleStatus"] == "TARGET_NOT_OBSERVED" for x in target))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,146 @@
"""B2.6:显示组和 PRIMARY 证据必须由独立人审,且审核通过不等于执行授权。"""
from __future__ import annotations
import copy
import importlib.util
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "tools/provider-pipeline/standard_display_review_gate.py"
spec = importlib.util.spec_from_file_location("review_gate_module", SCRIPT)
gate = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(gate)
HASHARGS = dict(queue_sha256="a" * 64, groups_sha256="b" * 64, style_sha256="c" * 64)
def fixtures():
queue = {
"schemaVersion": "navsea-feature-identity-scope-audit/v1",
"providerId": "unknown-provider", "datasetId": "test-v1",
"reviewQueue": [
{
"visualObjectTypeId": "VO-1", "assignmentStatus": "LAYER_ONLY",
"standardLayer": "reviewed_category", "primaryReviewStatus": "PENDING_REVIEW",
"componentCandidates": [
{"componentIndex": 0, "drawingRole": "line", "styleLayerId": "layer-A"},
{"componentIndex": 1, "drawingRole": "text", "styleLayerId": "layer-B"},
],
},
{
"visualObjectTypeId": "VO-2", "assignmentStatus": "UNKNOWN",
"standardLayer": None, "primaryReviewStatus": "PENDING_REVIEW",
"componentCandidates": [{"componentIndex": 0, "drawingRole": "icon", "styleLayerId": "layer-C"}],
},
],
}
groups = {
"schemaVersion": gate.GROUP_SCHEMA, "approvalStatus": "OBSERVED_NOT_APPROVED",
"groupIds": ["display-A", "display-B"],
}
draft = gate.generate_review_draft(queue, groups, **HASHARGS)
return queue, groups, draft
def review_both(draft):
std = draft["standardLayerDecisions"][0]
std.update(status="REVIEWED", displayGroupId="display-A", reviewer="real-reviewer",
evidenceRefs=["audit/signed-standard-review"])
obj = draft["visualObjectDecisions"][0]
obj.update(status="REVIEWED", reviewedStandardLayer="reviewed_category",
primaryComponentIndex=0, componentParentLinks=[
{"componentIndex": 1, "parentComponentIndex": 0}
], reviewer="real-reviewer", evidenceRefs=["audit/signed-primary-review"])
class StandardDisplayReviewGateTests(unittest.TestCase):
def test_pending_draft_is_not_executable_and_keeps_unknown(self):
queue, groups, draft = fixtures()
self.assertEqual(len(draft["standardLayerDecisions"]), 1)
self.assertEqual(len(draft["visualObjectDecisions"]), 2)
result = gate.validate_review_draft(draft, queue, groups, **HASHARGS)
self.assertEqual(result["status"], "PASS_PENDING_REVIEW")
self.assertEqual(result["runtimeExecutionAuthorizedCount"], 0)
self.assertEqual(result["visualObjectStatusCounts"], {"PENDING": 2})
def test_unapproved_host_groups_rejects_review(self):
queue, groups, draft = fixtures()
review_both(draft)
out = gate.validate_review_draft(draft, queue, groups, **HASHARGS)
self.assertEqual(out["status"], "FAIL_CLOSED")
self.assertTrue(any("未正式批准" in error for error in out["validationErrors"]))
def test_explicit_reviewed_records_structurally_valid_but_never_runtime_authorized(self):
queue, groups, draft = fixtures()
groups["approvalStatus"] = "APPROVED"
review_both(draft)
result = gate.validate_review_draft(draft, queue, groups, **HASHARGS)
self.assertEqual(result["status"], "PASS_PENDING_REVIEW")
self.assertEqual(result["reviewedObjectCount"], 1)
self.assertEqual(result["runtimeExecutionAuthorizedCount"], 0)
self.assertEqual(result["globalIdentityAuthorizedCount"], 0)
def test_incomplete_or_double_parent_component_links_fail_closed(self):
queue, groups, draft = fixtures()
groups["approvalStatus"] = "APPROVED"
review_both(draft)
first = draft["visualObjectDecisions"][0]
first["componentParentLinks"] = []
self.assertEqual(gate.validate_review_draft(draft, queue, groups, **HASHARGS)["status"], "FAIL_CLOSED")
first["componentParentLinks"] = [
{"componentIndex": 1, "parentComponentIndex": 0},
{"componentIndex": 1, "parentComponentIndex": 0},
]
self.assertEqual(gate.validate_review_draft(draft, queue, groups, **HASHARGS)["status"], "FAIL_CLOSED")
def test_unknown_classification_cannot_be_promoted_through_review(self):
queue, groups, draft = fixtures()
groups["approvalStatus"] = "APPROVED"
review_both(draft)
second = draft["visualObjectDecisions"][1]
second.update(status="REVIEWED", reviewedStandardLayer="reviewed_category",
primaryComponentIndex=0, componentParentLinks=[],
reviewer="not-authorized", evidenceRefs=["ticket/123"])
out = gate.validate_review_draft(draft, queue, groups, **HASHARGS)
self.assertEqual(out["status"], "FAIL_CLOSED")
self.assertTrue(any("UNKNOWN/AMBIGUOUS" in err for err in out["validationErrors"]))
def test_binding_digest_and_extra_or_missing_review_ids_fail_closed(self):
queue, groups, draft = fixtures()
with self.assertRaisesRegex(ValueError, "指纹"):
gate.validate_review_draft(draft, queue, groups, **{**HASHARGS, "style_sha256": "wrong"})
draft["visualObjectDecisions"].pop()
with self.assertRaisesRegex(ValueError, "缺少"):
gate.validate_review_draft(draft, queue, groups, **HASHARGS)
draft = gate.generate_review_draft(queue, groups, **HASHARGS)
draft["standardLayerDecisions"].append(copy.deepcopy(draft["standardLayerDecisions"][0]))
with self.assertRaisesRegex(ValueError, "重复"):
gate.validate_review_draft(draft, queue, groups, **HASHARGS)
def test_pending_row_cannot_smuggle_parent_or_group(self):
queue, groups, draft = fixtures()
draft["visualObjectDecisions"][0]["primaryComponentIndex"] = 0
result = gate.validate_review_draft(draft, queue, groups, **HASHARGS)
self.assertEqual(result["status"], "FAIL_CLOSED")
draft = gate.generate_review_draft(queue, groups, **HASHARGS)
draft["standardLayerDecisions"][0]["displayGroupId"] = "display-A"
result = gate.validate_review_draft(draft, queue, groups, **HASHARGS)
self.assertEqual(result["status"], "FAIL_CLOSED")
def test_cross_provider_visual_types_have_no_implied_same_semantics(self):
queue_a, groups, draft_a = fixtures()
queue_b = copy.deepcopy(queue_a)
queue_b["providerId"] = "independent-provider"
draft_b = gate.generate_review_draft(queue_b, groups, **HASHARGS)
self.assertEqual(draft_a["visualObjectDecisions"], draft_b["visualObjectDecisions"])
self.assertNotEqual(draft_a["binding"]["providerId"], draft_b["binding"]["providerId"])
with self.assertRaisesRegex(ValueError, "来源身份"):
gate.validate_review_draft(draft_a, queue_b, groups, **HASHARGS)
result = gate.validate_review_draft(draft_b, queue_b, groups, **HASHARGS)
self.assertEqual(result["runtimeExecutionAuthorizedCount"], 0)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,72 @@
"""B2.7 全量 Style Layer 归属审核:不同 Provider、混合 Layer、纯 residual、无观测。"""
import importlib.util
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location("isolation",ROOT/"tools/provider-pipeline/style_layer_isolation_audit.py")
audit = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(audit)
def assignment(oid, index, source_layer, status, standard):
return {
"visualObjectId": oid, "status": status, "standardLayer": standard,
"renderEvidence": {
"sourceLayer": source_layer,
"components": [{"styleIndex": index,"styleLayerId": "original-layer", "role": "icon", "resourceKey": "R"}],
},
}
class IsolationTests(unittest.TestCase):
def test_two_different_provider_source_names_one_class_is_candidate_only(self):
for name in ("raw-pbf-a", "provider-b-features"):
with self.subTest(name=name):
style={"layers":[{"id":"original-layer","source":"provider","source-layer":name},
{"id":"unused","source":"provider","source-layer":"other"}]}
mapping={"assignments":[assignment("A",0,name,"LAYER_ONLY","marks"),
assignment("B",0,name,"VISUAL_OBJECT_MATCH","marks")]}
out=audit.audit_isolation(style,mapping,2)
self.assertEqual(out["layerCategoryCounts"],{
"NO_OBSERVED_STYLE_FEATURES":1,
"ONE_STANDARD_IN_COMPLETE_CATALOG_CANDIDATE":1,
})
self.assertTrue(out["layers"][0]["possibleWholeLayerVisibilityInFrozenSnapshot"])
self.assertFalse(out["layers"][0]["productionWholeLayerVisibilityAuthorized"])
self.assertEqual(out["runtimeAuthorizationCount"],0)
def test_mixed_known_and_unknown_never_authorized(self):
style={"layers":[{"id":"original-layer","source":"A","source-layer":"shared"}]}
mapping={"assignments":[assignment("A",0,"shared","LAYER_ONLY","marks"),
assignment("B",0,"shared","UNKNOWN",None)]}
out=audit.audit_isolation(style,mapping,2)
self.assertEqual(out["layerCategoryCounts"],{"MIXED_OR_RESIDUAL":1})
self.assertFalse(out["layers"][0]["possibleWholeLayerVisibilityInFrozenSnapshot"])
def test_only_unknown_is_different_from_mixed(self):
style={"layers":[{"id":"original-layer","source":"A","source-layer":"shared"}]}
mapping={"assignments":[assignment("A",0,"shared","UNKNOWN",None)]}
out=audit.audit_isolation(style,mapping,1)
self.assertEqual(out["layerCategoryCounts"],{"ONLY_UNRESOLVED_OR_RESIDUAL":1})
def test_conflicting_known_groups_are_mixed_even_without_unknown(self):
style={"layers":[{"id":"original-layer","source":"A","source-layer":"shared"}]}
mapping={"assignments":[assignment("A",0,"shared","LAYER_ONLY","marks"),
assignment("B",0,"shared","VISUAL_OBJECT_MATCH","fishery")]}
out=audit.audit_isolation(style,mapping,2)
self.assertEqual(out["layerCategoryCounts"],{"MIXED_OR_RESIDUAL":1})
def test_missing_catalog_coverage_and_invalid_binding_fail_closed(self):
style={"layers":[{"id":"original-layer","source":"A","source-layer":"shared"}]}
mapping={"assignments":[assignment("A",0,"shared","LAYER_ONLY","marks")]}
with self.assertRaisesRegex(ValueError,"没有覆盖完整"):
audit.audit_isolation(style,mapping,2)
mapping["assignments"][0]["renderEvidence"]["components"][0]["styleLayerId"]="wrong"
with self.assertRaisesRegex(ValueError,"mismatch"):
audit.audit_isolation(style,mapping,1)
if __name__=="__main__":
unittest.main()

View File

@@ -0,0 +1,250 @@
#!/usr/bin/env python3
"""PBF 原始 Feature 身份作用域审计(只读;绝不授予 stable/global identity)。"""
from __future__ import annotations
import argparse
from collections import Counter, defaultdict
import hashlib
import json
from pathlib import Path
from typing import Any
from mapbox_vector_tile.Mapbox import vector_tile_pb2
def _tile_parts(value: Any) -> tuple[int, int, int] | None:
if not isinstance(value, str):
return None
parts = value.split("/")
if len(parts) != 3 or any(not x.isdigit() for x in parts):
return None
z, x, y = map(int, parts)
if z > 24 or x >= (1 << z) or y >= (1 << z):
return None
return z, x, y
def _property_signature(tile_layer: Any, feature: Any) -> str:
"""Canonicalize original protobuf properties without Provider field semantics."""
tags = feature.tags
props: dict[str, Any] = {}
for i in range(0, len(tags) - 1, 2):
key_i, value_i = tags[i], tags[i + 1]
if key_i >= len(tile_layer.keys) or value_i >= len(tile_layer.values):
continue
fields = tile_layer.values[value_i].ListFields()
props[tile_layer.keys[key_i]] = fields[0][1] if fields else None
serialized = json.dumps(props, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str)
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
def _source_owners(style: dict[str, Any]) -> dict[str, set[str]]:
result: dict[str, set[str]] = defaultdict(set)
for layer in style.get("layers", []):
if isinstance(layer, dict) and isinstance(layer.get("source-layer"), str):
result[layer["source-layer"]].add(layer.get("source"))
return result
def audit_scope(
catalog: dict[str, Any], style: dict[str, Any], pbf_root: Path,
*, provider_id: str, dataset_id: str,
mapping: dict[str, Any] | None = None,
sample_occurrences: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""One-example-per-type locator proof; explicit refusal of stable identity."""
objects = catalog.get("visual_objects")
if not isinstance(objects, list) or not objects:
raise ValueError("Catalog 缺少 visual_objects")
owners = _source_owners(style)
by_tile: dict[str, list[dict[str, Any]]] = defaultdict(list)
seen_types: set[str] = set()
for obj in objects:
object_id = obj.get("visual_object_id")
if not isinstance(object_id, str) or not object_id or object_id in seen_types:
raise ValueError("Visual Object 类型 ID 重复或无效")
seen_types.add(object_id)
example = obj.get("example")
sl = (obj.get("provenance") or {}).get("source_layer")
if not isinstance(example, dict) or _tile_parts(example.get("tile")) is None:
raise ValueError(f"{object_id}: 样本 Tile 路径不可信")
if not isinstance(sl, str) or not sl:
raise ValueError(f"{object_id}: Source Layer 缺失")
by_tile[example["tile"]].append(obj)
if mapping is not None:
assignments = mapping.get("assignments")
if not isinstance(assignments, list):
raise ValueError("Mapping assignments 无效")
assignment_by_id = {row.get("visualObjectId"): row for row in assignments}
if len(assignment_by_id) != len(assignments):
raise ValueError("Mapping Visual Object ID 重复")
else:
assignment_by_id = {}
results: list[dict[str, Any]] = []
sample_stats = Counter()
raw_tile_features = Counter()
cross_tile_ids: dict[tuple[str, str, int], set[str]] = defaultdict(set)
raw_property_signatures: dict[tuple[str, str, int], set[str]] = defaultdict(set)
observed_local_keys: dict[tuple[str, str, int], int] = {}
for tile in sorted(by_tile, key=lambda t: _tile_parts(t)):
z, x, y = _tile_parts(tile)
file = pbf_root / str(z) / str(x) / (str(y) + ".pbf")
lookup: dict[str, Any] = {}
same_name: set[str] = set()
if file.is_file():
decoded = vector_tile_pb2.tile()
decoded.ParseFromString(file.read_bytes())
layer_counts = Counter(layer.name for layer in decoded.layers)
same_name = {name for name, count in layer_counts.items() if count > 1}
lookup = {layer.name: layer for layer in decoded.layers}
for layer in decoded.layers:
local_ids = Counter(int(f.id) for f in layer.features if f.HasField("id"))
raw_tile_features["decodedFeatures"] += len(layer.features)
raw_tile_features["missingFeatureIds"] += sum(not f.HasField("id") for f in layer.features)
raw_tile_features["duplicateFeatureIdValues"] += sum(v > 1 for v in local_ids.values())
for f in layer.features:
if f.HasField("id"):
identity = (layer.name, "rawFeatureId", int(f.id))
cross_tile_ids[identity].add(tile)
raw_property_signatures[identity].add(_property_signature(layer, f))
for obj in by_tile[tile]:
sl = obj["provenance"]["source_layer"]
example = obj["example"]
fid = example.get("feature_id")
sources = owners.get(sl, set())
status = "TILE_LOCAL_LOCATOR_CONFIRMED"
source_id = next(iter(sources)) if len(sources) == 1 else None
if source_id is None or not isinstance(source_id, str):
status = "STYLE_SOURCE_SCOPE_UNPROVEN"
elif not file.is_file():
status = "PBF_TILE_MISSING"
elif sl in same_name:
status = "DUPLICATE_SOURCE_LAYER_IN_TILE"
elif sl not in lookup:
status = "SOURCE_LAYER_IN_TILE_MISSING"
elif type(fid) is not int or fid < 0:
status = "FEATURE_ID_UNAVAILABLE"
else:
count = sum(f.HasField("id") and int(f.id) == fid for f in lookup[sl].features)
if count != 1:
status = "FEATURE_ID_MISSING" if not count else "FEATURE_ID_AMBIGUOUS"
if status == "TILE_LOCAL_LOCATOR_CONFIRMED":
key = (tile, sl, fid)
if key in observed_local_keys:
# Invalidate both the original and subsequent claim; never
# leave the first conflicting Visual Object "confirmed".
status = "EXAMPLE_LOCATOR_SHARED_BY_VISUAL_TYPES"
previous = results[observed_local_keys[key]]
if previous["locatorStatus"] == "TILE_LOCAL_LOCATOR_CONFIRMED":
previous["locatorStatus"] = status
previous["identityScope"] = "UNVERIFIED"
previous["tileLocalFeatureLocator"] = None
sample_stats["TILE_LOCAL_LOCATOR_CONFIRMED"] -= 1
sample_stats[status] += 1
else:
observed_local_keys[key] = len(results)
sample_stats[status] += 1
assignment = assignment_by_id.get(obj["visual_object_id"])
components = obj.get("components", [])
results.append({
"visualObjectTypeId": obj["visual_object_id"],
"identityScope": "TILE_LOCAL_EXAMPLE_ONLY" if status == "TILE_LOCAL_LOCATOR_CONFIRMED" else "UNVERIFIED",
"locatorStatus": status,
"tileLocalFeatureLocator": {
"providerId": provider_id, "datasetId": dataset_id,
"sourceId": source_id, "sourceLayer": sl,
"z": z, "x": x, "y": y, "rawFeatureId": fid,
} if status == "TILE_LOCAL_LOCATOR_CONFIRMED" else None,
"stableEntityId": None,
"fidScopePromotionAuthorized": False,
"pickupDeduplicationAuthorized": False,
"assignmentStatus": assignment.get("status") if isinstance(assignment, dict) else None,
"standardLayer": assignment.get("standardLayer") if isinstance(assignment, dict) else None,
"primaryReviewStatus": "PENDING_REVIEW",
"primaryStyleComponentIndex": None,
"componentCandidates": [
{"componentIndex": i, "styleLayerId": c.get("style_layer_id"),
"styleIndex": c.get("style_index"), "drawingRole": c.get("role"),
"resourceKey": c.get("resource_key")}
for i, c in enumerate(components)
],
})
sampled_ids: dict[tuple[str, str, int], set[str]] = defaultdict(set)
if sample_occurrences is not None:
for occ in sample_occurrences:
fid = occ.get("feature_id")
if type(fid) is int and isinstance(occ.get("source_layer"), str) and _tile_parts(occ.get("tile")):
sampled_ids[(occ["source_layer"], "rawFeatureId", fid)].add(occ["tile"])
return {
"schemaVersion": "navsea-feature-identity-scope-audit/v1",
"scope": "RECORDED_EXAMPLE_PER_VISUAL_TYPE_NOT_GLOBAL_ENTITY_VERIFICATION",
"providerId": provider_id,
"datasetId": dataset_id,
"visualObjectCount": len(results),
"examinedUniqueTileCount": len(by_tile),
"exampleLocatorStatusCounts": dict(sorted((key, value) for key, value in sample_stats.items() if value > 0)),
"examinedTileRawFeatureStats": dict(sorted(raw_tile_features.items())),
"reusedRawFeatureIdAcrossExaminedTiles": sum(len(v) > 1 for v in cross_tile_ids.values()),
"reusedRawFeatureIdWithDifferingProperties": sum(
len(tiles) > 1 and len(raw_property_signatures[identity]) > 1
for identity, tiles in cross_tile_ids.items()
),
"sampleOccurrencesWithSameIdAcrossTiles": sum(len(v) > 1 for v in sampled_ids.values()),
"fidScope": "unknown",
"globalIdentityAuthorizedCount": 0,
"primaryAuthorizedCount": 0,
"pickupDeduplicationAuthorizedCount": 0,
"reviewQueue": sorted(results, key=lambda x: x["visualObjectTypeId"]),
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--catalog", type=Path, required=True)
parser.add_argument("--style", type=Path, required=True)
parser.add_argument("--pbf-root", type=Path, required=True)
parser.add_argument("--mapping", type=Path)
parser.add_argument("--sample-occurrences", type=Path)
parser.add_argument("--provider-id", required=True)
parser.add_argument("--dataset-id", required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
catalog = json.loads(args.catalog.read_text(encoding="utf-8"))
if catalog.get("schema") != "actual-visual-object-catalog/v1":
raise ValueError("Catalog schema 不匹配")
style_data = args.style.read_bytes()
original_style = (catalog.get("inputs") or {}).get("style")
if not isinstance(original_style, str) or not Path(original_style).is_file():
raise ValueError("Catalog 原始 Style 缺失,无法核验")
if hashlib.sha256(style_data).digest() != hashlib.sha256(Path(original_style).read_bytes()).digest():
raise ValueError("Catalog 与待查 Style SHA-256 不一致")
mapping = json.loads(args.mapping.read_text(encoding="utf-8")) if args.mapping else None
occurrences = None
if args.sample_occurrences:
raw = json.loads(args.sample_occurrences.read_text(encoding="utf-8"))
occurrences = raw.get("occurrences")
if not isinstance(occurrences, list):
raise ValueError("样本 occurrences 数据无效")
result = audit_scope(
catalog, json.loads(style_data), args.pbf_root,
provider_id=args.provider_id, dataset_id=args.dataset_id,
mapping=mapping, sample_occurrences=occurrences,
)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps({
"visualObjectCount": result["visualObjectCount"],
"examinedUniqueTileCount": result["examinedUniqueTileCount"],
"exampleLocatorStatusCounts": result["exampleLocatorStatusCounts"],
"examinedTileRawFeatureStats": result["examinedTileRawFeatureStats"],
"reusedRawFeatureIdAcrossExaminedTiles": result["reusedRawFeatureIdAcrossExaminedTiles"],
"reusedRawFeatureIdWithDifferingProperties": result["reusedRawFeatureIdWithDifferingProperties"],
"fidScope": result["fidScope"],
"globalIdentityAuthorizedCount": 0,
"output": str(args.output),
}, ensure_ascii=False))
return 0 if result["exampleLocatorStatusCounts"] == {"TILE_LOCAL_LOCATOR_CONFIRMED": result["visualObjectCount"]} else 2
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,191 @@
#!/usr/bin/env python3
"""只读按原始 PBF Feature 重放 Composite Visual Object;不推断 PRIMARY/语义。"""
from __future__ import annotations
import argparse
from collections import Counter, defaultdict
import hashlib
import importlib.util
import json
from pathlib import Path
from typing import Any
from PIL import Image
from mapbox_vector_tile.Mapbox import vector_tile_pb2
def load_instantiator() -> Any:
path = Path(__file__).resolve().parents[1] / "visual-object-instantiation" / "instantiate_pbf_visual_objects.py"
spec = importlib.util.spec_from_file_location("provider_actual_visual_instantiation", path)
if spec is None or spec.loader is None:
raise RuntimeError("无法加载实际 PBF Visual Object 实例化器")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def replay_catalog(
catalog: dict[str, Any],
style: dict[str, Any],
sprite_meta: dict[str, Any],
sprite_hashes: dict[str, str],
pbf_root: Path,
*,
instantiator: Any = None,
max_objects: int | None = None,
) -> dict[str, Any]:
"""Replay the recorded example raw Feature; retain explicit limits of one sample per VO."""
engine = instantiator or load_instantiator()
objects = catalog.get("visual_objects")
style_layers = style.get("layers")
if not isinstance(objects, list) or not isinstance(style_layers, list):
raise ValueError("Catalog visual_objects 或 Style layers 无效")
indexed: dict[str, list[tuple[int, dict[str, Any]]]] = defaultdict(list)
for index, layer in enumerate(style_layers):
if isinstance(layer, dict) and isinstance(layer.get("source-layer"), str):
indexed[layer["source-layer"]].append((index, layer))
source_owners = {source_layer: {layer.get('source') for _, layer in members}
for source_layer, members in indexed.items()}
cache: dict[str, dict[str, Any]] = {}
results: list[dict[str, Any]] = []
seen_ids: set[str] = set()
for obj in sorted(objects, key=lambda row: row.get("visual_object_id", ""))[:max_objects]:
object_id = obj.get("visual_object_id")
if not isinstance(object_id, str) or not object_id or object_id in seen_ids:
raise ValueError("Catalog Visual Object ID 无效或重复")
seen_ids.add(object_id)
provenance = obj.get("provenance") or {}
source_layer = provenance.get("source_layer")
example = obj.get("example") or {}
tile = example.get("tile")
fid = example.get("feature_id")
status = None
diagnostics: list[str] = []
replayed: list[dict[str, Any]] = []
if not isinstance(source_layer, str) or not source_layer or not isinstance(tile, str) or len(tile.split("/")) != 3:
status = "INVALID_CATALOG_EXAMPLE"
elif type(fid) is not int or fid < 0:
status = "UNVERIFIABLE_FEATURE_ID"
elif len(source_owners.get(source_layer, set())) != 1 or None in source_owners.get(source_layer, set()):
# The PBF source is not encoded in the catalog's example identity.
status = "AMBIGUOUS_STYLE_SOURCE"
else:
z, x, y = tile.split("/")
if not all(part.isdigit() for part in (z, x, y)):
status = "INVALID_CATALOG_EXAMPLE"
else:
if tile not in cache:
path = pbf_root / z / x / (y + ".pbf")
if not path.is_file():
cache[tile] = {}
else:
raw = vector_tile_pb2.tile()
raw.ParseFromString(path.read_bytes())
name_counts = Counter(layer.name for layer in raw.layers)
cache[tile] = {layer.name: list(layer.features) for layer in raw.layers}
cache[tile + ":duplicates"] = {name for name, count in name_counts.items() if count != 1}
# Retain original tile layer metadata for key-value decoding.
cache[tile + ":rawLayers"] = {layer.name: layer for layer in raw.layers}
feature_list = cache[tile].get(source_layer)
if source_layer in cache.get(tile + ":duplicates", set()):
status = "DUPLICATE_PBF_SOURCE_LAYER"
elif feature_list is None:
status = "PBF_TILE_OR_SOURCE_LAYER_UNAVAILABLE"
else:
candidates = [feature for feature in feature_list
if feature.HasField("id") and int(feature.id) == fid]
if len(candidates) != 1:
status = "FEATURE_ID_NOT_UNIQUE" if len(candidates) > 1 else "FEATURE_ID_NOT_FOUND"
else:
raw_feature = candidates[0]
tile_layer = cache[tile + ":rawLayers"][source_layer]
props = engine.decode_feature_properties(tile_layer, raw_feature)
for index, layer in indexed.get(source_layer, []):
if not engine.matches(layer, props, int(z)):
continue
replayed.extend(engine.component_from_layer(
layer, index, props, int(z), sprite_meta, sprite_hashes
))
raw_signature = {
"geometry_role": engine.geometry_role(raw_feature.type),
"components": engine.signature_components(replayed),
}
replayed_id = engine.stable_id("VO", raw_signature)
if not replayed:
status = "NO_RENDER_COMPONENT_REPLAYED"
elif replayed_id != object_id:
status = "COMPOSITE_SIGNATURE_MISMATCH"
diagnostics.append("原始 PBF 重放得到的视觉签名与 Catalog ID 不一致")
elif engine.signature_components(replayed) != engine.signature_components(obj.get("components", [])):
status = "CATALOG_COMPONENT_MISMATCH"
else:
status = "SAME_RAW_FEATURE_REPLAY_CONFIRMED"
results.append({
"visualObjectId": object_id,
"status": status,
"example": {"tile": tile, "sourceLayer": source_layer, "featureId": fid},
"componentCount": len(obj.get("components", [])),
"replayedComponentCount": len(replayed),
"sameRawFeature": status == "SAME_RAW_FEATURE_REPLAY_CONFIRMED",
"primaryComponentAuthorized": False,
"displaySelectorAuthorized": False,
"diagnostics": diagnostics,
})
counts = Counter(record["status"] for record in results)
return {
"schemaVersion": "navsea-feature-lineage-replay-audit/v1",
"scope": "ONE_RECORDED_EXAMPLE_PER_VISUAL_OBJECT_NOT_FULL_COVERAGE",
"visualObjectCount": len(results),
"sampleTileCount": len([k for k in cache if ":" not in k]),
"statusCounts": dict(sorted(counts.items())),
"sameRawFeatureConfirmedCount": counts["SAME_RAW_FEATURE_REPLAY_CONFIRMED"],
"primaryComponentAuthorizedCount": 0,
"displaySelectorAuthorizedCount": 0,
"results": results,
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--catalog", type=Path, required=True)
parser.add_argument("--style", type=Path, required=True)
parser.add_argument("--sprite-json", type=Path, required=True)
parser.add_argument("--sprite-png", type=Path, required=True)
parser.add_argument("--pbf-root", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--max-objects", type=int)
args = parser.parse_args()
catalog = json.loads(args.catalog.read_text(encoding="utf-8"))
style_bytes = args.style.read_bytes()
style = json.loads(style_bytes.decode("utf-8"))
if catalog.get("schema") != "actual-visual-object-catalog/v1":
raise ValueError("Catalog schema 不受支持")
catalog_style = (catalog.get("inputs") or {}).get("style")
if catalog_style:
source_style_path = Path(catalog_style)
if not source_style_path.is_file():
raise ValueError("Catalog 原始 Style 不存在,不可交叉校验")
if hashlib.sha256(source_style_path.read_bytes()).digest() != hashlib.sha256(style_bytes).digest():
raise ValueError("Catalog Style 与待验证 Style SHA-256 不一致")
if args.max_objects is not None and args.max_objects <= 0:
raise ValueError("max-objects 必须大于零")
sprites = json.loads(args.sprite_json.read_text(encoding="utf-8"))
with Image.open(args.sprite_png) as atlas:
sprite_hashes = load_instantiator().sprite_hashes(sprites, atlas.convert("RGBA"))
result = replay_catalog(
catalog, style, sprites, sprite_hashes, args.pbf_root,
max_objects=args.max_objects,
)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps({
"scope": result["scope"], "visualObjectCount": result["visualObjectCount"],
"statusCounts": result["statusCounts"],
"primaryAuthorized": 0, "displayAuthorized": 0,
"output": str(args.output),
}, ensure_ascii=False))
return 0 if result["sameRawFeatureConfirmedCount"] == result["visualObjectCount"] else 2
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""只读检查 Mapping 的逐 Style Layer 证据;绝不产生 EXECUTABLE 授权。"""
from __future__ import annotations
import argparse
import hashlib
from collections import Counter, defaultdict
import json
from pathlib import Path
from typing import Any
RESOURCE_PROPERTIES = {
"icon": (("layout", "icon-image"),),
"fill": (("paint", "fill-pattern"),),
"line": (("paint", "line-pattern"),),
}
KNOWN_STATUSES = {"VISUAL_OBJECT_MATCH", "LAYER_ONLY"}
def _direct_predicate(layer: dict[str, Any], component: dict[str, Any]) -> list[Any] | None:
key = component.get("resourceKey")
if not isinstance(key, str) or not key:
return None
for section, property_name in RESOURCE_PROPERTIES.get(component.get("role"), ()):
expr = layer.get(section, {}).get(property_name)
if isinstance(expr, list) and len(expr) == 2 and expr[0] == "get" and isinstance(expr[1], str) and expr[1]:
return ["==", ["get", expr[1]], key]
return None
def audit_mapping(style: dict[str, Any], mapping: dict[str, Any]) -> dict[str, Any]:
"""保守静态诊断:DIRECT 仅是构造候选,不是执行批准或对象身份确认。"""
layers = style.get("layers")
assignments = mapping.get("assignments")
if not isinstance(layers, list) or not isinstance(assignments, list):
raise ValueError("Style/Mapping 结构无效")
records: list[dict[str, Any]] = []
counts: Counter[str] = Counter()
layers_to_classes: dict[str, set[str]] = defaultdict(set)
seen: set[str] = set()
for item in sorted(assignments, key=lambda row: str(row.get("visualObjectId", ""))):
object_id = item.get("visualObjectId")
if not isinstance(object_id, str) or not object_id or object_id in seen:
raise ValueError("Visual Object ID 为空或重复")
seen.add(object_id)
status = item.get("status")
standard = item.get("standardLayer")
evidence = item.get("renderEvidence")
reasons: set[str] = set()
components_out: list[dict[str, Any]] = []
direct_fields: dict[str, set[str]] = defaultdict(set)
sources: set[str] = set()
has_unkeyed = False
if status not in KNOWN_STATUSES or not isinstance(standard, str) or not standard:
reasons.add("UNRESOLVED_CLASSIFICATION")
if not isinstance(evidence, dict) or not isinstance(evidence.get("components"), list) or not evidence["components"]:
reasons.add("MISSING_RENDER_EVIDENCE")
else:
for component in evidence["components"]:
index = component.get("styleIndex")
if type(index) is not int or index < 0 or index >= len(layers):
raise ValueError(f"{object_id}: Style 索引无效")
layer = layers[index]
layer_id = component.get("styleLayerId")
if not isinstance(layer, dict) or layer.get("id") != layer_id:
raise ValueError(f"{object_id}: Style Layer ID 不一致")
source = layer.get("source")
source_layer = layer.get("source-layer")
if not isinstance(source, str) or not source or source_layer != evidence.get("sourceLayer"):
reasons.add("SOURCE_OR_SOURCE_LAYER_MISMATCH")
else:
sources.add(source)
if isinstance(layer_id, str):
layers_to_classes[layer_id].add(standard if status in KNOWN_STATUSES and standard else "RESIDUAL")
pred = _direct_predicate(layer, component)
if pred is None:
has_unkeyed = True
reasons.add("COMPONENT_SELECTOR_NOT_PROVED")
else:
direct_fields[pred[1][1]].add(pred[2])
components_out.append({
"styleLayerId": layer_id, "styleIndex": index,
"drawRole": component.get("role"),
"selectorEvidence": "DIRECT_GET_RESOURCE_KEY_CANDIDATE" if pred is not None else "UNPROVED",
"candidateSelector": pred,
})
if len(sources) > 1:
reasons.add("CROSS_SOURCE_COMPONENTS")
if any(len(keys) > 1 for keys in direct_fields.values()):
reasons.add("CONTRADICTORY_RESOURCE_FIELDS")
if len(components_out) > 1:
reasons.add("MULTI_COMPONENT_IDENTITY_UNVERIFIED")
elif components_out:
reasons.add("PRIMARY_IDENTITY_UNDECLARED")
if status in KNOWN_STATUSES and standard:
# Mapping v2 has draw roles, not reviewed PRIMARY/COMPONENT roles.
reasons.add("STANDARD_TO_DISPLAY_GROUP_CONTRACT_UNBOUND")
if has_unkeyed:
reasons.add("INHERITANCE_REQUIRES_SAME_FEATURE_PROOF")
if components_out:
reasons.add("ORIGINAL_FILTER_REACHABILITY_UNVERIFIED")
if not reasons:
reasons.add("EXECUTION_AUTHORITY_UNDEFINED")
for reason in reasons:
counts[reason] += 1
records.append({
"visualObjectId": object_id, "assignmentStatus": status,
"standardLayer": standard, "renderComponents": components_out,
"blockingReasons": sorted(reasons),
"executionAuthorized": False,
})
mixed_layers = sorted(layer_id for layer_id, categories in layers_to_classes.items() if len(categories) > 1)
return {
"schemaVersion": "navsea-mapping-execution-evidence-audit/v1",
"scope": "STATIC_DIAGNOSTIC_ONLY",
"mappingObjectCount": len(records),
"executionAuthorizedCount": 0,
"blockingReasonCounts": dict(sorted(counts.items())),
"mixedStyleLayerCount": len(mixed_layers),
"mixedStyleLayerIds": mixed_layers,
"records": records,
}
def verify_mapping_style_fingerprint(mapping: dict[str, Any], style_bytes: bytes) -> None:
"""Fail closed if the declared original Style digest differs from this input."""
source = mapping.get("renderEvidenceSource")
if source is None:
return # Old mapping versions have no evidence source, and get no authority.
expected = source.get("styleSha256") if isinstance(source, dict) else None
if not isinstance(expected, str) or len(expected) != 64:
raise ValueError("声明的 Style SHA-256 缺失或格式无效")
actual = hashlib.sha256(style_bytes).hexdigest()
if actual.lower() != expected.lower():
raise ValueError("Mapping 声明的 Style SHA-256 与实际快照不匹配")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--style", type=Path, required=True)
parser.add_argument("--mapping", type=Path, required=True)
parser.add_argument("--output", type=Path, help="可选:写入审计 JSON,禁止用于运行时授权")
args = parser.parse_args()
style_bytes = args.style.read_bytes()
mapping = json.loads(args.mapping.read_text(encoding="utf-8"))
verify_mapping_style_fingerprint(mapping, style_bytes)
result = audit_mapping(json.loads(style_bytes.decode("utf-8")), mapping)
data = json.dumps(result, ensure_ascii=False, indent=2) + "\n"
if args.output is None:
print(data)
else:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(data, encoding="utf-8")
print(json.dumps({
"scope": result["scope"], "mappingObjectCount": result["mappingObjectCount"],
"executionAuthorizedCount": result["executionAuthorizedCount"],
"mixedStyleLayerCount": result["mixedStyleLayerCount"],
"output": str(args.output),
}, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -162,6 +162,34 @@ def load_execution_catalog(manifest_path: Path, relative: str, style_hash: str)
return catalog, source return catalog, source
def select_mapping_catalog(
sample_catalog: dict[str, Any], execution_catalog: dict[str, Any] | None
) -> dict[str, Any]:
"""Use the declared, Style-verified execution catalog for complete mapping.
The sample catalog remains the sole source of sample occurrenceIds. If the
full catalog does not contain every sampled Visual Object, fail closed.
"""
if execution_catalog is None:
return sample_catalog
full_objects = execution_catalog.get("visual_objects")
sample_objects = sample_catalog.get("visual_objects")
if not isinstance(full_objects, list) or not isinstance(sample_objects, list):
raise ValueError("映射 Catalog 缺少 visual_objects")
full_ids = [item.get("visual_object_id") for item in full_objects if isinstance(item, dict)]
sample_ids = [item.get("visual_object_id") for item in sample_objects if isinstance(item, dict)]
if (len(full_ids) != len(full_objects) or
len(sample_ids) != len(sample_objects) or
any(not isinstance(item, str) or not item for item in full_ids + sample_ids) or
len(set(full_ids)) != len(full_ids) or
len(set(sample_ids)) != len(sample_ids)):
raise ValueError("映射 Catalog Visual Object ID 无效或重复")
missing = set(sample_ids) - set(full_ids)
if missing:
raise ValueError(f"全量映射 Catalog 未覆盖样本 Visual Object:{len(missing)} 条")
return execution_catalog
def attach_render_evidence(assignments: list[dict[str, Any]], catalog: dict[str, Any]) -> list[str]: def attach_render_evidence(assignments: list[dict[str, Any]], catalog: dict[str, Any]) -> list[str]:
objects = {obj["visual_object_id"]: obj for obj in catalog["visual_objects"]} objects = {obj["visual_object_id"]: obj for obj in catalog["visual_objects"]}
missing = [] missing = []
@@ -278,29 +306,36 @@ def main() -> int:
row["occurrence_ids"] = [x["occurrence_id"] for x in matching_occurrences] row["occurrence_ids"] = [x["occurrence_id"] for x in matching_occurrences]
record["stages"]["PBF-03"] = {"status": "PASS", **actual["summary"], "occurrenceCount": len(actual.get("occurrences", []))} record["stages"]["PBF-03"] = {"status": "PASS", **actual["summary"], "occurrenceCount": len(actual.get("occurrences", []))}
evidence_path = manifest.get("mapping", {}).get("executionEvidenceCatalog")
execution_catalog = None
evidence_source = None
if evidence_path:
execution_catalog, evidence_source = load_execution_catalog(
manifest_path, evidence_path, style_info["sha256"]
)
# A declared full execution catalog is the mapping universe, not merely
# a lookup sidecar for the sampled subset of Visual Objects.
mapping_actual = select_mapping_catalog(actual, execution_catalog)
mapping_mode = manifest.get("mapping", {}).get("mode", "frozen-standard-visual") mapping_mode = manifest.get("mapping", {}).get("mode", "frozen-standard-visual")
if mapping_mode == "manual-assignment": if mapping_mode == "manual-assignment":
assignment_path = (manifest_path.parent / manifest["mapping"]["assignmentFile"]).resolve() assignment_path = (manifest_path.parent / manifest["mapping"]["assignmentFile"]).resolve()
assignment_doc = json.loads(assignment_path.read_text(encoding="utf-8")) assignment_doc = json.loads(assignment_path.read_text(encoding="utf-8"))
assignments = manual_assignment(actual, assignment_doc) assignments = manual_assignment(mapping_actual, assignment_doc)
mapping_source = str(assignment_path) mapping_source = str(assignment_path)
else: else:
standard_path = (manifest_path.parent / manifest.get("mapping", {}).get("standardCatalog", "../../tools/style-visual-object/output/navsea-standard/standard-catalog.json")).resolve() standard_path = (manifest_path.parent / manifest.get("mapping", {}).get("standardCatalog", "../../tools/style-visual-object/output/navsea-standard/standard-catalog.json")).resolve()
if not standard_path.exists(): if not standard_path.exists():
standard_path = ROOT / "tools/style-visual-object/output/navsea-standard/standard-catalog.json" standard_path = ROOT / "tools/style-visual-object/output/navsea-standard/standard-catalog.json"
standard = json.loads(standard_path.read_text(encoding="utf-8")) standard = json.loads(standard_path.read_text(encoding="utf-8"))
assignments = standard_assignment(actual, standard) assignments = standard_assignment(mapping_actual, standard)
mapping_source = str(standard_path) mapping_source = str(standard_path)
occurrence_lookup: dict[str, list[str]] = {} occurrence_lookup: dict[str, list[str]] = {}
for occ in actual.get("occurrences", []): for occ in actual.get("occurrences", []):
occurrence_lookup.setdefault(occ["visual_object_id"], []).append(occ["occurrence_id"]) occurrence_lookup.setdefault(occ["visual_object_id"], []).append(occ["occurrence_id"])
for row in assignments: for row in assignments:
row["occurrenceIds"] = occurrence_lookup.get(row["visualObjectId"], []) row["occurrenceIds"] = occurrence_lookup.get(row["visualObjectId"], [])
evidence_path = manifest.get("mapping", {}).get("executionEvidenceCatalog")
evidence_source = None
evidence_missing: list[str] = [] evidence_missing: list[str] = []
if evidence_path: if execution_catalog is not None:
execution_catalog, evidence_source = load_execution_catalog(manifest_path, evidence_path, style_info["sha256"])
evidence_missing = attach_render_evidence(assignments, execution_catalog) evidence_missing = attach_render_evidence(assignments, execution_catalog)
mapping_doc = {"schemaVersion": "navsea-standard-layer-mapping/v2" if evidence_path else "navsea-standard-layer-mapping/v1", "source": mapping_source, "assignments": assignments} mapping_doc = {"schemaVersion": "navsea-standard-layer-mapping/v2" if evidence_path else "navsea-standard-layer-mapping/v1", "source": mapping_source, "assignments": assignments}
if evidence_source: if evidence_source:
@@ -311,7 +346,10 @@ def main() -> int:
mapping_path.write_text(json.dumps(mapping_doc, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") mapping_path.write_text(json.dumps(mapping_doc, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
unresolved = [{"visualObjectId": x["visualObjectId"], "status": x["status"], "candidateStandardLayers": x["candidateStandardLayers"]} for x in assignments if x["status"] in {"AMBIGUOUS", "UNKNOWN", "COMPONENT_ONLY"}] unresolved = [{"visualObjectId": x["visualObjectId"], "status": x["status"], "candidateStandardLayers": x["candidateStandardLayers"]} for x in assignments if x["status"] in {"AMBIGUOUS", "UNKNOWN", "COMPONENT_ONLY"}]
unresolved += [{"visualObjectId": x, "status": "MISSING_RENDER_EVIDENCE"} for x in evidence_missing] unresolved += [{"visualObjectId": x, "status": "MISSING_RENDER_EVIDENCE"} for x in evidence_missing]
record["stages"]["PBF-04"] = {"status": "PASS_WITH_WARNINGS" if evidence_missing else "PASS", "mappingMode": mapping_mode, "assignments": len(assignments), "unresolved": len(unresolved), "missingRenderEvidence": evidence_missing, "source": mapping_source} record["stages"]["PBF-04"] = {"status": "PASS_WITH_WARNINGS" if unresolved else "PASS", "mappingMode": mapping_mode, "assignments": len(assignments), "unresolved": len(unresolved), "missingRenderEvidence": evidence_missing, "source": mapping_source,
"catalogScope": "DECLARED_EXECUTION_CATALOG" if execution_catalog is not None else "SAMPLED_PBF",
"sampleVisualObjects": len(actual.get("visual_objects", [])),
"mappingVisualObjects": len(mapping_actual.get("visual_objects", []))}
fingerprints = { fingerprints = {
"source": source_manifest["sourceFingerprint"], "style": style_info["sha256"], "source": source_manifest["sourceFingerprint"], "style": style_info["sha256"],

View File

@@ -0,0 +1,308 @@
#!/usr/bin/env python3
"""在实际原始 PBF/Style 上重放候选逐层 selector,报告误命中;仅用于人工审核。"""
from __future__ import annotations
import argparse
from collections import Counter, defaultdict
import hashlib
import importlib.util
import json
from pathlib import Path
from typing import Any
from PIL import Image
from mapbox_vector_tile.Mapbox import vector_tile_pb2
from feature_lineage_replay_audit import load_instantiator
KEY_EXPRESSIONS: dict[str, tuple[str, str]] = {
"icon": ("layout", "icon-image"),
"fill": ("paint", "fill-pattern"),
"line": ("paint", "line-pattern"),
}
ELIGIBLE = {"LAYER_ONLY", "VISUAL_OBJECT_MATCH"}
def predicate(layer: dict[str, Any], comp: dict[str, Any]) -> tuple[str, str] | None:
"""Only original direct-get expressions are candidates, never inferred names."""
role = comp.get("role")
property_location = KEY_EXPRESSIONS.get(role)
key = comp.get("resourceKey")
if property_location is None or not isinstance(key, str) or not key:
return None
section, property_name = property_location
expression = layer.get(section, {}).get(property_name)
if isinstance(expression, list) and len(expression) == 2 and expression[0] == "get" and isinstance(expression[1], str) and expression[1]:
return expression[1], key
return None
def build_candidates(mapping: dict[str, Any], style: dict[str, Any]) -> list[dict[str, Any]]:
"""Output candidate per *original* Style Layer, not one selector shared across layers."""
layers = style["layers"]
seen: set[str] = set()
result: list[dict[str, Any]] = []
for assignment in sorted(mapping["assignments"], key=lambda row: row["visualObjectId"]):
oid = assignment["visualObjectId"]
if oid in seen:
raise ValueError(f"visualObjectId 重复: {oid}")
seen.add(oid)
evidence = assignment.get("renderEvidence") or {}
comps = evidence.get("components") or []
eligible = (assignment["status"] in ELIGIBLE and
isinstance(assignment.get("standardLayer"), str) and
bool(assignment.get("standardLayer")))
source = None
source_layer = evidence.get("sourceLayer")
reasons: set[str] = set()
fields: dict[str, str] = {}
component_layers: set[int] = set()
own_direct: set[int] = set()
if not eligible:
reasons.add("UNRESOLVED_STANDARD_CLASSIFICATION")
if not comps or not isinstance(source_layer, str):
reasons.add("NO_COMPONENTS_OR_SOURCE_LAYER")
for c in comps:
index = c.get("styleIndex")
if type(index) is not int or index < 0 or index >= len(layers):
raise ValueError(f"{oid}: 无效 Style Index")
layer = layers[index]
if layer.get("id") != c.get("styleLayerId") or layer.get("source-layer") != source_layer:
raise ValueError(f"{oid}: Style component/source-layer 不匹配")
if source is None:
source = layer.get("source")
if layer.get("source") != source or not isinstance(source, str):
reasons.add("MULTIPLE_OR_MISSING_SOURCES")
component_layers.add(index)
pair = predicate(layer, c)
if pair:
field, value = pair
if field in fields and fields[field] != value:
reasons.add("CONTRADICTORY_DIRECT_FIELD")
fields[field] = value
own_direct.add(index)
if not fields:
reasons.add("NO_DIRECT_RESOURCE_FIELD")
terms = [["==", ["get", f], v] for f, v in sorted(fields.items())]
all_predicate = terms[0] if len(terms) == 1 else ["all", *terms] if terms else None
for index in sorted(component_layers):
status = "CANDIDATE_ONLY" if not reasons else "BLOCKED_STATIC"
result.append({
"visualObjectTypeId": oid,
"standardLayer": assignment.get("standardLayer"),
"assignmentStatus": assignment["status"],
"styleLayerId": layers[index]["id"],
"styleIndex": index,
"sourceId": source,
"sourceLayer": source_layer,
"selector": all_predicate if status == "CANDIDATE_ONLY" else None,
"selectorOrigin": "DIRECT_OR_CONJUNCTION" if index in own_direct else "INHERIT_SAME_RAW_FEATURE_CANDIDATE",
"staticBlockers": sorted(reasons),
"sampleTargetMatches": 0,
"sameStandardOtherTypeHits": 0,
"crossStandardHits": 0,
"unresolvedOrUnknownHits": 0,
"outOfCatalogHits": 0,
"counterexamples": [],
"runtimeAuthorized": False,
})
return result
def _append_counterexample(row: dict[str, Any], category: str, tile: str, fid: int | None, actual: str):
if len(row["counterexamples"]) < 3:
row["counterexamples"].append({
"category": category, "tile": tile, "featureId": fid,
"actualVisualObjectTypeId": actual,
})
def evaluate_examples(
rows: list[dict[str, Any]],
observations: list[dict[str, Any]],
assignments: dict[str, dict[str, Any]],
*,
eval_selector,
) -> dict[str, Any]:
"""Feature observations are original-Style filtered matches; no runtime grant."""
by_layer: dict[str, list[dict[str, Any]]] = defaultdict(list)
for row in rows:
if row["selector"] is not None:
by_layer[row["styleLayerId"]].append(row)
hits = Counter()
for obs in observations:
actual_id = obs["visualObjectTypeId"]
actual = assignments.get(actual_id)
for layer_id in obs["matchedStyleLayerIds"]:
for row in by_layer.get(layer_id, ()):
if not eval_selector(row["selector"], obs["properties"], obs["zoom"]):
continue
if actual_id == row["visualObjectTypeId"]:
field = "sampleTargetMatches"
elif actual is None:
field = "outOfCatalogHits"
elif actual.get("status") not in ELIGIBLE or not actual.get("standardLayer"):
field = "unresolvedOrUnknownHits"
elif actual["standardLayer"] == row["standardLayer"]:
field = "sameStandardOtherTypeHits"
else:
field = "crossStandardHits"
row[field] += 1
hits[field] += 1
if field not in {"sampleTargetMatches", "sameStandardOtherTypeHits"}:
_append_counterexample(
row, field, obs["tile"], obs.get("featureId"), actual_id
)
sample_status = Counter()
for row in rows:
if row["selector"] is None:
row["sampleStatus"] = "NO_CANDIDATE"
elif row["crossStandardHits"] or row["unresolvedOrUnknownHits"] or row["outOfCatalogHits"]:
row["sampleStatus"] = "CROSS_CLASS_OR_RESIDUAL_COLLISION"
elif row["sampleTargetMatches"] == 0:
row["sampleStatus"] = "TARGET_NOT_OBSERVED"
else:
row["sampleStatus"] = "SAMPLE_NO_FORBIDDEN_MATCH"
sample_status[row["sampleStatus"]] += 1
return {"candidateStatusCounts": dict(sorted(sample_status.items())),
"observationMatchCounts": dict(sorted(hits.items()))}
def run_audit(style: dict[str, Any], mapping: dict[str, Any], catalog: dict[str, Any],
pbf_root: Path, sprite_json: dict[str, Any], sprite_hashes: dict[str, str]) -> dict[str, Any]:
engine = load_instantiator()
rows = build_candidates(mapping, style)
catalog_ids = [obj.get("visual_object_id") for obj in catalog["visual_objects"]]
if len(catalog_ids) != len(set(catalog_ids)):
raise ValueError("Actual Catalog Visual Object ID 重复")
mapping_ids = {assignment["visualObjectId"] for assignment in mapping["assignments"]}
if set(catalog_ids) != mapping_ids:
raise ValueError("Mapping 与 Catalog 的 Visual Object ID 覆盖不一致")
relevant_layers: dict[str, list[tuple[int, dict]]] = defaultdict(list)
candidate_source_layers: set[str] = set()
for row in rows:
if row["selector"] is not None:
candidate_source_layers.add(row["sourceLayer"])
source_owners: dict[str, set[str]] = defaultdict(set)
for index, layer in enumerate(style["layers"]):
sl = layer.get("source-layer")
if sl in candidate_source_layers:
relevant_layers[sl].append((index, layer))
source_owners[sl].add(layer.get("source"))
if any(len(owners) != 1 or None in owners for owners in source_owners.values()):
raise ValueError("相同 source-layer 对应多个 Style Source,单 PBF Root 无法唯一验证")
# Only tiles already referenced by validated Catalog examples, all raw features
# within each tile, and all original Style components for those features.
examples = {obj["example"]["tile"] for obj in catalog["visual_objects"]}
assignment_by_id = {row["visualObjectId"]: row for row in mapping["assignments"]}
if len(assignment_by_id) != len(mapping["assignments"]):
raise ValueError("Mapping Visual Object ID 冲突")
# Limit in-memory observations by processing each tile separately.
sample_stats = Counter()
aggregate = Counter()
for tile in sorted(examples):
parts = tile.split("/")
if len(parts) != 3 or not all(p.isdigit() for p in parts):
raise ValueError(f"无效 tile: {tile}")
z, x, y = map(int, parts)
if x >= 1 << z or y >= 1 << z:
raise ValueError(f"超出 Tile 范围: {tile}")
source_path = pbf_root / str(z) / str(x) / (str(y) + ".pbf")
if not source_path.is_file():
raise ValueError(f"原始 PBF 不存在: {source_path}")
decoded = vector_tile_pb2.tile()
decoded.ParseFromString(source_path.read_bytes())
source_layer_counts = Counter(layer.name for layer in decoded.layers)
if any(source_layer_counts[sl] > 1 for sl in candidate_source_layers):
raise ValueError(f"原始 PBF 中 SourceLayer 名称重复: {tile}")
observations = []
for tile_layer in decoded.layers:
style_members = relevant_layers.get(tile_layer.name)
if not style_members:
continue
for feature in tile_layer.features:
sample_stats["sourceLayerRawFeaturesExamined"] += 1
props = engine.decode_feature_properties(tile_layer, feature)
matched_layers = []
components = []
for index, layer in style_members:
if engine.matches(layer, props, z):
matched_layers.append(layer["id"])
components.extend(engine.component_from_layer(layer, index, props, z, sprite_json, sprite_hashes))
if not matched_layers:
continue
vo_id = engine.stable_id("VO", {
"geometry_role": engine.geometry_role(feature.type),
"components": engine.signature_components(components),
})
observations.append({
"visualObjectTypeId": vo_id,
"properties": props, "zoom": z,
"matchedStyleLayerIds": matched_layers,
"tile": tile, "featureId": int(feature.id) if feature.HasField("id") else None,
})
sample_stats["styledRawFeaturesObserved"] += 1
partial = evaluate_examples(
rows, observations, assignment_by_id,
eval_selector=lambda expr, props, zoom: engine.evaluate(expr, props, zoom) is True,
)
for key, val in partial["observationMatchCounts"].items():
aggregate[key] += val
sample_stats["tilesScanned"] += 1
# Recompute final row status because some target samples may be found later.
statuses = Counter()
for row in rows:
if row["selector"] is None:
row["sampleStatus"] = "NO_CANDIDATE"
elif any(row[k] for k in ("crossStandardHits", "unresolvedOrUnknownHits", "outOfCatalogHits")):
row["sampleStatus"] = "CROSS_CLASS_OR_RESIDUAL_COLLISION"
elif not row["sampleTargetMatches"]:
row["sampleStatus"] = "TARGET_NOT_OBSERVED"
else:
row["sampleStatus"] = "SAMPLE_NO_FORBIDDEN_MATCH"
statuses[row["sampleStatus"]] += 1
return {
"schemaVersion": "navsea-selector-collision-evidence-audit/v1",
"scope": "261_RECORDED_EXAMPLE_TILES_ALL_RAW_FEATURES_NOT_ALL_PROVIDER_TILES",
"observedTileCount": sample_stats["tilesScanned"],
"sourceLayerRawFeaturesExamined": sample_stats["sourceLayerRawFeaturesExamined"],
"styledRawFeaturesObserved": sample_stats["styledRawFeaturesObserved"],
"candidateStatusCounts": dict(sorted(statuses.items())),
"observationMatchCounts": dict(sorted(aggregate.items())),
"displayRuntimeAuthorizedCount": 0,
"primaryAuthorizedCount": 0,
"entries": rows,
}
def main() -> int:
p = argparse.ArgumentParser(description=__doc__)
for name in ("style", "mapping", "catalog", "pbf-root", "sprite-json", "sprite-png", "output"):
p.add_argument(f"--{name}", type=Path, required=True)
args = p.parse_args()
style_bytes = args.style.read_bytes()
mapping = json.loads(args.mapping.read_text(encoding="utf-8"))
expected = (mapping.get("renderEvidenceSource") or {}).get("styleSha256")
if expected != hashlib.sha256(style_bytes).hexdigest():
raise ValueError("Mapping 绑定的原 Style SHA-256 不一致")
catalog = json.loads(args.catalog.read_text(encoding="utf-8"))
declared = (catalog.get("inputs") or {}).get("style")
if not isinstance(declared, str) or not Path(declared).is_file():
raise ValueError("Catalog 声明的原 Style 路径不可校验")
if hashlib.sha256(Path(declared).read_bytes()).digest() != hashlib.sha256(style_bytes).digest():
raise ValueError("Catalog 原 Style SHA-256 不一致")
style = json.loads(style_bytes)
sprite_json = json.loads(args.sprite_json.read_text(encoding="utf-8"))
with Image.open(args.sprite_png) as atlas:
hashes = load_instantiator().sprite_hashes(sprite_json, atlas.convert("RGBA"))
out = run_audit(style, mapping, catalog, args.pbf_root, sprite_json, hashes)
args.output.parent.mkdir(parents=True, exist_ok=True)
if args.output.exists():
raise ValueError("审计结果已存在:避免覆盖前一份不可变证据;请增版本")
args.output.write_text(json.dumps(out, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps({k: v for k, v in out.items() if k != "entries"}, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,220 @@
#!/usr/bin/env python3
"""标准显示组与 PRIMARY 人审门禁;只生成/检查审核草稿,不向 Runtime 授权。"""
from __future__ import annotations
import argparse
from collections import Counter
import hashlib
import json
from pathlib import Path
from typing import Any
DRAFT_SCHEMA = "navsea-standard-display-review-draft/v1"
GROUP_SCHEMA = "navsea-observed-host-group-candidates/v1"
APPROVABLE_ASSIGNMENTS = {"LAYER_ONLY", "VISUAL_OBJECT_MATCH"}
def sha256(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def _unique_ids(records: list[dict[str, Any]], key: str) -> dict[str, dict[str, Any]]:
output: dict[str, dict[str, Any]] = {}
for row in records:
value = row.get(key)
if not isinstance(value, str) or not value or value in output:
raise ValueError(f"{key} 缺失或重复:{value!r}")
output[value] = row
return output
def _load_inputs(queue: dict[str, Any], groups: dict[str, Any]) -> tuple[dict, dict, set[str]]:
if queue.get("schemaVersion") != "navsea-feature-identity-scope-audit/v1":
raise ValueError("身份审核队列 schema 不正确")
if groups.get("schemaVersion") != GROUP_SCHEMA:
raise ValueError("Host Group 候选集合 schema 不正确")
if not isinstance(queue.get("reviewQueue"), list) or not isinstance(groups.get("groupIds"), list):
raise ValueError("身份审核队列 / Host group IDs 不是数组")
by_object = _unique_ids(queue["reviewQueue"], "visualObjectTypeId")
group_ids = groups["groupIds"]
if not group_ids or any(not isinstance(g, str) or not g for g in group_ids) or len(set(group_ids)) != len(group_ids):
raise ValueError("Host Group ID 缺失或重复")
standards = sorted({row["standardLayer"] for row in by_object.values()
if isinstance(row.get("standardLayer"), str) and row["standardLayer"]})
if any(row.get("primaryReviewStatus") != "PENDING_REVIEW" for row in by_object.values()):
raise ValueError("输入身份队列已包含被外部修改的 PRIMARY 状态")
return by_object, {standard: None for standard in standards}, set(group_ids)
def generate_review_draft(
queue: dict[str, Any], groups: dict[str, Any],
*, queue_sha256: str, groups_sha256: str, style_sha256: str,
) -> dict[str, Any]:
objects, standards, _ = _load_inputs(queue, groups)
return {
"schemaVersion": DRAFT_SCHEMA,
"scope": "OFFLINE_HUMAN_REVIEW_ONLY",
"binding": {
"providerId": queue["providerId"],
"datasetId": queue["datasetId"],
"identityReviewQueueSha256": queue_sha256,
"observedHostGroupsSha256": groups_sha256,
"originalStyleSha256": style_sha256,
},
"standardLayerDecisions": [
{"standardLayer": name, "status": "PENDING", "displayGroupId": None,
"reviewer": None, "evidenceRefs": []}
for name in standards
],
"visualObjectDecisions": [
{"visualObjectTypeId": object_id, "status": "PENDING",
"reviewedStandardLayer": None, "primaryComponentIndex": None,
"componentParentLinks": [], "reviewer": None, "evidenceRefs": []}
for object_id in sorted(objects)
],
}
def _evidence(row: dict[str, Any]) -> bool:
reviewer = row.get("reviewer")
refs = row.get("evidenceRefs")
return (isinstance(reviewer, str) and bool(reviewer.strip()) and
isinstance(refs, list) and bool(refs) and
all(isinstance(ref, str) and bool(ref.strip()) for ref in refs))
def validate_review_draft(
draft: dict[str, Any], queue: dict[str, Any], groups: dict[str, Any],
*, queue_sha256: str, groups_sha256: str, style_sha256: str,
) -> dict[str, Any]:
"""Check structure and review completeness only; no semantic/executable approval."""
objects, standards, group_ids = _load_inputs(queue, groups)
if draft.get("schemaVersion") != DRAFT_SCHEMA or draft.get("scope") != "OFFLINE_HUMAN_REVIEW_ONLY":
raise ValueError("人审草稿 schema/scope 不正确")
binding = draft.get("binding")
expected = {
"providerId": queue["providerId"],
"datasetId": queue["datasetId"],
"identityReviewQueueSha256": queue_sha256,
"observedHostGroupsSha256": groups_sha256,
"originalStyleSha256": style_sha256,
}
if binding != expected:
raise ValueError("人审草稿来源身份/指纹不一致")
if not isinstance(draft.get("standardLayerDecisions"), list) or not isinstance(draft.get("visualObjectDecisions"), list):
raise ValueError("审核决策清单不正确")
by_standard = _unique_ids(draft["standardLayerDecisions"], "standardLayer")
by_decision = _unique_ids(draft["visualObjectDecisions"], "visualObjectTypeId")
if set(by_standard) != set(standards) or set(by_decision) != set(objects):
raise ValueError("审核清单缺少、增加了标准层或 Visual Object")
group_counts: Counter[str] = Counter()
visual_counts: Counter[str] = Counter()
invalid: list[str] = []
catalog_approved = groups.get("approvalStatus") == "APPROVED"
for name, item in by_standard.items():
status = item.get("status")
if status == "PENDING":
if (item.get("displayGroupId") is not None or item.get("reviewer") is not None or
item.get("evidenceRefs") != []):
invalid.append(f"standardLayer {name}: 待审项不得携带已审资料")
elif status == "REVIEWED":
if not catalog_approved:
invalid.append(f"standardLayer {name}: Group Catalog 未正式批准")
if item.get("displayGroupId") not in group_ids or not _evidence(item):
invalid.append(f"standardLayer {name}: Group 归属或审核证据不足")
elif status == "REJECTED":
if item.get("displayGroupId") is not None or not _evidence(item):
invalid.append(f"standardLayer {name}: 拒绝结论缺证据或带有归属")
else:
invalid.append(f"standardLayer {name}: 未知状态")
group_counts[str(status)] += 1
for object_id, item in by_decision.items():
status = item.get("status")
source = objects[object_id]
components = source.get("componentCandidates")
if not isinstance(components, list) or not components:
invalid.append(f"{object_id}: 原始组件证据缺失")
continue
if status == "PENDING":
if (item.get("reviewedStandardLayer") is not None or
item.get("primaryComponentIndex") is not None or
item.get("componentParentLinks") != [] or item.get("reviewer") is not None or
item.get("evidenceRefs") != []):
invalid.append(f"{object_id}: 待审项不能预先带 PRIMARY/父关系")
elif status == "REVIEWED":
classified = (source.get("assignmentStatus") in APPROVABLE_ASSIGNMENTS and
isinstance(source.get("standardLayer"), str) and
source["standardLayer"] in standards)
standard = source.get("standardLayer")
if not classified or item.get("reviewedStandardLayer") != standard:
invalid.append(f"{object_id}: UNKNOWN/AMBIGUOUS 或标准层重分类不允许直接通过")
if not (classified and by_standard[standard].get("status") == "REVIEWED"):
invalid.append(f"{object_id}: 标准层显示组尚未批准")
primary = item.get("primaryComponentIndex")
indices = {component.get("componentIndex") for component in components}
if type(primary) is not int or primary not in indices:
invalid.append(f"{object_id}: PRIMARY 下标不在原始组件内")
links = item.get("componentParentLinks")
other_indices = indices - {primary}
if not isinstance(links, list) or any(
not isinstance(link, dict) or
type(link.get("componentIndex")) is not int or
type(link.get("parentComponentIndex")) is not int or
link.get("parentComponentIndex") != primary
for link in links
) or {link.get("componentIndex") for link in links} != other_indices or len(links) != len(other_indices):
invalid.append(f"{object_id}: COMPONENT→PRIMARY 关系不完整或重复")
if not _evidence(item):
invalid.append(f"{object_id}: PRIMARY 审查证据不足")
elif status == "REJECTED":
if (item.get("primaryComponentIndex") is not None or
item.get("componentParentLinks") != [] or
item.get("reviewedStandardLayer") is not None or not _evidence(item)):
invalid.append(f"{object_id}: 拒绝 PRIMARY 的证据或字段无效")
else:
invalid.append(f"{object_id}: 未知状态")
visual_counts[str(status)] += 1
return {
"schemaVersion": "navsea-standard-display-review-gate-report/v1",
"scope": "STRUCTURAL_REVIEW_ONLY_NOT_RUNTIME_AUTHORIZATION",
"status": "PASS_PENDING_REVIEW" if not invalid else "FAIL_CLOSED",
"standardLayerStatusCounts": dict(sorted(group_counts.items())),
"visualObjectStatusCounts": dict(sorted(visual_counts.items())),
"reviewedObjectCount": visual_counts["REVIEWED"],
"runtimeExecutionAuthorizedCount": 0,
"globalIdentityAuthorizedCount": 0,
"validationErrors": invalid,
}
def main() -> int:
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--queue", type=Path, required=True)
p.add_argument("--groups", type=Path, required=True)
p.add_argument("--style", type=Path, required=True)
p.add_argument("--draft", type=Path, required=True)
p.add_argument("--generate", action="store_true")
p.add_argument("--report", type=Path)
a = p.parse_args()
q_bytes, g_bytes, s_bytes = a.queue.read_bytes(), a.groups.read_bytes(), a.style.read_bytes()
queue = json.loads(q_bytes)
groups = json.loads(g_bytes)
kwargs = {"queue_sha256": sha256(q_bytes), "groups_sha256": sha256(g_bytes), "style_sha256": sha256(s_bytes)}
if a.generate:
if a.draft.exists():
raise ValueError("草稿已存在:禁止无条件覆盖可能经人工修改的审核记录")
draft = generate_review_draft(queue, groups, **kwargs)
a.draft.parent.mkdir(parents=True, exist_ok=True)
a.draft.write_text(json.dumps(draft, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
else:
draft = json.loads(a.draft.read_text(encoding="utf-8"))
result = validate_review_draft(draft, queue, groups, **kwargs)
if a.report:
a.report.parent.mkdir(parents=True, exist_ok=True)
a.report.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps(result, ensure_ascii=False))
return 0 if result["status"] == "PASS_PENDING_REVIEW" else 2
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""全量 Visual Object Catalog 的原始 Style Layer 分类隔离审计,永不授权运行。"""
from __future__ import annotations
import argparse
from collections import Counter, defaultdict
import hashlib
import json
from pathlib import Path
from typing import Any
RESOLVED = frozenset({"LAYER_ONLY", "VISUAL_OBJECT_MATCH"})
def audit_isolation(style: dict[str, Any], mapping: dict[str, Any],
expected_count: int) -> dict[str, Any]:
layers = style.get("layers")
assignments = mapping.get("assignments")
if not isinstance(layers, list) or not isinstance(assignments, list) or not layers:
raise ValueError("Style/Mapping 输入结构无效")
if not isinstance(expected_count, int) or expected_count < 1:
raise ValueError("Full Catalog 预期对象数无效")
if len(assignments) != expected_count:
raise ValueError("Mapping 没有覆盖完整对象目录")
by_layer: dict[int, list[dict[str, Any]]] = defaultdict(list)
seen_objects: set[str] = set()
for row in assignments:
oid = row.get("visualObjectId")
if not isinstance(oid, str) or not oid or oid in seen_objects:
raise ValueError("Mapping Visual Object ID 重复/无效")
seen_objects.add(oid)
evidence = row.get("renderEvidence")
if not isinstance(evidence, dict) or not isinstance(evidence.get("components"), list) or not evidence["components"]:
raise ValueError(f"renderEvidence 缺失: {oid}")
observed_indices: set[int] = set()
for comp in evidence["components"]:
index = comp.get("styleIndex")
if type(index) is not int or index < 0 or index >= len(layers) or layers[index].get("id") != comp.get("styleLayerId"):
raise ValueError(f"Style index/layer identity mismatch: {oid}")
original = layers[index]
if original.get("source-layer") != evidence.get("sourceLayer"):
raise ValueError(f"Source Layer mismatch: {oid}")
observed_indices.add(index)
for index in observed_indices:
by_layer[index].append(row)
results: list[dict[str, Any]] = []
counts: Counter[str] = Counter()
for index, layer in enumerate(layers):
rows = by_layer.get(index, [])
statuses = Counter(row.get("status") for row in rows)
standards = Counter(row.get("standardLayer") for row in rows
if row.get("status") in RESOLVED and
isinstance(row.get("standardLayer"), str) and
row["standardLayer"])
unresolved = sum(row.get("status") not in RESOLVED or not isinstance(row.get("standardLayer"), str)
or not row["standardLayer"] for row in rows)
if not rows:
classification = "NO_OBSERVED_STYLE_FEATURES"
elif unresolved == len(rows):
classification = "ONLY_UNRESOLVED_OR_RESIDUAL"
elif unresolved > 0 or len(standards) > 1:
classification = "MIXED_OR_RESIDUAL"
elif len(standards) == 1:
classification = "ONE_STANDARD_IN_COMPLETE_CATALOG_CANDIDATE"
else:
classification = "NO_REVIEWED_STANDARD"
counts[classification] += 1
results.append({
"styleIndex": index, "styleLayerId": layer["id"],
"sourceId": layer.get("source"), "sourceLayer": layer.get("source-layer"),
"styleType": layer.get("type"), "observedVisualTypeCount": len(rows),
"assignedStandardCounts": dict(sorted(standards.items())),
"assignmentStatusCounts": dict(sorted(statuses.items())),
"unresolvedVisualTypeCount": unresolved,
"catalogClassification": classification,
"possibleWholeLayerVisibilityInFrozenSnapshot": classification == "ONE_STANDARD_IN_COMPLETE_CATALOG_CANDIDATE",
"productionWholeLayerVisibilityAuthorized": False,
"reasons": [
"原始 Style 可能含动态/未来数据;全量 Visual Object Catalog 并非完整 MapLibre GPU 结果",
"必须等待正式 standardLayer→displayGroup 审批及完整 Layer Selector 复核",
],
})
return {
"schemaVersion": "navsea-style-layer-isolation-audit/v1",
"scope": "FULL_RECORDED_VISUAL_TYPE_CATALOG_SNAPSHOT_NOT_RUNTIME_PROOF",
"expectedVisualTypeCount": expected_count,
"styleLayerCount": len(layers),
"layerCategoryCounts": dict(sorted(counts.items())),
"runtimeAuthorizationCount": 0,
"layers": results,
}
def main() -> int:
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--style", type=Path, required=True)
p.add_argument("--mapping", type=Path, required=True)
p.add_argument("--output", type=Path, required=True)
args = p.parse_args()
style_bytes = args.style.read_bytes()
mapping = json.loads(args.mapping.read_text(encoding="utf-8"))
source = mapping.get("renderEvidenceSource") or {}
if source.get("styleSha256") != hashlib.sha256(style_bytes).hexdigest():
raise ValueError("Mapping Style SHA-256 绑定不一致")
total = source.get("distinctVisualObjectCount")
out = audit_isolation(json.loads(style_bytes), mapping, total)
if args.output.exists():
raise ValueError("结果文件已存在:需要增加版本,不能覆盖历史审计")
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(out, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps({
"styleLayerCount": out["styleLayerCount"], "layerCategoryCounts": out["layerCategoryCounts"],
"runtimeAuthorizationCount": 0, "output": str(args.output),
}, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())