Skip to content

Commit 38bbc2f

Browse files
refactor(plugin-view): read the three declared object-view keys without a cast (#27273)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ819oUQ9cjPHsHVoZ112LVMA)_ Part of #25918 (the ListView reads and the lint rule stay open on the card). Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:ObjectView 把三个已声明键(`data`、`navigation`、`searchableFields`)的 `(schema as any)` 读法改成直接 `schema.K`;ObjectGrid 两处读法经核实都有真实生产者,保持不动。 - 为什么改:类型已经声明的键再走 `as any` 只是遮住类型检查,卡 #25918 要求删掉这类残留写法。 - 风险与回滚:零运行时变化(去掉 `as any` 后生成的 JS 相同);27 个宿主组合键的读法一个没动;回滚即 revert 本 PR。 - 先看:`ui/packages/plugin-view/src/ObjectView.tsx` 的 `renderListView` 区段;`objectViewHostSurface.test.tsx` 改过的两条断言。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/plugin-view/src/__tests__/objectViewHostSurface.test.tsx`,预期全部通过。 - 需要你做的:无,席位落地。 ## What changes - `ui/packages/plugin-view/src/ObjectView.tsx`: inside the `#region object-view HOST-COMPOSITION SURFACE (objectui#5097)` fence, the three keys in `OBJECT_VIEW_DECLARED_FORWARDED_KEYS` (`data`, `navigation`, `searchableFields`) are now read as `schema.K`. All three are declared on `ObjectViewSchema` (`data` through `BaseSchema`), so the cast hid nothing. The 27 `OBJECT_VIEW_HOST_COMPOSITION_KEYS` reads keep their cast, per the objectui#5097 ruling. The fence comment, the two docblocks that described all 30 reads as casts, and the `data` rung comment (which wrongly called node `data` one of the 27) now say what the code does. - `objectViewHostSurface.test.tsx`: the fence pin now derives the cast set and the uncast set separately. It asserts the cast reads are exactly the 27 exempt keys, and that each declared key is read uncast. The named-view section's count drops from 27+3 to 27 for the same reason. - `object-view-unmirrored-keys-7779.test.ts` (`@object-ui/types`): three literal source-text pins (the `searchableFields` read twice, the `data` read once) follow the new text. Its census regex already matched both spellings, so no count changed. `ObjectGrid.tsx` is unchanged. Both of its `(schema as any)` reads are on keys that a real producer writes onto an `object-grid` node, so neither read can stop here: - `rowActionDefs`: `DevRowActions.tsx` (console dev page, an `object-grid` node literal), `ListView`'s grid hand-off, `RelatedList` and app-shell's `objectViewListRenderer`. The key is declared on `DataTableSchema` but not on `ObjectGridSchema`, and `ComponentPropsMap['object-grid']` refuses it by name. - `hideRowHeightToggle`: `ListView`'s `baseProps` set it to `true` on the `object-grid` node it builds. It is declared nowhere. Both are already recorded as ruled non-author surface (objectui#5091) and pinned by `gridNonAuthorKeys.test.tsx`. They are reported to the seat as findings and not filed here. ## Tests Run on head `4c0dabbb` (merge of `origin/main` `1e8e5296`): - Closure build `pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-view^...' build` under `os-verify-lock`: VERDICT command-exit 0. - `pnpm --dir ui exec vitest run --maxWorkers=2 packages/plugin-view/` plus every test outside the package that reads `plugin-view/src/ObjectView.tsx` off disk (six `@object-ui/types` census files, plugin-form `affordanceGrantMap-12082`, plugin-list `ViewSwitcher.i18n-4024`, two plugin-map files, react `navigationOverlayConsumers.onRowClickArity-9357`, schema-catalog `catalog-gallery-render`): Test Files 89 passed (89), Tests 1738 passed (1738). - `pnpm --filter @object-ui/plugin-view run type-check` exit 0, and `pnpm --filter @object-ui/types run type-check` exit 0. Both scripts include `tsc -p tsconfig.test.json`, so the edited tests are compiled. - No ablation: no read was stopped and no runtime behaviour changed. Dropping an `as any` emits the same JavaScript, and the existing `forwards all 30 reads` render test still pins every value end to end. ## Acceptance notes - `(activeView as any)?.data` and `(activeView as any)?.rowActionDefs` in the same fence are casts on the host `views` prop entry, not on `schema`. They are outside this card's surface and are left as they are. - No changeset (`skip-changeset`): the emitted code of both published packages is unchanged. Only comments and type-level casts moved, and the `@object-ui/types` change is test-only. ## Verification `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 24 gate commands on this diff, and all 24 exited 0 (reconciled with `--ran`). Also `pnpm run check:new-line-citations` (ui): 0 new citations. `dispatch-gates --tier`: no path-derived mandate. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [ ] 3. No formatter ran; `pnpm lint` passes. (No formatter ran; lint was not run locally, CI `Lint UI` reads it.) - [x] 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. (Not applicable: nothing published changes; `skip-changeset`.) - [ ] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (Not applicable: `packages/spec` untouched.) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [ ] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JQwxbaGCrvY8peSVMF3rQw --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbaGCrvY8peSVMF3rQw)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent e9c88f7 commit 38bbc2f

3 files changed

Lines changed: 50 additions & 26 deletions

File tree

‎ui/packages/plugin-view/src/ObjectView.tsx‎

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -864,11 +864,11 @@ type FormMode = 'create' | 'edit' | 'view';
864864
* ## The verdict
865865
*
866866
* The delegation branch in `renderContent` below reads 30 distinct keys off
867-
* the object-view node through `(schema as any).K` and forwards them to the
868-
* host's list renderer. Three of them — `data`, `navigation`,
869-
* `searchableFields`, listed in
867+
* the object-view node and forwards them to the host's list renderer. Three
868+
* of them — `data`, `navigation`, `searchableFields`, listed in
870869
* {@link OBJECT_VIEW_DECLARED_FORWARDED_KEYS} — are declared members of
871-
* `ObjectViewSchema`. The other 27, listed here, are not, and stay that way:
870+
* `ObjectViewSchema` and are read without a cast. The other 27, listed here,
871+
* are read through `(schema as any).K`, are not declared, and stay that way:
872872
* they are HOST-COMPOSITION surface, not authored surface. This constant is
873873
* their single home; the branch is fenced by `#region` markers so the pin in
874874
* `src/__tests__/objectViewHostSurface.test.tsx` fails BY NAME when a read is
@@ -986,9 +986,10 @@ export const OBJECT_VIEW_HOST_COMPOSITION_KEYS = [
986986
] as const;
987987

988988
/**
989-
* The other three keys the same branch reads through `(schema as any)`: these
990-
* ARE declared members of `ObjectViewSchema` (`data` via `BaseSchema`), so
991-
* they are authored surface and are NOT part of the objectui#5097 exemption.
989+
* The other three keys the same branch reads off the node: these ARE declared
990+
* members of `ObjectViewSchema` (`data` via `BaseSchema`), so they are
991+
* authored surface, are read as `schema.K` with no cast, and are NOT part of
992+
* the objectui#5097 exemption.
992993
* They are listed so the pin can assert the split, not just the exempt half —
993994
* a read that moves from this list into the one above is a key losing its
994995
* declaration, and must fail loudly.
@@ -2954,9 +2955,11 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
29542955
// If a custom renderListView is provided, use it
29552956
// #region object-view HOST-COMPOSITION SURFACE (objectui#5097)
29562957
//
2957-
// 30 of the values assembled below are read off the object-view node with
2958-
// `(schema as any).K`. Three are declared members of `ObjectViewSchema`;
2959-
// the other 27 are NOT, deliberately: the maintainer ruling of 2026-08-18
2958+
// 30 of the values assembled below are read off the object-view node. Three
2959+
// are declared members of `ObjectViewSchema`
2960+
// (`OBJECT_VIEW_DECLARED_FORWARDED_KEYS`) and are read as `schema.K`, with
2961+
// no cast; the other 27 are read with `(schema as any).K` because they are
2962+
// NOT declared, deliberately: the maintainer ruling of 2026-08-18
29602963
// on objectui#5097 (verbatim 「同意」) ruled them HOST-COMPOSITION surface,
29612964
// not authored surface, because this branch is entered only when a HOST
29622965
// passes `renderListView` and the registered renderer never does.
@@ -3106,16 +3109,16 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
31063109
// never reaches the renderer.
31073110
// objectui#8980 / objectui#7928's open half: the cast on the
31083111
// named-view config is GONE — `data` is a declared `NamedListView`
3109-
// member now, so the read and the declaration are one fact. The
3110-
// `activeView` and node rungs keep their casts: the host `views` entry
3111-
// is an untyped host shape and `data` on the node is one of the 27
3112-
// objectui#5097 host-composition keys.
3113-
data: currentNamedViewConfig?.data ?? (activeView as any)?.data ?? (schema as any).data,
3112+
// member now, so the read and the declaration are one fact. The node
3113+
// rung is uncast for the same reason: `data` is declared on the node
3114+
// (via `BaseSchema`). The `activeView` rung keeps its cast: the host
3115+
// `views` entry is an untyped host shape.
3116+
data: currentNamedViewConfig?.data ?? (activeView as any)?.data ?? schema.data,
31143117
// Propagate new spec properties (P0/P1/P2)
3115-
navigation: currentNamedViewConfig?.navigation ?? activeView?.navigation ?? (schema as any).navigation,
3118+
navigation: currentNamedViewConfig?.navigation ?? activeView?.navigation ?? schema.navigation,
31163119
selection: currentNamedViewConfig?.selection ?? activeView?.selection ?? (schema as any).selection,
31173120
pagination: currentNamedViewConfig?.pagination ?? activeView?.pagination ?? (schema as any).pagination,
3118-
searchableFields: currentNamedViewConfig?.searchableFields ?? activeView?.searchableFields ?? (schema as any).searchableFields,
3121+
searchableFields: currentNamedViewConfig?.searchableFields ?? activeView?.searchableFields ?? schema.searchableFields,
31193122
resizable: currentNamedViewConfig?.resizable ?? activeView?.resizable ?? (schema as any).resizable,
31203123
hiddenFields: currentNamedViewConfig?.hiddenFields ?? activeView?.hiddenFields ?? (schema as any).hiddenFields,
31213124
// objectui#8980 — TWO SLOTS THAT HAD NO RUNG ON THIS BRANCH AT ALL.

‎ui/packages/plugin-view/src/__tests__/objectViewHostSurface.test.tsx‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
* `ObjectViewSchema`; 27 are not. objectui#13118 dropped one of the four,
3030
* `filterableFields`, with the spec's retirement of the key (objectstack#24640,
3131
* ruling C), so the branch reads 30 today: three declared, the 27 unchanged.
32+
* objectstack#25918 dropped the cast from the three declared reads, so only
33+
* the 27 are still read through `(schema as any).K`.
3234
*
3335
* The maintainer ruled on 2026-08-18 (verbatim 「同意」) that the 27 are HOST
3436
* surface, exempted with reasons — not authored surface — on a MEASURED
@@ -129,6 +131,18 @@ const castReadsIn = (slice: string): string[] =>
129131
),
130132
].sort();
131133

134+
/**
135+
* Every UNCAST read of a key off `schema` in a slice of CODE, distinct, sorted.
136+
* The lookbehind keeps `objectSchema.K` and `x.schema.K` out; the cast form
137+
* never matches, because `schema` is followed by ` as any)` there, not `.`.
138+
*/
139+
const uncastReadsIn = (slice: string): string[] =>
140+
[
141+
...new Set(
142+
[...stripComments(slice).matchAll(/(?<![\w$.])schema\.([A-Za-z_$][\w$]*)/g)].map((m) => m[1]),
143+
),
144+
].sort();
145+
132146
const regionSlice = (): string => {
133147
const start = SOURCE.indexOf(REGION_OPEN);
134148
const end = SOURCE.indexOf(REGION_CLOSE);
@@ -153,8 +167,14 @@ describe('the forwarded key set equals the documented exemption (objectui#5097)'
153167
const exempt: string[] = [...OBJECT_VIEW_HOST_COMPOSITION_KEYS].sort();
154168
const declared: string[] = [...OBJECT_VIEW_DECLARED_FORWARDED_KEYS].sort();
155169

156-
it('the fence reads exactly the 27 exempt keys plus the 3 declared ones', () => {
157-
expect(castReadsIn(regionSlice())).toEqual([...exempt, ...declared].sort());
170+
it('the fence reads exactly the 27 exempt keys through the cast, and the 3 declared ones without it', () => {
171+
// The three declared members are read as `schema.K` since objectstack#25918:
172+
// a cast on a key the type already declares hides nothing and is retired.
173+
// ⛔ That is the ONLY kind of cast this card removed; the 27 stay cast,
174+
// because they are undeclared by ruling.
175+
expect(castReadsIn(regionSlice())).toEqual(exempt);
176+
const uncast = uncastReadsIn(regionSlice());
177+
for (const k of declared) expect(uncast, `\`${k}\` is no longer read off the node uncast`).toContain(k);
158178
});
159179

160180
it('subtracting the declared members leaves exactly the exempt list — by name', () => {
@@ -342,7 +362,7 @@ describe('the fence reads the named view first for every protocol member it rela
342362
expect(cast, `\`${k}\` is now read off the node through a cast`).not.toContain(k);
343363
expect(namedViewReadsIn(regionSlice())).toContain(k);
344364
}
345-
expect(cast).toHaveLength(OBJECT_VIEW_HOST_COMPOSITION_KEYS.length + OBJECT_VIEW_DECLARED_FORWARDED_KEYS.length);
365+
expect(cast).toHaveLength(OBJECT_VIEW_HOST_COMPOSITION_KEYS.length);
346366
});
347367
});
348368

‎ui/packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ const READ_TEXT: Record<ReadMirrored, ReadonlyArray<readonly [file: string, text
200200
// objectui#10885: the node's `navigation` is still read, now as the fallback
201201
// behind the active named view's.
202202
navigation: [[READER, 'const navigationConfig: ViewNavigationConfig | undefined = currentNamedViewConfig?.navigation ?? schema.navigation;']],
203-
searchableFields: [[READER, 'searchableFields: currentNamedViewConfig?.searchableFields ?? activeView?.searchableFields ?? (schema as any).searchableFields,']],
203+
searchableFields: [[READER, 'searchableFields: currentNamedViewConfig?.searchableFields ?? activeView?.searchableFields ?? schema.searchableFields,']],
204204
allowCreateView: [
205205
[READER, 'allowCreateView: schema.allowCreateView,'],
206206
[SWITCHER, 'const createViewButton = schema.allowCreateView ? ('],
@@ -1095,7 +1095,7 @@ describe('objectui#7779 — the renderer reads the eight mirrored keys, which is
10951095
expect(namedViewReads()).not.toContain(SPEC_RETIRED_UNREAD);
10961096
expect(src).not.toContain('activeView?.filterableFields');
10971097
// Control on the same file and the same spelling: its sibling's rung stands.
1098-
expect(src).toContain('searchableFields: currentNamedViewConfig?.searchableFields ?? activeView?.searchableFields ?? (schema as any).searchableFields,');
1098+
expect(src).toContain('searchableFields: currentNamedViewConfig?.searchableFields ?? activeView?.searchableFields ?? schema.searchableFields,');
10991099
// …and `NamedListView` carries the tombstone, while the node's two faces keep
11001100
// the spec's slot by reference, which carries the spec's own tombstone in at
11011101
// the pin bump that takes the retirement.
@@ -1656,10 +1656,11 @@ describe('objectui#7924 — the per-member liveness census on `NamedListView`, r
16561656
expect(declared.has('data'), '`data` is a declared member since objectui#8980; the census arithmetic subtracts the whole read set').toBe(true);
16571657
expect(reads.reads).toContain('data');
16581658
// …and it is reached through the DECLARED path: the cast on the named-view
1659-
// config is gone. The two neighbouring casts are untouched and stay — the
1660-
// host `views` entry is an untyped host shape, and `data` on the node is one
1661-
// of the 27 objectui#5097 host-composition keys.
1662-
expect(readRepo(READER)).toContain('data: currentNamedViewConfig?.data ?? (activeView as any)?.data ?? (schema as any).data,');
1659+
// config is gone, and so is the one on the node (objectstack#25918) — `data`
1660+
// is declared there too, via `BaseSchema`, and is not one of the 27
1661+
// objectui#5097 host-composition keys. The `activeView` cast stays: the
1662+
// host `views` entry is an untyped host shape.
1663+
expect(readRepo(READER)).toContain('data: currentNamedViewConfig?.data ?? (activeView as any)?.data ?? schema.data,');
16631664
expect(readRepo(READER)).not.toContain('(currentNamedViewConfig as any)?.data');
16641665
});
16651666

0 commit comments

Comments
 (0)