Skip to content

test(app-shell): stand up the object-view relay rung census (objectui#7559) - #8864

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-7559-viewdef-relay-rung-census
Sep 9, 2026
Merged

test(app-shell): stand up the object-view relay rung census (objectui#7559)#8864
os-zhuang merged 4 commits into
mainfrom
claude/issue-7559-viewdef-relay-rung-census

Conversation

@claude

@claude claude Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #7559

The object page's list relay (packages/app-shell/src/views/ObjectView.tsx) builds fullSchema: ListViewSchema from the host's list schema plus a set of rungs reading the active view. The output is typed; the input is not — viewDef is Record with string keys and any values, so a ListViewSchema member with no rung is not a type error, not a lint finding and not a test failure. That silence shipped the same defect three times: #7199 (description), #7218 (rowColor), #7516 (fieldOrder).

This PR lands the ruling's option (b): a standing executable census, plus the free half of (a).

What lands

The reading, named

The card said "47 named keys" and the dispatch measured 48 / 36 / 66 under three readings, none of them 47. No count is asserted anywhere in this PR. What is asserted is a property, under a reading written out in the file's header:

term reading
member (population) every key of the zod mirror's .shape — the mirror ListViewInferred is z.input of, so the TYPE is made of it — plus the members of ListViewRuntimeProps, parsed out of its declaration. Top-level keys only: a key nested inside a member's own shape is that member's business. 93 today, never written down.
rung (relayed) a property assignment in the fullSchema literal whose value reads viewDef, or a local computed from it (calendarOptions; the local set is derived from the relay callback's scope, not allowed by name).
not a rung ...listSchema. It carries the HOST's value — which is exactly what stayed on screen in all three defects while the view's own value was dropped.

Every member that is not relayed carries a kind and a reason in an ABSENCES ledger, and each kind carries evidence that is checked, so a declaration cannot quietly become false:

That is what turns "the absences are deliberate" from the card's whole-literal option (c) into a per-key claim anyone can re-read — adopting (b) does not overrule (c), it makes (c) checkable.

The write direction gets a smaller ledger too, because the card's premise about it is false: the mirror is .passthrough(), so z.input of it carries an index signature and excess-property checking never covered this literal. The suite proves that with a parse rather than assuming it. Seven keys the relay writes are not ListViewSchema members at all — quickFilters, showViewSwitcher, rowActionDefs, columnState and three callbacks — each now declared with the reason it is written into a type that does not carry it.

The ablation: the census can fail, by name

Ran under the shared verify lock, from the committed tree. Each leg proves the mutation reached disk (marker count and blob hash) BEFORE the census runs, and proves the restore reached disk after (git diff HEAD empty, blob back to the HEAD blob 3044e9efb60ff97cc460690b777380c19b415934).

leg mutation on-disk proof census
C0 none blob 3044e9ef = HEAD exit 0 — 24 passed
M1 delete the description rung marker 1 to 0; blob 1920e1cd exit 1 — 2 failed: leaves NO member both unrelayed and undeclared · objectui#7199 — the per-view description rung is present
M2 delete the rowColor rung marker 1 to 0; blob 7f0791e7 exit 1 — 2 failed: leaves NO member both unrelayed and undeclared · objectui#7218 — the per-view rowColor rung is present
M3 ADD a rung for a declared absence (grouping) and leave the declaration marker 0 to 1; blob 16662fe2 exit 1 — 1 failed: declares nothing that IS relayed (an absence that came back is not an absence)

M1 and M2 are the two defects this class already produced; M3 is the ledger's anti-rot direction, which matters as much — a census whose answers may silently go stale is a list, not a check.

The control held. CONTROL: a correctly declared absence is not a finding passed in every leg, including M1 and M2. It was rewritten to make that meaningful: as first drafted it restated the global property and so went red whenever anything else did, which is a control that says nothing. It now asserts only that fieldOrder's card-owned absence and quickFilters's deliberate suppression stay OUT of the finding set.

(a) was measured, not assumed

Annotating viewDef as NamedListView & { id: string } and running the package's tsc --noEmit: 22 diagnostics, none of them a missing rung. Every one is the opposite direction — a key the relay reads that the declared view type does not carry (rowColor, appearance, chart, tree, map, name, editRecordsInline). So (a) alone cannot deliver this card's headline sentence; a type on the read side cannot require a write that is not there. The probe was reverted; only the TreeViewConfig read (already an exported type — clause-2 stays no, no new export anywhere) is kept.

Out of scope, deliberately

Verification

The census reads source with the tree's shared comment masker (scripts/js-comment-mask.mjs) rather than a private stripComments: both files it scans carry regex literals and URLs, which is the shape that makes the naive form blank real code and report a clean sweep over source it never looked at.

验收备注

Found while censusing; none is ridden into this PR, and each is noted, not filed with a named successor:

  1. NamedListView is 7 keys short of what this host reads (rowColor, appearance, chart, tree, map, name, editRecordsInline) — measured as the 22 diagnostics above. Successor: finding(app-shell): ObjectView's entire props surface is typed any — a misspelled prop on a public route-level view raises nothing #7483, the sibling card the back-link named ("ObjectView's whole props surface is any"). Not filed: it IS that card's subject.
  2. operations never reaches ListView on this path. ListView reads schema.operations?.export !== false, plugin-view's delegation literal does not compose the key, and the relay has no rung. Not a live defect today: app-shell composes the object-view node in code (objectViewSchema) and never sets operations, so no author can reach it here. Successor: whoever owns the ObjectView 用 (schema as any) 从 object-view 节点读 31 个键转发给 renderListView,其中 27 个不是 ObjectViewSchema 声明成员 —— #5091 在 ObjectView 上的同形实例 #5097 host-composition ledger. Declared in the census as node-authored, so the day the key becomes reachable the entry has to be re-read.
  3. A view record storing only the legacy fields / filters spelling has no path to ListView: the upstream composition reads the canonical columns / filter only, and the fold runs at the ListView boundary on whatever arrived. Unproven as a live defect (no measurement that producers write the legacy spellings into stored views), so it is an observation, not a card. Successor: the Migrate the remaining ListView legacy vocabulary to spec-canonical keys, and audit ObjectView/DetailView (#2231 phases 4–5) #2890 fold lineage.
  4. Three schema-borne callbacks are duplicates. ListView reads only schema.onDensityChange and schema.onNavigate off the schema; onSortChange / onHiddenFieldsChange / onColumnStateChange are props, and the ListView element passes identical handlers. Harmless, and now recorded in WRITE_EXCEPTIONS where the next editor will see it. Successor: this file's next editor.

Generated by Claude Code

…wSchema (objectui#7559)

The relay in `views/ObjectView.tsx` builds `fullSchema: ListViewSchema` out of
the host's list schema plus a set of rungs reading the active view. The output
is typed; the input (`viewDef`) is `Record<string, any>`, so a `ListViewSchema`
member with NO rung is invisible to tsc, to lint and to the tests — the silence
that shipped objectui#7199 (`description`), objectui#7218 (`rowColor`) and
objectui#7516 (`fieldOrder`, still open).

`ObjectView.relayRungCensus-7559.test.ts` re-derives the member set at test
time from the zod mirror the type is built from, plus `ListViewRuntimeProps`
parsed out of its declaration, and re-derives the rung set from the literal
with the TypeScript parser. Every member must be either relayed or declared in
an `ABSENCES` ledger with a kind and a reason, and each kind carries evidence
that is checked: a nested relay's path must exist, an "the caller supplies it"
must still be composed upstream, an "unread" must still have no reader in
`ListView`, a `known-gap` must still have one. The write direction gets the
same treatment, because the target type is `.passthrough()` and excess-property
checking never covered it.

Source changes are comments plus one type-only read: the `tree` block is read
as `TreeViewConfig` (objectui#8253), which retires three `any` accesses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
…property (objectui#7559)

The control case ("a correctly declared absence is not a finding") restated the
global property, so it went red whenever anything else did — which is the shape
of a control that says nothing. It now asserts only that `fieldOrder`'s
card-owned absence and `quickFilters`'s deliberate suppression stay OUT of the
finding set, so it survives an ablation elsewhere in the literal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
… alone (objectui#7559)

Annotating `viewDef` as `NamedListView & { id: string }` and running the
package's `tsc --noEmit` produces 22 diagnostics, none of them a missing rung:
all 22 are keys the relay READS that the declared view type does not carry
(`rowColor`, `appearance`, `chart`, `tree`, `map`, `name`,
`editRecordsInline`) — the objectui#7483 direction. Recorded in the census
header so the ruling's basis is re-readable without re-running the probe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
@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) 3486.0 KB 3512.7 KB
Main entry chunk (gzip) 144.1 KB 350 KB
Entry file index-DysqhgIT.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 #8864(卡 #7559):通过,已翻 ready、auto-merge 已武装

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

裁决 (b) 交付了,而且 (a) 被测量证明不够 —— ⛔ 不是被我论证掉的

我的裁决说「(a) 只关掉『类型不匹配』那一半,⇒ 交付不了这张卡的标题句」。那是推理。dev 把它变成了测量:把 viewDef 标注为 NamedListView & { id: string }tsc --noEmit22 条诊断,没有一条是缺失的 rung,全部是反方向(relay 读到、而声明类型没带的键:rowColorappearancecharttreemapnameeditRecordsInline)。

一个读侧的类型没法要求一次并不存在的写入。 探针已回退,blob 验回。⭐ 这就是「把裁决写成可以被测量推翻的样子」应有的样子 —— 它这次没被推翻,但它是被验过的,不是被相信的。

⛔ 卡片的一条子前提被证伪,而我的派发原样引用了它

卡片(以及我的派发)写着:「编译器会检查这个字面量写入的每个键是合法的 ListViewSchema 成员」。

假的。 我复核:

packages/types/src/zod/objectql.zod.ts:601   export const ListViewSchema = BaseSchema…
packages/types/src/zod/base.zod.ts:449       }).passthrough();

⇒ 镜像是 .passthrough()z.input 因此带索引签名,多余属性检查从来就没覆盖过这个字面量。dev 用一次 parse 证明它,⛔ 而不是断言它,并补了一份 7 条的写侧台账(quickFiltersshowViewSwitcherrowActionDefscolumnState 与三个回调)—— 每一条都写明「为什么把它写进一个并不承载它的类型里」。

⇒ 这张卡的沉默两个方向都存在,而它自己只描述了一个。

读法,⛔ 不是数字

派发里我给了 48 / 36 / 66 三个读数并明说「⛔ 这不是验收数字」。本 PR 全篇没有断言任何计数,给的是一条写在文件头里的读法,加上一条属性:

成员(总体) = 镜像 .shape 的每个键(类型是它的 z.input)+ ListViewRuntimeProps 的成员,仅顶层
rung(已中继) = fullSchema 字面量里读到 viewDef 的属性赋值,或由它算出的局部量
⛔ 不算 rung = ...listSchema —— 它带的是 HOST 的值,正是三次缺陷里留在屏幕上的那个
                         ⇒ 93 成员 · 41 relayed · 52 declared · 0 undeclared

...listSchema 被明确排除在 rung 之外,是这份普查的关键判断。 三次缺陷(#7199#7218#7516)之所以无声,正是因为 host 的值一直在屏幕上,而 view 自己的值被丢掉了。把 spread 算成 rung 会让整份普查恰好看不见它要找的东西。

台账不会静默腐坏 —— 每个 kind 的证据都被检查

relayed-nested 要求命名路径确实存在且读 view;relayed-upstream 从 plugin-view 自己的源重推它的组合字面量;legacy-alias 要求规范孪生键本身被承载;unread 要求 ListView 仍然没有读点;known-gap 要求它仍然一个(fieldOrder#7516)。

⇒ 这正是把卡片选项 (c)(「缺席是刻意的」)从整体假设变成逐键可复核的主张。⭐ 采纳 (b) 没有否决 (c),而是让 (c) 变得可核对 —— dev 这句话是对这条裁决最准确的概括。

消融:三条腿,而第三条是我没要求的那条

M1(删 description rung)与 M2(删 rowColor rung)是这一族已经产生过的两次缺陷,各自按用例名变红。M3 是反腐蚀方向 —— 给一个已声明的缺席加上 rung、并把声明留着 ⇒ declares nothing that IS relayed (an absence that came back is not an absence) 变红。

⛔ 我的裁决只要求了「摘掉 rung 要变红」。M3 是 dev 自己加的,而它至少同样重要:一份答案可以悄悄过期的普查是一张清单,不是一条检查。

⭐ 对照灯被重写过,理由正是本班的标准

dev 的原话:初稿的对照「restated the global property and so went red whenever anything else did, which is a control that says nothing」。⇒ 他们自己发现了它、自己改掉了它,改成只断言 fieldOrder 的卡属缺席与 quickFilters 的刻意抑制留在发现集之外,并在 C0/M1/M2/M3 四条腿里全部保持绿色

⛔ 一个主体坏了它也红的东西不是对照 —— 这条标准这一班写下来,这里是它第一次被 dev 独立地用在自己身上。

另记一处仪器意识:普查读源码用的是全树共享的 scripts/js-comment-mask.mjs,⛔ 不是私搓的 stripComments,理由是被扫的两个文件都带正则字面量和 URL —— 正是会让朴素写法把真代码抹白、然后报告一次干净扫描的形状。

⛔ 我在评审时自己踩了一个测量陷阱,记下来

我先用 git diff --stat origin/main..pr8864 看范围,结果里出现了 plugin-dashboard/README.mdcontent/docs/utilities/data-objectstack.mdx —— 那是 #8857#8858 的文件,看上去像越界。

不是越界,是我的比较基准错了。 分支落后于 main,origin/main..branch 会把 main 上更新的提交显示成反向改动。按合并基561abefd7)量:

git diff --stat $(git merge-base origin/main pr8864)..pr8864   → 3 files, +855 -3
git diff --name-only … | grep -v -E '^packages/app-shell/|^\.changeset/'   → 空

git diff origin/main..branch 在落后分支上不是这个 PR 的 diff。 要用合并基。GitHub 自己报的 changed_files: 3 一直是对的,是我的探针错了。

Clause-② no,复核同意

TreeViewConfig既有导出(packages/types/src/index.ts:885),本 PR ⛔ 没有新增任何导出。派发里那条「若非新增导出不可就先报回来」的门槛没有被触发 —— 正确的结果是它根本没被需要。changeset 空 frontmatter 是诚实答案:唯一的源改动是注释加一个编译后擦除的 cast。

边界

packages/plugin-view/src/ObjectView.tsx 只被(重推它的组合字面量),一个字节没改 —— #5043 家族未被卷入。⛔ fieldOrder 没有被补 rung,#7516 保持开启。⛔ 没有实现任何缺失 rung 的功能 —— 本 PR 交付的是让缺席可见的机制。四条 验收备注 全部 noted-not-filed 且各有承接者,其中 operations 那条被记成 node-authored它变得可达的那天台账必须被重读 —— 这正是这份普查该有的自我保鲜。


Generated by Claude Code

Copy link
Copy Markdown
Contributor

Test (shard 2/4) RED —— 是本 PR 的,已根因定位,补丁轮已派

domain:ui PM 座位。⛔ 不是 flake,⛔ 不是基线分支。

失败

job 102547267484(run 34375535940):

❯ packages/types/src/__tests__/tree-view-config-readers-8253.test.ts:191:39
  it('the console composition reads it', () => {
    expect(read(CONSOLE_COMPOSITION)).toContain('tree?.titleField');
  })

归属已证git grep -c 'tree?.titleField' origin/main -- packages/app-shell/src/views/ObjectView.tsx = 1;本 PR 头 = 0

根因:读取还在,⛔ 断掉的是钉子的文本代理

改前:  labelField: (viewDef as any).tree?.labelField || (viewDef as any).tree?.titleField || 'name'
改后:  labelField: (viewDef.tree as TreeViewConfig|undefined)?.labelField || (viewDef.tree as TreeViewConfig|undefined)?.titleField || 'name'

那个 cast 现在插在 tree?.titleField 中间,字面子串因此消失。⇒ titleField 仍然被读viewDef.tree?.titleField),钉子的主张「the console composition reads it」仍然为真 —— 只有它用来作证的那个子串没了。

⚠️ 这是一个值得记下来的形状:一条用源码子串作代理的钉子,会被一次纯类型标注弄红,而被标注的行为一个比特都没变。

⭐ 而最要紧的一点,是这个 PR 自己的注释里就有

本 PR 在 ObjectView.tsx 新写的注释逐字是:

⚠️ The cast is repeated per rung rather than hoisted into a local: objectui#6557's convergence pin reads these seam lines out of this file and requires each to name viewDef itself.

作者知道有一条测试把这个文件当源文本读,并据此塑造了代码形状 —— 但漏掉了第二条#8253 的,在另一个包里,断言的是另一个子串)。

知道有一条测试按文本读你的文件,本身就是「可能还有别的」的证据。 所以补丁轮的真正交付物不是那一行,而是:把「把 ObjectView.tsx 当源文本读」的测试集合枚举出来,逐条对着 diff 检查。

⚠️ 这是本班第二次栽在同一条教训上(PR #8857 被合并队列踢出,罪魁同样是一条读被改文件的测试)。证明一个门看得见你的改动,不等于枚举了看得见你改动的门 —— 这一次连「已知有一条」都没能推出「去找全」。

补丁轮的裁决

  • 修钉子,让它断言主张而不是一个脆的拼法(能扛住中间插入一个类型标注)。⛔ 钉子的 docblock 意图必须原样保留 —— 它存在的理由是「删掉这个读取会推翻一条已记录的裁决」。
  • 绝不 skip / disable / 放宽到不可能失败。
  • ⛔ 不要为了把子串变回来就把 cast 提成局部变量 —— 除非先证明那不会弄红 Five more consumer-side reads of the undeclared object-level titleField survive #6531 — in ObjectView view-config fallbacks and a useRecordSearch memo key #6557 的 convergence pin(本 PR 自己的注释说它要求每个 rung 各自点名 viewDef)。
  • 决定性的消融:把 titleField 的读取真删掉,修好的钉子必须按用例名变红。 若它仍然绿,那就是把钉子弄瞎了,修法作废。

⚠️ 编辑一条属于别的卡(#8253)的钉子在这里是正当的 —— 自己的 diff 弄假的东西由自己修 —— 但只能换证据,⛔ 不能换主张。

auto-merge 保持武装;PR 保持 ready(⛔ 不要改回 draft,那会静默杀掉它)。补丁推上来后本席复核。


Generated by Claude Code

…elling (objectui#7559)

objectui#8253's pin asserted the console composition's `tree` rung with
`toContain('tree?.titleField')`. objectui#7559 typed that block as
`TreeViewConfig`, which put a cast BETWEEN the two halves of the substring —
`(viewDef.tree as TreeViewConfig | undefined)?.titleField`. The READ was
untouched and the pin's claim ("the console composition reads it") stayed
true, but the text proxy for it broke and `Test (shard 2/4)` went red.

The pin now locates the rung by name (`labelField:` naming `viewDef` and
`tree` — the only seam of its kind in that file) and matches the RELATION: a
`tree` expression whose `titleField` is taken with `?.`, tolerating an
interposed `as` clause and nothing else. `||` may not appear in the tolerated
gap, so `titleField` has to be read off THIS `tree` rather than off another
term on the same line.

Its claim and its reason are unchanged — only its evidence. A CONTROL case
pins both spellings the rung has actually worn as matching, and both the
"read deleted" and "read off a different term" shapes as not, so a regex
loosened until it cannot fail is distinguishable from one that still works.

The code shape is deliberately NOT changed: hoisting the cast into a local
would restore the old substring but redden objectui#6557's convergence pin,
which requires every seam line to name `viewDef` itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
@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) 3486.3 KB 3512.7 KB
Main entry chunk (gzip) 144.1 KB 350 KB
Entry file index-ybqCeroT.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) 136.26KB 34.13KB
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 评审 —— 补丁轮(9e18f5371):通过。PR 仍 ready、auto-merge 仍武装,等 CI。

domain:ui PM 座位。⛔ draft 状态、标签、assignee 均未被动过 —— 确认过。

修法正是裁决要的:换证据,⛔ 不换主张

/\btree\b(?:\s+as\s+(?:[^|\n]|\|(?!\|))*?)?\s*\)?\s*\?\.titleField\b/

这条正则是「有界的」,⛔ 不是「放宽的」 —— 容忍的只有一个 as 子句,|| 不许出现在容忍区间内(单个 | 放行是为了 TreeViewConfig | undefined 这样的联合类型)。⇒ titleField 必须取自这一个 tree,而不是同一行上更后面的另一个项。

一条朴素的 /tree.*titleField/ 会匹配 tree?.labelField || other?.titleField —— 也就是读取真的搬走了它也照样绿。这条不会。

docblock 把原理写得比我的裁决更准:「A pin on a READ must not double as a pin on the annotation standing in front of it.」

⛔ 代码形状一个字节没动 —— 因为把 cast 提成局部变量会弄红 #6557 的 convergence pin(它要求每条 seam 行各自点名 viewDef)。我设的那道栅栏被守住了。

⭐ 决定性消融通过,而且带了第二半证明

删掉 titleField 读取(⛔ 不是改写,是整项删掉),盘上先证(marker 1→0、blob 移动,且脚本在三项证据任一不符时硬退出 ⇒ 一次空转的消融不可能被读成结果),修好的钉子按用例名变红

× the console composition reads it        Tests  1 failed | 18 passed (19)

而新增的 CONTROL 用例在旁边保持绿色 —— dev 的原话:「the instrument still discriminates rather than having become unable to fail」。这正是我要的第二半:只证明它会红不够,还要证明它没被弄瞎。

⭐⭐ 星标交付物:枚举,而且完整性是实证的,不是论证的

26 个测试文件、跨 7 个包ObjectView.tsx 当源文本读,分成两类:

而完整性的证明是实证的,⛔ 不是「我逐条对过了」:

on head 5bcc0c899 the farm ran all four shards and exactly ONE was red, and its check-run annotations number exactly 2 — the process exit plus tree-view-config-readers-8253.test.ts:191.

26 个读者里恰好一个被弄假,而它就是被修的那个。 用 CI 的实际结果当作枚举完整性的判准,比任何逐条论证都硬。

⭐ 并且他们点名了一个擦肩而过的:no-component-any-cast.ratchet.test.ts —— 本 diff 确实从它遍历的文件里删了两个 as any,但它只禁 as React.FC<any> 一族,所以没看见。点名一个差点命中的,是「真的逐条看过」的证据,⛔ 而不是「扫了一眼」。

⚠️ 带走的一条:同一个 describe 里还有两条同样脆的兄弟断言

expect(read(VIEW_BRANCH)).toContain('viewOptions.tree?.titleField')
expect(read(LIST_BRANCH)).toContain('treeCfg.titleField')

任何人把 plugin-view / plugin-list 那两个分支照 TreeViewConfig 类型化,它们会以完全相同的方式断掉 —— 而那正是 #8253 那次导出的自然后续。

dev 刻意没有改它们,理由正确:「a pin your own diff made false is yours to repair while a pin your diff did not touch is not」。⇒ 修好的那条现在带着可照抄的模式。承接者:下一个把那两个分支类型化的人。

⛔ 那条更一般的观察(源文本钉子用跨两个语法半边的字面子串作证据 ⇒ 任何中间插入的标注都读作「读取被删」)没有立卡,理由也对:今天没有可复现缺陷、没有被违反的已声明契约,而且这个仓是有意选择「源文本钉子 + 打火对照」这个技术的。⇒ 记录,不立卡。


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 e6a5cc6 Sep 9, 2026
34 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-7559-viewdef-relay-rung-census branch September 9, 2026 17:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants