diff --git a/docs/50_Step记录/STEP-076_QBIN2_V4_CoreHostAPI与RasterArraySource接线_v2.md b/docs/50_Step记录/STEP-076_QBIN2_V4_CoreHostAPI与RasterArraySource接线_v2.md new file mode 100644 index 000000000..2b8defefb --- /dev/null +++ b/docs/50_Step记录/STEP-076_QBIN2_V4_CoreHostAPI与RasterArraySource接线_v2.md @@ -0,0 +1,39 @@ +# STEP-076 — QBIN2 V4 Core Host API 与 RasterArraySource 接线 v2 + +状态:完成 + +日期:2026-09-13 + +## 目标与基线 + +- 开始 HEAD:`ae03d8bfd2813eb5e65074db16018e37c7cff1c4`。 +- 任务开始前检测到 STEP-062 两处已有 dirty,以及历史 untracked/build 输出;均未修改、暂存、提交或清理。 +- v1 production 接线保持不变;本 v2 只补 V4 Host API public-path 测试证明与任务文档。 + +## 完成项 + +- [x] 针对 `RejectedDecode`、`RejectedContract`、`RejectedInvalidFieldKey`、`RejectedStaleRevision`,先由公开 `RasterArraySource` API 建立可 resolve 的 V4 Particle projection;每次 reject 后重新 resolve,并验证 content identity、T0/T1 coordinate、U/V selected-plane storage 与 shared validity storage 仍为同一内容身份。 +- [x] 保留 v1 三个 Source V4 测试,并新增 `RasterArrayV4HostApiRejectsKeepPublicProjectionStable`;该 suite 已为 4 个测试。 +- [x] clear 测试先通过公开 V3 `submitInitialRuntimeFieldWindow(...)` 和既有离线 fixture 建立 V3 `field/nextField`。`clearEncodedField`、`clearAllEncodedFields` 后均核验 V3 双端 identity/generation/validTime 不变;V4 resolve 按 fail-closed 返回,重新提交 V4 后既有 binding intent 再次 resolve。 +- [x] 未调用 `clearHostFields()`;原子性证明未访问 V4 `encodedFieldStore` 私有成员或私有 helper。 + +## 修改范围 + +- `test/source/raster_array_v4_host_api.test.cpp` +- 本 Step、完成报告与 `粒子开发记录.md` +- 未修改 production API、Renderer/shader、JNI、Android/RN/Test Host、QBIN decoder 或 Field Runtime/Binding production 代码。 + +## 验证 + +- [x] 真实执行 `cmake --build build-linux-opengl --target mbgl-test-runner -j3`,通过全局 `build-output-throttle`,PASS。 +- [x] `Source.RasterArrayV4HostApi*`:Running 4,0 failure。 +- [x] `ParticleFieldBinding.*`:Running 6,0 failure。 +- [x] `ParticleFieldRuntime.*:ParticleFieldProfileValidator.*`:Running 11,0 failure。 +- [x] `ParticleQbinV2Decoder.*:ParticleEncodedFieldIngress.*`:Running 24,0 failure。 +- [x] `Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*`:Running 11,0 failure。 +- [x] `git diff --check`:PASS。 + +## 边界、阻塞与停止条件 + +- 未执行 Android 编译、Gradle/APK、ADB、实体机、Jest 或 iOS/Metal。 +- 无阻塞;仅完成本任务书的审计收口,停止,不进入下一 Step。 diff --git a/docs/报告/STEP-076_QBIN2_V4_CoreHostAPI与RasterArraySource接线_Codex完成报告_v2.md b/docs/报告/STEP-076_QBIN2_V4_CoreHostAPI与RasterArraySource接线_Codex完成报告_v2.md new file mode 100644 index 000000000..eb2297e02 --- /dev/null +++ b/docs/报告/STEP-076_QBIN2_V4_CoreHostAPI与RasterArraySource接线_Codex完成报告_v2.md @@ -0,0 +1,35 @@ +# STEP-076 QBIN2 V4 Core Host API 与 RasterArraySource 接线 Codex 完成报告 v2 + +## 结果 + +已完成 v1 独立审计指出的测试证明缺口,未改变任何 production API 或架构。四种指定 V4 reject 都在公共 `RasterArraySource` resolve 路径上证明无 partial mutation;V4 clear 已在真实可观察的 V3 初始窗口存在时,证明不影响 V3 `field/nextField`,且 V4 binding intent 可在重新提交后继续 resolve。 + +## 基线与修改 + +- 开始 HEAD:`ae03d8bfd2813eb5e65074db16018e37c7cff1c4`。 +- 实际 production 修改:无。 +- 实际修改文件:`test/source/raster_array_v4_host_api.test.cpp`、`docs/50_Step记录/STEP-076_QBIN2_V4_CoreHostAPI与RasterArraySource接线_v2.md`、`docs/报告/STEP-076_QBIN2_V4_CoreHostAPI与RasterArraySource接线_Codex完成报告_v2.md`、`粒子开发记录.md`。 +- 开始时已有 STEP-062 dirty、历史 untracked 与 build 输出均保持原样;本次 scoped commit 不包含它们。 + +## 测试证明 + +- `RejectedDecode`、`RejectedContract`、`RejectedInvalidFieldKey`、`RejectedStaleRevision`:每次 reject 前后 public Particle projection 均可 resolve;比较了 projection content identity、T0/T1 coordinate、两条 selected plane storage 和 validity storage identity。 +- `clearEncodedField(...)` 与 `clearAllEncodedFields()`:先用公开 V3 `submitInitialRuntimeFieldWindow(...)` 建立窗口,clear 后 V3 field/nextField 均存在,identity/generation/validTime 不变;V4 resolve 均为 fail-closed;重新提交 V4 后 Color/Particle binding 都可 resolve。 +- V4 suite 保留原 3 个并新增第 4 个测试;未使用旧 `clearHostFields()`,未读取 V4 Store 私有成员。 + +| Filter | 实际 Running | Failure | +| --- | ---: | ---: | +| `Source.RasterArrayV4HostApi*` | 4 | 0 | +| `ParticleFieldBinding.*` | 6 | 0 | +| `ParticleFieldRuntime.*:ParticleFieldProfileValidator.*` | 11 | 0 | +| `ParticleQbinV2Decoder.*:ParticleEncodedFieldIngress.*` | 24 | 0 | +| `Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*` | 11 | 0 | + +真实执行 `cmake --build build-linux-opengl --target mbgl-test-runner -j3` 并 PASS;所有 noisy build/test 均经全局 `build-output-throttle`。`git diff --check` PASS。 + +## 范围与 Git 收尾 + +- 未执行 Android 编译/Gradle/APK/ADB/实体机,也未执行 Jest 或 iOS/Metal。 +- 未修改 Renderer、shader、JNI、Android、RN、Test Host、NavSea、QBIN decoder 或 production Field Runtime/Binding。 +- scoped commit message:`test: close STEP-076 V4 host API audit`;push target:`origin/particle/main`。实际 commit hash、push result、remote branch 和 Final HEAD 以紧随本报告的 scoped Git 收尾输出为准。 +- 未完成/阻塞:无;完成后停止,不进入下一 Step。 diff --git a/test/source/raster_array_v4_host_api.test.cpp b/test/source/raster_array_v4_host_api.test.cpp index 4eaab0cae..95cb8112f 100644 --- a/test/source/raster_array_v4_host_api.test.cpp +++ b/test/source/raster_array_v4_host_api.test.cpp @@ -1,5 +1,7 @@ #include +#include #include +#include #include @@ -101,30 +103,89 @@ void submit(style::RasterArraySource& source, const char* key, const char* id, u style::RasterArrayEncodedFieldSubmitResult::AcceptedInserted); } +void expectSameParticleProjection(const particle::ParticleProjection& expected, + const particle::ParticleProjection& actual) { + EXPECT_EQ(actual.contentIdentity, expected.contentIdentity); + EXPECT_EQ(actual.contentIdentity.t0Coordinate, expected.contentIdentity.t0Coordinate); + EXPECT_EQ(actual.contentIdentity.t1Coordinate, expected.contentIdentity.t1Coordinate); + EXPECT_EQ(actual.t0FirstPlaneStorage, expected.t0FirstPlaneStorage); + EXPECT_EQ(actual.t1FirstPlaneStorage, expected.t1FirstPlaneStorage); + EXPECT_EQ(actual.t0SecondPlaneStorage, expected.t0SecondPlaneStorage); + EXPECT_EQ(actual.t1SecondPlaneStorage, expected.t1SecondPlaneStorage); + EXPECT_EQ(actual.t0ValidityStorage, expected.t0ValidityStorage); + EXPECT_EQ(actual.t1ValidityStorage, expected.t1ValidityStorage); +} + } // namespace TEST(Source, RasterArrayV4HostApiRejectsWithoutPartialMutationAndMapsStoreResults) { - style::RasterArraySource source{"v4"}; const auto first = qbin(Base); + style::RasterArraySource source{"v4"}; + const auto first = qbin(Base); EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 1), windContract(), first), style::RasterArrayEncodedFieldSubmitResult::AcceptedInserted); EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 1), windContract(), first), style::RasterArrayEncodedFieldSubmitResult::Duplicate); EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 2, 2), windContract(), first), style::RasterArrayEncodedFieldSubmitResult::AcceptedReplacedRevision); - EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 1), windContract(), first), - style::RasterArrayEncodedFieldSubmitResult::RejectedStaleRevision); - auto corrupt = first; corrupt[0] = 'X'; - EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 3), windContract(), corrupt), - style::RasterArrayEncodedFieldSubmitResult::RejectedDecode); - EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 3), waveContract(), first), - style::RasterArrayEncodedFieldSubmitResult::RejectedContract); - EXPECT_EQ(source.submitEncodedField("", metadata("wind", 3), windContract(), first), - style::RasterArrayEncodedFieldSubmitResult::RejectedInvalidFieldKey); submit(source, "wind", "wind", Base + Hour, 3); source.setParticleBinding({particle::ParticleBindingMode::VectorComponents, "wind", {1, 1, particle::FieldComponent::UEast}, {2, 1, particle::FieldComponent::VNorth}, {}, 1}); ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour / 2)); - EXPECT_TRUE(source.resolveParticleProjection().accepted()); + const auto baseline = source.resolveParticleProjection(); + ASSERT_TRUE(baseline.accepted()); + + const auto expectNoPartialMutation = [&] { + const auto resolved = source.resolveParticleProjection(); + ASSERT_TRUE(resolved.accepted()); + expectSameParticleProjection(*baseline.projection, *resolved.projection); + }; + + EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 1), windContract(), first), + style::RasterArrayEncodedFieldSubmitResult::RejectedStaleRevision); + expectNoPartialMutation(); + auto corrupt = first; corrupt[0] = 'X'; + EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 3), windContract(), corrupt), + style::RasterArrayEncodedFieldSubmitResult::RejectedDecode); + expectNoPartialMutation(); + EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 3), waveContract(), first), + style::RasterArrayEncodedFieldSubmitResult::RejectedContract); + expectNoPartialMutation(); + EXPECT_EQ(source.submitEncodedField("", metadata("wind", 3), windContract(), first), + style::RasterArrayEncodedFieldSubmitResult::RejectedInvalidFieldKey); + expectNoPartialMutation(); +} + +TEST(Source, RasterArrayV4HostApiRejectsKeepPublicProjectionStable) { + style::RasterArraySource source{"v4"}; + const auto t0 = qbin(Base); + const auto t1 = qbin(Base + Hour); + submit(source, "wind", "wind", Base, 2); + submit(source, "wind", "wind", Base + Hour, 3); + source.setParticleBinding({particle::ParticleBindingMode::VectorComponents, "wind", + {1, 1, particle::FieldComponent::UEast}, {2, 1, particle::FieldComponent::VNorth}, {}, 1}); + ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour / 2)); + const auto baseline = source.resolveParticleProjection(); + ASSERT_TRUE(baseline.accepted()); + const auto expectNoPartialMutation = [&] { + const auto resolved = source.resolveParticleProjection(); + ASSERT_TRUE(resolved.accepted()); + expectSameParticleProjection(*baseline.projection, *resolved.projection); + }; + + auto corrupt = t1; + corrupt[0] = 'X'; + EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 4), windContract(), corrupt), + style::RasterArrayEncodedFieldSubmitResult::RejectedDecode); + expectNoPartialMutation(); + EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 4), waveContract(), t1), + style::RasterArrayEncodedFieldSubmitResult::RejectedContract); + expectNoPartialMutation(); + EXPECT_EQ(source.submitEncodedField("", metadata("wind", 4), windContract(), t1), + style::RasterArrayEncodedFieldSubmitResult::RejectedInvalidFieldKey); + expectNoPartialMutation(); + EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 1), windContract(), t0), + style::RasterArrayEncodedFieldSubmitResult::RejectedStaleRevision); + expectNoPartialMutation(); } TEST(Source, RasterArrayV4HostApiResolvesIndependentBindingsFromOneForecastClock) { @@ -154,6 +215,28 @@ TEST(Source, RasterArrayV4HostApiResolvesIndependentBindingsFromOneForecastClock TEST(Source, RasterArrayV4HostApiClearRetainsBindingsAndNeverTouchesV3) { style::RasterArraySource source{"v4"}; + const auto v3T0 = particle::OfflineFixtureParticleAdapter::load("uniform-east", 71); + auto v3T1 = particle::OfflineFixtureParticleAdapter::load("uniform-north", 72); + v3T1.validTime = v3T0.validTime + Hour; + ASSERT_EQ(source.submitInitialRuntimeFieldWindow(v3T0, v3T1), style::RasterArrayRuntimeFieldUpdateResult::Applied); + ASSERT_TRUE(source.impl().field); + ASSERT_TRUE(source.impl().nextField); + const auto v3FieldIdentity = source.impl().field->fieldID; + const auto v3FieldGeneration = source.impl().field->generation; + const auto v3FieldValidTime = source.impl().field->validTime; + const auto v3NextIdentity = source.impl().nextField->fieldID; + const auto v3NextGeneration = source.impl().nextField->generation; + const auto v3NextValidTime = source.impl().nextField->validTime; + const auto expectV3Unchanged = [&] { + ASSERT_TRUE(source.impl().field); + ASSERT_TRUE(source.impl().nextField); + EXPECT_EQ(source.impl().field->fieldID, v3FieldIdentity); + EXPECT_EQ(source.impl().field->generation, v3FieldGeneration); + EXPECT_EQ(source.impl().field->validTime, v3FieldValidTime); + EXPECT_EQ(source.impl().nextField->fieldID, v3NextIdentity); + EXPECT_EQ(source.impl().nextField->generation, v3NextGeneration); + EXPECT_EQ(source.impl().nextField->validTime, v3NextValidTime); + }; submit(source, "wind", "wind", Base, 1); submit(source, "wind", "wind", Base + Hour, 2); submit(source, "wave", "wave", Base, 1, true); submit(source, "wave", "wave", Base + 3 * Hour, 2, true); source.setColorBinding({"wave", {99, 0, particle::FieldComponent::None}, particle::FieldValueClass::Scalar, 77}); @@ -162,8 +245,15 @@ TEST(Source, RasterArrayV4HostApiClearRetainsBindingsAndNeverTouchesV3) { ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour)); ASSERT_TRUE(source.resolveColorProjection().accepted()); EXPECT_TRUE(source.clearEncodedField("wave")); EXPECT_TRUE(source.resolveParticleProjection().accepted()); EXPECT_EQ(source.resolveColorProjection().error, particle::BindingProjectionError::FieldSeriesMissing); + expectV3Unchanged(); submit(source, "wave", "wave", Base, 1, true); submit(source, "wave", "wave", Base + 3 * Hour, 2, true); EXPECT_TRUE(source.resolveColorProjection().accepted()); source.clearAllEncodedFields(); EXPECT_EQ(source.resolveColorProjection().error, particle::BindingProjectionError::FieldSeriesMissing); EXPECT_EQ(source.resolveParticleProjection().error, particle::BindingProjectionError::FieldSeriesMissing); + expectV3Unchanged(); + submit(source, "wind", "wind", Base, 1); submit(source, "wind", "wind", Base + Hour, 2); + submit(source, "wave", "wave", Base, 1, true); submit(source, "wave", "wave", Base + 3 * Hour, 2, true); + EXPECT_TRUE(source.resolveColorProjection().accepted()); + EXPECT_TRUE(source.resolveParticleProjection().accepted()); + expectV3Unchanged(); } diff --git a/粒子开发记录.md b/粒子开发记录.md index e2adb8c85..afa1cb532 100644 --- a/粒子开发记录.md +++ b/粒子开发记录.md @@ -760,3 +760,9 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2` - `RasterArraySource` 新增完全隔离的 V4 QBIN Host API:submission 严格执行 key/runtime metadata → QBIN decoder → Profile validator → per-field Store,稳定返回 insert/replace/duplicate 与各类 fail-closed rejection;失败不留下 partial Store mutation。 - V4 owner 独立持有 `EncodedFieldStore`、`ForecastClock`、Color/Particle binding;每个 binding 在 resolve 时按自己的 fieldKey 获得 window/mix。没有 Host T0/T1/MIX API,没有切换 renderer,也未改动 V3 timeline。 - 离线 Source V4 suite 3/3、Binding 6/6、Runtime/Profile 11/11、QBIN/Ingress 24/24、V3 Source 11/11,均为 0 failure;Linux runner 真实重建、`git diff --check` PASS。Android Gradle/APK/ADB/Jest、iOS/Metal 未执行;STEP-062 dirty 与既有 untracked/build 输出未纳入。 + +### 2026-09-13 STEP-076 v2 审计测试收口 + +- V4 public-path reject 现在逐项证明 `RejectedDecode`、`RejectedContract`、`RejectedInvalidFieldKey`、`RejectedStaleRevision` 不改变既有可 resolve projection 的 content identity、T0/T1 coordinate、selected U/V plane 与 validity storage identity;未读取 Store 私有状态。 +- clear 测试先用公开 `submitInitialRuntimeFieldWindow(...)` 建立 V3 `field/nextField`,对 `clearEncodedField`、`clearAllEncodedFields` 和 V4 re-submit 全程核验 V3 identity/generation/validTime 不变;V4 清空后 fail-closed,binding intent 仍可重解。 +- Linux runner 真实重建 PASS;Source V4 4/4、Binding 6/6、Runtime/Profile 11/11、QBIN/Ingress 24/24、Source regression 11/11,全部 0 failure,`git diff --check` PASS。未执行 Android 编译/Gradle/APK/ADB/Jest/iOS-Metal;STEP-062 dirty 与历史 untracked/build 输出未纳入 scoped commit。