Skip to content

finding(types): MobileOverrides.navigation 发布了三值导航词表,全仓零读点 —— 与 #3985 同族的另一处「声明即全部」 #4919

Description

@yinlianghui

发现于 #3985 的实施(PR 见下),不在该 PR 处理 —— #3985 的完成范围是 app-schema-renderer 的 mobileNavMode 这一个键(声明收窄 + 退休零读点的值);这一条是普查退休提及时在另一个包、另一个类型、另一个键上撞见的同族现象,修法(退休 vs 实装)同样要先有裁定,不该由实施 agent 顺手择一。未认领,交 PM 分诊。

机制

packages/types/src/mobile.ts:73-86 发布了 MobileOverrides:

export interface MobileOverrides {
  layout?: 'stack' | 'tabs' | 'accordion' | 'carousel';
  columns?: number;
  useBottomSheet?: boolean;
  fullScreen?: boolean;
  touchTarget?: 'default' | 'large' | 'xlarge';
  navigation?: 'bottom-tabs' | 'hamburger' | 'drawer';
}

它是公开面:packages/types/src/index.ts:578 导出,packages/mobile/src/index.ts:60 再导出一次,所以 @object-ui/types 与 @object-ui/mobile 两个包的消费者都能写它。

全仓对这个类型的提及只有三处,没有一处是读:

  • packages/types/src/mobile.ts:73 —— 声明本身;
  • packages/types/src/mobile.ts:283 —— mobileOverrides?: MobileOverrides;,挂在另一个类型上的可选属性;
  • 两处 barrel 再导出。

grep -rn "mobileOverrides"(小写首字母,即真正会被读到的属性名)在 packages/ / apps/ / examples/ 下只命中 mobile.ts:283 声明自身 —— 没有任何渲染器、hook 或适配层取过它,更没有谁 switch 过 navigation 的三个值。也就是说整棵 MobileOverrides 目前是「声明即全部」,而 navigation 的三值词表是其中最像能力的一项:三个值读起来是三种移动端导航形态,实际三者行为完全一致(都不发生任何事)。

与 #3985 的关系,以及为什么单独立卡

同族(#3818 / #3829 / #3985 那条线:发布的语义与实际读点不符),但不是同一个键,也不在 #3985 的完成范围内:

顺带说明这一条为什么不能照抄 #3985 的结论:mobileNavMode 的裁定是「收窄到实现了的值」,而这里实现了的值是空集,收窄的终点是把键删掉;是删 navigation、删整个 MobileOverrides,还是反过来立实装卡(移动端 overrides 本来就是路线图上的东西),是产品面的取舍,不是机械动作。

可达性诚实标注

仓内实际受害者为 0:没有任何 JSON 元数据或 React 调用侧写过 mobileOverrides。今天能被误导的只有仓外照着 @object-ui/types / @object-ui/mobile 的 .d.ts 写移动端配置的消费者 —— 他们写了 mobileOverrides: { navigation: 'bottom-tabs' } 会通过类型检查、通过构建,然后什么都不发生,且没有任何诊断。属 observation-class(今天没有用户能撞到),因此打 finding、不挂 pm:queue,定级请分诊时自行判断。

参考位置

  • packages/types/src/mobile.ts:73-86 —— MobileOverrides 声明,navigation 在 :85
  • packages/types/src/mobile.ts:283 —— 唯一的挂载点 mobileOverrides?: MobileOverrides
  • packages/types/src/index.ts:578 / packages/mobile/src/index.ts:60 —— 两处公开再导出

关联:#3985(发现于其实施,方向相近但另一个键)、#3829(声明了却零读点)、#3818(同族、方向相反)。

Activity

  1. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    Triage (concentrated finding round): finding → needs-user-decision. Removing a published key is the manual floor, so this cannot be seat-adjudicated even though the evidence is one-sided.

    Question: MobileOverrides (and specifically its three-value navigation vocabulary) has zero read points repo-wide — retire it, retire only navigation, or file an implementation card (mobile overrides as roadmap work)?

    Four-axis block:

    • Platform long-term coherence: the app-schema-renderer 的 mobileNavMode 声明成自由文本 string,实现却是三值联合 —— 写错的值静默回落,且词表里的 'hamburger' 在渲染器零读点 #3985 lineage's rule is "narrow to the implemented values"; here the implemented set is empty, so the honest endpoint of that rule is removal — keeping it declares capability the runtime will not honor.
    • Measured business pull: zero JSON metadata and zero call sites author mobileOverrides; the only possible consumers are external .d.ts readers, unmeasured. No pull for implementation today.
    • AI-agent error-resistance: navigation: 'bottom-tabs' type-checks, builds, and silently does nothing — the exact declare-without-enforce shape the platform doctrine forbids; removal is the strongest guard, implementation the costliest.
    • Startup scope discipline: implementing mobile overrides now is capability expansion with no pull; remove > declare-and-maintain.

    Recommendation: retire the whole MobileOverrides type (both barrel re-exports, the mobileOverrides mount point), with the usual tombstone/changeset discipline; re-introduce deliberately if mobile overrides ever get real renderer work. Not finding-parked because only a ruling can move it.

    本评论来自分诊座位 Routine (session session_018euHiF12oV8iSfYdjVioUF).


    Generated by Claude Code

  2. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 21, 2026
  3. self-assigned this
    on Aug 24, 2026
  4. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    Contributor

    Claiming this card.

    • Dev agent session (dispatched by PM session 68ad4d1a-c111-591c-9633-f2fb64fa1803)
    • Branch: claude/issue-4919-retire-mobile-overrides
    • Repo: objectstack-ai/objectui

    Carrying the recorded maintainer ruling in comment 5339691949 (「全部接受」) as given: retire the whole MobileOverrides type — declaration, the mobileOverrides? mount point, and both barrel re-exports. Not re-deciding it.

    Two things I will determine by measurement before deleting anything, and report either way:

    1. This repo's actual tombstone convention for removing a published TS type (one-stage removal vs. a two-stage @deprecated-then-remove), from precedent in repo history — not assumed from the metadata-spec retiredKey() path.
    2. Re-verification of the card's zero-read claim against current main (the card is from 2026-08-17); if a read point has appeared since, I stop and report rather than delete.

    If an earlier claim comment from a different session exists on this card, it wins — I read the full comment thread before starting and found none.


    Generated by Claude Code

  5. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    Contributor
    {
      "issue": 4919,
      "status": "done",
      "branch": "claude/issue-4919-retire-mobile-overrides",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5941",
      "head_sha": "da50609e0126a4463aeaf2a57e72d5833aff27f2",
      "premise_still_valid": true,
      "summary": "Carried the recorded maintainer ruling (comment 5339691949) as given: removed the MobileOverrides interface from packages/types/src/mobile.ts, its mobileOverrides? mount point on MobileComponentConfig, and both barrel re-exports (packages/types/src/index.ts, packages/mobile/src/index.ts). The three-value navigation vocabulary is not narrowed and not kept in any form; mobile overrides are not implemented. Added a minor changeset for @object-ui/types and @object-ui/mobile describing the published-surface break, plus a source note at the removal site recording the reopen condition. Draft PR #5941, four files, +65/-19.",
      "files_changed": [
        "packages/types/src/mobile.ts",
        "packages/types/src/index.ts",
        "packages/mobile/src/index.ts",
        ".changeset/4919-retire-mobile-overrides.md"
      ],
      "tombstone_branch": "(a) one-stage removal — done in full in this PR, no stage 2 outstanding",
      "tombstone_precedent": "Measured, not assumed. The ADR-0087 retiredKey() path does NOT apply to a TS type here: packages/types/src/mobile.ts has no Zod twin (no mobile.zod.ts in packages/types/src/zod/), so the 'loud parse rejection in the Zod twin' half of that discipline has nothing to attach to. This repo has TWO live retirement forms for published TS surface, and the discriminator is NOT chronology — they interleave: removed outright = ToggleGroupItem.icon (objectui#4632, commit 99bd015, 2026-08-14), AccordionItem.icon (objectui#4652, commit 58b8346, 2026-08-15), BaseFieldMetadata.indexed, theme spacing/breakpoints; `?: never` tombstone = action confirm (objectui#4314, commit d2ce342, 2026-08-18), StaticTableColumn hoverable/striped (objectui#5474, commit 3ccd9e8cb, 2026-08-23), theme fontSize/fontWeight. The rule the set shows: a tombstone exists to steer authors to a NAMED LIVE REPLACEMENT (confirm->confirmText; hoverable/striped->data-table; fontSize->customVars) or to keep loud a key the docs actively taught; outright removal is for zero pull with no successor. AccordionItem.icon's changeset states it verbatim: it 'had zero measured pull anywhere in the corpus and no established convention to lean on, so under this platform's declared=enforced doctrine it is removed rather than speculatively implemented.' MobileOverrides is that exact shape — zero pull, never documented, no successor spelling, and the ruling forbids keeping the vocabulary in any form. Confirmed in current source that icon is FULLY ABSENT from both AccordionItem and ToggleGroupItem (no `?: never`, no @deprecated residue), i.e. the changelog claim matches reality. Also confirmed NO two-stage @deprecated-then-remove convention exists: every @deprecated in packages/types/src is either a still-working legacy alias (app.ts menu, base.ts visibleOn, the MenuItem and ActionSchema interfaces — all still typed and still read) or a tag applied in the SAME commit as the `?: never` retyping, where the tombstone is terminal. Branch (b) would have shipped a release in which mobileOverrides still type-checks and still does nothing — prolonging the declare-without-enforce shape the ruling exists to close.",
      "zero_read_reverification": "Re-measured on origin/main @ ed35c23bb before deleting anything; card premise HOLDS. MobileOverrides had exactly 4 mentions repo-wide, none a read: packages/types/src/index.ts:585 (barrel), packages/types/src/mobile.ts:73 (declaration), packages/types/src/mobile.ts:283 (mobileOverrides?: MobileOverrides, the only mount point), packages/mobile/src/index.ts:60 (barrel). Note the card's line numbers drifted (index.ts 578 -> 585) — same four sites. Case-insensitive sweep of the lower-case property name across ALL file types (not just ts/tsx: md, mdx, json included) found zero JSON metadata, zero docs, zero schema-catalog entries, zero example-app authors. Sibling checkouts /home/user/objectstack and /home/user/objectstack-ai: 0 hits each. Of the navigation values, 'bottom-tabs' had 1 source hit (the declaration line) and 'hamburger' 3 — the other 2 belong to MobileNavMode in packages/layout, the objectui#3985 sibling on a different consumption path, untouched. After the change, the identical repo-wide grep returns 0.",
      "dist_evidence": "Same grep, same scope (packages/*/dist --include='*.d.ts'), rebuilt on each side. BEFORE (control, proving the instrument sees the case): 4 hits for MobileOverrides across 3 files — packages/mobile/dist/index.d.ts:36, packages/types/dist/index.d.ts:73, packages/types/dist/mobile.d.ts:63 ('export interface MobileOverrides {'), packages/types/dist/mobile.d.ts:262 ('mobileOverrides?: MobileOverrides;'); 1 hit for the lower-case property; 1 hit for 'bottom-tabs' (mobile.d.ts:75). AFTER (build exit 0): 0 / 0 / 0. The source retirement note names the type in a comment; the 0 count in dist confirms it does not leak into the published .d.ts.",
      "loud_signal_measurement": "The ruling's 'loud signal rather than a silent no-op' clause was TESTED, not asserted — a real consumer compiled against the built .d.ts on both sides, with a control. BEFORE: probe A (import type MobileOverrides) exit 0 silent; probe B (author mobileOverrides: { navigation: 'bottom-tabs' } on MobileComponentConfig) exit 0 SILENT — the card's defect reproduced exactly; control C (responsive, a live key on the same interface) exit 0. AFTER: A = TS2305 'Module \"@object-ui/types\" has no exported member MobileOverrides'; B = TS2353 'Object literal may only specify known properties, and mobileOverrides does not exist in type MobileComponentConfig'; C still exit 0. The control earned its place: on its first run all three probes failed on a tsconfig error (TS5101 baseUrl deprecation), which would have made A's 'failure' indistinguishable from success — C staying green is what shows the fixed instrument measures the retirement. Excess-property checking is load-bearing for B, so I verified MobileComponentConfig has NO index signature and NO extends (BaseSchema's [key: string]: any would have swallowed the diagnostic; this interface is not in that family).",
      "tests": "All from repo root with path filters per AGENTS.md (never `pnpm --filter <pkg> test`, which is the cwd trap). Exit codes captured before any pipe. `pnpm --filter @object-ui/types --filter @object-ui/mobile build` BUILD_EXIT=0. `... type-check` TYPECHECK_EXIT=0 — both script names echoed ('packages/types type-check$ tsc --noEmit && ...', 'packages/mobile type-check$ ...'), so this was not a zero-match silent pass; note objectui spells it type-check, hyphenated. `pnpm exec vitest run packages/types/ packages/mobile/ --maxWorkers=2` VITEST_EXIT=0, 'Test Files 56 passed (56)', 'Tests 601 passed (601)' — 56 = 52 types + 4 mobile, so the filter matched MY packages, not another agent's. `pnpm --filter @object-ui/types --filter @object-ui/mobile lint` LINT_EXIT=0, 'types: 253 problems (0 errors, 253 warnings)', 'mobile: 18 problems (0 errors, 18 warnings)' — all pre-existing in examples/ and __tests__/, none in the changed files. Gates, each quoting its own verdict line: check-control-bytes EXIT=0 'OK (scanned 4929 tracked text file(s))' plus a manual grep -naP control-byte scan of all four changed files (no hits); check-changeset-presence EXIT=0 '3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'; check-changeset-no-major EXIT=0 'No changeset declares a major bump'; check-spec-symbol-derivation EXIT=0; check-published-dist-tooling EXIT=0; check-doc-component-types EXIT=0 'Every documented component type is registered'. ABLATION (the before leg): reverted the 3 files with `git checkout origin/main -- <paths>` AFTER committing, confirmed the mutation ON DISK by anchored counts on BOTH the deleted and the injected text (deleted 'export interface MobileOverrides' count 0->1, injected 'RETIRED (objectui#4919' count 1->0, mount point count 0->1) rather than trusting any editor exit code, then REBUILT so the mutation actually reached dist/ (verified: 4 hits back in the .d.ts) before reading any result, all under a `trap ... EXIT INT TERM` restore. Restore verified byte-exact: git status clean against the commit, markers back to 1/0, dist back to 0 hits. Verification union ran against the final commit da50609e0 (nothing landed after it).",
      "reference_keyword": "Fixes #4919 — first line of the PR body. Justified because branch (a) means this PR genuinely completes the card; Part of would have been wrong. AUDITED AS GITHUB STORED IT: re-fetched the body from api.github.com/repos/objectstack-ai/objectui/pulls/5941 (not my transcription) and regex-scanned the stored 10227 bytes with GitHub's keyword set (close/closes/closed, fix/fixes/fixed, resolve/resolves/resolved + #N). Exactly ONE match in the whole body: 'Fixes #4919'. The other 7 issue references carry no closing keyword — the second #4919 is preceded by 'and the ', #5942 by 'filed for triage as ', and objectui#4632/#4652/#4314/#5474/#3985 are bare parenthetical citations. Read-back also confirms draft:true, head da50609e0, and that the signature footer survived in session-URL form (the bare form is stripped on every edit; the body was edited once, after which the footer is still present).",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #5942: MobileComponentConfig is now a fully inert published type — after this PR removes its only interesting property, it has zero mount points and zero readers (4 mentions: declaration, a doc-comment cross-reference, two barrel re-exports). Same family as this card, different type, not covered by the ruling — so filed unassigned with the `finding` label and NO pm:queue (observation-class, zero in-repo victims) rather than widened into this PR. Caveat recorded on the issue itself: the pre-file duplicate search could not be run because GitHub search/list endpoints were returning 'API rate limit already exceeded'; please dedupe at triage."
      ],
      "not_done_and_why": [
        "Did NOT wait for CI to converge. PR #5941 read mergeable_state 'unstable' at report time (checks still running). Per the dispatch and the standing rule, the report is delivered at draft-PR time and CI convergence is the PM seat's to read.",
        "Did NOT un-draft and did NOT enable auto-merge, as instructed. The diff touches no governed surface (packages/ and .changeset/ only, none of AGENTS.md / CLAUDE.md / .claude/** / docs/adr/**), so the hold is the dispatch's instruction, not the governed-surface rule.",
        "Did NOT apply a `skip-changeset` label — that mechanism does not exist in objectui, and this PR ships a real changeset anyway.",
        "Did NOT run the repo-wide `pnpm lint` (turbo run lint across 47 packages) or the full test farm; scoped to the two affected packages. Declared narrowing: eslint here is NOT type-aware (no projectService/parserOptions.project in eslint.config.js), so this diff cannot move the verdict on any untouched file. CI runs the farm regardless.",
        "Did NOT touch content/docs/releases/ (forbidden); the release-notes input is the changeset alone.",
        "Did NOT re-decide the ruling, did NOT preserve the navigation vocabulary in any form, and did NOT implement mobile overrides."
      ],
      "notes_for_pm": [
        "One process slip worth recording: an early attempt to scaffold the tsc probe used an `&&` chain whose first link (`echo >> .git/info/exclude`) failed — in a linked worktree .git is a FILE, not a directory — after which newline-separated heredocs ran in the REPO ROOT, creating 5 stray files there (AGENTS.md forbids writing to the repo root). Caught immediately via git status; all 5 were untracked, no tracked file was overwritten (the root tsconfig.json was inside the short-circuited chain and is byte-unchanged), and they were removed. The probe was then rebuilt entirely inside the scratchpad. Final tree is clean and only the 4 intended files are in the commit.",
        "A near-miss false red worth knowing: I first linted with `--no-inline-config`, which is NOT this repo's lint command (`eslint .` is). It reported 1 error at packages/types/src/index.ts:1179 by disabling a documented, pre-existing eslint-disable block for a FormField spec import — unrelated to this change and present on main. Under the repo's real command that file is clean. Reported here so the PR body's honesty note is not mistaken for a real finding.",
        "AGENTS.md conflicts with the generic dispatch boilerplate on two points, resolved in AGENTS.md's favour and flagged rather than silently chosen: (1) tests must run from the repo ROOT via `pnpm exec vitest run <path>`, never `pnpm --filter <pkg> test` (which silently runs another package's suite and reports green); (2) objectui has no scripts/pm/os-verify-lock.sh, so heavy commands were simply kept path-filtered and short rather than routed through a lock that does not exist here."
      ]
    }

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:queue

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions