diff --git a/docs/50_Step记录/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_v3.md b/docs/50_Step记录/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_v3.md new file mode 100644 index 000000000..cd4f287cc --- /dev/null +++ b/docs/50_Step记录/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_v3.md @@ -0,0 +1,47 @@ +# STEP-075 — QBIN2 V4 Binding 与 Projection Identity v3 + +状态:完成 + +日期:2026-09-13 + +## 目标与基线 + +- 基于 `653e6f2f1598b7817b7912a39877690f248d6f25`,仅关闭 inactive selector 污染 `ParticleBinding` identity 的审计问题。 +- 不进入 STEP-076;测试仅使用离线 C++ fixture。 + +## 必做项 + +- [x] `ParticleBinding::operator==` 改为 mode-aware:VectorComponents 只比较 U/V selector;DirectionAngle 只比较 direction selector;两者均继续比较 mode、fieldKey 与 expectedUnitID。 +- [x] selector identity 继续仅为 `(semanticID, groupID)`;component 保持为 resolved Plane descriptor assertion,不进入 identity。 +- [x] VectorComponents 覆盖:仅变更 inactive `directionPlane` 时 binding equality 与 resolved `ParticleProjectionContentIdentity` 均相同;变更 active U selector 则 binding 不同且 `PlaneMissing` fail-closed。 +- [x] DirectionAngle 覆盖:仅变更 inactive U/V selectors 时 binding equality 与 resolved identity 均相同;变更 active direction selector 则 binding 不同且 `PlaneMissing` fail-closed。 + +## 允许修改 + +- `include/mbgl/particle/field/field_binding.hpp` +- `src/mbgl/particle/field/field_binding.cpp` +- `test/particle/field_binding.test.cpp` +- 本 Step、完成报告 v3 与 `粒子开发记录.md` + +## 禁止修改 + +- 未修改 Decoder、Runtime、Renderer、Host/JNI/RN/Android,未进入 STEP-076。 +- 未执行 Android Gradle/APK/ADB/Jest、iOS/Metal,未使用真实服务器 QBIN。 + +## 验证与当前结果 + +- Linux OpenGL:`cmake --build build-linux-opengl --target mbgl-test-runner -j3`,真实重建 PASS(全局 build-output-throttle)。 +- `ParticleFieldBinding.*`:6/6,0 failure。 +- `ParticleFieldRuntime.*:ParticleFieldProfileValidator.*`:11/11,0 failure。 +- `ParticleQbinV2Decoder.*:ParticleEncodedFieldIngress.*`:24/24,0 failure。 +- `Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*`:11/11,0 failure。 +- `git diff --check` PASS。 + +## Git + +- scoped commit、push result、remote branch 与 Final HEAD 见完成报告 v3 的 Git 记录。 + +## 阻塞与下一步 + +- 无实现阻塞。 +- STEP-075 v3 已关闭;按任务书停止,不进入 STEP-076。 diff --git a/docs/报告/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_Codex完成报告_v3.md b/docs/报告/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_Codex完成报告_v3.md new file mode 100644 index 000000000..c03976438 --- /dev/null +++ b/docs/报告/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_Codex完成报告_v3.md @@ -0,0 +1,25 @@ +# STEP-075 QBIN2 V4 Binding 与 Projection Identity Codex 完成报告 v3 + +日期:2026-09-13 + +## 已完成 + +- `ParticleBinding` equality 现严格按 mode 比较实际消费的 selector,inactive selector 不再污染 Projection content identity。 +- VectorComponents 的 inactive `directionPlane` 与 DirectionAngle 的 inactive U/V selector 都已由离线单元测试证明不会改变 binding equality 或 resolved `ParticleProjectionContentIdentity`。 +- 两种 mode 的 active selector 变更仍使 binding 不同,并经缺失 selector 的 `PlaneMissing` 路径 fail-closed。 +- selector identity 仍限定为 `(semanticID, groupID)`,component 仍只参与 selected-plane descriptor assertion。 + +## 验证结果 + +- Linux OpenGL `mbgl-test-runner` 真实重建 PASS,且所有命令经全局 build-output-throttle 执行。 +- Binding 6/6、Runtime/Profile 11/11、QBIN/Ingress 24/24、Source 11/11;每组均为 0 failure。 +- `git diff --check` PASS。 + +## 未完成或未验证 + +- Android Gradle/APK/ADB/Jest、iOS/Metal、Renderer/GPU、Host/JNI/RN、网络及真实 QBIN 均按任务禁止或不在范围内,未执行,不能标记 PASS。 +- 无实现阻塞;本任务不进入 STEP-076。 + +## Git + +- scoped commit、push result、remote branch 与 Final HEAD 将在 push 完成后追加到本报告。 diff --git a/include/mbgl/particle/field/field_binding.hpp b/include/mbgl/particle/field/field_binding.hpp index 260a51c83..3fc3a6e45 100644 --- a/include/mbgl/particle/field/field_binding.hpp +++ b/include/mbgl/particle/field/field_binding.hpp @@ -29,6 +29,8 @@ struct ParticleBinding { FieldPlaneKey directionPlane; uint16_t expectedUnitID = FieldUnitNone; + // Equality includes only the selectors consumed by mode; selector identity + // remains semanticID/groupID, while component is a resolved-plane assertion. bool operator==(const ParticleBinding&) const; }; diff --git a/src/mbgl/particle/field/field_binding.cpp b/src/mbgl/particle/field/field_binding.cpp index d7dcef2f0..50da3a359 100644 --- a/src/mbgl/particle/field/field_binding.cpp +++ b/src/mbgl/particle/field/field_binding.cpp @@ -66,9 +66,11 @@ bool ColorBinding::operator==(const ColorBinding& other) const { expectedValueClass == other.expectedValueClass && expectedUnitID == other.expectedUnitID; } bool ParticleBinding::operator==(const ParticleBinding& other) const { - return mode == other.mode && fieldKey == other.fieldKey && sameSelector(uPlane, other.uPlane) && - sameSelector(vPlane, other.vPlane) && sameSelector(directionPlane, other.directionPlane) && - expectedUnitID == other.expectedUnitID; + if (mode != other.mode || fieldKey != other.fieldKey || expectedUnitID != other.expectedUnitID) return false; + if (mode == ParticleBindingMode::VectorComponents) + return sameSelector(uPlane, other.uPlane) && sameSelector(vPlane, other.vPlane); + if (mode == ParticleBindingMode::DirectionAngle) return sameSelector(directionPlane, other.directionPlane); + return false; } bool ProjectionFrameCoordinate::operator==(const ProjectionFrameCoordinate& other) const { return fieldID == other.fieldID && validTimeMs == other.validTimeMs; diff --git a/test/particle/field_binding.test.cpp b/test/particle/field_binding.test.cpp index bcc69c303..62a34443b 100644 --- a/test/particle/field_binding.test.cpp +++ b/test/particle/field_binding.test.cpp @@ -148,6 +148,63 @@ TEST(ParticleFieldBinding, ProjectionIdentityIsolatedBySelectedRevision) { EXPECT_NE(afterVectorParticle.projection->contentIdentity, afterValidityParticle.projection->contentIdentity); } +TEST(ParticleFieldBinding, VectorComponentsIgnoresInactiveDirectionSelectorInIdentity) { + particle::EncodedFieldStore store; + submit(store, "weather.wind", makeFrame("wind", Base, 1, "0")); + submit(store, "weather.wind", makeFrame("wind", Base + Hour, 1, "1")); + + const auto first = wind(); + auto inactiveDirectionChanged = first; + inactiveDirectionChanged.directionPlane = {99, 4, particle::FieldComponent::WUp}; + EXPECT_EQ(first, inactiveDirectionChanged); + const auto firstProjection = particle::BindingProjectionResolver::resolve(store, first, Base); + const auto inactiveDirectionProjection = + particle::BindingProjectionResolver::resolve(store, inactiveDirectionChanged, Base); + ASSERT_TRUE(firstProjection.accepted()); + ASSERT_TRUE(inactiveDirectionProjection.accepted()); + EXPECT_EQ(firstProjection.projection->contentIdentity, inactiveDirectionProjection.projection->contentIdentity); + + auto activeUChanged = first; + activeUChanged.uPlane.semanticID = 99; + EXPECT_NE(first, activeUChanged); + EXPECT_EQ(particle::BindingProjectionResolver::resolve(store, activeUChanged, Base).error, + particle::BindingProjectionError::PlaneMissing); +} + +TEST(ParticleFieldBinding, DirectionAngleIgnoresInactiveVectorSelectorsInIdentity) { + particle::EncodedFieldStore store; + auto d0 = *makeFrame("direction", Base, 1, "d0", false, false); + auto d1 = *makeFrame("direction", Base + Hour, 1, "d1", false, false); + const auto directionBytes = std::make_shared>(std::vector{1, 2, 3, 4}); + for (auto* frame : {&d0, &d1}) { + frame->planes.push_back(makePlane(10, 0, particle::FieldValueClass::DirectionAngle, particle::FieldComponent::None, + particle::FieldUnitDegrees, "direction", directionBytes)); + } + submit(store, "weather.direction", std::make_shared(std::move(d0))); + submit(store, "weather.direction", std::make_shared(std::move(d1))); + + particle::ParticleBinding first; + first.mode = particle::ParticleBindingMode::DirectionAngle; + first.fieldKey = "weather.direction"; + first.directionPlane = {10, 0, particle::FieldComponent::None}; + first.expectedUnitID = particle::FieldUnitDegrees; + auto inactiveVectorsChanged = first; + inactiveVectorsChanged.uPlane = {1, 7, particle::FieldComponent::UEast}; + inactiveVectorsChanged.vPlane = {2, 7, particle::FieldComponent::VNorth}; + EXPECT_EQ(first, inactiveVectorsChanged); + const auto firstProjection = particle::BindingProjectionResolver::resolve(store, first, Base); + const auto inactiveVectorsProjection = particle::BindingProjectionResolver::resolve(store, inactiveVectorsChanged, Base); + ASSERT_TRUE(firstProjection.accepted()); + ASSERT_TRUE(inactiveVectorsProjection.accepted()); + EXPECT_EQ(firstProjection.projection->contentIdentity, inactiveVectorsProjection.projection->contentIdentity); + + auto activeDirectionChanged = first; + activeDirectionChanged.directionPlane.semanticID = 99; + EXPECT_NE(first, activeDirectionChanged); + EXPECT_EQ(particle::BindingProjectionResolver::resolve(store, activeDirectionChanged, Base).error, + particle::BindingProjectionError::PlaneMissing); +} + TEST(ParticleFieldBinding, FailsClosedAndSupportsDirectionAngle) { particle::EncodedFieldStore store; submit(store, "weather.wind", makeFrame("wind", Base, 1, "0")); diff --git a/粒子开发记录.md b/粒子开发记录.md index 353b74660..7c9bccf93 100644 --- a/粒子开发记录.md +++ b/粒子开发记录.md @@ -747,3 +747,9 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2` - Store public-path `ReplacedRevision` 对称覆盖:Scalar-only 仅改变 Color、U/V-only 仅改变 Particle、validity-only 同时改变两者;MIX 仍不进入 identity,in-window storage 仍稳定。 - Resolver 删除不可达 `InterpolationIncompatible`;相邻 frame compatibility 继续由既有 `FieldSeries::submit` fail-closed 保证。真实 Linux runner rebuild、规定过滤 50/50、`git diff --check` 全部 PASS;未执行 Android Gradle/APK/ADB/Jest,按范围关闭并停止,不进入 STEP-076。 - scoped commit `90d05f596a42c157502f9a2a5f20425c999cebab`(`fix: close STEP-075 projection identity audit`)已成功 push 至 `origin/particle/main`;STEP-062 dirty 和既有 untracked 均未暂存或提交。 + +### 2026-09-13 STEP-075 v3 Binding/Projection Identity 最终审计收口 + +- `ParticleBinding::operator==` 收敛为 mode-aware active-selector identity:VectorComponents 仅比较 U/V,DirectionAngle 仅比较 direction;共同仍比较 mode、fieldKey、expectedUnitID,selector 保持 `(semanticID, groupID)`,component 继续只作 resolved descriptor assertion。 +- 新增两组离线回归:Vector 的 inactive direction 与 Direction 的 inactive U/V 都不改变 binding/resolved projection identity;各自 active selector 变更仍 binding 不同并 `PlaneMissing` fail-closed。 +- Linux OpenGL runner 真实重建 PASS;Binding 6/6、Runtime/Profile 11/11、QBIN/Ingress 24/24、Source 11/11,均 0 failure;`git diff --check` PASS。Android Gradle/APK/ADB/Jest、iOS/Metal 未执行;STEP-062 dirty 与历史 untracked/build 输出未纳入。