Repository navigation
fix(app-shell,plugin-list): retire the invented gallery cover binding, and draw the switcher only when it can switch (objectui#7547) - #8188
Conversation
…he whitelist `showViewSwitcher` reaches `ListView` already decided, and both faces that decide it — `app-shell/ObjectView` and `app-shell/InterfaceListPage` — count the LENGTH of `appearance.allowedVisualizations`. That is the whitelist BEFORE `availableViews` intersects it with the ADR-0047 capability gate, so a view whitelisting `['grid', 'timeline']` with no timeline block whitelisted two, resolved one, and drew switcher chrome (a collapsed "List" dropdown, its separator and the border cluster) around a single Grid entry that could not switch to anything. Neither face can compute the right number: the gate that answers "does this type resolve?" lives in `ListView`. So the predicate is applied at the one site holding both halves (`viewSwitcherOffered`), which is also why one change covers both doors and cannot drift from the list the dropdown is handed. The two relay sites keep their length test — it is the author's INTENT — and each gains a note saying the whitelist is not the offer. Part of objectui#7547. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
…er invents one `options.gallery.imageField` was floored at `'image'` for every object view, declared or not — the last member on this face of the class objectui#7029 (calendar) and objectui#7070 (gantt dates) removed. What it actually broke is the GATE, not the screen. `ObjectGallery` resolves the cover per record and collapses the cover area when none resolves, so the invented name degraded to a gallery of coverless cards. But `ListView`'s ADR-0047 capability gate asks `schema.options?.gallery?.imageField`, and a key this relay always supplied made that check answer YES for every object view whitelisting `gallery`: whitelist AND resolvable — the mechanism that exists to stop a visualization being offered with nothing behind it — was answering about a name this file wrote rather than one the author did. `galleryViewOptions` now mirrors `ganttViewOptions` next door: the view's own block spread whole, the two legacy cover spellings cross-filling each other ONLY when one was declared, and `titleField` keeping its `'name'` display floor. The premise was measured before the deletion, as objectui#7070 requires: `ObjectGallery` keeps its own `coverField ?? 'image'` rung and that is correct there — it is the component's decision about an unconfigured record, and it is honest because the cover area collapses when the guess yields nothing. The `'image'` positive controls in the objectui#7029 and objectui#7070 source scans are RE-ANCHORED onto the chart branch's `|| 'value'` measure floor, per the instruction each of them carries. They are not deleted and not weakened. Part of objectui#7547. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
…ly spelling `ObjectView.titleFieldConvergence.test.tsx` pins all six `titleField` / `labelField` seams to ONE expression shape — a chain of VIEW-declared rungs floored at 'name', with `viewDef` as the mechanical proxy for "view-declared". `galleryViewOptions` read the rung off its local `gallery` alias, which resolves the same value but blinds that scan to exactly what it hunts. The pin is right and the code was wrong: restoring `viewDef?.gallery?.titleField` costs nothing and keeps the scan making its claim. Weakening the regex to accept an arbitrary local would have been the other way round. Part of objectui#7547. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
…DataPage) Comment only, no behaviour. objectui#7547 named two faces computing `showViewSwitcher` from the whitelist LENGTH; enumerating every `ListView` render site instead of grepping the spelling turns up a third, `ObjectDataPage.tsx`, with the identical expression. All three are already corrected by the `viewSwitcherOffered` predicate in `ListView` — that is the point of fixing it at the site that holds both halves — so this adds the same INTENT note its two twins carry rather than a fix. The same enumeration cleared the gallery axis on both sibling faces: `defaultGalleryFromObject` returns a cover only when the OBJECT declares an image-typed field and `undefined` otherwise, so neither invents a name. Only `ObjectView`'s `|| 'image'` was fabricating. Part of objectui#7547. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
✅ Console Performance Budget
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
Size Limits
|
Contract review — PASS. Clause-② does not bind. Held on CI, and on being a partial delivery by design.PM seat, session Clause-② — re-measured rather than acceptedThe diff adds exactly one export, With zero star re-exports and no touch to ⭐ The best thing in this run: it broke a pin and fixed the codeThe app-shell suite failed on The pin was right and the code was wrong, so the code went back to the family spelling ( ⭐ The dispatch's omission warning found a third defect siteI have been propagating one method rule all session: a grep for an explicit spelling cannot measure a population defined by omission — enumerate the sites, then ask which opt in. Applied here to the six The same enumeration also cleared two sibling faces on the gallery axis, which derive covers via ⛔ The card's prescribed route for the map axis is FALSIFIED — and refusing it was right
Shipping the prescribed route would have made this defect worse. Filing #8169 for a ruling instead of executing it is exactly the behaviour the dispatch asked for when it said the premise might be wrong and I wanted to hear that rather than a patch. Graded Verification worth noting
Status: partial by designThis card is Held on CI convergence: at review time Generated by Claude Code |
Part of #7547.
The class
objectui#7029 (calendar), #7070 (gantt dates) and #7500 (timeline) each removed one member of a family: a relay that invents a binding name the author never wrote, which downstream is indistinguishable from a real one. #7547 is the survivors. They live only on the FORCED path — an authored
type:or an interface page's leading whitelist entry — because the switcher path already builds its offer from PRESENT bindings.Commit 1 — the switcher is drawn for the RESOLVED list, not the whitelist
showViewSwitcherreachesListViewalready decided, and every face that decides it counts the LENGTH ofappearance.allowedVisualizations. That is the whitelist BEFOREavailableViewsintersects it with the ADR-0047 capability gate. A view whitelisting['grid', 'timeline']with no timeline block whitelists two and resolves one, so the toolbar drew the collapsed switcher affordance, its separator and the border cluster around a single Grid entry that could not switch to anything.Neither face can compute the right number — the gate lives in
ListView— so the predicate is applied at the one site holding both halves (viewSwitcherOffered). One change, every door, and it cannot drift from the list the dropdown is handed. The relay sites keep their length test as the author's INTENT and each gains a note saying the whitelist is not the offer.⭐ There are THREE faces carrying that length count, not the two the card names. Found by enumerating every
ListViewrender site rather than grepping for the spelling — a population defined partly by omission (a site that passes nothing takes the default) cannot be measured by grepping the explicit form. The six real render sites:app-shell/src/views/ObjectView.tsxallowedVisualizations?.length ?? 0) > 1app-shell/src/views/InterfaceListPage.tsxallowed.length > 1app-shell/src/views/ObjectDataPage.tsxallowedVisualizations.length > 1— the third, unlisted anywhereapp-shell/src/views/studio-design/StudioDesignSurface.tsxplugin-list/src/ListViewBlock.tsxapps/console/src/sdui-workbench-preview.tsxAll three are corrected by the single
ListView-side predicate;ObjectDataPagegets the same INTENT note as its twins (comment only, no behaviour).Commit 2 — the object page forwards a gallery cover binding, never invents one
options.gallery.imageFieldwas floored at'image'for every object view, declared or not.⭐ What that literal actually broke is the GATE, not the screen.
ObjectGalleryresolves the cover per record and collapses the cover area when none resolves, so the invented name degraded to a gallery of coverless cards — which is why it survived three cards of this family. ButListView.availableViewsasksschema.options?.gallery?.imageField, and a key this relay always supplied made that check answer YES for every object view whitelistinggallery. Whitelist ∩ resolvable — the mechanism that exists to stop a visualization being offered with nothing behind it — was answering about a name this file wrote rather than one the author did.galleryViewOptionsnow mirrorsganttViewOptionsnext door: the view's own block spread whole, the two legacy cover spellings cross-filling each other only when one was declared, andtitleFieldkeeping its'name'display floor (a display default is not a binding — the same rung objectui#3129 and #6557 established and #7070 kept).gallerywithout agallery:block loses the Gallery toggle. That is ADR-0047 working; it was only ever offered because this relay answered the gate on the author's behalf.ObjectGallerykeeps its own cover guess and that is correct there: it is the component's decision about an unconfigured record, and it is honest because the cover area collapses when the guess yields nothing.Fixture triage — two positive controls RE-ANCHORED, not deleted
The source scans in
ObjectView.calendarBinding-7029.test.tsxandObjectView.ganttBinding-7070.test.tsxeach carried a same-class positive control anchored on this PR's'image'literal, with an explicit instruction to whoever retired it: re-anchor, do not delete, do not weaken to the machinery control. Both are re-anchored onto the chart branch's|| 'value'measure floor — still present, same one-rung class, and deliberately left standing here. Both instructions are carried forward verbatim to whoever retires'value'.What is deliberately NOT here, and why
'name'/'value'at three faces. Filed asObjectCharthas no refusal path for an unresolvable legacy binding — the prerequisite for retiring the forced-path'name'/'value'floors at three faces (objectui#7547) #8168.ObjectCharthas NO refusal screen:runAggregatepassesgroupBystraight through with no guard, and thefindleg buckets onrecord[groupBy] ?? 'Unknown'. Deleting the floors today produces a backend-dependent outcome, not an honest refusal — so it fails objectui#7070's stated precondition. The refusal screen landed as its own card for timeline (objectui#7459) before fix(plugin-list,plugin-view): retire the 'created_at' timeline date-axis floors so the refusal becomes reachable #7500 deleted the floors; this follows that shape.'location'at two faces. Filed as map'location': two faces hold OPPOSITE documented postures on the same guess, and the relay floor silently shadows the component's own lat/lng defaults (objectui#7547) #8169, and it runs the OTHER WAY.ObjectMap.getMapConfighas a documented default branch guessinglatitude/longitude/location, kept deliberately by objectui#5953 while that card removed the title guess from the same branch. The relay floor makesgetMapConfigtake its FLAT branch, which suppresses the lat/lng half — so deleting the relay floor would make the tree invent three names where it invents one today. Two faces holding opposite documented postures on one guess is a ruling, not a patch.'progress'/'dependencies'— objectui#7499's subject, inpm:awaiting-maintainer. Untouched.packages/i18n/**was held by two in-flight PRs. No locale pack is touched by this PR.Census re-measured on this branch
Re-measured independently rather than trusted from the card or triage; every line number in both had moved. Three corrections to the card's accounting:
'name'and'value'— neither the card nor triage recorded the second;showViewSwitcherlength count has three faces, not two (table above);defaultGalleryFromObject, which returns a cover only when the OBJECT declares an image-typed field andundefinedotherwise — a derivation from the object definition, never a fabricated name, the same shape asdetectStatusFieldunder ADR-0085. OnlyObjectView's|| 'image'was inventing.titleField … || 'name'is deliberately NOT in this class anywhere: it is the display-name floor objectui#3129 / #6557 established and #7070 explicitly kept ("a display-name default, not a date axis").Tests
Everything
turbo ls --affectednames against20316bac3, in one run: 845 test files, 10256 tests, 1 skipped, 0 failures. The affected set is 8 packages; the 5 that carry tests are@object-ui/app-shell(636),@object-ui/console(89),@object-ui/plugin-list(70),@object-ui/example-schema-catalog(29),@object-ui/plugin-map(20) and@object-ui/example-console-starter(1) — 845, which is the run's file count exactly.@object-ui/example-byo-backend-consoleand@object-ui/sitecarry none. Nothing in the affected set went unrun.ObjectView.titleFieldConvergence.test.tsx(objectui#6557) pins all sixtitleField/labelFieldseams to ONE expression shape, usingviewDefas the mechanical proxy for "view-declared".galleryViewOptionsfirst read the rung off a local alias — same value, but it blinds that scan to exactly what it hunts. The pin was right and the code was wrong, so the code changed back to the family spelling; weakening the regex would have been the other way round.New pins:
ListView.switcherChrome-7547.test.tsx(8 cases) andObjectView.galleryBinding-7547.test.tsx(13 cases). Every absence arm is paired with a control that whitelists the SAME types or declares the SAME block, so a fix that simply emitted nothing cannot pass both halves.Reverse verification — direction predicted before running, then observed. Each mutation applied after committing; the anchor line printed before and after (not merely counted, since a no-op edit exits 0 and would read as a run); restore proved BY STATE,
git hash-objectequal togit rev-parse HEAD:PATH, plusgit diff HEADat 0 lines.{viewSwitcherOffered && (to{showViewSwitcher && (— anchor 1 before, 1 injected, 0 left. 4 failed / 4 passed: the reds are exactly the four arms asserting no chrome, the greens exactly the four controls.|| 'image'tail on thedeclaredImagerung — anchor 1 before, 1 injected. 6 failed / 7 passed: the reds are exactly the four "invents no cover field" arms plus the two source-scan cases, the greens exactly the declared-config controls, which are unobservable in either world. That asymmetry is the point.Type-check —
Donefor@object-ui/app-shell,@object-ui/plugin-list,@object-ui/plugin-map(rc 0), and for@object-ui/consoleonce its dependency closure is built. The script istsc --noEmit && tsc -p tsconfig.test.json:tsconfig.jsonEXCLUDES tests and a dedicated test project includes them, chained fromtype-checkand enforced byscripts/check-type-check-coverage.mjs. Proved by--listFilesrather than assumed: all three app-shell pins appear in the 4425-file test program and the gallery pin appears 0 times in the 3685-file source program; the switcher pin appears in plugin-list's 1495-file test program.Lint —
eslint --no-inline-config --format jsonover the 7 changed source files: array length 7, 0 errors, 400 warnings, all pre-existing classes. Measured per-rule against the same files at20316bac3via--stdin-filename:ListView.tsxandInterfaceListPage.tsxhave a zero delta;ObjectView.tsxis +2 (no-explicit-any143 to 144 andreact-refresh/only-export-components13 to 14), both from the newgalleryViewOptionsexport — exactly the two warnings each of its three sibling helpers already contributes. No new rule appears anywhere. The narrowing to changed files is a measurement, not a sample:eslint.config.jsextendstseslint.configs.recommended, NOT the type-checked preset, and declares noparserOptions.project/projectService, so no rule reads cross-file type information and this diff cannot move the verdict on a file it does not contain.Gates —
check-changeset-presence(8 source files of 2 released packages, 1 changeset),check-changeset-no-major,check:control-bytes,check:self-import,check:phantom-deps,check:vi-mock-specifiers,check:vi-mock-inherit,check:unreferenced-sources, andcheck-governed-queue-guard --testover all 9 changed paths (NOT GOVERNED) — all green. The repo-widepnpm lintis CI's.Clause-② — assessed NO, and both traps checked explicitly
No declared type or schema member moves and no
@objectstack/specchange; what changes is that a renderer stops inventing a binding name and a switcher predicate narrows. Nothing that was accepted is now rejected at a validation boundary — the same metadata still validates and still renders; it is simply no longer offered a toggle it had no binding for. Behaviour change on a published renderer, not an accept-set change.git grep galleryViewOptionsover the whole repo: the only non-test hits are its definition and its single call site, both in this file. No collision.export *widening the public face?packages/app-shell/src/index.tscontains zeroexport *statements — every export is an explicit named list, and none of the four*ViewOptionshelpers appears in it. The package's only entry is./dist/index.jsfrom that file, and the soleexport * fromanywhere in the package'ssrcis inside a test file.galleryViewOptionsis module-scoped for its co-located regression suite and does not reach the published face — the same standing asganttViewOptions,calendarViewOptionsandtimelineViewOptions.No⚠️ Say so if you read it differently and I will attach it.
needs:contract-reviewlabel, on that measurement.🤖 Generated with Claude Code
https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
Generated by Claude Code