Skip to content

feat(components): a kind:'react' page's author scope injects useDataInvalidation (objectui#10887) - #10973

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-10887-react-page-invalidation
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-10887-react-page-invalidation

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #10887
Clause-②: yes — one identifier, useDataInvalidation, joins the published author scope of kind: 'react' pages, and content/docs/guide/react-pages.md teaches it.

Why a closing keyword. The card has three members. Members 1 and 2 landed in PR #10898 (squash 9f0c84a44). This PR lands member 3, ruled A (ruling 5864668610), so all three are in once it merges. The four review notes from the members 1 and 2 round ride this round: (a) and (d) are text and are corrected here; (b) and (c) need a behaviour change, so they are reported as findings and not changed here (see Acceptance notes). The objectstack half of the ruling (the docs page, the skill and the showcase pages) is carried by objectstack-ai/objectstack#20395. The Blocked-by line on objectui#10519 is the seat's to update; this PR does not touch that card.

What changed

  • @object-ui/components, the react-page renderer. ReactKindPage injects useDataInvalidation from @object-ui/react into the author scope, beside useAdapter. It is the same hook ListView reads to refresh its rows: useDataInvalidation('object') returns a nonce that moves when the bus reports a write to that object, or '*'. A page names it in the dependency list of the effect that reads through useAdapter, and that read re-runs in place. The hook is a module-level function, and the scope memo still depends on the schema and the adapter only, so the scope keeps its identity and no page recompiles or loses state. The file header's scope list gains the entry.
  • The react-pages guide. The scope table gains a useDataInvalidation row. The Live data example reads const changed = useDataInvalidation('showcase_project') and names changed in the effect's dependency array, followed by one sentence on why: the nonce moves each time the bus reports a write to that object, so the effect reads again in place without remounting the page.
  • Changeset 10887-react-page-data-invalidation: minor for @object-ui/components, Clause-②: yes.
  • No new dependency edge. useDataInvalidation is exported by @object-ui/react, which @object-ui/components already depends on and already imports useAdapter from.

Review notes from the members 1 and 2 round (5864212493)

note what it said here
(a) the plugin-view changeset's "switches off that view's own bus reader" is imprecise for a gantt handed zero rows Corrected (text). The pending changeset 10887-object-view-non-grid-bus-reader now names the exception: a gantt handed zero rows still queries for itself and keeps its own bus reader (objectui#7333). Frontmatter byte-identical (the first three lines hash the same before and after); the file has not shipped (no CHANGELOG carries its text), so it is corrected in place. The same imprecision in ObjectView's member 1 comment is corrected too.
(b) fetchDrawsView is a deny-list, so a host views entry the renderer does not draw adds one unused read Not changed: behaviour. An allow-list changes which views subscribe. Reported as a finding.
(c) a calendar re-read flashes its loading placeholder Not changed: behaviour. ObjectView's non-grid fetch sets loading on every run and passes it to the view, and ObjectCalendar shows it; a silent re-read changes behaviour. Reported as a finding.
(d) the identityKey comment is incomplete Corrected (comment). It now says the non-grid fetch re-reads when refreshKey moves or when the bus reports a change to the object (objectui#10887), that a gantt handed rows draws them while a gantt handed zero rows queries for itself (objectui#7333), and that tree reads the bus itself (objectui#10778).

Both comment edits sit inside ObjectView's component body, so neither reaches a published .d.ts; no plugin-view surface moves and the plugin-view changeset keeps its patch.

Pin, through a real react page

packages/components/src/__tests__/react-page-invalidation.test.tsx. The page is compiled from source by @object-ui/react-runtime inside the real ReactKindPage, dispatched by the real PageRenderer for type: 'home'; the events go through the real bus (notifyDataChanged). Only the adapter is a stand-in, so reads can be counted. The page source is the guide's Live data pattern with the nonce.

  • One record-scoped bus event on showcase_project re-runs the page's find exactly once (1 to 2), the re-read rows reach the page, and a counter the user clicked before the event still reads 1, so the page was not remounted.
  • Lit control: an event on showcase_invoice does not re-run the read (find stays at 1, the rows stay the first answer), while a bare useDataInvalidation('showcase_invoice') reader mounted beside the page moves from 0 to 1, so the event demonstrably reached the bus.
run result
base react-page.tsx (blob f70b8f425, restored from HEAD afterwards: blob equal, git diff HEAD empty) Tests 2 failed (2): both tests, each at the page-error assertion, ReferenceError: useDataInvalidation is not defined in the page's error panel
head Tests 2 passed (2) on the implementation's first commit f64ba6e66, and again inside the package run below at af76d224c
ablation: the injected hook replaced by a stub that never moves (ablation-replace, anchor useDataInvalidation, hit once, blob ab712dfac to 9625f569b, restored to the HEAD blob, git diff HEAD empty) Tests 1 failed | 1 passed (2): only the re-read test, expected 'read 1' to be 'read 2'; the lit control stays green

The pin imports its subject by relative path, and @object-ui/react and @object-ui/react-runtime through the root vitest alias to their sources, so no dist/ sits between a mutation and the run.

Tests and gates (head af76d224c)

  • pnpm exec vitest run packages/components/ packages/plugin-view/ --maxWorkers=2 under the verify lock at af76d224c (the run printed that sha): Test Files 379 passed | 1 skipped (380), Tests 3689 passed | 24 skipped (3713).
  • pnpm exec vitest run scripts/ --maxWorkers=2 under the verify lock at af76d224c: the whole scripts/ test project, so every scripts/__tests__ gate whose corpus reads a touched file (the changesets, the guide, the package sources) is in it: Test Files 177 passed | 2 skipped (179), Tests 5332 passed | 2 skipped (5334).
  • type-check for @object-ui/components and @object-ui/plugin-view (echoed tsc --noEmit && tsc -p tsconfig.test.json): Done, Done, at 2ca2478ee, after turbo run build over both packages' dependency closures (15 tasks). The one later commit reflows comment and changeset prose only. tsc -p tsconfig.test.json --listFiles in packages/components names the new pin once, so the test program compiles it.
  • At af76d224c, each exit 0: check:control-bytes, check:new-line-citations (0 new citation(s)), changeset:check, check-changeset-presence (3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)), check-changeset-overwrite (reports the plugin-view changeset edit as a correction, its case 2), check:changeset-claims, check:pending-changeset-literals, check:test-path-roots, check:phantom-deps, check:unused-deps, check:shell-escape-residue, check:doc-fences, check:doc-types, check:doc-example-ids, check:doc-example-readers, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:comment-mask-corpus.
  • ESLint over the three changed source files, --no-inline-config --format json: 3 files, 0 errors. The new pin carries 0 warnings. react-page.tsx (6) and ObjectView.tsx (60) carry the same warnings per rule as their base blobs, read through --stdin-filename. This is a narrowed run, not the repo lint: the root ESLint config enables no type-aware linting (no parserOptions.project, no projectService), so this diff cannot move a verdict on an untouched file. pnpm lint is CI's.
  • check:doc-snippets and check:doc-examples: NOT MEASURED locally, prerequisite not met (they need the whole covered package set built). The guide's changed block is fenced jsx, which check:doc-snippets does not compile (its compiled fence set is ts, tsx, typescript). CI runs both.
  • check-governed-queue-guard --test over the 6 changed paths: NOT GOVERNED.

One case the widened scope can break

The scope's names become the parameters of the function the page source is evaluated in. A page that declares its own top-level const or let named useDataInvalidation therefore now fails with a SyntaxError (Identifier 'useDataInvalidation' has already been declared), shown in the page's error panel; a function or var declaration of that name only shadows the injected one. Measured once through the real renderer with a throwaway test (panel shown, message as quoted), not kept. Census: no page source in objectui (apps, examples, content, skills) or in objectstack origin/main (examples, content, skills, packages) declares the name. The changeset states this case.

Serial

Open PRs, read again just before this PR (12 open, every file list complete under 100 entries): none touches react-page.tsx, the react-page-* tests, react-pages.md or the 10887-* changesets. One touches ObjectView.tsx: PR #10967 (opened after this claim, route 2's named-view reads). git merge-tree --write-tree of this head against its head 600cf8901: clean, exit 0. Its hunks and this PR's two comment hunks do not overlap. PR #10777 touches PageView.tsx, its tests and its own changesets only.

Acceptance notes


Generated by Claude Code

…nvalidation

A react page's own useAdapter reads now have a bus reader to name: the
scope injects useDataInvalidation from @object-ui/react beside useAdapter,
and the react-pages guide's scope table and Live data example teach the
nonce in the effect's dependency list. Pinned through a real react page:
one bus event on the page's object re-runs its read once, in place, and
an event on another object does not.

The members 1-2 review notes that are text ride along: the plugin-view
changeset and ObjectView's comments now name the zero-row gantt, which
keeps its own reader, and the identityKey comment names the bus re-read.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
@github-actions github-actions Bot added documentation Improvements or additions to documentation package: components plugin tests labels Sep 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 11 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/10735-html-tier-manifest.md

  • names react-page.tsx → packages/components/src/renderers/layout/react-page.tsx — edited by this change

    What moves for a consumer. ComponentRegistry.getPublicConfigs() — the read both manifest producers serialise — now returns the curated tier AND the html tier's intrinsic elements, the latter from the new HTML_TIER_INTRINSICS roster (@object-ui/core) and each stamped tier: 'html' in the projection. manifestFromConfigs carries that one value into a new optional ManifestComponent.tier (@object-ui/sdui-parser), so the regenerated sdui.manifest.json grows by the roster (47 entries, each with its registered inputs and its child slot declared exactly where the registration renders a child list — the void tags img / hr / br declare none) and sdui-intrinsics.d.ts types them. A reader that whitelists tags by key — the objectstack gate — needs no change and accepts the tags. A reader that means the CURATED vocabulary filters on the stamp: generateBlockList now sections the html tier under its own count, and the kind:'react' JSX scope skips stamped entries (@object-ui/components, react-page.tsx), so no P / A / Img wrapper is injected into react pages — on that tier a lowercase ANGLE-BRACKETS(p) is React's own element.

.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/6740-flex-is-container.md

  • names renderers/layout/react-page.tsx → packages/components/src/renderers/layout/react-page.tsx — edited by this change

    Second consumer, and the reason this is not purely a declaration change. renderers/layout/react-page.tsx builds the JSX scope of every kind:'react' page with if (!tag || cfg.isContainer) continue;. While flex omitted the flag it was the one layout primitive of the five still injected there, so ANGLE-BRACKETS(Flex) resolved in react page source — and rendered an EMPTY div, because the injected wrapper drops children. It now behaves like its four siblings and is not injected, which is what content/docs/guide/react-pages.md has documented all along ("Layout containers are deliberately not injected ... ANGLE-BRACKETS(flex), ANGLE-BRACKETS(grid), ANGLE-BRACKETS(card) and friends have no injected wrapper"). A react page that wrote ANGLE-BRACKETS(Flex) moves from silently swallowing its children to the page-level error panel naming the identifier, with that page's documented remedy being real HTML: ANGLE-BRACKETS(div style={{ display: 'flex', gap: 16 }}).

  • names content/docs/guide/react-pages.md → content/docs/guide/react-pages.md — edited by this change

    Second consumer, and the reason this is not purely a declaration change. renderers/layout/react-page.tsx builds the JSX scope of every kind:'react' page with if (!tag || cfg.isContainer) continue;. While flex omitted the flag it was the one layout primitive of the five still injected there, so ANGLE-BRACKETS(Flex) resolved in react page source — and rendered an EMPTY div, because the injected wrapper drops children. It now behaves like its four siblings and is not injected, which is what content/docs/guide/react-pages.md has documented all along ("Layout containers are deliberately not injected ... ANGLE-BRACKETS(flex), ANGLE-BRACKETS(grid), ANGLE-BRACKETS(card) and friends have no injected wrapper"). A react page that wrote ANGLE-BRACKETS(Flex) moves from silently swallowing its children to the page-level error panel naming the identifier, with that page's documented remedy being real HTML: ANGLE-BRACKETS(div style={{ display: 'flex', gap: 16 }}).

.changeset/6764-container-declaration-census.md

  • names renderers/layout/react-page.tsx → packages/components/src/renderers/layout/react-page.tsx — edited by this change

    Scoped by measurement, not by sweep. The census behind this change rendered every registered key through the real SchemaRenderer and put it through validateTree: of 131 bare authoring tags, 58 render schema.children, 5 declared the flag, and 53 did not. These 8 are the subset where the second consumer is provably unaffected — renderers/layout/react-page.tsx drops containers from the kind:'react' JSX scope, but it reads getPublicConfigs() and none of the 8 is in the curated public contract. The remaining 45 are reported on the card rather than swept in, button among them precisely because it IS public.

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

  • 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/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/components-react-page-published-dts-text-5666.md

  • names content/docs/guide/react-pages.md → content/docs/guide/react-pages.md — edited by this change

    • The injected-scope note no longer tells authors that layout is left to "plain HTML + Tailwind". It says plain HTML. - A new paragraph states the styling contract for kind:'react' page source: source is runtime metadata, not build input. Style with inline style objects using hsl(var(--token)) theme colors, and render overlays through ObjectForm with formType "drawer" or "modal" rather than a hand-rolled fixed inset-0 backdrop. Do not author Tailwind utility classes in page source: the console's Tailwind is compiled at build time by scanning the console's own src and there is no safelist, so an authored utility class silently produces no CSS and no error anywhere. os validate reports it as page-source-className-tailwind. (ADR-0065; ADR-0080's 2026-06-30 amendment; see content/docs/guide/react-pages.md.)

.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/page-source-tailwind-framing-5461.md

  • names content/docs/guide/react-pages.md → content/docs/guide/react-pages.md — edited by this change

    This is a published type surface: the TSDoc ships in @object-ui/types's built .d.ts and is what an author reads on hover over kind. It said a kind:'html' page is "constrained JSX/HTML + Tailwind" — and it links content/docs/guide/react-pages.md, which 688cb93ad has already corrected to say the opposite. Shipped type documentation was contradicting the guide it points readers to.

  • names packages/components/src/renderers/layout/react-page.tsx → packages/components/src/renderers/layout/react-page.tsx — edited by this change

    packages/components/src/renderers/layout/react-page.tsx carries the same correction on its two source comments (the injected-scope note and buildComponentScope), and gains the styling note the file was missing. Those are internal comments — they do not project into any .d.ts and change no export — so they get no entry of their own; there is nothing an @object-ui/components consumer could read in a CHANGELOG and act on.

.changeset/page-source-tailwind-prose-retraction-5469.md

  • names content/docs/guide/react-pages.md → content/docs/guide/react-pages.md — edited by this change

    The tiers themselves are unchanged and every load-bearing claim survives — parse-never-execute, the untrusted-author safety argument for html, and the deprecated 'jsx' alias. Only the styling primitive is corrected, to the wording content/docs/guide/react-pages.md §Styling already uses:

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.

Angle-bracketed names in the quoted prose above are rewritten as ANGLE-BRACKETS(name): GitHub deletes tag-shaped fragments from a stored body, and a quote that silently loses the identifier it is about is worse than a visible repair.

Compared the checked-out tree with 328abeb55 (merge-base with origin/main): 4 file(s) changed outside .changeset/, read against 1689 pending declaration(s) that publish a body (2289 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3103.3 KB 3104.5 KB
Main entry chunk (gzip) 149.5 KB 350 KB
Entry file index-g67sGbIF.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.58KB 6.17KB
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.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
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.70KB 10.94KB
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) 558.59KB 133.88KB
core (index.js) 9.93KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.61KB 63.16KB
fields (index.js) 261.01KB 66.28KB
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.32KB 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.96KB 14.83KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.08KB 46.94KB
plugin-dashboard (index.js) 137.82KB 36.70KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 233.51KB 61.80KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 161.21KB 41.41KB
plugin-gantt (index.js) 170.35KB 42.19KB
plugin-grid (index.js) 228.33KB 62.59KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.86KB 28.64KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.17KB 12.20KB
plugin-timeline (index.js) 31.15KB 9.14KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 89.15KB 22.35KB
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.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.22KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
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.26KB
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.27KB 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: af76d224cb6bd8180a879176ffc4a44f32e388e8

① Derived judgments

Read at 2026-09-28T12:40Z. Diff base: merge-base 3b469c8ea127ff46d0af23c87090d7b6d6c8e767; origin/main at read time c328900167c6c8463605e5840b9be28d1e16a4ba; six files, +237/−23, three commits. Everything below was measured on the head, not taken from the dev report.

The injection.

  • Canonical: react-page.tsx imports useDataInvalidation from @object-ui/react — the module-level hook in packages/react/src/data-invalidation.ts (exported through index.ts) that plugin-list/src/ListView.tsx:2210 reads. packages/react is not in the diff. No second channel.
  • Scope memo deps stay [schema, adapter]; the hook is a module import, so no page can hold a stale hook and none recompiles: ReactRunner.getDerivedStateFromProps recompiles only when code or the scope identity changes, and neither moves.
  • No new dependency edge: packages/components/package.json is not in the diff; @object-ui/react is already workspace:* there and the file already imported useAdapter from it.
  • Rules of hooks: the same as useAdapter. The runtime renders the page's exports.default through createElement, so both are hooks called unconditionally from the Page component body; useDataInvalidation carries useState and useEffect where useAdapter carries useContext, on the same host React.

The collision.

  • evalCode in packages/react-runtime/src/index.tsx is new Function(...Object.keys(scope), code)(...), so scope names are the parameters of the function the source runs in. Probed through the real ReactKindPage at head (throwaway test, removed): a top-level const or let named useDataInvalidation renders React page error with SyntaxError: Identifier 'useDataInvalidation' has already been declared; a top-level class of that name does too; a top-level function or var only shadows the injected hook (the page renders and reads the shadow).
  • Census re-run. objectui at head: the only declaration of the name is the hook's own; no page source under apps/, examples/, content/, skills/ declares it (the one embedded react source, apps/console/src/sdui-workbench-preview.tsx, does not). objectstack origin/main 87c37aec1: no file names useDataInvalidation at all; the three showcase react pages (crm-workbench, renewals-pipeline, task-desk) do not declare it. The census holds.
  • Stated honestly: the changeset's Clause-② paragraph names the const/let case, the panel it shows in, and the rename remedy (class is not named — nit under ②). minor is right for it: the accept set widens, nothing is removed, the census is empty, and ruling 5864668610 orders minor.

The pin (packages/components/src/__tests__/react-page-invalidation.test.tsx), run in a detached worktree at af76d224c after an offline frozen install:

  • head: Tests 2 passed (2). Real SchemaRenderer, real PageRenderer for type: 'home', real ReactKindPage, real @object-ui/react-runtime compile, real notifyDataChanged. The root vitest config aliases @object-ui/react and @object-ui/react-runtime to src (vitest.config.mts:504,508), so no dist sits between a mutation and the run.
  • one record-scoped event on showcase_project: find 1 to 2, rows read 2, and the counter the user clicked still reads 1 — re-run once, in place, no remount. Lit control: an event on showcase_invoice leaves find at 1 and the rows at read 1 while the bare reader beside the page moves 0 to 1.
  • ablation (the injected entry replaced by a stub returning 0; anchor useDataInvalidation, hit once): Tests 1 failed | 1 passed (2), only the re-read test, expected 'read 1' to be 'read 2'; the lit control stays green. Restored; git diff HEAD empty.
  • base leg (react-page.tsx from 3b469c8ea, 0 occurrences of the name): Tests 2 failed (2), both at the page-error assertion, ReferenceError: useDataInvalidation is not defined. Restored.
  • Does it still isolate behaviour? Yes. Base reds both tests for one reason (the page cannot compile), so base alone does not discriminate; the ablation is the isolating leg and reds exactly the re-read test. Red on base and behaviour-isolating together.

The guide (content/docs/guide/react-pages.md).

  • Scope table row: true — a number that moves when a write to that object is reported. It also moves on an unscoped '*', which the row and the "why" sentence leave unsaid; the changeset and the file header state it. Omission, not a falsehood.
  • Live data example: the fence at head was run verbatim through the real ReactKindPage: it compiles, reads once with $filter: ['status', '=', 'open'], re-reads exactly once on a bus event for showcase_project, and once more on '*'. changed is in the dependency array, [adapter, changed].
  • The one sentence on why is one sentence and true: the effect reads again in place, the page is not remounted and keeps its state (pinned).
  • A copied page refreshes by default: yes, the taught fence carries the nonce.
  • No other sentence in the guide, and no other file under content/docs, skills/ or apps/console/src, enumerates the injected scope; nothing went stale.

Notes (a) and (d).

  • (a) .changeset/10887-object-view-non-grid-bus-reader.md: the first three lines hash identical at base and head (sha256); the sentence has shipped to no CHANGELOG.md (grep over every CHANGELOG at origin/main: no match), so the in-place correction is right. The corrected claim is true: ObjectGantt.tsx sets hostRows to the handed array only when it is a non-empty array (null otherwise) and reads useDataInvalidation(recordQueryDerivesExpand && resource ? resource : undefined) with recordQueryDerivesExpand = hostRows === null && !hasInlineData, so a gantt handed zero rows queries for itself and keeps its own bus reader; ObjectView hands data={data} (line 2729). The same sentence in ObjectView's member 1 comment is corrected the same way.
  • (d) the identityKey comment reads true against the code: fetchDrawsView = !renderListView && type !== 'grid' && type !== 'tree' && type !== 'chart' gates the fetch's nonce; ObjectTree.tsx:760 reads the bus for itself and re-queries on the handed array (objectui#10778); ObjectChart.tsx:970 reads the bus when it fetches for itself and ObjectView's chart case hands it no rows; a gantt handed rows draws them (objectui#7333).
  • ObjectView.tsx: every changed line is a // comment line (mechanically: no non-comment +/- line in the hunk). Both hunks sit inside the component body; no .d.ts projection; nothing but comments changed.

Notes (b) and (c) were correctly left out: both are true and both need behaviour.

  • (b) fetchDrawsView is a deny-list (grid, tree, chart), so a host views type generateViewSchema has no case for still subscribes and re-reads. An allow-list changes which views subscribe.
  • (c) the non-grid fetch calls setLoading(true) on every run (ObjectView.tsx:1236) and passes loading={loading} (line 2730); ObjectCalendar syncs externalLoading into its own loading and returns the Loading calendar… placeholder while it is set. A silent re-read is a behaviour change.

CI at af76d224c: 43 check-runs, 40 success, 3 skipped (dependabot, Test (coverage), the coverage-shard matrix placeholder), 0 failures; combined status success (Vercel). Doc Snippet Type Check is green; the changed fence is jsx, outside TS_FENCE_LANGUAGES (ts, tsx, typescript — scripts/check-doc-snippet-types.mjs:811), as the PR body says.

Commits: three (f64ba6e66, 2ca2478ee, af76d224c); each body carries Co-Authored-By: Claude and a Claude-Session line and nothing else. No commit carries a model name. 2ca2478ee touches the pin only (2 lines); af76d224c touches the changeset and ObjectView.tsx comment lines only, as the body claims.

② Semver level

  • @object-ui/components: minor. One identifier joins a published author scope (Clause-②: yes), nothing is removed or renamed, the one breaking shape (a top-level lexical declaration of the name) is stated and has an empty census, and the ruling orders this level. Right.
  • @object-ui/plugin-view: the pending changeset keeps patch; frontmatter byte-identical; the edit is prose in a file no CHANGELOG carries. Right.
  • Every sentence of both changesets was read against the code and holds: "the same hook ListView reads" (ListView.tsx:2210); "a module-level function, so the scope's identity is as stable as it was and injecting it recompiles no page" (memo deps unchanged); "a write to another object does not re-run it" (lit control); "a page that does not name the hook behaves as before" (an unnamed hook subscribes nothing); the zero-row gantt exception (above).
  • Nit, non-blocking: "The one source this can break is a page that declares its own top-level const or let" — a top-level class of that name is a SyntaxError too (measured). No page declares one; the remedy sentence covers it.

③ Boundary flags

Implemented-by: claude/issue-10887-react-page-invalidation
Reviewed-by: session_01DuWo5bdP9SdVebamn99GGk

VERDICT: PASS


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation package: components plugin tests

Projects

None yet

2 participants