fix: guard STEP-113 empty annotation and expose PBF blockers
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
tei
2026-10-11 11:50:03 +09:00
parent e2d202b560
commit c48a72a9d4
14 changed files with 726 additions and 12 deletions

View File

@@ -0,0 +1,73 @@
# STEP-116 STEP-113 原 PBF Fast Path 阻塞安全修复审查 v1
日期:2026-10-11(JST)。结论:**部分实现(PARTIAL);空 Annotation 生命周期受控修复已落源码,缺源和 Symbol R 准入继续 BLOCKED。没有新的设备 Fast 证据。**
## 1. 当前事实与范围
仓库 `/Users/tei/dev/MapLibreParticleLab`,`particle/main@e2d202b560ad0165101546597c43f2ef0ccf4542`,Darwin 24.6.0 / x86_64。起点 tracked clean,只有 STEP-110/111 和本次 STEP-116 任务书未跟踪。版本仍 `13.6.1-particle.23`,不能用版本号相同推断 APK 源码相同。Integration main `80667d6c343411d03a760425438e3d292f452118` 为用户提供的 RDC 核验事实,本轮只读相关规范,没有 fetch 或跨仓写入。
已读取 AGENTS、PARTICLE_BASELINE、项目规范全部八文件、总则 README 和四份有效总则、STEP-116 任务及 STEP-112/113/114/115 当前记录/报告和相关源码;Integration AGENTS、README、WORKFLOW、TEAM_MODEL、contracts、rfc、adr、instructions、status、compatibility 只读。本轮不使用子代理,不修改公开头文件、公开 API/ABI、Host/RN/JNI 控制协议、NavSea 或公网 Style。
## 2. 缺源 R:证据成立,空依赖准入不实施
保留文件 `artifacts/maplibre-particle/android-validation/step113/source-change-after-full.json`,SHA-256:
`24a06fa1cb072a628c650723c58a880895ec656f4972fa55d3cb064bfb2aabda`
Native 原层 index=2、ID=`tide-spots`、type=`symbol`、source=`tide`。R[0,63) 依赖 `mapple/navsea_delivery/tide`。Native 实际 Source 表是 `mapple:raster`、`navsea_delivery:vector`、`org.maplibre.annotations:annotations`、`rn-weather-fields:raster-array`、`step113-source-probe:geojson`;没有 tide。该文件属于旧 SOURCE_CHANGE 完整窗口,不冒充本次 STATIC_RC 实测。原记录中 fallback=unknown-dependency;当前 STEP-114 代码已提前报 missing-source。
`RenderOrchestrator::retainedSegmentSourceState` 在查 revision 前先检查缺源及 CustomMVTVector;普通 Vector 不走 CustomMVTVector 拒绝分支。`renderSegmentDependencies` 不因隐藏或当前桶为空删依赖。`RenderSegmentRevisionBook::revisionFor` 对未登记来源返回空值,保留删除墓碑用于同名重建;墓碑存在不能代替当前 Source 的 readiness。
源码链路审查:
| 链路 | 当前实现与限制 |
|---|---|
| 零贡献候选 | createRenderTree 的 source/layer 匹配循环不会给缺源层生成 LayerRenderItem,updateList 默认 false;这支持“本轮没有对应新 RenderItem”,不等于全部旧 GPU 对象已完成禁用的证书 |
| 旧 GPU 生命周期 | RenderLayer::markLayerRenderable 调用 activateLayerGroup 提交 ChangeRequest;Symbol 还处理 collisionTileLayerGroup,layerRemoved 停用两类 group。CPU 状态、待处理 requests、旧 buckets 与实际执行时点需要一起核验;无 source 的循环边界也不能只靠 bool 推断 |
| Source 创建/删除/同名恢复 | createRenderTree 的 sourceDiff 递增全局内容及对应 source revision;prepare 输入的 immutable sources 身份变化使 NonCameraSignature 不匹配,必须先 Full |
| 同名更新/异步完成 | changed sourceDiff、onTileChanged → TileMutation → 内容/source revision + observer invalidate;feature-state 同样走 source 变更,不能按 HTTP 下载或 loaded 时间代替 |
| 全局输入 | layerDiff / 层序、imageDiff、Glyph URL/fontFaces、onGlyphsLoaded、Sprite/ImageMissing、context/style cleanup 保留原失效链 |
因此本轮**没有把 missing-source 当 empty**,没有登记虚构 tide revision,没有补假源或删除 tide-spots。后续需要“该 R 在成功 Full 中实际参与 GPU 绘制的 group/碰撞 group 已排除、无待应用资源变化”的有界证据,以及 source 出现/恢复后下一次尝试必定 Full 的回归。当前仅 Source 表缺失不足以承担这项新准入契约。服务器正式 Style 缺源仍是 BLOCKED。
本轮只在现有诊断 JSON 的每段增加可选 `missingSourceIds`,由同次 snapshot 的依赖和 Source 表计算,包含缺失来源的具体 ID。查询不写 revision、不构造缓存、不修改首错和 24 Gate。
## 3. 空 Annotation C:局部修复与保护
旧 STATIC_RC Native 66 层:原63层 + weather-color + raster-particle + 自动 points Symbol。C 从 rn-weather-color 开始,普通 Symbol 不支持 particle dynamic range,阻挡 #22,并影响聚合 #23。SOURCE_CHANGE 的额外 circle 也必须保留。
仅在 Step113PbfGateScene 的 STATIC_RC 第二次 Style 加载完成时执行一次空层调整:
1. 生产基础 ID 不含 points,生产Sources不得占用org.maplibre.annotations;SDK `map.annotations` 必须为空;实际层必须是 SymbolLayer、sourceId 为 org.maplibre.annotations,内部 Source 必须存在。来源由本次原始JSON无内部ID、Native加载路径与实际层共同核对,避免把生产同名Source误当空SDK来源。
2. 使用 `Style.removeLayer(Layer)` 对象重载,保留可重加的 wrapper;Source 保留,原63层定义/顺序及生产 Sources 原样。
3. 记录 `emptyBuiltinAnnotationLayerRemoved`、实例数0、Source保留、移除层ID/作用范围,保存 productionSources;poll/export 继续使用真实 Native 计数。静态预期正常 STATIC_RC 65 层,不能当实际计数。
4. 不周期删除任何层。后续普通 Style.addLayer 保持原行为,native immutable layer/signature/topology 变化继续 Full;Style 更换后不对新 Style 再做移除。
5. stop 在相同 Style 且该 ID 仍缺失时重加原 wrapper,已有同名层不覆盖。Style 身份改变或 Annotation/points 重新出现时,3秒 poll 撤销场景验收条件并报 BLOCKED;poll 是报告保护,不承担恢复可见内容的时序保证。
关键修复位于内部 `src/mln/annotation/annotation_manager.cpp/.hpp`:原 updateStyle 只在 Source 不存在时创建 points,Source 保留而层被删除会让后续 Marker 无绘制层。提取私有 ensurePointLayer,独立检查 Source 和层;首次添加 SymbolAnnotation、更新已有 SymbolAnnotation、Style loaded 三个入口修复缺失层。更新没有 geometry/icon 变化时也把 dirty 置真。已有同名 Layer 不被覆盖。
时序:Map::addAnnotation / updateAnnotation → AnnotationManager 修复 Style layer vector → Map::Impl::onUpdate 捕获新的 immutable layers → Fast NonCameraSignature/annotationMutation 拒绝旧树 → Full 执行正常 Annotation 渲染。恢复不依赖3秒poll,也没有在 renderer thread 的 updateData 内重写 Style。Shape Annotation 原接口/图层仍按原逻辑处理,碰撞、Fade、Glyph/Image 和 C 判断完全保留。
风险:这是 Shared Core 内部生命周期修复,所有后端共用;需 RDC 编译并跑真实图像用例,确认无锁/重入问题及旧 Annotation 行为回归。用户主动提供同名层时尊重其语义;若其配置本身无法绘制,该情况不被当作 Test App 空层安全条件。恢复失败在日志保留,不写成功。
## 4. Symbol R:首次 Build 仍 BLOCKED
已读 PlacementController、Placement::placeLayers/commit/symbolFadeChange、RenderTreeImpl::prepare、SymbolBucket::updateVertices、renderer Full 准入和 scene 聚合。
| Continuous Full 分支 | 当前 Symbol R gate |
|---|---|
| placement 不 recent | placementChanged=true;placeLayers 后 commit、setPlacement,changed 本身阻断 |
| placement recent | setPlacementStale;hasTransitions 返回 true,阻断 |
| transitions 关闭 | placementIsRecent=false,Full changed=true;symbolFadeChange 返回1,不是 settled=0 |
RenderTreeImpl::prepare 在 bucket updateVertices 中更新 opacity/dynamic attributes,并置 uploaded/dynamicUploaded/placementChangesUploaded 等状态。Placement commit 只提交 CPU placement/opacity 结果,不证明当前 R 的最终 GPU 输入一致。per-R `renderSegmentPlacementGatePassed` 和 `retainedSegmentIsStable`,全局 retainedPlacementStable / retainedSupported / preparedSceneSupported 均保留。隐藏/缺源/当前 placementData 为空的 Symbol 仍结构上具备 Placement 能力。
**需要的额外设计证据**:成功 Full 对应的 GPU 上传/绘制完成代次、R 使用的 bucket/tiles/跨瓦片ID与排序、跨Source碰撞与所有参与碰撞层的 Placement结果、text/icon opacity和dynamic vertices、atlas/glyph/image代次、Style/Source/layer identities与revision、camera/projection/viewport/pixelRatio、uniform fade及debug/3D条件、待处理requests/异步变更纪元。证书必须绑定一次成功 Cache Build,变化即 invalid/Full,并对 GL/Metal 验证等价及 shadow。现有代码没有完整证书,本轮不做跨层重写,不用毫秒阈值、loaded、无手势或counter装配代替。
## 5. 测试源码与结论边界
新增四个 `Step116Safety.*` C++ 用例:缺源/Source加入未loaded/异步revision/删除墓碑和恢复;JSON缺源审计与Placement并存、查询无副作用;Symbol两个Continuous分支及关闭transition保护、无变化/Source与全局资源revision;Annotation/层序/disable/reload多失败 shadow 全集。revision用例验证的是账本契约,不伪造实际 Glyph/Tile 回调或 GPU完成。
新增三个 `Annotations.Step116*` 用例:空points移除后真实添加在render前恢复并比对既有point_annotation图像;已有Marker no-op update恢复且出图;已有同名层不覆盖、reload恢复且出图。这些是测试源码,全部 NOT RUN;设备的annotation/Style、async mutation、GL/Metal回退还须独立验收。
本轮实现后预期消除原 STATIC_RC 的空内部 C Symbol;无法承诺 #23 清除,也无法承诺 R Build/Fast>0。旧快照、AST或diff检查均不构成新制品安全、像素或性能 PASS。

