Skip to content

fix(plugin-grid,plugin-gantt,plugin-charts,plugin-view,app-shell): grid, gantt and chart refetch in place on the invalidation bus; no view is remounted to show a write (objectui#10035) - #10494

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-10035-renderers-read-invalidation
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-10035-renderers-read-invalidation

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #10035
Clause-②: no — renderers refetch in place instead of remounting. No schema, export or accept set moves.

The remainder of objectui#10035, after objectui#10378 (Part of #10035, merged 8e49a9981). Governing text: AGENTS.md §5 commandment #8's corollary, "refresh data, don't rebuild UI — after a save/action, invalidate the affected data (notifyDataChanged from @object-ui/react) so consumers refetch in place; never bump a key= to remount a subtree". Dispatched by the domain:ui seat 2 PM, session session_014mXUNuFomfj24w7s1pZzhN (mode:subagent, so that id is also this run's).

What this changes

The three renderers that read no refresh input now read the data-invalidation bus, so neither ObjectView layer remounts anything to show a write.

  • @object-ui/plugin-grid ObjectGrid: the load effect names useDataInvalidation(objectName)'s nonce and re-reads into the same table (the RefreshIndicator path, not the skeleton). Not subscribed when a host hands the rows down (data, bind, inline value). Only the fetch effect region and the import line are touched; the page-size region objectui#10278 edits is not.
  • @object-ui/plugin-gantt ObjectGantt: a nonce move triggers reload({ silent: true }), the path the toolbar refresh and every write-readback already use, so GanttView stays mounted (scroll, collapsed groups, zoom). The main fetch effect's reload flips loading and swaps GanttView for the placeholder, which is a remount one level down, so the bus does not go through it. Each nonce is answered once; one that lands while the object-schema gate is closed is dropped, because the gated first load reads rows written before it.
  • @object-ui/plugin-charts ObjectChart: the fetch effect names the nonce, on both binding shapes. Object-bound (aggregate / find) keys on schema.objectName. Dataset-bound (queryDataset) keys on the dataset's base object as the query answer names it (object, the field the dashboard DatasetWidget reads for drill-through), because a dataset node carries no objectName.
  • @object-ui/plugin-view ObjectView: REMOUNT_TO_REFRESH_VIEW_TYPES and the grid's remountKey are gone; every view is keyed on its identity. The sites that move refreshKey also notify the bus: the host's own form save and delete (announceOwnWrite), and each onMutation write it hears (republished with the event's recordId).
  • @object-ui/app-shell ObjectView: REMOUNT_TO_REFRESH_VISUALIZATIONS, the whitelist scan and the chart branch's remountKey are gone; ListView and both chart branches are keyed on identity. The bump sites that change DATA without a data-source write now go through refreshData (counter plus notifyDataChanged): the action runtime's refresh (server, api and flow actions), useObjectActions' refresh (delete, the refresh action), realtime events, and import completion. View CRUD keeps bumping the counter alone: it changes view metadata, which reaches every renderer as props.
  • .changeset/10035-renderers-read-invalidation.md (patch on the five packages). The pending .changeset/10035-objectview-refresh-in-place.md from the first half is corrected in prose only (frontmatter untouched): its closing paragraph said gantt, chart and standalone-grid views "are still remounted after a write", which this change makes false before it could publish.

Measurement (dispatch zone 2)

  • H1 confirmed on BASE ab50ccc12: both remount sets and the grid remountKey present (REMOUNT_TO_REFRESH_VIEW_TYPES 4 occurrences, REMOUNT_TO_REFRESH_VISUALIZATIONS 3; useDataInvalidation 0 in all three renderer files).
  • H2(a): a data-source write reaches the bus only through useMutationInvalidationBridge, which the console mounts once in AppContent; nothing else bridges. So the bus hears a data-source write in the console, and hears nothing in an embed that does not mount the bridge.
  • H2(b), which bump sites stop reaching the renderer once the key stops carrying the counter:
    • plugin-view: all three (handleFormSuccess, the delete's onRefresh, the onMutation subscription) are data-source writes, so they reach the bus through the bridge in the console and never outside it. All three now notify it themselves.
    • app-shell: realtime (no local write at all), import (importRecords emits no onMutation, read in data-objectstack), the action runtime (raw HTTP), and the refresh action (no write) would stop. The delete through useObjectActions reaches the bus through the bridge anyway, and now also notifies. externalRefreshKey needs nothing: AppContent bumps it for a form save (a data-source write, which the bridge announces) and for undo / redo (which already call notifyDataChanged). View CRUD is metadata.
  • H3: the kept-remount pins flipped (below), both sets removed with their comments.
  • H4: both chart paths refetch, one pin each.
  • H5: one invalidation causes one refetch per renderer. Every in-place pin asserts the delta is exactly 1 (renderer pins: find / aggregate / queryDataset calls; host pins: the self-fetching view's own query).

Why the bus, not a host counter prop or a per-renderer onMutation subscription: a prop is one more relay per host (and per renderer), the silent-gap class objectui#7559's relay census exists for, and an onMutation subscription misses every change that is not a data-source write (realtime, import, server actions).

Pins: red on base, green on head

Reverse run, all five source files checked out at BASE ab50ccc12 with the head pins kept (on-disk blob == base blob for all five; restore proven by git diff HEAD empty, status clean, blob == HEAD): Test Files 5 failed (5), Tests 12 failed | 15 passed (27). Every failing in-place case failed on the half the defect predicts:

pin file cases red on base half
ObjectGrid.invalidationRefetch-10035.test.tsx (new) object-scoped, record-scoped (b) rows never re-read
ObjectGantt.invalidationRefetch-10035.test.tsx (new) in-place refetch (b) "expected '1' to be '2'"
ObjectChart.invalidationRefetch-10035.test.tsx (new) object-bound, dataset-bound (b) never re-ran its query
plugin-view ObjectView.refreshInPlace-10035.test.tsx (flipped) gantt, chart, grid, grid delete (a) remounted ("expected '4' to be '2'", "expected '2' to be '1'")
app-shell ObjectView.refreshInPlace-10035.test.tsx (flipped) whitelist offering gantt and chart, gantt console write, gantt realtime (a) list remounted; the console-write case trips (b) first: 2 gantt queries, one from the remount and one from the bus

The negative cases (another object, host rows, inline rows) and the six data-fed plugin-view cases stay green on base, as they should. The gantt file was reworked after that run; its reverse was re-run on the final head: red on (b), restore proven (blob == HEAD).

Ablations (each through ablation-replace.mjs: anchor hit exactly once, blob changed, restored to the HEAD blob)

id mutation result
A1 plugin-view onMutation stops notifying the bus gantt, chart, grid data-source-write cases red on (b); 7 green
A2 plugin-view announceOwnWrite stops notifying grid delete case red on (b); 9 green
A3 app-shell refreshData stops notifying gantt realtime case red on (b); 4 green (re-run on the final head, same result)
A4 gantt answers the nonce with a non-silent reload red on (a): GanttView unmounted through the placeholder (final head)
A5 gantt subscribes to nothing red on (b) (final head)
A6 chart fetch effect drops the nonce object-bound and dataset-bound red on (b)
A7 chart never records the dataset's base object dataset-bound red on (b), object-bound green
A8 grid load effect drops the nonce object- and record-scoped red on (b)

Each ablation's counterpart halves stayed green, so each pin fails for the reason it names.

Local verification (all at HEAD af9e92846 unless noted)

  • New and flipped pins together: Test Files 5 passed (5), Tests 27 passed (27).
  • Package suites: plugin-gantt 73 passed (73) / 561 passed (final head); plugin-view 48 / 443, plugin-charts 70 / 812, plugin-grid 154 / 1474 (at 904a6e061; those packages' files did not change after it).
  • app-shell, narrowed and declared: the 92 test files that mention ObjectView or AppContent (the only app-shell source file touched is views/ObjectView.tsx): 92 passed (92), 892 passed at 904a6e061; the app-shell pin re-ran green on the final head. The full app-shell package runs in CI.
  • Type-check (tsc --noEmit && tsc -p tsconfig.test.json), after turbo run build of the dependency closure: plugin-grid, plugin-charts, plugin-view Done at ea9d7bce9 (unchanged since); plugin-gantt and app-shell Done at the final head.
  • Lint, narrowed and measured: eslint --format json over the 10 touched ts/tsx files reads 10 files, 0 errors, 579 warnings. The rules come from eslint.config.js (--print-config resolves 118 rules for ObjectView.tsx). Untouched files cannot change verdict: no parserOptions.project / projectService, so linting is not type-aware, and no eslint-rules/ rule reads the disk. One finding on an added line: the exhaustive-deps note on the grid load effect's dependency line, which names perms / schema (not the nonce). BASE has the same note on the same effect (a lint probe of the BASE file reads it; ObjectGrid.tsx has 208 warnings on both sides).
  • Gates, exit 0 each, at the final head: check-changeset-presence (1 changeset, 5 released packages), check-changeset-no-major, check:new-line-citations (0 new), check:control-bytes, check-vi-mock-override-shape, check-test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:phantom-deps, check:unreferenced-sources; check-governed-queue-guard --test NOT GOVERNED. At 904a6e061 (no import or changeset changed after it): check-changeset-fixed, check:self-import, check:esm-specifiers (--specifiers-only), check:handler-key-reads, check:pending-changeset-literals. Report-only: check-changeset-overwrite reports the pending 10035 changeset correction (its case 2); check:changeset-claims lists 16 pending changesets naming touched files, and none makes a claim about refresh, remount, keys or invalidation (each body scanned).
  • origin/main at b274b6103 touches none of these files; git merge-tree against it is clean.

Acceptance notes

  • ListView draws its loading skeleton in place of the visualization while it refetches an empty list (loading && data.length === 0), so a gantt or chart there is unmounted and remounted by the list's own refetch when the list has no rows. With no rows the gantt draws nothing either, so no state is lost in practice. ListView.tsx is outside this claim (objectui#10327 is in flight on it).
  • A dataset-bound chart follows its base object only. A write to an object the dataset joins does not refresh it, the same object granularity every bus reader has. If the query answer names no object, the chart has no object to follow and refreshes on its own inputs only. The in-tree adapter passes the field through from the server envelope.
  • With the console bridge mounted, a plugin-view onMutation write reaches the bus twice in one synchronous dispatch (the bridge, then the host), and React batches them into one render. A form save may then be announced a second time by announceOwnWrite after the save resolves. That is at most the two refetches the two key bumps it replaces cost, since each remount fetched.
  • No docs page or README describes these components' refresh behaviour, so none became false. commandment Add automated testing infrastructure and CI/CD workflows #2's docs half has nothing to correct here.
  • Embeds that mount the renderers without the console bridge get the bus only from the hosts' own declarations. object-view covers its own writes and republishes onMutation.

Deviations

  • File surface: the pending .changeset/10035-objectview-refresh-in-place.md from the first half is corrected (prose only), as above. It is not in the claim's surface. Leaving it would have published a false paragraph.
  • Hosts, not only renderers: app-shell's refreshData wraps the bump sites named in H2(b). The ablations show which pins depend on each.

Generated by Claude Code

…n for its object (objectui#10035)

ObjectGrid read no refresh input: its load effect moved only on its own
internal counter, so a host could show it a write only by remounting it.
The load effect now names useDataInvalidation(objectName)'s nonce, so a
write reported on the bus re-reads the rows into the same table. Rows a
host handed down are not subscribed.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
…ion for its object (objectui#10035)

The registered object-gantt renderer hands ObjectGantt only schema and
dataSource, and its own query named no refresh input, so a host could show
it a write only by remounting it. It now reads
useDataInvalidation(resource) and answers a nonce move with the SILENT
reload the toolbar refresh and every write-readback already use, so
GanttView stays mounted and keeps its scroll, collapse and zoom. Host rows
and inline value sets are not subscribed.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
…tion, on both fetch paths (objectui#10035)

ObjectChart's fetch effect named no refresh input, so a host could show it
a write only by remounting it. The effect now names
useDataInvalidation's nonce: the object-bound aggregate keys on
schema.objectName, the dataset-bound queryDataset on the base object the
query's answer names (the node carries no objectName). Bound and inline
rows are not subscribed.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
… writes on the bus (objectui#10035)

With ObjectGrid, ObjectGantt and ObjectChart now refetching in place on
the data-invalidation bus, both ObjectView layers drop their remount sets
(REMOUNT_TO_REFRESH_VIEW_TYPES, REMOUNT_TO_REFRESH_VISUALIZATIONS) and the
grid / chart remountKey: every view is keyed on its identity alone.

The bump sites those renderers no longer hear through a key now declare
the change on the bus: plugin-view's own saves and deletes
(announceOwnWrite) and each onMutation write it hears; app-shell's server
actions, flows, imports, realtime events and refresh action (refreshData).
View CRUD keeps bumping the counter alone - it is metadata, delivered as
props.

The kept-remount pins from the first half flip to in-place pins. The
pending #10035 changeset's closing paragraph, which this change makes
false, is corrected.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
…the bus (objectui#10035)

app-shell does not depend on plugin-gantt, so the side-effect import of
the real renderer did not type-check. The pin registers an object-gantt
stand-in that reads the real useDataInvalidation hook, as the
stableFilterIdentity-7237 pin does, and additionally asserts the
visualization itself keeps its instance.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
…ng render (objectui#10035)

The gate on the object-schema read was carried into the invalidation
effect through a ref written during render (react-hooks/refs). The effect
now names the gate's inputs and records the last nonce it answered: a
nonce that lands while the gate is closed is marked handled and dropped,
so the gate opening later cannot add a query beside the gated first load.

The app-shell pin's harness hands out its controls from an effect instead
of assigning them during render (react-hooks/globals).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 16 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/4730-retire-dead-locale-key-batch.md

  • names ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    • Superseded twin vocabularies. cellRender.* and rowAction.* duplicated a grid.* vocabulary that won. RowActionMenu.tsx is fully i18n-wired and reads grid.openMenu / grid.edit / grid.delete; ObjectGrid.tsx reads grid.empty / grid.yes / grid.no / grid.systemFields. The twins had no reader on either side. - Labels that outlived their control. calendar.agenda labelled a view mode objectui#5740 retired from CalendarViewMode (now 'month' | 'week' | 'day'). home.quickActions.createApp*, layout.systemNav.createApp, actionDialog.defaultActionTitle / .ok and grid.bulk.selectPlaceholder sit in namespaces whose consumers are live and wired but demonstrably read other siblings. - Surfaces that left the product. map.* is the strongest form: @object-ui/plugin-map declares no @object-ui/i18n dependency and contains no t() call at all, so it cannot consume a locale string. home.stats.* and recordDetail.viewersTooltip name surfaces nothing renders.

.changeset/6475-gantt-block-face-declared.md

  • names plugin-gantt/src/ObjectGantt.tsx → packages/plugin-gantt/src/ObjectGantt.tsx — edited by this change

    The gantt nested-block spelling of a gantt config ({ type: 'object-gantt', gantt: { … } }) had no mirror entry at all: it rode through BaseSchema's .passthrough() entirely unvalidated, and the published TypeScript never taught the shape either — an author writing gantt: { … } got no completion, no type checking, no error on a misspelt member. It was the 28th and last of the keys getGanttConfig (plugin-gantt/src/ObjectGantt.tsx) reads off the schema that objectui#6051 (PR feat(types,plugin-gantt): declare the flattened GanttConfig face ObjectGantt reads (#6051) #6472) did not declare — severed into its own card because, unlike the other 27, declaring it changes what a published tool refuses.

.changeset/6597-retire-fieldmeta-referenceto.md

  • names plugin-grid/src/ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    No authoring story survived the search either. ObjectGrid's own relational-meta pass-through (applyRelationalMeta, plugin-grid/src/ObjectGrid.tsx) copies reference_to / reference / display_field / etc. from the SCHEMA field def only, at all three of its call sites — never from an authored column override. No doc, example, or fixture in this repo shows a table column pinning a lookup's target away from what its schema field already says. Under the maintainer's standing startup-stage rule (2026-08-27: deprecated/alias spellings retire immediately, no transition windows), no measured demand selects withdraw.

.changeset/6726-find-envelope-records-arms.md

  • names plugin-view/src/ObjectView.tsx → packages/plugin-view/src/ObjectView.tsx — edited by this change

    | module | what it does | | --- | --- | | components/src/hooks/related-count-store.ts | related-list tab badge count | | components/src/renderers/basic/data-list.tsx | element:repeater rows | | components/src/renderers/basic/elements.tsx | element:number client-side aggregate | | components/src/renderers/basic/record-picker.tsx | element:record_picker options | | plugin-detail/src/renderers/record-activity.tsx | record:activity self-fetch | | plugin-detail/src/renderers/record-history.tsx | record:history self-fetch | | plugin-view/src/ObjectView.tsx | non-grid (kanban / calendar / gallery / timeline) fetch |

.changeset/6939-objectql-record-source-refinement.md

  • names plugin-gantt/src/ObjectGantt.tsx → packages/plugin-gantt/src/ObjectGantt.tsx — edited by this change

    Both renderers resolve their records from one of three keys, in this order — getDataConfig in plugin-map/src/ObjectMap.tsx and plugin-gantt/src/ObjectGantt.tsx: data, then staticData, then objectName. Both mirrors required objectName alone, so a document authored on staticData drew correctly and was refused by safeValidateSchema — six catalog entries, three per component.

.changeset/7070-no-invented-gantt-date-fields.md

  • names app-shell/src/views/ObjectView.tsx → packages/app-shell/src/views/ObjectView.tsx — edited by this change

    • app-shell/src/views/ObjectView.tsx — the console object page. The inline branch becomes ganttViewOptions, the sibling of calendarViewOptions and timelineViewOptions: the declared block spread whole, title floored at 'name', no date field invented. - plugin-list/src/ListView.tsx — the render branch AND the capability gate. - plugin-view/src/ObjectView.tsx — generateViewSchema, the authored object-view element route, which bypasses ListView entirely.
  • names plugin-view/src/ObjectView.tsx → packages/plugin-view/src/ObjectView.tsx — edited by this change

    • app-shell/src/views/ObjectView.tsx — the console object page. The inline branch becomes ganttViewOptions, the sibling of calendarViewOptions and timelineViewOptions: the declared block spread whole, title floored at 'name', no date field invented. - plugin-list/src/ListView.tsx — the render branch AND the capability gate. - plugin-view/src/ObjectView.tsx — generateViewSchema, the authored object-view element route, which bypasses ListView entirely.

.changeset/7499-gantt-non-axis-floors-omitted.md

  • names plugin-view/src/ObjectView.tsx → packages/plugin-view/src/ObjectView.tsx — edited by this change

    • plugin-list/src/ListView.tsx — the object-gantt render branch. - plugin-view/src/ObjectView.tsx — generateViewSchema, the authored object-view element route, which bypasses ListView entirely.

.changeset/7762-object-grid-export-options-bare-array-refusal.md

  • names ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    What was measured, on this branch's base. The mirror declared NO exportOptions member at all, and BaseSchema is .passthrough(), so ObjectGridSchema.safeParse({ type: 'object-grid', objectName: 'accounts', exportOptions: ['csv', 'xlsx'] }) returned success: true with the array back VERBATIM — as did { formats: ['csv', 'pdf'], compression: 'gzip' }. Nothing on the render path parses, and ObjectGrid.tsx reads schema.exportOptions?.formats and only that, so the authored array then lost SILENTLY to the ['csv', 'json'] default: the useEffect that warns about dropped formats reads .formats too and returns early when it is absent, while !!schema.exportOptions kept the export button on screen. An author declared ['csv', 'xlsx'] and got csv/json with no error, no warning and no console line. The two authoring faces disagreed in the direction opposite to objectui#6956's: the TypeScript interface already declared the object form only, so TS refused what zod admitted.

.changeset/8650-chart-foreign-dialect-retired.md

  • names ObjectChart.tsx → packages/plugin-charts/src/ObjectChart.tsx — edited by this change

    Also deletes six (schema as any) casts that the published declarations had already made unnecessary — colors, categoryColors and categoryOrder on ChartRenderer, and colors, compareTo and series on ObjectChart (the objectui#8327 bucket-(b) class: declared, then read through a needless cast). No behaviour changes with them; ObjectChart.tsx now has no (schema as any) read left at all.

.changeset/8653-listview-title-retired-rowactiondefs-pinned.md

  • names packages/plugin-view/src/ObjectView.tsx → packages/plugin-view/src/ObjectView.tsx — edited by this change

    title — retired. ListView resolved its export filename through schema.label || (schema as any).title. @objectstack/spec/ui's ListViewSchema refuses title by name (unrecognized_keys: ['title']) while ObjectGridPropsSchema accepts it; packages/types mirrors the platform contract rather than ruling over it, so declaring title on ListViewSchema would have made this repo accept what the platform save gate rejects. That asymmetry is also why objectui#6639 could take the declare branch for ObjectGridSchema.title one package over and this site could not. A parse-based census of apps/ examples/ content/ and packages/ found zero list-view nodes authoring title, so the retirement costs no author a filename. Over that same corpus the instrument reports three object-grid nodes carrying the key: two authored ones, both in content/docs/api/schema-reference.md, plus one that is not authored at all — packages/plugin-view/src/ObjectView.tsx composes title: schema.table?.title onto a grid node it builds, so it is a producer writing the key rather than an author declaring it. ObjectGrid's own title reads are untouched — they remain declared, ruled and read.

.changeset/8885-object-chart-drilldown-title-compareto.md

  • names ObjectChart.tsx → packages/plugin-charts/src/ObjectChart.tsx — edited by this change

    ObjectChart.tsx reads all three off schema, and until now neither published copy declared any of them: not the TS interface (packages/types/src/objectql.ts) and not the zod mirror (packages/types/src/zod/objectql.zod.ts). They rode BaseSchema's index signature / .passthrough() and arrived unvalidated. drillDown was the sharpest case — this component's registry inputs advertise it to the designer palette, and @objectstack/spec publishes ChartDrillDownSchema for exactly this carrier, so an author was offered a key that neither published shape mentioned.

.changeset/9722-bulk-executor-datasource-face.md

  • names ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    • ObjectGrid.tsx → RecordDetailPanel: removing it type-checks GREEN. It was paying for nothing — both sides already declare the same DataSource | undefined from @object-ui/types. Removed outright. - ObjectGrid.tsx → BulkActionDialog: removing it reddens, and it was paying for TWO things at once — the optional-vs-required arm (the grid declares dataSource?: DataSource, the dialog demands one) AND the bulk-door contravariance. Only the first still needs erasing, so it is now a non-null assertion: today's runtime behaviour is preserved exactly, and any future drift of the face reddens at that site instead of passing silently.

.changeset/9853-grid-non-positive-page-size.md

  • names ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    Refuse a non-positive pageSize at all three of ObjectGrid.tsx's read points, instead of giving two different answers for one authored value (objectui#9853).

.changeset/humanize-label-single-home-5444.md

  • names ObjectChart.tsx → packages/plugin-charts/src/ObjectChart.tsx — edited by this change

    humanizeLabel turns a stored value into a display string when nothing else resolves it — an option with no declared label, an object name, a chart axis member. It existed twice, byte for byte: once in @object-ui/fields (read by plugin-grid, plugin-gantt, plugin-detail and by that package's own renderers) and once as a deliberate local copy in plugin-charts' ObjectChart.tsx, whose comment said it was there "to avoid a dependency on @object-ui/fields".

.changeset/object-view-unmirrored-keys-7779.md

  • names packages/plugin-view/src/ObjectView.tsx → packages/plugin-view/src/ObjectView.tsx — edited by this change

    What was measured. Every reading was taken on the object-view node renderer (packages/plugin-view/src/ObjectView.tsx, registered by plugin-view/src/index.tsx) with schema.objectName / schema.layout as the positive controls of the same schema.KEY query, so each zero is a reading; the repo-wide census of viewTabBar finds the key in no source file outside @object-ui/types (two doc tables listed it as authorable and are corrected here). The spec side was read through the installed pin (@objectstack/spec@17.2.0, ui entry, 117 exported object schemas walked; control keys objectName / columns / navigation / listViews hit): the three spec-modelled keys are optional slots on ListViewSchema and ObjectListViewSchema; the six local keys have no spec slot anywhere.

.changeset/objectchart-schema-anchor-7946.md

  • names ObjectChart.tsx → packages/plugin-charts/src/ObjectChart.tsx — edited by this change

    • ObjectChartProps.schema is ObjectChartSchema; the published .d.ts no longer says props: any. ObjectChartProps is exported. - The four keys are declared on BOTH copies, with value types taken from their READ sites (ChartRendererProps for xAxisKey / series, ObjectChart.tsx for filter) rather than copied from any producer's literal — except where @objectstack/spec already owns the shape, which is aggregate: that one is declared BY REFERENCE as ChartAggregate / ChartAggregateSchema, so the authoring door here and at the react-page publish gate are one shape and cannot drift into two dialects. - colors converges: the zod mirror has declared it since objectui#3913 and the TS interface did not, a drift no ratchet could see because a mirror-only key is in neither of the parity guard's two difference ledgers.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 80a0ecda9 (merge-base with origin/main): 10 file(s) changed outside .changeset/, read against 1410 pending declaration(s) that publish a body (1989 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3045.1 KB 3104.5 KB
Main entry chunk (gzip) 147.8 KB 350 KB
Entry file index-jRvYUrG0.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 544.59KB 130.15KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.86KB 62.25KB
fields (index.js) 256.48KB 64.96KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.22KB 14.56KB
plugin-charts (index.js) 74.95KB 20.88KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.59KB 35.39KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 231.81KB 61.27KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.50KB 37.51KB
plugin-gantt (index.js) 168.43KB 41.50KB
plugin-grid (index.js) 215.46KB 58.91KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.48KB 28.29KB
plugin-map (index.js) 21.83KB 7.07KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.64KB 8.94KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 88.00KB 21.97KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: af9e928466e250f58f8c7df23592bceb26fbcfc9

Branch check: git rev-parse refs/review/pr-10494 = af9e928466e2… (matches; not moved). Merge base with main ab50ccc12. 6 commits, 12 files (+952/-175). Diff and all source reads taken from refs/review/pr-10494 via git show; checkout untouched.

① Derived judgments

Mechanism — grid (plugin-grid/src/ObjectGrid.tsx). useDataInvalidation(hasInlineData ? undefined : objectName) at 1852; nonce appended to the load effect deps at 2366. In place: the table stays mounted, RefreshIndicator active={loading && data.length > 0} (5687); the skeleton branch is only loading && data.length === 0 (3605). hasInlineData is true for host data, bind and inline value alike (the dataConfig memo at 1455-1474 folds passedData and boundData into provider: 'value'), so the "not subscribed for host rows" claim holds. No onMutation subscription in the file (grep: 0), so a bus move costs one effect run. Own writes in the console: inline edit (4402/4423) and row-action success (3830/3846) bump the internal refreshKey after await, and the bridge already bumped the nonce in the adapter's dispatch tick, so the effect runs twice with the first aborted by cancelled — 2 requests, same count as base (internal counter + host remount fetch), no remount.

Mechanism — gantt (plugin-gantt/src/ObjectGantt.tsx 999-1006). Nonce keyed on resource only when recordQueryDerivesExpand (!hasHostData && !hasInlineData, 937). Effect answers each nonce once via handledInvalidationRef, drops a nonce while the object-schema gate is closed (same gate the main effect uses at 972), and calls reloadRef.current({ silent: true }). reload (790) sets refreshing not loading when silent and has reloadSeqRef sequencing, so no placeholder (1987) and a superseded answer cannot win; silent failure keeps last data (892). Mount: nonce 0 equals ref 0, no extra query. Own drag/update: write-readback silent reload (1871/2105) plus the bridge's nonce → 2 silent reloads, seq-guarded; base cost was readback plus a remount fetch. Equal count, no remount.

Mechanism — chart (plugin-charts/src/ObjectChart.tsx 956-963, 979). Keyed on schema.objectName (aggregate/find) or on datasetObject (dataset). datasetObject is set from the queryDataset answer's object (788); the in-tree adapter populates that field from the server envelope (data-objectstack/src/index.ts 6462/6477: object read from data.object and returned). Sound for the in-tree adapter. Before the first answer the hook is called with undefined and is unsubscribed; a first-query failure or an answer with no object leaves a dataset chart permanently unsubscribed, where base remounted it — the PR's Acceptance notes name the no-object case, not the failure case. Refetch keeps ChartRenderer mounted when rows exist (RefreshIndicator, 1516); with zero rows the pre-existing loading && finalData.length === 0 branch (1299) draws ChartSkeleton, so a zero-row chart is still rebuilt one level down — the new comment says "not the skeleton" without that caveat. No double fetch: fetchesForItself is the same predicate the effect used before.

Double-fetch summary. No path fetches three times. plugin-view's onMutation notify and the console bridge fire in one synchronous dispatch → batched → one nonce step. Paths at 2 (bus plus counter/readback) are listed above and match base counts, except app-shell delete through useObjectActions (recordDelete.run → onRefresh → refreshData, useObjectActions.ts 97): bridge nonce plus refreshData nonce after await → 2 gantt/chart requests where base did 1 remount fetch. One extra request, no remount; the gantt's seq guard discards the stale one.

Hosts — plugin-view (plugin-view/src/ObjectView.tsx). setRefreshKey sites at head: 872 (onMutation, now also notifyDataChanged with recordId), 1403 (announceOwnWrite, reached by handleFormSuccess 1411 and the delete runner's onRefresh 2621). The card's four sites are all covered. refreshKey still drives the non-grid fetch effect (1194) and reaches renderListView as refreshTrigger/refreshKey (2427/2433). Keys: 2445 and 2467 are identityKey. REMOUNT_TO_REFRESH_VIEW_TYPES: 0 hits in src.

Hosts — app-shell (app-shell/src/views/ObjectView.tsx). refreshData (1517) = counter plus notifyDataChanged({ objectName: objectDef.name }); wired at 1532 (useConsoleActionRuntime onRefresh → server/api/flow actions, useConsoleActionRuntime.tsx 184/669), 2163 (useObjectActions: delete and refresh), 2185 (realtime), 3258 (import). Remaining counter-only sites 1361, 1440, 2013, 2046, 2063, 2089, 2121, 3446 are view-config save/create/rename/delete/pin/default/reorder/panel change — metadata, as declared. 1610 mirrors externalRefreshKey; AppContent.tsx bumps it for form save (bridge announces, 449) and undo/redo (463-471, already notify). combinedRefreshKey still reaches ListView as refreshTrigger (2615). Keys 2525, 2556, 2948 are identityKey; REMOUNT_TO_REFRESH_VISUALIZATIONS and the whitelist scan are gone. No pre-existing notifyDataChanged on the action-runtime path (grep of non-test src: only AppContent, FlowRunner, RecordDetailView, RelatedRecordActionsBridge), so refreshData is not a second notify there. Realtime effect: adding refreshData to deps is inert relative to base because resolveAllConflicts already re-keys on user-${objectDef.name} (useConflictResolution.ts 187) and useRealtimeSubscription never resets lastMessage; the pre-existing stale re-fire on object switch now also notifies the bus (one spurious nonce for the new object).

In-list gantt. ListView hands data down but the registered object-gantt wrapper does not forward it (ListView 1960-1973 comment; first-half report), so the gantt self-fetches inside ListView and the bus path is what refreshes it. Holds when the list has rows; see ③ for the empty-list skeleton.

§5 #8 corollary. Nothing in either ObjectView.tsx is keyed on a counter. Repo-wide (git grep over packages/*/src, non-test, both key={…refreshKey…} and template fragments): the only remaining counter key is app-shell/src/views/PageView.tsx 117 and 120 (key={refreshKey} on InterfaceListPage and the page SchemaRenderer, bumped by onRefresh at 98) — outside this card's two named sites; same class, needs its own card.

Pins. Read, not run. Renderer pins: grid (table node same, "Written later" present, find delta 1; record-scoped, other-object, host-rows cases), gantt (same GanttView instance, placeholder never painted, task count 1→2, find delta 1; other-object and host-rows cases), chart (same ChartRenderer instance, rows 1→2, query delta 1, on both aggregate and dataset paths; other-object and inline cases). Flipped host pins: plugin-view gantt/chart/grid assert rows '2', selfQuery delta 1, same instance, plus a grid-delete case that resolves delete without onMutation; app-shell whitelist case asserts listQueries delta 1 and same list node, gantt console-write and realtime cases assert ganttQueries delta 1, same list node, same gantt instance. Every in-place case asserts (a) and (b). The app-shell gantt is a registered stand-in reading the real hook (declared in the file header; app-shell does not depend on plugin-gantt).

Deviation. Editing .changeset/10035-objectview-refresh-in-place.md (prose only, frontmatter unchanged, no package name lost) is the gate's own case 2 ("CORRECTING a declaration on purpose … prose that no longer matches the change. Legitimate"); its header measured 19/19 modifications legitimate and chose report-only. AGENTS.md has no rule against it (grep of the changeset rules: presence, no-major, fixed group only). CI Changeset Overwrite Report = success. The correction was right: the file still exists on current main (git cat-file -e refs/review/main:.changeset/10035-objectview-refresh-in-place.md), so both changesets ship in one release and the old "are still remounted after a write" paragraph would have contradicted the new note.

② Semver level

patch on plugin-grid, plugin-gantt, plugin-charts, plugin-view, app-shell — all five in .changeset/config.json fixed. AGENTS.md 版本号策略: no major (enforced by check-changeset-no-major, CI success); minor/patch evolve freely. No export, schema or accept-set change; a behaviour fix → patch is the right level. Both changesets read sentence by sentence against head: new changeset true throughout (silent gantt reload as the toolbar does, 2253; both chart shapes; host rows excluded by dataConfig/recordQueryDerivesExpand/fetchesForItself); "declares … the changes it learns of without a data-source write" under-lists the delete (also routed through refreshData) but is not false. Edited changeset true: the first sentence now holds at both ObjectView layers; "in this release" holds while both are pending.

③ Boundary flags

Implemented-by: claude/issue-10035-renderers-read-invalidation
Reviewed-by: session_014mXUNuFomfj24w7s1pZzhN

VERDICT: PASS


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants