fix: finalize S-52 runtime production contract

This commit is contained in:
OpenAI Codex
2026-09-18 19:28:45 +08:00
parent b9c24ff5c7
commit 46beab7191
3 changed files with 124 additions and 3 deletions

View 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、对应检查工具和本报告。

View File

@@ -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],

View File

@@ -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",