Skip to content

fix(plugin-timeline,plugin-list,plugin-map,components): five more page blocks re-read on the data-invalidation bus (objectui#10623) - #10649

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-10623-invalidation-readers
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-10623-invalidation-readers

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #10623

Clause-②: no

Five readers begin honouring the published data-invalidation contract; no export, schema or accept set moves.

What changed

Each renderer's fetch effect now names the useDataInvalidation nonce for the object it reads, the objectui#10494 shape. There is no host counter prop, no relay and no key= remount. An unscoped change ('*') matches too, and a change to another object does not.

block file object the nonce keys on subscribed only when
object-timeline plugin-timeline/src/ObjectTimeline.tsx schema.objectName (what find names) no authored items, host data or bind rows
object-gallery plugin-list/src/ObjectGallery.tsx schema.objectName no host data, authored data or bind rows
object-map plugin-map/src/ObjectMap.tsx the object provider's object (dataObjectName) the object provider and no host data prop
element:number components/src/renderers/basic/elements.tsx (ElementNumberRenderer) properties.object an adapter and an aggregate are present
element:repeater components/src/renderers/basic/data-list.tsx (RepeaterRenderer) properties.object an adapter with find is present

object-map needed more than the nonce, and its fetch effect now answers the bus the way ObjectGantt's reload does (objectui#10494, objectui#7237, objectui#10578). Four mechanisms, each pinned in ObjectMap.busReread-10623.test.tsx:

  • A. A silent run is a re-read of the query on screen. The map's loading gate unmounts MapGL for the length of a fetch, and MapGL reads its camera once, at mount. A run is silent (no loading gate, so the map and the user's camera stay) only when the nonce moved since the effect's last run AND the run's query inputs equal, value for value, the inputs whose rows were last committed. So a query change always goes through the gate and re-fits the camera, even when a bus event lands in the same commit. A bus event before the first rows, or after a query other than the one on screen failed, is loud too, because nothing on screen answers that query yet. This is ObjectGantt's split between its silent reload (same query) and its reported one (changed query). One difference: the gantt keeps its chart mounted through a changed query too (objectui#7237), and the map deliberately does not, because remounting is how its camera re-fits.
  • B. Only the newest run commits. A sequence ref with an isCurrent() check, as in ObjectGantt's reloadSeqRef, guards every write of rows, the error and the loading flag. The effect's cleanup advances the sequence, so a run superseded by a re-run, or outliving an unmount, commits nothing. Before this change the effect had no cleanup, no cancelled flag and no sequence, so a slow older answer overwrote a newer one.
  • C. A failed silent run keeps the map. It keeps the last good rows and the mounted map, and logs [ObjectMap] Failed to refresh data: to the console, which is exactly what ObjectGantt's silent reload does on failure ([ObjectGantt] Failed to refresh data:). The gantt shows no toast or notice for it, so neither does the map. A failed loud run is reported on the error screen, as before, but only when it is the newest run.
  • D. Committed rows clear the error. Every commit goes through one helper that writes the rows, the ceiling and setError(null). The error is cleared when the current run commits, never when a run starts, which is objectui#10578's rule on ObjectGantt, for the reason that PR measured. Before this change nothing ever cleared the map's error.

The initialViewState docblock was reworded to match.

Census (H1): real SchemaRenderer and registrations over a counting fake data source

The probe was temporary and has been deleted. It ran one mount per block with the AdapterCtx provider around the tree. It read the counts at mount, after notifyDataChanged({ objectName: '*' }), after an event naming ANOTHER object (the negative control), and after an event naming the block's own object. A read is one find or aggregate call.

block base d583db11b head (008a5df9d, and again at cef514df8)
object-timeline 1 → 1 → 1 → 1 1 → 2 → 2 → 3
object-gallery 1 → 1 → 1 → 1 1 → 2 → 2 → 3
object-map 2 → 2 → 2 → 2 2 → 3 → 3 → 4
element:number 1 → 1 → 1 → 1 1 → 2 → 2 → 3
element:repeater 1 → 1 → 1 → 1 1 → 2 → 2 → 3
control object-grid 1 → 2 → 2 → 3 1 → 2 → 2 → 3

The dispatch hypotheses

  • H2, the idiom. The call shape is the one ObjectGrid and ObjectChart use: useDataInvalidation(fetchesForItself ? object : undefined), with the nonce in the fetch effect's dependency list. useDataInvalidation(undefined) and useDataInvalidation('') add no listener, because the hook's effect returns early on a falsy name. The nonce therefore stays 0, and a block with no object binding never re-runs its effect. Each pin's static-rows case measures this (0 reads before and after a '*' event).
  • H3, object-map's two reads on mount. It is a double fetch, measured with the probe above. Without a lookup field the two calls are the same query: find task with $top 2001, twice. With a lookup field the first call carries no $expand and the second carries $expand: ['owner']. The fetch effect keys on objectSchema, which lands after the first query. That is the ungated shape objectui#7895 and objectui#7903 removed from the timeline and the gallery. The nonce neither causes nor cures it: the bus adds exactly one query in both variants (2 → 3). Round 2's sequence guard does not change the count either (still 2 calls at cef514df8); when the second run starts before the first answer lands, that answer is now discarded instead of painted. It is recorded as a finding for the seat and is not fixed here; carrier: card to be filed by the seat.
  • H4, refresh in place. No block changes a key=. The pins measure that the same DOM node survives on the two blocks with local state: the timeline canvas (timeline-canvas, so its scroll survives) and the map (the MapGL node, so its camera survives, on a failed re-read as well as a successful one). The gallery pin collapses a group, fires the event, sees the renamed card, and sees the group still collapsed.
  • H5, pins. There is one census file per package, following the sibling's reason: a single cross-package file would import packages its own package does not declare. Each file carries a positive control, which is a bare useDataInvalidation reader mounted beside the block that must read 1 after the event, and the negative control of an event naming another object.
  • H6, published prose. No sentence is falsified. The READMEs of the four packages have no sentence about refresh or invalidation. The docs pages (plugin-map.mdx, plugin-timeline.mdx, guide/slotted-pages.md, guide/architecture-overview.md) make no claim this change moves. The pending changesets that mention the bus (10035-*, flow-resume-result-5417, 7237-gantt-refresh-in-place, 10578-gantt-error-clears, 10184-*, 10059-*) make no claim about these five blocks that this change moves either. flow-resume-result-5417's "Everything mounted refetches in place" is closer to true after this change, not further from it. No dated note was needed.

Tests, round 1

All of the following ran at 008a5df9d, unless a line says otherwise. Round 2 re-ran what it touched; see the next section.

  • Dependency closure: turbo run build --concurrency=2 with a --filter=PKG^... for each of the four packages exits 0 (13 tasks).
  • type-check (tsc --noEmit && tsc -p tsconfig.test.json) exits 0 for plugin-timeline, plugin-list, plugin-map and components. --listFiles counts each new pin as a program input once per package (1/1/1/1).
  • Whole package suites, run as pnpm exec vitest run --maxWorkers=2 packages/PKG/ under the verify lock:
    • plugin-timeline: Test Files 30 passed (30), Tests 341 passed (341).
    • plugin-list: Test Files 95 passed (95), Tests 1134 passed (1134).
    • plugin-map: Test Files 29 passed (29), Tests 173 passed (173).
    • components: Test Files 306 passed | 1 skipped (307), Tests 3022 passed | 17 skipped (3039).
  • New pins, shown red on base and green on head. The four files were copied onto a d583db11b worktree, where they gave Tests 11 failed | 5 passed (16). Every failure is the missing re-read ("expected to be called 2 times, but got 1 times" / "expected +0 to be 1"; in the components file's new-data case, the new row never appears). In each failing case that mounts the positive control, the control had already read 1; the new-data case mounts none. The 5 passes are the static-rows cases, which assert no read either way. On head the result is Tests 16 passed (16).
  • Ablation, with the fix committed. Mutations went through ablation-replace.mjs with a trap that restores from HEAD by absolute path. Nothing involves dist: each pin imports its subject through a relative path.
    • Nonce leg: , invalidationNonce] was dropped from all five dependency lists. The on-disk count went 1 → 0 in each file, and the result was Tests 11 failed | 5 passed (16): every block's re-read cases went red.
    • Map silent leg: if (!silent) setLoading(true); became if (silent || !silent) setLoading(true);. The on-disk count went 1 → 0, the marker count 0 → 1, and the result was Tests 1 failed | 15 passed (16), with exactly "the re-read unmounted the map". A first attempt at this leg used the replacement setLoading(true);. The tool refused it (the replacement is a substring of the anchor, so its count cannot rise), restored the file, and ran no tests. It is not counted.
    • After both legs, all five blobs equal HEAD and git diff HEAD is empty.
  • Gates, all exit 0: check-changeset-presence ("9 source file(s) of 4 released package(s) changed, and this change declares 1 changeset(s)"), check:new-line-citations, check:control-bytes, check:changeset-claims, check:pending-changeset-literals, check-changeset-no-major, check:phantom-deps, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit and check:vi-mock-override-shape. check-governed-queue-guard --test answers NOT GOVERNED.
  • Lint: targeted eslint --no-inline-config --format json over the 9 touched .ts/.tsx files gives 0 errors. For the five source files, the per-rule warning counts are identical on base and head. The repo-wide pnpm lint and pnpm test are left to CI.
  • git merge-tree against origin/main f99f9cde7 is clean, and none of the 11 commits main gained since the base touch these files.

Round 2 (contract review of the map's silent re-read)

The review failed the map's round-1 silent re-read: silence was decided from the nonce alone, the effect had no stale guard, a failed silent run unmounted the map through the error screen, and nothing cleared the error. Round 2 changes only ObjectMap.tsx, adds ObjectMap.busReread-10623.test.tsx, and corrects the changeset. The four fixes are described under "What changed".

All of the following ran at cef514df8. The pins, the ablations, the build, the type-check and the suite ran under the verify lock (VERDICT command-exit 0, held 200s, shared box). The gates, the lint and the census probe ran outside it.

  • Pins, red then green. ObjectMap.busReread-10623.test.tsx has 8 cases: the control (a plain query change mounts a new map fitted to the new rows), A (a query change in the same commit as a bus event re-fits), three for B (a slow older answer does not overwrite a newer one; a slow older failure does not show the error screen; a superseded answer does not release the loading gate), C (a failed bus re-read keeps the rows and the same map node, and logs), and two for D (a later query that succeeds clears the error; a bus re-read after a reported failure goes through the gate and clears it).
    • On 008a5df9d: Tests 7 failed | 1 passed (8). The one pass is the control. Every failure is the fault named in its case.
    • On cef514df8: all 8 pass.
  • Census pins, base and head. The plugin-map and plugin-timeline census pins (*.invalidationRefetch-10623.test.tsx) on d583db11b give Tests 4 failed | 2 passed (6); the passes are the static-rows cases. At cef514df8 those two files plus the round-2 file give Test Files 3 passed (3), Tests 14 passed (14). The census probe (temporary, deleted; run unlocked at --maxWorkers=1) re-run at cef514df8 reads the same as the table above for all six rows.
  • Per-hunk ablation, each through ablation-replace.mjs with a trap restoring from HEAD by absolute path. Each leg's anchor count went 1 → 0 and its marker count 0 → 1 on disk, and each restore left the blob equal to HEAD with git diff HEAD empty. Run over the round-2 file and the round-1 map pin (11 tests):
    • A (silence from the nonce alone): Tests 1 failed | 10 passed (11), the A case.
    • B (isCurrent always true): Tests 3 failed | 8 passed (11), the three B cases.
    • C (a failed silent run reports): Tests 1 failed | 10 passed (11), the C case.
    • D (no error clear on commit): Tests 2 failed | 9 passed (11), the two D cases.
  • plugin-map. The dependency-closure build gives Tasks: 13 successful, 13 total. type-check exits 0, and --listFiles counts the new pin once. The whole suite gives Test Files 30 passed (30), Tests 181 passed (181).
  • Gates, all exit 0: check-changeset-presence ("10 source file(s) of 4 released package(s) changed, and this change declares 1 changeset(s)"), check-changeset-no-major, check:control-bytes, check:new-line-citations ("0 new citation(s)"), check:changeset-claims, check:pending-changeset-literals, check:phantom-deps, check:test-path-roots and check:vi-mock-specifiers/inherit/override-shape. check:changeset-claims lists four pending changesets that name files this PR edits (6726-*, 6783-*, 6939-*, 9618-*). I re-read each paragraph, and none is made false.
  • Lint. Targeted eslint --no-inline-config gives 0 errors on ObjectMap.tsx and on the new pin. ObjectMap.tsx has the same per-rule warning counts as on 008a5df9d (13 no-explicit-any). The first draft of the pin had 2 errors (rules-of-hooks in the map stub, react-hooks/globals in the host); the harness was reworked before the red and green readings above were taken.
  • Changeset. The second sentence now reads: "These blocks read neither onMutation nor the bus, so any write — through the data source or past it (a page action over raw HTTP, a flow, a server action) — showed only when something remounted them." On d583db11b, onMutation appears 0 times in each of the five files; the control is ListView.tsx with 3. The map bullet now says what a failed re-read does and names the two older faults fixed here. The timeline bullet is scoped to "while the re-read runs", because a failed re-read still shows the error screen (see the timeline finding under Acceptance notes).
  • Main. git merge-tree against a freshly fetched origin/main (785d3a731, 16 commits past the base) is clean, and none of those commits touches this PR's files, so no merge was made.

Acceptance notes

  • element:number shows its … placeholder, and element:repeater its Loading… line, for the length of a re-read. Neither holds local state, so nothing is lost, but the list is swapped out briefly. Same shape as the sibling's calendar note.
  • object-timeline and object-gallery show their placeholder during a re-read only when they have no rows yet. Otherwise the canvas and the card grid stay mounted.
  • object-map's stale-answer guard and error clear are fixed here (round 2, B and D). Both faults predate this PR, and both are pinned red on 008a5df9d and green on cef514df8.
  • object-timeline never clears error, and its error screen is an early return above the canvas. So a failed re-read replaces the canvas with the error screen, and a later re-read that succeeds leaves it there until a remount. A temporary probe measured it at cef514df8 through the real registration: after a failing bus re-read the canvas is gone and the error shows; after a succeeding one (3 find calls in all) the error still shows. The mechanism predates this PR, and this PR widens its reach, since a bus event can now trigger the re-read. Not fixed here; carrier: card to be filed by the seat.
  • element:repeater's effect reads properties.sort but does not list it as a dependency. Read from the source, not measured. Carrier: card to be filed by the seat.

Generated by Claude Code

…on bus (objectui#10623)

The fetch effect now names the useDataInvalidation nonce for the object it
queries, so a write declared with notifyDataChanged re-reads the rows in
place. Authored items, host data and bound rows are not subscribed.

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

The fetch effect now names the useDataInvalidation nonce for the object it
queries, so a write declared with notifyDataChanged re-reads the cards in
place; collapsed groups survive. Host data, authored data and bound rows
are not subscribed.

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

The fetch effect now names the useDataInvalidation nonce for the object it
queries. A run the bus caused does not flip loading, so MapGL stays
mounted and the user's camera survives the re-read. Only the object
provider without host rows is subscribed.

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

Each effect now names the useDataInvalidation nonce for the object in its
properties.object, so a write declared with notifyDataChanged re-reads
the value or the rows. A block with no object or no adapter is not
subscribed.

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

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

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 4 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/6726-find-envelope-records-arms.md

  • names components/src/renderers/basic/data-list.tsx → packages/components/src/renderers/basic/data-list.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 |

  • names components/src/renderers/basic/elements.tsx → packages/components/src/renderers/basic/elements.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/6783-readprops-degenerate-config-bag.md

  • names elements.tsx → packages/components/src/renderers/basic/elements.tsx — edited by this change

    Five modules under packages/components/src/renderers/basic/ — elements.tsx, data-list.tsx, text-input.tsx, record-picker.tsx, metadata-viewer.tsx — each carried a copy of the same reader, { ...(schema?.props ?? {}), ...(schema?.properties ?? {}) }. ?? only replaces null/undefined, so a non-object bag went into the object spread and came back out as indexed keys: for properties: 'not-a-bag', the config bag a renderer received was { '0': 'n', '1': 'o', … '8': 'g' } — nine keys nobody authored. The five copies are now one readProps (renderers/basic/readProps.ts) that asks isConfigBag, and a degenerate bag on either side contributes no keys.

  • names data-list.tsx → packages/components/src/renderers/basic/data-list.tsx — edited by this change

    Five modules under packages/components/src/renderers/basic/ — elements.tsx, data-list.tsx, text-input.tsx, record-picker.tsx, metadata-viewer.tsx — each carried a copy of the same reader, { ...(schema?.props ?? {}), ...(schema?.properties ?? {}) }. ?? only replaces null/undefined, so a non-object bag went into the object spread and came back out as indexed keys: for properties: 'not-a-bag', the config bag a renderer received was { '0': 'n', '1': 'o', … '8': 'g' } — nine keys nobody authored. The five copies are now one readProps (renderers/basic/readProps.ts) that asks isConfigBag, and a degenerate bag on either side contributes no keys.

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

  • names plugin-map/src/ObjectMap.tsx → packages/plugin-map/src/ObjectMap.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/9618-record-source-ladder-attribution.md

  • names ObjectMap.tsx → packages/plugin-map/src/ObjectMap.tsx — edited by this change

    Their zod .describe strings and TS docs said the data → staticData → objectName ladder is resolved by getDataConfig. Neither renderer has had a function by that name since the ladder moved into @object-ui/core's shared resolveRecordSourceConfig (objectui#7632), so the text now names that function. The ladder order is unchanged, and so is every accepted document. The ObjectMapSchema text still says getDataConfig, which is true: ObjectMap.tsx keeps a local wrapper by that name that delegates to the shared ladder. The resolveRecordSourceConfig docblock in @object-ui/core now says that the text it quotes is the map faces' text.

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 785d3a731 (merge-base with origin/main): 10 file(s) changed outside .changeset/, read against 1505 pending declaration(s) that publish a body (2089 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3051.4 KB 3104.5 KB
Main entry chunk (gzip) 148.0 KB 350 KB
Entry file index-jf1ZSt66.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) 547.12KB 130.87KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.91KB 62.28KB
fields (index.js) 260.06KB 65.86KB
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.49KB 14.64KB
plugin-charts (index.js) 74.94KB 20.89KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.60KB 35.41KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.96KB 61.65KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 152.20KB 39.05KB
plugin-gantt (index.js) 169.64KB 41.91KB
plugin-grid (index.js) 215.37KB 58.92KB
plugin-kanban (index.js) 48.35KB 15.08KB
plugin-list (index.js) 114.64KB 28.35KB
plugin-map (index.js) 22.57KB 7.44KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.75KB 8.98KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.84KB 21.96KB
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) 116.21KB 38.14KB
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) 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: CONTRACT_REVIEW_TIER
Head-sha: 008a5df9d3d66ab7275b4b4f68f53762fc752bce

① Derived judgments

The idiom, per block (read from the head source; the hook's dataChangeMatches returns true on '*', false on another object name, and useDataInvalidation returns early on a falsy name, so an unsubscribed block's nonce stays 0).

  • object-timeline: keyed on schema.objectName, which is the name dataSource.find(schema.objectName, …) passes. Gate fetchesForItself = !!schema.objectName && !boundData && !schema.items && !(props as any).data is byte-equal to the effect's former if condition, and the effect now branches on the same binding. No extra state. The pre-existing refreshKey in the deps is the pull-to-refresh's own local counter (setRefreshKey in handleRefresh), not a host prop. Correct.
  • object-gallery: keyed on schema.objectName, the name find passes. Gate equals the former if. fetchesForItself added to the deps is derived from deps already listed, so it adds no re-run. Correct.
  • object-map: keyed on dataObjectName, which is the objectName the object arm passes to find. Gate !Array.isArray(dataProp) && dataProvider === 'object' excludes host data and the inline value provider. One ref added (answeredInvalidationRef), no state. Correct as a subscription; the silent flag is judged below.
  • element:number: keyed on props.object, the name both the aggregate call and the find fallback pass. Gate adapter && props.aggregate matches the effect's !adapter || !props.object || !props.aggregate early return. Correct.
  • element:repeater: keyed on props.object, the name adapter.find(props.object, query) passes. Gate adapter && typeof adapter.find === 'function' matches the effect's early return. Correct.
  • The call shape is ObjectGrid's and ObjectChart's (useDataInvalidation(fetchesForItself ? object : undefined), nonce in the fetch deps). The import into @object-ui/components is from @object-ui/react, already a declared dependencies entry (it already imports useAdapter from there): no layer inverted.

object-map's silent re-read. Judged from the head source (lines 776 to 967 and the loading / error returns), not measured, because the verify lock was never granted to me (see ③).

  1. Same commit, nonce plus query change: silent is computed from the nonce alone, so the run skips setLoading(true), MapGL stays mounted and initialViewState (read once at MapGL mount) is not re-applied. New rows paint under the old camera and are not re-fit. INTRODUCED here. The PR body's sentence "A query change still goes through the gate and re-fits the camera, as before." is false for that case. ObjectGantt avoids it structurally: its nonce effect is separate from its query reload.
  2. Overlap: the effect has no cleanup, no cancelled flag and no sequence counter, on base and on head (base lines 778 to 940 confirm), so a slow older response overwrites a newer one and a response can land after unmount. PRE-EXISTING mechanism; the bus adds a new way to start the overlapping run. ObjectGantt has reloadSeqRef / isCurrent(); the sibling finding for gantt is already open as objectui#10633 (draft PR objectui#10652).
  3. Failure: the silent run's catch does setError(err); the render returns the error screen before MapGL, so a failed silent re-read unmounts the map and loses the camera the silent mode exists to keep, and nothing ever calls setError(null), so a later successful silent run updates data while the error screen stays until remount. Mechanism PRE-EXISTING; its REACH is introduced here: on base a failure could arise only from mount or a query change, now every bus event (every console dataSource write, via useMutationInvalidationBridge in AppContent) can put a healthy map on a permanent error screen. ObjectGantt's silent mode keeps the last good rows on failure and clears error on commit (objectui#10578). The PR body's "ObjectGantt answers the bus the same way" overstates the likeness. The same never-cleared error exists in object-timeline (setError only at line 425, no clear; if (error) returns above the canvas), which the PR does not note.
  4. Ref timing: the ref is written at the top of the effect, before the await, and starts at the hook's initial 0. Mount: nonce 0 equals ref 0, not silent. A query-only re-run leaves the nonce unchanged, not silent. StrictMode's double mount effect: both runs read 0 equals 0, so both are non-silent and the ref stays 0; a later bump reads 1 versus 0, silent, ref set to 1. Correct. Two bumps batched into one commit read once, silent. Correct.

The census and the pins. The four pin files are well-formed: each mounts a bare useDataInvalidation reader beside the block (positive control, asserted '1' before the block's count) and fires an event naming some_other_object (negative control, asserted 0 added), and each in-place assertion is concrete (toBe(canvas) on timeline-canvas; toBe(canvas) on the mocked MapGL node plus 2 markers; the gallery asserts the renamed card present AND the collapsed group's Acme absent, so it cannot pass vacuously). Read from source, a key={invalidationNonce} on the canvas or on MapGL, or a reset of collapsedGroups on the nonce, would each red exactly one in-place assertion. ⛔ NOT RE-RUN BY ME: head and base worktrees were built (node_modules borrowed, four pins copied onto d583db1), five ablation legs (nonce dropped from ObjectMap and ObjectTimeline deps; MapGL and the timeline canvas keyed on the nonce; gallery collapsed state reset on the nonce) and a probe for the four map scenarios were scripted with on-disk grep and blob proof, and queued under os-verify-lock.sh, which stayed with holder pid 31482 (over 20 min) and 4 other waiters until this record was due. The waiter was withdrawn and both worktrees removed. The PR body's base reading 11 failed | 5 passed (16), head 16 passed, and the suite counts (30/341, 95/1134, 29/173, 306|1/3022|17) are therefore the dev's numbers, not confirmed here. Numstat confirmed: +667/-16 over 10 files, per-file figures match the report.

The dev's three findings.

  • object-map double fetch on mount: REAL, PRE-EXISTING. The fetch effect lists objectSchema, set by a separate effect after getObjectSchema resolves, so mount issues find twice (timeline and gallery gate on objectDefReady; the map does not). Owes a card (the dev names the useSettledSchema family).
  • No stale guard and no error clear: REAL, PRE-EXISTING in mechanism, reach widened here (item 3 above). "Carrier: none" is wrong: both owe a card, with the gantt precedents objectui#10633 and objectui#10578, and the timeline's never-cleared error belongs on the same card.
  • RepeaterRenderer sort dependency: REAL, PRE-EXISTING. The effect reads props.sort into $orderby and its deps list [adapter, props.object, filterKey, props.limit, invalidationNonce]. Owes a card (small).

② Semver level

patch ×4 and Clause-②: no are right: no export, schema or accepted key moves; the only new import is an already-declared dependency. Changeset sentence by sentence: the first and the five bullets and the last are true, with two qualifications. (a) "A write that bypasses the data source … fires no onMutation, so these blocks used to show it only when something remounted them." misstates the cause: none of the five read onMutation on base (0 hits in all five files), and the console bridges every dataSource write onto the bus, so on base a write THROUGH the data source was missed exactly as a bypass write was. The sibling's sentence was accurate for ListView, which did read onMutation (3 sites on base); copied here it narrows the defect. Replacement: "These blocks read neither onMutation nor the bus, so any write — through the data source or past it (a page action over raw HTTP, a flow, a server action) — showed only when something remounted them." (b) The map bullet holds only while the silent run succeeds; a failed one shows the error screen (① item 3). Pending changesets: the grep over .changeset on main names the five blocks only in 10105-pull-to-refresh-late-host, 10106-settled-schema-equal-payload, 7627-shared-record-source-reader and olive-cameras-repeat, none of which claims anything this change moves; flow-resume-result-5417's "Everything mounted refetches in place" gets closer to true; 10035-*, 10572-*, 7237-*, 10578-* make no claim about these five. The four READMEs have zero refresh or invalidation prose; content/docs/plugins/plugin-map.mdx and plugin-timeline.mdx have none; slotted-pages.md lines 270 to 274 and architecture-overview.md line 285 describe the bus and are not falsified. H6 holds.

③ Boundary flags

  • CI on the head: 43 check runs, 40 success, 3 skipped, none red, none pending. Test (shard 2/8) is GREEN, so objectui#10645 does not apply here.
  • git merge-tree --write-tree against current main f99f9cde7: clean (tree 2aafc94689b1…); none of the 11 main commits since the merge-base d583db11b touches the six relevant files.
  • Open PRs: 16 open; none touches the five source files or data-invalidation.ts. objectui#10620 (seat 3, objectui#10572) is MERGED (merged: true), touched plugin-list/src/ListView.tsx and not ObjectGallery.tsx; its six 10572-* changesets are already pending on main, and a second @object-ui/plugin-list: patch changeset from this PR combines with them. objectui#10635 touches ListView.tsx only.
  • Verify lock: my census, ablation and probe legs never ran (lock held by pid 31482 for over 20 min with 4 other waiters at the deadline). Whoever lands this should re-run the base and head pins for plugin-map plus one more package under the lock.
  • The PR is a Draft.

Implemented-by: claude/issue-10623-invalidation-readers
Reviewed-by: session_014mXUNuFomfj24w7s1pZzhN

VERDICT: FAIL

What stops landing is wording that the squash merge would publish, plus one unmeasured reach change; the mechanical fix is the right idiom in all five blocks. Required edits, exact text:

  1. PR body, What changed, replace "A query change still goes through the gate and re-fits the camera, as before." with "A query change alone still goes through the gate and re-fits the camera, as before; a query change that lands in the same commit as a bus event is treated as the bus's run, keeps the map mounted and does not re-fit (a known limit of deciding silence from the nonce alone; ObjectGantt decides it in a separate effect)."
  2. PR body, same paragraph, replace "ObjectGantt answers the bus the same way (objectui#10494)." with "ObjectGantt also answers the bus without its loading placeholder (objectui#10494), but through a sequenced reload whose silent failure keeps the last good rows and clears error on commit; this effect has neither guard."
  3. PR body, Acceptance notes, third bullet: replace "Both are read from the source (not measured) and predate this change. Carrier: none." with "Both are read from the source (not measured). The mechanism predates this change; the reach does not: a bus event is a new way to start an overlapping run, and a failed silent re-read now puts a mounted map on its error screen until remount (a camera loss the silent mode exists to prevent). object-timeline never clears error either. Carrier: a card in the objectui#10633 / objectui#10578 family, to be filed by the seat."
  4. Changeset, second sentence, replace with the text given in ② (a).
    Also re-run the base and head pins for plugin-map plus one more package under the verify lock and quote the readings, since this review could not.

Generated by Claude Code

…; newest run commits; errors clear on success (objectui#10623)

Mirrors ObjectGantt: a run is silent only when the invalidation nonce
moved and its query inputs equal the ones whose rows were last
committed, so a query change arriving with a bus event still goes
through the loading gate and re-fits. A sequence ref lets only the
newest run commit rows, an error or the loading flag, and the effect
cleanup retires a run on re-run or unmount. A failed silent run keeps
the last good rows and the mounted map and logs, as ObjectGantt's
silent reload does. Committed rows clear an earlier error
(objectui#10578's rule).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
… failure and the error clear; changeset wording (objectui#10623)

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

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3051.5 KB 3104.5 KB
Main entry chunk (gzip) 147.9 KB 350 KB
Entry file index-bWzVcHOq.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) 547.12KB 130.87KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.91KB 62.28KB
fields (index.js) 260.14KB 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 (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.49KB 14.64KB
plugin-charts (index.js) 74.94KB 20.89KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.60KB 35.41KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.85KB 61.58KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 152.20KB 39.05KB
plugin-gantt (index.js) 169.71KB 41.93KB
plugin-grid (index.js) 215.37KB 58.92KB
plugin-kanban (index.js) 48.35KB 15.08KB
plugin-list (index.js) 114.64KB 28.35KB
plugin-map (index.js) 23.01KB 7.60KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.75KB 8.98KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.77KB 21.99KB
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) 116.21KB 38.14KB
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) 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: CONTRACT_REVIEW_TIER
Head-sha: cef514df8840b98e1195bd932a850915013d3f74

① Derived judgments

The four mechanisms, read from the head source (ObjectMap.tsx lines 807 to 1012, the render gates at 1265 to 1283) and attacked.

  • A. "Query inputs" is the effect's dependency list less the nonce, built at line 824: dataProp, dataProvider, dataObjectName, dataItems, dataSource, hasInlineData, schema.filter, schema.sort, objectSchema, perms, compared with Object.is against committedQueryRef, which only commit writes (line 840). Every input of the find at line 970 is in it: the object, $filter, $orderby, $expand (built from objectSchema.fields and gated by perms), the adapter; $top is a constant and this site passes no column list, so there are no fields. The lat/lng/title names (mapConfig) are not in it, and were never in the deps on base: they change no fetched row, only the marker transform, and a change to them alone starts no run on base or head, so nothing becomes newly silent. Before the first commit committed is null, so loud; after a loud failure the failed query differs from the committed one, so loud. ONE CORNER: if the host reverts the query to exactly the committed inputs after a different query failed, and a bus event lands in that same commit, the run is silent; its rows do answer that query, and if it fails the earlier error stays up, which is the gantt's pinned rule ("a SILENT reload that fails after an error leaves it up", objectui#10578). So the body's "or after a changed query failed, is loud too" is exact only when "changed query" means one whose rows are not the committed ones. Harmless; noted, not a required edit (optional tightening: "or after a query other than the one on screen failed").
  • B. setData, setRowCeiling, setError(null) and the committed-query write sit inside commit, which returns unless isCurrent() (line 836); setError(err) at 996 and the finally release at 1004 are guarded. The only unguarded write is setLoading(true) at 844, synchronous at run start before any await, so no stale run can reach it. The cleanup at 1009 to 1011 advances the sequence; React runs it before every re-run on a dependency change and on unmount. StrictMode: effect (seq 1), cleanup (seq 2), effect (seq 3); run 1's answer and its finally are dropped, run 3 commits and releases loading. Not stuck. A silent run overtaking a loud one (reachable only through the revert corner) releases loading in its own finally, as the comment says.
  • C. The catch is the gantt's shape: silent logs and keeps rows, else isCurrent() reports; the log string differs only in the tag. loading cannot stay true after a silent failure: a silent run never set it and its finally releases it when current. A stale error is possible only in the revert corner above, where the gantt leaves it up too. Main's gantt has moved since round 1: objectui#10633 (9d89c6f4c, merged 14:45Z, before round 2 was written) added reportOwedRef, so a silent reload that overtakes a changed query inherits its report. The map needs no such ref: a silent run exists only when its query equals the committed one, so the rows in state already answer it, and the situation 10633 repairs is unreachable. "exactly what ObjectGantt's silent reload does on failure" therefore holds for every silent run the map can start.
  • D. setError(null) appears once, inside commit (line 839); nothing clears on start. A loud failure's error screen cannot outlive a later successful run of either mode, because every success commits. The bus re-read after a reported failure is loud whenever the failed query is not the committed one (pin D2 measures the changed-query case); in the revert corner it is silent, and success still clears.

Measured. Worktrees on cef514df8, 008a5df9d and d583db11b under the scratchpad, node_modules borrowed from a sibling checkout at the same lockfile hash, all removed at the end. The legs ran under os-verify-lock.sh: two 540 s budgets expired unacquired (plus one run lost to the container restart and one to my own bad --reporter flag, which ran no tests), the third grant came in 0 s and held 77 s. The seat's withdraw order arrived after the required legs had finished; I killed the run before ablations A and C and the suite, restored the file, and released the lock.

  • Head: ObjectMap.busReread-10623 (8) + map census (3) + timeline census (3): Test Files 3 passed (3), Tests 14 passed (14).
  • 008a5df9d with the round-2 file copied in: Tests 7 failed | 1 passed (8). The pass is the control. The failures name their faults: A "the bus event made a changed query silent, so the map kept the old camera"; B "the superseded south answer overwrote the east rows", "a superseded failure replaced the current rows", "the superseded run cleared the loading gate"; C "a failed bus re-read put the map on its error screen"; D "the map stayed on the error screen" ×2.
  • d583db11b with the map and timeline census pins copied in: Tests 4 failed | 2 passed (6) ("expected +0 to be 1"; "called 2 times, but got 1 times"); the passes are the static-rows cases.
  • Ablation B (isCurrent always true): anchor 1 to 0, marker 0 to 1, blob d7bf83f0… against HEAD 9f71dd71…; Tests 3 failed | 8 passed (11), exactly the three B cases. Restored: blob 9f71dd71…, git diff HEAD empty.
  • Ablation D (setError(null) removed from commit; the anchor spanned two lines, so its count read 2 to 1): marker 0 to 1, blob bd1ffb21…; Tests 2 failed | 9 passed (11), exactly the two D cases. Restored: blob equals HEAD, diff empty.
  • Not measured: ablations A and C (A's mutation was on disk, blob 1ea90a87…, when I withdrew; restored, no reading) and the whole plugin-map suite (the dev reads 30 files / 181 tests). CI's eight shards and Type Check are green on the head, which covers the suite. The dev's deleted census probe was not re-run; the pins corroborate the map and timeline rows' delta of one read per event.
  • eslint --no-inline-config on the head: ObjectMap.tsx 0 errors, 13 no-explicit-any; the pin 0 errors, 8 warnings. Matches the body.

Census table and Acceptance notes. The table's head column is the dev's probe; H3's "still 2 calls at cef514df8" follows from the source (the schema effect lands after the mount run and re-fires the fetch), and "that answer is now discarded instead of painted" is B. Acceptance notes: the map note is true (both faults red at 008a5df9d, green at head, pre-existing per round 1). The timeline note is true from the source (setError only at line 425, if (error) early return at 819 above the canvas at 859; the probe reading is the dev's). The repeater note is true (props.sort read at line 141, deps at 164 omit it).

② Semver level

patch ×4 and Clause-②: no hold: round 2 touches one source file, one new test and the changeset; no export, prop, schema or accepted key moves. Changeset: the second sentence is the round-1 text verbatim (string-checked). The map bullet is true clause by clause: "the markers it is showing", failure logged and rows kept, changed query re-fits even with a bus event, stale answer guard, error cleared by a successful load. The timeline bullet "stays mounted while the re-read runs" is true: the placeholder shows only with no items (line 827); the error screen is a separate early return, which the Acceptance note now records. Landing body: A to D true with the one precision note in ①A; "exactly what ObjectGantt's silent reload does" true for every silent run the map can start; "the gantt keeps its chart mounted through a changed query too (objectui#7237)" true, its query effect calls reload({ inPlace: true }) once loadedOnceRef is set (main line 1054), which sets refreshing, not loading. Round 2 section: every reading I repeated matches ("7 failed | 1 passed", "4 failed | 2 passed", "14 passed", B 3 failed, D 2 failed, the lint counts); "changes only ObjectMap.tsx, adds the pin, and corrects the changeset" is true (3 files); the merge-tree sentence was true at 785d3a731 and remains true. The body has 0 angle brackets; the only closing keyword is "Fixes #10623" on line 1 (line 71's "The four fixes are described" names no issue and closes nothing).

③ Boundary flags

  • CI on the head: 43 check runs, 40 success, 3 skipped, none red, none pending. Test (shard 2/8) is GREEN; objectui#10645 does not apply.
  • git merge-tree --write-tree against main 531c74627 (19 commits past the merge-base d583db11b): clean, tree f05997b8c5d1; none of the 19 touches the six relevant files. Two of them moved ObjectGantt.tsx (objectui#10578, objectui#10633), so the likeness above was judged against main's file, not the stale /home/user/objectui checkout, whose gantt predates both.
  • Open PRs: 14 open; none touches the five source files, data-invalidation.ts or ObjectGantt.tsx.
  • Verify lock: heavily contended (two full budgets expired, one grant held 77 s, withdrawn on the seat's order). Ablations A and C and the whole plugin-map suite went unmeasured here; CI covers the suite.
  • The PR is a Draft.

Implemented-by: claude/issue-10623-invalidation-readers
Reviewed-by: session_014mXUNuFomfj24w7s1pZzhN

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 25, 2026 16:03
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit 02e6d36 Sep 25, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10623-invalidation-readers branch September 25, 2026 16:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants