Skip to content

refactor: cross-layer cleanup and sink duplicated native logic into the shared C++ cores - #657

Merged
sunnylqm merged 109 commits into
masterfrom
claude/code-cleanup-refactor-dd7306
Oct 12, 2026
Merged

sunnylqm merged 109 commits into
masterfrom
claude/code-cleanup-refactor-dd7306

Conversation

@sunnylqm

@sunnylqm sunnylqm commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

概要

一轮全仓清理和三端收敛,分两部分:

  1. 按层清理:JS、Android、iOS、鸿蒙、C++ 胶水、CI 和文档,删掉死代码、合并重复实现、简化写法。
  2. 下沉到共享 C++:Android、iOS、鸿蒙原先各写一份的纯逻辑,现在只在 cpp/ 里保留一份,三端只剩薄绑定。

顺带修了几处三端行为不一致的问题,详见下文。

规模

范围 增 删
src/(不含测试) +453 −713
android/src +1119 −2213
ios/ +1716 −2398
harmony/ +1222 −2975
cpp/(不含 tests) +3347 −667
cpp/**/tests(新增向量和用例) +4427 −21
docs/ +141 −788

平台代码合计净减约 3500 行。新增的 C++ 大部分是共享内核,替代了三端各自的实现,并且有测试向量和 fuzz 覆盖。

一、按层清理

  • JS
    • 合并两份 deep-equal、两条上报 POST,错误出口统一为 fail()。
    • 新增 web 安全的 nativeMethod。
    • 端点处理去掉重复的去重和排除。
    • error.ts 的错误码只写一份。
    • 清理死导出和未使用的 locale key。
    • 测试:新增共享 helper(helpers.ts / nativeSources.ts),mock 一律透传真实模块;provider 渲染测试改用真实 client。
  • Android
    • 新旧架构合并为一个 UpdateModule:oldarch 只留约 70 行手写的 NativePushySpec。
    • 改用 lambda,新增 Threads 工具,合并 UiThreadRunner。
    • IO 和 HTTP 工具去重,删除 Gradle 7.5 以下的分支和两条无用的 ProGuard 规则。
  • iOS
    • RCTPushy.mm(约 2140 行)按职责拆成 State / InstallRecord / Installer / Launch 四个文件。RCTPushyInstaller 自己持有文件队列,+engine 已删除。
    • dispatch_once 改为函数内 static。
    • 删除 +rollback、RN 0.62 以前的 reload 兜底分支,以及 podspec 里失效的分支和路径。
  • 鸿蒙
    • 删除死任务类型、isFirstTime / clearFirstTime,以及提交进仓库的生成文件 BuildProfile.ets 和 OAT.xml。
    • NAPI 的异步样板抽成 QueueWork;TurboModule 方法表改为 X-macro,只写一遍。
    • HAR 构建去掉无用的 staging 拷贝。
    • hypium 套件(CI 从未运行)中的纯逻辑用例迁到 bun。
  • C++ 和构建
    • 只给 Android 用的 JNI 胶水挪到 cpp/android_jni/,hpatch 挪到 cpp/hpatch/。
    • Android.mk、CMake、podspec 和测试脚本都改为按目录收集源文件,不再在 5 处手抄同一份清单。
    • 重新编译 android/lib/*/librnpushy.so(NDK 28.2.13676358)。
  • CI 和脚本
    • 新增 composite action e2e-common 和 publish-npm,e2e workflow 去重。
    • check-constant-parity.js 移到 scripts/,新增 Java 和 JS 常量的比对,并接入 bun lint。
    • check-packlist 改为从 files 推导,C++ 测试脚本抽出公共部分。
  • 文档:精简 NATIVE_CHECK_FOLLOWUPS.md(529 → 44 行)、CODE_AUDIT.md 和 BUNDLEHASH_MIGRATION.md,修正悬空引用。

二、下沉到共享 C++

逻辑 新位置 统一掉的差异
配置门控、配置等价、配置校验 update_flow_core/native_config disabled 一律按 JS 真值判断(Android 原来只认 true / "true");appKey 按 JS 的 trim 规则;配置 JSON 后面跟多余内容一律拒绝
__diff.json 清单解析 patch_core/patch_manifest,三端共用一个 applyArchiveDelta 入口 deletes 接受对象或数组;copiesCrc 按 copy 目标取值并严格校验类型;类型错误一律报错,不再强转;hbcTransform 不是对象时报错,原来会被静默忽略
安装记录 patch_core/install_record 写入统一为"临时文件 + fsync + rename + 目录 fsync";文件 stamp 复核原来只有 iOS 做,现在三端都做;schema 和字段类型严格校验
编排器纯逻辑(请求组装、端点查询循环、hashInfo、响应缓存、attempt 规划) update_flow_core/native_round 非字符串字段一律跳过,不再强转;鸿蒙遇到畸形配置不再整轮失败
归档限额 patch_core/archive_limits 压缩比改为精确比较;整包压缩比、symlink 拒绝、各层路径的保留名检查,三端都执行

每处差异都在测试向量里记下了改动前各端的实际输出。

修复的问题

  • 原生编排器遇到 PATCH_FAILED 后不再逐个镜像重下重打,直接换下一个策略,和 JS 一致。
  • Android 和 iOS 补上远程端点及制品 URL 的 https-only 过滤,原来只有鸿蒙做。
  • Object.hasOwn 改为 hasOwnProperty.call:iOS 15.4 以前的 JSC 没有这个方法,原生缓存比对会抛错并静默判为不命中。
  • JNI 字符串改用 NewString,非 BMP 字符不再出错。
  • patch_core.cpp 改为用相对路径引用 hpatch.h,use_frameworks! 构建不再解析失败。

验证

检查 结果
bun lint(biome、tsc、鸿蒙类型、spec 一致性、常量一致性) ✅
bun test src/__tests__(含 --randomize) ✅ 435/435
test:patch-core / test:flow-core(含 SANITIZE=1),新增 fuzz 目标 ✅
iOS purge-restore(含 SANITIZE=1 VERIFY_REGRESSIONS=1) ✅ 10/10,负向探针全部检出
check-packlist(普通和 --release)、verify-android-so、check-binary-strings ✅
Android R8 混淆构建 + dexdump ✅ 6 个 JNI 类、18 个 native 方法和结果类的 <init>、字段都保留;混淆包装机后完整走通一次更新
Android e2e(core + 原生冷启动) ✅ 3/3 + 1/1
iOS e2e(core + 原生冷启动) ✅ 3/3 + 1/1
鸿蒙 e2e(release hap) ✅ 11/11
iOS use_frameworks! dynamic 构建 ✅ bridge reload 路径已编入
旧架构(RN 0.77)Android 构建,在模拟器上运行 ✅

没有做的(需要另行决定)

  • 没把 lint.yml 并入 test.yml:required check 的名字会变,要同步改分支保护。
  • 没删 domains.json:已部署的老客户端可能还会去 master 拉它。
  • 没替换 hbc_transform_wire 的手写 JSON 解析器:差异测试发现新旧解析器在 6 类边界输入上行为不同。
  • ensureFreeSpace 在可用空间为 0 时的处理三端仍不一致:iOS 当作真实值,Android 和鸿蒙当作"未知"放行。改成最严格有误报风险。
  • Example/e2etest/ios/Podfile.lock 没更新:本机 pod install 生成的结果带着 worktree 的绝对路径。CI 不用 --deployment,不受影响。

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

sunnylqm and others added 30 commits October 12, 2026 14:41
Move check-constant-parity.js from harmony/pushy/src/test to scripts/ and
extend it to compare archive_limits.h / install_record.h / error_codes.h
against ArchiveLimits.java / InstallRecord.java / ErrorCodes.java and the
error codes known to src/error.ts. Runs as part of bun lint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jsonDeepEqual (native config equivalence, C1) called Object.hasOwn, which
throws on iOS < 15.4 JSC. Use Object.prototype.hasOwnProperty.call and add a
test that runs the comparison with Object.hasOwn removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
client.ts carried its own deep-equal (jsonValuesEqual) for the native
check-cache request comparison; the only difference to updateFlowCore's
jsonDeepEqual was skipping undefined-valued members. Fold that rule into
jsonDeepEqual (a no-op for the JSON-parsed native configs it already
compared; flow vectors unchanged) and export it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The union type and KNOWN_CODES listed every code twice; keep a single
'as const' array and derive both from it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The native cold-start orchestrator retried every mirror of a strategy even
when the artifact downloaded fine but could not be applied (PATCH_FAILED).
Every mirror serves the same bytes, so match JS client.ts and the C++
IsMirrorRetryableCode: break out to the next strategy instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On web PushyModule is a noop Proxy, so every 'typeof PushyModule.x ===
"function"' feature detect was true and each call site carried its own
Platform.OS === 'web' guard plus try/catch to turn a synchronous bridge
throw into a rejection. nativeMethod(name) returns undefined on web and for
older natives, and always returns a promise; the per-call guards and the
dead web branch of ping() (web never reaches a download) are gone.

The VM-evaluated client harness in nativeConfiguration.test.ts depended on
client.ts's free identifiers; its assertions now live in client.test.ts
(native ownership suppresses constructor/setOptions writes until handed
back), so it is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- UpdateContext isFirstTime()/clearFirstTime() and the ArkTS
  STATE_OP_CLEAR_FIRST_TIME mirror (the C++ enum stays).
- DownloadTask types that are never created (TASK_TYPE_CLEANUP,
  TASK_TYPE_PLAIN_DOWNLOAD): doCleanUp, their switch cases, the plain
  download failure branch and the now always-true isPatchTask checks. The
  params default type becomes TASK_TYPE_PATCH_FULL.
- Test-only ErrorCodes.parseErrorCodePrefix, unused
  ARCHIVE_PATCH_TYPE_FULL, an empty extern "C" block in pushy.cpp.
- PushyPackage.h used a copy-pasted GEO_LOCATION_PACKAGE_H guard.
- BuildProfile.ets is regenerated by hvigor on every build (nothing
  imports it): untrack and ignore it. OAT.xml was an unused template.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- getRemoteEndpoints no longer filters out configured endpoints: the
  endpoint fallback already dedupes candidates and drops failed ones.
- getConfiguredCheckEndpoints returns server.main as is (deduplicated on
  assignment and again by the endpoint plan).
- Inline the single-caller getCheckUrl; the per-endpoint failure log moves
  from fetchCheckResult's tryEndpoint wrapper into requestCheckResult.
- endpoint.ts no longer re-exports dedupeEndpoints; fetchWithTimeout drops
  the unused params.signal fallback; drop explicit default timeouts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…flags

Expo SDK 50+ (the only caller of the node fallback) needs Gradle 8, so
the Project.exec branch is dead. Set version once, drop the commented
useExpoPublishing, and declare Java 8 explicitly so lambdas compile on
AGP < 4.2. Android.mk repeated -std=c++17 and --exclude-libs that
Application.mk already applies to every module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ewStringUTF

update_core_android.cpp passed std::string values to NewStringUTF, which
expects JNI modified UTF-8 and mangles supplementary (4-byte) code points.
Move update_flow_jni.cpp's correct UTF-8 -> UTF-16 ToJString into the shared
jni_util.h and use it for every Java string the patch/state glue returns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
reportExceptionToServer and reportToServer each built the same POST to
{endpoint}/report/{appKey}; both now go through postReport. The event
shape {type,message,code,data} is a single ReportEvent type, the
single-caller send closure is inlined, and captureException's guard absorbs
the duplicated opt-out checks (plus the no-hot-update check).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- delta::BuildPatchManifest (patch_core.h): one pure builder for the
  parallel copy_froms/copy_tos lists with the length-mismatch check, so the
  Harmony bridge can reuse it too; jni_util::ReadPatchManifest wraps it for
  the three JNI entry points that built it by hand.
- jni_util::GuardJni replaces the six copies of the
  catch(std::exception)/catch(...) -> RuntimeException block.
- Drop the pure forwarders ToArchivePatchType and MakeStateResult
  (NewStateCoreResult takes the default arguments now).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The toUpdateError -> emitError -> throw/throwIfEnabled sequence was
spelled out at every catch site; fail() does the first two and returns the
error. runBeforeReload now wraps a beforeReload throw as USER_HOOK_ERROR
itself (event type/data derived from the context) instead of switchVersion
and restartApp each doing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…round

The cold-start orchestrator re-downloaded the same artifact from every
mirror even when the failure was a patch failure, which no mirror can
change. Break out to the next strategy like update_flow_core's
IsMirrorRetryableCode and the JS client do. The download-failure code
mapping moves to ErrorCodes so the module and the orchestrator share it,
and the attempt loop takes a DownloadStarter so it is JVM-testable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Same rule as Harmony's SyncCoordinator (isHttpsOnly): when every
configured endpoint is https, endpoints fetched from queryUrls and
artifact URLs in the decision must not downgrade the round to http.
Non-https remote endpoints and cleartext artifact URLs are skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- patchFailed/patchErrorMessages are exactly the exhausted strategies
  (only PATCH_FAILED exhausts one, and an exhausted strategy never runs
  again), so derive them instead of tracking them in parallel.
- Drop the unreachable 'failed without lastError' branch: every attempt
  runs at least one download, so a failed download always has an error.
- Progress listeners are local subscriptions released in finally (also on
  an unexpected throw) instead of sharedState.progressHandlers entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The four promise-returning bindings (applyDeltaFromSource,
cleanupOldEntries, sha256HexFileAsync, sha256HexRawFdAsync) each repeated
~70 lines of create-promise / create-async-work / queue / settle / free.
QueueWork<W>(env, name, data, execute, settle) now owns that lifecycle;
every failure path (promise creation, work creation, queueing, aborted
work) still settles the promise exactly once and frees the call.

NewStateResult takes a StateResultExtras struct with defaults instead of
five positional extras, and NewString absorbs MakeUtf8String (it now
throws on failure like the latter did).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Method names were written twice (host-function macro + registerMethod)
and could drift. PUSHY_METHODS(X) now lists (name, argc, SYNC|ASYNC) once
and generates both the host functions and the registrations.
check-native-spec-parity.js parses that table: a spec method missing from
it, a Promise method registered SYNC, a SYNC entry wrapping an async ArkTS
method, or a duplicated row all still fail the check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/lib/cpp-test-common.sh holds CORE_WARNINGS, the SANITIZE=1 flags
and the libFuzzer probe that test-patch-core.sh and test-update-flow-core.sh
each carried a copy of. The probe also tries Homebrew LLVM (Apple clang has
no libFuzzer runtime) and honours FUZZ_CXX. test.yml installs clang with the
other build dependencies instead of a second apt-get update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- checkUpdate/downloadUpdate share surfaceCaughtError for errors that
  bypassed the client pipeline (user hooks).
- switchVersion/switchVersionLater come from one forUpdateHash factory.
- restartApp/resetToPackagedBundle are the client's own stable arrow
  methods, like markSuccess.
- downloadAndInstallApk drops the Android check the client already does.
- Linking cleanup prefers subscription.remove() and only falls back to
  removeEventListener on RN < 0.65 (no deprecation warning on 0.65-0.69).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The allowed prefixes were a hand copy of package.json `files`. Build them
from `files` instead (globs included) and also flag a packed path that a
negated `files` entry should have excluded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- writeFileContent writes through SaveFile.writeFully (now exported, and
  checks short writes) instead of its own 64KB chunk loop;
  toArrayBufferSlice and FILE_COPY_BUFFER_SIZE go away.
- SaveFile.unlinkIfExists replaces the access-then-unlink pattern in
  DownloadTask and InstallRecord.
- One buildResumeMeta closure builds the resume sidecar for both the
  initial and the final write.
- 'bundle.harmony.js' literals use HARMONY_BUNDLE_FILE_NAME.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
downloadFullUpdate/downloadPatchFromPackage wrapped executeTask in a
try/log/rethrow that duplicated DownloadTask's own failure log
(downloadPatchFromPpk never had it). Preferences.getAllSync exists since
API 10 and the HAR targets API 12, so reset calls it directly; only the
API 14 flushSync keeps its structural probe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- i18n: getLocale/addTranslations and the I18n class export were only
  used by tests; t() resolves its template in one expression; the stale
  usage comment is gone. Tests use the real singleton instead.
- locales: checking_update / download_progress were only referenced by
  the i18n test and that comment.
- telemetry ServerReportPayload, type.ts Up-to-date/UpdateAvailable/
  Expired CheckResult variants (not exported from index), core
  isFirstTimeDebug, the utils joinUrls re-export (tests import it from
  updateFlowCore) and the defaultContext export.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No caller ever passed it (the client uuid is inside the request body's
cInfo). The fingerprint is in-memory only and not part of the flow
vectors (unchanged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
runQueryRequest walked configured and remotely discovered endpoint bases
with two copies of the normalize / dedupe / budget / request / validate
sequence; one tryBase closure (plus claimAttempt for the shared 8-request
budget) now serves both. The three commitSyncResult calls share one
commit helper that also maps "reset during the round" to cancelled/reset,
and the response cache JSON is built once.

harmonySyncQuery.test.ts pins the fallback order, dedupe, cleartext
filter, single remote list and request budget (it passes against the
previous implementation too).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… uuid

The four hilog levels shared one body, now Logger.log(fn, args).
getConstants logged a hand-copied subset of its result to keep the
install uuid out of hilog; it now logs the whole result without uuid
(plus uuidSet), so new constants are not silently left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
isRecordForVersion (presence) and expectedBundleSha256ForActivation
(activation) each spelled out the legacy-empty and schema/version rules.
Activation now reuses isRecordForVersion and only adds the mandatory
bundle digest (#655) on top, so the two cannot drift.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
restartApp is API 12, the HAR's minimum (compatibleSdkVersion 12), so
the structural typeof probe is dead. A throwing restartApp still falls
back to startAbility/terminateSelf exactly as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
src/oldarch now only holds a hand-written NativePushySpec mirroring the
codegen spec (name, constants hook, abstract methods), so UpdateModule
in src/main absorbs UpdateModuleImpl and extends NativePushySpec under
either architecture. Every spec method carries @ReactMethod for the old
bridge, which reads the annotations from the module class itself. The
unused UpdateModuleImpl(ReactApplicationContext) constructor goes with
it; removeListeners takes the codegen's double on the old bridge too.

scripts/check-native-spec-parity.js still reads the deleted per-arch
UpdateModule files and needs to follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sunnylqm and others added 24 commits October 12, 2026 16:06
…izer

pushy.cpp exports configsEquivalent / runnableConfig /
normalizeConfiguration over cpp/update_flow_core/native_config.h.
UpdateContext's ArkTS canonicalizer, SyncCoordinator's hand-rolled gate
and PushyConfiguration's validator (with the default addresses and
POLICY_NEXT_LAUNCH) are gone; the public PushyConfiguration type stays.

The bun tests no longer evaluate those ArkTS copies: the C1 vectors run
against the JS client only (the core replays them in C++), the
normalization cases moved to native-config-vectors.json, and the
evaluated Harmony modules get a NativePatchCore stand-in whose gate is
checked against the same vectors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
flowjson::Parse gains a ParseLimits overload (input bytes, node count) for
documents bounded by something other than a checkUpdate response; the
patch core's __diff.json parser needs its 16 MB manifest cap. Objects
with more than 16 members are deduplicated through a hash index while
parsing instead of Value::Set's linear scan, so a manifest's copies map
of thousands of members no longer parses in quadratic time. Duplicate
keys keep the first position and the last value as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ry point

cpp/patch_core/patch_manifest.h: ParsePatchManifest(text, options) is the
single parser of a patch archive's manifest, taking the shape pushy-go and
the CLI emit (copies to->from, copiesCrc per target, deletes object,
hbcTransform per bundle patch entry) under these rules, where the three
platform parsers used to differ:
 - deletes: an object's keys or an array of strings (Android ignored
   arrays); any other type, or a non-string element, is an error;
 - copiesCrc: keyed by copy target, an integer in [0, 2^32) or null.
   Android keyed it by source with the last target winning; now two
   targets of one source with different CRCs fail the manifest, and a
   string/fractional/oversized/boolean CRC fails instead of being coerced
   or skipping the content check;
 - types: copies must be an object of strings (Harmony coerced with
   String(), Android threw, iOS rejected); a null or empty source means the
   target's own path on every platform (Harmony ppk kept "");
 - copy_source_prefix (HarmonyOS rawfiles) is stripped only at the start
   (Harmony replaced its first occurrence anywhere);
 - hbcTransform and its bundle entry must be objects when present; the
   platforms silently dropped other types and applied the patch untransformed.
Every path still goes through ValidateManifest. The text is capped at
kMaxManifestBytes and 4 * kMaxEntries values.

PatchManifest now carries bundle_hbc_transform_meta (moved from
FileSourcePatchOptions). archive_patch gains PlanArchivePatch (entry check,
parse, merge plan) and ApplyArchivePatch (plan + apply; with
host_copies_resources it only rebuilds the bundle and hands the copies
back as CopyGroups, which now carry the source's CRC). BuildCopyGroups is
linear in the number of copies.

The test replays tests/patch-manifest-vectors.json (server shapes, the
drift cases, hostile paths) and covers the caps, the plan and both apply
modes, including a transform-domain patch driven by the manifest; FUZZ=1
adds a libFuzzer target for the parser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DownloadTask no longer parses the manifest: extractPatchArchive keeps the
raw __diff.json bytes and both patch tasks call one native,
applyArchiveDelta (JNI over archive_patch::ApplyArchivePatch), which
checks the entries, parses and validates the manifest, rebuilds the
bundle and, for ppk patches, copies/deletes/merges. The APK task passes
hostCopiesResources and gets back the copy groups, now with the CRC each
source must match, for BundledResourceCopier.

The manifest crosses JNI as byte[] rather than a jstring, so paths with
supplementary characters are no longer re-encoded as modified UTF-8.
appendManifestEntries, the buildArchivePlan/buildCopyGroups/
applyDeltaFromSource natives, ArchivePatchPlanResult and buildCopyList's
canonical-path check are gone: ValidateManifest rejects every lexical
escape (absolute, '..', '.', empty segment, backslash, control bytes), and
the staging directory holds only what SafeZipFile extracted, which never
creates symlinks. SafeZipFile's own entry checks stay, since zip entry
names never reach the C++ validation.

Behaviour changes, from the shared rules: copiesCrc is read per target
(conflicting CRCs for one source fail the patch instead of the last one
winning), a deletes array is honoured, and type errors are patch failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
applyDeltaForHash reads the manifest (size-capped as before) and calls
archive_patch::ApplyArchivePatch with it, so PushyPatchManifestFromJson,
the NSJSONSerialization round trip and the hbcTransform extraction are
gone. The rules are the shared ones: a CRC must be an integer in
[0, 2^32) (true, 1.5 or 2^32 are no longer read as 1, 1 or 0) and
non-object hbcTransform metadata fails the patch instead of being skipped.
A missing manifest is now reported as "diff.json not found".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pushy.cpp exports applyArchiveDelta (async, over
archive_patch::ApplyArchivePatch) in place of buildArchivePlan,
buildCopyGroups and applyDeltaFromSource; the manifest is parsed on the
worker thread with the rest of the patch. DownloadTask reads the manifest
text and passes it with the entry names: the ppk task lets the core copy,
delete and merge, the package task passes hostCopiesResources and the
"resources/rawfile/" prefix and copies the returned groups, each with the
CRC its source must match.

parseManifestToArrays and its bun tests are gone; the cases moved to
cpp/patch_core/tests/patch-manifest-vectors.json. Behaviour changes, from
the shared rules: the rawfile prefix is stripped only at the start of a
source, an empty source means the target's path in ppk patches too, and
values of the wrong type fail the patch instead of being coerced with
String() or ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No binding assembles a manifest from parallel copy lists any more; every
platform hands __diff.json to ApplyArchivePatch. CODE_AUDIT 3.14 records
the manifest parser as sunk into the shared core.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ord.cpp

cpp/patch_core/install_record.h was a header of constants with the logic
kept three times (InstallRecord.java, RCTPushyInstallRecord.mm,
InstallRecord.ts). It now carries the whole contract on POSIX IO and
digest::Sha256File:
 - Parse / Read / Names / HasRecordFile / RecordedBundleSha256;
 - Build (refuses an empty bundleSha256) and Write: temp file + fsync,
   rename over the record, directory fsync (best-effort). Android wrote in
   place with fsyncs, iOS atomically without fsync, Harmony in place with a
   file fsync only;
 - Seal (hash the staged bundle unless the digest is known, build, write;
   tells a file-system failure from a bundle failure) and Promote (rename
   the staging directory, fsync the versions root, which only Android did);
 - IsComplete (bundle is a regular file + legacy marker or a schema-1
   record naming the version) for launch/dedup;
 - CheckActivation, the switchVersion gate: bundle present; no record file
   is grandfathered only for the current/last version; the zero-byte
   legacy marker passes; otherwise schema 1, versionHash and a non-empty
   bundleSha256 matching the bundle, which must not change while hashed
   (iOS's stamp check, now everywhere). Unchanged re-checks cheaply that
   the directory still passes on the same basis (iOS's under-lock check).

Where the platforms disagreed, the stricter reading wins: only a zero-byte
file is the legacy marker (all three trusted "{}" too), "schema" must be
the number 1 (Android took "1" and 1.5, iOS true and 1.5), versionHash and
bundleSha256 must be strings (Android coerced numbers), the bundle must be
a regular file, and a grandfathered version still needs its bundle (iOS
skipped that check).

tests/install-record-vectors.json holds 31 record shapes with what each
means for IsComplete, CheckActivation and RecordedBundleSha256; the C++
test replays them and covers the size cap, Build, atomic Write, Seal,
Promote and Unchanged. check-constant-parity.js learns `only`, so the
mirrors are held to the constants they still use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
InstallRecord.java keeps the paths (bundleFile, stagingDirectoryFor) and
wraps five natives (JNI over cpp/patch_core/install_record.h, in
cpp/android_jni/install_record_jni.cpp): isInstalled, verifyInstalled
(CheckActivation, now taking the grandfathering decision), seal, promote
and recordedBundleSha256. build/write/read/isComplete/verifyForActivation/
syncDirectory and the org.json parsing are gone.

- DownloadTask.promoteStaging seals the staging directory (reusing the
  digest taken while extracting a full package) and promotes it; the
  record is now written atomically (temp + rename) and the directory
  fsync no longer needs API 26.
- verifySwitchTarget hands current/last to the core, which grandfathers
  only a directory without a record, as before; a markerless directory
  outside current/last now fails with "is incomplete." like iOS/Harmony.
- The bundle hash moves from MessageDigest to the core's SHA-256 on the
  same threads as before (state serial thread, sync coordinator, download
  tasks); nothing on the UI thread hashes. Launch-time callers only stat
  and read the record.

InstallRecordTest keeps the path helpers; the record logic is covered by
the C++ vectors (the JVM cannot load the library).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
install_record.cpp reports a delta::Status but needs nothing else of
patch_core.cpp; with the two factories in the header it links on its own,
which lets the iOS purge-restore harness compile the real record core
without hpatch, HDiffPatch and lzma.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RCTPushyInstallRecord.mm keeps the paths and becomes a thin layer over
cpp/patch_core/install_record.h: PushyHasCompletedVersionAtPath is
IsComplete, PushyRecordedBundleSha256 (constants, crash attribution) is
RecordedBundleSha256, and PushyPromoteStagingDir seals the staging
directory (Seal: bundle failures stay PATCH_FAILED, a record that cannot
be written is FILE_OPERATION_FAILED instead of a raw Cocoa error) and
promotes it (Promote, which now also fsyncs the record and the versions
root; NSDataWritingAtomic did neither).

switchVersion:error: calls CheckActivation outside the state lock with
the current/last decision taken under it, and re-checks with Unchanged
under the lock (same grandfathering, record text and bundle stamp) before
committing. PushyReadInstallRecord, PushyInstallRecordMatches,
PushyHasInstallRecordFile, PushyStampFile, PushyFileStamp and
PushyVerifyInstallForActivation are gone. A grandfathered version now
needs its bundle like on Android/Harmony.

The purge-restore harness compiles the extractor's copy of
install_record.cpp next to the extracted switchVersion:error:, so
install_record_activation and switch_version_records exercise the shared
core; the trust-digestless and grandfather-recorded mutants now alter that
copy and are still caught by their named assertions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The struct moved into the shared record core (install_record::FileStamp).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pushy.cpp exports the record core (cpp/patch_core/install_record.h):
isInstallComplete and recordedBundleSha256 synchronously (a stat and a
read of the record, as before), sealInstallRecord, promoteStaging and
checkActivation as promises on libuv workers, so neither the bundle hash
nor the fsyncs run on the JS thread.

InstallRecord.ts keeps the paths and the reserved-entry rule and maps
rejections to the stable codes (install steps PATCH_FAILED, activation
SWITCH_VERSION_FAILED). parse/read/build/write, the record matching and
assertActivatable are gone; UpdateContext.verifyActivation hands
current/last to the core, DownloadTask seals and promotes. The record is
now written atomically with a directory fsync (it was unlinked and
rewritten in place) and the versions root is fsynced after the rename.

The bun tests now cover only the binding: paths, the arguments handed to
the NAPI module, the error-code mapping, and that NativePatchCore.ts
declares exactly the functions pushy.cpp exports. The record logic is the
C++ vectors'. check-constant-parity holds both mirrors to kStagingSuffix
alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sponse

cpp/update_flow_core/native_round.{h,cpp} takes over the pure parts the
Android, iOS and Harmony orchestrators each carried around the shared
decision layer:

- PrepareRound(config, env, sample): the checkUpdate request body (identity,
  cInfo and input assembled from the persisted config and the launch env,
  diffV from this core) plus the first endpoint-query state.
- NextQueryStep(state, text): the sequential endpoint fallback — configured
  endpoints in sampled order, then the first queryUrls list that parses,
  https-only for remote bases, the 8-request budget and the response schema
  gate. The request path now lives here, encoded.
- HandleRoundResponse(...): the decision with attempts planned for the device
  (no diff without a running version, no cleartext URL under an https-only
  config), the hashInfo to persist (incl. forceBootRescue) and the
  response-cache text.

tests/native-round-vectors.json pins it and records, per case, where the
three platforms disagreed: non-string packageVersion/rnu/rn overrides,
absent rnu/rn on iOS, Android's optBoolean forceBoot marker and lenient
remote-list JSON, non-string endpoint/queryUrls handling, and Harmony's
failures on non-array endpoints or a non-string afterDownload. The core
takes the JS-equivalent or strictest behaviour. The fuzz target walks the
new entry points.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…core

SyncCoordinator hands the config text and a launch env to
FlowBridge.prepareRound / nextQueryStep / handleResponse (JNI over
cpp/update_flow_core/native_round.h) instead of assembling identity, cInfo
and the request input, walking the endpoints (QueryRun, isHttpsOnly), building
hashInfo and the response-cache JSON, and filtering attempts itself. The
buildRequestBody / orderEndpointCandidates / isValidResponse natives are gone;
the config gate returns just the rejection (configRejection), since the core
reads the appKey itself. HttpUtils loses QUERY_PATH, stripTrailingSlashes and
isCleartextUrl; the request path lives encoded in the core.

Behaviour aligned with iOS/Harmony/JS: a truthy non-boolean forceBoot (1,
"false") now marks forceBootRescue like the activation it already caused;
non-string packageVersion/rnu/rn are no longer coerced to strings;
non-string endpoints and queryUrls entries no longer spend a request; a
remote endpoint list must be strict JSON; a config with duplicate keys is no
longer rejected by org.json after the shared gate accepted it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RCTPushyOrchestrator passes the config text and a launch env to
flowcore::PrepareRound / NextQueryStep / HandleRoundResponse
(cpp/update_flow_core/native_round.h) instead of assembling identity, cInfo
and the request input, walking the endpoints with its own tried-set and
budget, projecting the version info and filtering attempts.
commitRoundWithGeneration takes the core's response-cache text (cache:)
in place of the four fields it serialized itself; nil still marks the crash
handler's activation-only commit. PushyLoadRunnableConfig only hands back
the config text, RCTPushyQueryPath and the https/endpoint helpers are gone.

Behaviour aligned with Android/Harmony/JS: absent rnu/rn are sent as ""
(cInfo used to omit them) and non-string ones as "", like the core's C1
defaults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…core

SyncCoordinator passes the config text and a launch env to the new NAPI
prepareRound / nextQueryStep / handleResponse (cpp/update_flow_core/
native_round.h) instead of assembling identity, cInfo and the request
input, walking the endpoints itself, projecting hashInfo, building the
response-cache JSON and filtering attempts. The buildRequestBody /
orderEndpointCandidates / isValidResponse exports and Texts.QUERY_PATH are
gone; the request path now lives only in the core.

harmonySyncQuery checks the glue follows the core's steps (the walk's
policy moved to native-round-vectors.json); harmonySyncAttempts and the
host-API harness use the new signatures, and nativeTextEncoding expects the
request path in cpp/ only.

Behaviour aligned with Android/iOS: non-array endpoints still try the list
discovery (the round used to stop), a non-string endpoint is skipped (it
used to throw and fail the round), a non-string afterDownload never
activates (it used to fail the round as invalid_response), non-string
queryUrls entries no longer spend a request, and non-string
packageVersion/rn overrides are ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed core

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
….cpp

cpp/patch_core/archive_limits.h was a header of constants with the rules
kept three times (SafeZipFile.inspect, PushyUnzipGuard, ArchiveLimits.ts).
It now carries them:
 - EntryAccumulator(archiveBytes).Accept(size, compressed, symlink, name):
   entry count, reserved name, symlink, per-entry cap, running total and
   whole-archive ratio, per-entry ratio; AcceptExtracted for an entry already
   on disk whose compressed size was never seen (no per-entry ratio);
 - CheckEntryCount, CheckUncompressedSize(archiveBytes, uncompressedBytes)
   and IsReservedEntryName (install_record::kReservedPrefix, new);
 - CheckExtractedDirectory: an iterative lstat walk of a freshly unpacked
   tree through the accumulator (Harmony, whose zlib exposes no entries);
 - PlanUnknownLengthFreeSpaceProbe: the chunked-download disk probe plan
   all three platforms implemented identically.

Where the platforms disagreed, the strictest reading wins:
 - the ratio is size > compressed * 100 (Android/iOS divided, so up to
   101:1 minus one byte passed), and a zero compressed size past the 1 MiB
   threshold is an unbounded ratio (Android/iOS skipped the check);
 - the whole-archive ratio (Harmony only) now applies on every platform;
 - a reserved `.pushy-` prefix is refused in any path component (Android
   checked the base name, iOS the last component of each unpacked path);
 - symlinks are refused everywhere (iOS only; Harmony's walk followed them);
 - an entry of unknown (negative) size is refused (Android skipped it);
 - a non-positive archive size bounds nothing (Harmony skipped the ratio).

tests/archive-limits-vectors.json pins the accumulator, the whole-archive
check, the probe plan and the reserved names (ported from the Harmony bun
cases and SafeZipFileTest, with `was` notes); the C++ test also walks real
trees (reserved directory, symlink, 20000/20001 entries).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hive_limits

PushyUnzipGuard feeds an archive_limits::EntryAccumulator (sized by the
archive file, for the whole-archive ratio) from the should-unzip callback
and checks the entry count up front with CheckEntryCount; its own size,
total and ratio arithmetic is gone. The reserved-name check in the
did-unzip callback now covers every component of the path below the
destination (IsReservedEntryName), not just the last one; an unpacked path
that is not below the destination is a violation. The downloader's
unknown-length disk probe is PlanUnknownLengthFreeSpaceProbe.

Behaviour aligned with the shared rules: ratio as size > compressed * 100
(was integer division), a zero compressed size past 1 MiB is refused, the
whole-archive ratio applies, a `.pushy-` directory component is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…over JNI

SafeZipFile.inspect(archiveBytes) collects the central directory once and
hands names, declared and compressed sizes to ArchiveLimits.checkEntries
(one coarse JNI call per archive, cpp/android_jni/archive_limits_jni.cpp,
over archive_limits::EntryAccumulator), which returns the total for the
free-space check or throws the first violation. The Inspection class,
the Java count/size/ratio arithmetic and isReservedEntryName are gone; the
reserved-name rule now comes from the core during inspect, which both
extraction paths run before any entry is written (the iterator keeps the
traversal check, unzipToPath the canonical-path check). The download loop
asks ArchiveLimits.planFreeSpaceProbe (JNI over
PlanUnknownLengthFreeSpaceProbe) before each chunk.

Behaviour aligned with the shared rules: ratio as size > compressed * 100,
a zero compressed size past 1 MiB is refused, the whole-archive ratio
applies, a `.pushy-` directory component is refused, an entry of unknown
size is refused instead of skipped.

ArchiveLimits.java keeps the five caps it still compares against itself;
check-constant-parity holds it to exactly those. SafeZipFileTest keeps the
traversal and extraction cases; the accounting cases moved to
cpp/patch_core/tests/archive-limits-vectors.json (the JVM cannot load the
library). The JNI registration test accepts a throws clause on a native.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… NAPI

pushy.cpp exports the extraction accounting (cpp/patch_core/archive_limits.h):
checkUncompressedSize (whole-archive expansion and ratio, synchronous,
returns the reason), planFreeSpaceProbe (the unknown-length download probe
plan, per chunk) and checkExtractedDirectory (the post-unpack walk, a promise
on a libuv worker). zlib exposes no entries, so the accumulator runs over
the unpacked tree there.

ArchiveLimits.ts keeps checkOriginalSize (the getOriginalSize gate, Harmony
only) and ensureFreeSpace, and maps refusals to PATCH_FAILED.
planUnknownLengthFreeSpaceProbe and measureExtractedDirectory are gone, and
so are InstallRecord.ts's RESERVED_ENTRY_PREFIX / isReservedEntryName. The
walk no longer runs on the JS thread and uses lstat: a symlink in the
unpacked tree is refused instead of followed.

ArchiveLimits.ts mirrors only the caps it still compares against
(MAX_ARCHIVE_BYTES, MAX_MANIFEST_BYTES, FREE_DISK_MARGIN_BYTES);
check-constant-parity holds it to exactly those. The bun tests keep the
getOriginalSize gate and cover only the binding (arguments, error code);
the probe and ratio cases are the C++ vectors'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NDK 28.2.13676358 (scripts/build-android-so.sh pin). Picks up the
native_config, archive delta, install record, native round and archive
limit bindings added on this branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Too many files!

This PR contains 202 files, which is 52 over the limit of 150.

To get a review, reduce the PR to 150 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 55dd3cbd-eb64-4ac8-adfd-b24b48fa9dce

📥 Commits

Reviewing files that changed from the base of the PR and between 4ca8972 and 9f35e68.


⛔ Files ignored due to path filters (4)
  • android/lib/arm64-v8a/librnpushy.so is excluded by !**/*.so
  • android/lib/armeabi-v7a/librnpushy.so is excluded by !**/*.so
  • android/lib/x86/librnpushy.so is excluded by !**/*.so
  • android/lib/x86_64/librnpushy.so is excluded by !**/*.so

📒 Files selected for processing (202)
  • .github/actions/e2e-android-setup/action.yml
  • .github/actions/e2e-common/action.yml
  • .github/actions/publish-npm/action.yml
  • .github/workflows/e2e_android.yml
  • .github/workflows/e2e_ios.yml
  • .github/workflows/harmony-build.yml
  • .github/workflows/lint.yml
  • .github/workflows/publish.yml
  • .github/workflows/test.yml
  • .gitignore
  • CODE_AUDIT.md
  • Example/e2etest/e2e/entry.brick.ts
  • Example/e2etest/e2e/entry.fix.ts
  • Example/e2etest/e2e/local-merge.test.ts
  • Example/e2etest/e2e/native/native-check.test.ts
  • Example/e2etest/scripts/ios-rescue-server.ts
  • android/build.gradle
  • android/jni/Android.mk
  • android/proguard.pro
  • android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java
  • android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java
  • android/src/main/java/cn/reactnative/modules/update/ArchivePatchPlanResult.java
  • android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java
  • android/src/main/java/cn/reactnative/modules/update/ConfigEquivalence.java
  • android/src/main/java/cn/reactnative/modules/update/CopyGroupResult.java
  • android/src/main/java/cn/reactnative/modules/update/CrashHold.java
  • android/src/main/java/cn/reactnative/modules/update/DownloadTask.java
  • android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java
  • android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java
  • android/src/main/java/cn/reactnative/modules/update/FileOperationException.java
  • android/src/main/java/cn/reactnative/modules/update/FlowBridge.java
  • android/src/main/java/cn/reactnative/modules/update/HttpClients.java
  • android/src/main/java/cn/reactnative/modules/update/HttpUtils.java
  • android/src/main/java/cn/reactnative/modules/update/InstallRecord.java
  • android/src/main/java/cn/reactnative/modules/update/NativeCore.java
  • android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java
  • android/src/main/java/cn/reactnative/modules/update/PushyConfiguration.java
  • android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java
  • android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java
  • android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java
  • android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java
  • android/src/main/java/cn/reactnative/modules/update/SyncCoordinator.java
  • android/src/main/java/cn/reactnative/modules/update/Threads.java
  • android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java
  • android/src/main/java/cn/reactnative/modules/update/UpdateContext.java
  • android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java
  • android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java
  • android/src/main/java/cn/reactnative/modules/update/UpdateModule.java
  • android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java
  • android/src/newarch/cn/reactnative/modules/update/UpdateModule.java
  • android/src/oldarch/cn/reactnative/modules/update/NativePushySpec.java
  • android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java
  • android/src/test/java/cn/reactnative/modules/update/ConfigEquivalenceTest.java
  • android/src/test/java/cn/reactnative/modules/update/HttpUtilsTest.java
  • android/src/test/java/cn/reactnative/modules/update/InstallRecordTest.java
  • android/src/test/java/cn/reactnative/modules/update/SafeZipFileTest.java
  • android/src/test/java/cn/reactnative/modules/update/SyncCoordinatorTest.java
  • cpp/android_jni/archive_limits_jni.cpp
  • cpp/android_jni/install_record_jni.cpp
  • cpp/android_jni/jni_log.h
  • cpp/android_jni/jni_natives.h
  • cpp/android_jni/jni_registration.cpp
  • cpp/android_jni/jni_util.h
  • cpp/android_jni/update_core_android.cpp
  • cpp/android_jni/update_flow_jni.cpp
  • cpp/hpatch/hpatch.c
  • cpp/hpatch/hpatch.h
  • cpp/patch_core/archive_limits.cpp
  • cpp/patch_core/archive_limits.h
  • cpp/patch_core/archive_patch_core.cpp
  • cpp/patch_core/archive_patch_core.h
  • cpp/patch_core/error_codes.h
  • cpp/patch_core/install_record.cpp
  • cpp/patch_core/install_record.h
  • cpp/patch_core/jni_natives.h
  • cpp/patch_core/jni_util.h
  • cpp/patch_core/patch_core.cpp
  • cpp/patch_core/patch_core.h
  • cpp/patch_core/patch_core_android.cpp
  • cpp/patch_core/patch_manifest.cpp
  • cpp/patch_core/patch_manifest.h
  • cpp/patch_core/state_ops.h
  • cpp/patch_core/tests/archive-limits-vectors.json
  • cpp/patch_core/tests/fuzz_patch_manifest.cpp
  • cpp/patch_core/tests/install-record-vectors.json
  • cpp/patch_core/tests/patch-manifest-vectors.json
  • cpp/patch_core/tests/patch_core_test.cpp
  • cpp/update_flow_core/flow_json.cpp
  • cpp/update_flow_core/flow_json.h
  • cpp/update_flow_core/native_config.cpp
  • cpp/update_flow_core/native_config.h
  • cpp/update_flow_core/native_round.cpp
  • cpp/update_flow_core/native_round.h
  • cpp/update_flow_core/tests/config-equivalence-vectors.json
  • cpp/update_flow_core/tests/fuzz_flow_json.cpp
  • cpp/update_flow_core/tests/native-config-vectors.json
  • cpp/update_flow_core/tests/native-round-vectors.json
  • cpp/update_flow_core/tests/update_flow_core_test.cpp
  • cpp/update_flow_core/update_flow_core.h
  • cpp/update_flow_core/update_flow_jni.cpp
  • docs/BUNDLEHASH_MIGRATION.md
  • docs/NATIVE_CHECKUPDATE_DESIGN.md
  • docs/NATIVE_CHECK_FOLLOWUPS.md
  • harmony/hvigor-plugin.ts
  • harmony/pushy/BuildProfile.ets
  • harmony/pushy/OAT.xml
  • harmony/pushy/oh-package.json5
  • harmony/pushy/src/main/cpp/CMakeLists.txt
  • harmony/pushy/src/main/cpp/PushyPackage.h
  • harmony/pushy/src/main/cpp/PushyTurboModule.cpp
  • harmony/pushy/src/main/cpp/pushy.cpp
  • harmony/pushy/src/main/ets/ArchiveLimits.ts
  • harmony/pushy/src/main/ets/DownloadTask.ts
  • harmony/pushy/src/main/ets/DownloadTaskParams.ts
  • harmony/pushy/src/main/ets/ErrorCodes.ts
  • harmony/pushy/src/main/ets/EventHub.ts
  • harmony/pushy/src/main/ets/InstallRecord.ts
  • harmony/pushy/src/main/ets/Logger.ts
  • harmony/pushy/src/main/ets/NativePatchCore.ts
  • harmony/pushy/src/main/ets/PushyConfiguration.ts
  • harmony/pushy/src/main/ets/PushyTurboModule.ts
  • harmony/pushy/src/main/ets/SaveFile.ts
  • harmony/pushy/src/main/ets/SyncCoordinator.ts
  • harmony/pushy/src/main/ets/Texts.ts
  • harmony/pushy/src/main/ets/UpdateContext.ts
  • harmony/pushy/src/test/ArchiveLimits.test.ets
  • harmony/pushy/src/test/ContentRange.test.ets
  • harmony/pushy/src/test/DownloadTaskParams.test.ets
  • harmony/pushy/src/test/ErrorCodes.test.ets
  • harmony/pushy/src/test/EventHub.test.ets
  • harmony/pushy/src/test/InstallRecord.test.ets
  • harmony/pushy/src/test/List.test.ets
  • harmony/pushy/src/test/ManifestParsing.test.ets
  • harmony/pushy/src/test/NativeConfig.test.ets
  • harmony/pushy/src/test/Validation.test.ets
  • harmony/pushy/src/test/check-constant-parity.js
  • ios/RCTPushy/RCTPushy+Launch.mm
  • ios/RCTPushy/RCTPushy.mm
  • ios/RCTPushy/RCTPushyCommon.h
  • ios/RCTPushy/RCTPushyConfiguration.h
  • ios/RCTPushy/RCTPushyConfiguration.mm
  • ios/RCTPushy/RCTPushyDownloader.mm
  • ios/RCTPushy/RCTPushyInstallRecord.mm
  • ios/RCTPushy/RCTPushyInstaller.mm
  • ios/RCTPushy/RCTPushyInternal.h
  • ios/RCTPushy/RCTPushyOrchestrator.mm
  • ios/RCTPushy/RCTPushyState.mm
  • package.json
  • react-native-update.podspec
  • scripts/build-harmony-har.js
  • scripts/check-constant-parity.js
  • scripts/check-harmony-types.js
  • scripts/check-native-spec-parity.js
  • scripts/check-packlist.js
  • scripts/lib/cpp-test-common.sh
  • scripts/test-ios-purge-restore.sh
  • scripts/test-patch-core.sh
  • scripts/test-update-flow-core.sh
  • scripts/tests/ios-purge-restore/README.md
  • scripts/tests/ios-purge-restore/extract.py
  • scripts/tests/ios-purge-restore/purge_restore_test.mm
  • scripts/tests/ios-purge-restore/test_extract.py
  • scripts/verify-android-so.js
  • src/__tests__/client.test.ts
  • src/__tests__/configEquivalenceVectors.test.ts
  • src/__tests__/context.test.ts
  • src/__tests__/core.test.ts
  • src/__tests__/endpoint.test.ts
  • src/__tests__/harmonyArchiveLimits.test.ts
  • src/__tests__/harmonyDownloadTask.test.ts
  • src/__tests__/harmonyErrorCodes.test.ts
  • src/__tests__/harmonyInstallRecord.test.ts
  • src/__tests__/harmonySyncAttempts.test.ts
  • src/__tests__/harmonySyncQuery.test.ts
  • src/__tests__/harmonyValidation.test.ts
  • src/__tests__/helpers.ts
  • src/__tests__/i18n.test.ts
  • src/__tests__/jniRegistration.test.ts
  • src/__tests__/metadata.test.ts
  • src/__tests__/nativeConfiguration.test.ts
  • src/__tests__/nativeHostApi.test.ts
  • src/__tests__/nativeHostApiNaming.test.ts
  • src/__tests__/nativeSources.ts
  • src/__tests__/nativeTextEncoding.test.ts
  • src/__tests__/provider.contract.test.tsx
  • src/__tests__/provider.render.test.tsx
  • src/__tests__/setup.ts
  • src/__tests__/updateFlowCore.test.ts
  • src/__tests__/utils.test.ts
  • src/client.ts
  • src/context.ts
  • src/core.ts
  • src/endpoint.ts
  • src/error.ts
  • src/i18n.ts
  • src/locales/en.ts
  • src/locales/zh.ts
  • src/provider.tsx
  • src/telemetry.ts
  • src/type.ts
  • src/updateFlowCore.ts
  • src/utils.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

sunnylqm and others added 3 commits October 12, 2026 21:04
- Android: skip the per-chunk free-space plan (a JNI call) when the
  download length is known; nothing is ever probed then.
- iOS: an undefined round decision fails the round as invalid_response,
  as on Android, instead of committing as 'none'.
- archive_limits: CheckExtractedDirectory walks directories without
  counting them, matching iOS/Android which count only declared zip
  entries. Reserved directory names are still rejected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing forceBoot

The relaunch right after the 'no directive' check starts its own delayed
round (5s after launch). The test flipped forceBoot as soon as the base
label showed, so that in-flight round could read the flipped server and
activate one launch early (seen on CI: request at +5s, flip at +4s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The beforeAll only uninstalls, reinstalls and launches the app; on slow
macOS runners simctl alone took 5-9 minutes and the 300s testTimeout
failed the suite before any step ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sunnylqm
sunnylqm merged commit 1260b11 into master Oct 12, 2026
14 checks passed
sunnylqm added a commit that referenced this pull request Oct 12, 2026
Bump package.json and harmony/pushy/oh-package.json5 together. Ships the
cross-layer cleanup (#657): the native round, patch-manifest parsing,
install records and archive limits now live once in the shared C++ core,
plus the unreleased #654/#655 fixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant