Skip to content

fix(mobile): the native bridge platform is an open set, and a newer bridge protocol reads as no bridge - #29475

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-fggkhz
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-fggkhz

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread

Fixes #29330

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:原生壳桥的 platform 类型从 ios | android 放开为开放集合(桌面壳可报其它值);新增桥协议版本门:壳报的 version 大于 1 或不是数字时,Console 当作没有壳、走浏览器路径;写明 ready 的含义;README 示例去掉按壳切换文案的分支。
  • 为什么改:桌面壳即将上线且报 desktop,旧的封闭类型会让消费方写出漏掉它的穷举分支;version 从未被读,将来协议破坏性升级时旧 Console 会误调新协议;壳将依赖 ready 揭开加载遮罩(mobile#162),需要明确它在鉴权加载态就会发出。
  • 风险与回滚:运行时只多一道版本判断,现有壳都报版本 1,行为不变;类型放宽只可能让对 platform 做穷举 switch 的代码编译不过,全仓只有 Console 一处把它原样写进属性。回滚即 revert 本 PR。
  • 先看:ui/packages/mobile/src/nativeHost.ts(getNativeHost 的版本门与 platform 类型)→ ui/apps/console/src/__tests__/App.nativeShellOpenSet-29330.test.tsx → ui/apps/console/src/__tests__/main.nativeBridgeVersion-29330.test.tsx。
  • 两分钟验证:在 ui/ 下跑 pnpm exec vitest run apps/console/src/__tests__/App.nativeShellOpenSet-29330.test.tsx,预期 4 个用例全绿(桌面壳、鉴权加载态时的 ready、版本 2 不进壳模式及其对照)。
  • 需要你做的:无,席位落地。

What changes

  • ObjectStackNative.platform is an open set. The type widens from 'ios' | 'android' to 'ios' | 'android' | (string & {}), so the two known values still autocomplete and the desktop shell's value type-checks. The runtime already treated it as open: getNativeHost never checked it, the Console writes it verbatim into data-os-native-shell, and the call wrappers gate on has() only. The README and the pending .changeset/29039-mobile-native-shell-mode.md now say the attribute carries the shell's platform string, an open set.
  • Bridge protocol version gate. The bridge type says version "bumps only on a breaking change". nativeHost.ts gains NATIVE_BRIDGE_VERSION = 1 (module export used by the tests; not added to the package entry, which this PR leaves untouched). getNativeHost() answers null for a host whose version is not a number (NaN included) or is greater than 1. Every reader goes through it, so such a host gets the browser behaviour: no shell mode, no ready, no routeChanged, no navigate listener, the PWA manifest kept, and (since feat(console): installable desktop PWA with an offline app shell and a push-ready Service Worker #29326 merged) the service worker treated as in a browser.
  • What ready means, decided: document it, don't move it. ready means "the Console has painted its own UI", which may be a loading fallback such as AuthGuard's spinner on a cold start to a protected route. The shell lifts its loading cover on ready, so the cover hands over to the Console's own spinner; waiting for AuthGuard would hold the native cover through a network round-trip. Written on the ObjectStackNative.ready member, the postNativeReady and useNativeHostRouting docblocks, and the README's bridge section. The timing is unchanged and now pinned.
  • README nit. The SaveButton example drops host ? 'Save' : 'Save changes' and keeps one label, following the "viewport and capability, not host" rule.

Tests

  • ui/apps/console/src/__tests__/App.nativeShellOpenSet-29330.test.tsx (new, the real App, ProtectedRoute and AuthGuard under the real BrowserRouter):
    • a platform: 'desktop' bridge announcing haptic and openExternal only: shell mode on, ready posted once, data-os-native-shell="desktop", the two announced calls reach the shell and getPushToken, navigate and resetServer resolve unavailable and are never called;
    • a cold start to / with the session read still open: ready arrives while AuthGuard's loading fallback is on screen and before the route renders;
    • a version: 2 bridge: no root attribute, no ready, no route post, calls unavailable; control: the same bridge at version: 1 is the shell.
  • ui/apps/console/src/__tests__/main.nativeBridgeVersion-29330.test.tsx (new, the real main.tsx with its boot calls and its manifest icon step stubbed): a version: 2 bridge starts the manifest step and the generated manifest replaces the static one; controls: version: 1 never starts the step and leaves no manifest, no bridge behaves like version: 2.
  • ui/packages/mobile/src/__tests__/nativeHost.test.tsx (extended): a desktop bridge is a bridge; NATIVE_BRIDGE_VERSION is 1; a newer, string, NaN or missing version is null and its calls are unavailable and uncalled; versions 1 and 0 are the bridge; useNativeHostRouting posts nothing and installs no listener for a version 2 bridge.

Acceptance notes

  • Reverse checks. Version gate ablated (the typeof version line replaced): 7 cases red across the three files (main version 2, App version 2, four mobile detection cases, mobile routing case), restored byte-identical to HEAD. Old closed union restored on platform: tsc --noEmit in ui/apps/console reds the desktop fixture with TS2322 "Type 'string' is not assignable to type '"ios" | "android"'", restored byte-identical.
  • Widening, not narrowing, for readers. git grep of the bridge symbols across ui/, packages/ and apps/ finds only @object-ui/mobile and the Console; the one reader of platform is NativeShellRouting, which writes it into an attribute. No exhaustive switch exists to break.
  • Expected red: check-empty-changeset. This PR edits the pending .changeset/29039-mobile-native-shell-mode.md, wording only: data-os-native-shell="ios|android" becomes the shell's platform string, an open set. That is the gate's deliberate-correction class: the old sentence is false after this PR, so it is corrected here rather than restored, by the card's ruling. The gate stays red by design; it is not one of the required contexts.
  • Surface addition. The manifest half of the version-gate pin lives where the decision lives, main.tsx, so it has its own test file beside the App-level one. No source file outside the card's surface changed; App.tsx and index.ts are untouched.
  • Merged with main after feat(console): installable desktop PWA with an offline app shell and a push-ready Service Worker #29326 (service worker), which also edits ui/packages/mobile/README.md and main.tsx; the merge was clean and the version gate now also covers its worker decision through getNativeHost(). A second merge brought fix(console): PWA manifest declares the branding icon's real type and size, with 192/512 PNG fallbacks #29405, which moved the manifest into an async step after first paint; the manifest test waits for that step and pins whether it starts.

Verification

All readings at head 28cb3792 (main merged at a6441636; the 13 main commits since touch none of this PR's 8 files).

  • Touched suites, from ui/: pnpm exec vitest run of the two new console files, App.nativeShellMode-29039.test.tsx, packages/mobile/ and scripts/__tests__/check-vi-mock-inherit.test.ts: 13 files, 220 tests passed.
  • pnpm --filter @object-ui/mobile run type-check and pnpm --filter @object-ui/console run type-check (both cover the test files): exit 0. eslint on the five touched source and test files: exit 0.
  • Derived gates: node scripts/pm/dispatch-gates.mjs --commands lists 34; all 34 run and reconciled with --ran (0 NOT-MEASURED). 33 exit 0, including pnpm --dir ui exec vitest run scripts/ (169 files, 4921 tests) and check-sdui-manifest; check-empty-changeset exits 1, the expected deliberate-correction red above.
  • Changed lines: 524 (+515 / -9) in 8 files, under the 3000-line threshold.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (Not touched.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 28cb37926df54e24fdede36303d5a9041a5b12fd
Local-runs: none

Face: declaration. The PR body and the changeset declare Clause-②: yes (widening) (claim 6578214873). No published schema and no governed path are touched: check-governed-merges --pr 29475 hit 0 of 8 paths, reviewFace on the non-changeset paths answers no schema and no rules hit, and nothing is under packages/spec/src/**. So the review owed is the lane seat's TIER_DEFAULT record.

① Derived judgments

  • The widening is one member type. ObjectStackNative.platform goes from 'ios' | 'android' to 'ios' | 'android' | (string & {}), as ruled. The accepted set widens, and the two known values still autocomplete. A reader that switched exhaustively over the two old values now sees another string; the changeset says so and names the remedy (branch on has() or the viewport). The dev's reverse check (the closed union gives TS2322 on the desktop fixture) shows the old type refused the desktop shell. Correct, and a widening.
  • The version gate narrows nothing a reader can see. getNativeHost() answers null for a bridge whose version is not a number or is greater than NATIVE_BRIDGE_VERSION (1). Every shipped shell speaks version 1, so no host that worked before loses shell mode; a future breaking bridge gets the browser path rather than calls into a protocol the Console cannot read. The bridge type's own docblock already said version "bumps only on a breaking change". !(version <= 1) also refuses NaN.
  • NATIVE_BRIDGE_VERSION is module-only. ui/packages/mobile/src/index.ts re-exports nativeHost.js by name and is untouched, and package.json exports only ., so the constant is not package surface.
  • ready is documented, not moved, on the member, postNativeReady, useNativeHostRouting and the README, as ruled: the Console's own painted UI, which may be the auth guard's spinner.
  • The README example keeps a single Save label and no longer imports useNativeHost, as ruled.
  • The pending 29039 changeset changes one sentence, data-os-native-shell="ios|android" → the shell's platform string, an open set. That is the ruled correction.

② Semver level

The new changeset is '@object-ui/mobile': patch with Metadata-protocol: none. AGENTS.md Post-Task Checklist step 3: a non-protocol change, widening or breaking, ships patch. Consistent.

③ Boundary flags

  • Expected red, accepted: check-empty-changeset refuses the edit to the pending 29039 changeset (deliberate-correction class). The correction is ruled in the brief and named in the PR body. It is not a required context.
  • Deviation, accepted: main.nativeBridgeVersion-29330.test.tsx pins the manifest decision in the real main.tsx, where it lives. The seat allowed it as the brief's console-level test.
  • Process note: the two first wip(mobile) commits were made with --no-verify. Every later commit and the push ran the hooks, the final head passes the 34 derived gates, and the queue squashes the history. Recorded, not a block.
  • The PR author is objectstack-fleet[bot], so there is no off-relay writer to name.

Implemented-by: claude/project-thread-fggkhz
Reviewed-by: session_01QsR2jR7ZFkpqmm4U8S1f9y

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

席位核对 · ACCEPT · 2026-11-02T16:40Z

Head-sha: 28cb37926df54e24fdede36303d5a9041a5b12fd

  • Shape: draft → main, Fixes #29330 in the body only. 8 files, +515 / −9 (524 lines ≤ 3000). No governed path: check-governed-merges --pr 29475 hit 0 of 8 paths. Clause-②: yes (widening) and Metadata-protocol: none are line-initial in the body and the new changeset.
  • Contract review: the seat's TIER_DEFAULT record 6582637070 on this head, PASS. The widening is ObjectStackNative.platform's open set.
  • File surface: nativeHost.ts, useNativeHost.ts, nativeHost.test.tsx, the console App-level test App.nativeShellOpenSet-29330.test.tsx, ui/packages/mobile/README.md, the 29039 changeset (wording only) and the new changeset, as claimed (6578214873). One addition, allowed by the seat before pr_create: main.nativeBridgeVersion-29330.test.tsx, the console-level pin of the manifest decision in main.tsx.
  • Read in the diff: all five rulings are carried out. platform is widened. ready is documented on the member, postNativeReady, useNativeHostRouting and the README. Version gate: getNativeHost() is null for a non-number or > 1 version, with NATIVE_BRIDGE_VERSION = 1 module-only. The README example has a single label. The 29039 sentence is corrected.
  • Tests: 13 files and 220 tests are green at this head. The desktop App-level case asserts shell mode on, ready posted once, the root attribute reading desktop, and an unannounced method never called. ready is pinned while AuthGuard still shows its fallback. Version 2 means no shell mode, no ready and the manifest kept, and version 1 drops the manifest. The version-gate ablation turns 7 red, and the closed union gives TS2322 on the desktop fixture. Type-check of mobile and console exits 0, and eslint exits 0.
  • Gates (check-runs on this head): 43 success, 11 skipped, 1 failed, 0 pending, including all 16 Test UI shards. All ten required contexts succeeded, or were skipped by their path filter (Build Core). The one red is Check Changeset (check-empty-changeset), the deliberate-correction class this PR names in its body; it is not a required context.
  • Main merges: the branch merged main after feat(console): installable desktop PWA with an offline app shell and a push-ready Service Worker #29326 and after fix(console): PWA manifest declares the branding icon's real type and size, with 192/512 PNG fallbacks #29405, which made the manifest step async; the boot-manifest test waits on that step. Later main commits touch none of the 8 files.
  • Landing: ordinary queue landing, pr_ready then automerge_enable. Fixes #29330 closes the card on merge.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 16:43
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 9f7e408 Nov 2, 2026
55 of 56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-fggkhz branch November 2, 2026 17:04
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

Landed · 2026-11-02T17:20Z · PR #29475 merged through the merge queue as 9f7e408f. Record: seat TIER_DEFAULT contract review 6582637070 PASS and ACCEPT 6582667680 at 28cb3792 (Clause-②: yes (widening)).

probe before (9f7e408f~1) now (origin/main)
readonly platform: 'ios' | 'android' | (string & {}); in ui/packages/mobile/src/nativeHost.ts (the bridge's platform is an open set) 0 1
export const NATIVE_BRIDGE_VERSION = 1; in the same file (a newer bridge protocol reads as no bridge) 0 1

The card closed through Fixes. pm:dispatched comes off with this note.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Native host: make ObjectStackNative.platform an open set and pin it with an App-level test (follow-up to #29140)

1 participant