fix: finalize S-52 runtime production contract
This commit is contained in:
95
docs/报告/CM95_S52_ProductionPipelineV2与九州运行报告_v2.md
Normal file
95
docs/报告/CM95_S52_ProductionPipelineV2与九州运行报告_v2.md
Normal file
@@ -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、对应检查工具和本报告。
|
||||
@@ -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],
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user