diff --git a/docs/报告/CM95_S52_ProductionPipelineV2与九州运行报告_v2.md b/docs/报告/CM95_S52_ProductionPipelineV2与九州运行报告_v2.md new file mode 100644 index 0000000..b52232a --- /dev/null +++ b/docs/报告/CM95_S52_ProductionPipelineV2与九州运行报告_v2.md @@ -0,0 +1,95 @@ +# CM95 S-52 Production Pipeline v2 与九州运行报告 v2 + +日期:2026-09-18 + +## 1. 本次收尾范围 + +本报告记录九州工程 AOI 的 S-52 Production Pipeline v2 正式生产、最终 release gate、runtime contract 修正与 finalize 结果。 + +- Release ID:`cm95-d1-62a85bd40d3a-bbaaeafee1b4` +- Plan ID:`bbaaeafee1b400dc97bcc774ec36247cbc4cf68860b267dea8befe1bdd890564` +- Pipeline:`cm95pbf-production/2.0-s52-aoi` +- Builder contract:`cm95pbf-builder/0.4.8-s52-mariner-runtime-boundary` +- AOI:`kyushu-main-nearby-v1` +- bbox:`128.0,29.5,132.5,34.8` +- profile:`delivery` +- S-52:enabled,point=`Paper`,boundary=`Plain`,mariner_mode=`runtime` +- Lookup SHA256:`f63cbc0d3c9317b1873c0b0c470a5e7371d1b6d8d58393da660f70b837a52eb7` + +该 bbox 是工程测试范围,不是九州行政边界。 + +## 2. Production 最终结果 + +生产 ledger 最终为: + +- jobs:1109 +- PASS:1109 +- FAILED:0 +- PBF:1109 +- planned output features:393856 +- report output features:393856 +- decoded PBF features:393856 + +因此 planned / report / decoded 三者完全一致,PBF 数量与 Plan job 数完全一致。 + +各 zoom job 数保持 Plan 冻结值:z5=2、z6=4、z7=12、z8=24、z9=70、z10=197、z11=503、z12=297;z0-z4 为 0。 + +## 3. 首次 finalize 发现的问题 + +首次执行 `finalize-production --skip-style-contract` 时,PBF 技术数量与 Feature 数均正常,但 S-52 runtime contract 报 134 个失败。 + +调查确认这 134 个并不是 PBF 生成错误,而是 release gate 对“源数据合法未知值”要求过严: + +- 133 个 `OBSTRN04`:全部为 `EXPSOU=1` 且源对象没有 `VALSOU`;此时 hazard depth 本来就是未知,运行时应结合 EXPSOU、WATLEV 与周围 DEPARE 摘要判断,不能伪造 `s52_hazard_depth_m`。 +- 1 个 `DEPARE01`:对象为 `DRGARE`,源记录 `attributes_json={}`;reference CSP 本来就对“疏浚区缺 DRVAL1”有专门缺省呈现规则,不能伪造 `depth_min_m`。 + +典型源记录核对: + +- `cm95:1:03300360/D/03630390.D:955:point`:`UWTROC`,属性为 `{"EXPSOU": 1, "WATLEV": 3}`。 +- `cm95:1:03300360/E/03710393.E:27:area`:`DRGARE`,属性为 `{}`。 + +因此收尾修正采用“保留未知事实 + gate 明确记录例外”的方式,没有为了通过 finalize 向 PBF 填充虚构深度,也没有重建 1109 个已经正确生成的 PBF。 + +## 4. Runtime contract gate 修正 + +`src/cm95pbf/release.py` 的 runtime gate 已改为: + +- 普通 DEPARE/DEPARE02 仍必须有 `depth_min_m`; +- 仅 `canonical_object_type=dredged_area` 且源深度未知时允许缺失,并记录例外; +- OBSTRN04/WRECKS02 默认仍必须有 `s52_hazard_depth_m`; +- 仅 `OBSTRN04 + s52_expsou_code=1` 的未知 hazard depth 允许缺失,并记录例外; +- 其他缺失仍然 fail-closed; +- dynamic final state 泄漏规则不变。 + +Standalone runtime contract checker 同步采用同一语义,避免工具与 finalize gate 不一致。 + +新增审计字段:`unknown_fact_exceptions`。 + +## 5. 最终 finalize 结果 + +再次执行 `finalize-production --skip-style-contract`: + +- release status:**PASS** +- S-52 runtime contract:**PASS** +- mariner dependent features:237849 +- DEPARE01:35722 +- DEPCNT02:37634 +- OBSTRN04:35661 +- SOUNDG02:128126 +- WRECKS02:706 +- `dynamic_final_leaks = {}` +- `failure_count = 0` +- `unknown_fact_exceptions = {"DEPARE:dredged_area_missing_depth_min": 1, "OBSTRN04:EXPSOU1_unknown_hazard_depth": 133}` +- nav name conflict audit:**PASS** +- quantization fallbacks:10 +- quantization omissions:154 +- non-renderable omissions:2,均为 `seabed_text_point` +- release failures:`[]` + +`--skip-style-contract` 仅跳过旧 NavSea semantic style/sprite contract;本次 PBF 技术检查与 S-52 runtime contract 均实际执行并通过。 + +## 6. 尚未宣称完成的范围 + +`semantic_completeness` 仍为 `PENDING_REVIEW`,当前仍有 112 个 pending semantic review variants / 52828 rows。该项不是本次九州 Production Pipeline v2 的失败 gate,因此不能据此声称全部 ENC/S-52 对象语义已经完整支持。 + +本次收尾没有修改或提交现有 Linux/MySQL migration dirty 文件;只处理 S-52 runtime release gate、对应检查工具和本报告。 diff --git a/src/cm95pbf/release.py b/src/cm95pbf/release.py index be344fc..6976779 100644 --- a/src/cm95pbf/release.py +++ b/src/cm95pbf/release.py @@ -345,6 +345,7 @@ def finalize_release( s52_runtime_dependent_count = 0 s52_runtime_leaks = Counter() s52_runtime_fact_counts = Counter() + s52_runtime_unknown_fact_exceptions = Counter() s52_dynamic_final_fields = { "s52_is_safety_contour", "s52_sounding_prefix", @@ -379,7 +380,14 @@ def finalize_release( s52_runtime_leaks[key] += 1 if csp in {"DEPARE01", "DEPARE02"}: if "depth_min_m" not in props: - s52_runtime_failures.append(f"{pbf_rel}:{layer}:{fid} {csp} missing depth_min_m") + if props.get("canonical_object_type") == "dredged_area": + s52_runtime_unknown_fact_exceptions[ + "DEPARE:dredged_area_missing_depth_min" + ] += 1 + else: + s52_runtime_failures.append( + f"{pbf_rel}:{layer}:{fid} {csp} missing depth_min_m" + ) if "s52_area_color" in props: s52_runtime_leaks["DEPARE:s52_area_color"] += 1 elif csp == "DEPCNT02" and "contour_depth_m" not in props: @@ -387,7 +395,14 @@ def finalize_release( elif csp == "SOUNDG02" and "s52_sounding_depth_m" not in props: s52_runtime_failures.append(f"{pbf_rel}:{layer}:{fid} SOUNDG02 missing s52_sounding_depth_m") elif csp in {"OBSTRN04", "WRECKS02"} and "s52_hazard_depth_m" not in props: - s52_runtime_failures.append(f"{pbf_rel}:{layer}:{fid} {csp} missing s52_hazard_depth_m") + if csp == "OBSTRN04" and props.get("s52_expsou_code") == 1: + s52_runtime_unknown_fact_exceptions[ + "OBSTRN04:EXPSOU1_unknown_hazard_depth" + ] += 1 + else: + s52_runtime_failures.append( + f"{pbf_rel}:{layer}:{fid} {csp} missing s52_hazard_depth_m" + ) elif "s52_runtime_csp" in props: s52_runtime_failures.append(f"{pbf_rel}:{layer}:{fid} runtime CSP without mariner-dependent marker") for key in ( @@ -460,6 +475,7 @@ def finalize_release( "runtime_csp_counts": dict(sorted(s52_runtime_csp_counts.items())), "runtime_layer_counts": dict(sorted(s52_runtime_layer_counts.items())), "fact_counts": dict(sorted(s52_runtime_fact_counts.items())), + "unknown_fact_exceptions": dict(sorted(s52_runtime_unknown_fact_exceptions.items())), "dynamic_final_leaks": dict(sorted(s52_runtime_leaks.items())), "failure_count": len(s52_runtime_failures), "failure_examples": s52_runtime_failures[:100], diff --git a/tools/cm95_s52_runtime_contract_check.py b/tools/cm95_s52_runtime_contract_check.py index 3a58783..9952978 100755 --- a/tools/cm95_s52_runtime_contract_check.py +++ b/tools/cm95_s52_runtime_contract_check.py @@ -40,6 +40,7 @@ def audit_runtime(features: list[tuple[str, dict[str, Any]]]) -> dict[str, Any]: csp_counts = Counter(str(props.get("s52_runtime_csp") or "") for _, props in runtime) leaked = Counter() + unknown_fact_exceptions = Counter() for _, props in runtime: csp = str(props.get("s52_runtime_csp") or "") if not csp: @@ -51,13 +52,21 @@ def audit_runtime(features: list[tuple[str, dict[str, Any]]]) -> dict[str, Any]: leaked[key] += 1 if csp in {"DEPARE01", "DEPARE02"}: if "depth_min_m" not in props: - failures.append(f"{csp} 缺少 depth_min_m") + if props.get("canonical_object_type") == "dredged_area": + unknown_fact_exceptions["DEPARE:dredged_area_missing_depth_min"] += 1 + else: + failures.append(f"{csp} 缺少 depth_min_m") if "s52_area_color" in props: leaked["DEPARE:s52_area_color"] += 1 elif csp == "DEPCNT02" and "contour_depth_m" not in props: failures.append("DEPCNT02 缺少 contour_depth_m") elif csp == "SOUNDG02" and "s52_sounding_depth_m" not in props: failures.append("SOUNDG02 缺少 s52_sounding_depth_m") + elif csp in {"OBSTRN04", "WRECKS02"} and "s52_hazard_depth_m" not in props: + if csp == "OBSTRN04" and props.get("s52_expsou_code") == 1: + unknown_fact_exceptions["OBSTRN04:EXPSOU1_unknown_hazard_depth"] += 1 + else: + failures.append(f"{csp} 缺少 s52_hazard_depth_m") if leaked: failures.append(f"发现 runtime 最终状态泄漏: {dict(leaked)}") @@ -79,6 +88,7 @@ def audit_runtime(features: list[tuple[str, dict[str, Any]]]) -> dict[str, Any]: "s52_surrounding_min_drval2_m", ) }, + "unknown_fact_exceptions": dict(unknown_fact_exceptions), "dynamic_final_leaks": dict(leaked), "failures": failures, "status": "PASS" if not failures else "FAIL",