View File

@@ -0,0 +1,26 @@
# STEP-116:STEP-113 原 PBF Fast Path 实际阻塞修复
## 授权、当前基线与配置
用户要求继续修复,并以原 STEP-113 专用 Android APP 的 STATIC_RC 真正进入 Fast 为目标,而非 STEP-114 简化 FAST_SMOKE。当前仓库 Mac /Users/tei/dev/MapLibreParticleLab,particle/main HEAD e2d202b560ad0165101546597c43f2ef0ccf4542。Integration 已由RDC fetch 并核实 main 80667d6c343411d03a760425438e3d292f452118。仅内部 Shared Core 安全修复与 Android 专用 Test App;不改跨平台公开 API/ABI/协议,不改 NavSea、不发行。
Codex 模型必须为 GPT-6.1-Sol High (gpt-6.1-sol / model_reasoning_effort=high)。先读根AGENTS.md、PARTICLE_BASELINE.md、docs/00_项目规范、当前 docs/00_项目总则 README及四份有效总则,读 STEP-112/113/114/115、Integration instructions/README/WORKFLOW/TEAM_MODEL/contracts/rfc/adr/status/compatibility,按事实和边界执行。Codex只编辑、静态检查、中文报告;不可执行Native/Gradle大编译、ADB、安装、Jest、发版、commit/push。Native独立审计由RDC;Android编译安装由用户手工集中一次完成。
## 已确定的真正阻塞
STEP-113 STATIC_RC:正式63个PBF基础层,Test App Native实际66层;R[0,63) Build0、Fast0、Source fallback unknown-dependency,C[63,66)中有自动添加的 org.maplibre.annotations.points Symbol,#22/#23持续失败。后续 STEP-114 源码/快照发现R依赖mapple、navsea_delivery和tide,Native source表实际没有tide,tide-spots Symbol层引用它;navsea_delivery实际是Vector,不是CustomMVT。即使R的missing-source得到解决,Symbol R首次cache Build仍被Placement的changed/stale/fade保护阻挡。不可把去掉一个Gate误写为全套修好。
## 工作:真正解决而非假通
1. 通过静态源码和保留的 STEP-113完整JSON逐环确认R缺源的原层及fallback、真正的 SourceType、source/Style变化导致的revision与GPU生命周期。
2. R的缺失源:只有能证明缺失源永远贡献零像素,且后续Source创建/恢复/同名更新强制缓存失效/Full重新处理时,才允许把它作为有界的empty依赖;否则保留fail-closed并明确报告服务器正式Style存在缺源而无法安全缓存。不能偷偷创建假的tide Source或删除原始tide-spots层。
3. C的内部Annotation Symbol:可以在专用 Test App 确认地图无任何Annotation实例时,参照 STEP-114 仅移除 SDK 自动创建的空内部 Annotation 层,保留原正式63层定义、Source/Layer顺序不变;必须有代码防止遗留Annotation/Style添加时漏内容,并在日志、manifest明确记录原STYLE层原样、仅内部辅助层调整。其他已有原场景继续原语义,不为了Fast删除SOURCE_CHANGE中的测试circle。未经证明不能改Core C准入逻辑。
4. Symbol R初建:必须读PlacementController、onRender、commit/fade、renderSegmentPlacementGatePassed、retainedRangeIsStable和full-frame时的状态。真正放宽必须有结果等价的Placement/atlas/glyph/tile/style/source/camera/opacity证据,并在变更时立即invalid/full;不能仅凭stable多少毫秒、场景没有手动操作、tile加载完成来放行。若现有Core没有可核实的已提交GPU结果签名,禁止危险跨层重写,列为BLOCKED并说明需要的精确额外数据/设计。
5. 要保留24Gate shadow mismatch=0与实际Cache Build/Reuse/Full/Fast原生计数。可添加同帧诊断或快速失败细项,便于用户一次Android build后通过原STATIC_RC脚本迅速定位仍失败的Gate。保留PBF63、PARTICLE_OFF、PARTIAL_RCRR、SOURCE_CHANGE五场景、STEP114独立Smoke和STEP102原场景;不要改变公网PBF Style或NavSea,也不要求精确113层。
6. 增加与安全修复对应的非Jest C++源码测试,含缺源/源加入/async mutation、Annotation、Symbol R /全局变化/无变化、Style/Source/Tile/Glyph/Image以及Layer顺序/disable/reload;不要假造rendered Fast、不要把不运行测试记PASS。
## 交付文件,全部本轮必须实际生成
- 本轮实际源码和适当非Jest测试源码;修改Test App后更新必要README。
- docs/20_调查/STEP-116_STEP113原PBF_FastPath阻塞安全修复审查_v1.md:每条阻塞证据/解决方案/风险/未解项。
- docs/50_Step记录/STEP-116_STEP113原PBF_FastPath实际修复_v1.md:真实完成度及未运行项目。
- docs/60_测试/STEP-116_STEP113原PBF专用APP验收_v1.md:严格按最新HEAD一次用户手工Android build/install→STEP115 preflight→原STEP113 STATIC_RC 75s以上→完整120帧/24Gate/源码/设备SHA/用户画面;失败后如何判断R/C/Symbol,不得依赖旧APK。
- docs/报告/STEP-116_STEP113原PBF_FastPath实际修复_Codex完成报告_v1.md:实际模型全名档位、改动清单、公共API(必须不变)、逐项Implemented/Blocked/Not Run、Git/源码证据、RDC具体命令及用户构建/设备步骤。
- 粒子开发记录.md追加中文结果与下一步;必要README与相关已有文档说明保持一致。
- Codex运行git diff --check、静态核查;不运行Native编译、Android编译、Jest、ADB、安装、Git commit/push;本轮用户原有两个未跟踪STEP-110/111 MD一律保护。禁止git add -A/.、reset --hard、clean。完成后停止由RDC独立审计并scoped提交推送;若Fast尚未实测只能PARTIAL归档,不写Closed/PASS。

View File

@@ -0,0 +1,54 @@
# STEP-116 STEP-113 原 PBF Fast Path 实际修复 v1
日期:2026-10-11(JST)。状态:**部分实现(PARTIAL),停止于源码交付,待 RDC 独立审计。**
## 当前基线
- Mac / Darwin 24.6.0 / x86_64;`/Users/tei/dev/MapLibreParticleLab`。
- branch `particle/main`,起点 HEAD `e2d202b560ad0165101546597c43f2ef0ccf4542`,本轮不提交,HEAD 不变。
- 实际模型 GPT-6.1-Sol(gpt-6.1-sol)/ high,从当前 rollout 的 turn_context 核实;没有切换或第二实例。
- Native Android 版本 `13.6.1-particle.23`,没有发版;Integration main 80667d6c 为用户给定 RDC 核验,相关文档只读。
## 交付与未完成
| 工作 | 状态 | 事实 |
|---|---|---|
| 强制前置/实际代码/旧完整JSON调查 | 已完成 | 原R缺 tide,navsea_delivery 为 Vector;GPU/source生命周期见审查文档 |
| 空Annotation生命周期 | 已实现,未验收 | Native add/update/style-loaded 恢复缺失points;Test App仅STATIC_RC一次移除空辅助层,保留Source,退出恢复,后续变更撤销验收条件 |
| 原63层/PBF资源/五场景 | 已保留源码语义 | 不改公网或tide-spots;SOURCE_CHANGE circle保留;STEP114/102源码冻结 |
| 缺源empty准入 | BLOCKED | 缺GPU零贡献证书;继续missing-source,不补假源 |
| Symbol首次Cache Build | BLOCKED | 无绑定成功Build的Placement/GPU最终输入证书,现有Gate全部保留 |
| 诊断与复测工具 | 已实现,未运行 | JSON缺源ID +独立Placement摘要;75秒实际等待、源码锁定/生命周期/blocked/24Gate拒绝条件 |
| 非Jest C++测试源码 | 已实现,NOT RUN | 四个Step116Safety、三个Annotations.Step116;真实图像检查交RDC |
| 四份中文文档/README/开发记录 | 已生成 | 具体路径见完成报告 |
| 真正Fast/像素/性能 | NOT VERIFIED | 没有新APK、设备计数、截图或FPS/CPU结论 |
## 检查与收尾
Codex仅执行阅读、Git只读状态/diff、旧JSON与冻结文件SHA、Python AST解析、git diff --check及文档静态检查。无 CMake/Ninja/Bazel/Native编译或测试、Gradle/ADB/安装/Jest、commit/push/发布;没有改构建流水线、公开头文件/API/ABI/Host控制协议。
保护 STEP-110/111 未跟踪任务书。下一步 RDC 按 `docs/60_测试/STEP-116_STEP113原PBF专用APP验收_v1.md` 独立审计并锁定提交后的源码,用户集中一次手工 build/install,再执行 STEP115 preflight、原 STATIC_RC ≥75秒、完整120帧和24Gate、SHA及画面。不得使用旧APK或旧测试runner给本轮记PASS。
## RDC 后续结果填写(本轮尚未执行)
- 审计后的源码HEAD/精确diff/SHA:待填写。
- Native配置/编译/定向测试及首错/日志:NOT RUN。
- 最新用户构建APK SHA、build-info、设备安装SHA:NOT RUN。
- STATIC_RC实际层数/空层调整/原63层manifest/Source:NOT VERIFIED。
- Cache Build/Reuse、Full/Fast增量、120帧/24Gate/mismatch:NOT VERIFIED。
- Annotation恢复、Source/async/Style/资源/层序/disable/reload回退及用户画面:NOT VERIFIED。
- 最终阶段结论:当前PARTIAL;缺源与Symbol仍BLOCKED。
## RDC 独立核验追加(2026-10-11)
**本轮结论:限定范围源码/接口及操作说明静态审查完成;Mac Native配置 BLOCKED;Android APK/设备Fast NOT RUN,整体PARTIAL。**
- 起点粒子仓库 particle/main=e2d202b560ad0165101546597c43f2ef0ccf4542;Integration origin/main已由RDC fetch并与本地main同步,HEAD=80667d6c343411d03a760425438e3d292f452118。未接触NavSea与Integration正式文件。
- Codex实际模型 gpt-6.1-sol/high;本轮实现仅原STEP-113 STATIC_RC 的 SDK 空Annotation辅助层、Native AnnotationManager缺层恢复、缺源只读JSON诊断和测试验证器,不改变正式63个PBF layer、原五场景其他分支、Fast Gate/caching的准入或公开控制面。新增7项非Jest C++用例源码,尚未执行。
- RDC直接核对 Android Style.java 对象removeLayer(Layer)文档明确注明被移除Layer可以重新添加;与STEP116在stop中恢复原wrapper的目的相符。Native AnnotationManager恢复与事件生命周期仍须编译、图像和设备验证,不凭静态阅读标PASS。
- RDC核对旧SOURCE_CHANGE实际完整JSON:Native FastGate gates每项有order,firstFrame的knownMask等字段存在;STEP116验收脚本的原JSON结构检查有数据来源,但并非本轮运行证据。
- Mac shell当前无rg可执行,RDC将STEP116手动导出命令中的rg替换为标准grep -E,保留同样的文件筛选表达式。Python验收器ast.parse通过,git diff --check通过。
- Mac Native按当前标准命令重新执行真正的cmake --preset macos-metal配置(使用/Users/tei/Library/Android/sdk/cmake/3.31.6/bin/cmake),退出码1:Required package libuv缺失,日志保存artifacts/maplibre-particle/step116-native/configure.log。严格串行停止,未执行构建目标、未运行7项C++测试。失败是环境依赖,不应改写成代码编译通过/失败或跳过继续。
- 未执行Android Gradle、APK安装、ADB启动STEP113、新Fast计数、逐帧画面、iOS Metal回归、发行/tag/Source Lock。正式PBF缺tide以及Symbol R Placement仍保留fail-closed;此任务只是安全部分实现,并不解决原STATIC_RC无法进入Fast的全部原因。
- 用户在本轮源码scoped Git锁定且push远端核验后,需手工一次集中Android编译与install -r,再执行STEP115同HEAD/安装SHA预检、原STATIC_RC 90秒、完整120帧+24Gate+截图与其它负场景验收。新APK前禁止把旧c6d13e1产物Fast0作为本轮修复验收。若仍Fast0,必须保留缺源、Placement和C实际结果并交跨平台Core负责人确定GPU结果证书。

View File

@@ -0,0 +1,151 @@
# STEP-116 STEP-113 原 PBF 专用 APP 验收 v1
日期:2026-10-11(JST)。**以下命令交 RDC / 用户执行,Codex 本轮全部未运行。** 本轮源码状态 PARTIAL;缺 tide 与 Symbol R 准入仍阻塞,预期仍可能 Fast=0。不得拿 STEP114 Smoke 或旧 V23 APK 替代原 STATIC_RC。
## 1. RDC 源码锁定和 Native 定向验证
先重新读取 AGENTS、当前基线、STEP-116 审查/报告与 diff。审计包含 Native Annotation 修复的线程/生命周期、原63层逐层定义/顺序、移除对象重载、stop恢复、后续marker/style添加、五场景分支、生产Gate未变和七项新测试。执行只读命令:
```bash
cd /Users/tei/dev/MapLibreParticleLab
git branch --show-current
git rev-parse HEAD
git status --short
git diff --check
git diff -- src/mln/annotation/annotation_manager.cpp src/mln/annotation/annotation_manager.hpp src/mln/renderer/render_segment_json.hpp test/api/annotations.test.cpp test/renderer/retained_underlay_cache.test.cpp platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step113PbfGateScene.kt tools/particle_android_validation/verify_step113_fast_path.py tools/particle_android_validation/README.md 粒子开发记录.md
shasum -a 256 docs/40_任务/STEP-110_R0_Placement跨段归因只读调查_Codex任务_v1.md docs/40_任务/STEP-111_R段Placement结果等价与独立刷新可行性只读调查_Codex任务_v1.md
```
两任务书 SHA 应分别为 `33037df71b4b30aa7956d59f4b99f77af3085405b24111bc0fe11ec857a93db0`、`f3a5806f25384bf46055db2edfea0d21bfbc4124184bc813b46e38b8079c1ec4`。不要暂存它们。RDC按获授权的scoped流程收尾后,记录真正的最新HEAD;本报告起点 e2d202b 并不包含本轮修改。构建前 tracked 必须 clean;原未跟踪任务书可保留,build-info 的 git_dirty 不能单独用来推断源码不一致。
Native 定向命令逐项执行,上一项失败立即停止,保存完整 stdout/stderr 与首个真实 error;历史缺 libuv 不当成新结果,不运行旧runner:
```bash
cmake --preset macos-metal
```
```bash
cmake --build build-macos-metal --target mbgl-test-runner --parallel 3
```
```bash
build-macos-metal/mbgl-test-runner --gtest_filter='Step116Safety.*:Annotations.*:FastGateTrace.*:ParticleFastPath.*:RenderSegmentDiagnostics.*:RenderSegmentPlacementFree.*:RenderSegmentLocality.*:RenderSegmentPlan.*' --gtest_color=no
```
Annotations.Step116 三用例使用现有 point_annotation 图像做实际渲染比较;不能仅检查 getLayer 指针就声称像素 PASS。账本/谓词用例不构成 GPU rendered Fast,Metal host 成功也不代替 Android OpenGL/设备回退。若依赖/编译阻塞,保持现场并在报告标 BLOCKED/NOT RUN,Android集中构建是否继续由用户安排。
## 2. 用户一次手工 Android 构建与安装
在锁定的最新 HEAD 上,用户手工依序执行,Codex/RDC不得代用户启动 Android 构建或安装:
```bash
cd /Users/tei/dev/MapLibreParticleLab
git rev-parse HEAD
git status --short
./tools/particle_android_validation/build_validation_apk.sh
```
标准脚本使用 Java17、OpenGL/x86_64、strip native debug symbols,保存 build-info/build.log,检查 ZIP/ABI/体积/SHA。失败不得续用原路径旧 APK。
```bash
cat artifacts/maplibre-particle/android-validation/build-info.txt
shasum -a 256 artifacts/maplibre-particle/android-validation/MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk
/Users/tei/Library/Android/sdk/platform-tools/adb -s 192.168.200.139:5555 install -r artifacts/maplibre-particle/android-validation/MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk
```
记录源码HEAD、branch、精确修改文件/SHA、构建日志、APK大小/ABI/SHA、设备序号及型号。不能仅凭 `13.6.1-particle.23` 或安装 Success 认定是新源码。
## 3. STEP115 preflight → 原 STATIC_RC ≥75秒
用户安装成功后,由 RDC / 用户执行,serial/adb路径明确:
```bash
python3 -B tools/particle_android_validation/verify_step113_fast_path.py --adb /Users/tei/Library/Android/sdk/platform-tools/adb --serial 192.168.200.139:5555 --preflight-only
```
preflight 检查当前 tracked clean、build-info.commit==HEAD、APK SHA==build-info.sha256、设备 ABI x86_64、安装包 SHA==本地 APK。STALE_APK / SOURCE_NOT_LOCKED / SHA失败时停止,必须先纠正版本。PREFLIGHT_PASS只代表版本一致。
```bash
python3 -B tools/particle_android_validation/verify_step113_fast_path.py --adb /Users/tei/Library/Android/sdk/platform-tools/adb --serial 192.168.200.139:5555 --scenario STATIC_RC --seconds 90
```
使用90秒,脚本在取得 PID 后实际等待完整90秒(≥75秒);加载太慢导致摘要不足则 INSUFFICIENT_SAMPLES / NO_READY_SUMMARY,不推断Fast。保留 result.json、logcat.txt、launch.txt、实际观察秒数。摘要须同sceneGeneration/lifecycle、planValid/basePreserved、24项、mismatch=0、累计计数单调;独立blocked日志和摘要撤销不能被早先Fast盖过。退出2为没有可接受Fast证据,退出3为版本/环境等阻塞,不能改成0制造PASS。
`FAST_OBSERVED`仅指真实 Native fastRendered 增量>0,不是安全/像素/性能终验。要求原63层manifest、NavSea原PBF/Sprite/Glyph/Vector URI及定义/顺序不变,仅声明SDK空辅助层调整;正常静态预期Native65层/C仅weather-color和raster-particle,实际以新导出为准。PUBLIC PBF若变成非63层,场景保留现场并 BLOCKED,不更换简化Style来通过。
## 4. 手动导出完整120帧及设备画面
保持同次 STATIC_RC 运行,用户在 STEP093 → STEP113 菜单选择 EXPORT LAST SNAPSHOT。这是最近一次3秒共享采样,不额外渲染。以下由 RDC / 用户回读,必须核对所选文件确属本次新运行:
```bash
mkdir -p artifacts/maplibre-particle/android-validation/step116
step116_file="$(/Users/tei/Library/Android/sdk/platform-tools/adb -s 192.168.200.139:5555 shell run-as org.maplibre.android.testapp ls -t files | tr -d '\r' | grep -E '^step113-STATIC_RC-.*[.]json$' | head -n 1)"
test -n "$step116_file"
/Users/tei/Library/Android/sdk/platform-tools/adb -s 192.168.200.139:5555 exec-out run-as org.maplibre.android.testapp cat "files/$step116_file" > artifacts/maplibre-particle/android-validation/step116/static-rc-full.json
shasum -a 256 artifacts/maplibre-particle/android-validation/step116/static-rc-full.json
/Users/tei/Library/Android/sdk/platform-tools/adb -s 192.168.200.139:5555 exec-out screencap -p > artifacts/maplibre-particle/android-validation/step116/static-rc-screen.png
/Users/tei/Library/Android/sdk/platform-tools/adb -s 192.168.200.139:5555 logcat -b crash -d > artifacts/maplibre-particle/android-validation/step116/crash.log
```
`test -n`失败立即停止后续命令。同一个Full JSON先做离线证据结构检查(不是安全测试):
```bash
python3 -B - <<'PY'
import json
from pathlib import Path
p = Path('artifacts/maplibre-particle/android-validation/step116/static-rc-full.json')
v = json.loads(p.read_text())
assert v['scenario'] == 'STATIC_RC' and v['sceneReady'] and not v['blockedReason']
assert v['commands']['emptyBuiltinAnnotationLayerRemoved'] is True
assert v['commands']['annotationInstancesAtRemoval'] == 0
assert v['commands']['annotationSourcePreserved'] is True
assert v['styleParity']['baseLayerCount'] == 63 and v['styleParity']['baseLayersPreserved']
assert not v['styleParity']['full113Equivalent']
n = v['native']; t = n['fastGateTrace']; frames = t['frames']
assert n['valid'] and len(frames) == t['windowCount'] == 120
assert len(t['gates']) == 24 and t['decisionMismatch'] == 0
assert [g['order'] for g in t['gates']] == list(range(1, 25))
assert all(f['knownMask'] == 0xffffff and f['decisionConsistent'] and len(f['gateStatus']) == 24 for f in frames)
assert all(a['sequence'] < b['sequence'] for a, b in zip(frames, frames[1:]))
complete = [f for f in frames if f['completed']]
assert complete and all(f['post'] is not None for f in complete)
print('结构完整;仍须独立核对源码/制品SHA、同次generation/lifecycle及用户画面')
print('生命周期 Full/Fast:', t['fullRendered'], t['fastRendered'])
print('窗口 completed/Full/Fast:', len(complete), t['windowFullRendered'], t['windowFastRendered'])
for s in n['segments']:
if s['strategy'] == 'RETAINED':
print('R', s['beginIndex'], s['endExclusive'], s['missingSourceIds'],
s['placementGatePassed'], s['cache'])
print('窗口失败项:', [(g['order'], g['reason'], g['windowFailures']) for g in t['gates'] if g['windowFailures']])
PY
```
允许最后一帧 pending,但不能把其post作为当前Full。pre为上次完成树、post/full为本次完成树,避免跨帧拼状态。环形窗口可能覆盖Mutation瞬间,必须变更前/后分别及时手动导出,若没捕获则如实 NOT VERIFIED。120条帧不等于120条completed。
用户确认基础PBF/标注/天气色带/粒子可见、无漏内容/闪烁/层序错误;截图归档及用户判断不能代替同条件Full/Fast像素比较。当前缺源/Placement未解时应记录 Fast0 和每项失败,不宣布性能提升。
## 5. 失败定位与受保护回归
| 结果 | 判断与下一步 |
|---|---|
| missingSourceIds含tide,R Build0 | 正式Style缺源仍fail-closed;不补假源、不删tide-spots,记录Source类型/缺失层ID |
| 缺源消失但Symbol placementGatePassed=false | 独立Symbol准入阻塞,检查Full changed/stale/fade与pre/post;不要用等候时间放行 |
| #22仍失败 | 查Native C范围实际层清单;空层调整是否被撤销/失败,普通circle或真实Annotation仍应阻挡 |
| #23仍失败 | 查retainedSupported和preparedScene聚合,消除#22不保证#23通过 |
| mismatch>0 / unknown Gate /版本不符 | 停止记录通过,保留首错和完整证据 |
| Fast>0 | 先核查没有缺源/场景撤销、原Style完整,继续资源变化/回退/画面对照;仅记FAST_OBSERVED |
原五场景仍在。每项独立运行并保留导出,不连续启动冒充采样:
```bash
python3 -B tools/particle_android_validation/verify_step113_fast_path.py --scenario PBF63 --seconds 90
python3 -B tools/particle_android_validation/verify_step113_fast_path.py --scenario PARTICLE_OFF --seconds 90
python3 -B tools/particle_android_validation/verify_step113_fast_path.py --scenario PARTIAL_RCRR --seconds 90
python3 -B tools/particle_android_validation/verify_step113_fast_path.py --scenario SOURCE_CHANGE --seconds 90
```
这些场景可能预期Fast0,脚本退出2是受保护负例结果,不自动视为回归失败或改预期。SOURCE_CHANGE保留circle与Annotation;用户执行一次原 MUTATE PROBE SOURCE ONCE,导出前后commands和native lastMutation/affectedSegmentIndices,区分同时发生的真实瓦片更新。
Native七项新测试之外,还需 RDC/用户记录:遗留Annotation拒绝移除;STATIC_RC后添加真实Marker(同步恢复points再Full、像素可见),no-op更新;额外Style.addLayer/重排不删除新内容;disable、退出到STEP102、Style reload及后台恢复;Source创建/同名更新/async Tile、Glyph/Image变化必定invalid/Full。Test App没有新增自动Mutation来假装覆盖这些事件;若当前UI无法触发,标该项设备NOT VERIFIED并使用对应Native真实测试/另行授权的操作。STEP114四场景回归使用其既有命令;Smoke结果只归其独立场景。
最终提交证据应包含最新HEAD/精确源码diff、Native日志/首错、用户build-info/build.log/APK及安装SHA、设备信息、原STATIC_RC摘要和完整JSON、manifest、24Gate/真实Build/Reuse/Full/Fast增量、回退与画面。编译成功、preflight成功或静态检查都不把STEP-116判为CLOSED/PASS。

View File

@@ -0,0 +1,112 @@
# STEP-116 STEP-113 原 PBF Fast Path 实际修复 Codex 完成报告 v1
日期:2026-10-11(JST)。**源码交付 PARTIAL;原 STATIC_RC Fast 未实测,缺 tide 与 Symbol R 准入仍 BLOCKED。已按授权完成可控修改后停止,交 RDC 独立审计。**
## 1. 实际模型与工作区
- 实际模型全名 **GPT-6.1-Sol(gpt-6.1-sol)/ high**。读取本地会话 `01a1286a-d6a4-7e32-b91a-a9f3e63d8f3e` rollout 的 `turn_context.model/effort` 得到 gpt-6.1-sol/high;未切换模型、无子代理或第二Codex进程。
- 机器 Darwin 24.6.0 / x86_64;仓库 `/Users/tei/dev/MapLibreParticleLab`,branch `particle/main`。
- 起点/交付HEAD:`e2d202b560ad0165101546597c43f2ef0ccf4542`,未commit/push,未回读远端HEAD。起点tracked clean,STEP-110/111/116任务书未跟踪;子模块状态正常,未操作。
- Android Native版本仍 `13.6.1-particle.23`。没有新APK/AAR,不能使用原STEP113 APK代表此次源码。
- Integration main `80667d6c343411d03a760425438e3d292f452118` 采用用户给定RDC事实,只读AGENTS/规范/contract/coordination;没有NavSea/Integration写入、同步或发行。
- 已读强制前置:根AGENTS/PARTICLE_BASELINE、全部项目规范、总则README及四份有效总则、STEP112/113/114/115现行交付及STEP116任务、相关实际实现和保留完整JSON。详细范围见调查文件。
## 2. 已实现 / 阻塞 / 未运行
| 项目 | 状态 | 结果 |
|---|---|---|
| 空SDK Annotation C | 已实现(Implemented),未验收 | STATIC_RC确认零实例/内部身份后仅移除一次points,保留Source、原63层;stop恢复,后续添加/Style替换撤销验收条件 |
| 后续真实Annotation不漏内容 | 已实现,未验收 | 私有ensurePointLayer在Native Symbol add/update及style-loaded同步恢复缺层;no-op update也dirty,在Map::onUpdate采样前完成,poll只报告,不承担恢复时序 |
| 缺源R | 阻塞(Blocked) | tide-spots依赖不存在tide;缺已执行GPU零贡献证书,不实现empty准入,维持missing-source |
| Symbol R Build | 阻塞 | Continuous重算changed、不重算stale两分支均阻断;无最终GPU/Placement/atlas等证书,不放宽Gate |
| 诊断/原生计数 | 已实现 | 可选missingSourceIds、独立Placement状态、真实Fast UI;24Gate及生产首错/计数不改 |
| STEP115复测工具 | 已实现,未运行 | tracked源码锁定;PID后实际≥75秒等待;同生命周期/24Gate/blocked撤销/计数reset拒绝,仍校验APK与设备SHA |
| C++测试源码 | 已实现,未运行(Not Run) | 四个Step116Safety账本/诊断/保守Gate用例,三个Annotations.Step116真实渲染回归 |
| 真正原PBF Fast、视觉/性能 | 无法验证 | 没有新构建、设备120帧或Fast增长,不写Closed/PASS |
未删除tide-spots、SOURCE_CHANGE circle、任何正式Symbol;没有改FPS/forecast/原PBF URI、Core C准入、Placement算法、cache成功计数或制造Fast。STEP114独立Smoke和STEP102 fixture未修改。非STATIC_RC四场景保留原Annotation语义;所有场景共享的Native Annotation修复仅在缺Source/points时补回,已有Layer不替换。
## 3. 实际文件清单
修改的源码/测试/工具:
1. `src/mln/annotation/annotation_manager.cpp`:缺失points的同步恢复与no-op更新dirty。
2. `src/mln/annotation/annotation_manager.hpp`:仅私有ensurePointLayer方法,无数据布局变化。
3. `src/mln/renderer/render_segment_json.hpp`:同snapshot的每段missingSourceIds只读诊断。
4. `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step113PbfGateScene.kt`:STATIC_RC空辅助层一次调整、恢复/撤销、manifest及独立阻塞摘要。
5. `test/api/annotations.test.cpp`:三项实际出图/恢复/不覆盖/reload测试源码。
6. `test/renderer/retained_underlay_cache.test.cpp`:四项缺源/async revision/资源全局/保守Symbol/多Gate诊断测试源码。
7. `tools/particle_android_validation/verify_step113_fast_path.py`:时长和拒绝条件;本轮没有执行脚本。
8. `tools/particle_android_validation/README.md`:更新当前复测边界并追加STEP116说明。
9. `粒子开发记录.md`:仅末尾追加本Step。
新增四份中文文档(不覆盖历史版本):
1. `docs/20_调查/STEP-116_STEP113原PBF_FastPath阻塞安全修复审查_v1.md`。
2. `docs/50_Step记录/STEP-116_STEP113原PBF_FastPath实际修复_v1.md`。
3. `docs/60_测试/STEP-116_STEP113原PBF专用APP验收_v1.md`。
4. 本报告。
**公开API/ABI与控制协议不变**:无include公开头、C/Java/Kotlin/RN/JNI方法签名、公共配置、版本/发行坐标变更;新增私有方法不改AnnotationManager成员布局。现有可选诊断JSON每段增加missingSourceIds,Test App私有summary增加诊断字段;这是任务授权的观测扩展,不新增Host控制协议或准入契约。
## 4. 静态结果与保护证据
已经执行:仓库/branch/HEAD/status/submodule只读核对;前置文档及源码阅读;保留JSON解析和SHA;diff人工审查、Python AST解析(没有导入/运行脚本);git diff --check、新增文档空白/围栏和范围检查。最终结果见文末静态收尾。静态检查不等同C++/Kotlin编译或测试通过。
未执行:Native/CMake/Ninja/Bazel、链接/codegen/测试程序、Gradle/APK、ADB/安装/Logcat/设备截图、Jest、性能测量、commit/push/远端回读、发布。没有观察到新编译/测试失败,因为未执行;不能推断将通过。
保留旧JSON SHA=`24a06fa1cb072a628c650723c58a880895ec656f4972fa55d3cb064bfb2aabda`。
- STEP-110未跟踪文件 SHA=`33037df71b4b30aa7956d59f4b99f77af3085405b24111bc0fe11ec857a93db0`。
- STEP-111未跟踪文件 SHA=`f3a5806f25384bf46055db2edfea0d21bfbc4124184bc813b46e38b8079c1ec4`。
- STEP102 Placement fixture SHA=`e44912970dd6353787be7a3e5f9924fe1249e2305d495f1e25e707352c97d3c7`。
- STEP114 FastSmoke源码 SHA=`94c28a111883fa784b0d9d81515c9e7ce7232d77a51eab48b4954705f1a39eb0`。
这些文件保持原样;保护任务书未暂存,开发记录原HEAD字节前缀保留。先前探索个别猜测路径不存在后,均按rg找到真实src/mln路径,未将不存在路径当证据或创建替代文件。
## 5. 精确定向交接
完整、按失败停止的命令和证据结构位于 `docs/60_测试/STEP-116_STEP113原PBF专用APP验收_v1.md`。必要顺序:
1. RDC只读审计diff/七项测试、线程和安全边界,按独立授权scoped锁定本轮源码;不得包含保护任务书或无关文件。
2. RDC Native:`cmake --preset macos-metal` → `cmake --build build-macos-metal --target mbgl-test-runner --parallel 3` → 定向filter `Step116Safety.*:Annotations.*:FastGateTrace.*:ParticleFastPath.*:RenderSegmentDiagnostics.*:RenderSegmentPlacementFree.*:RenderSegmentLocality.*:RenderSegmentPlan.*`。首项失败即停,不用旧runner。
3. 用户在最新锁定HEAD手工一次执行标准 `./tools/particle_android_validation/build_validation_apk.sh`,再明确serial手工 `adb install -r`,记录build-info/APK/安装SHA。
4. RDC/用户:`python3 -B tools/particle_android_validation/verify_step113_fast_path.py --preflight-only`,成功后 `--scenario STATIC_RC --seconds 90`;默认serial为192.168.200.139:5555。版本/源码锁不符停止。
5. 用户手动EXPORT LAST SNAPSHOT,RDC回读完整120帧,核对24Gate/mismatch0/pre-post、原63层/Source/空层manifest、真实Build/Reuse/Full/Fast;用户画面及安全回退单独验收。
预期仍有missing-source/Symbol阻塞。即使C空层修复成立,仍不得保证#23、R Build或Fast成功。缺源empty需要GPU排除/资源时序证据,Symbol需要绑定成功Full/Build的结果证书;当前任务授权不足以通过危险跨层重写弥补这两项。收到真实RDC编译/测试错误后按固定闭环修复,本轮停止于实现交付。
## 6. 实际静态收尾
- `git diff --check` 退出0,无空白诊断;四份新文档逐份 `git diff --no-index --check /dev/null <明确路径>` 无诊断,围栏成对;验收文档bash代码块仅以 `bash -n` 解析,内嵌Python和复测工具仅AST解析,没有执行命令正文或导入工具。
- 9个tracked文件修改,统计308行新增/12行删除;另有本轮四份文档新增。起点三份未跟踪任务书仍未跟踪,暂存区为空,branch/HEAD保持原值。
- 开发记录HEAD原始164685字节前缀完全保留,原SHA=`0adbe3fbe366651e3a79369dbf13d2e494a2b4545e29efaad72b299b43d2f4dd`,仅追加1440字节(8行)。
- 已逐字与HEAD比较:生产 `particle_fast_path.hpp`、公开 `render_segment.hpp`、`renderer_impl.cpp`、`render_orchestrator.cpp` 和 Activity接线完全未变;STEP110/111/102/114冻结SHA与上节相符,旧完整JSON也未修改。测试源码实际新增7个用例。
本轮实现与测试源的交付SHA-256(独立审计后若修正,应重算并记录,不能沿用此表冒充新源码):
| 文件 | SHA-256 |
|---|---|
| Step113PbfGateScene.kt | d50281713ae551edc9a48b1550254602e4950c9ddca66855bb689620c9f6f2b6 |
| annotation_manager.cpp | 6e0faf6ab585362bfb1ee4d6e79163390041295331d7c8c652156963318bfe6a |
| annotation_manager.hpp | a4c18d12a12dd4be3ddca7c667e9c45da8a57d6108956e605e1e4fe0796932c0 |
| render_segment_json.hpp | 9c1fa02fa0e7a2eccf4aeb8fe346913fb87b62258c662c303db087399cf9ca1d |
| annotations.test.cpp | b316c23c557a46f819895b7ea74a415ff5c833d0751521c28a5dd8251166daf6 |
| retained_underlay_cache.test.cpp | cc1d26540008df3b74cc97fa13e54e77092f2edb596d4c8c56d7a15504bdb109 |
| verify_step113_fast_path.py | 87a797eee0e907078cd8f49dbceb83dd3affda9b00c51cc691c1f4c553eb705e |
以上只证明交付范围和静态一致性,不证明Native/Kotlin可编译、测试通过或Fast安全。编译、设备、像素及性能仍全部未验收。
## RDC 独立核验追加(2026-10-11)
**本轮结论:限定范围源码/接口及操作说明静态审查完成;Mac Native配置 BLOCKED;Android APK/设备Fast NOT RUN,整体PARTIAL。**
- 起点粒子仓库 particle/main=e2d202b560ad0165101546597c43f2ef0ccf4542;Integration origin/main已由RDC fetch并与本地main同步,HEAD=80667d6c343411d03a760425438e3d292f452118。未接触NavSea与Integration正式文件。
- Codex实际模型 gpt-6.1-sol/high;本轮实现仅原STEP-113 STATIC_RC 的 SDK 空Annotation辅助层、Native AnnotationManager缺层恢复、缺源只读JSON诊断和测试验证器,不改变正式63个PBF layer、原五场景其他分支、Fast Gate/caching的准入或公开控制面。新增7项非Jest C++用例源码,尚未执行。
- RDC直接核对 Android Style.java 对象removeLayer(Layer)文档明确注明被移除Layer可以重新添加;与STEP116在stop中恢复原wrapper的目的相符。Native AnnotationManager恢复与事件生命周期仍须编译、图像和设备验证,不凭静态阅读标PASS。
- RDC核对旧SOURCE_CHANGE实际完整JSON:Native FastGate gates每项有order,firstFrame的knownMask等字段存在;STEP116验收脚本的原JSON结构检查有数据来源,但并非本轮运行证据。
- Mac shell当前无rg可执行,RDC将STEP116手动导出命令中的rg替换为标准grep -E,保留同样的文件筛选表达式。Python验收器ast.parse通过,git diff --check通过。
- Mac Native按当前标准命令重新执行真正的cmake --preset macos-metal配置(使用/Users/tei/Library/Android/sdk/cmake/3.31.6/bin/cmake),退出码1:Required package libuv缺失,日志保存artifacts/maplibre-particle/step116-native/configure.log。严格串行停止,未执行构建目标、未运行7项C++测试。失败是环境依赖,不应改写成代码编译通过/失败或跳过继续。
- 未执行Android Gradle、APK安装、ADB启动STEP113、新Fast计数、逐帧画面、iOS Metal回归、发行/tag/Source Lock。正式PBF缺tide以及Symbol R Placement仍保留fail-closed;此任务只是安全部分实现,并不解决原STATIC_RC无法进入Fast的全部原因。
- 用户在本轮源码scoped Git锁定且push远端核验后,需手工一次集中Android编译与install -r,再执行STEP115同HEAD/安装SHA预检、原STATIC_RC 90秒、完整120帧+24Gate+截图与其它负场景验收。新APK前禁止把旧c6d13e1产物Fast0作为本轮修复验收。若仍Fast0,必须保留缺源、Placement和C实际结果并交跨平台Core负责人确定GPU结果证书。

View File

@@ -13,6 +13,8 @@ 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.Layer
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
@@ -43,11 +45,14 @@ internal class Step113PbfGateScene(
private var exportSequence = 0L
private var commandResults = JSONObject()
private var savedGestures: List<Boolean>? = null
private var annotationStyle: Style? = null
private var detachedEmptyAnnotationLayer: Layer? = null
private val poll = object : Runnable {
override fun run() {
if (!running || paused) return
if (running) {
try {
checkAnnotationOwnership()
// One shared snapshot query per refresh; all UI/log/export uses this copy.
val native = JSONObject(map.getRenderSegmentDiagnostics())
latest = envelope(native)
@@ -63,6 +68,7 @@ internal class Step113PbfGateScene(
append("STEP-113 $scenario · ${if (ready) "READY/PARTIAL" else failureReason ?: "LOADING"} · actual=${native.optInt("styleLayerCount")}\n")
append("first=$firstFailure seq=${lastFrame?.optLong("sequence")} full=${trace?.optLong("fullRendered")} ")
append("window=${trace?.optInt("windowCount")} mismatch=${trace?.optLong("decisionMismatch")}\n")
append("Fast=${trace?.optLong("fastRendered")} · 空Annotation调整=${commandResults.optBoolean("emptyBuiltinAnnotationLayerRemoved")}\n")
append(rank.joinToString(" / ") { "${it.optString("reason")}=${it.optLong("failures")}" })
for (i in 0 until segments.length()) {
val s = segments.getJSONObject(i)
@@ -138,6 +144,8 @@ internal class Step113PbfGateScene(
.put("deviceReferenceSelector", "用户STEP-112:RCRR_PREFIX[0,13)/[13,33)/[33,107)/[107,113)")
.put("baseLayerManifest", layerManifest(base)).put("sprite", root.optString("sprite"))
.put("glyphs", root.optString("glyphs")).put("vectorSource", JSONObject(source.toString()))
.put("productionSources", JSONObject(root.getJSONObject("sources").toString()))
.put("internalAnnotationPolicy", "仅STATIC_RC加载时移除空SDK辅助层;后续添加不再移除;原63层及Source原样")
val hasField = scenario != Scenario.PBF63
if (hasField) {
val sources = root.getJSONObject("sources")
@@ -176,9 +184,10 @@ internal class Step113PbfGateScene(
if (hasField) "当前NavSea源码RC:rn-weather-color" else "基础63层全RETAINED")
.put("requestedPlan", JSONObject(plan))
commandResults.put("planAccepted", map.setRenderSegmentPlan(plan))
map.setStyle(Style.Builder().fromJson(root.toString())) testStyle@ {
map.setStyle(Style.Builder().fromJson(root.toString())) testStyle@ { style ->
if (!running || epoch != generation) return@testStyle
try {
if (scenario == Scenario.STATIC_RC) removeEmptyAnnotationLayer(style, ids)
map.cameraPosition = CameraPosition.Builder().target(LatLng(33.45, 129.97))
.zoom(10.5).bearing(0.0).tilt(0.0).build()
if (hasField) initializeFields()
@@ -192,6 +201,57 @@ internal class Step113PbfGateScene(
}
}
private fun removeEmptyAnnotationLayer(style: Style, baseIds: List<String>) {
check(ANNOTATION_LAYER !in baseIds && map.annotations.isEmpty()) {
"存在生产同名层或遗留Annotation,禁止移除辅助层"
}
check(!parity.getJSONObject("productionSources").has(ANNOTATION_SOURCE)) {
"生产Style占用内部Annotation Source ID,禁止推断为空SDK依赖"
}
val layer = style.getLayer(ANNOTATION_LAYER)
check(layer is SymbolLayer && layer.sourceId == ANNOTATION_SOURCE) {
"内建Annotation层身份不符,保留现场"
}
check(style.getSource(ANNOTATION_SOURCE) != null) { "内建Annotation Source缺失" }
// The object overload preserves the detached wrapper for restoration.
check(style.removeLayer(layer)) { "空内建Annotation层移除失败" }
annotationStyle = style
detachedEmptyAnnotationLayer = layer
check(style.getSource(ANNOTATION_SOURCE) != null) { "不得移除内建Source" }
commandResults.put("emptyBuiltinAnnotationLayerRemoved", true)
.put("annotationInstancesAtRemoval", 0).put("annotationSourcePreserved", true)
parity.put("removedInternalLayer", ANNOTATION_LAYER)
.put("adjustmentScope", "STATIC_RC一次;仅SDK空辅助层;非生产63层")
logRecord("annotation-policy", JSONObject(commandResults.toString()).put("scenario", scenario.name))
}
private fun restoreEmptyAnnotationLayer(reason: String) {
val style = annotationStyle
val layer = detachedEmptyAnnotationLayer
try {
if (style != null && layer != null && map.style === style && style.getLayer(ANNOTATION_LAYER) == null) {
style.addLayer(layer)
}
commandResults.put("annotationRemovalEnded", reason)
} finally {
annotationStyle = null
detachedEmptyAnnotationLayer = null
}
}
private fun checkAnnotationOwnership() {
val style = annotationStyle ?: return
if (map.style !== style) {
restoreEmptyAnnotationLayer("style-replaced")
fail(IllegalStateException("Style已替换,空层调整停止;新Style图层原样保留"))
} else if (map.annotations.isNotEmpty() || style.getLayer(ANNOTATION_LAYER) != null) {
// Native AnnotationManager restores the point layer synchronously
// at add/update, before Map::onUpdate. Polling only reports revocation.
restoreEmptyAnnotationLayer("annotation-or-layer-added")
fail(IllegalStateException("Annotation/辅助层已添加,保留内容并撤销STATIC_RC空层验收条件"))
}
}
private fun addFields(layers: JSONArray) {
layers.put(JSONObject().put("id", COLOR).put("type", "weather-color").put("source", SOURCE)
.put("paint", JSONObject().put("weather-color-opacity", 0.62)))
@@ -276,6 +336,10 @@ internal class Step113PbfGateScene(
.put("build", c.optLong("build")).put("reuse", c.optLong("reuse"))
.put("invalidation", c.optLong("invalidation")).put("fallback", c.optLong("fallbackCount"))
.put("reason", c.optString("lastFallbackReason")))
rs.getJSONObject(rs.length() - 1)
.put("missingSourceIds", s.optJSONArray("missingSourceIds") ?: JSONArray())
.put("placementCapable", s.optBoolean("placementCapable"))
.put("placementGatePassed", s.optBoolean("placementGatePassed"))
}
return JSONObject().put("schema", "step113-summary").put("schemaVersion", 1).put("scenario", scenario.name)
.put("sceneReady", ready).put("blockedReason", failureReason ?: JSONObject.NULL)
@@ -291,6 +355,11 @@ internal class Step113PbfGateScene(
.put("overwritten", t.optLong("overwritten")).put("decisionMismatch", t.optLong("decisionMismatch"))
.put("fullRendered", t.optLong("fullRendered")).put("fastRendered", t.optLong("fastRendered"))
.put("gateFailureCounts", counts).put("retained", rs)
.put("commands", JSONObject(commandResults.toString()))
.put("sources", native.optJSONArray("sources") ?: JSONArray())
.put("placementChanged", native.optBoolean("placementChanged"))
.put("placementTransitionActive", native.optBoolean("placementTransitionActive"))
.put("symbolFadeChange", native.opt("symbolFadeChange") ?: JSONObject.NULL)
}
fun export() {
@@ -339,6 +408,8 @@ internal class Step113PbfGateScene(
running = false
++generation
handler.removeCallbacksAndMessages(null)
try { restoreEmptyAnnotationLayer("scene-stopped") }
catch (error: Exception) { Log.e(TAG, "annotation-layer-restore-failed", error) }
runtime?.dispose()
runtime = null
savedGestures?.let { flags ->
@@ -365,6 +436,8 @@ internal class Step113PbfGateScene(
private const val PARTICLE = "rn-weather-particles"
private const val PROBE_SOURCE = "step113-source-probe"
private const val PROBE_LAYER = "step113-source-probe-circle"
private const val ANNOTATION_SOURCE = "org.maplibre.annotations"
private const val ANNOTATION_LAYER = "org.maplibre.annotations.points"
private const val FORECAST_MS = 1786060800000L
private fun planJson(s: Scenario, finalBaseId: String): String {

View File

@@ -75,6 +75,9 @@ void AnnotationManager::removeAnnotation(const AnnotationID& id) {
}
void AnnotationManager::add(const AnnotationID& id, const SymbolAnnotation& annotation) {
// Repair an explicitly removed empty helper layer before Map::onUpdate
// captures the style. A subsequent real annotation must never be hidden.
ensurePointLayer();
auto impl = std::make_shared<SymbolAnnotationImpl>(id, annotation);
symbolTree.insert(impl);
symbolAnnotations.emplace(id, impl);
@@ -101,6 +104,9 @@ void AnnotationManager::update(const AnnotationID& id, const SymbolAnnotation& a
const SymbolAnnotation& existing = it->second->annotation;
// Even an otherwise no-op update must publish the repaired style.
dirty = ensurePointLayer() || dirty;
if (existing.geometry != annotation.geometry || existing.icon != annotation.icon) {
dirty = true;
@@ -173,13 +179,18 @@ std::unique_ptr<AnnotationTileData> AnnotationManager::getTileData(const Canonic
return tileData;
}
void AnnotationManager::updateStyle() {
bool AnnotationManager::ensurePointLayer() {
// Create annotation source, point layer, and point bucket. We do everything
// via Style::Impl because we don't want annotation mutations to trigger
// Style::Impl::styleMutated to be set.
bool changed = false;
if (!style.get().impl->getSource(SourceID)) {
style.get().impl->addSource(std::make_unique<AnnotationSource>());
changed = true;
}
// Source and layer lifetimes are independent. Never replace a layer that
// the application has supplied with this ID.
if (!style.get().impl->getLayer(PointLayerID)) {
std::unique_ptr<SymbolLayer> layer = std::make_unique<SymbolLayer>(PointLayerID, SourceID);
using namespace expression::dsl;
@@ -190,7 +201,13 @@ void AnnotationManager::updateStyle() {
layer->setIconIgnorePlacement(true);
style.get().impl->addLayer(std::move(layer));
changed = true;
}
return changed;
}
void AnnotationManager::updateStyle() {
ensurePointLayer();
std::scoped_lock lock(mutex);

View File

@@ -65,6 +65,7 @@ private:
void remove(const AnnotationID&);
void updateStyle();
bool ensurePointLayer();
std::unique_ptr<AnnotationTileData> getTileData(const CanonicalTileID&);

View File

@@ -7,6 +7,7 @@
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include <algorithm>
#include <optional>
#include <set>
#include <string>
@@ -323,6 +324,15 @@ inline std::string renderSegmentDiagnosticsToJSON(const RenderSegmentDiagnostics
for (const auto& layer : segment.layers) writeStyleLayerDiagnostics(writer, layer);
writer.EndArray();
writeStringArray(writer, "dependentSourceIds", segment.dependentSourceIds);
// Snapshot-only audit: do not infer an empty drawable result from an
// absent source, or hide it behind the first cache fallback reason.
writer.Key("missingSourceIds"); writer.StartArray();
for (const auto& id : segment.dependentSourceIds) {
const auto found = std::find_if(snapshot.sources.begin(), snapshot.sources.end(),
[&](const auto& source) { return source.sourceId == id; });
if (found == snapshot.sources.end()) writer.String(id.c_str());
}
writer.EndArray();
writeStringArray(writer, "unknownDependencies", segment.unknownDependencies);
writer.Key("placementCapable"); writer.Bool(segment.placementCapable);
writer.Key("placementGatePassed"); writer.Bool(segment.placementGatePassed);

View File

@@ -4,8 +4,10 @@
#include <mln/test/map_adapter.hpp>
#include <mln/annotation/annotation.hpp>
#include <mln/annotation/annotation_manager.hpp>
#include <mln/style/style.hpp>
#include <mln/style/image.hpp>
#include <mln/style/layers/symbol_layer.hpp>
#include <mln/map/map_options.hpp>
#include <mln/util/io.hpp>
#include <mln/util/run_loop.hpp>
@@ -66,6 +68,53 @@ TEST(Annotations, SymbolAnnotation) {
// }
}
TEST(Annotations, Step116EmptyPointLayerRestoredBeforeRealAdd) {
AnnotationTest test;
test.map.getStyle().loadJSON(util::read_file("test/fixtures/api/empty.json"));
test.frontend.render(test.map);
auto& style = test.map.getStyle();
ASSERT_NE(style.getSource(AnnotationManager::SourceID), nullptr);
ASSERT_TRUE(style.removeLayer(AnnotationManager::PointLayerID));
test.map.addAnnotationImage(namedMarker("default_marker"));
test.map.addAnnotation(SymbolAnnotation{Point<double>(0, 0), "default_marker"});
// Check before rendering: a polling/render callback is too late to repair
// the immutable UpdateParameters captured by Map::onUpdate.
ASSERT_NE(style.getLayer(AnnotationManager::PointLayerID), nullptr);
test.checkRendering("point_annotation");
}
TEST(Annotations, Step116NoOpUpdateRepairsPointLayerAndSchedulesRender) {
AnnotationTest test;
test.map.getStyle().loadJSON(util::read_file("test/fixtures/api/empty.json"));
test.map.addAnnotationImage(namedMarker("default_marker"));
const SymbolAnnotation annotation{Point<double>(0, 0), "default_marker"};
const auto id = test.map.addAnnotation(annotation);
test.checkRendering("point_annotation");
ASSERT_TRUE(test.map.getStyle().removeLayer(AnnotationManager::PointLayerID));
test.map.updateAnnotation(id, annotation);
ASSERT_NE(test.map.getStyle().getLayer(AnnotationManager::PointLayerID), nullptr);
test.checkRendering("point_annotation");
}
TEST(Annotations, Step116ReloadRestoresHelperWithoutReplacingExistingLayer) {
AnnotationTest test;
const auto json = util::read_file("test/fixtures/api/empty.json");
test.map.getStyle().loadJSON(json);
test.frontend.render(test.map);
auto& style = test.map.getStyle();
ASSERT_TRUE(style.removeLayer(AnnotationManager::PointLayerID));
auto supplied = std::make_unique<mln::style::SymbolLayer>(AnnotationManager::PointLayerID,
AnnotationManager::SourceID);
auto* identity = supplied.get();
style.addLayer(std::move(supplied));
test.map.addAnnotationImage(namedMarker("default_marker"));
test.map.addAnnotation(SymbolAnnotation{Point<double>(0, 0), "default_marker"});
EXPECT_EQ(style.getLayer(AnnotationManager::PointLayerID), identity);
style.loadJSON(json);
test.checkRendering("point_annotation");
EXPECT_NE(style.getLayer(AnnotationManager::PointLayerID), nullptr);
}
TEST(Annotations, SymbolAnnotationTileBoundary) {
// Almost exactly the same as SymbolAnnotation test above, but offset my
// fractions of a degree tests precision issue from

View File

@@ -1200,6 +1200,111 @@ TEST(RenderSegmentLocality, ReadinessAndFadeFollowOnlyDependencies) {
EXPECT_FALSE(assessRenderSegmentSources({{}, {}, true}, lookup(true, false)).ready);
}
TEST(Step116Safety, MissingSourceIsNotAnEmptyDependencyEvenWithRevisionTombstone) {
const RenderSegmentDependencies dependencies{{"chart", "tide"}, {"symbol/glyph/image/placement"}, false};
RenderSegmentRevisionBook book;
book.advanceSource("chart");
EXPECT_FALSE(book.revisionFor(dependencies));
bool exists = false;
bool loaded = false;
const auto lookup = [&](const std::string& id) -> std::optional<RenderSegmentSourceStatus> {
if (id == "chart") return RenderSegmentSourceStatus{true, false};
if (id == "tide" && exists) return RenderSegmentSourceStatus{loaded, false};
return std::nullopt;
};
auto assessment = assessRenderSegmentSources(dependencies, lookup);
EXPECT_FALSE(assessment.ready);
EXPECT_STREQ(assessment.reason, "missing-source");
exists = true;
book.advanceSource("tide"); // source addition, before async tile delivery
const auto created = book.revisionFor(dependencies);
ASSERT_TRUE(created);
EXPECT_FALSE(assessRenderSegmentSources(dependencies, lookup).ready);
loaded = true;
book.advanceSource("tide"); // async layout/tile mutation
EXPECT_NE(book.revisionFor(dependencies), created);
EXPECT_TRUE(assessRenderSegmentSources(dependencies, lookup).stable);
exists = false;
book.advanceSource("tide"); // tombstone survives removal; readiness must not
ASSERT_TRUE(book.revisionFor(dependencies));
EXPECT_FALSE(assessRenderSegmentSources(dependencies, lookup).ready);
exists = true;
book.advanceSource("tide"); // same-name restoration receives a new revision
EXPECT_NE(book.revisionFor(dependencies), created);
}
TEST(Step116Safety, MissingSourceAuditDoesNotHidePlacementOrMutateSnapshot) {
RenderSegmentDiagnostics snapshot;
RenderSegmentDiagnosticsEntry segment;
segment.strategy = RenderSegmentStrategy::Retained;
segment.dependentSourceIds = {"navsea_delivery", "tide"};
segment.placementCapable = true;
segment.placementGatePassed = false;
segment.cache.emplace();
segment.cache->lastFallbackReason = "missing-source";
snapshot.segments.push_back(segment);
RenderSourceReadinessDiagnostics source;
source.sourceId = "navsea_delivery";
source.sourceType = "vector";
snapshot.sources.push_back(source);
auto json = renderSegmentDiagnosticsToJSON(snapshot);
EXPECT_NE(json.find("\"missingSourceIds\":[\"tide\"]"), std::string::npos);
EXPECT_NE(json.find("\"placementGatePassed\":false"), std::string::npos);
EXPECT_EQ(renderSegmentDiagnosticsToJSON(snapshot), json);
source.sourceId = "tide";
snapshot.sources.push_back(source);
EXPECT_NE(renderSegmentDiagnosticsToJSON(snapshot).find("\"missingSourceIds\":[]"), std::string::npos);
// Presence alone grants neither a successful Build nor a placement pass.
EXPECT_EQ(snapshot.segments[0].cache->build, 0u);
EXPECT_FALSE(snapshot.segments[0].placementGatePassed);
}
TEST(Step116Safety, SymbolContinuousBranchesAndResourceChangesRemainProtected) {
const std::vector<StyleLayerDiagnostics> layers{
{0, "tide-spots", "symbol", false, "tide", std::nullopt}};
ASSERT_TRUE(renderSegmentHasPlacementCapableLayer(layers, 0, 1));
// Normal Full: recompute => changed; recent => stale/transition.
EXPECT_FALSE(renderSegmentPlacementGatePassed(true, true, 0.0f, false));
EXPECT_FALSE(renderSegmentPlacementGatePassed(true, false, 0.0f, true));
EXPECT_FALSE(renderSegmentPlacementGatePassed(true, true, 1.0f, false)); // transitions disabled
const auto dependencies = renderSegmentDependencies(layers, 0, 1);
RenderSegmentRevisionBook book;
book.advanceSource("tide");
const auto unchanged = book.revisionFor(dependencies);
EXPECT_EQ(book.revisionFor(dependencies), unchanged);
// Test the revision contract, not fabricated glyph/tile events or GPU completion.
for (const auto* event : {"style", "glyph", "image", "context/reload"}) {
SCOPED_TRACE(event);
const auto before = book.revisionFor(dependencies);
book.advanceGlobal();
EXPECT_NE(book.revisionFor(dependencies), before);
}
for (const auto* event : {"source-update", "tile/layout"}) {
SCOPED_TRACE(event);
const auto before = book.revisionFor(dependencies);
book.advanceSource("tide");
EXPECT_NE(book.revisionFor(dependencies), before);
}
}
TEST(Step116Safety, AnnotationMutationAndLayerOrderDisableReloadKeepAllShadowFailures) {
auto input = fastPathInput();
input.cacheValid = false;
input.annotationMutation = true;
input.unsupportedContinuousLayer = true;
input.supportedScene = false;
input.topologyMatches = false; // added/reordered layer
input.retainedEnabled = false; // explicit disable
input.hasPreparedTree = false; // style reload/context replacement
const auto decision = evaluateParticleFastPath(input);
EXPECT_FALSE(decision.candidate);
EXPECT_EQ(decision.reason, ParticleFastPathMissReason::NoPreparedTree);
const auto failures = particleFastGateFailureMask(input);
for (const auto bit : {0, 1, 2, 10, 21, 22, 23}) EXPECT_NE(failures & (1u << bit), 0u);
auto unmodified = fastPathInput();
EXPECT_EQ(particleFastGateFailureMask(unmodified), 0u); // predicate-only; no rendered Fast claim
}
TEST(RenderSegmentLocality, MixedExecutionDrawsEachLiveRangeOnceInStyleOrder) {
ResolvedRenderSegmentPlan plan;
plan.valid = true;

View File

@@ -114,7 +114,7 @@ MAPLIBRE_VALIDATION_OUT_DIR=/tmp/my-validation \
## STEP-113 原 PBF 专用 Test App Fast Path 复测
本测试不使用 STEP-114 的简化 FAST_SMOKE。保持原 Step113PbfGateScene、PBF URL、63 个正式基础层与原 Native 诊断不变。需要用户手工完成新源码的标准 APK 编译和安装。工具先检查 build-info 中的 Git commit 与当前 HEAD、APK SHA、设备已安装 APK SHA 是否一致;任何不一致即拒绝并说明原因。
本测试不使用 STEP-114 的简化 FAST_SMOKE。保留原 Step113PbfGateScene 的 PBF URL、63 个正式基础层与 Native 真实计数。STEP-116 仅对 STATIC_RC 的空 SDK Annotation 辅助层做一次移除及生命周期保护,并增加诊断;其余四场景不做此调整。需要用户在 RDC 独立审计并锁定源码后手工完成标准 APK 编译和安装。工具检查 tracked clean、build-info Git commit 与 HEAD、APK SHA、设备安装 SHA;不一致即拒绝。原 STEP-110/111 等未跟踪任务书可以保留。
在 Mac 的 MapLibreParticleLab 根目录依序执行:
@@ -129,3 +129,9 @@ MAPLIBRE_VALIDATION_OUT_DIR=/tmp/my-validation \
前两项为用户手工 Android 构建和更新安装,不得在未授权情况下代劳。后两项由 RDC 或用户执行。脚本使用明确 ADB serial 启动原有 Activity 的 STEP-113 原场景,按当前 PID 还原 STEP113Gate 的 Base64 Logcat summary,计算实际 Fast/Full 计数差及 R Build/Reuse,保存到忽略 Git 的 artifacts/maplibre-particle/android-validation/step113-retest-时间/场景/ 目录。
FAST_OBSERVED 仅表示实际 Fast 数字增长,不代表像素等价、性能或所有安全回退已验收;NO_FAST_OBSERVED 表示仍未进入,记录完整 R fallback 和 24 Gate 聚合。STALE_APK / INSUFFICIENT_SAMPLES 不是 PASS。依次需要时用 --scenario PBF63、PARTICLE_OFF、PARTIAL_RCRR、SOURCE_CHANGE。SOURCE_CHANGE 的手动 Source mutation 仍须通过 App 原菜单做独立验证,脚本不会自行修改 Source。阶段事实详见 docs/50_Step记录/STEP-115_STEP113原PBF专用App复测准备_v1.md。
## STEP-116 当前交接
本轮只交源码,缺 `tide` 与 Symbol R 缓存准入继续 fail-closed,不能宣称原 STATIC_RC 已出现 Fast。Native AnnotationManager 在后续真实 Symbol Annotation add/update 与 Style loaded 时同步恢复缺失 points;Test App stop 重加原空层,后续 Annotation/Style 变化撤销空层验收条件,不周期删除新内容。原63层和Source定义保留,内部Annotation Source不删除;源码预期STATIC_RC由66层变65层,须以最新设备manifest核验。
复测脚本现要求至少75秒,在PID出现后等待完整请求时长;建议 `--scenario STATIC_RC --seconds 90`。同generation/lifecycle、plan/base parity、24Gate、mismatch0、计数单调及blocked撤销均为必要条件。summary新增缺源ID/Placement/commands/Source表;120帧仍需用户手动导出。精确命令、用户一次手工build/install、RDC Native定向测试及阴性结果解释见 [STEP-116 专用APP验收](../../docs/60_测试/STEP-116_STEP113原PBF专用APP验收_v1.md)。历史STEP115记录保留为当时事实,不代表本轮设备已验收。

View File

@@ -45,6 +45,9 @@ def inspect(apk, adb, serial):
info = dict(line.split("=", 1) for line in BUILD_INFO.read_text(encoding="utf-8").splitlines()
if "=" in line)
source_head = call(["git", "-C", str(ROOT), "rev-parse", "HEAD"])
tracked_changes = call(["git", "-C", str(ROOT), "status", "--porcelain", "--untracked-files=no"])
if tracked_changes:
raise RuntimeError("SOURCE_NOT_LOCKED: tracked源码有未提交变更;RDC须先独立审计并scoped提交,用户再构建最新HEAD")
if info.get("commit") != source_head:
raise RuntimeError(f"STALE_APK: build-info commit={info.get('commit')}, "
f"current HEAD={source_head}; 禁止用旧APK验收本轮修复")
@@ -76,7 +79,7 @@ def extract(logcat, pid, expected):
continue
try:
item = json.loads(found.group(2))
if item.get("encoding") != "base64-utf8" or item.get("kind") != "summary":
if item.get("encoding") != "base64-utf8" or item.get("kind") not in ("summary", "blocked"):
continue
ident = (item["exportId"], item["kind"])
record = chunks.setdefault(ident, {"count": item["chunkCount"], "items": {}})
@@ -85,13 +88,18 @@ def extract(logcat, pid, expected):
except (KeyError, ValueError, TypeError):
continue
result = []
for (ident, _), record in chunks.items():
for (ident, kind), record in chunks.items():
if len(record["items"]) != record["count"]:
continue
try:
packed = "".join(record["items"][i] for i in range(record["count"]))
sample = json.loads(base64.b64decode(packed, validate=True).decode("utf-8"))
if sample.get("scenario") == expected and sample.get("sceneReady") and not sample.get("blockedReason"):
if kind == "blocked":
sample["blockedReason"] = sample.get("reason") or "SCENE_BLOCKED"
sample["sceneReady"] = False
# Keep revoked/blocked summaries as negative evidence too; an
# earlier Fast counter must not conceal a later Annotation guard.
if sample.get("scenario") == expected:
result.append((int(ident.split("-", 1)[0]), sample))
except (KeyError, ValueError, TypeError, UnicodeError):
continue
@@ -107,8 +115,8 @@ def run():
parser.add_argument("--seconds", type=int, default=75)
parser.add_argument("--preflight-only", action="store_true")
args = parser.parse_args()
if args.seconds < 65 and not args.preflight_only:
parser.error("至少65秒以取得首次及下一次30秒周期摘要;不是FPS测量")
if args.seconds < 75 and not args.preflight_only:
parser.error("原STEP-113场景至少75秒;摘要与完整120帧导出仍须分别验收")
if not Path(args.adb).is_file():
raise RuntimeError(f"adb not found: {args.adb}")
info, local_sha, head, device = inspect(Path(args.apk), args.adb, args.serial)
@@ -136,12 +144,17 @@ def run():
if process_id is None:
raise RuntimeError("APP_PROCESS_NOT_RUNNING after launch")
print(f"APP_PID={process_id}")
time.sleep(max(0, args.seconds - 12))
# PID acquisition can take less than 12 seconds. Never subtract the
# maximum poll duration from the actual scenario observation period.
observation_started = time.monotonic()
time.sleep(args.seconds)
observation_elapsed = time.monotonic() - observation_started
raw_log = device("logcat", "-d", "-v", "brief", "-s", "STEP113Gate:I", "*:S")
output.joinpath("logcat.txt").write_text(raw_log + "\n", encoding="utf-8")
samples = extract(raw_log, process_id, args.scenario)
all_samples = extract(raw_log, process_id, args.scenario)
samples = [s for s in all_samples if s.get("sceneReady") and not s.get("blockedReason")]
if not samples:
status = "NO_READY_SUMMARY"
status = "SCENE_BLOCKED_OR_REVOKED" if any(s.get("blockedReason") for s in all_samples) else "NO_READY_SUMMARY"
fast_delta = full_delta = 0
else:
start, end = samples[0], samples[-1]
@@ -149,8 +162,20 @@ def run():
full_delta = end["fullRendered"] - start["fullRendered"]
if len(samples) < 2:
status = "INSUFFICIENT_SAMPLES"
elif any(s.get("blockedReason") for s in all_samples):
status = "SCENE_BLOCKED_OR_REVOKED"
elif any((s.get("sceneGeneration"), s.get("lifecycle")) !=
(start.get("sceneGeneration"), start.get("lifecycle")) for s in samples):
status = "SUMMARY_LIFECYCLE_CHANGED"
elif any(not s.get("planValid") or not s.get("basePreserved") for s in samples):
status = "INVALID_PLAN_OR_BASE_PARITY"
elif any(len(s.get("gateFailureCounts", [])) != 24 for s in samples):
status = "INCOMPLETE_GATE_SUMMARY"
elif any(s["decisionMismatch"] != 0 for s in samples):
status = "DECISION_MISMATCH"
elif any(b["fastRendered"] < a["fastRendered"] or b["fullRendered"] < a["fullRendered"]
for a, b in zip(samples, samples[1:])):
status = "COUNTER_RESET"
elif fast_delta > 0:
status = "FAST_OBSERVED"
else:
@@ -165,7 +190,9 @@ def run():
"scenario": args.scenario,
"sourceReport": "STEP-113 original PBF test scene; not STEP-114 smoke",
"durationSecondsRequested": args.seconds,
"durationSecondsObservedAfterPid": observation_elapsed,
"completeSummarySamples": len(samples),
"blockedSummarySamples": [s for s in all_samples if s.get("blockedReason")],
"status": status,
"fastDelta": fast_delta,
"fullDelta": full_delta,

View File

@@ -1157,3 +1157,13 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2`
- 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,后续用户一次手工构建验收。
- 2026-10-11 STEP-115:为原STEP-113专用APP增加Fast Path复测工具,严格校验源码HEAD、build-info APK SHA与设备安装SHA;旧包源码为c6d13e1,与现行21a1081不匹配,preflight正确拒绝。已验证旧Logcat可解码STEP113真实摘要,原五场景保持不改。尚无新Android编译/安装/真正Fast,下一步用户手工build/install后以STATIC_RC为先验收,状态PARTIAL。
### 2026-10-11 STEP-116 原 STEP-113 PBF Fast Path 受控修复(源码交付 PARTIAL)
- 实际模型 GPT-6.1-Sol(gpt-6.1-sol)/ high,已读取会话turn_context确认;Mac particle/main起点e2d202b560ad0165101546597c43f2ef0ccf4542,无commit/push/发行/子代理。
- 仅STATIC_RC确认空实例后一次移除SDK内部points层,保留Source及原63层;stop恢复,后续添加/Style替换撤销验收条件。Native AnnotationManager在Symbol add/update/style-loaded同步恢复缺层,no-op更新也dirty,避免依赖3秒poll漏Marker。
- R缺tide仍missing-source,Symbol R无最终GPU结果证书仍BLOCKED;未补假源/删tide-spots/放宽Core C或Placement Gate。JSON加missingSourceIds,摘要同次保留Placement与真实Build/Reuse/Full/Fast。
- 新增四个Step116Safety与三个Annotations.Step116非Jest测试源码;STEP115脚本修复实际等待时长并拒绝未锁定tracked源码、生命周期变化/撤销/计数reset。四份中文交付和README已落盘,开发记录仅追加。
- 已做静态diff/AST/旧JSON及冻结SHA核查,未编译/运行Native测试/Gradle/ADB/安装/Jest;真实Fast与画面NOT VERIFIED。保护STEP110/111,保留STEP114/102源码。下一步RDC独立审计锁定源码,用户一次手工build/install,原STATIC_RC≥75秒与120帧/24Gate/SHA/画面闭环,详见docs/60_测试/STEP-116_STEP113原PBF专用APP验收_v1.md及本Step完成报告。
- 2026-10-11 STEP-116 RDC:审查Native空Annotation恢复、原STEP113受限空层移除、缺源诊断、7个C++测试源码与同HEAD/SHA复测脚本,git diff --check、Python AST通过。修正Mac测试命令rg为grep -E。实际CMake macos-metal配置exit1缺libuv,未继续C++测试/Android Gradle/真机Fast,PARTIAL;缺tide与Symbol R仍BLOCKED。