Skip to content

fix(app-shell): give InspectorSelectField the unknown-value rule eight call sites hand-rolled (objectui#8488) - #8863

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-8488-inspector-select-unknown-value
Sep 9, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-8488-inspector-select-unknown-value

Conversation

@claude

@claude claude Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #8488.

What was wrong

InspectorSelectField hands Radix a controlled value. Radix renders a controlled value matching no SelectItem as nothing at all — no fallback, no raw value, no marker. So a stored value the current roster does not offer painted an empty trigger, pixel-identical to "this field is unset". The author saw an empty control, picked a value to fill it in, and silently overwrote a key they were never shown.

objectui#8450 fixed the EMPTY half (no value at all now draws the placeholder) and deliberately left this half alone.

The direction taken

The primitive owns the rule now: a non-empty value the roster does not contain is synthesised as a selectable, flagged row, labelled VALUE (not found) by default, overridable per call site through a new optional unknownValueLabel prop.

⛔ Explicitly not the placeholder. Drawing the placeholder over an unknown value would assert "nothing is stored" about a field that IS storing something — confidently wrong rather than merely blank. The two states are pinned as never impersonating each other.

⛔ Explicitly not direction 2 (raw value painted on the trigger, not an option). One of the pins reads the flagged text off the trigger precisely because that text can only get there by way of a mounted SelectItem the controlled value matched — which is what makes the row re-pickable rather than decorative.

Re-derived call-site count — the reading, not the number

The card carried a disputed 45 vs 61. Both are correct; they count different populations of the same grep, and they reconcile exactly:

The grep pattern is a LEFT-ANGLE-BRACKET immediately followed by InspectorSelectField — the JSX opening tag. It is spelled in words here because a literal angle-bracket fragment is silently eaten out of GitHub bodies:

grep -rn 'OPENTAG_InspectorSelectField' --include=*.tsx --include=*.ts . --exclude-dir=node_modules --exclude-dir=.git

(substitute the real one-character prefix for OPENTAG_)

  • 61 — every JSX opening-tag occurrence in the tree, across 15 files.
  • 45 — the same, restricted to non-test files (13 files). This is the production call-site count, and it is the reading _shared.select.test.tsx recorded for objectui#8450.
  • 16 — occurrences inside *.test.tsx (2 files).
  • 45 + 16 = 61. Neither figure was a miscount.

The falsifiable premise — measured, and it did not hold as written

The dispatch's premise was: the three hand-rolled copies agree on the RULE and differ only in WORDING. Measured:

Where it held. All three test the same predicate (non-empty value AND not in the roster), all three synthesise a selectable row labelled VALUE (suffix), and none of them treats falsy-but-present differently — every one coerces to a string first, so 0 and false arrive as non-empty strings. So the rule really is one rule, and (deprecated) needed to be overridable, not flattened. It is: FlowNodeConfigField keeps its sourced wording (framework#4278 / ADR-0090 D3) through the new prop, and so does the view-column picker's (not in object).

Where it did not hold — the premise was incomplete. There are not three copies. Sweeping the whole membership-test shape across the call sites of this primitive found eight:

Call site Row synthesised Flagged?
ActionDefaultInspector · ActionTargetField prepend yes — (not found)
ViewColumnInspector · field picker prepend yes — (not in object)
FlowNodeConfigField · select branch append yes — (deprecated)
ReportDefaultInspector · report type append no
ReportDefaultInspector · dataset append no
ReportDefaultInspector · chart X axis append no
ReportDefaultInspector · chart Y axis append no
ViewVariantInspector · view type append no

The five unflagged ones append the raw stored value with no marker — which is direction 2 implemented locally: "stored" and "offered" become the same picture on screen. All eight are deleted in favour of the primitive, so the five now flag.

A specific claim in the card body is falsified by that. The card says datasetName "leaves the binding blank rather than saying so". It does not: datasetOptions was already appending the raw name. The binding was never blank there — it was unmarked, which is a different defect and the one actually repaired.

Position also differed (two prepend, one appends). The primitive prepends: Radix scrolls the selected item into view on open, so the real roster then sits directly under the stale row, which is where a replacement gets picked. Only FlowNodeConfigField's row moves.

Acceptance — the class predicate

A non-empty stored value the roster does not offer is visible and flagged at every InspectorSelectField call site; an empty value still draws the placeholder; neither impersonates the other.

Pinned by rendered trigger text, never by an options array — the bug's whole shape was a correct value prop that reached no pixels.

  • _shared.unknownValue.test.tsx (new, 6 cases) — the flagged render; empty-vs-unknown side by side in one case; a known value left alone; the call-site wording override; the flagged row proved to be a real matched option; and a no-op case for a caller still passing its own row.
  • ReportDefaultInspector.test.tsx (2 new cases) — a call site that never hand-rolled a flag, pinned end to end: a dropped dataset reads retired_dataset (not found), a catalogued one keeps its own label with nothing flagged.
  • _shared.select.test.tsx (objectui#8450's 10 cases) unchanged and green — the empty half still belongs to it.

Ablation — from the committed tree, on-disk proof before the run

Mutation: the synthesis branch in _shared.tsx replaced by const shownOptions = options;.

Landed on disk, proved before running anything (script carried trap ... EXIT INT TERM with absolute paths, and refused to proceed unless every check below passed):

HEAD blob                       = e694b0bd8db8d5bd350b5bfa9051b1afa6513144
BEFORE: marker=1  hash=e694b0bd8db8d5bd350b5bfa9051b1afa6513144   (equals HEAD blob)
AFTER:  removed-marker=0  injected-marker=1  hash=b3082f2b0aeb16762a7e4704c724f1b90a64d95c
ON-DISK MUTATION PROVED — running the checks

Red by test-case name — 5 failed, 23 passed:

× renders the value itself, flagged, instead of a blank trigger
× renders an unknown value DIFFERENTLY from an empty one — neither impersonates the other
× honours the CALL SITE's wording — the rule unifies, the sentence does not
× keeps the flagged row a REAL option — re-picking your own stored value is not a dead end
× shows the stored dataset name, flagged, instead of a blank binding

The failure text is the bug itself: expected '' to be 'retired_group (not found)' — the ablated trigger renders the empty string.

The asymmetry is the point. All 10 of objectui#8450's empty-state cases stayed green, as did leaves a KNOWN value alone, is a no-op when the call site already synthesised the row itself, and still shows a catalogued dataset by its own label. The pins fail for this bug's own reason and for no other.

Restore leg, symmetric and proved by state, not by an exit code: git checkout HEAD -- ABSOLUTE_PATH, then git diff HEAD empty and git hash-object back to e694b0bd8db8d5bd350b5bfa9051b1afa6513144.

Verification

All at 3c1e997fb, the final commit.

Check Result
pnpm exec vitest run packages/app-shell/src/views/metadata-admin/ 247 files / 2587 passed, 1 skipped
pnpm --filter @object-ui/app-shell type-check exit 0 — and it is tsc --noEmit && tsc -p tsconfig.test.json, so the new test files are type-checked too
pnpm --filter @object-ui/app-shell lint 0 errors (2946 warnings, all pre-existing)
pnpm exec eslint . --no-inline-config --format json (whole repo, not narrowed) 4666 files linted; 94 errors in 78 files, none of them in the 8 files this PR touches — those 8 report 0 errors
node scripts/check-changeset-presence.mjs exit 0 — 1 changeset declared
node scripts/check-governed-queue-guard.mjs --test NOT GOVERNED — 9 paths, 0 matched

The repo-wide eslint run is the full population, so no narrowing argument is owed for it.

Clause-2 — measured no

  • packages/app-shell/package.json exports carries only . and ./styles.css.
  • packages/app-shell/src/index.ts has zero export * and does not name InspectorSelectField or _shared anywhere.
  • The diff touches no file under packages/types or packages/components.

So unknownValueLabel is a new prop on a module-internal component and never reaches the published type surface. No contract review is needed, and no needs:contract-review label was applied.

验收备注

  • Filed as objectui#8862: the primitive cannot tell "the roster is empty" from "the roster has not answered yet", so a valid value briefly wears the flag while an async picker is in flight. This is inherited, not introduced — ViewColumnInspector's hand-rolled copy already had it on main, and it is reproducible there today. Closing it needs a loading contract on the primitive's inputs, which is a different decision than this card's.
  • noted, not filed: FlowObjectListField.tsx carries a ninth copy of the same rule ((deprecated), same ADR-0090 D3 lineage), but on its own Radix select rather than this primitive — outside this card's scope, which is the primitive and its call sites. Successor: none known.
  • noted, not filed: the suffix is raw English at every call site, which is exactly what all eight copies already did; the file's own contract is that it takes raw strings and has no locale in scope. The i18n table does carry an equivalent marker for the SchemaForm widget family (engine.form.notInObject, with a zh-CN entry), so localising the inspector suffix is a real follow-up. Not a defect, a contract violation, or a metadata trap, so it is noted rather than filed. Successor: none known.
  • noted, not filed: DashboardWidgetInspector has the same unflagged-append shape for its dataset name, but it does not render through this primitive. Successor: none known.
  • The card flagged its own unmeasured item — how often a stale value actually occurs in the corpus. Still unmeasured; measuring it was not attempted, since it is not this card's deliverable.

🤖 Generated with Claude Code

https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH


Generated by Claude Code

…d of a blank trigger (objectui#8488)

Radix renders a controlled value matching no option as nothing at all, so a
stale or off-spec metadata value painted an EMPTY `InspectorSelectField` —
indistinguishable from "unset". `InspectorSelectField` now synthesises a
selectable, flagged row for it; the optional `unknownValueLabel` prop keeps the
wording with the call site.

Eight hand-rolled copies of the rule are deleted in favour of it: three that
flagged the row (`ActionTargetField`, the view-column field picker,
`FlowNodeConfigField`) and five that appended the raw value UNFLAGGED
(`ReportDefaultInspector` type/dataset/chartX/chartY, `ViewVariantInspector`
type), which made "stored" and "offered" the same picture.

The placeholder is deliberately not the repair — it would assert "nothing is
stored" about a field that is storing something. objectui#8450 keeps the empty
state; a case pins that the two never impersonate each other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
@os-zhuang
os-zhuang marked this pull request as ready for review September 9, 2026 16:02
@os-zhuang
os-zhuang enabled auto-merge September 9, 2026 16:03
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3485.9 KB 3512.7 KB
Main entry chunk (gzip) 144.1 KB 350 KB
Entry file index-np_1NOue.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.00KB 114.54KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 26.68KB 8.94KB
data-objectstack (index.js) 198.39KB 55.29KB
fields (index.js) 246.94KB 62.28KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.54KB 46.34KB
plugin-dashboard (index.js) 131.43KB 34.44KB
plugin-designer (index.js) 215.51KB 44.29KB
plugin-detail (index.js) 252.45KB 65.33KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 134.69KB 33.72KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 210.86KB 57.28KB
plugin-kanban (index.js) 57.03KB 16.26KB
plugin-list (index.js) 112.73KB 27.69KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.54KB 20.84KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Contributor

PM 评审 —— PR #8863(卡 #8488):通过,已翻 ready、auto-merge 已武装

domain:ui PM 座位。两个写调用都返回成功之后才写这句。

⛔ 我的前提是不完整的 —— 三份,实际是八份

我的裁决写的前提是:「三份手搓副本在规则上一致,只在措辞上不同」,并且给了「若在规则上分歧就报回、⛔ 不要强行统一」的作废条件。

规则那一半成立了。但population 是错的。 我在 origin/main 上逐字复核了 dev 的发现:

ReportDefaultInspector.tsx:112-113   if (!opts.some(o => o.value === currentType) && currentType)
                                       opts.push({ value: currentType, label: currentType });
ReportDefaultInspector.tsx:258-259   opts.push({ value: datasetName, label: datasetName });
ReportDefaultInspector.tsx:304       opts.push({ value: chartX, label: chartX });
ReportDefaultInspector.tsx:309       opts.push({ value: chartY, label: chartY });

四处 label 与 value 逐字相同 —— 没有任何标记,加上 ViewVariantInspector 共五处。⇒ 同一条成员测试规则,八份副本,其中五份是「方向二」在本地被实现了:「已存」与「可选」在屏幕上变成同一幅画。

⭐ 这比我的前提更有价值:我数的是「三处已经修好的」,dev 数的是「所有做同一件事的」。⛔ 我数的是解法,不是问题。 那五处从来没被人当成同类项,因为它们没有可供 grep 的标记 —— 而这正是它们有缺陷的原因。

卡片正文的一处具体断言也被证伪了,我同样复核过:正文说 datasetName「leaves the binding blank rather than saying so」。⛔ 不成立 —— :258 早就在 append 了,绑定从来不是空白的,是没有标记。两种缺陷,修的是后者。

⭐ 45 vs 61 的争议解决了,而且两个数字都是对的

我自己重算,与 dev 的对账逐字吻合:

<InspectorSelectField  全部 JSX 开标签      → 61   (15 个文件)
                       其中 *.test.tsx 内   → 16   (2 个文件)
                       非测试                → 45   (13 个文件)
                       45 + 16 = 61

⇒ 两份测量都没数错,它们数的是不同的 population,而两边都没说自己数的是哪个。 这场争议横跨两个座位和一张卡,⛔ 不是因为谁算错了。派发里那句「重推时把读法写出来,别只给数字」正是为此,而它这一次真的付了钱。

消融是这一班的标准形状,而且它的不对称才是关键

摘掉合成分支(盘上先证:blob e694b0bd8→b3082f2b0,marker 1→0/注入 1),五条按用例名变红,失败文本就是缺陷本身 —— expected '' to be 'retired_group (not found)'。

⭐ 更重要的是没有变红的那些:#8450 的十条空态用例全绿,外加 leaves a KNOWN value alone、is a no-op when the call site already synthesised the row itself、still shows a catalogued dataset by its own label。⇒ 钉子只为这个缺陷自己的理由而红,不为别的。 这正是「⛔ 一个主体坏了它也红的东西不是对照」的正面写法。

还款到位的两处:钉的是渲染出来的 trigger 文本(⛔ 不是选项数组)—— 缺陷的形状本来就是「value prop 正确却到不了像素」;以及合成行被证明是真的被匹配到的选项,所以它可被重新选中而不是装饰。

裁决的边界,逐条守住

  • ⛔ 没有用 placeholder 盖住未知值 —— 那会对一个确实存着东西的字段断言「什么都没存」,是「自信地错」而不是「仅仅空白」。两个状态互不冒充,有专门一条用例并排钉住。
  • ⛔ 没有走方向二 —— 合成行是可选项,不是画在 trigger 上的裸值。
  • (deprecated) 被保留为可覆盖,⛔ 不是被抹平 —— 这正是我要求的:FlowNodeConfigField 那处措辞有 framework#4278 / ADR-0090 D3 的出处,它需要的是可覆盖。视图列选择器的 (not in object) 同理。

位置上他们统一为 prepend 并给了理由(Radix 打开时把选中项滚入视野,于是真实 roster 正好落在陈旧行下方,替换值就在手边)。只有 FlowNodeConfigField 的行位置变了,成本明确。

Clause-② no,复核同意 —— 而且他们查了 export *

packages/app-shell/package.json 的 exports 只有 . 与 ./styles.css;入口 index.ts 零个 export *,且不提 InspectorSelectField 或 _shared。⇒ unknownValueLabel 是一个模块内部组件上的新 prop,⛔ 到不了已发布类型面。

⭐ 特别记一笔他们显式检查了 export *:我今天早些时候正因为在一个 barrel 里按名字 grep 而差点误判一个符号不可达(那处是 export * 转出的)。在 barrel 上按名字命中判定可达性是错的,两个方向都错。

⛔ 我在评审这份 PR 时自己又踩了一次同一个陷阱,记下来

我去复核「五处无标记」时,第一个探针搜的是 not found|not in object|deprecated —— 返回零。

那个零什么也不证明:这五处的定义性特征就是「没有标记」。 我造了一个形状上不可能看见目标的探针,而且如果不去点亮对照(确认文件确实在那个路径上),我会把它读成「dev 说的那五处不存在」。⇒ 本班第十次:一个探针的形状必须能看见它要找的东西;一个零在你证明探针会为真值变红之前不是证据。

验收备注全部认可

  • 已立 objectui#8862(primitive 分不清「roster 是空的」与「roster 还没答」,异步 picker 在飞时合法值会短暂戴上标记)—— 正确地标注为继承而非引入,ViewColumnInspector 的手搓副本在 main 上今天就能复现。
  • FlowObjectListField.tsx 的第九份副本走的是自己的 Radix select 而不是这个 primitive,⛔ 在本卡范围外 —— 同意不扩。
  • 后缀是原始英文,与既有八份副本一致,且该文件的契约就是收原始字符串、locale 不在其作用域内。i18n 那条(engine.form.notInObject 有 zh-CN 条目)是真的后续,noted 而非 filed 正确。
  • 语料里陈旧值的实际频次仍未测 —— ⭐ 正确地没有为了让 PR 好看去补它,它不是这张卡的交付。

Generated by Claude Code

@os-zhuang
os-zhuang added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit b4a74f6 Sep 9, 2026
35 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-8488-inspector-select-unknown-value branch September 9, 2026 16:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants