diff --git a/docs/50_Step记录/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_v4.md b/docs/50_Step记录/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_v4.md new file mode 100644 index 000000000..45800018e --- /dev/null +++ b/docs/50_Step记录/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_v4.md @@ -0,0 +1,45 @@ +# STEP-073 — QBIN2 V4 Core 与通用 Field Profile Validator 最终关闭修正 v4 + +状态:完成 + +日期:2026-09-13 + +## 基线 + +- 开始 HEAD:`0b66a5cd30e91423cbc864a327a53bed06cae91c`。 +- 开始工作区含 STEP-062 文档修改及多项历史 untracked 文件;本 Step 未修改、暂存或提交它们。 + +## 目标 + +关闭独立审计在实际重链 `mbgl-test-runner` 后发现的三项问题:Frame 的唯一共享 validity domain、实际已知 Core semantic 的错误优先级,以及 ForecastClock 测试的 pause/resume 语义。 + +## 完成项 + +- [x] `DecodedFieldFrame` 允许空 numeric `planes[]`,完整 designated validity 仍结构合法;所缺 required numeric plane 由 Validator 返回 `MissingRequiredPlane`。 +- [x] designated validity 强制 `Validity/Bitset/NONE/group0/None/REQUIRED`、cell count 与 bit payload 长度一致,并与 `validityBits` 使用同一 `shared_ptr`;任一违反均为 `InvalidFrame`。 +- [x] 禁止 `planes[]` 出现第二张 Validity plane,确保一帧只有唯一共享 validity domain。 +- [x] 保留静态数据表驱动的 Core semantic contract,并在 required-plane descriptor 匹配前检查实际 plane;semantic 1 Scalar 伪装、semantic 10 component/group 冲突均返回 `SemanticContractMismatch`。 +- [x] unknown Scalar 及 opaque unit 的可扩展行为保持;Direction circular quantization v3 规则未变。 +- [x] ForecastClock 测试在 `pause()` 后显式 `play()`,未改变生产 `pause()` 语义。 + +## 允许修改 + +仅修改 Field Runtime Core、通用 Validator、对应 C++ 测试和本 Step 文档/报告/开发记录。 + +## 禁止修改确认 + +未修改 QBIN2 Decoder、Binding/Projection、Renderer、shader、JNI、`platform/android/**`、`packages/maplibre-particle-rn/**`、`integration/react-native/**`、Style JSON/source schema 或 V3 `submitField(...)` 路径;未执行 Android Gradle、APK、ADB、真机、Jest 或 iOS/Metal。 + +## 验证 + +构建/测试完整输出均落入 `/tmp` 限流日志(仓库未提供 build-output-throttle 指定的 helper script,仅在失败时读取有限末尾窗口)。 + +- [x] `cmake --build build-linux-opengl --target mbgl-test-runner -j3`:实际重编译并重链 PASS。 +- [x] `mbgl-test-runner --gtest_list_tests`:确认包含 `ParticleFieldProfileValidator.` 与 `ParticleFieldRuntime.`。 +- [x] `mbgl-test-runner --gtest_filter='ParticleFieldRuntime.*:ParticleFieldProfileValidator.*'`:实际运行 11 tests,11 PASS,0 failed;绝非 0 tests。 +- [x] `mbgl-test-runner --gtest_filter='Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*'`:实际运行 11 tests,11 PASS,0 failed。 +- [x] `git diff --check`:PASS。 + +## 当前结果、阻塞与下一步 + +STEP-073 v4 的关闭条件已满足,无范围内阻塞。停止于此,等待 ChatGPT 独立复验;不进入 STEP-074。 diff --git a/docs/报告/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_Codex完成报告_v4.md b/docs/报告/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_Codex完成报告_v4.md new file mode 100644 index 000000000..517512557 --- /dev/null +++ b/docs/报告/STEP-073_QBIN2_V4_Core与通用FieldProfileValidator_Codex完成报告_v4.md @@ -0,0 +1,28 @@ +# STEP-073 QBIN2 V4 Core 与通用 Field Profile Validator 完成报告 v4 + +日期:2026-09-13 + +## 结果 + +STEP-073 最终关闭修正完成。Frame Core 现在明确维护 single shared validity invariant;空 numeric plane list 不再被结构层误判。Validator 在 required descriptor 匹配前拒绝实际保留 Core semantic 的物理含义冲突,unknown Scalar opaque 行为与 Direction v3 量化规则保持。 + +## 实际修改 + +- `DecodedFieldFrame::isStructurallyValid()` 对 designated validity 强制 `Validity`、`Bitset`、`NONE`、group 0、`None` component、`REQUIRED`、正确 cells/bit payload,并要求 descriptor storage 与 `validityBits` 是同一共享引用;常规 `planes[]` 不允许任何 Validity plane。 +- 删除空 `planes[]` 的结构拒绝。于是完整 validity-only Frame 保持结构有效,Generic Validator 对缺失 Profile numeric required plane 返回 `MissingRequiredPlane`。 +- 以既有静态协议表先核验 actual planes,故 semantic 10 的 component/group 错误,以及 semantic 1 伪装成 Scalar,都优先返回 `SemanticContractMismatch`,而不是 `MissingRequiredPlane` 或 `PlaneDescriptorMismatch`。 +- 测试新增 second validity、designated validity descriptor 各字段、`requiredFlag=false` 与 storage identity 的 fail-closed 覆盖;修正 ForecastClock pause 后的测试恢复播放。 + +## 独立 runner 验证 + +`mbgl-test-runner` 本体已重新构建和链接,并通过 `--gtest_list_tests` 确认加载 `ParticleFieldProfileValidator.`、`ParticleFieldRuntime.`。 + +- V4 filter:实际 `Running 11 tests`,`PASSED 11 tests`,failed=0。 +- V3 定向回归:实际 `Running 11 tests`,`PASSED 11 tests`,failed=0。 +- `git diff --check`:PASS。 + +构建/测试输出按节流要求写入 `/tmp` 日志;仓库当前不存在 skill 所指的 `scripts/run_limited.py` helper。首次 sandbox 构建因只读 ccache 失败,获准后以同一受限日志模式重跑并成功,不涉及源代码失败。 + +## 范围与未完成项 + +未触及 QBIN2 Decoder、Binding/Projection、Renderer、JNI、Android、RN、Style schema 或 V3 `submitField(...)`。未执行 Android Gradle、APK、ADB、真机、Jest、iOS/Metal。没有范围内未完成项;按任务要求停止,等待独立复验,不进入 STEP-074。 diff --git a/src/mbgl/particle/field/field_profile_validator.cpp b/src/mbgl/particle/field/field_profile_validator.cpp index 1cf295350..b78a99482 100644 --- a/src/mbgl/particle/field/field_profile_validator.cpp +++ b/src/mbgl/particle/field/field_profile_validator.cpp @@ -99,6 +99,15 @@ FieldProfileValidationResult FieldProfileValidator::validate(const ResolvedField actual.push_back(&frame.validityPlane); for (const auto& plane : frame.planes) actual.push_back(&plane); + // Inspect actual reserved Core semantics before profile matching. Otherwise + // an invalid known semantic can be hidden behind a missing/mismatched + // profile descriptor instead of reporting its protocol violation. + for (const auto plane : actual) { + if (!matchesCoreSemanticContract(plane->key.semanticID, plane->valueClass, plane->unitID, + plane->key.groupID, plane->key.component)) + return result(FieldProfileValidationCode::SemanticContractMismatch, "actual Core semantic descriptor"); + } + for (const auto& required : contract.requiredPlanes) { const auto same = std::find_if(actual.begin(), actual.end(), [&required](const PlaneRef plane) { return sameKey(required, *plane); @@ -109,9 +118,6 @@ FieldProfileValidationResult FieldProfileValidator::validate(const ResolvedField } for (const auto plane : actual) { - if (!matchesCoreSemanticContract(plane->key.semanticID, plane->valueClass, plane->unitID, plane->key.groupID, - plane->key.component)) - return result(FieldProfileValidationCode::SemanticContractMismatch, "actual Core semantic descriptor"); const auto declared = std::find_if(contract.requiredPlanes.begin(), contract.requiredPlanes.end(), [plane](const FieldPlaneRequirement& requirement) { return matches(requirement, *plane); diff --git a/src/mbgl/particle/field/field_runtime.cpp b/src/mbgl/particle/field/field_runtime.cpp index 020720bf4..769c5b7c3 100644 --- a/src/mbgl/particle/field/field_runtime.cpp +++ b/src/mbgl/particle/field/field_runtime.cpp @@ -99,16 +99,22 @@ bool DecodedFieldGrid::operator==(const DecodedFieldGrid& other) const { bool DecodedFieldFrame::isStructurallyValid() const { if (!frameContentIdentity.isValid() || referenceTimeMs == 0 || !grid.isValid() || !validityBits || - validityBits->empty() || validityContentIdentity.empty() || planes.empty()) return false; + validityBits->empty() || validityContentIdentity.empty()) return false; const uint64_t cells = static_cast(grid.width) * grid.height; const uint64_t expectedValidityBytes = (cells + 7u) / 8u; if (validityBits->size() != expectedValidityBytes) return false; - if (!validityPlane.isStructurallyValid(cells) || validityPlane.contentIdentity != validityContentIdentity || - validityPlane.storage != validityBits) return false; + if (!validityPlane.isStructurallyValid(cells) || validityPlane.valueClass != FieldValueClass::Validity || + validityPlane.dtype != FieldDType::Bitset || validityPlane.unitID != FieldUnitNone || + validityPlane.key.groupID != 0 || validityPlane.key.component != FieldComponent::None || + !validityPlane.requiredFlag || validityPlane.contentIdentity != validityContentIdentity || + validityPlane.storage != validityBits) + return false; std::set keys; if (!keys.insert(validityPlane.key).second) return false; for (const auto& plane : planes) { - if (!plane.isStructurallyValid(cells) || !keys.insert(plane.key).second) return false; + if (!plane.isStructurallyValid(cells) || plane.valueClass == FieldValueClass::Validity || + !keys.insert(plane.key).second) + return false; } return true; } diff --git a/test/particle/field_runtime.test.cpp b/test/particle/field_runtime.test.cpp index e9037596e..0533c53b1 100644 --- a/test/particle/field_runtime.test.cpp +++ b/test/particle/field_runtime.test.cpp @@ -71,6 +71,7 @@ TEST(ParticleFieldProfileValidator, AcceptsGenericProfileAndOpaqueScalarUnit) { TEST(ParticleFieldProfileValidator, EnforcesRequiredPlanesAndAdditionalPlanePolicy) { auto c = contract(); auto f = frame(); f.planes.clear(); + EXPECT_TRUE(f.isStructurallyValid()); EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::MissingRequiredPlane); f = frame(); f.planes.front().unitID = 99; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::PlaneDescriptorMismatch); @@ -85,6 +86,42 @@ TEST(ParticleFieldProfileValidator, EnforcesRequiredPlanesAndAdditionalPlanePoli EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::Accepted); } +TEST(ParticleFieldProfileValidator, EnforcesSingleSharedValidityStructuralInvariant) { + auto f = frame(); + const auto numbers = f.planes.front().storage; + + f.planes.push_back(plane(901, particle::FieldValueClass::Validity, particle::FieldUnitNone, 0, + particle::FieldComponent::None, particle::FieldDType::Bitset, false, numbers, "second-validity")); + EXPECT_FALSE(f.isStructurallyValid()); + EXPECT_EQ(validate(contract(), f), particle::FieldProfileValidationCode::InvalidFrame); + + f = frame(); + f.validityPlane.requiredFlag = false; + EXPECT_FALSE(f.isStructurallyValid()); + EXPECT_EQ(validate(contract(), f), particle::FieldProfileValidationCode::InvalidFrame); + + f = frame(); f.validityPlane.valueClass = particle::FieldValueClass::Scalar; + EXPECT_FALSE(f.isStructurallyValid()); + EXPECT_EQ(validate(contract(), f), particle::FieldProfileValidationCode::InvalidFrame); + f = frame(); f.validityPlane.dtype = particle::FieldDType::UInt8; + EXPECT_FALSE(f.isStructurallyValid()); + EXPECT_EQ(validate(contract(), f), particle::FieldProfileValidationCode::InvalidFrame); + f = frame(); f.validityPlane.unitID = particle::FieldUnitMetersPerSecond; + EXPECT_FALSE(f.isStructurallyValid()); + EXPECT_EQ(validate(contract(), f), particle::FieldProfileValidationCode::InvalidFrame); + f = frame(); f.validityPlane.key.groupID = 1; + EXPECT_FALSE(f.isStructurallyValid()); + EXPECT_EQ(validate(contract(), f), particle::FieldProfileValidationCode::InvalidFrame); + f = frame(); f.validityPlane.key.component = particle::FieldComponent::UEast; + EXPECT_FALSE(f.isStructurallyValid()); + EXPECT_EQ(validate(contract(), f), particle::FieldProfileValidationCode::InvalidFrame); + + f = frame(); + f.validityPlane.storage = std::make_shared>(*f.validityBits); + EXPECT_FALSE(f.isStructurallyValid()); + EXPECT_EQ(validate(contract(), f), particle::FieldProfileValidationCode::InvalidFrame); +} + TEST(ParticleFieldProfileValidator, EnforcesVelocityDirectionAndValidityCoreRules) { EXPECT_EQ(particle::FieldUnitNone, 0); EXPECT_EQ(particle::FieldUnitMetersPerSecond, 1); @@ -121,11 +158,11 @@ TEST(ParticleFieldProfileValidator, EnforcesVelocityDirectionAndValidityCoreRule c.requiredPlanes.front().unitID = 2; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::SemanticContractMismatch); c = contract(); f = frame(); f.validityPlane.dtype = particle::FieldDType::UInt8; - EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::InvalidCoreUnit); + EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::InvalidFrame); f = frame(); f.validityPlane.unitID = 1; - EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::InvalidCoreUnit); + EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::InvalidFrame); f = frame(); f.validityPlane.valueClass = particle::FieldValueClass::Scalar; - EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::InvalidCoreUnit); + EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::InvalidFrame); } TEST(ParticleFieldProfileValidator, EnforcesReservedCoreSemanticContractsFromProtocolTable) { @@ -149,7 +186,7 @@ TEST(ParticleFieldProfileValidator, EnforcesReservedCoreSemanticContractsFromPro c.requiredPlanes.back().valueClass = particle::FieldValueClass::Scalar; f.validityPlane.key.semanticID = 3; f.validityPlane.valueClass = particle::FieldValueClass::Scalar; - EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::SemanticContractMismatch); + EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::InvalidFrame); } TEST(ParticleFieldProfileValidator, EnforcesDirectionCircularQuantizationContract) { @@ -264,6 +301,7 @@ TEST(ParticleFieldRuntime, ForecastClockPreservesHalfMillisecondRemainder) { ASSERT_TRUE(clock.setRequestedForecastTime(3'000)); ASSERT_TRUE(clock.setPlaybackSpeed(1.0f)); + clock.play(); EXPECT_TRUE(clock.advance(3)); EXPECT_EQ(clock.requestedForecastTime(), 3'003); ASSERT_TRUE(clock.setPlaybackSpeed(2.0f)); diff --git a/粒子开发记录.md b/粒子开发记录.md index 272bd9c31..bb0017295 100644 --- a/粒子开发记录.md +++ b/粒子开发记录.md @@ -704,3 +704,9 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2` - Generic Validator 新增静态数据表驱动的 Native Core semantic contract:`1/2` 固定 U/V velocity、`3` 固定 shared validity、`10/11` 固定 Direction DEGREE/NONE/group0;Contract 与 Frame 的冲突均结构化拒绝为 `SemanticContractMismatch`,不依赖任何 profile 或产品名称。 - `DIRECTION_ANGLE` 通用 Circular Quantization 现强制 `DEGREE=4`、NONE、group0、UINT16、offset=0、`scale=360/65536`(极小 float32 tolerance);opaque unknown Scalar unit 保持可扩展。 - Linux OpenGL `mbgl-test`、Validator/Runtime 过滤测试和 V3 Field/Timeline 过滤回归均 PASS,`git diff --check` PASS;未执行 Android Gradle/APK/ADB/真机/Jest/iOS-Metal,未进入 QBIN Decoder、Binding/Projection、Renderer、JNI、Android 或 RN。 + +### 2026-09-13 STEP-073 v4 最终关闭修正 + +- `DecodedFieldFrame` 固化 single shared validity domain:designated validity 必须为 `Validity/Bitset/NONE/group0/None/REQUIRED`,与 `validityBits` 同一共享引用;普通 `planes[]` 禁止第二张 Validity。空 numeric `planes[]` 仍结构有效,缺 Profile numeric requirement 正确返回 `MissingRequiredPlane`。 +- Validator 在 required descriptor 匹配前检查 actual reserved Core semantic;semantic 1 Scalar 伪装、semantic 10 的 component/group 冲突优先为 `SemanticContractMismatch`。unknown Scalar opaque 与 Direction circular quantization 规则保持。 +- ForecastClock 测试在 pause 后显式恢复 `play()`,生产 pause 语义未改。实际重建并重链 `mbgl-test-runner` 后,V4 filter 11/11 PASS、V3 定向回归 11/11 PASS、`git diff --check` PASS;未进入 Decoder/Renderer/JNI/Android/RN,停止等待独立复验。