Repository navigation
ListView: fold data={{ provider: 'object', object }} → objectName and read specType/type for the view kind — step 6 of #2890, released by objectstack#14791 ruling B #7477
Description
Activity
os-dev-report
{ "issue": 7477, "status": "done", "branch": "claude/issue-7477-listview-object-provider-fold", "pr": "https://github.com/objectstack-ai/objectui/pull/7628", "premise_still_valid": true, "summary": "Both inert halves of the react-blocks binding now fold in ONE place — normalizeListViewSchema (packages/core/src/utils/normalize-list-view.ts). data:{provider:'object',object} folds onto objectName (gap-fill only: an existing objectName wins, and `data` is deliberately NOT deleted, because it carries four providers, two read live in ListView, and the block is forwarded to child views whose getDataConfig reads it before objectName). The author's view kind is read from specType, then from a bare `type` when it names a kind ListView draws — the same two legs, in the same order, as normalizeChartSchema; an explicit viewType still wins, and an undrawable kind falls to the pre-existing grid default. ListView.tsx is untouched: no seventh per-block copy, no renderer-side ?? dual-read. The accepted kind set is a total Record over @object-ui/types' ViewType minus 'list'/'detail', so a kind added upstream fails the build here rather than silently staying unreadable. Clause-②: YES, judged from the diff and declared under a Housekeeping heading in the PR body with reasoning; I added needs:contract-review to PR #7628 myself and read the label back (live labels: package: core, plugin, tests, needs:contract-review — nothing stripped). The card carrier is the PM's. One non-blocking question for the contract reviewer is recorded in open_questions: it is a choice I made and defended, not a blocker.", "tests": "All runs on branch tip d865352c; git status clean and git diff HEAD empty at run time and after. Exit codes captured BEFORE any pipe (redirect-then-capture). (1) pnpm exec vitest run packages/core/ packages/plugin-list/ packages/plugin-view/ -> EXIT=0, 'Test Files 214 passed (214) / Tests 3405 passed (3405)'. (2) pnpm exec vitest run packages/app-shell/ (the third caller of the normalizer) -> EXIT=0, 'Test Files 614 passed (614) / Tests 5908 passed | 1 skipped (5909)'. (3) pnpm exec turbo run type-check --concurrency=2, repo-wide -> EXIT=0, 'Tasks: 81 successful, 81 total' (first attempt was RED on a real it.each typing error in my new core test; fixed, then green). (4) eslint over the three changed files -> EXIT=0, 0 errors and 6 no-explicit-any WARNINGS, the same class the sibling ListView.calendar-binding-7029.test.tsx carries (5). (5) gates: check-changeset-presence EXIT=0 ('3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'), check-changeset-fixed / -no-major / -overwrite EXIT=0, check-control-bytes EXIT=0 (6230 tracked text files), check-phantom-dependencies EXIT=0 (the new @object-ui/types import is a declared dep of @object-ui/core), check-package-self-import EXIT=0, check-element-data-source-declaration EXIT=0, check-side-effects-array EXIT=0. check-sdui-registration-pins is NOT MEASURED, not red: it refuses without a console bundle (PREREQUISITE: build @object-ui/console first); CI runs it with the bundle present. (6) Final pin run against the exact final commit d865352c: EXIT=0, 'Test Files 2 passed (2) / Tests 87 passed (87)'. ABLATION, three legs, each predicted before running: no rebuild is needed and none was done — vitest.config.mts aliases @object-ui/core to packages/core/src, so the run reads mutated SOURCE and no dist can serve a stale copy. Every leg mutated a COMMITTED tree, proved the mutation on disk (python anchor asserted to match exactly once; then grep counts: removed-text 0, injected marker 1 — A0 checked both anchors and wanted 2 markers), and proved the restore two ways: git diff HEAD empty AND live blob hash == HEAD blob hash (printed: 'RESTORE OK: blob 8e2b3e2178e967f62e222f84d30f5b2589e1d8d4 == HEAD blob 8e2b3e21...'), with a trap on EXIT/INT/TERM using absolute paths. A1 (drop the objectName assignment): predicted 3 red in plugin-list plus the core objectName cases; OBSERVED 7 red (4 plugin-list + 3 core) — one MORE than predicted and in an informative direction, because with no objectName the child view does not mount at all rather than mounting with an undefined binding. A2 (viewType='grid' instead of authoredViewKind ?? 'grid'): predicted 5 red in plugin-list plus the core specType reads; OBSERVED exactly those 5 plus 4 of the 10 core kind cases — the other 6 assert the grid default / the hasOwnProperty guard / that specType survives, which A2 restores, so they are green BY CONSTRUCTION and the PR body records that rather than counting them as coverage. A0 (both legs, i.e. origin/main behaviour): predicted every plugin-list case red; OBSERVED 7 of 7 red, criterion case failing at 'expected 0 to be greater than 0' — the kanban never mounts. MEASURED before/after for the card's executable criterion, driven through the REAL kind:'react' page tier: before, NEITHER the object-kanban spy nor the object-grid spy mounts (both waitFors time out) and dataSource.find is called 0 times; after, object-kanban mounts with objectName 'crm_task' and find's first argument is 'crm_task'. The card's 'no diagnostic' half is NOT separately measured and is marked unmeasured in the PR body.", "mcp_calls": "4 — issue_read(get), issue_read(get_comments), issue_write(create #7627), create_pull_request(#7628). Everything else went over REST, not MCP: a repo-scoped REST read probed 200, `gh` is absent, and the REST /search/issues path is refused for this session ('sessions are bound to their configured repositories'), so duplicate checking used two repo-scoped issue LIST pages plus a local grep with a lit control (5 open ListView-titled cards matched) rather than a wide MCP search; the needs:contract-review label was written with the ADDITIVE REST endpoint (POST .../issues/7628/labels, HTTP 200) and read back; and this report comment was POSTed over REST (HTTP 201) and read back. The count was corrected from 5 to 4 in one edit of this comment after the REST route for the comment succeeded.", "open_questions": [ { "question": "NON-BLOCKING, for the contract reviewer only — when a schema carries BOTH an objectui `viewType` and a spec-side kind (`specType` / a bare renderable `type`), which wins? I implemented viewType-wins and shipped it; this asks whether review wants it inverted.", "options": [ "A (shipped) — an explicit viewType wins; specType/type only fills the gap that used to resolve to 'grid'. Strictly additive: no input that renders today renders differently, which is what keeps this PR a widening.", "B — the spec spelling wins, per this normalizer's stated 'the canonical key wins when both are present' rule. Consistent with the other folds, but it CHANGES what already-working input renders, and it is reachable in practice only from a react page whose author wrote both spellings." ], "recommendation": "A, because the file's canonical-wins rule governs pairs where the legacy key is DELETED, and viewType is neither deleted nor legacy here — it is the key this fold writes. B is a strictly larger contract change than the card asked for, and 'never trim, never inflate a ruled change' cuts both ways. If review prefers B it is a two-line change plus its pins, and I flagged it in the PR body rather than leaving it implicit." } ], "out_of_scope_findings": [ "filed as #7627 (labels: finding, domain:ui; no assignee): the six per-block `data.provider === 'object'` folds could collapse onto one shared reader in @object-ui/core. The card's own ask offered that shape as an alternative and the dispatch fenced it out (plugin-dashboard held by #7509, components/src/ui held by #7397), so it is carried forward rather than lost. Includes the one concrete defect found while counting the seven read-sites: ObjectGantt.tsx:661 and :1374 use OPPOSITE precedence for the same question, so two reads in one file disagree about which side wins when a view carries both bindings. Searched for duplicates first over two repo-scoped REST pages with a lit control; none found." ] }
Generated by Claude Code
Contract review — PR #7628 at head
d865352c: PASS atCONTRACT_REVIEW_TIER; the open question is ruled A (an explicitviewTypewins)Director seat (objectstack #12708, session_01LsEjuNMPitCHwEfYftZ1um), 2026-09-04 ~09:1xZ, on the
domain:uiseat's parking (5538044487: parked for the director seat at tier).- Reviewed-by: session_01LsEjuNMPitCHwEfYftZ1um
- Implemented-by: branch
claude/issue-7477-listview-object-provider-fold(mode:subagentdev under thedomain:uiseat, session_01KbJQ1y1J12nZxYzFWhP8Q3, atopusunder the quota exemption) - Tier fuse:
get_session09:05Z —session_context.modelandlast_served_modelbothclaude-fable-5-1.
Read: the card body, the seat's claim 5537465852, the dev report 5538022829, the PM review 5538044487, the PR body, the source diff of
packages/core/src/utils/normalize-list-view.tsand the changeset atd865352cagainst merge base0d8fd7ce(from the fetched head); objectuiorigin/main1cca678for the premises. Not read: the dispatch brief.① Derived judgments — the contract increment
- Accept behaviour widens, and only widens. Two authored spellings the published
react-blockscontract recommends —data: { provider: 'object', object }and an authortypeparked asspecType— were inert in the renderer and now bind the view. Verified onmain:ListView.tsxreadsprovider === 'value'(:941,:1639) and'api'(:953,:1588) and never'object';react-page.tsx:91-93parks a react-tiertypeunderspecType. The fold is a gap-fill in both legs:foldObjectNameis computed only whenobjectNameis absent or empty, andauthoredViewKindis consulted only inside the existingdefaultViewKindbranch (!viewType || viewType === 'list'). Nothing that renders today renders differently. Correct, and Clause-② yes was the right call from the diff. - No spelling removed, no key deleted.
objectName/viewTypestay accepted;datais deliberately not deleted (four providers, two read live, the block forwarded to child views whosegetDataConfigreadsdatafirst). Right — deleting it for one provider would change what a child resolves, i.e. a narrowing this card was not ruled to make. - The kind set is derived, not transcribed.
LIST_VIEW_KINDSis a totalRecordover@object-ui/types'ViewTypeminuslist/detail(union verified atpackages/types/src/views.ts:27, eleven members) — a kind added upstream fails the build here.hasOwnPropertyrather thanin. Right shape. - The executable criterion is measured through the real react page tier (neither child mounts before;
object-kanbanmounts withcrm_taskafter;dataSource.find0 → called), and the ablations are reported with their surprises (A1 one more red than predicted, A2's six green-by-construction cases named as such). Holds. - Not re-derived here: that the spec's
objectprovider is astrictObjectof exactly{ provider, object }— accepted from the PR body and the dev's reading of@objectstack/spec; the fold reads onlyproviderandobject, so a wider provider shape would not change what lands inobjectName.
② Semver —
@object-ui/coreminor, correctNew accepted behaviour on a published normalizer, nothing removed.
packages/plugin-listgains only a test file, so no second changeset is owed (check-changeset-presencegreen). Consistent with the PR body.③ Boundary flags — the dev's open question: A, as shipped
When a schema carries both an objectui
viewTypeand a spec-side kind, the explicitviewTypewins. Reasoning: the normalizer's "canonical key wins" rule governs pairs where the legacy key is deleted; hereviewTypeis neither legacy nor deleted — it is the key this fold writes. B would change what already-working input renders, a strictly larger change than objectstack#14791's ruling B authorised, and the retirement ofviewType/objectNameis objectstack's later move (#14343), at which point the question dissolves. The||/??nuance the seat noted elsewhere does not arise here (the guards test for a non-empty string).Landing — same stroke
Not governed (four paths under
packages/**and.changeset/).needs:contract-reviewremoved here and on PR #7628 (read-modify-write, read back), PR marked ready, squash auto-merge armed; this seat follows it to MERGED and this card closes onFixes. #7627 (the six-fold collapse + theObjectGantt.tsx:661/:1374precedence disagreement) stays afindingfor triage; objectstack#14343 waits on this landing and the pin bump, as the card's sequencing note says.
Generated by Claude Code
- added a commit that references this issue
on Sep 9, 2026
Filed by the director seat (objectstack#12708, summon #11, session
session_01ShyhexkB2d1AeRZ85tgAAe) as the objectui half of the maintainer's ruling B on objectstack#14791 (2026-09-03, verbatim 「同意」 to the batch recommendation; ruling comment on that card). This card releases step 6 of #2890 only; the hold on #2890's other steps is unchanged.What is true today (objectui
origin/main39af82f, measured by the objectstack triage seat on objectstack#14791)packages/plugin-list/src/ListView.tsxreadsschema.objectNameat 52 sites anddata.object/dataConfig.object/specType/provider === 'object'at 0;packages/core/src/utils/normalize-list-view.tsfolds ten legacy pairs and.objectoccurs 0 times. So a react page bound with the spelling the publishedreact-blockscontract recommends —data={{ provider: 'object', object }}+type— validates green on the objectstack side and renders an empty list here, with no diagnostic; withviewTypeabsent the kind is forced togrid. Six sibling blocks (ObjectCalendar, ObjectGantt, ObjectGrid, ObjectMap, ObjectTree, plugin-dashboard utils) each carry a localdata.objectfold; ListView never took it.The ask (one PR)
normalizeListViewSchema— or one shared helper the six sibling folds can collapse into — folddata.provider === 'object'+data.object→objectName, and read the author's view kind fromspecType(the slotpackages/components/src/renderers/layout/react-page.tsxparks the react-tiertypein) so thattype="kanban"renders a kanban, not a grid. ⛔ Not a seventh per-block copy; ⛔ no renderer-side??dual-read (AGENTS.md #0.1 — the fold lives in the one documented normalizer).packages/plugin-listexercisingprovider: 'object'on ListView (today: zero) and thespecTypekind read.Executable criterion
A react page binding ListView with
data={{ provider: 'object', object: 'x' }}andtype="kanban"and noobjectName/viewTyperenders objectxas a kanban.Sequencing
No
Blocked-by:. When this merges and objectstack's objectui pin moves, the objectstack spec lane retiresobjectName/viewTypeas authored spellings (no deprecation window — maintainer principle 2026-08-27) and #14343 renames the four sites and the skill.Refs: objectstack#14791 (the ruling) · objectstack#11284 (the convergence ruling this finishes) · #2890 step 6 · objectstack#14343.