Found while implementing objectui#7218 (relay a per-view rowColor). Filed rather
than folded into that PR: it is the MECHANISM behind that card's defect class, not
that card's defect, and acting on it is a design decision rather than a rung.
The observation
packages/app-shell/src/views/ObjectView.tsx builds the list schema its
renderListView hands to ListView by spreading the host's schema and then
relaying 47 named keys off the active view:
const fullSchema: ListViewSchema = {
...listSchema,
label: viewDef.label ?? listSchema.label,
description: viewDef.description ?? listSchema.description,
...45 more
};
The OUTPUT is typed — fullSchema: ListViewSchema. The INPUT is not. viewDef
is activeView, an element of the array buildViewTabs returns, whose declared
type is Array<Record<string, any> & { id: string }>. So every rung reads any.
⇒ The compiler checks that each key this literal WRITES is a legal
ListViewSchema member. Nothing checks the other direction — that a declared
ListViewSchema member the renderer READS actually has a rung. A key with no
rung is not a type error, not a lint finding, and not a test failure. It is
silence.
Why this is worth a card: the class has now recurred three times
| card |
key |
state |
| objectui#7199 |
description |
fixed |
| objectui#7218 |
rowColor |
fixed (this finding's PR) |
| objectui#7516 |
fieldOrder |
open, needs-user-decision |
All three are the same shape, and the card that found the second pair described
it exactly: "nothing errors, every authoring gate passes, and the only symptom
is that an authored column order (or row colour) has no effect on screen. An
author has no way to notice short of diffing the DOM."
Three instances of one mechanism, found one at a time by eye, is the signal that
the mechanism deserves the attention rather than the instances.
What this is NOT
⛔ Not the objectui#5043 family track. That one owns the (schema as any) reads
in plugin-view's ObjectView — typing the HOST-COMPOSITION block as an
explicit host-side prop contract. This is the other host and the other direction:
app-shell's relay has no casts to retire, it has a source object whose type is
Record<string, any> and a set of rungs nobody can enumerate against the target.
⛔ Not a claim that a rung is missing today. I did not census the remaining
ListViewSchema members against the 47 rungs — that census is most of the work
and is what a dispatched card would do first.
Not verified here
Whether the right answer is (a) type viewDef as the view-entry type it actually
carries so the compiler can see the gap, (b) a gate that enumerates
ListViewSchema members against the relay's key set and requires each absence to
be declared, or (c) neither, because a relay is deliberately a whitelist and the
absences are choices rather than omissions. Option (c) is a real possibility and
is why this is a finding rather than a bug: quickFilters: undefined in that
same literal is an absence that IS deliberate and documented.
There is prior art for the typing half being genuinely hard — objectui#4337
recorded that the legacy list-view vocabulary has no declared type at all, so
every fixture exercising the fold must cast.
Where to start
- The relay:
packages/app-shell/src/views/ObjectView.tsx, the fullSchema
literal inside the renderListView callback.
- The source type:
buildViewTabs in the same file, and viewEntry in
packages/app-shell/src/utils/viewIdentity.ts.
- The target:
ListViewSchema in packages/types/src/objectql.ts, which imports
its spec-owned members by reference.
- The sibling host, for contrast:
packages/plugin-view/src/ObjectView.tsx, whose
equivalent literal IS fenced and DOES have a pin that re-derives its read set
from the source at test time (objectViewHostSurface.test.tsx) — a working
example of the "enumerate the set mechanically" option applied to the other
direction.
Filed unassigned, per the dispatch. Related: objectui#7199, objectui#7218,
objectui#7516, objectui#5043, objectui#4337 — none of them is this, and none of
them is duplicated by it.
Generated by Claude Code, session session_01EMrWaQw3XS5DxTHxp4yRyC, while
implementing objectui#7218.
Found while implementing objectui#7218 (relay a per-view
rowColor). Filed ratherthan folded into that PR: it is the MECHANISM behind that card's defect class, not
that card's defect, and acting on it is a design decision rather than a rung.
The observation
packages/app-shell/src/views/ObjectView.tsxbuilds the list schema itsrenderListViewhands toListViewby spreading the host's schema and thenrelaying 47 named keys off the active view:
The OUTPUT is typed —
fullSchema: ListViewSchema. The INPUT is not.viewDefis
activeView, an element of the arraybuildViewTabsreturns, whose declaredtype is
Array<Record<string, any> & { id: string }>. So every rung readsany.⇒ The compiler checks that each key this literal WRITES is a legal
ListViewSchemamember. Nothing checks the other direction — that a declaredListViewSchemamember the renderer READS actually has a rung. A key with norung is not a type error, not a lint finding, and not a test failure. It is
silence.
Why this is worth a card: the class has now recurred three times
descriptionrowColorfieldOrderneeds-user-decisionAll three are the same shape, and the card that found the second pair described
it exactly: "nothing errors, every authoring gate passes, and the only symptom
is that an authored column order (or row colour) has no effect on screen. An
author has no way to notice short of diffing the DOM."
Three instances of one mechanism, found one at a time by eye, is the signal that
the mechanism deserves the attention rather than the instances.
What this is NOT
⛔ Not the objectui#5043 family track. That one owns the
(schema as any)readsin
plugin-view'sObjectView— typing the HOST-COMPOSITION block as anexplicit host-side prop contract. This is the other host and the other direction:
app-shell's relay has no casts to retire, it has a source object whose type isRecord<string, any>and a set of rungs nobody can enumerate against the target.⛔ Not a claim that a rung is missing today. I did not census the remaining
ListViewSchemamembers against the 47 rungs — that census is most of the workand is what a dispatched card would do first.
Not verified here
Whether the right answer is (a) type
viewDefas the view-entry type it actuallycarries so the compiler can see the gap, (b) a gate that enumerates
ListViewSchemamembers against the relay's key set and requires each absence tobe declared, or (c) neither, because a relay is deliberately a whitelist and the
absences are choices rather than omissions. Option (c) is a real possibility and
is why this is a
findingrather than a bug:quickFilters: undefinedin thatsame literal is an absence that IS deliberate and documented.
There is prior art for the typing half being genuinely hard — objectui#4337
recorded that the legacy
list-viewvocabulary has no declared type at all, soevery fixture exercising the fold must cast.
Where to start
packages/app-shell/src/views/ObjectView.tsx, thefullSchemaliteral inside the
renderListViewcallback.buildViewTabsin the same file, andviewEntryinpackages/app-shell/src/utils/viewIdentity.ts.ListViewSchemainpackages/types/src/objectql.ts, which importsits spec-owned members by reference.
packages/plugin-view/src/ObjectView.tsx, whoseequivalent literal IS fenced and DOES have a pin that re-derives its read set
from the source at test time (
objectViewHostSurface.test.tsx) — a workingexample of the "enumerate the set mechanically" option applied to the other
direction.
Filed unassigned, per the dispatch. Related: objectui#7199, objectui#7218,
objectui#7516, objectui#5043, objectui#4337 — none of them is this, and none of
them is duplicated by it.
Generated by Claude Code, session
session_01EMrWaQw3XS5DxTHxp4yRyC, whileimplementing objectui#7218.