From 4467adcce02db7f1bbbea1f4a829023c53085e39 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Wed, 9 Sep 2026 07:11:17 +0800 Subject: [PATCH] feat: add RN binary wind field transport --- .../STEP-061_Binary_UV_Field_Transport_v1.md | 41 +++++++++ ...inary_UV_Field_Transport_Codex完成报告_v1.md | 83 +++++++++++++++++++ .../MapLibreParticleRNTestHost/App.tsx | 45 ++++++++++ .../MapLibreParticleRNTestHost/README.md | 14 +++- .../android/build.gradle | 2 +- .../weather/WeatherRuntimeBridgeModule.kt | 45 ++++++++++ .../src/NativeWeatherRuntimeBridge.ts | 3 + .../weather-runtime-bridge/src/index.ts | 1 + .../package-lock.json | 1 + .../MapLibreParticleRNTestHost/package.json | 1 + .../plugins/withParticleNativeAndroid.js | 2 +- .../src/canonicalWindFixture.ts | 55 ++++++++++++ platform/android/VERSION | 2 +- 粒子开发记录.md | 15 ++++ 14 files changed, 303 insertions(+), 7 deletions(-) create mode 100644 docs/50_Step记录/STEP-061_Binary_UV_Field_Transport_v1.md create mode 100644 docs/报告/STEP-061_Binary_UV_Field_Transport_Codex完成报告_v1.md create mode 100644 integration/react-native/MapLibreParticleRNTestHost/src/canonicalWindFixture.ts diff --git a/docs/50_Step记录/STEP-061_Binary_UV_Field_Transport_v1.md b/docs/50_Step记录/STEP-061_Binary_UV_Field_Transport_v1.md new file mode 100644 index 000000000..ba164e698 --- /dev/null +++ b/docs/50_Step记录/STEP-061_Binary_UV_Field_Transport_v1.md @@ -0,0 +1,41 @@ +# STEP-061 Binary U/V Field Transport + +- 状态:`进行中 / WAITING_RDC_REVIEW` +- 起始 HEAD:`e3c36880960ddb74abeafefe5c5662e1a61b46dc` +- 分支:`particle/main` + +## 目标 + +以既有 `WeatherRuntime.submitWindField(metadataJson, byte[])` 为唯一 Core ingress,完成 RN Test Host 的 canonical U/V 原始二进制 file-reference transport,并保留 Core 对 metadata、byteLength、Timeline 和 Runtime Field Window 的 fail-closed 所有权。 + +## 已完成 + +- [x] 调查确认 HEAD 与 `origin/particle/main` 一致;既有 Java/JNI/Core Host ingress 可复用。 +- [x] TurboModule 增加 `clearWindFields(reactTag)` 与 `submitWindField(reactTag, metadataJson, payloadFileUri)`。 +- [x] Kotlin 只读取受限的本地 `file://` URI 或绝对路径为原始 `byte[]`,再调用既有 WeatherRuntime;相对路径、非本地、空、不可读、`IOException` 及超过 64 MiB 文件均 fail-closed,metadata 上限按 UTF-8 bytes 计算。 +- [x] Test Host 用 Expo FileSystem 56.0.11 写入两个 4×3 `float32-le/interleaved-uv` canonical WIND/GLOBAL frame;`DataView.setFloat32(..., true)` 明确 little-endian。 +- [x] Test Host 初始/每次提交均为 PENDING,严格断言 invalid/T0/T1/Native state 依次为 `REJECTED_ADAPTER`/`ACCEPTED_PENDING_INITIAL_WINDOW`/`INITIAL_WINDOW_READY`/`READY`;不符即 ERROR。 +- [x] Runtime UI 区分 Core evidence 与 producer identity;MIX 标为 Core-owned/not exposed,最终 T0/T1 真值留给既有 Android logcat 事件验证。 +- [x] 坐标已准备为唯一 `13.4.1-step061`(`platform/android/VERSION`、plugin、bridge dependency、README);未覆盖 `step060f`,本轮未 build/publish。 +- [x] 本轮修正后的 `npm run typecheck`、TurboModule Codegen(输出至 `/tmp`)、plugin `node --check`、禁止 transport 审计与 `git diff --check` 通过。 + +## 允许修改 + +RN Test Host、weather-runtime-bridge、其 README/package 直接依赖,以及本 Step/完成报告/开发记录。 + +## 禁止修改 + +不修改 NavSea、provider/network/download/cache 业务、Particle/Core Timeline 数据模型或视觉参数;不提交 build 产物;不加入或运行 Jest。 + +## 当前结果 + +RDC NEEDS FIX 的 UI、I/O 与版本坐标修正已实现并通过本轮静态验证。Runtime snapshot 当前只公开 State/Playback/FPS;`INITIAL_WINDOW_READY + READY` 是 Core evidence,界面中的 generation/time 明确只是 producer identity,MIX 继续由 Core 独占且未新增观测 API。 + +## 未完成/阻塞 + +- [ ] RDC 对真实 diff 审查。 +- [ ] 审查通过并获后续明确指令后,在指定 `particle-codex:step061` tmux window 构建并 Maven Local publish 唯一 `13.4.1-step061` AAR,再继续 Android Gradle/APK 和实机端到端验收;本次按任务要求未执行。 + +## 下一步 + +停在工作区,等待 RDC 审查后再进行任务书限定的端到端流程。 diff --git a/docs/报告/STEP-061_Binary_UV_Field_Transport_Codex完成报告_v1.md b/docs/报告/STEP-061_Binary_UV_Field_Transport_Codex完成报告_v1.md new file mode 100644 index 000000000..8ac9654f0 --- /dev/null +++ b/docs/报告/STEP-061_Binary_UV_Field_Transport_Codex完成报告_v1.md @@ -0,0 +1,83 @@ +# STEP-061 Binary U/V Field Transport Codex完成报告 v1 + +日期:2026-09-09 +结论:`IMPLEMENTED / WAITING_RDC_REVIEW` + +## 调查结论与真实路径 + +开工前确认 `particle/main` 的 HEAD `e3c36880960ddb74abeafefe5c5662e1a61b46dc` 与 `origin/particle/main` 一致。STEP-053/057 已有唯一正式 ingress: + +`RN Test Host → WeatherRuntimeBridge → WeatherRuntime.submitWindField(metadataJson, byte[]) → MapLibreMap.submitRasterArraySourceHostField → JNI → RasterArraySource::submitHostField → LocalParticleFieldFeed → ParticleFieldDataAdapter → Core Timeline/Runtime Field Window`。 + +因此本次没有新建 RasterArray、Descriptor、Snapshot、Timeline 或 Adapter,也没有改动 Core。 + +## 修改清单 + +- `integration/react-native/MapLibreParticleRNTestHost/App.tsx`:新增固定 binary fixture 提交按钮与可见证据。 +- `integration/react-native/MapLibreParticleRNTestHost/src/canonicalWindFixture.ts`:新增确定性 4×3 T0/T1/invalid canonical WIND fixture。 +- `integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/src/NativeWeatherRuntimeBridge.ts`、`src/index.ts`:新增稳定 `clearWindFields` 与 `submitWindField(metadataJson, payloadFileUri)` contract/result type。 +- `integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/android/src/main/java/.../WeatherRuntimeBridgeModule.kt`:本地文件读取、大小保护与既有 WeatherRuntime 调用。 +- `integration/react-native/MapLibreParticleRNTestHost/package.json`、`package-lock.json`:将现有实际安装的 `expo-file-system@56.0.11` 提升为 Test Host 直接依赖。 +- `integration/react-native/MapLibreParticleRNTestHost/README.md`、本 Step 记录与开发记录:补充 transport contract/状态。 +- `platform/android/VERSION`、`integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeAndroid.js`、bridge `android/build.gradle`:只将待发布坐标统一为 `13.4.1-step061`,不改 Core 行为。 + +## RDC NEEDS FIX 修正 + +- App 初始状态和每次提交开始都为 PENDING;仅当 invalid/T0/T1/Native state 严格分别等于 `REJECTED_ADAPTER`、`ACCEPTED_PENDING_INITIAL_WINDOW`、`INITIAL_WINDOW_READY`、`READY` 时才显示 READY。任一不符立即显示 ERROR。 +- Runtime Field Window 文案改为 `Core evidence: INITIAL_WINDOW_READY + runtime READY`,另行标注 producer identity;MIX 为 `Core-owned / not exposed`。Android 最终 T0/T1 真值继续由既有 `HOST_FIELD_SNAPSHOT_READY` 与 `HOST_FIELD_INITIAL_WINDOW_READY` logcat 事件验证,不新增 Core getter。 +- Kotlin metadata 长度按 UTF-8 bytes 检查;无 URI scheme 的路径必须 absolute,并显式捕获 `IOException` 为 `PAYLOAD_FILE_UNREADABLE`。读取相关其他运行时异常同样 fail-closed。 +- 版本坐标统一为新的 `13.4.1-step061`;`13.4.1-step060f` 保持冻结。本轮依照明确限制未实际 build AAR 或 Maven publish。 + +## Binary transport + +JS 以 `ArrayBuffer + DataView.setFloat32(offset, value, true)` 明确写入 `float32-le`,每个 row-major north-west grid cell 连续写入 U(eastward)再 V(northward),layout 为 `interleaved-uv`。Expo `File.write(Uint8Array)` 将 raw bytes 写入 App cache。跨 TurboModule 的仅有 `metadataJson + file:// URI/path`,不传 `number[]`、`Array`、payload JSON、base64 或 hex。 + +Kotlin 仅接受 `file://` 或绝对本地路径,拒绝其他 scheme 和相对路径;metadata 限制按 UTF-8 bytes 检查。检查文件存在、可读、非空及不超过 64 MiB 后读取 raw `byte[]`,显式捕获 `IOException`,直接调用既有 `WeatherRuntime.submitWindField(metadataJson, payload)`。文件错误不会提交空数据或伪造成功。metadata/payload 的 schema、grid、bounds、layout、encoding、byteLength、generation、validTime 等最终校验继续完全由 `LocalParticleFieldFeed`/`ParticleFieldDataAdapter` fail-closed。 + +## RN 验收证据与预期 + +`Submit binary WIND T0/T1` 先调用既有 `clearWindFields()`,再提交: + +1. 声明正常 byteLength、实际少一个 byte 的 INVALID:预期 `REJECTED_ADAPTER`。 +2. generation=1、validTime=T0 的 frame:预期 `ACCEPTED_PENDING_INITIAL_WINDOW`。 +3. generation=2、validTime>T0 的兼容 frame:预期 `INITIAL_WINDOW_READY`。 + +UI 初始与每轮开始均显示 PENDING;只有四个严格断言全部通过才显示 `READY: FILE_REF / FLOAT32_LE / INTERLEAVED_UV`。`Core evidence: INITIAL_WINDOW_READY + runtime READY` 是已建立 initial window 的证据;generation/time 单列为 producer identity,不计算或持有 T0/T1/MIX runtime 状态;MIX 仍为 Core-owned/not exposed。最终 Android T0/T1 真值由既有 `HOST_FIELD_SNAPSHOT_READY` / `HOST_FIELD_INITIAL_WINDOW_READY` logcat 验证,本任务未新增 Core getter。 + +## 实际静态验证 + +- PASS(本轮修正后):RN Test Host `npm run typecheck`(build-output-throttle)。 +- PASS(本轮修正后):RN 0.85.2 Codegen 输出至 `/tmp/step061-weather-runtime-codegen`;Java abstract methods 仍为 `clearWindFields(double, Promise)` 与 `submitWindField(double, String, String, Promise)`,与 Kotlin override 一致;未提交生成物。 +- PASS(本轮修正后):`node --check plugins/withParticleNativeAndroid.js`。 +- PASS(本轮修正后):禁止 transport 审计未发现 base64、hex、number-array 或 raw-payload-JSON;`git diff --check` 通过。 + +## 未执行与范围 + +未执行 `make apackage`、AAR build/publish、Windows Maven Local、Windows APK、Android Gradle APK、ADB、设备/端到端验收,也未加入、运行 Jest。下一阶段需构建并 Maven Local publish 唯一 `13.4.1-step061` AAR,绝不覆盖冻结的 step060f;本轮 Codex 任务书限制在 RDC 源码审查前不得构建/发布 AAR。未修改任何 MapLibre Core、JNI、WeatherRuntime、NavSea、provider/network/vendor/download/cache 或粒子视觉代码。 + +## 工作区状态 + +本次未 commit、未 push。最终 `git status --short` 与本任务有关的范围为: + +```text + M integration/react-native/MapLibreParticleRNTestHost/App.tsx + M integration/react-native/MapLibreParticleRNTestHost/README.md + M integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/android/src/main/java/.../WeatherRuntimeBridgeModule.kt + M integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/src/NativeWeatherRuntimeBridge.ts + M integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/src/index.ts + M integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/android/build.gradle + M integration/react-native/MapLibreParticleRNTestHost/package-lock.json + M integration/react-native/MapLibreParticleRNTestHost/package.json + M integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeAndroid.js + M platform/android/VERSION + M 粒子开发记录.md +?? integration/react-native/MapLibreParticleRNTestHost/src/canonicalWindFixture.ts +?? docs/50_Step记录/STEP-061_Binary_UV_Field_Transport_v1.md +?? docs/报告/STEP-061_Binary_UV_Field_Transport_Codex完成报告_v1.md +``` + +开始前已存在大量历史 untracked 文档、`STEP-046-e48fa087-review.diff`、Test Host `android/` 生成目录及 bridge `android/build/`;均未触碰。`.codex-build-logs/` 为验证工具生成的未跟踪日志目录,未纳入提交。 + +## 停止条件 + +已停在工作区,等待 ChatGPT 通过 RDC 审查真实 diff;审查前不继续 AAR 或 Android 端到端流程。 diff --git a/integration/react-native/MapLibreParticleRNTestHost/App.tsx b/integration/react-native/MapLibreParticleRNTestHost/App.tsx index 0d43bf6ed..e696942fb 100644 --- a/integration/react-native/MapLibreParticleRNTestHost/App.tsx +++ b/integration/react-native/MapLibreParticleRNTestHost/App.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { Button, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View, findNodeHandle } from "react-native"; import { Map } from "@maplibre/maplibre-react-native"; import { WeatherRuntimeBridge, type RuntimeSnapshot, type TargetFps } from "@maplibre-particle/weather-runtime-bridge"; +import { writeCanonicalWindFixture } from "./src/canonicalWindFixture"; import { weatherTestStyle } from "./src/weatherStyle"; const initialSnapshot: RuntimeSnapshot = { state: "WAITING_FOR_FIELDS", playbackState: "PAUSE", targetFps: "AUTO", disposed: false }; @@ -11,6 +12,11 @@ export default function App() { const [snapshot, setSnapshot] = useState(initialSnapshot); const [result, setResult] = useState("Style is loading"); const [opacity, setOpacity] = useState("0.62"); + const [binaryTransport, setBinaryTransport] = useState("PENDING: submit fixture to validate FILE_REF / FLOAT32_LE / INTERLEAVED_UV"); + const [invalidProbe, setInvalidProbe] = useState("PENDING"); + const [t0Submit, setT0Submit] = useState("PENDING"); + const [t1Submit, setT1Submit] = useState("PENDING"); + const [fieldWindow, setFieldWindow] = useState("TIMELINE_NOT_READY"); const attach = useCallback(async (tag: number) => { try { setSnapshot(await WeatherRuntimeBridge.attach(tag)); setReactTag(tag); setResult("WeatherRuntime attached"); } @@ -34,6 +40,39 @@ export default function App() { catch (error) { setResult(`${name}: ${String(error)}`); } }, [reactTag]); + const submitBinaryFixture = useCallback(async () => { + if (reactTag == null) return setResult("MAP_NOT_READY: wait for style ready"); + try { + setBinaryTransport("PENDING: clearing fields and validating FILE_REF / FLOAT32_LE / INTERLEAVED_UV"); + setInvalidProbe("PENDING"); + setT0Submit("PENDING"); + setT1Submit("PENDING"); + setFieldWindow("PENDING: awaiting Core initial window"); + const clear = await WeatherRuntimeBridge.clearWindFields(reactTag); + if (clear !== "ACCEPTED") throw new Error(`clearWindFields: ${clear}`); + const [t0, t1, invalid] = writeCanonicalWindFixture(); + const rejected = await WeatherRuntimeBridge.submitWindField(reactTag, invalid.metadataJson, invalid.payloadFileUri); + setInvalidProbe(rejected); + if (rejected !== "REJECTED_ADAPTER") throw new Error(`invalid probe expected REJECTED_ADAPTER, got ${rejected}`); + const t0Result = await WeatherRuntimeBridge.submitWindField(reactTag, t0.metadataJson, t0.payloadFileUri); + setT0Submit(t0Result); + if (t0Result !== "ACCEPTED_PENDING_INITIAL_WINDOW") throw new Error(`T0 expected ACCEPTED_PENDING_INITIAL_WINDOW, got ${t0Result}`); + const t1Result = await WeatherRuntimeBridge.submitWindField(reactTag, t1.metadataJson, t1.payloadFileUri); + setT1Submit(t1Result); + if (t1Result !== "INITIAL_WINDOW_READY") throw new Error(`T1 expected INITIAL_WINDOW_READY, got ${t1Result}`); + const native = await WeatherRuntimeBridge.getState(reactTag); + setSnapshot(native); + if (native.state !== "READY") throw new Error(`Native WeatherRuntime expected READY, got ${native.state}`); + setFieldWindow(`Core evidence: INITIAL_WINDOW_READY + runtime READY; producer identity: T0=${t0.generation}@${t0.validTime}, T1=${t1.generation}@${t1.validTime}; MIX: Core-owned / not exposed`); + setBinaryTransport("READY: FILE_REF / FLOAT32_LE / INTERLEAVED_UV"); + setResult(`Binary field submit: invalid=${rejected}, T0=${t0Result}, T1=${t1Result}`); + } catch (error) { + setBinaryTransport(`ERROR: ${String(error)}`); + setResult(`Binary field submit: ${String(error)}`); + setFieldWindow("TIMELINE_NOT_READY"); + } + }, [reactTag]); + const buttons = useMemo(() => [ ["getState", (tag: number) => WeatherRuntimeBridge.getState(tag)], ["enable", (tag: number) => WeatherRuntimeBridge.setWeatherEnabled(tag, true)], @@ -49,7 +88,13 @@ export default function App() { reactTag: {reactTag ?? "not attached"} runtime: {snapshot.state} / {snapshot.playbackState} / {snapshot.targetFps} + Binary Transport: {binaryTransport} + Invalid Probe: {invalidProbe} + T0 Submit Result: {t0Submit} + T1 Submit Result: {t1Submit} + Runtime Field Window: {fieldWindow} {result} +