fix: close STEP-073 validator runner audit
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
OpenAI Codex
2026-09-13 10:32:45 +08:00
parent 0b66a5cd30
commit b8a629b7b6
6 changed files with 140 additions and 11 deletions

View File

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

View File

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

View File

@@ -99,6 +99,15 @@ FieldProfileValidationResult FieldProfileValidator::validate(const ResolvedField
actual.push_back(&frame.validityPlane); actual.push_back(&frame.validityPlane);
for (const auto& plane : frame.planes) actual.push_back(&plane); 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) { for (const auto& required : contract.requiredPlanes) {
const auto same = std::find_if(actual.begin(), actual.end(), [&required](const PlaneRef plane) { const auto same = std::find_if(actual.begin(), actual.end(), [&required](const PlaneRef plane) {
return sameKey(required, *plane); return sameKey(required, *plane);
@@ -109,9 +118,6 @@ FieldProfileValidationResult FieldProfileValidator::validate(const ResolvedField
} }
for (const auto plane : actual) { 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(), const auto declared = std::find_if(contract.requiredPlanes.begin(), contract.requiredPlanes.end(),
[plane](const FieldPlaneRequirement& requirement) { [plane](const FieldPlaneRequirement& requirement) {
return matches(requirement, *plane); return matches(requirement, *plane);

View File

@@ -99,16 +99,22 @@ bool DecodedFieldGrid::operator==(const DecodedFieldGrid& other) const {
bool DecodedFieldFrame::isStructurallyValid() const { bool DecodedFieldFrame::isStructurallyValid() const {
if (!frameContentIdentity.isValid() || referenceTimeMs == 0 || !grid.isValid() || !validityBits || 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<uint64_t>(grid.width) * grid.height; const uint64_t cells = static_cast<uint64_t>(grid.width) * grid.height;
const uint64_t expectedValidityBytes = (cells + 7u) / 8u; const uint64_t expectedValidityBytes = (cells + 7u) / 8u;
if (validityBits->size() != expectedValidityBytes) return false; if (validityBits->size() != expectedValidityBytes) return false;
if (!validityPlane.isStructurallyValid(cells) || validityPlane.contentIdentity != validityContentIdentity || if (!validityPlane.isStructurallyValid(cells) || validityPlane.valueClass != FieldValueClass::Validity ||
validityPlane.storage != validityBits) return false; 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<FieldPlaneKey> keys; std::set<FieldPlaneKey> keys;
if (!keys.insert(validityPlane.key).second) return false; if (!keys.insert(validityPlane.key).second) return false;
for (const auto& plane : planes) { 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; return true;
} }

View File

@@ -71,6 +71,7 @@ TEST(ParticleFieldProfileValidator, AcceptsGenericProfileAndOpaqueScalarUnit) {
TEST(ParticleFieldProfileValidator, EnforcesRequiredPlanesAndAdditionalPlanePolicy) { TEST(ParticleFieldProfileValidator, EnforcesRequiredPlanesAndAdditionalPlanePolicy) {
auto c = contract(); auto f = frame(); auto c = contract(); auto f = frame();
f.planes.clear(); f.planes.clear();
EXPECT_TRUE(f.isStructurallyValid());
EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::MissingRequiredPlane); EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::MissingRequiredPlane);
f = frame(); f.planes.front().unitID = 99; f = frame(); f.planes.front().unitID = 99;
EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::PlaneDescriptorMismatch); EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::PlaneDescriptorMismatch);
@@ -85,6 +86,42 @@ TEST(ParticleFieldProfileValidator, EnforcesRequiredPlanesAndAdditionalPlanePoli
EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::Accepted); 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<const std::vector<uint8_t>>(*f.validityBits);
EXPECT_FALSE(f.isStructurallyValid());
EXPECT_EQ(validate(contract(), f), particle::FieldProfileValidationCode::InvalidFrame);
}
TEST(ParticleFieldProfileValidator, EnforcesVelocityDirectionAndValidityCoreRules) { TEST(ParticleFieldProfileValidator, EnforcesVelocityDirectionAndValidityCoreRules) {
EXPECT_EQ(particle::FieldUnitNone, 0); EXPECT_EQ(particle::FieldUnitNone, 0);
EXPECT_EQ(particle::FieldUnitMetersPerSecond, 1); EXPECT_EQ(particle::FieldUnitMetersPerSecond, 1);
@@ -121,11 +158,11 @@ TEST(ParticleFieldProfileValidator, EnforcesVelocityDirectionAndValidityCoreRule
c.requiredPlanes.front().unitID = 2; c.requiredPlanes.front().unitID = 2;
EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::SemanticContractMismatch); EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::SemanticContractMismatch);
c = contract(); f = frame(); f.validityPlane.dtype = particle::FieldDType::UInt8; 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; 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; 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) { TEST(ParticleFieldProfileValidator, EnforcesReservedCoreSemanticContractsFromProtocolTable) {
@@ -149,7 +186,7 @@ TEST(ParticleFieldProfileValidator, EnforcesReservedCoreSemanticContractsFromPro
c.requiredPlanes.back().valueClass = particle::FieldValueClass::Scalar; c.requiredPlanes.back().valueClass = particle::FieldValueClass::Scalar;
f.validityPlane.key.semanticID = 3; f.validityPlane.key.semanticID = 3;
f.validityPlane.valueClass = particle::FieldValueClass::Scalar; 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) { TEST(ParticleFieldProfileValidator, EnforcesDirectionCircularQuantizationContract) {
@@ -264,6 +301,7 @@ TEST(ParticleFieldRuntime, ForecastClockPreservesHalfMillisecondRemainder) {
ASSERT_TRUE(clock.setRequestedForecastTime(3'000)); ASSERT_TRUE(clock.setRequestedForecastTime(3'000));
ASSERT_TRUE(clock.setPlaybackSpeed(1.0f)); ASSERT_TRUE(clock.setPlaybackSpeed(1.0f));
clock.play();
EXPECT_TRUE(clock.advance(3)); EXPECT_TRUE(clock.advance(3));
EXPECT_EQ(clock.requestedForecastTime(), 3'003); EXPECT_EQ(clock.requestedForecastTime(), 3'003);
ASSERT_TRUE(clock.setPlaybackSpeed(2.0f)); ASSERT_TRUE(clock.setPlaybackSpeed(2.0f));

View File

@@ -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 或产品名称。 - 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 保持可扩展。 - `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。 - 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,停止等待独立复验。