Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 20 additions & 17 deletions ui/packages/plugin-view/src/ObjectView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -864,11 +864,11 @@ type FormMode = 'create' | 'edit' | 'view';
* ## The verdict
*
* The delegation branch in `renderContent` below reads 30 distinct keys off
* the object-view node through `(schema as any).K` and forwards them to the
* host's list renderer. Three of them — `data`, `navigation`,
* `searchableFields`, listed in
* the object-view node and forwards them to the host's list renderer. Three
* of them — `data`, `navigation`, `searchableFields`, listed in
* {@link OBJECT_VIEW_DECLARED_FORWARDED_KEYS} — are declared members of
* `ObjectViewSchema`. The other 27, listed here, are not, and stay that way:
* `ObjectViewSchema` and are read without a cast. The other 27, listed here,
* are read through `(schema as any).K`, are not declared, and stay that way:
* they are HOST-COMPOSITION surface, not authored surface. This constant is
* their single home; the branch is fenced by `#region` markers so the pin in
* `src/__tests__/objectViewHostSurface.test.tsx` fails BY NAME when a read is
Expand Down Expand Up @@ -986,9 +986,10 @@ export const OBJECT_VIEW_HOST_COMPOSITION_KEYS = [
] as const;

/**
* The other three keys the same branch reads through `(schema as any)`: these
* ARE declared members of `ObjectViewSchema` (`data` via `BaseSchema`), so
* they are authored surface and are NOT part of the objectui#5097 exemption.
* The other three keys the same branch reads off the node: these ARE declared
* members of `ObjectViewSchema` (`data` via `BaseSchema`), so they are
* authored surface, are read as `schema.K` with no cast, and are NOT part of
* the objectui#5097 exemption.
* They are listed so the pin can assert the split, not just the exempt half —
* a read that moves from this list into the one above is a key losing its
* declaration, and must fail loudly.
Expand Down Expand Up @@ -2954,9 +2955,11 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
// If a custom renderListView is provided, use it
// #region object-view HOST-COMPOSITION SURFACE (objectui#5097)
//
// 30 of the values assembled below are read off the object-view node with
// `(schema as any).K`. Three are declared members of `ObjectViewSchema`;
// the other 27 are NOT, deliberately: the maintainer ruling of 2026-08-18
// 30 of the values assembled below are read off the object-view node. Three
// are declared members of `ObjectViewSchema`
// (`OBJECT_VIEW_DECLARED_FORWARDED_KEYS`) and are read as `schema.K`, with
// no cast; the other 27 are read with `(schema as any).K` because they are
// NOT declared, deliberately: the maintainer ruling of 2026-08-18
// on objectui#5097 (verbatim 「同意」) ruled them HOST-COMPOSITION surface,
// not authored surface, because this branch is entered only when a HOST
// passes `renderListView` and the registered renderer never does.
Expand Down Expand Up @@ -3106,16 +3109,16 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
// never reaches the renderer.
// objectui#8980 / objectui#7928's open half: the cast on the
// named-view config is GONE — `data` is a declared `NamedListView`
// member now, so the read and the declaration are one fact. The
// `activeView` and node rungs keep their casts: the host `views` entry
// is an untyped host shape and `data` on the node is one of the 27
// objectui#5097 host-composition keys.
data: currentNamedViewConfig?.data ?? (activeView as any)?.data ?? (schema as any).data,
// member now, so the read and the declaration are one fact. The node
// rung is uncast for the same reason: `data` is declared on the node
// (via `BaseSchema`). The `activeView` rung keeps its cast: the host
// `views` entry is an untyped host shape.
data: currentNamedViewConfig?.data ?? (activeView as any)?.data ?? schema.data,
// Propagate new spec properties (P0/P1/P2)
navigation: currentNamedViewConfig?.navigation ?? activeView?.navigation ?? (schema as any).navigation,
navigation: currentNamedViewConfig?.navigation ?? activeView?.navigation ?? schema.navigation,
selection: currentNamedViewConfig?.selection ?? activeView?.selection ?? (schema as any).selection,
pagination: currentNamedViewConfig?.pagination ?? activeView?.pagination ?? (schema as any).pagination,
searchableFields: currentNamedViewConfig?.searchableFields ?? activeView?.searchableFields ?? (schema as any).searchableFields,
searchableFields: currentNamedViewConfig?.searchableFields ?? activeView?.searchableFields ?? schema.searchableFields,
resizable: currentNamedViewConfig?.resizable ?? activeView?.resizable ?? (schema as any).resizable,
hiddenFields: currentNamedViewConfig?.hiddenFields ?? activeView?.hiddenFields ?? (schema as any).hiddenFields,
// objectui#8980 — TWO SLOTS THAT HAD NO RUNG ON THIS BRANCH AT ALL.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
* `ObjectViewSchema`; 27 are not. objectui#13118 dropped one of the four,
* `filterableFields`, with the spec's retirement of the key (objectstack#24640,
* ruling C), so the branch reads 30 today: three declared, the 27 unchanged.
* objectstack#25918 dropped the cast from the three declared reads, so only
* the 27 are still read through `(schema as any).K`.
*
* The maintainer ruled on 2026-08-18 (verbatim 「同意」) that the 27 are HOST
* surface, exempted with reasons — not authored surface — on a MEASURED
Expand Down Expand Up @@ -129,6 +131,18 @@ const castReadsIn = (slice: string): string[] =>
),
].sort();

/**
* Every UNCAST read of a key off `schema` in a slice of CODE, distinct, sorted.
* The lookbehind keeps `objectSchema.K` and `x.schema.K` out; the cast form
* never matches, because `schema` is followed by ` as any)` there, not `.`.
*/
const uncastReadsIn = (slice: string): string[] =>
[
...new Set(
[...stripComments(slice).matchAll(/(?<![\w$.])schema\.([A-Za-z_$][\w$]*)/g)].map((m) => m[1]),
),
].sort();

const regionSlice = (): string => {
const start = SOURCE.indexOf(REGION_OPEN);
const end = SOURCE.indexOf(REGION_CLOSE);
Expand All @@ -153,8 +167,14 @@ describe('the forwarded key set equals the documented exemption (objectui#5097)'
const exempt: string[] = [...OBJECT_VIEW_HOST_COMPOSITION_KEYS].sort();
const declared: string[] = [...OBJECT_VIEW_DECLARED_FORWARDED_KEYS].sort();

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

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

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

Expand Down
Loading