Skip to content

fix(plugin-gantt): an EMPTY host data array is "no host rows yet", not the rows (objectui#7333) - #10812

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-7333-gantt-empty-host-data
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-7333-gantt-empty-host-data

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #7333

Clause-②: no (copied from the claim): the gantt stops treating an empty host array as authoritative. It keeps fetching from the view's own source, as it does with no host array at all; no declared key, export or accept set moves.

What changed

ObjectGantt read a host data prop at two sites, and both accepted []:

  • the short-circuit at the head of reload tested data && Array.isArray(data). [] is truthy, so the chart adopted it and returned before its own query;
  • the gating flag (hasHostData, a bare Array.isArray(data)) fed recordQueryDerivesExpand, so the same [] also switched off the object-schema gate and the data-invalidation subscription of the query the chart should still issue.

Both sites now read ONE predicate, hostRows, evaluated once per render: a host array counts only when it has rows. An empty one reads as "no host rows yet", and the chart reads from its own source exactly as it does with no host array at all. A non-empty host array is adopted as before.

reload reads the predicate through hostRowsRef instead of closing over rest. reload does not key on the host rows, so a closed-over value is the one from whichever render last rebuilt reload. When a host's rows arrive after an empty first render, recordQueryDerivesExpand flips and re-fires the fetch effect, and that run has to see the rows that flipped it. Ablation A3 below shows that the closure form gets this wrong. The eslint-disable reason on reload's dependency list said (rest as any).data intentionally untracked, which is no longer true. It now names what it actually suppresses, searchFields, measured by linting the file with the directive stripped: base reports rest and searchFields, head reports searchFields only.

Which gate, and why (objectui#7210 half 2, ruling a′, PR #7391)

The card left this open: gate on length, or on an explicit "the host owns the rows" signal. Its routing note said the length gate is enough if the wrapper does not forward host props. Ruling a′ lets a non-grid view fetch the whole filtered set up to a platform ceiling, and PR #7391 implemented it with the object-gantt renderer still forwarding nothing. Its "Not in this PR" section calls forwarding data "the explicitly fenced-off wrong fix". So no host owns a gantt's rows on the registry path, and none is planned. Adding a host-ownership signal would be a new prop contract with nothing pulling for it. The length gate is therefore the one that matches the ruling.

Premise and reach (measured on 6cf599985)

  • Both sites existed as the card and triage describe them. Base run of the new pins: Tests 3 failed | 2 passed (5). Only the two controls pass.
  • Reach: no caller hands ObjectGantt a data prop today. ObjectGanttRenderer (index.tsx) still destructures only schema and renders ObjectGantt with schema and dataSource, and ObjectGantt.hostDataProp-7210.test.tsx pins that. A tree-wide git grep for direct JSX uses of ObjectGantt outside tests turns up index.tsx and demo/main.tsx, both passing only schema and dataSource, plus the README and docs examples, which pass schema, dataSource and onTaskClick. ObjectGanttProps does not declare data, so a typed caller can only reach this through a cast or an untyped spread, which is how the existing tests do it. ⇒ This hardens a published component's input. It does not fix a live defect. The triage ruled it owed anyway.
  • ListView's rows array for an api-provider gantt view: useState starts it as [], and the fetch effect's ganttOwnsData branch returns without writing it. So from mount it is exactly the [] this card is about. ListView withholds it from the chart, which is unchanged here.

Surface note: two plugin-list comments corrected (outside the claimed file surface)

My change made two comments false, so I corrected them. Both are comment-only, with no code bytes changed:

  • packages/plugin-list/src/ListView.tsx, the ganttOwnsData block, said the chart short-circuits on [] and that forwarding would paint an EMPTY chart;
  • packages/plugin-list/src/__tests__/ListView.test.tsx, the comment on the api-provider gantt describe, said the same.

Both now say that an empty host array no longer blanks the chart and that the withholding still guards against a non-empty array being adopted in place of the endpoint's rows. ObjectGanttRenderer and the persistLayoutKey comment are untouched, as the claim requires.

Tests

New file packages/plugin-gantt/src/ObjectGantt.emptyHostData-7333.test.tsx, five cases:

  1. api provider + host data: []: the endpoint is read and its rows paint;
  2. CONTROL, api provider + host rows: those rows paint and the endpoint is never read;
  3. CONTROL, api provider + no host data: the endpoint is read;
  4. host rows that arrive after an empty first render are adopted;
  5. object provider + host data: []: ONE query, issued after the schema settles, already carrying $expand (this pins the gating-flag site).

Ablation, run once with a prediction written before each run. Mutations go through ablation-replace WRAP mode: the anchor hit once, the blob moved, the restore was proven against the HEAD blob, and git diff HEAD was empty after each leg. Tests import ./ObjectGantt relatively, so the resolution path is src and no build is involved.

leg mutation predicted observed
A1 reload's site back to "any array" 3 failed / 2 passed Tests 3 failed | 2 passed (5)
A2 gating flag back to a bare Array.isArray 2 failed / 3 passed (cases 4, 5) Tests 2 failed | 3 passed (5)
A3 reload reads the closed-over hostRows, not the ref 1 failed / 4 passed (case 4) Tests 1 failed | 4 passed (5)

Local gates, all at HEAD 404ea62

  • vitest run --maxWorkers=2 packages/plugin-gantt/ packages/plugin-list/src/__tests__/ListView.test.tsx: Test Files 83 passed (83), Tests 764 passed (764), lock VERDICT command-exit 0. That is 82 plugin-gantt test files plus ListView.test.tsx.
  • pnpm --filter @object-ui/plugin-gantt type-check (tsc --noEmit && tsc -p tsconfig.test.json): TYPECHECK_EXIT=0, after building the @object-ui/plugin-gantt^... closure (Tasks: 12 successful, 12 total). --listFilesOnly on the test program shows the new test file and 82 test files in it.
  • plugin-list type-check was not re-run: the plugin-list diff is comment-only.
  • check-changeset-presence: ✅ 4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s).
  • check-changeset-fixed and check-changeset-no-major: ✅.
  • check:changeset-claims (report-only): 9 pending changesets name ObjectGantt.tsx or ListView.tsx. I read each paragraph, and none describes the host-data short-circuit.
  • check:pending-changeset-literals, check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:test-path-roots, check:unreferenced-sources: all OK.
  • check:new-line-citations: VERDICT new-cross-file-line-citations: 0 new citation(s).
  • node scripts/check-governed-queue-guard.mjs --test on the 5 paths: ✅ NOT GOVERNED.
  • Lint, narrowed to the changed files and proven complete.
    • Population: pnpm lint is each package's eslint ., and the root lint:root ignores packages/*. So the changed files are linted by the plugin-gantt and plugin-list configs.
    • Count: eslint --format json reports 2 + 2 files, 0 errors, 0 ignored.
    • Invariance: eslint.config.js sets no parserOptions.project / projectService, so type-aware linting is off, and no local rule reads another file. So this diff cannot move a verdict on an untouched file.
    • ObjectGantt.tsx warnings went from 84 at base to 81 at head: no-explicit-any 75 to 71, and react-hooks/refs 2 to 3 (the render-time ref write, the same pattern as effectiveDataSourceRef and reloadRef).
  • Repo-wide lint, the full test shards and the other check:* families are left to CI.

Acceptance notes (not filed)

  • Measured behaviour change in an edge case with no caller. A direct caller that spreads data: [] into a gantt with no record source and no dataSource now sees the chart's own error, "DataSource required for object/api providers", where base drew an empty chart. I measured this with a throwaway probe against both blobs and deleted the probe afterwards. It follows directly from the ruled semantics: the chart reads its own source, and here it has none. Once the host's rows arrive, the host branch adopts them and clears the error, as the adapter branch does. That last sentence is read from the code; I did not measure it for this no-source case.
  • Unchanged and still untracked: a host swapping one NON-EMPTY array for another does not re-run the chart's load. The fetch effect keys on whether there are host rows, not on which rows they are. That was already the case before this change, and no caller exists. Carrier: none.
  • ListView.tsx's ganttOwnsData block still calls objectui#7210 half 2 "an open maintainer decision", and the surfaceDrawsFetchedRows block still says the gantt query "carries no $top at all". Both have been stale since PR feat(non-grid): a platform row ceiling with a loud footnote, and the settled-schema convergence #7391. I left them alone because they are outside this card. Carrier: none.

Session: https://claude.ai/code/session_01MCg3z5cGrV51xEddpGcxEY (PM seat domain:ui#4, dispatched os-dev run).


Generated by Claude Code

…not the rows

`ObjectGantt` read a host `data` prop at two sites and both accepted `[]`:
`reload`'s short-circuit (`data && Array.isArray(data)`; `[]` is truthy)
adopted it and returned before the chart's own query, and
`recordQueryDerivesExpand` (`Array.isArray(data)`) switched off the
object-schema gate and the invalidation subscription of that query. A gantt
whose `data` names its own source painted an EMPTY chart whenever a host
handed it an array it had not filled yet.

Both sites now read one predicate, `hostRows`: a host array is adopted only
when it has rows. `reload` reads it through `hostRowsRef`, so host rows that
arrive after an empty first render are the rows the re-fired fetch effect
adopts, not the `[]` its closure saw.

The two plugin-list comments that described the old short-circuit as the
reason `ListView` withholds its rows are corrected to the new behaviour
(comment-only).

Claude-Session: https://claude.ai/code/session_01MCg3z5cGrV51xEddpGcxEY
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 9 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/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/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 plugin-list/src/ListView.tsx → packages/plugin-list/src/ListView.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-list/src/ListView.tsx → packages/plugin-list/src/ListView.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/7773-kanban-adapter-groupfield-write.md

  • names ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    Who is NOT affected — the boundary is node-local. Every VIEW-LEVEL groupField read is untouched and still live: it is a legacy alias of the spec's groupByField on the kanban view config, mapped by normalize-list-view.ts, and both adapters still resolve lanes through it (ObjectView.tsx's kanbanCfg.groupField ||, ListView.tsx's groupByField || groupField). Authoring options.kanban.groupField on a list-view or object-view keeps working exactly as documented in packages/plugin-list/README.md. groupField is dead only on the generated object-kanban NODE.

.changeset/7780-object-kanban-record-source.md

.changeset/8990-object-kanban-groupby-optional.md

  • names packages/plugin-list/src/ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    • packages/plugin-list/src/ListView.tsx generates the node as groupBy: laneField. objectDef loads asynchronously, so laneField is undefined on every load until it lands, and stays undefined whenever the object offers no stageField hint and none of status / stage / state / phase. The renderer serves that node; both published faces refused it. - content/docs/utilities/data-objectstack.mdx documents an object-kanban node that is exactly { type, dataSource }, with no groupBy. ⚠️ This one is weaker and is cited for what it is: that fragment is still refused after this change, at RECORD_SOURCE_REQUIRED, because dataSource is not a rung of the record-source ladder. It shows a lane-less board is a documented authoring; it is not a document this change admits.

.changeset/list-user-actions-collision-5398.md

  • names ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    The harvest now reads the object block only. Both userActions read sites in ListView.tsx carry a comment naming the collision, and __tests__/ListView.userActionsCollision.test.tsx pins each clause of it: the two shapes, a producer that manufactures the view one, the harvest's blindness to it, and the projection that must keep the object's operand with a toolbar block — or an empty block — present on the view.

.changeset/listview-comment-pair-4559.md

  • names ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    Two comment corrections in ListView.tsx (objectui#4559, objectui#4966). No runtime behaviour changes and the emitted bundle is byte-identical; the published .d.ts does change, which is why this is a patch rather than an empty frontmatter.

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 6fa5f64a1 (merge-base with origin/main): 4 file(s) changed outside .changeset/, read against 1599 pending declaration(s) that publish a body (2188 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3078.3 KB 3104.5 KB
Main entry chunk (gzip) 148.3 KB 350 KB
Entry file index-CAJhiLKn.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.57KB 6.15KB
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.17KB 10.58KB
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.39KB 1.21KB
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) 556.38KB 133.09KB
core (index.js) 9.57KB 3.81KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 224.97KB 62.47KB
fields (index.js) 259.68KB 65.89KB
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 (translateFn.js) 0.20KB 0.18KB
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.67KB 14.70KB
plugin-charts (index.js) 83.58KB 22.75KB
plugin-chatbot (index.js) 198.40KB 47.22KB
plugin-dashboard (index.js) 134.11KB 35.62KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 233.23KB 61.74KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 158.35KB 40.47KB
plugin-gantt (index.js) 169.83KB 41.99KB
plugin-grid (index.js) 218.12KB 59.72KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 114.70KB 28.42KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.91KB 9.05KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 87.83KB 22.01KB
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.81KB 3.58KB
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) 119.16KB 39.05KB
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) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
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) 2.52KB 1.31KB
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: 117/117 CONTRACT_REVIEW_TIER
Head-sha: 404ea622397113a067c955aa4487fb1e4a433656

Read: card objectui#7333 (body and all five comments: routing 5548457269, stale-block pointer 5853812598, direction 5857177033, seat claim 5857391261, and the os-dev-report 5857798620 for its empty open_questions only); objectui#7210 (body, the split 5494598855, ruling a′ 5508048888, the 7391 dev report and the contract-review PASS 5527271911) and PR objectui#7391 (body incl. its "Not in this PR" section on objectui#7222, file list, merged at 7c3df8f0c); PR objectui#10812 body, file list (5 files) and diff.patch; head sources at refs/review/pr-10812 (merge-base with origin/main = 6cf599985): packages/plugin-gantt/src/ObjectGantt.tsx (props interface, destructuring, effectiveDataSourceRef, useSettledSchema, reload, the fetch effect, recordQueryDerivesExpand, the invalidation subscription), packages/plugin-gantt/src/index.tsx (renderer and registration inputs), packages/plugin-list/src/ListView.tsx (ganttOwnsData block, its fetch-effect early return, the withheld data spread on the child renderer), packages/plugin-list/src/__tests__/ListView.test.tsx (the edited describe comment), the new ObjectGantt.emptyHostData-7333.test.tsx, ObjectGantt.hostDataProp-7210.test.tsx, ObjectGantt.fetchGate-7225.test.tsx; base versions of ObjectGantt.tsx and ListView.tsx at the merge-base; objectui AGENTS.md §5 commandments (#6, #9, #10, #11) and §9 (changeset presence rule, version policy, test-running and flaky-test discipline); .changeset/config.json (one fixed group of 40, both packages in it); packages/plugin-gantt/package.json and packages/plugin-list/package.json (both 17.6.0, published, files unchanged); the header of scripts/check-changeset-presence.mjs; the new changeset and every pending changeset on the head that names these mechanisms (7210-gantt-paging-chrome, gantt-owns-data-comment-7222, 7334-gantt-navigation-forward, 8348-record-source-data-arm-per-spec-row, 7390-gallery-host-ownership-pin, 10036-gantt-reload-identity-dep, 7225-settled-schema-convergence, 10035-renderers-read-invalidation, 10623-readers-follow-invalidation-bus, 10578-gantt-error-clears, 10633-gantt-silent-overtake, 7237-gantt-refresh-in-place); a tree-wide git grep of ObjectGantt JSX uses outside tests on the head; the check-runs for the head SHA, polled twice (final: 43 runs, 40 success, 3 skipped, 0 failure, incl. all 8 test shards, Type Check, Lint, Changeset Declaration, Changeset Bump Policy, Changeset Claim Re-read, Line Citation Gate, Governed Surface Queue Guard). No gate or suite was re-run; no probe was needed.

① Derived judgments

J1. The hostRows predicate (empty array reads as null) at both sites — RIGHT. Head ObjectGantt.tsx computes hostRowsProp from rest.data and hostRows as the array only when Array.isArray holds and its length is greater than zero, else null; rest is read for data at exactly one place in the file (the git grep shows no surviving (rest as any).data). Site 1, the head of reload, now branches on handedRows = hostRowsRef.current; site 2 is recordQueryDerivesExpand = hostRows === null && !hasInlineData. For a non-empty array both sites behave exactly as base (adopt; gate and subscription off); for undefined or a non-array both behave exactly as base (own query; gate and subscription on); only [] moves, and it moves to the undefined behaviour at both sites together, which is what the routing 5548457269 required ("两处必须一起收紧") and what the direction 5857177033 restated. The base text showed the two divergent forms (data && Array.isArray(data) and bare Array.isArray(data)); both are gone. Gate choice: ruling a′ did not decide wrapper forwarding, but PR #7391's "Not in this PR" records that the renderer still forwards nothing and that forwarding data is "the explicitly fenced-off wrong fix"; head index.tsx still renders ObjectGantt with schema and dataSource only, and the 7210 pin holds it. That is the branch of the routing's fork in which the length gate suffices; an explicit host-ownership signal would be a new prop contract with no puller, so choosing the length gate is consistent with the ruling.

J2. reload reading hostRowsRef instead of the closed-over rest — RIGHT under React rules, with two caveats recorded. (a) Stale closure: base reload closed over rest while its dependency list (unchanged on head) omits it, so reload kept whichever rest.data the last rebuilding render had; on arrival of host rows the fetch effect fired (because hasHostData flipped) but the stale reload still saw the old value and queried the adapter instead of adopting. The ref removes the closure entirely, so every caller of reloadRef.current (mount effect, arrival, invalidation, toolbar refresh, write-readback) reads the current render's value. (b) Re-render on arrival: hostRows goes null to array, recordQueryDerivesExpand goes true to false, and that boolean is in the fetch effect's dependency list, so the effect fires, the gate check is bypassed (correct: the host branch derives nothing from the schema), reloadRef.current runs and adopts the rows; the new case 4 pins it. (c) Ref written during render: hostRowsRef.current = hostRows is a render-phase write, which the letter of React's rules discourages and which the compiler-era react-hooks/refs rule reports (the PR body records the file's warning count going 2 to 3). It is, however, the same pattern the file already carries twice by a ruled design (effectiveDataSourceRef and reloadRef, objectui#10036, commandment #10), it is a pure function of the current render's props so a StrictMode double render or a discarded concurrent render writes the same value the committing render writes, and reload is only ever invoked from effects and event handlers, which run after commit. Nothing in AGENTS.md forbids the pattern; Lint passed. Judged consistent, not a defect. (d) The rewritten eslint-disable reason — TRUE. The old reason ("(rest as any).data intentionally untracked") is false on head because reload no longer reads rest. The new reason names searchFields (read inside the callback via the $searchFields spread, keyed by searchFieldsKey, which is in the list) and says the host rows are read through the ref (a useRef result is exempt from the rule, so the clause is explanatory, and it is true). One precision for the record: for a useCallback the rule's logic also classes the declared-but-unread value-list entries (adapterInputsKey, dataSource, apiFetch, hasInlineData, dataProvider, searchFieldsKey) as unnecessary; that finding is hidden behind the missing-dependency message the rule prints first, it is pre-existing and unchanged by this PR, and the block immediately above the directive documents it as the objectui#10036 value list. The directive is on the line directly above the dependency array, so it governs the right line.

J3. recordQueryDerivesExpand keyed on hostRows === null — RIGHT. On base, [] made hasHostData true, so recordQueryDerivesExpand was false: the fetch effect skipped the object-schema gate (the chart issued its first find before the definition landed, with no $expand, and a second after: the ungated signature [null, ['owner']] that fetchGate-7225 records) and useDataInvalidation was handed undefined, so a write to the object never re-read the chart. On head, [] leaves recordQueryDerivesExpand true: one gated query already carrying $expand: ['owner'] (new case 5) and the invalidation subscription live for the query the chart really issues. For a non-empty host array the flag is false as before (host rows are the host's to refresh, as the pending 10035 and 10623 changesets say), and for no host prop it is true as before. The subscription and the gate now agree with the branch reload takes, because both read the one predicate of the same render.

J4. The measured edge (host [], no record source, no dataSource) — not a regression any real caller can reach. Head throws DataSource required for object/api providers where base painted an empty chart. Reach on head: ObjectGanttProps declares schema, dataSource, className, onTaskClick, onRowClick, onEdit, onDelete, onBeforeTaskUpdate and no data, so a typed caller cannot pass it (excess-property check) and only a cast or untyped spread reaches rest.data; the registered renderer forwards schema and dataSource only; ListView withholds data for the api-provider gantt and every other path goes through the registry; the tree-wide grep of ObjectGantt JSX outside tests finds index.tsx, demo/main.tsx, the README and the docs page, none passing data. The new answer is also the ruled semantics applied honestly: an empty host array is "no host rows yet", the chart reads its own source, and here it has none. Recorded in ③ as a consequence, not a defect.

J5. Clause-②: no holds. No declared key moves (registration inputs in index.tsx unchanged; ObjectGanttSchema untouched), no export moves (index.tsx export list unchanged), no accept set moves (ObjectGanttProps unchanged and still does not declare data). What changes is the handling of an undeclared prop on a path no published face admits.

J6. The changeset, sentence by sentence against the head — all TRUE. "no longer treats an EMPTY host data array as the rows it was handed": true (J1). "read a host data prop at two places, and both accepted []": true of base (reload short-circuit and hasHostData). "The reload's short-circuit tested data && Array.isArray(data), and an empty array is truthy, so the chart adopted it and returned before its own query": true of base. "The gating flag tested Array.isArray(data) alone, so the same array also switched off the object-schema gate and the data-invalidation subscription of that query": true of base (J3). "A gantt whose data names its own source ... painted an EMPTY chart whenever a host handed it an array it had not filled yet": true of base; the dev's base run of the pins (3 failed, controls green) is consistent with it. "Both places now read one predicate": true. "A host data array is adopted only when it has rows": true. "An empty one reads as 'no host rows yet': the chart queries its own source, exactly as it does with no host array at all, and adopts the host's rows when they arrive": true (both sites give the undefined behaviour for []; arrival pinned by case 4). "the registered object-gantt renderer still forwards no host prop, and ObjectGanttProps does not declare data, so only a caller that renders ObjectGantt directly and spreads data in was affected": true (J4). No line addresses, English, patch frontmatter.

J7. The edited comments, sentence by sentence — all TRUE on the head. ObjectGantt.tsx hostRows docblock: "Rows a HOST component handed down as a data prop, or null when it handed none" true; "An EMPTY array is null here" true; the description of the two base sites true; "an empty array leaves the chart reading from its own source, as it does with no host array at all; a NON-EMPTY one is adopted as it always was. Pinned in ObjectGantt.emptyHostData-7333.test.tsx" true (file exists, cases 1 to 4); "One predicate, evaluated once per render, for BOTH sites" true; the closure explanation true (J2a); "When a host's rows arrive after an empty first render, recordQueryDerivesExpand flips and fires the fetch effect, and that run must see the rows that flipped it" true (J2b); "the registered object-gantt renderer forwards no host prop (ObjectGantt.hostDataProp-7210.test.tsx pins that), and ObjectGanttProps does not declare data" true. The reload inline comment "Rows a host handed down — non-empty only ... see hostRows above for why this reads the ref" true. The recordQueryDerivesExpand docblock: "Rows a host handed down and an inline value set both paint with no metadata read at all" true (the inline provider passes undefined to useSettledSchema); "'Rows a host handed down' is hostRows, the predicate reload reads — an EMPTY host array is not one, so it keeps the gate and the invalidation subscription of the query the chart still issues" true (J3). ListView.tsx ganttOwnsData block, edited sentences: "This view's rows array is not the endpoint's answer (the fetch effect below returns early for it)" true (the effect returns after setLoading(false) when ganttOwnsData); "ObjectGantt adopts any NON-EMPTY host data array as its rows" true; "An EMPTY one no longer blanks the chart: it reads as 'no host rows yet' and the chart still queries its endpoint" true; "So the withholding guards against this array ever being adopted in the endpoint's place" true (the spread on the child renderer still hands {} when ganttOwnsData). ListView.test.tsx describe comment, edited sentences: same three claims, true. One pre-existing sentence in the unchanged context of BOTH edited paragraphs is false on the head and was not made false by this PR: "an open maintainer decision (objectui#7210, half 2)"; see ③.

J8. Pending changesets naming these files — none made false. 7210-gantt-paging-chrome ("forwards no prop but schema, so the data prop never reaches the chart") still true; gantt-owns-data-comment-7222 ("no host prop reaches ObjectGantt at all") still true; 7334 ("its wrapper drops host props entirely", "No host prop is forwarded to the chart") still true; 8348 ("its renderer forwards no host props"; the data prop carrier "untouched on every block" describes that changeset's own change) still true; 10035 and 10623 ("Rows a host hands these components ... are still the host's to refresh; only the components' own queries follow the bus") still true and now also true for the [] case, where the query IS the component's own; 7225 ("It now issues one query, already expanded") still true and extended by case 5; 10036, 10578, 10633, 7237, 7390 unaffected.

J9. Tests and discipline. The five cases pin the two sites and the arrival; cases 2 and 3 are real controls (endpoint never read for host rows; endpoint read with no prop). The negative assertion in case 5 waits a 100 ms settle() before asserting [['owner']], which is a bounded absence window rather than a raised timeout and is stricter than the 7225 pin's immediate assertion; not a flaky-test pattern under §9. Test names carry no line address and no parenthesised issue tag (the -t regex trap). vi.mock shapes match the sibling 7210 pin and the vi-mock gates passed. The file imports ./ObjectGantt relatively, so it exercises src. The (rest as any) casts at both sites are gone (commandment #6 direction; the PR records no-explicit-any 75 to 71).

② Semver level

patch on @object-ui/plugin-gantt is right: a behaviour change confined to an undeclared prop path, no declared key, export or accept-set move (J5), no new authorable surface. One changeset is enough under objectui §9's presence rule: the rule and the authoritative check-changeset-presence.mjs header are per CHANGE ("the same change must declare a .changeset/*.md; an EMPTY frontmatter counts"), i.e. declare once, not once per package; the Changeset Declaration check is green on the head. The plugin-list edits are comment-only (the diff changes no code bytes there: -6/+6 and -4/+4 inside comments), so there is no published behaviour to describe for that package, and both packages sit in the single fixed group of 40 that versions together anyway. No major is declared (Changeset Bump Policy green), consistent with §9's rule that objectui changesets never declare major.

③ Boundary flags

  • F1 — File-surface deviation, reported, judged acceptable. The claim 5857391261 confined the surface to ObjectGantt.tsx's two sites, tests beside it and one changeset. The PR also edits two comments in packages/plugin-list (ListView.tsx ganttOwnsData block; ListView.test.tsx describe comment). Both sentences it replaced ("[] satisfies both", "forwarding would paint an EMPTY chart") were made false by this very change, so leaving them would have shipped a false comment; the edits are comment-only and the dev reported the breach in the PR body and the report. Neither fenced item (persistLayoutKey comment, ObjectGanttRenderer) was touched; git merge-tree against the moved main is reported clean and the CI ran at the head.
  • F2 — Stale prose left standing in both edited paragraphs, not introduced here. "whether a non-grid view may fetch unbounded at all is an open maintainer decision (objectui#7210, half 2)" in ListView.tsx, and "(objectui#7210, half 2, an open maintainer decision)" in ListView.test.tsx, are false on the head: half 2 was ruled a′ on 2026-09-02 and landed via PR feat(non-grid): a platform row ceiling with a loud footnote, and the settled-schema convergence #7391 on 2026-09-03. The surfaceDrawsFetchedRows block in ListView.tsx likewise still says the gantt query "carries no $top at all". The dev recorded all three as out of scope with "Carrier: none". They sit in the paragraphs this PR edited, so a carrier is owed; not a defect of this PR.
  • F3 — Render-phase ref write. hostRowsRef.current = hostRows adds a third instance of the file's ruled latest-ref pattern (J2c) and one more react-hooks/refs warning. Consistent with objectui#10036; worth knowing if the repo ever adopts the React Compiler, which treats such writes as errors.
  • F4 — Semantic consequences with no caller, recorded. (i) A host that goes from a non-empty array to [] now flips recordQueryDerivesExpand and the chart re-queries its own source (base kept the stale non-empty rows on screen). (ii) A host swapping one non-empty array for another still does not re-run the load, because the effect keys on whether host rows exist, not which; pre-existing, unchanged, no caller. (iii) The J4 error for [] with no source and no adapter.
  • F5 — Evidence provenance. The ablation legs (A1 to A3) and the base run of the new pins are the dev's figures and were not re-run here, per the review's no-rerun rule; the CI record at the head (40 success, 3 skipped, 0 failure across two polls) is the independent evidence relied on, and the reasoning in J1 to J3 reproduces the dev's predicted directions from the source alone. The dev's open_questions is empty.

Implemented-by: claude/issue-7333-gantt-empty-host-data
Reviewed-by: session_01MCg3z5cGrV51xEddpGcxEY

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 17:02
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit 67d4ed9 Sep 27, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-7333-gantt-empty-host-data branch September 27, 2026 17:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants