docs: complete STEP-077 V4 bridge investigation
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
OpenAI Codex
2026-09-13 15:25:34 +08:00
parent 0759a06104
commit e0088a5f08
4 changed files with 322 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
# STEP-077 QBIN2 V4 Android / JNI / RN 桥接调查报告 v1
状态:完成(只读调查)
日期:2026-09-13
基线:`0759a06104e1fb30f036db2efb6ef34ca5859759`(`particle/main` = `origin/particle/main` 于调查开始时)
## 结论
V4 应新增一条与 V3 完全平行的、低频的 `WeatherRuntime` / `MapLibreMap` / `NativeMapView` 通道;不得接到现有 `submitField`、`clearFields`、`setForecastTime`、`setPlaybackState` 或 `setPlaybackSpeed`。这些现有命令分别写入 V3 Host field、`clearHostFields()`、`seekRuntimeTimelineToTimestamp()` 和 `controlRuntimeTimeline()`,会操作 V3 timeline,不能承载 V4 的 `EncodedFieldStore` / `ForecastClock`。
推荐 RN 只传绝对本地 `qbinFileUri`(或 `file://` URI)及小型、typed primitive ABI 元数据;Kotlin 有界读取为 Java `byte[]`,JNI 复制为 C++ `std::vector<uint8_t>`,再调用 `RasterArraySource::submitEncodedField(...)`。不传 Base64,不由 JS 构造 `number[]` / 大型 `ReadableArray`。V4 Core 独占每个 field 的 T0/T1/MIX 选择;桥接只传 requested time、play/pause 与速度,绝不传 endpoint、mix 或 renderer snapshot。
## 实际当前链路
```text
FieldRuntime.ts public facade
-> NativeWeatherRuntimeBridge.ts TurboModule Spec (primitive arguments / Promise)
-> WeatherRuntimeBridgeModule.kt
runOnUiQueueThread -> Fabric reactTag
UIManagerHelper(FABRIC).resolveView -> MLRNMapView.mapLibreMap
local file bounded read -> WeatherRuntime
-> MapLibreMap (map-owned WeatherRuntime facade)
-> NativeMapView Java (checkState) -> registered NativeMapView JNI
-> native_map_view.cpp: style.getSource(id), dynamic_cast<RasterArraySource*>
-> RasterArraySource V3 Host API OR isolated V4 Host API
```
源码事实如下。
- `FieldRuntime.submitField` 仍是 V3 public API,封装 `FieldPayloadRefs` 为 validity/scalar/vector 三个 URI;`submitLegacyVectorField` 保留 v1 兼容入口。`NativeWeatherRuntimeBridge.ts` 的 codegen Spec 已刻意使用 primitive 参数,空 string 仅表达 V3 optional plane。
- Kotlin 对 V3 已验证本地绝对路径或 `file://`、单文件和 aggregate 64 MiB、读中长度变化;再形成 `ByteArray`。它不保存 reactTag、map 或 timeline,而是每次在 UI queue 解析当前 Fabric view。
- `WeatherRuntime.submitField` 把 V3 bytes 重新 pack 后调用 `submitRasterArraySourceHostFieldV2`;`NativeMapView` 用 `GetArrayRegion` 复制到 C++,然后拆 planes 并调用 `submitHostField`。这是可复用的“file -> Java byte[] -> JNI copy -> vector” seam,而不是可复用的 V4 API。
- V4 `RasterArraySource` 已有独立 `EncodedFieldStore`、`ForecastClock`、binding intent 和 `submitEncodedField` / `set*Binding` / `setRequestedForecastTime` / `setForecastPlayback*` / `clear*EncodedFields`;注释和 STEP-076 public-path 测试都证明它不写 V3 T0/T1/MIX 或 renderer snapshot。
## QBIN 传输形态
| 方案 | 当前支持与成本 | 结论 |
| --- | --- | --- |
| A:RN `qbinFileUri`,Kotlin 读取 | 已有 `readLocalPayload` / `resolveLocalFile` 精确先例;仅接受本地文件、1..64 MiB,16 KiB buffer,有 read-time size guard;Java/JNI 仍各有一次受控 copy | 推荐。新增 V4 方法复用读取 helper,但不复用 V3 submit/timeline API。 |
| B:RN `byte[]` / `number[]` | 当前 TurboModule Spec 无 binary array ABI;`ReadableArray` 是现有 color stops 的小对象集合,不适合 QBIN。JS number array 造成 JS/native 转换、对象/number 开销与额外 copy;Base64 更扩大内容且无现有 ABI | 拒绝。不得为了方便绕过 64 MiB file guard。 |
| C:直接复用 Java/JNI `byte[]` seam | Kotlin -> `WeatherRuntime` -> Map -> `NativeMapView` 的 `byte[]` 和 `GetArrayRegion` 已存在,适合作为 A 的下半段 | 复用实现模式;新增命名为 V4-specific JNI 方法,不复用 V3 方法。 |
64 MiB 是现有 Kotlin `MAX_PAYLOAD_BYTES` 与 Java `MAX_FIELD_PAYLOAD_BYTES` 的边界;V4 要在 Kotlin、Java、JNI 均检查 raw QBIN 长度 `1..64 MiB`,随后 Core Decoder 继续执行其自身 QBIN 全文件/decoded-raw 限制。该限制不授权桥接解析 QBIN。
## 推荐 V4 API 与跨层 ABI
所有 String 以 UTF-8 byte length 做小型上限校验;`reactTag`、generation、dataVersion、time 使用 JS 安全整数检查(当前 bridge 对 timestamp 已采用 finite/nonnegative/integral guard)。枚举使用固定 string;所有 enum numeric 值只在 Kotlin/Java/C++ 边界转换。
| RN public / Turbo Spec | Kotlin / Java facade | JNI / Core | 参数形态及责任 |
| --- | --- | --- | --- |
| `submitEncodedField(tag, fieldKey, fieldID, generation, dataVersion, contractJson, qbinFileUri)` | `submitEncodedField(...)` | `submitRasterArraySourceEncodedField(sourceId, fieldKey, fieldID, long generation, long dataVersion, contractJson, byte[] raw)` | URI 为文件;metadata 用 primitive;contractJson 是唯一结构化小协议,Kotlin/Java 各只作长度、空值、range;JNI 解析为 Core structs 并调用 V4 API。 |
| `setColorBinding(tag, fieldKey, semanticID, groupID, expectedValueClass, expectedUnitID)` | 同名 | `setRasterArraySourceEncodedColorBinding(...)` | 完全 primitive;JNI 构造 `ColorBinding`,Core resolver 仍做最终 plane assertion。 |
| `setParticleBinding(tag, mode, fieldKey, firstSemanticID, firstGroupID, secondSemanticID, secondGroupID, expectedUnitID)` | 同名 | `setRasterArraySourceEncodedParticleBinding(...)` | mode-dependent selector 仍占固定 primitive slots;Direction 使用 first selector,second 传 0/0。Kotlin/Java 检查 mode 和 uint16 range,不引入 `ReadableMap`。 |
| `setRequestedForecastTime(tag, timestampMs)` | `setEncodedForecastTime` | `setRasterArraySourceEncodedForecastTime` | 独立 V4 名称,`long`;Core ForecastClock 选择各 field window/mix。 |
| `setForecastPlaybackState(tag, "PLAY"|"PAUSE")` | `setEncodedForecastPlaybackState` | `setRasterArraySourceEncodedForecastPlaybackState` | V4 enum,绝不调用 V3 `controlRasterArraySourceTimeline`。 |
| `setForecastPlaybackSpeed(tag, "HALF"|"NORMAL"|"DOUBLE")` | `setEncodedForecastPlaybackSpeed` | `setRasterArraySourceEncodedForecastPlaybackSpeed` | Kotlin/Java 映射为 0.5/1/2;Core 保留 speed validity。 |
| `clearEncodedField(tag, fieldKey)` / `clearAllEncodedFields(tag)` | 同名 | 同名 V4-only JNI | 不清 V3 Host fields、不改变 V3 cadence/timeline。single-clear 返回 `CLEARED` / `FIELD_NOT_FOUND`;all-clear 返回 accepted command。 |
`ResolvedFieldProfileContract` 需要 `contractSchemaVersion, profileID, profileVersion, allowAdditionalPlanes, sharedValidityRequired, timeRelation, requiredPlanes[]`,每个 plane 为 `semanticID, valueClass, unitID, groupID, component, requiredFlag`。它是唯一确实需要 array 的稳定数据;当前 TurboModule codegen 对 public object ABI 已有历史约束,故不建议 `ReadableMap/ReadableArray`,也不应将每个平面拆成无边界的 JS 数组。推荐在 RN/Spec 中传受大小限制的 `contractJson`,在 JNI 的单一 parser 转为 C++ contract;Kotlin/Java 不语义解析。这样 Host/SDK 负责已经 resolved 的 profile contract,Native 不知道 provider/profile 业务;Core `FieldProfileValidator` 仍是 structural/semantic 唯一裁决者。
这不是让 JSON 承载 QBIN 或 server manifest:`contractJson` 是受 schema 版本约束的小 control-plane ABI。若 RN codegen 后续稳定支持 readonly typed object + typed array,才可在破坏性 ABI 版本中替换;本 Step 不建议 `ReadableMap`,因为它绕过 generated Spec、会把多层 field parsing 复制进 Kotlin。
## 兼容与所有权策略
- V3 `FieldRuntime.submitField`、`submitLegacyVectorField` 与现有 `FieldSubmitResult` 保留不变;V4 新增 `submitEncodedField` 与新的 `EncodedFieldSubmitResult` union,不能改名覆盖。
- `WeatherRuntime.setForecastTime`、`setPlaybackState`、`setPlaybackSpeed` 必须不复用:它们当前调用 V3 `seekRasterArraySourceTimelineToTimestamp` / `controlRasterArraySourceTimeline`。V4 用 `setEncoded...` Java/JNI names;RN public 名可按任务书使用 `setRequestedForecastTime` 等,但需明确是 V4 namespace / facade(例如 `EncodedFieldRuntime`),避免误导。
- `clearFields` 和 `clearEncodedFields` 必须分离。前者还参与 V3 state/cadence/disable 行为并调用 `clearHostFields()`;后者只清 `EncodedFieldStore`,保留 V4 bindings,正如 STEP-076 证明,且不得触及 V3。
- `setColorBinding` / `setParticleBinding` 不修改 WeatherColor/RasterParticle renderer properties;它们只存 V4 binding intent。实际 renderer 接线不在本 Step 范围。
## 错误模型
Promise rejection 仅用于桥接前条件、生命周期或无法执行;已进入 Core 的 `RasterArrayEncodedFieldSubmitResult` 必须 resolve 成可区分的 V4 result string,不能折叠为 `UNKNOWN` 或 `NATIVE_REJECTED`。
| Core result | 推荐 RN resolved result |
| --- | --- |
| `AcceptedInserted` | `ACCEPTED_INSERTED` |
| `AcceptedReplacedRevision` | `ACCEPTED_REPLACED_REVISION` |
| `Duplicate` | `DUPLICATE` |
| `RejectedDecode` | `REJECTED_DECODE` |
| `RejectedContract` | `REJECTED_CONTRACT` |
| `RejectedInvalidFieldKey` | `REJECTED_INVALID_FIELD_KEY` |
| `RejectedIdentityConflict` | `REJECTED_IDENTITY_CONFLICT` |
| `RejectedStaleRevision` | `REJECTED_STALE_REVISION` |
| `RejectedIncompatible` | `REJECTED_INCOMPATIBLE` |
| `RejectedCapacity` | `REJECTED_CAPACITY` |
桥接层 rejection code 应为:`INVALID_ARGUMENT`(tag、enum、整数、UTF-8/control size)、`PAYLOAD_FILE_UNREADABLE`(空/越限/非本地/不可读/读中变更)、`MAP_VIEW_NOT_FOUND`、`MAP_NOT_READY`、`STYLE_NOT_READY`、`WEATHER_OBJECTS_NOT_READY`、`RUNTIME_DISPOSED`,以及 V4 source 获取失败时的 `SOURCE_MISSING`。JNI 在 Source 不存在或类型不符时不得伪装为 Core `RejectedDecode`;应返回明确 native bridge status,再由 Java 映射为 `SOURCE_MISSING`。style 已存在但 source 尚未 ready 与 runtime disposed 同样不能混成 submit result。
## 生命周期与线程
`WeatherRuntime` 被 `MapLibreMap` 私有拥有,且是 style-scoped;`replaceWeatherRuntime` 先 dispose,`setStyle` 也 dispose,Kotlin 的 MapView detach listener 调用 `map.disposeWeatherRuntime()`。每个 bridge call 在 `runOnUiQueueThread` 通过 Fabric `reactTag -> UIManagerHelper -> MLRNMapView -> mapLibreMap` 当场解析;不允许新增 map registry、缓存 tag、长期保存 `MapLibreMap*` 或 native source pointer。
未来 V4 command 应沿用 `withRuntime`,在 Android UI 主线程上从当前 style 查 source。`NativeMapView.checkState` 是 Java 层 native lifetime guard;C++ 必须每次 `style.getSource(sourceId)` 和 `dynamic_cast<RasterArraySource*>`,不可缓存指针。style reload/detach/dispose 后,后续命令得到上述 lifecycle error;旧 V4 state 随 Source/style 销毁失效,Host 要在重新 attach、style ready 后按自己的缓存重新提交 raw QBIN 与 binding intent。
## 可用验证与后续验收
本调查没有执行 Android 编译、APK、ADB 或实体机操作。当前 Linux 可用入口与边界:
| 层 | 后续可验证项 | 边界 |
| --- | --- | --- |
| TS | `npx tsc -p packages/maplibre-particle-rn/tsconfig.json --noEmit`;Test Host `npm run typecheck` | package 未定义独立 typecheck script;依赖由 Test Host node_modules/paths 提供。 |
| TurboModule | RN codegen 由 package `codegenConfig`(`WeatherRuntimeBridgeSpec`、`src`、Kotlin package)与 Android Gradle/RN integration 驱动 | 没有发现完全脱离 Gradle 的仓库级 codegen 命令;实现 Step 应记录实际 generation task。 |
| C++/JNI | 头文件/`native_map_view.cpp` 的精确 compile 与 Linux Core unit tests;JNI registration list 与 Java native declarations 静态一一审计 | 精确 Android ABI/JNI compile 仍需 Android NDK Gradle/native build;Linux Core build 不证明 Android linkage。 |
| Java/Kotlin | `WeatherRuntimeStateTest` 是 Mockito/JVM facade 状态测试,可新增 V4 mapping/隔离测试 | 它依赖 Android Gradle test classpath;未发现无需 Gradle 的现成 Kotlin/Java standalone validation。 |
建议实施拆分:
1. 先冻结 RN Spec/types 与 Kotlin file/control validation,新增 V4 result union,并以 typecheck + codegen 审计验收。
2. 再新增 Java `WeatherRuntime` V4-only facade、`MapLibreMap` / `NativeMap` / `NativeMapView` Java declarations、C++ JNI registration 与 source lookup;JVM Mockito 覆盖 V3/V4 time-clear 不串线和 error mapping。
3. 最后才接 renderer consumer(独立 Step),以 C++ Source tests 证明 V4 projection、style reload/detach rejection、JNI exact build、Android Debug OpenGL/Vulkan 与离线 fixture 验收。不得下载真实 QBIN。
## 实施文件清单与风险
预期修改:`packages/maplibre-particle-rn/src/{types.ts,NativeWeatherRuntimeBridge.ts,FieldRuntime.ts}`、`WeatherRuntimeBridgeModule.kt`、`WeatherRuntime.java`、`MapLibreMap.java`、`NativeMap.java`、`NativeMapView.java`、`native_map_view.hpp/.cpp`,以及相应 JVM/C++ tests。不会改 package version、Test Host、Renderer/GPU。
主要风险为 codegen 对 complex object/array 的限制、64 MiB 下 Kotlin/Java/JNI 三份瞬时内存、JS number 精度(所有 uint64 必须限制在 safe integer 或改 decimal string ABI)、source/style 生命周期竞争、以及错接旧 V3 timeline。前四项通过 primitive+small-contract JSON、固定 64 MiB guards、UI queue late lookup 和 typed result 处理;最后一项通过 V4-only 方法命名、独立 NativeMap API 和隔离测试消除。

View File

@@ -0,0 +1,170 @@
# STEP-077 — QBIN2 V4 Android / JNI / RN 桥接只读调查 Codex 任务 v1
状态:完成
日期:2026-09-13
## 1. 任务性质
本任务是只读架构调查,不实现桥接,不修改 production code。
当前 `particle/main` / `origin/particle/main` 基线:
`0759a06104e1fb30f036db2efb6ef34ca5859759`
STEP-073~076 已完成 V4 Core、Decoder/Ingress、Binding/Projection Identity、`RasterArraySource` C++ Host API。
## 2. 调查目标
回答:V4 的以下能力应如何最小、稳定地穿过现有 Android/JNI/RN 链路:
```text
submitEncodedField
setColorBinding
setParticleBinding
setRequestedForecastTime
setForecastPlaybackState
setForecastPlaybackSpeed
clearEncodedField / clearAllEncodedFields
```
Host 仍不得控制 T0/T1/MIX。
## 3. 固定架构边界
必须保护:
```text
Test Host / NavSea JS/TS:下载、缓存、重试、gzip 解压、Manifest/Time Index
MapLibreParticle:只从 Host 已准备好的 raw QBIN bytes 开始
Core:选择 per-field T0/T1/MIX
Renderer:不解析 QBIN,不知道 provider/network/profile 业务
```
真实服务器 QBIN 不在本调查中下载或使用。
## 4. 必查实际链路
必须基于当前真实代码逐层记录:
```text
packages/maplibre-particle-rn/src/FieldRuntime.ts
packages/maplibre-particle-rn/src/NativeWeatherRuntimeBridge.ts
packages/maplibre-particle-rn/src/types.ts
WeatherRuntimeBridgeModule.kt
WeatherRuntime.java
MapLibreMap.java
NativeMapView.java
native_map_view.hpp / native_map_view.cpp
RasterArraySource V4 Host API
```
不得只引用旧 STEP 文档替代源码调查。
## 5. QBIN 传输形态调查
重点比较并给出推荐:
```text
A. RN 传 qbinFileUri,本地 Android 层读取后交给 Java/JNI
B. RN 直接传 byte[] / number[]
C. 其他现有可复用的本地 binary seam
```
必须结合当前 TurboModule/codegen、Kotlin 文件读取、Java byte[]、JNI array copy 与 64 MiB 限制分析。
默认倾向不得由 JS 构造大 number array / Base64;但最终结论必须以现有代码和成本为依据。
## 6. RuntimeMetadata / ProfileContract / Binding ABI 调查
分别给出跨层最小 ABI 推荐:
```text
EncodedFieldRuntimeMetadata
ResolvedFieldProfileContract + requiredPlanes[]
ColorBinding
ParticleBinding
Forecast playback state/speed/time
```
比较:typed primitive parameters、primitive arrays、JSON string、ReadableMap/ReadableArray 等可行形态。
结论必须说明每一层在哪里解析、在哪里只做边界校验、如何避免 Server/SDK/Host/Native 多处重复业务规则。
## 7. 兼容性与命名调查
必须明确:
```text
现有 V3 FieldRuntime.submitField 是否保留
V4 是否新增独立 submitEncodedField,而不是替换旧 submitField
WeatherRuntime 现有 setForecastTime / setPlaybackState / setPlaybackSpeed 是否可复用或必须新增 V4 语义 API
clearFields 与 clearEncodedFields 是否必须分离
旧 Renderer/V3 timeline 是否可能被 V4 命令误操作
```
不得为了表面统一而让 V4 clear/time/playback 复用会修改 V3 timeline 的旧实现。
## 8. 生命周期与线程模型
必须调查并说明:
```text
WeatherRuntime 与 MapLibreMap / MapView 的所有权
Fabric reactTag → MLRNMapView → MapLibreMap 的现有解析路径
Java 主线程约束
NativeMapView / Style Source 查找与生命周期
style reload / map detach / runtime dispose 后 V4 状态如何失效
```
不得引入第二套 Map registry 或长期保存 reactTag/map pointer 的新机制。
## 9. 错误模型调查
必须把 C++ `RasterArrayEncodedFieldSubmitResult` 与 RN 可见结果的推荐映射列全:
```text
AcceptedInserted
AcceptedReplacedRevision
Duplicate
RejectedDecode
RejectedContract
RejectedInvalidFieldKey
RejectedIdentityConflict
RejectedStaleRevision
RejectedIncompatible
RejectedCapacity
```
同时说明文件不可读、参数非法、runtime disposed、source missing/style not ready 等桥接层错误与 Core submit result 如何分层,不得混成一个 UNKNOWN。
## 10. 验证能力调查
不执行 Android 编译、APK、ADB 或实体机操作。
调查当前可在 Linux 完成的静态验证入口,包括:
```text
TurboModule codegen schema
TypeScript typecheck
JNI C++ 精确编译或 Linux 可编译部分
Java/Kotlin 是否有无需 Android Gradle 的现成静态验证入口
现有 WeatherRuntime JVM tests 的边界
```
给出后续实现 Step 的分层验收清单,不在本任务实际执行大规模 build。
## 11. 输出文档
必须生成:
```text
docs/20_调查/STEP-077_QBIN2_V4_Android_JNI_RN桥接调查报告_v1.md
docs/报告/STEP-077_QBIN2_V4_Android_JNI_RN桥接调查_Codex完成报告_v1.md
```
并追加 `粒子开发记录.md`。
调查报告必须包含当前链路图、推荐 V4 API 表、每层参数形态、错误映射、生命周期、兼容策略、实施文件清单、风险与后续实现拆分建议。
## 12. 禁止项
不得修改 production code、不得新增桥接 API、不得修改 package version、不得修改 Test Host、不得进入真实 QBIN 联调、不得修改 Renderer/GPU。
任务开始前已有 dirty/untracked 内容必须保持原样,不得清理、覆盖、暂存或提交。
## 13. Git 收尾
本任务会生成调查文档,因此完成后只提交本任务新增/修改的文档,commit 后 push `origin/particle/main`。
完成报告必须记录 commit hash、commit message、push result、remote branch、Final HEAD;push 失败不得声称任务完成。
完成后立即停止,不进入 STEP-077 实现。

View File

@@ -0,0 +1,29 @@
# STEP-077 QBIN2 V4 Android / JNI / RN 桥接只读调查 v1
状态:完成
日期:2026-09-13
## 目标与结果
- [x] 基于实际 RN、Kotlin、Java、JNI、`RasterArraySource` 源码完成 V4 桥接调查。
- [x] 给出 file URI 传输、small control-plane ABI、V3/V4 timeline/clear 隔离、结果和 lifecycle 映射建议。
- [x] 生成调查报告、完成报告并追加开发记录。
## 范围
只读调查;未修改 production code、package version、Test Host、Renderer/GPU,未下载真实 QBIN,未执行 Android 编译、APK、ADB 或实体机操作。
## 核心结论
V4 必须新增独立 `submitEncodedField` / binding / forecast / clear 命名空间。V3 `submitField`、`clearFields`、forecast/playback API 当前会进入 V3 Host field / timeline,不能复用。RN 传 `qbinFileUri`,Kotlin 以现有 64 MiB local-file guard 读取,再走 Java `byte[]` 和 JNI copy;不要使用 JS number array 或 Base64。
## 验证
- [x] 对要求的全部实际源码路径、JNI registration、Core public V4 API、package codegen/typecheck config 和 `WeatherRuntimeStateTest` 做静态审计。
- [x] `git diff --check`(仅本任务文档)将在提交前执行。
- [ ] Android/Gradle/APK/ADB/实体机:任务明确不执行。
## 阻塞与下一步
无调查阻塞。后续只能按报告中拆分实现 Step,先做 Spec/ABI 与 V4-only facade/JNI 隔离测试;本 Step 到此停止。