fix: close STEP-075 binding selector identity audit
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
OpenAI Codex
2026-09-13 14:21:41 +08:00
parent 653e6f2f15
commit 2645bd725f
6 changed files with 142 additions and 3 deletions

View File

@@ -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。

View File

@@ -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 完成后追加到本报告。

View File

@@ -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;
};

View File

@@ -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;

View File

@@ -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<const std::vector<uint8_t>>(std::vector<uint8_t>{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<const particle::DecodedFieldFrame>(std::move(d0)));
submit(store, "weather.direction", std::make_shared<const particle::DecodedFieldFrame>(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"));

View File

@@ -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 输出未纳入。