feat: prepare STEP-114 guarded fast smoke validation
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
tei
2026-10-11 10:01:10 +09:00
parent b02e54de3d
commit 21a1081643
11 changed files with 1032 additions and 6 deletions

View File

@@ -0,0 +1,78 @@
# STEP-114 Fast Path 安全准入与闭环判定 v1
日期:2026-10-11(JST)。结论:**PARTIAL;安全子集源码与独立最小场景已交付,真实 Fast 尚未验收。**
## 1. 基线、证据等级与阅读
Mac / Darwin 24.6.0 / x86_64,`/Users/tei/dev/MapLibreParticleLab`,`particle/main@b02e54de3d45fcf612126ae8f037cba204f590ea`。源码版本 `13.6.1-particle.23`,没有发行或改 Source Lock。实际模型 GPT-6.1-Sol / high 已从当前本地 rollout 的 `turn_context` 核实。
已读根 AGENTS、PARTICLE_BASELINE、项目规范全部八份、项目总则 README 与四份有效总则、本任务、STEP-112 调查/完成报告、STEP-113 完成报告/Step(含两轮 RDC 追加)、实际 Shared Core 和 Test App 源码。Integration 只读 AGENTS、README、WORKFLOW、TEAM_MODEL、C-001/C-002、Contract 版本规则、RFC-001/002/004、ADR-001、COORD-005 决议;不写跨项目文件,不改 RN/JS API/ABI。
SOURCE-PROVEN 表示本轮源码控制流;RETAINED-EVIDENCE 表示本机保留的旧 RDC JSON;IMPLEMENTED 表示已改源码;NOT RUN 表示未编译/运行。旧设备证据不是本轮改动的设备验收。
## 2. R 的真实 SourceType 与初建失败点
不能从 `unknown-dependency` 推断 source 一定是 CustomMVTVector:它还覆盖未知依赖、缺 revision 登记等情况。
本轮只读 `artifacts/maplibre-particle/android-validation/step113/source-change-after-full.json`:Native source 表为 mapple/raster、navsea_delivery/vector、org.maplibre.annotations/annotations、rn-weather-fields/raster-array、step113-source-probe/geojson。R0 `[0,63)` 的依赖为 mapple、navsea_delivery、**tide**;第2层 `tide-spots` 是 `symbol`、sourceId=`tide`,source 表却没有 tide。R0 的 sourceRevision=0、sourceReady=false、Build=0、fallback=unknown-dependency。
源码 `style/conversion/source.cpp::convertVectorSource` 创建 `VectorSource`;其构造函数指定 `SourceType::Vector`;`RenderSource::create` 创建 `RenderVectorSource`。因此该 Test App 的正式 PBF source **不是 CustomMVTVector**。任务书列出的 CustomMVTVector 限制是独立安全约束,本轮完整保留。没有改旧报告,也没有补造 tide 数据、删原正式 PBF 层或修改服务 Style。
`retainedSegmentSourceState` 原先先读取 revisionFor,缺少 tide 的登记会返回 nullopt,从而提前返回默认 unknown-dependency。现在在读取 revision 前发现缺失 RenderSource 时明确返回 `missing-source`;本例的ready/stable=false、revision=0 不变。有旧tombstone记录的缺源也提前返回revision=0,而不暴露旧登记hash;它仍不得Build/Reuse,RevisionBook登记机制不变。这是诊断修正,**不改变缓存准入**。source 存在但无 revision、CustomMVTVector、未知 layer dependency 仍 fail-closed。
后续即使 Host 合法补齐 tide,R 仍包含大量 Symbol,Placement 首建阻塞不会自动消失。缺源与 Placement 是不同失败点。
## 3. 已支持的本地 GeoJSON/circle 子集
FAST_SMOKE 选择本地 GeoJSON 非空点与 circle R,不新增 source 类型授权。其现有追踪链:
| 输入或事件 | 源码链与保护 |
|---|---|
| GeoJSON setGeoJSON | `GeoJSONSource::setGeoJSONData` 替换 immutable Impl 并通知 SourceChanged;Fast non-camera signature 比较 sources;Full `diffSources` 推进全局 content 和本 source revision |
| Source 增删/同ID重建 | `createRenderTree` 的 sourceDiff added/removed/changed 都 advanceSource;RevisionBook 保留 tombstone;PreparedTree 在 Full 更新对象前释放 |
| 异步 GeoJSON 数据/布局 | `RenderGeoJSONSource::update` → GeoJSONTile::updateData;capturedData 和 weakPtr 防陈旧/销毁回调;GeometryTile correlationID 保护 pending;`onLayout` 每次更换 layoutResult 都 onTileChanged;RenderSource → orchestrator 推进 source-local revision 和全局 content,再 invalidate |
| Tile 未就绪/淡入 | `isLoaded`、hasFadingTiles、source assessment、#17 与 R 稳定性检查继续保留 |
| Layer paint/layout/顺序 | immutable layers signature、per-R content fingerprint、resolved plan/topology;有 transition/repaint 仍阻止 |
| Camera/viewport/pixelRatio | #6/#7/#8、RetainedUnderlayKey 与 revision state;旧 texture 不能按新矩阵复用 |
| Style reload | styleLoaded/signature、配置/层序/Source 新身份、PreparedTree 生命周期保护;trace 在实际看到 loaded→false 时重置 |
| Image/Sprite | imageDiff、spriteLoaded、imageManager readiness、missing/unused image callbacks 推进全局 revision;#8 先捕获新 images 输入 |
| Glyph/fontFaces/URL | non-camera signature、glyph URL/font identity 追踪、onGlyphsLoaded 推进全局 revision;不删除资源门禁 |
| Annotation | #24 pending 检查、source/tile/图片变化与层表;实际 Symbol 继续纳入全场结构检查 |
| context/reset/destroy | markContextLost、Renderer.clearData 清 PreparedTree/trace,orchestrator 推进 revision;析构 detach observer 前排空 source/tile 工作;Test App 取消 callback、dispose runtime、删除自有 marker,MapView 原生命周期仍由 Activity 执行 |
这一闭环是现有实现的源码依据,**不是本轮已跑出的 Source mutation/GPU PASS**。异步过渡中 Full 也可能继续展示尚未更新的旧 bucket;要求 Fast 与同一时刻合法 Full 等价,而非承诺提交 API 后同步完成画面更新。成功布局后必须失效并正确重建;不可用两张稳定末态截图代替事件顺序证据。
R source policy、RevisionBook、GPU Build/Reuse 算法本轮零语义修改。新增 C++ 用例验证既有 revision/cache 状态机组合;unit 中显式推进 epoch/标记构建只用于状态机测试,不是实际 Native 帧或 GPU 成功的证据。
## 4. C、Annotation 与 Scene 聚合
`continuousRangeSupportsParticleFastPath` 要求每个 C layer 都 startsRetainedDynamicRange,并至少有粒子动画层。WeatherColor 与 RasterParticle 符合;普通 circle、Symbol、内建 Annotation Symbol 不符合。`particlePreparedSceneSupported = retainedSupported && fastContinuousSupported`,并要求单一 C。#22/#23 可以共享 C 不支持根因。
本轮没有修改 #22/#23、C GPU 复合流程、层序或 AnnotationManager。不能证明非粒子 C 与既有 Fast prepare/postRender 等价,故不放行。
独立 FAST_SMOKE 在确认 `map.annotations.isEmpty()` 后,仅从本场景 Style 删除自动生成且空的 `org.maplibre.annotations.points` layer,保留内建 source;若缺层或 remove 失败则 BLOCKED。Native annotation 更新规则未变,正式 PBF 五场景不删除该层。baseline 层序应为 background、R circle、weather-color、raster-particle,实际 JSON 仍需 RDC 核对。
ANNOTATION_BLOCKED 重新加载一个正常 Style,保留自动内建层并用现有 `map.addMarker` 加真实 marker;可单次修改其 position。内建 Symbol 位于 C,#22/#23 必须继续阻止 Fast。从该场景切回 baseline 时只删除本场景拥有的 marker,不能清除其他 Annotation。若存在其他 Annotation,baseline 拒绝启动。该场景转换含合法 Style reload,不是生产 Annotation 零重载支持的证明。
## 5. Placement:安全局部修复与 BLOCKED
正常 Continuous Full:placementIsRecent=false 则 placeLayers/commit,placementChanged=true;recent=true 则 setPlacementStale,controller.hasTransitions=true;关闭 transitions 时正常 Full changed=true、fade=1。含结构性 Symbol 的 R 无可证明的首次 Build 稳定窗口。
本轮保持 Symbol R 的三项 gate、scene placementStable、全图碰撞、fade/transition 和跨 source 规则。完整 GPU symbol 输入、bucket/atlas/opacity/duplicate/anchor 的结果证书仍不存在;建立它需要独立结果签名或更大生命周期设计。**Symbol R 准入修复 BLOCKED,不能删除 changed/stale/fade 条件。**
唯一行为修复限定于**完整非空层表没有任何结构性 Symbol**:此时 controller 的 stale/transition 没有 Symbol 像素消费者,单独阻止 #13 不代表画面可能变化。`particleFastPathHasPlacementTransition` 按完整 Style 表检查;隐藏/空数据的 Symbol 与 Annotation Symbol 一律保守保护。production 还核对层表数量与 renderLayers 数量,不一致保持原 transition;空表也保守保留。
这是 Shared Core 内部实现优化,无 Android/iOS 分叉,不新增控制 API、不禁止正常 Full Placement、不修改原始 controller。其余23项 Gate、source/cache/scene 条件全部继续生效。新增 Symbol 尚未进入旧表时,immutable layers 的 non-camera signature 不相等,且 Annotation pending 单独阻止 Fast;Full 更新后表重新包含 Symbol。未取得新层表前不得只凭这个 helper 放行。
反例边界:同场只要存在一个隐藏 Symbol、空 Symbol、C Annotation Symbol,仍使用原 controller transition;缺层表、source mutation、未完成 tile layout、image/glyph 更新、camera/viewport、style reload、debug/context 问题都不能被本修复抵消。未来若引入新的 Placement-capable layer 类型,应先扩展结构检测及测试;当前源码仅 Symbol 的 LayerTypeInfo 使用 CrossTileIndex::Required。
## 6. 实施结果、验证方案和限制
- IMPLEMENTED:缺源明确诊断、无 Symbol 场景的 #13 限定检查;3个非 Jest C++ 用例源码;独立4场景及 UI/Intent、真实 Native 快照与操作前后自动采样;中文文档和追加记录。
- 保留 fail-closed:CustomMVTVector、未知 revision、缺源;非粒子 C;所有 Symbol R Build/Placement 保护。
- NOT RUN:Native/Kotlin 编译、C++ 测试、Android build/设备/GPU/Fast 命中、像素对照、性能、iOS Metal 设备验证。
- 保留:正式 STEP-113 五个 PBF 场景实现原样、STEP-102 fixture 原样、STEP-110/111 未跟踪文件原样。新增菜单明确标识独立 smoke,不冒充63/113层等价。
验收步骤和命令见 `docs/60_测试/STEP-114_独立FastSmoke场景与RDC验收_v1.md`;Native 定向命令也直接写在本轮完成报告。baseline 必须取得真实 Native Full>0、Fast 增长、CacheBuild>0、Reuse 增长、completed fast-rendered、24 PASS/knownMask 完整/decisionMismatch=0;否则维持 PARTIAL,不能标 Fast Path 已修复。
Source/Image/Camera/缓存禁用/Reload 的反向证据需保存事件前/提交后/100/500/1500ms 快照,必要时以更早的精确帧复核;未捕获瞬间 gate 不能用计数猜测。后台/恢复/Teardown、Symbol/Annotation 与原 PBF 回归、双平台共享源码独立检查仍为 RDC/用户待执行项。

View File

@@ -0,0 +1,41 @@
# STEP-114:Fast Path 安全准入受控修复与 Test App 闭环(Codex 任务 v1)
## 执行授权与基线
- 用户明确同意继续受控修复,不要求完全复刻 NavSea 113 层。
- Mac 仓库 /Users/tei/dev/MapLibreParticleLab,particle/main 起点 b02e54de3d45fcf612126ae8f037cba204f590ea。Integration 最新 main 80667d6c343411d03a760425438e3d292f452118 经 RDC 验证。
- Codex 固定 GPT-6.1-Sol High (gpt-6.1-sol / high)。先读 AGENTS.md、PARTICLE_BASELINE.md、docs/00_项目规范、docs/00_项目总则/README.md 及四份有效总则、STEP-112/113 报告、Integration 的正式 Contracts/RFC/ADR,严格遵守项目边界。
- 本轮 Codex 只改代码与文档;不能运行 Android Gradle、ADB、Jest、CMake/Ninja/Bazel 大编译或正式打包;Native 定向验证由 RDC 另行执行,Android 编译由用户在授权条件下手工统一验收。
- 不碰原有两份未跟踪 STEP-110/111 文件、无关文件、NavSea 源码和 Integration;禁止 git add -A / reset --hard / clean;不得变更 RN/JS 公共 ABI/API、发行版本或 Source Lock。
## 根因事实
- STEP-113 STATIC_RC 66 Native layers,R[0,63) Build=0、unknown-dependency;C[63,66) 含天气色场、粒子、内部 Annotation Symbol。Fast=0。
- 119 个最新 completed Full 帧:24 Gate shadow mismatch=0;#3 cache invalid、#4 revision mismatch、#22 unsupported continuous layer、#23 unsupported scene 全部失败;#12/#13 placement 105 帧失败。
- RenderOrchestrator::retainedSegmentSourceState 对 CustomMVTVector 明确 fail-closed,已有源码注释说明无 retained cache revision contract;不得删掉这个限制来伪造 Fast。
- continuousRangeSupportsParticleFastPath 要求 C 全层以 Particle dynamic API 标记;内部 Annotation Symbol 不满足,且 #23 聚合依赖部分与 #22 重合。
- STEP-112 证明含结构性 Symbol 的 R 在旧 Placement stale/changed/fade 时可能根本无法进入首次 Cache Build;禁止只删条件或假设视觉没变。
## 实施步骤(依次、受控)
1. 做当前源码静态闭环:明确 Test App 对应 SourceType、Source/Tile/Style/Glyph/Image/Annotation mutation 追踪、R 的初建失败点、C 准入及 Scene 复合条件、Placement 控制流。
2. R 缓存:只对确有完整 Source revision 与异步失效追踪的类型建立安全准入;若 CustomMVTVector 无法严格证明,保留其 fail-closed。优先为已支持的保守 source 子集提供可用的独立 R-cache 正向测试。保留 Source mutation、Tile update、camera、style reload、image/glyph、destroy/reset 的反向用例。
3. C 非粒子层:只有证明原有 GPU 复合内容/Source revision/Placement/Annotation 在静止场景与 Full 等价、变化时 Full 回退,才能受限放行。否则不可移除 Gate #22:可通过单独 Test App 最小场景或安全 Layer 拓扑证明能进入 Fast。不要丢失原始正式 PBF 5 场景。
4. Symbol R Placement:基于已提交的真实 Placement/资源 revision 与 transition/fade 审核是否有安全稳定窗口;如须新增独立结果签名或其他跨平台设计,停止该项并在报告中写 BLOCKED,不做危险简化。
5. 优先实现独立 Test App FAST_SMOKE 场景(如安全可行),按 UI 菜单明确区分正式 PBF 与独立最小场景。使用 Native 真实 Full/Fast/CacheBuild/Reuse 计数和24 Gate JSON,测试稳定、Source mutation、Annotation、Pan/Zoom、Reload、前后台/Teardown;不许伪造 Frame 结果或直接注入 candidate=true。
6. 所有公共 Shared Core 代码仍跨 Android/iOS 共用,不得建立平台分叉。任何需要改公共语义/API的部分暂停,留给 Integration RFC。
## 测试/运行要求
- 可改/新增现有非 Jest C++ 相关测试源,但 Codex 不执行测试/编译。
- 必须写出 RDC 可以执行的精确定向 Mac Native 检查命令;后续 Android 编译和设备 Gate 集中一次进行。
- 只证明能证明的部分;可产出 PARTIAL / BLOCKED 与可用局部修复,不允许“关掉Gate提升Fast数字”。
## 必做输出
1. 本次实际修改的 C++、Android Test App 和非 Jest 测试源(不能安全改的部分写明零修改)。
2. docs/20_调查/STEP-114_FastPath安全准入与闭环判定_v1.md:R/C/Placement 证据、可能的反例、实现范围、安全前提、实际测试方案。
3. docs/50_Step记录/STEP-114_FastPath安全准入与TestApp闭环_v1.md:当轮修改、状态、验证事实及后续步骤。
4. docs/报告/STEP-114_FastPath安全准入与TestApp闭环_Codex完成报告_v1.md:模型全名档位、改动文件、影响/兼容、RDC 具体命令、已做/未做的验收、Git check。
5. 追加中文 粒子开发记录.md:当前改动、当前状态、下一步。
6. 新增 Test App 入口时更新相关场景说明,清楚区分真实 PBF 与独立 smoke。
7. 文档必须实际写入并保留,不能任务后要求用户补交。
## Git 收尾
- Codex 只能执行无构建型静态检查与 git diff --check,回报 git status 和必要的验证命令;禁止将未运行的测试写成 PASS。
- Codex 完成后先停下由 RDC 独立审计/运行定向检查;阶段结果达到合理门槛后由 RDC scoped commit 到 particle/main 并 push origin,核验远端 SHA。若未达真实 Fast,不得标示 Fast Path 已修复。正式发行独立处理。

View File

@@ -0,0 +1,77 @@
# STEP-114 Fast Path 安全准入与 Test App 闭环 v1
日期:2026-10-11(JST)。状态:**进行中;源码交付 PARTIAL,运行闭环待 RDC/用户,Symbol R 准入 BLOCKED。**
## 目标、基线与范围
- Mac / Darwin 24.6.0 / x86_64;`/Users/tei/dev/MapLibreParticleLab`;`particle/main@b02e54de3d45fcf612126ae8f037cba204f590ea`。
- 实际 GPT-6.1-Sol(gpt-6.1-sol)/ high 已从本会话执行记录核实。没有模型升级、子代理或另一 Codex 实例。
- Native/Test App 源码版本 `13.6.1-particle.23` 未改;Integration 契约仅只读,RN/JS公共API/ABI、Source Lock、正式发行不变。
- 只修改 Shared Core 内部安全子集判定/缺源诊断、现有非 Jest 测试、Android Test App、本Step中文文档和开发记录追加。
- 禁止 Codex 执行编译/测试/Gradle/ADB/Jest/CMake/Ninja/Bazel、commit/push、发行、跨项目写入或触碰原有 STEP-110/111 文件。
## 按顺序完成的源码工作
- [x] 静态闭环:Test App source类型、R初建失败、mutation/异步布局/资源/Annotation追踪、C/scene复合条件、Placement两分支。
- [x] 本机旧STEP-113完整JSON回读发现:navsea_delivery是Vector;R依赖tide但Native source表缺tide。缺revision默认unknown-dependency不能作为CustomMVT已出现的证据。
- [x] R缺失 RenderSource 提前返回missing-source,仍ready/stable=false、revision0;不补假数据,不放行CustomMVT/未知revision。
- [x] 保守GeoJSON/circle R正向独立场景;source admission/cache GPU算法不改,3个C++用例源码补充结构保护与revision/cache状态机覆盖。
- [x] 非粒子C继续fail-closed;独立scene只在确认没有Annotation实例时移除空内建层,正式PBF五场景原样保留。
- [x] 无Symbol全场的controller stale不影响Symbol像素消费者;限定#13只在完整层表有结构性Symbol时阻止。空表/数量不一致仍保守,隐藏或空Symbol不豁免。
- [x] Symbol R真稳定窗口审查:现有commit/fade证据不足,保留全部Build/scene条件;停止更大结果签名设计,明确BLOCKED。
- [x] 独立FAST_SMOKE、CIRCLE_C_BLOCKED、ANNOTATION_BLOCKED、SYMBOL_R_BLOCKED;菜单/Intent、低频真实Native UI、单次Source/Image/Camera/Annotation/Cache命令与操作前后文件、Reload/前后台/Teardown取证入口。
- [x] 调查、场景/定向验收说明、Step、中文完成报告、粒子开发记录实际落盘。
- [ ] Mac Native配置/编译和3个新用例及指定回归实际运行。
- [ ] Kotlin/Android一次手工编译、真实NativeFast>0、视觉与反向/生命周期设备闭环。
- [ ] Symbol R结果证书、CustomMVT完整revision契约、非粒子C复合等价证明:未实现,维持BLOCKED/fail-closed。
- [ ] iOS Metal实机/性能与生产Fast:NOT VERIFIED,不能由smoke替代。
## 当前结果与限制
没有新的编译、测试、APK、设备、GPU计数或性能结果。SOURCE-PROVEN/IMPLEMENTED与旧RETAINED-EVIDENCE分开;整体PARTIAL,不标Fast Path已修复。旧PBF缺tide诊断修正不代表该R可以Build;即使补源仍受SymbolPlacement保护。
预期FAST_SMOKE的真实Native层序4层、R[0,2)/C[2,4),必须取得Build/Reuse/Full/Fast与24Gate同帧证据。UIsceneReady只代表加载/命令成功,实际Native结构由导出JSON检查。三个负场景继续阻止Fast。Source等操作自动100/500/1500ms取证仍可能错过瞬态,缺证据如实记NOT VERIFIED。
## 已做检查与精确验收入口
Codex仅源码/文档阅读、已有JSON解析、Git只读状态、静态函数/范围/哈希核对、git diff --check和新增文件空白核对。未执行任何构建/测试/设备/Git写操作。
精确命令:`docs/60_测试/STEP-114_独立FastSmoke场景与RDC验收_v1.md` 第2节Mac Native配置/target/过滤器/日志/失败停止;第3—5节用户后续手工Android/ADB、JSON检查和反向/生命周期矩阵。完成报告也列出Native最小命令。配置失败保留现场,不能使用旧runner或自行安装依赖伪造通过。
## 独立验收待填写
| 项目 | 当前事实 / RDC或用户证据待填 |
|---|---|
| 源码baseline/diff/hash与制品版本 | 未提交工作区源码;用户构建前保存明确diff/hash与dirty状态 |
| Mac configure/build/新用例3个与回归 | NOT RUN;记录exit/耗时/真实测试数/skipped/日志/首错 |
| APK/ZIP/ABI/大小/SHA/签名 | NOT RUN;唯一现有Validation脚本由用户手工 |
| FAST_SMOKE真实层表/plan/cache/Fast | NOT RUN;4层R[0,2)/C[2,4),decisionMismatch=0,Fast/Reuse delta>0 |
| Source异步失效/Tile/Image/Camera/Cache恢复 | NOT RUN;保存五phase文件与精确变化帧,不从末态猜Gate |
| Circle C/Annotation/Symbol R负场景 | NOT RUN;保持阻止并具备可见内容与真实更新 |
| Reload/前后台/Teardown/重新进入 | NOT RUN;销毁前文件不是销毁完成证据 |
| 原STEP-113五场景与STEP-102回归 | NOT RUN;保留正式PBF全部定义,缺源改报missing-source |
| iOS Metal设备/像素/CPU/GPU性能 | NOT VERIFIED;不以Mac状态机测试替代 |
| Git提交/推送/远端HEAD | NOT RUN且本轮禁止;后续另行授权RDCscoped收尾 |
## 必做输出与下一步
调查:`docs/20_调查/STEP-114_FastPath安全准入与闭环判定_v1.md`。
验收:`docs/60_测试/STEP-114_独立FastSmoke场景与RDC验收_v1.md`。
报告:`docs/报告/STEP-114_FastPath安全准入与TestApp闭环_Codex完成报告_v1.md`。
以上及本记录、源文件、开发记录已由Codex实际生成。接下来由RDC独立审计并执行MacNative定向验证;用户一次手工Android/设备闭环。若缺libuv、编译失败或真实Fast仍为0,保留首错/24Gate再回交修复;不得继续扩大Symbol/CustomMVT/C放行或进入发行。
## RDC 独立审核追加(2026-10-11)
**结论:源码/差异/接口静态审核 PASS;Native 与 Android 编译、Fast 帧 NOT VERIFIED;整体 STEP-114 PARTIAL。**
- RDC 独立阅读本轮 Native diff、C++ 新用例、STEP-114 Android Test App 新场景与菜单、生命周期接线;git diff --check 退出码 0。本轮只有范围内的五个 tracked 改动和新增 Kotlin/中文文档;原 STEP-110/111 未跟踪任务书不修改、不暂存。
- 新增三项 Step114FastPathSafety 用例存在,覆盖无 Symbol、隐藏/Annotation Symbol 和 Source revision/cache 状态机,但**没有实际运行**。
- Step113PbfGateScene.kt SHA-256 为 6e7befeb90ae172aaeff4b636a7640274dfb1065d8c97744cbc6af8d22b4ccae,Step102PlacementFixture.kt SHA-256 为 e44912970dd6353787be7a3e5f9924fe1249e2305d495f1e25e707352c97d3c7,均未变化。
- Java SDK 只读核对实际提供 Style.removeLayer(String) 与 MapLibreMap.getAnnotations()。Kotlin/Gradle 编译未执行。
- Mac 预检:pkg-config --exists libuv 返回 1,与先前 macos-metal CMake required libuv 失败一致。独立 C++ header -fsyntax-only 试验退出码 1,首错为 Xcode AppleClang 找不到标准 C++ 头文件 array,实际在进入项目源码检查前终止。日志存于 Mac /tmp/step114-header-check.log。两项均是环境阻塞,不是本轮 Native 测试 PASS 或已证明代码错误;未安装/修改依赖。
- 实际 Fast Path 命中、正反 GPU 渲染、Android 设备、性能、iOS 均未验证。正式 PBF 仍有 tide 缺源、Symbol R Placement、内建 Annotation C 的阻断。STEP-114 独立最小场景属于待用户一次 Android 手工构建集中验收内容,不能用旧 APK。
- 后续 scoped Git 收尾若执行仅表示 PARTIAL 源码及测试入口已归档,**不代表正式发布或 Fast Path 修复完成**,不打 Tag/Source Lock。

View File

@@ -0,0 +1,172 @@
# STEP-114 独立 Fast Smoke 场景与 RDC 验收 v1
日期:2026-10-11。状态:**源码交付;下列编译、测试和设备操作全部 NOT RUN。** Codex 不执行本页命令。Mac Native 由 RDC/普通 Terminal 独立执行;Android 构建、安装、ADB 与实体机步骤由用户在后续授权条件下手工统一执行。
## 1. 场景入口和范围
Test App:`STEP 093 → STEP-114 独立 FAST_SMOKE(非正式 PBF)`。正式网络 PBF 仍在 `STEP 093 → STEP-113 FZG1-PBF GATE`,保留 PBF63、STATIC_RC、PARTICLE_OFF、PARTIAL_RCRR、SOURCE_CHANGE 原实现。
| 独立场景 | 预期层序/保护 | 待验收结果 |
|---|---|---|
| FAST_SMOKE | R=background + 非空本地 GeoJSON/circle;C=WeatherColor + RasterParticle。无 Annotation 实例时删除空内建 Annotation layer,保留 source | Native 实际4层;R Build/Reuse 和 Full/Fast 必须真实增长 |
| CIRCLE_C_BLOCKED | baseline 后追加普通 circle C;Annotation 层仍按独立空场景移除 | 实际5层,#22/#23 阻止 Fast;R 可正常 Build/Reuse,不等于整个 scene 可 Fast |
| ANNOTATION_BLOCKED | baseline 加正常自动内建 Annotation Symbol C,并创建真实 marker;不删该层 | 实际5层;#22/#23 阻止 Fast;marker 可见、可单次 position 更新 |
| SYMBOL_R_BLOCKED | baseline 的 R 内加本地16×16图标 Symbol;无需网络 glyph/sprite | 实际5层;Symbol R 首建仍被 Placement gate 阻止;不能把关闭 transitions 当修复 |
全部使用既有离线 ECMWF canonical 双帧资产、固定预报 `1786060800000` ms、PAUSE、30fps目标和1024粒子,SDK camera 初始33.45/129.97、zoom10.5、bearing/pitch0;手势禁用且退出恢复。30fps目标不是实际帧率测量。actualForecastTimeObservedMs 保持 null,不能当成 Native 已回读请求时间。sceneReady 只表示加载回调与控制命令成功,不替代 Native 层表/valid/实际渲染核验。
不要在旧正式 V23 APK 上验收本轮新入口;版本号未变,必须保存本轮源码 diff/hash、build-info 的 dirty 状态与 APK SHA。不得发行或覆盖已有版本。
## 2. Mac Native 精确定向命令(仅 RDC 执行)
基于现有 `macos-metal` preset 和实际 `mbgl-test-runner` target,无全仓测试、无 Jest。STEP-113 曾缺 libuv;本轮没有安装依赖。配置失败则停止,不运行旧 runner。
```bash
cd /Users/tei/dev/MapLibreParticleLab
bash <<'SH'
set -eu
mkdir -p artifacts/maplibre-particle/step114-native
if cmake --preset macos-metal > artifacts/maplibre-particle/step114-native/configure.log 2>&1; then
echo 'configure exit=0'
else
step114_rc=$?
tail -n 80 artifacts/maplibre-particle/step114-native/configure.log
exit "$step114_rc"
fi
if cmake --build build-macos-metal --target mbgl-test-runner --parallel 3 > artifacts/maplibre-particle/step114-native/build.log 2>&1; then
echo 'build exit=0'
else
step114_rc=$?
tail -n 80 artifacts/maplibre-particle/step114-native/build.log
exit "$step114_rc"
fi
if build-macos-metal/mbgl-test-runner --gtest_filter='Step114FastPathSafety.*:FastGateTrace.*:ParticleFastPath.*:RetainedUnderlayCache.*:RetainedUnderlayRevision.*:RenderSegmentPlacementFree.*:RenderSegmentLocality.*:RenderSegmentPlan.*:RenderSegmentDiagnostics.*:RenderOrchestratorError.*:RenderOrchestratorTeardown.*:ImageManager.*:GlyphPBF.*' --gtest_color=no --gtest_output=xml:artifacts/maplibre-particle/step114-native/tests.xml > artifacts/maplibre-particle/step114-native/tests.log 2>&1; then
tail -n 20 artifacts/maplibre-particle/step114-native/tests.log
else
step114_rc=$?
tail -n 80 artifacts/maplibre-particle/step114-native/tests.log
exit "$step114_rc"
fi
SH
```
记录 configure/build/tests 各退出码、耗时、实际测试数量与是否 skipped;失败回传首个真实 error、target、必要上下文与日志路径。新增测试应实际发现3个 `Step114FastPathSafety` 用例,否则不能把空过滤器执行成功作为 PASS。这些主要是纯状态机/结构判定测试,**不能证明 Android GPU Fast 命中、帧像素一致或 iOS 设备通过**。
## 3. Android 后续一次集中手工验收
由用户手工,在设备 ABI=x86_64 且 Java17/SDK 环境满足现有规范时,运行唯一既有 Validation Pipeline;不自行改 ABI、build 参数或依赖,不全量 clean。
```bash
cd /Users/tei/dev/MapLibreParticleLab
./tools/particle_android_validation/build_validation_apk.sh
```
失败保留 build.log,禁止复用旧 APK。用户手工核对同包签名和数据后再安装;本页不授权卸载用户数据。下面 ADB 全部固定既有 FZG1-2 serial,仅作为用户后续手工命令:
```bash
adb -s 192.168.200.139:5555 shell getprop ro.product.cpu.abi
adb -s 192.168.200.139:5555 install -r artifacts/maplibre-particle/android-validation/MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk
adb -s 192.168.200.139:5555 shell am start -S -n org.maplibre.android.testapp/org.maplibre.android.testapp.activity.particle.ParticleCoreLayerValidationActivity --es step114_scenario FAST_SMOKE
```
启动后预热10秒,再观察60秒。在开始和结束分别点击“导出当前 Native 快照”。标签 `STEP114Gate` 每30秒只输出真实 Native 摘要,完整JSON写 App 私有 files。生命周期计数可以含预热,必须比较同 lifecycle/sceneGeneration 的两次计数 delta;不要称整个累计为60秒帧率。
菜单“单次更新 R GeoJSON”“Pan/Zoom 往返”“单次 Image 变更”“禁用/恢复缓存”保存 before/submitted/100/500/1500ms 的5份快照。一次只做一项,等恢复稳定并手动导出后再做下一项。自动文件有 actionSequence 和 phase,不追加 candidate 输入或人工 frame 结果。
每个负场景单独启动、预热、观察、导出,不把以下命令连续执行当验收:
```bash
adb -s 192.168.200.139:5555 shell am start -S -n org.maplibre.android.testapp/org.maplibre.android.testapp.activity.particle.ParticleCoreLayerValidationActivity --es step114_scenario CIRCLE_C_BLOCKED
adb -s 192.168.200.139:5555 shell am start -S -n org.maplibre.android.testapp/org.maplibre.android.testapp.activity.particle.ParticleCoreLayerValidationActivity --es step114_scenario ANNOTATION_BLOCKED
adb -s 192.168.200.139:5555 shell am start -S -n org.maplibre.android.testapp/org.maplibre.android.testapp.activity.particle.ParticleCoreLayerValidationActivity --es step114_scenario SYMBOL_R_BLOCKED
```
ANNOTATION_BLOCKED 点击“单次更新真实 Annotation”,截图应见位置变化,Native 保持 fail-closed。FAST_SMOKE 中点击该操作会明确 BLOCKED(没有 marker),不创建隐藏 Annotation 或假事件。
## 4. 回读完整快照(用户手工)
在普通 bash Terminal 保存全部 step114 JSON,不通过 Logcat 分块拼接。路径来自当前 Debug App:
```bash
cd /Users/tei/dev/MapLibreParticleLab
bash <<'SH'
set -eu
mkdir -p artifacts/maplibre-particle/android-validation/step114
adb -s 192.168.200.139:5555 shell run-as org.maplibre.android.testapp ls files > /tmp/step114-files.txt
tr -d '\r' < /tmp/step114-files.txt > /tmp/step114-files-normalized.txt
while IFS= read -r step114_file; do
case "$step114_file" in
step114-*.json)
adb -s 192.168.200.139:5555 exec-out run-as org.maplibre.android.testapp cat "files/$step114_file" > "artifacts/maplibre-particle/android-validation/step114/$step114_file"
;;
esac
done < /tmp/step114-files-normalized.txt
SH
adb -s 192.168.200.139:5555 logcat -d -v time -s STEP114Gate:I '*:S' > artifacts/maplibre-particle/android-validation/step114/device.log
adb -s 192.168.200.139:5555 logcat -b crash -d > artifacts/maplibre-particle/android-validation/step114/crash.log
adb -s 192.168.200.139:5555 exec-out screencap -p > artifacts/maplibre-particle/android-validation/step114/screen.png
```
每阶段独立命名截图/日志;同名覆盖前先归档。App files 自动快照只在显式操作和生命周期变化时创建,数量无硬上限;验收后由用户管理归档,Codex 不删文件。100ms采样也可能错过一帧的变更窗口,未捕获不能标反向 Gate PASS,需保留日志再复核。
下面是读取**实际导出文件**的正向检查;把路径替换为稳定 FAST_SMOKE 手动快照的明确文件名(不要随意用最后的 teardown/旧场景文件):
```bash
python3 - 'artifacts/maplibre-particle/android-validation/step114/替换为稳定FAST_SMOKE文件.json' <<'PY'
import json, sys
from pathlib import Path
d = json.loads(Path(sys.argv[1]).read_text())
assert d['scenario'] == 'FAST_SMOKE' and d['sceneReady'] and d['blockedReason'] is None
n = d['native']; t = n['fastGateTrace']
assert n['valid'] and n['styleLayerCount'] == 4
assert [l['layerType'] for l in n['styleLayers']] == ['background', 'circle', 'weather-color', 'raster-particle']
assert [s['strategy'] for s in n['segments']] == ['RETAINED', 'CONTINUOUS']
r, c = n['segments']
assert (r['beginIndex'], r['endExclusive'], c['beginIndex'], c['endExclusive']) == (0, 2, 2, 4)
assert not r['placementCapable'] and r['placementGatePassed']
assert r['cache']['sourceReady'] and r['cache']['sourceStable'] and r['cache']['supported']
assert r['cache']['build'] > 0 and r['cache']['reuse'] > 0
assert t['fullRendered'] > 0 and t['fastRendered'] > 0 and t['decisionMismatch'] == 0
assert len(t['gates']) == 24
fs = [f for f in t['frames'] if f['completed'] and f['outcome'] == 'fast-rendered'
and f['post']['configRevision'] == n['configRevision']
and f['post']['topologyRevision'] == n['topologyRevision']
and f['post']['contentRevision'] == n['contentRevision']]
assert fs, '完整窗口没有真实完成的 Fast 帧'
for f in fs:
assert f['candidate'] and f['decisionConsistent'] and f['knownMask'] == 0xffffff and f['failMask'] == 0
assert f['gateStatus'] == ['PASS'] * 24
assert not f['cacheInputsTruncated'] and f['cacheInputs']
assert all(x['valid'] and x['buildRevision'] == x['staticRevision'] for x in f['cacheInputs'])
assert f['post']['fullCpuFrame'] == f['pre']['fullCpuFrame']
assert any(x['actualExecution'] == 'composite' for x in f['post']['retained'])
print('仅此快照 Native 正向证据通过;反向/生命周期/视觉/双平台仍需独立验收')
PY
```
不能通过修改导出的 JSON、补造 frame、重命名旧文件或放宽断言制造结果。
## 5. 定向反向、生命周期与回归矩阵
| 项目 | 必需证据/接受条件 |
|---|---|
| 稳定 baseline | 两次同生命周期快照 Fast/Reuse delta>0;至少一个 completed Fast 的24 Gate全通过;Build>0;orange R circle可见,天气/粒子正常;decisionMismatch=0 |
| Source mutation/异步 Tile | 单次sourceMutationCount增长;sources 表及 lastMutation 定位 step114-r-geojson;content/source revision变化;#8/#5/ready/pending 等真实拒绝证据、成功布局后 Full/Build 增长再恢复 Fast。tombstone/失败布局用Native状态机用例补充,不冒充GPU验证 |
| Image 更新 | images signature/content/global revision 导致 Full;重建后恢复。baseline图标未引用也保守失效;不能宣称已验证 glyph atlas像素 |
| Glyph | baseline完全无文字;本轮未新增 glyph mutation Test App控制。已有 GlyphPBF/ImageManager 测试仅资源回归;真实 Symbol glyph URL/font/atlas变化属于 Symbol结果证书阻塞,NOT VERIFIED,不能以image替代 |
| Pan/Zoom 往返 | SDK cameraObservedFromSdk与Native gate/key对应,Full/Build增量;恢复相机后不能直接复用旧key;截图无陈旧circle/错位 |
| 缓存禁用/恢复 | 禁用时#2失败、实际Full/live,无completed Fast增长;恢复后合法Build与Fast恢复;比较截图确认R内容/层序一致 |
| Reload | 保存reload-before与加载后快照;sceneGeneration变更、Native当前层表/plan正确,重新Build;trace lifecycle仅在实际Native reset时变,不要求UI generation=Native lifecycle |
| 非粒子C/真实Annotation | CIRCLE_C_BLOCKED/ANNOTATION_BLOCKED稳定窗口#22/#23失败、无completed Fast;真实marker可见/位置变化;#24是短暂pending保护,不要求在3秒末态仍FAIL |
| Symbol R | 本地icon可见;R placementCapable=true,正常Full placementGatePassed=false/Build0或对应安全fallback;Fast不放行,scene #23可能另挡;无证书不改判定 |
| 前后台 | baseline导出、手工Home后台10秒、回前台观察/导出;pause-before文件、恢复后Native增长、无死循环/错层/crash。此处未强制改变WeatherRuntime播放语义 |
| Teardown/重建 | 退出场景/关闭Activity产生teardown-before,dispose与callback取消;用户确认MapView销毁返回/无crash,重新启动baseline。teardown-before不是销毁完成证明 |
| 原PBF5场景 | 分别采集STEP-113五场景;仍保留全部基础63层定义/顺序及Annotation;缺tide时应明确missing-source,CustomMVT若实际出现仍unknown;不要求生产Fast>0、不篡改旧统计 |
| 原STEP-102 | T1/T2/T3/T4 fixture和既有动态poll回归;尤其含Symbol路径不因本轮无Symbol优化被放行 |
| iOS/Metal | 同Shared Core源码的Mac Metal定向测试先执行;iOS实机与GPU像素/性能未验收。Android Fast成功不能替代此项 |
## 6. 收口规则
记录各场景来源commit/dirty diff、APK SHA/版本、device serial/ABI、真实Native层表/plan、before/after文件与SHA、24Gate、Full/Fast/Build/Reuse增量、截图、首错及crash日志。processSimulationAdvance是进程级,不作为单Map Fast证据。post的原始 placementTransitionActive仍可能true,但无Symbol的preFastPlacementTransition=false是本轮受控变化,decisionMismatch应仍0。
没有真实Fast或反向证据不足,保持 PARTIAL;Symbol证书/CustomMVT/C放行分别继续 BLOCKED/fail-closed。不得根据静态检查、runner退出0或APK成功宣称整个Fast Path修复/性能PASS。Codex未commit/push,RDC后续Git收尾另需明确授权与scoped路径,不包括STEP-110/111或正式发行。

View File

@@ -0,0 +1,134 @@
# STEP-114 Fast Path 安全准入与 Test App 闭环 Codex 完成报告 v1
日期:2026-10-11(JST)。
## 1. 执行结论
**PARTIAL:受控安全子集源码、独立最小场景与中文交付已完成;真实 Fast、编译和设备闭环未验收。Symbol R 准入 BLOCKED,CustomMVTVector 与非粒子 C 继续 fail-closed。**
核心结果:完整场景无任何结构性 Symbol 时,全局 Placement controller 的 stale 单独不再阻止 #13;有隐藏/空Symbol或AnnotationSymbol时保持原判定。R缺源明确报missing-source,仍拒绝Build/Reuse,revision登记机制不变。独立FAST_SMOKE用现有安全GeoJSON/circle R + WeatherColor/Particle C及真实Native计数;另交付3个反向场景,不直接注入candidate或人工Frame结果。
本机旧STEP-113 JSON明确navsea_delivery为普通Vector,R还依赖未在Native source表中的tide;不能把该unknown-dependency归因于CustomMVTVector。这项新事实已记录于调查,旧报告与正式PBF五场景不改。
## 2. 实际模型、Git与版本
- **GPT-6.1-Sol(gpt-6.1-sol)/ high**。实际从本机 `/Users/tei/.codex/sessions/2026/10/11/rollout-2026-10-11T09-44-12-01a1286a-d6a4-7e32-b91a-a9f3e63d8f3e.jsonl` 的最新 `turn_context.model/effort/cwd` 核实;未切换/升级模型,未启动子代理、另一实例或后台Codex。
- Mac / Darwin 24.6.0 / x86_64;仓库 `/Users/tei/dev/MapLibreParticleLab`。
- 起点branch=`particle/main`,start HEAD=`b02e54de3d45fcf612126ae8f037cba204f590ea`;无分支/worktree变更,未commit/push,收尾HEAD保持起点。
- 起点tracked clean;已有STEP-110/111/114三份未跟踪任务书。STEP-110/111原样保护,STEP-114只读不修改;暂存区未写。
- `git submodule status` 起点全部空格前缀,无未初始化或偏移标记;未操作子模块。
- Native/Test App=`13.6.1-particle.23`;没有改版本、RN公共方法/类型、Source Lock或发行。当前旧APK不包含本轮新源码。
- Integration相关正式Contracts/RFC/ADR只读;任务提供main=`80667d6c343411d03a760425438e3d292f452118`采用既有RDC事实,没有同步/fetch/pull或远端回读,没有跨项目写入。
## 3. 实际修改文件(tracked)
| 文件 | 实际内容与影响 |
|---|---|
| `src/mln/renderer/particle_fast_path.hpp` | 内部结构检测helper;非空完整层表无Symbol才可忽略controller transition;生产24Gate evaluator与失败掩码未改 |
| `src/mln/renderer/render_orchestrator.cpp` | #13输入按全场结构限定,层表数量不符保守;缺RenderSource先报missing-source;CustomMVT拒绝仍保留 |
| `test/renderer/retained_underlay_cache.test.cpp` | 3个Step114FastPathSafety用例源码:无Symbol/其它Gate保护,隐藏/空/AnnotationSymbol保护,revision/cache注册/异步失效/全局失效状态机 |
| `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt` | 独立菜单/Intent/状态恢复、统一场景退出、诊断区域与前后台生命周期;原STEP-113菜单明确标正式PBF |
| `粒子开发记录.md` | 仅末尾追加本轮中文状态与下一步 |
## 4. 实际新增文件
1. `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step114FastSmokeScene.kt`:独立离线FAST_SMOKE/3负场景,真实Native低频UI、操作前后文件、Source/Image/Camera/缓存/真实Annotation操作及Reload/生命周期取证。
2. `docs/20_调查/STEP-114_FastPath安全准入与闭环判定_v1.md`:R/C/Placement控制流、旧JSON事实、mutation追踪、安全前提/反例及停止范围。
3. `docs/60_测试/STEP-114_独立FastSmoke场景与RDC验收_v1.md`:场景说明、Mac精确定向命令、后续Android用户手工命令、实际JSON正向检查与反向/生命周期矩阵。
4. `docs/50_Step记录/STEP-114_FastPath安全准入与TestApp闭环_v1.md`:已做/未做勾选、真实结果与待验收表。
5. 本报告。
用户提供的未跟踪STEP-114任务书不是本轮生成物。旧STEP-110/111未跟踪文件未修改/暂存;原STEP-113场景类与STEP-102 fixture未修改。
## 5. 安全范围、兼容与零修改部分
R准入/RevisionBook/GPUcache算法零语义修改;缺源只修正诊断原因。没有证明CustomMVT完整revision契约,保留其拒绝。普通Vector和GeoJSON不是CustomMVT,不混淆类型。独立GeoJSON子集现有sourceDiff/immutable signature/onTileChanged/资源epoch/key保护详见调查。
非粒子C准入与Scene复合条件零修改,#22/#23保留。FAST_SMOKE独立Style在没有Annotation实例时移除其空内建layer;保留source,不改AnnotationManager。ANNOTATION_BLOCKED重新加载合法Style,保留内建layer并创建真实marker;不是生产零重载Annotation能力。原正式PBF五场景完整保留。
Symbol R的Build gate、commit/fade、scene placementStable、全图碰撞、controller计算、资源链均零修改;无最终GPU输入结果证书,停止新增签名设计。#13改动只影响完整无Symbol场景;空表/数量不一致保守,有一个隐藏/空Symbol也按原transition阻止。尚未进入旧表的新layer被immutable signature、Annotation pending和其它原Gate保护。
Shared Core没有平台分叉,没有新RN/JS/JNI控制API/ABI。既有可选诊断schema和24Gate顺序未改;缺源理由从模糊unknown-dependency变为已有理由missing-source是有意诊断差异。preFastPlacementTransition为本轮限定后的实际输入,post原始placementTransitionActive可能仍true;此差异不是shadow mismatch,也不是修改controller。
风险/限制:Native与Kotlin未编译;FAST_SMOKE尚未实测;本地sceneReady不是Native运行PASS;自动短延迟采样可能漏瞬态;App私有快照文件随显式操作增长;性能/像素/GL与Metal差异未测。Future新Placement-capable类型须同步扩展结构检测,当前只有Symbol声明CrossTileIndex::Required。
## 6. 与任务书差异与停止点
按步骤完成源码审查与可安全范围实现;任务允许PARTIAL。没有危险地放行CustomMVT、Annotation C或Symbol R,也没有复刻NavSea113层。R cache正向利用现有已支持GeoJSON,不新增未证source准入。新增无Symbol场景#13限定是调查中确认的安全局部修复;缺源更准确诊断使真正失败点可见。
真实Fast/反向/lifecycle闭环仍待执行。Symbol R结果证书需要更大跨平台设计,列BLOCKED;若涉及公共语义/API,回Integration RFC,不自行扩改。任务允许改测试源但禁止Codex运行编译/测试,遵守该边界。
## 7. 精确定向 RDC 验收命令
下面只交RDC/普通Terminal执行。进入仓库后逐项执行,任何失败立即停止链,保留完整日志,不用旧runner;带完整退出码保护的可复制脚本在测试说明第2节。
```bash
cd /Users/tei/dev/MapLibreParticleLab
cmake --preset macos-metal
cmake --build build-macos-metal --target mbgl-test-runner --parallel 3
build-macos-metal/mbgl-test-runner --gtest_filter='Step114FastPathSafety.*:FastGateTrace.*:ParticleFastPath.*:RetainedUnderlayCache.*:RetainedUnderlayRevision.*:RenderSegmentPlacementFree.*:RenderSegmentLocality.*:RenderSegmentPlan.*:RenderSegmentDiagnostics.*:RenderOrchestratorError.*:RenderOrchestratorTeardown.*:ImageManager.*:GlyphPBF.*' --gtest_color=no
```
renderer=Metal,Debug preset,parallel=3,target仅mbgl-test-runner。记录命令/exit/耗时/log路径/实际用例数量/skipped;新增3个Step114用例必须被发现并执行。STEP-113缺libuv的配置失败日志仍是既有证据,本轮未再次配置、未安装依赖;若仍失败,保留首错并记BLOCKED,不能宣称测试通过。
Android由用户后续一次手工运行 `./tools/particle_android_validation/build_validation_apk.sh`;其后手工安装/启动/取证命令逐条见测试说明第3—5节,固定serial=`192.168.200.139:5555`。入口Intent=`--es step114_scenario FAST_SMOKE`,三个负场景名为CIRCLE_C_BLOCKED/ANNOTATION_BLOCKED/SYMBOL_R_BLOCKED。完整快照为App files/step114-*.json;保存source/image/camera/cache动作前、提交后、100/500/1500ms证据。Codex没有执行这些命令。
## 8. 已执行检查与未执行验收
已执行:前置规范/任务/实际源码阅读,Git branch/HEAD/status/submodule只读;本地当前rollout模型字段核实;保留的STEP-113JSON解析、SourceType与缺tide取证;无构建静态函数/范围/哈希/追加完整性核对;tracked及新增文件空白检查。
**NOT RUN**:CMake/Ninja/Bazel/Native编译链接、C++测试、Kotlin编译、Gradle/APK/AAR、ADB/安装/logcat/截图/设备、Jest、性能/GPU/像素对照、iOS实机、发行、commit/push/远端HEAD。没有本轮编译/测试失败日志,也不能据此推断会通过。3个新增C++用例是源码交付,不是运行PASS;状态机markBuildSucceeded不是GPU成功证据。
静态检查的最终实际结果与保护哈希在第12节记录。终态仍有本轮tracked/untracked改动,未暂存、未提交。
## 9. 产物
实际产物仅源文件、测试源、四份中文文档与开发记录追加。无本轮APK/AAR/XCFramework、版本/SHA、构建/设备日志或运行截图。artifact目录的STEP-113JSON属于旧RDC证据,本轮只读未修改,不作为本轮新产物。
## 10. 剩余阻塞与未完成事项
| 项目 | 状态/原因 |
|---|---|
| 独立smoke真实Fast | 未验证;必须NativeFull/Fast/Build/Reuse及24Gate和截图共同证明 |
| Mac配置/编译/测试 | 未执行;旧libuv阻塞是否仍存在未知 |
| Android编译与设备闭环 | 未执行,留用户一次手工统一验收 |
| Source异步变化/资源/camera/cache/Reload/前后台/销毁 | 已有源码和精确取证入口,运行证据待执行;捕获失败不可假称安全反向PASS |
| Glyph实际结果/资源失效 | 未新增TestApp glyph控制;无Symbolbaseline不含glyph像素;现有资源回归不能代替Symbol证书 |
| Symbol R首次Build | BLOCKED;commit/fade没有充分稳定结果证书,不做危险简化 |
| CustomMVT/非粒子C | fail-closed;无完整revision/复合等价证明 |
| PBF缺tide/大量Symbol | 缺源诊断已清楚,原数据/业务层不改;不承诺生产Fast |
| iOS/Metal设备与CPU/GPU收益 | NOT VERIFIED;不以Android或Mac状态机结果替代 |
## 11. 独立验收证据与后续
RDC先核对本报告/调查/源码diff与安全前提,再跑Mac定向命令;用户后续统一Android。baseline必须真实4层与R[0,2)/C[2,4),至少一个completed fast-rendered、knownMask=0xffffff/failMask=0、decisionMismatch=0、cacheInputs valid/revision相等,Full>0、Build>0、同生命周期Fast/Reuse delta>0;同时截图确认Rcircle/天气/粒子及反向重建正确。测试说明含读取真实JSON的断言。
负场景必须继续阻止Fast并正确显示/更新内容;Source/Tile/Image/Camera/缓存禁用/Reload/前后台/Teardown、旧PBF5场景及STEP-102独立回归仍需日志/快照与可见结果。只见Fast数字不等于安全/性能PASS。若未达真实Fast维持PARTIAL,失败保留现场再返回受控修复。
本轮禁止Git提交/push;后续达合理门槛且获授权后RDC仅scoped提交本轮明确路径并核对origin远端HEAD。不得含原STEP-110/111,不发行,不自行创建后续Step。
## 12. 收尾静态事实
- `git diff --check` 退出0;五个tracked修改路径,无其它tracked修改。四份新中文文档与新Kotlin文件各执行no-index空白检查,无诊断;Markdown代码围栏成对。
- 生产`evaluateParticleFastPath`和`particleFastGateFailureMask`与HEAD提取的完整函数逐字相同;C范围准入、R全局/局部稳定性函数逐字相同,CustomMVT拒绝分支仍在。3个新增用例仅核对源码数量,未运行。
- 原`Step113PbfGateScene.kt`与HEAD字节相同,SHA-256=`6e7befeb90ae172aaeff4b636a7640274dfb1065d8c97744cbc6af8d22b4ccae`;`Step102PlacementFixture.kt`同样未变,SHA-256=`e44912970dd6353787be7a3e5f9924fe1249e2305d495f1e25e707352c97d3c7`。
- 开发记录的HEAD原始162365字节前缀完整保留,原SHA-256=`0479875a30577cb497b973f89a0d74b3b8b8d300d5dfb683ed3a537749b37b3f`;仅9行/1583字节追加。
- STEP-110 SHA-256=`33037df71b4b30aa7956d59f4b99f77af3085405b24111bc0fe11ec857a93db0`,STEP-111=`f3a5806f25384bf46055db2edfea0d21bfbc4124184bc813b46e38b8079c1ec4`,与既有保护值相同;本次任务书只读SHA-256=`415c20df0e8d514b68dfa79cd6d9394e4c6d2c4f215c402d9a4a365862c87cfc`。
- 旧RDC完整`source-change-after-full.json`只读SHA-256=`24a06fa1cb072a628c650723c58a880895ec656f4972fa55d3cb064bfb2aabda`;本轮新场景需要的两个cycle各metadata.json/field.f32已静态确认存在。
- 一次用Python stdin追加中文记录的尝试因UTF-8源码解析退出1,未写文件;随后用apply_patch完成追加。最终原前缀核对通过。这是工具文本编码失败,不是构建/测试失败。
- 终态`particle/main@b02e54de3d45fcf612126ae8f037cba204f590ea`;`git diff --cached --name-only`为空。status为本轮5个tracked修改、5个新增文件,以及原有STEP-110/111/114三任务书;未commit/push/回读远端。
以上只证明静态一致性、范围与保护文件完整性,不作为Native/Kotlin编译、GPU/Fast或全任务PASS。
## RDC 独立审核追加(2026-10-11)
**结论:源码/差异/接口静态审核 PASS;Native 与 Android 编译、Fast 帧 NOT VERIFIED;整体 STEP-114 PARTIAL。**
- RDC 独立阅读本轮 Native diff、C++ 新用例、STEP-114 Android Test App 新场景与菜单、生命周期接线;git diff --check 退出码 0。本轮只有范围内的五个 tracked 改动和新增 Kotlin/中文文档;原 STEP-110/111 未跟踪任务书不修改、不暂存。
- 新增三项 Step114FastPathSafety 用例存在,覆盖无 Symbol、隐藏/Annotation Symbol 和 Source revision/cache 状态机,但**没有实际运行**。
- Step113PbfGateScene.kt SHA-256 为 6e7befeb90ae172aaeff4b636a7640274dfb1065d8c97744cbc6af8d22b4ccae,Step102PlacementFixture.kt SHA-256 为 e44912970dd6353787be7a3e5f9924fe1249e2305d495f1e25e707352c97d3c7,均未变化。
- Java SDK 只读核对实际提供 Style.removeLayer(String) 与 MapLibreMap.getAnnotations()。Kotlin/Gradle 编译未执行。
- Mac 预检:pkg-config --exists libuv 返回 1,与先前 macos-metal CMake required libuv 失败一致。独立 C++ header -fsyntax-only 试验退出码 1,首错为 Xcode AppleClang 找不到标准 C++ 头文件 array,实际在进入项目源码检查前终止。日志存于 Mac /tmp/step114-header-check.log。两项均是环境阻塞,不是本轮 Native 测试 PASS 或已证明代码错误;未安装/修改依赖。
- 实际 Fast Path 命中、正反 GPU 渲染、Android 设备、性能、iOS 均未验证。正式 PBF 仍有 tide 缺源、Symbol R Placement、内建 Annotation C 的阻断。STEP-114 独立最小场景属于待用户一次 Android 手工构建集中验收内容,不能用旧 APK。
- 后续 scoped Git 收尾若执行仅表示 PARTIAL 源码及测试入口已归档,**不代表正式发布或 Fast Path 修复完成**,不打 Tag/Source Lock。

View File

@@ -67,6 +67,10 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
private var step113DiagnosticsMaxLines = 6
private var step113Scene: Step113PbfGateScene? = null
private var step113Scenario: Step113PbfGateScene.Scenario? = null
private var step114Scene: Step114FastSmokeScene? = null
private var step114Scenario: Step114FastSmokeScene.Scenario? = null
private var step114DiagnosticsHeight: Int? = null
private var step114DiagnosticsMaxLines = 6
private var step102Scenario: Step102PlacementFixture.Scenario? = null
private var step102Tick = 0L
private val step102Handler = Handler(Looper.getMainLooper())
@@ -356,6 +360,11 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
} else intent.getStringExtra("step113_scenario")
step113Scenario = savedStep113
?.let { value -> Step113PbfGateScene.Scenario.values().firstOrNull { it.name == value } }
val savedStep114 = if (savedInstanceState?.containsKey("step114_scenario") == true) {
savedInstanceState.getString("step114_scenario")
} else intent.getStringExtra("step114_scenario")
step114Scenario = savedStep114
?.let { value -> Step114FastSmokeScene.Scenario.values().firstOrNull { it.name == value } }
super.onCreate(savedInstanceState)
Log.i(
TAG,
@@ -369,6 +378,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
diagnostics = findViewById(R.id.validationDiagnostics)
mapView.addOnDidFailLoadingMapListener { errorMessage ->
step113Scene?.onStyleLoadFailed(errorMessage)
step114Scene?.onStyleLoadFailed(errorMessage)
if (navSeaPbfMode || purePbfMode) {
Log.e(TAG, "NAVSEA_PBF_STYLE_ERROR reason=$errorMessage")
} else if (realWeatherMode) {
@@ -389,7 +399,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
updateNavSeaPbfButtonState()
updatePbfModeButtonState()
updateRetainedCacheButtonState()
if (step113Scenario != null) startStep113Scene(step113Scenario!!)
if (step114Scenario != null) startStep114Scene(step114Scenario!!)
else if (step113Scenario != null) startStep113Scene(step113Scenario!!)
else if (step102Scenario != null) loadStep102Style()
else if (step101Scenario != null) loadStep101Style()
else if (step097Topology != null) loadStep097StencilStyle()
@@ -700,6 +711,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
menu.add(0, 18, 18, "STEP-101 TEARDOWN")
menu.add(0, 19, 19, "STEP-102 PLACEMENT LOCALITY")
menu.add(0, 20, 20, "STEP-113 FZG1-PBF GATE")
menu.add(0, 21, 21, "STEP-114 独立 FAST_SMOKE(非正式 PBF)")
setOnMenuItemClickListener { item ->
when (item.itemId) {
in Step093Topology.values().indices -> applyStep093Scene(Step093Topology.values()[item.itemId])
@@ -714,6 +726,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
18 -> showStep101Menu(anchor)
19 -> showStep102Menu(anchor)
20 -> showStep113Menu(anchor)
21 -> showStep114Menu(anchor)
}
true
}
@@ -777,7 +790,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
private fun showStep113Menu(anchor: android.view.View) {
PopupMenu(this, anchor).apply {
Step113PbfGateScene.Scenario.values().forEachIndexed { index, scenario ->
menu.add(0, index, index, "STEP-113 ${scenario.name}")
menu.add(0, index, index, "STEP-113 正式 PBF · ${scenario.name}")
}
menu.add(0, 10, 10, "EXPORT LAST SNAPSHOT")
menu.add(0, 11, 11, "MUTATE PROBE SOURCE ONCE")
@@ -826,6 +839,9 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
private fun stopStep113Scene() {
// Existing style-switch entry points all call this method; also stop the
// independent smoke before another scene takes ownership of this Map.
stopStep114Scene()
step113Scene?.stop()
step113DiagnosticsHeight?.let { originalHeight ->
diagnostics.maxLines = step113DiagnosticsMaxLines
@@ -836,6 +852,72 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
step113Scenario = null
}
private fun showStep114Menu(anchor: android.view.View) {
PopupMenu(this, anchor).apply {
Step114FastSmokeScene.Scenario.values().forEachIndexed { index, scenario ->
menu.add(0, index, index, "独立 smoke · ${scenario.name}")
}
listOf("导出当前 Native 快照", "单次更新 R GeoJSON", "Pan/Zoom 往返", "Reload 当前场景",
"单次 Image 变更", "禁用/恢复缓存", "单次更新真实 Annotation", "退出独立 smoke")
.forEachIndexed { index, title -> menu.add(0, 10 + index, 10 + index, title) }
setOnMenuItemClickListener { item ->
when (item.itemId) {
in Step114FastSmokeScene.Scenario.values().indices ->
startStep114Scene(Step114FastSmokeScene.Scenario.values()[item.itemId])
10 -> step114Scene?.export()
11 -> step114Scene?.mutateSource()
12 -> step114Scene?.panZoom()
13 -> step114Scene?.reload()
14 -> step114Scene?.mutateImage()
15 -> step114Scene?.toggleCache()
16 -> step114Scene?.mutateAnnotation()
17 -> { stopStep113Scene(); loadValidationStyle(active, enabled, "STEP114_EXIT") }
}
true
}
show()
}
}
private fun startStep114Scene(scenario: Step114FastSmokeScene.Scenario) {
stopStep113Scene()
stopStep102Scenario()
stopStep099Scenario()
stopStep093DiagnosticsPolling()
step101Handler.removeCallbacksAndMessages(null)
step101Scenario = null
step097Topology = null
step093Mode = false
pureParticleMode = false
purePbfMode = false
navSeaPbfMode = false
realWeatherMode = false
weatherRuntime?.dispose()
weatherRuntime = null
styleReady = false
++styleLoadEpoch
step114Scenario = scenario
retainedCacheEnabled = true
updateRetainedCacheButtonState()
step114DiagnosticsHeight = diagnostics.layoutParams.height
step114DiagnosticsMaxLines = diagnostics.maxLines
diagnostics.maxLines = 10
diagnostics.layoutParams = diagnostics.layoutParams.apply { height = (156 * resources.displayMetrics.density).toInt() }
status.text = "STEP-114 独立 smoke · $scenario · 待真实 Native 验收"
step114Scene = Step114FastSmokeScene(this, maplibreMap) { diagnostics.text = it }.also { it.start(scenario) }
}
private fun stopStep114Scene() {
step114Scene?.stop()
step114DiagnosticsHeight?.let { originalHeight ->
diagnostics.maxLines = step114DiagnosticsMaxLines
diagnostics.layoutParams = diagnostics.layoutParams.apply { height = originalHeight }
}
step114DiagnosticsHeight = null
step114Scene = null
step114Scenario = null
}
private fun showStep102Menu(anchor: android.view.View) {
PopupMenu(this, anchor).apply {
Step102PlacementFixture.Scenario.values().forEachIndexed { index, scenario ->
@@ -2481,8 +2563,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
} else if (step093Mode || step097Topology != null) startStep093DiagnosticsPolling()
}
override fun onResume() { super.onResume(); mapView.onResume(); step113Scene?.resume() }
override fun onPause() { step113Scene?.pause(); mapView.onPause(); super.onPause() }
override fun onResume() { super.onResume(); mapView.onResume(); step113Scene?.resume(); step114Scene?.resume() }
override fun onPause() { step113Scene?.pause(); step114Scene?.pause(); mapView.onPause(); super.onPause() }
override fun onStop() {
step101Handler.removeCallbacksAndMessages(null)
step093Handler.removeCallbacks(step093DiagnosticsPoll)
@@ -2508,6 +2590,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
override fun onSaveInstanceState(outState: Bundle) {
outState.putString("step113_scenario", step113Scenario?.name)
outState.putString("step114_scenario", step114Scenario?.name)
outState.putBoolean(STATE_ACTIVE, active)
outState.putBoolean(STATE_ENABLED, enabled)
outState.putInt(STATE_MASK_MODE, maskMode)

View File

@@ -0,0 +1,329 @@
package org.maplibre.android.testapp.activity.particle
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.util.Log
import org.json.JSONArray
import org.json.JSONObject
import org.maplibre.android.annotations.Marker
import org.maplibre.android.annotations.MarkerOptions
import org.maplibre.android.camera.CameraPosition
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.maps.MapLibreMap
import org.maplibre.android.maps.Style
import org.maplibre.android.maps.WeatherRuntime
import org.maplibre.android.style.layers.PropertyFactory.iconImage
import org.maplibre.android.style.layers.SymbolLayer
import org.maplibre.android.style.sources.GeoJsonSource
import org.maplibre.android.testapp.BuildConfig
import org.maplibre.geojson.Feature
import org.maplibre.geojson.Point
import java.io.File
/** 独立离线最小场景;不代表正式 PBF/NavSea。所有帧与缓存计数仅来自 Native。 */
internal class Step114FastSmokeScene(
private val context: Context,
private val map: MapLibreMap,
private val display: (String) -> Unit
) {
enum class Scenario { FAST_SMOKE, CIRCLE_C_BLOCKED, ANNOTATION_BLOCKED, SYMBOL_R_BLOCKED }
private val handler = Handler(Looper.getMainLooper())
private var generation = 0
private var running = false
private var paused = false
private var ready = false
private var blocked: String? = null
private var scenario = Scenario.FAST_SMOKE
private var runtime: WeatherRuntime? = null
private var marker: Marker? = null
private var savedGestures: List<Boolean>? = null
private var commands = JSONObject()
private var mutation = 0
private var cameraAlternate = false
private var actionSequence = 0L
private var lastLogMs = 0L
private val poll = object : Runnable {
override fun run() {
if (!running || paused) return
try {
val native = JSONObject(map.getRenderSegmentDiagnostics())
val trace = native.optJSONObject("fastGateTrace")
val segments = native.optJSONArray("segments") ?: JSONArray()
display(buildString {
append("STEP-114 $scenario · 独立 smoke · ${if (ready) "READY/待验收" else blocked ?: "LOADING"}\n")
append("Native Full=${trace?.optLong("fullRendered")} Fast=${trace?.optLong("fastRendered")} ")
append("windowFast=${trace?.optLong("windowFastRendered")} mismatch=${trace?.optLong("decisionMismatch")}\n")
for (i in 0 until segments.length()) {
val s = segments.getJSONObject(i)
val c = s.optJSONObject("cache")
append("${s.optString("strategy")}[${s.optInt("beginIndex")},${s.optInt("endExclusive")}) ")
append("B=${c?.optLong("build") ?: 0} Reuse=${c?.optLong("reuse") ?: 0} ")
append("${s.optString("actualExecution")} ${c?.optString("lastFallbackReason") ?: ""}\n")
}
append("固定预报/相机;操作前后快照保存在 files/step114-*.json")
})
val now = SystemClock.elapsedRealtime()
if (now - lastLogMs >= 30000L) {
lastLogMs = now
Log.i(TAG, "scenario=$scenario ready=$ready full=${trace?.optLong("fullRendered")} " +
"fast=${trace?.optLong("fastRendered")} mismatch=${trace?.optLong("decisionMismatch")}")
}
} catch (error: Exception) { fail(error) }
handler.postDelayed(this, 3000L)
}
}
fun start(next: Scenario = Scenario.FAST_SMOKE) {
stop()
running = true
scenario = next
ready = false
blocked = null
commands = JSONObject()
mutation = 0
cameraAlternate = false
lastLogMs = 0L
val epoch = ++generation
display("STEP-114 $scenario · 独立离线场景加载中;正式 PBF 五场景另在 STEP-113")
val ui = map.uiSettings
savedGestures = listOf(ui.isScrollGesturesEnabled(), ui.isRotateGesturesEnabled(),
ui.isTiltGesturesEnabled(), ui.isZoomGesturesEnabled(),
ui.isDoubleTapGesturesEnabled(), ui.isQuickZoomGesturesEnabled())
ui.setAllGesturesEnabled(false)
map.cancelTransitions()
map.setRetainedUnderlayEnabled(true)
val plan = JSONObject().put("schemaVersion", 1).put("segments", JSONArray()
.put(JSONObject().put("strategy", "RETAINED"))
.put(JSONObject().put("strategy", "CONTINUOUS").put("startLayerId", COLOR_LAYER)))
commands.put("requestedPlan", plan).put("planAccepted", map.setRenderSegmentPlan(plan.toString()))
if (!paused) handler.post(poll)
map.setStyle(Style.Builder().fromJson(styleJson())) loaded@ { style ->
if (!running || epoch != generation) return@loaded
try {
check(commands.getBoolean("planAccepted")) { "RenderSegmentPlan 未接受" }
check(map.annotations.isEmpty()) { "存在非本场景 Annotation,禁止移除其层" }
if (scenario != Scenario.ANNOTATION_BLOCKED) {
// 只在这个没有 Annotation 实例的独立场景移除空内建层。
// 保留内建 source;生产场景和 Native Annotation 行为均不修改。
check(style.getLayer(ANNOTATION_LAYER) != null && style.removeLayer(ANNOTATION_LAYER)) {
"空内建 Annotation 层未能移除,保留 fail-closed 现场"
}
commands.put("emptyBuiltinAnnotationLayerRemoved", true)
} else {
check(style.getLayer(ANNOTATION_LAYER) != null) { "内建 Annotation 层缺失" }
marker = map.addMarker(MarkerOptions().position(LatLng(33.45, 129.97)))
commands.put("realAnnotationAdded", true)
}
if (scenario == Scenario.SYMBOL_R_BLOCKED) {
style.addImage(IMAGE, image(Color.YELLOW))
style.addLayerBelow(SymbolLayer(SYMBOL_LAYER, PROBE_SOURCE)
.withProperties(iconImage(IMAGE)), COLOR_LAYER)
commands.put("structuralSymbolInR", true)
}
map.cameraPosition = camera(false)
initializeFields()
ready = true
handler.removeCallbacksAndMessages(null)
if (!paused) handler.post(poll)
} catch (error: Exception) { fail(error) }
}
}
private fun styleJson(): String {
val layers = JSONArray()
.put(JSONObject().put("id", "step114-background").put("type", "background")
.put("paint", JSONObject().put("background-color", "#172c40")))
.put(JSONObject().put("id", "step114-r-circle").put("type", "circle").put("source", PROBE_SOURCE)
.put("paint", JSONObject().put("circle-radius", 12).put("circle-color", "#ff8800")))
.put(JSONObject().put("id", COLOR_LAYER).put("type", "weather-color").put("source", FIELD_SOURCE)
.put("paint", JSONObject().put("weather-color-opacity", 0.62)))
.put(JSONObject().put("id", PARTICLE_LAYER).put("type", "raster-particle").put("source", FIELD_SOURCE)
.put("paint", JSONObject().put("raster-particle-active", true)
.put("raster-particle-particle-count", 1024)))
if (scenario == Scenario.CIRCLE_C_BLOCKED) {
layers.put(JSONObject().put("id", "step114-c-circle").put("type", "circle").put("source", PROBE_SOURCE)
.put("paint", JSONObject().put("circle-radius", 5).put("circle-color", "#00ffff")))
}
return JSONObject().put("version", 8).put("name", "STEP-114 independent offline smoke")
.put("sources", JSONObject()
.put(PROBE_SOURCE, JSONObject().put("type", "geojson")
.put("data", JSONObject(Feature.fromGeometry(Point.fromLngLat(129.97, 33.45)).toJson())))
.put(FIELD_SOURCE, JSONObject().put("type", "raster-array").put("host-field-submission", true)))
.put("layers", layers).toString()
}
private fun initializeFields() {
val host = WeatherRuntime(map, WeatherRuntime.Config(FIELD_SOURCE, COLOR_LAYER, PARTICLE_LAYER))
runtime = host
val submitted = JSONArray()
for (cycle in listOf("2026080700", "2026080706")) {
val path = "integration/particle-weatherv2/ecmwf-hres-wind-20260807/$cycle"
val meta = context.assets.open("$path/metadata.json").bufferedReader().use { it.readText() }
val data = context.assets.open("$path/field.f32").use { it.readBytes() }
val result = host.submitWindField(meta, data)
submitted.put(result.name)
check(result.accepted()) { "离线 Field 提交失败: $cycle $result" }
}
commands.put("fieldSubmit", submitted)
.put("forecastSeek", host.setForecastTime(FORECAST_MS).name)
.put("forecastPause", host.setPlaybackState(WeatherRuntime.PlaybackState.PAUSE).name)
.put("particleVisibility", host.setParticleVisible(true).name)
.put("targetFps", host.setTargetFps(WeatherRuntime.TargetFps.FPS_30).name)
check(host.state == WeatherRuntime.State.READY && listOf("forecastSeek", "forecastPause",
"particleVisibility", "targetFps").all { commands.getString(it) == "ACCEPTED" }) {
"固定预报/粒子控制未全部接受: $commands"
}
}
// 操作前、提交后与短延迟分别保存真实 Native 窗口;不等待、不强制 Full、不构造 Gate 输入。
private fun action(name: String, change: () -> Unit) {
if (!running || !ready || paused) return
val epoch = generation
val id = ++actionSequence
try {
capture("$id-$name-before")
change()
commands.put("lastAction", name).put("actionSequence", id)
capture("$id-$name-submitted")
for (delay in listOf(100L, 500L, 1500L)) {
handler.postDelayed({
if (running && !paused && epoch == generation) {
try { capture("$id-$name-after-${delay}ms") } catch (error: Exception) { fail(error) }
}
}, delay)
}
} catch (error: Exception) { fail(error) }
}
fun mutateSource() = action("source") {
val source = checkNotNull(map.style?.getSourceAs<GeoJsonSource>(PROBE_SOURCE))
++mutation
source.setGeoJson(Feature.fromGeometry(Point.fromLngLat(129.97 + (mutation % 2) * 0.01, 33.45)))
commands.put("sourceMutationCount", mutation)
}
fun panZoom() = action("pan-zoom") {
cameraAlternate = !cameraAlternate
map.cameraPosition = camera(cameraAlternate)
commands.put("cameraAlternate", cameraAlternate)
}
fun mutateImage() = action("image") {
val count = commands.optInt("imageMutationCount") + 1
checkNotNull(map.style).addImage(IMAGE, image(if (count % 2 == 1) Color.RED else Color.YELLOW))
commands.put("imageMutationCount", count)
}
fun toggleCache() = action("cache-toggle") {
val enabled = !commands.optBoolean("cacheDisabled", false)
map.setRetainedUnderlayEnabled(!enabled)
commands.put("cacheDisabled", enabled)
}
fun mutateAnnotation() = action("annotation") {
val annotation = checkNotNull(marker) { "请先选择 ANNOTATION_BLOCKED" }
++mutation
annotation.position = LatLng(33.45, 129.97 + (mutation % 2) * 0.01)
commands.put("annotationMutationCount", mutation)
}
fun reload() {
if (!running) return
try { capture("reload-before") } catch (error: Exception) { fail(error); return }
start(scenario)
}
private fun envelope(native: JSONObject, phase: String) = JSONObject()
.put("schema", "step114-fast-smoke").put("schemaVersion", 1).put("appVersion", BuildConfig.VERSION_NAME)
.put("scenario", scenario.name).put("sceneGeneration", generation).put("sceneReady", ready)
.put("blockedReason", blocked ?: JSONObject.NULL).put("sampleElapsedMs", SystemClock.elapsedRealtime())
.put("phase", phase).put("evidence", "INDEPENDENT-SMOKE; 非PBF/非113层等价")
.put("commands", JSONObject(commands.toString())).put("forecastRequestedMs", FORECAST_MS)
.put("actualForecastTimeObservedMs", JSONObject.NULL)
.put("fieldBasis", "既有离线 ECMWF canonical 双帧;非NavSea当前QBIN")
.put("cameraObservedFromSdk", JSONObject().put("latitude", map.cameraPosition.target?.latitude)
.put("longitude", map.cameraPosition.target?.longitude).put("zoom", map.cameraPosition.zoom)
.put("bearing", map.cameraPosition.bearing).put("pitch", map.cameraPosition.tilt))
.put("native", native)
private fun capture(phase: String) {
val record = envelope(JSONObject(map.getRenderSegmentDiagnostics()), phase)
write(record, phase)
}
private fun write(record: JSONObject, phase: String) {
val file = File(context.filesDir, "step114-${scenario.name}-${generation}-${SystemClock.elapsedRealtime()}-$phase.json")
file.writeText(record.toString(), Charsets.UTF_8)
Log.i(TAG, "snapshot=${file.name} scenario=$scenario generation=$generation phase=$phase")
}
fun export() {
if (!running) return
try { capture("manual") } catch (error: Exception) { fail(error) }
}
fun onStyleLoadFailed(message: String) { if (running) fail(IllegalStateException(message)) }
private fun fail(error: Exception) {
ready = false
blocked = "BLOCKED: ${error.message}"
display("STEP-114 $scenario $blocked")
Log.e(TAG, "scene-blocked", error)
}
fun pause() {
if (running) try { capture("pause-before") } catch (error: Exception) { fail(error) }
paused = true
handler.removeCallbacksAndMessages(null)
}
fun resume() {
paused = false
handler.removeCallbacksAndMessages(null)
if (running) handler.post(poll)
}
fun stop() {
if (running) try { capture("teardown-before") } catch (error: Exception) { Log.e(TAG, "capture-failed", error) }
running = false
++generation
handler.removeCallbacksAndMessages(null)
runtime?.dispose()
runtime = null
marker?.let { map.removeMarker(it) }
marker = null
savedGestures?.let { flags ->
val ui = map.uiSettings
ui.setScrollGesturesEnabled(flags[0]); ui.setRotateGesturesEnabled(flags[1])
ui.setTiltGesturesEnabled(flags[2]); ui.setZoomGesturesEnabled(flags[3])
ui.setDoubleTapGesturesEnabled(flags[4]); ui.setQuickZoomGesturesEnabled(flags[5])
}
savedGestures = null
ready = false
Log.i(TAG, "scene-stop generation=$generation;MapView 销毁验收由 Activity/设备日志确认")
}
companion object {
const val TAG = "STEP114Gate"
private const val ANNOTATION_LAYER = "org.maplibre.annotations.points"
private const val PROBE_SOURCE = "step114-r-geojson"
private const val FIELD_SOURCE = "step114-fields"
private const val COLOR_LAYER = "step114-color"
private const val PARTICLE_LAYER = "step114-particles"
private const val SYMBOL_LAYER = "step114-r-symbol"
private const val IMAGE = "step114-image"
private const val FORECAST_MS = 1786060800000L
private fun camera(alternate: Boolean) = CameraPosition.Builder()
.target(LatLng(33.45, if (alternate) 130.0 else 129.97))
.zoom(if (alternate) 11.0 else 10.5).bearing(0.0).tilt(0.0).build()
private fun image(color: Int): Bitmap = Bitmap.createBitmap(16, 16, Bitmap.Config.ARGB_8888)
.apply { eraseColor(color) }
}
}

View File

@@ -1,11 +1,22 @@
#pragma once
#include <mln/renderer/render_segment.hpp>
#include <array>
#include <cstdint>
#include <memory>
namespace mln {
// A stale controller alone cannot affect pixels in a scene with no Symbol layers.
// Check the complete structural layer table, including hidden/empty Symbols and
// Annotation Symbols. Never substitute this for per-R placement/build evidence.
inline bool particleFastPathHasPlacementTransition(const std::vector<StyleLayerDiagnostics>& layers,
bool controllerTransition) noexcept {
return controllerTransition && (layers.empty() || renderSegmentHasPlacementCapableLayer(
layers, 0, static_cast<int32_t>(layers.size())));
}
enum class ParticleFastPathMissReason : uint8_t {
None,
NoPreparedTree,

View File

@@ -1,5 +1,6 @@
#include <mln/renderer/render_orchestrator.hpp>
#include <mln/renderer/continuous_layer_range.hpp>
#include <mln/renderer/particle_fast_path.hpp>
#include <mln/annotation/annotation_manager.hpp>
#include <mln/layermanager/layer_manager.hpp>
@@ -1179,7 +1180,13 @@ RenderOrchestrator::RetainedSegmentSourceState RenderOrchestrator::retainedSegme
// Keep its ordinary renderer active and fail closed only for cache reuse.
for (const auto& sourceId : dependencies.sourceIds) {
const auto source = renderSources.find(sourceId);
if (source != renderSources.end() && source->second->baseImpl->type == style::SourceType::CustomMVTVector) {
if (source == renderSources.end()) {
// A missing source also has no revision-book entry. Report the
// concrete dependency failure before revisionFor collapses it to unknown.
result.reason = "missing-source";
return result;
}
if (source->second->baseImpl->type == style::SourceType::CustomMVTVector) {
return result;
}
}
@@ -1278,7 +1285,10 @@ ParticleFastPathDynamicState RenderOrchestrator::particleFastPathDynamicState(Ti
}
}
}
result.placementTransition = placementController.hasTransitions(timePoint);
const bool controllerTransition = placementController.hasTransitions(timePoint);
result.placementTransition = controllerTransition &&
(retainedStyleLayers.size() != renderLayers.size() ||
particleFastPathHasPlacementTransition(retainedStyleLayers, controllerTransition));
for (const auto& entry : renderSources) {
result.fadingTiles = result.fadingTiles || entry.second->hasFadingTiles();
}

View File

@@ -526,6 +526,86 @@ TEST(ParticleFastPath, ParticleOnlyStableRevisionIsCandidate) {
EXPECT_EQ(decision.reason, ParticleFastPathMissReason::None);
}
TEST(Step114FastPathSafety, PlacementFreeSceneIgnoresControllerStaleOnly) {
EXPECT_TRUE(particleFastPathHasPlacementTransition({}, true)); // Missing evidence stays closed.
const std::vector<StyleLayerDiagnostics> layers{
{0, "background", "background", true, std::nullopt, std::nullopt},
{1, "probe", "circle", true, "probe-source", std::nullopt},
{2, "color", "weather-color", true, "field", std::nullopt},
{3, "particle", "raster-particle", true, "field", std::nullopt}};
auto input = fastPathInput();
input.hasPlacementTransition = particleFastPathHasPlacementTransition(layers, true);
EXPECT_FALSE(input.hasPlacementTransition);
EXPECT_TRUE(evaluateParticleFastPath(input).candidate);
// This narrow change does not override any source/tile/resource/camera gate.
input.contentRevisionMatches = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::ContentMutation);
input.contentRevisionMatches = true;
input.hasPendingChanges = true;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::PendingChanges);
input.hasPendingChanges = false;
input.cameraMatches = false;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::CameraChanged);
input.cameraMatches = true;
input.unsupportedContinuousLayer = true;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::UnsupportedContinuousLayer);
input.unsupportedContinuousLayer = false;
input.annotationMutation = true;
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::AnnotationMutation);
}
TEST(Step114FastPathSafety, HiddenEmptyAndAnnotationSymbolsKeepPlacementProtection) {
for (const bool visible : {false, true}) {
for (const char* id : {"symbol-in-R", "symbol-in-C", "org.maplibre.annotations.points"}) {
const std::vector<StyleLayerDiagnostics> layers{
{0, "background", "background", true, std::nullopt, std::nullopt},
{1, id, "symbol", visible, "symbols", std::nullopt}};
EXPECT_TRUE(particleFastPathHasPlacementTransition(layers, true));
EXPECT_FALSE(particleFastPathHasPlacementTransition(layers, false));
auto input = fastPathInput();
input.hasPlacementTransition = particleFastPathHasPlacementTransition(layers, true);
EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::PlacementOrFade);
EXPECT_FALSE(renderSegmentPlacementGatePassed(true, true, 0.0f, false));
EXPECT_FALSE(renderSegmentPlacementGatePassed(true, false, 0.0f, true));
EXPECT_FALSE(renderSegmentPlacementGatePassed(true, false, 1.0f, false));
}
}
}
TEST(Step114FastPathSafety, SourceRevisionRequiresRegistrationAndAsyncCompletionInvalidatesBuild) {
RenderSegmentRevisionBook book;
const RenderSegmentDependencies dependency{{"local-geojson"}, {}, false};
EXPECT_FALSE(book.revisionFor(dependency));
const auto missing = assessRenderSegmentSources(dependency, [](const std::string&)
-> std::optional<RenderSegmentSourceStatus> { return std::nullopt; });
EXPECT_FALSE(missing.ready);
EXPECT_FALSE(missing.stable);
EXPECT_STREQ(missing.reason, "missing-source");
book.advanceSource("local-geojson");
const auto initial = book.revisionFor(dependency);
ASSERT_TRUE(initial);
RetainedUnderlayCache cache;
EXPECT_EQ(cache.evaluate(key(), *initial, true), RetainedUnderlayDecision::Build);
cache.markBuildSucceeded();
EXPECT_EQ(cache.evaluate(key(), *initial, true), RetainedUnderlayDecision::Reuse);
book.advanceSource("local-geojson"); // Immutable source data change.
const auto submitted = book.revisionFor(dependency);
ASSERT_TRUE(submitted);
EXPECT_NE(initial, submitted);
EXPECT_EQ(cache.evaluate(key(), *submitted, false, "source-not-loaded"), RetainedUnderlayDecision::Fallback);
EXPECT_FALSE(cache.isValid());
book.advanceSource("local-geojson"); // Models onTileChanged after asynchronous layout.
const auto completed = book.revisionFor(dependency);
ASSERT_TRUE(completed);
EXPECT_NE(submitted, completed);
EXPECT_EQ(cache.evaluate(key(), *completed, true), RetainedUnderlayDecision::Build);
cache.markBuildSucceeded();
EXPECT_EQ(cache.evaluate(key(), *completed, true), RetainedUnderlayDecision::Reuse);
book.advanceGlobal(); // Image/glyph/style/context change remains global.
EXPECT_NE(completed, book.revisionFor(dependency));
EXPECT_EQ(cache.evaluate(key(), *book.revisionFor(dependency), true), RetainedUnderlayDecision::Build);
}
TEST(ParticleFastPath, RevisionCameraAndViewportMismatchReject) {
auto input = fastPathInput();
input.revisionMatches = false;

View File

@@ -1144,3 +1144,14 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2`
- 2026-10-11 STEP-113 RDC:标准Android Validation APK构建exit=0;FZG1-2安装成功,五场景稳定Logcat摘要(Native层64/66/66/66/67)均decisionMismatch=0;STATIC_RC Full875/Fast0,PARTIAL_RCRR Full861/Fast0,发现cache-invalid外#22/#23同时阻断。完整113层、手动Source变化、性能和回归未验收,整体PARTIAL;详细证据追加于STEP与报告。
- 2026-10-11 STEP-113第二轮:FZG1-2已保存Source变更前后各120帧完整Snapshot,decisionMismatch=0;单次Source mutation=1,Native最后变更为step113-source-probe、影响C1;稳定119帧同时命中#3/#4/#22/#23。已确认内建annotation symbol进入C,且#22/#23可共享根因;Mac Native GTest因缺libuv未运行,NavSea完整113层/性能仍PARTIAL。详见STEP记录追加段落。
### 2026-10-11 STEP-114 Fast Path 安全准入与独立 Test App(源码交付 PARTIAL)
- 实际模型已核实 GPT-6.1-Sol(gpt-6.1-sol)/ high;Mac particle/main 起点 b02e54de3d45fcf612126ae8f037cba204f590ea。无模型切换、子代理、commit/push/发行。
- Shared Core 仅修正完整无 Symbol 场景被 controller stale 误挡的 #13 输入;隐藏/空/Annotation Symbol、空表/数量不一致继续保守。CustomMVTVector、非粒子 C、Symbol R Build/scene/资源/碰撞保护均保留 fail-closed。
- 回读旧 STEP-113 完整 JSON:navsea_delivery 实际是 Vector;R 依赖 tide-spots→tide 但 Native source 表缺 tide。缺源提前明确报 missing-source,不补假源、不放行缓存、不改原 PBF 五场景。
- 新增独立 FAST_SMOKE 与 Circle C/真实 Annotation/Symbol R 三个反向场景;只在独立空 Annotation 场景移除空内建 layer,使用 Native 真实 Full/Fast/Build/Reuse/24Gate,单次操作自动保存前后短延迟快照。新增3个非 Jest C++用例源码。
- 中文调查、场景/RDC精确验收命令、Step与完成报告已实际落盘;仅静态检查,未编译/测试/Gradle/ADB/Jest/设备。真实 Fast 未验收,Symbol R 结果证书 BLOCKED,不标 Fast Path 已修复。
- 下一步 RDC 独立审计/执行 Mac Native 定向检查;用户一次手工 Android/设备闭环并保存源码diff/制品SHA/逐帧/截图证据。详见 docs/报告/STEP-114_FastPath安全准入与TestApp闭环_Codex完成报告_v1.md 与 docs/60_测试/STEP-114_独立FastSmoke场景与RDC验收_v1.md。
- 2026-10-11 STEP-114 RDC:独立核对安全Gate改动、3项C++测试源码、Android Test App接线及STEP-113/102冻结SHA,git diff --check PASS。Mac缺libuv,单独C++ header检查因Xcode找不到标准头array环境阻塞;Android/真机/Fast未执行,PARTIAL,后续用户一次手工构建验收。