Skip to content

fix(app-shell,plugin-detail): a record open sends one approvals read and one record read (objectui#12701) - #12748

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-12701-record-page-one-fetch
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-12701-record-page-one-fetch

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12701
Clause-②: yes

What changed

Target 1, approvals: one read per record open, shared by every consumer. useRecordApprovals now keeps the read and its answer in a module-scoped store keyed by object + record. This is the same structural choice as hooks/sharedUserFeeds.ts: no provider is needed above every call site, so no consumer can opt out. It has no polling. Every instance mounted for a record shares one GET /approvals/requests?object=…&recordId=…. A consumer that mounts later (an authored record:approvals block, a tab opened after load) is served the stored answer, and a read already on the wire is joined. The entry lives while any consumer is mounted, so opening the record again reads it again. refresh() always reads (after a decision or a saved edit), and every consumer of the record sees the result. In RecordDetailView, the approvals invalidation effect now fires only when the invalidation nonce actually moves; it used to skip only its first run.

Target 2, the record: one fetch. record:details passes DetailView a new prop, recordFromHost. With it, the body never reads the record itself: while schema.data is absent for the record the host names, it shows its loading state, and it renders the page's record when that arrives. A host that names no record (a binding with no row) renders exactly what it rendered before. Without the prop, DetailView reads its record as before. The page's own read already expands every relation the viewer may read, which covers any section's list. When that list grows (the definition gains a relation), the page reads once more with the union, and the body still sends nothing.

Target 3: RecordDetailView.recordOpenRequests-12701.test.tsx counts requests by URL on the real page, the real adapter and the real hook, under React.StrictMode as the console renders it, on an authored page that also places record:approvals.

What the measurement found

The three identical approvals reads were not three consumers. Caller stacks put all three on the page's one useRecordApprovals: two from its mount effect, which StrictMode replays in development, and the third from the page's skip-the-first-run invalidation effect, which the replay walked through. The fix also covers the genuine multi-consumer case (an authored record:approvals mounts a second hook instance), and the test pins that case. The second record read came from record:details' DetailView, reading the row while the page's read was still on the wire. That one is not StrictMode-related.

Deviations

  • packages/plugin-detail/src/renderers/record-details.tsx is touched: it is the call site that passes the new prop.
  • RecordDetailView.recordOpenRequests-11699.test.tsx is updated: its pin counted the body's own GET /data/OBJ/ID as 1, and that is the read this change removes. It now expects 0.
  • New exported surface: the one DetailView prop, recordFromHost. The app-shell entry is unchanged; the store's test reset is exported from useRecordApprovals.ts only.
  • The full app-shell suite exceeds the container's foreground time cap as one run, so it ran as six shards of the same file set.

Network log, HotCRM, objectstack dev against the vite console

All requests naming the record, from one full page load each. Before = e6175b57f, after = this branch. The rest of each log is console boot reads (session, localization, runtime config, the bell's feeds), whose counts vary from run to run and do not reach this diff.

Lead /apps/app.objectstack.hotcrm/crm_lead/record/2ZErCkJGyOdJVC1A, before:

GET /api/v1/meta/object/crm_lead
GET /api/v1/approvals/requests?object=crm_lead&recordId=2ZErCkJGyOdJVC1A
GET /api/v1/approvals/requests?object=crm_lead&recordId=2ZErCkJGyOdJVC1A
GET /api/v1/approvals/requests?object=crm_lead&recordId=2ZErCkJGyOdJVC1A
GET /api/v1/data/crm_lead?populate=organization_id,created_by,updated_by,owning_business_unit_id,owner_id,converted_account,converted_contact,converted_opportunity,duplicate_of_lead,duplicate_of_contact&top=1&filter=["id","=","2ZErCkJGyOdJVC1A"]
GET /api/v1/data/sys_comment?sort=created_at&filter=["thread_id","=","crm_lead:2ZErCkJGyOdJVC1A"]
GET /api/v1/data/sys_activity?populate=actor_id&top=200&sort=timestamp&filter=["and",["object_name","=","crm_lead"],["record_id","=","2ZErCkJGyOdJVC1A"]]
GET /api/v1/data/crm_lead?populate=converted_account,converted_contact,converted_opportunity,duplicate_of_lead,duplicate_of_contact&top=1&filter=["id","=","2ZErCkJGyOdJVC1A"]
GET /api/v1/data/crm_task?top=1&filter=["and",["related_to_lead","=","2ZErCkJGyOdJVC1A"],[["status","not_equals","completed"]]]

After:

GET /api/v1/meta/object/crm_lead
GET /api/v1/approvals/requests?object=crm_lead&recordId=2ZErCkJGyOdJVC1A
GET /api/v1/data/crm_lead?populate=organization_id,created_by,updated_by,owning_business_unit_id,owner_id,converted_account,converted_contact,converted_opportunity,duplicate_of_lead,duplicate_of_contact&top=1&filter=["id","=","2ZErCkJGyOdJVC1A"]
GET /api/v1/data/sys_comment?sort=created_at&filter=["thread_id","=","crm_lead:2ZErCkJGyOdJVC1A"]
GET /api/v1/data/sys_activity?populate=actor_id&top=200&sort=timestamp&filter=["and",["object_name","=","crm_lead"],["record_id","=","2ZErCkJGyOdJVC1A"]]
GET /api/v1/data/crm_task?top=1&filter=["and",["related_to_lead","=","2ZErCkJGyOdJVC1A"],[["status","not_equals","completed"]]]

The lead has no approval request, so I also measured an opportunity that has a pending, record-locking request (created through the API: amount 200,000, which opens HotCRM's Large Deal Approval). Record-scoped approvals and record requests only, /apps/app.objectstack.hotcrm/crm_opportunity/record/USK6xSbdaYCtPBCP, before:

GET /api/v1/approvals/requests?object=crm_opportunity&recordId=USK6xSbdaYCtPBCP   x3
GET /api/v1/approvals/requests/areq_eaece249-467c-4686-9c84-500c192951eb          x3
GET /api/v1/approvals/requests/areq_eaece249-467c-4686-9c84-500c192951eb/actions  x1
GET /api/v1/data/crm_opportunity?populate=organization_id,created_by,updated_by,owning_business_unit_id,owner_id,crm_account,primary_contact,crm_campaign&top=1&filter=["id","=","USK6xSbdaYCtPBCP"]
GET /api/v1/data/crm_opportunity?populate=primary_contact,crm_campaign&top=1&filter=["id","=","USK6xSbdaYCtPBCP"]

After:

GET /api/v1/approvals/requests?object=crm_opportunity&recordId=USK6xSbdaYCtPBCP   x1
GET /api/v1/approvals/requests/areq_eaece249-467c-4686-9c84-500c192951eb          x1
GET /api/v1/approvals/requests/areq_eaece249-467c-4686-9c84-500c192951eb/actions  x1
GET /api/v1/data/crm_opportunity?populate=organization_id,created_by,updated_by,owning_business_unit_id,owner_id,crm_account,primary_contact,crm_campaign&top=1&filter=["id","=","USK6xSbdaYCtPBCP"]

The opportunity page draws the same thing before and after: the decision bar (Approve, Reject, Reassign, Send back, Request info, Recall, Reply), the Locked for approval band with Recall approval, the disabled Edit, and the details body. The lead page draws the same thing too. The record's sys_activity read is once before and once after; it is not duplicated.

Tests, at the head this body names

Head 13a95b0 (origin/main merged in; no conflict):

  • pnpm exec vitest run packages/app-shell/ --shard=K/6, K = 1 to 6: every shard exit 0. 1471 of 1471 files (1 skipped), 13841 tests passed, 9 skipped.
  • pnpm --filter @object-ui/plugin-detail test: exit 0. Test Files 276 passed, 1 skipped; Tests 2666 passed, 14 skipped.
  • type-check for both packages, including their test tsconfigs: exit 0. Build of @object-ui/app-shell and its dependency closure, including plugin-detail: 29 of 29 tasks. eslint --no-inline-config on the touched files: 0 errors.
  • check:i18n-keys, check:i18n-drift, check:new-line-citations, check-changeset-presence: all exit 0.

Each new pin was ablated once. Each mutation was confirmed on disk, the targeted test went red, and the file was restored with git diff HEAD empty:

  • every mount reading: approvals reads 4;
  • the nonce guard removed: approvals reads 3;
  • recordFromHost removed from record:details: record reads 2, and the objectui#11699 pin also goes red;
  • the DetailView guard disabled: findOne called once;
  • the shared read publishing no rows: the CONTROL goes red;
  • the expand list dropped from the record effect's dependencies: the union test reads 1 instead of 2.

Acceptance notes

  • In development, after a git merge under a running vite dev server, the HMR graph kept serving the old record-details module and the second record read reappeared. A fresh dev server reads 1. This is a dev-server artifact, not a code defect.
  • The opportunity page's related-list previews read each relation twice, once with top=3 and once with top=1 (as filtered count reads): quotes, line items and tasks. These are different queries, outside this change.
  • Production builds do not double-invoke effects. The approvals read was likely already 1 per open there on synthesized pages; not measured.

Session: https://claude.ai/code/session_01LbpjhSB18hFom4adrPRd4B


Generated by Claude Code

…and one record read (objectui#12701)

useRecordApprovals shares one read per object + record across every
consumer mounted on the page, and joins a read already in flight, so
StrictMode's replayed mount no longer sends a second one. The record
page's invalidation effect re-reads approvals only when the nonce moves.

record:details hands DetailView the new recordFromHost prop: the body
waits for the page's record instead of reading the same row again.

Claude-Session: https://claude.ai/code/session_01LbpjhSB18hFom4adrPRd4B
Co-authored-by: Claude <noreply@anthropic.com>
…(objectui#12701)

Mounts the real record page over the real adapter and hook under
StrictMode, on an authored page that also places record:approvals:
approvals 1, record 1, the activity read unchanged; a CONTROL that every
approvals consumer still shows its state; and a wider expansion that
triggers exactly one refetch with the union. The objectui#11699 pin now
expects the body to send no record read of its own. Adds a DetailView pin
for recordFromHost, and the changeset.

Claude-Session: https://claude.ai/code/session_01LbpjhSB18hFom4adrPRd4B
Co-authored-by: Claude <noreply@anthropic.com>
… request count (objectui#12701)

With equal populate lists the adapter's in-flight sharing folded the
body's own record read into the page's, so the count could not see it.
A relation the page expands and no section draws reproduces the measured
two distinct URLs.

Claude-Session: https://claude.ai/code/session_01LbpjhSB18hFom4adrPRd4B
Co-authored-by: Claude <noreply@anthropic.com>
…ut of render-time ref access (objectui#12701)

The per-instance "approvals route absent" flag is state adjusted during
render and mirrored into the ref the mount effect and refresh read, so
the hook reads no ref while rendering. The DetailView pin drops its any.

Claude-Session: https://claude.ai/code/session_01LbpjhSB18hFom4adrPRd4B
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — ACCEPT · objectui#12701 · head 13a95b0 · 2026-10-16T17:45Z

Seat domain:ui#4, session session_01SJn1GEkrbbBQkhGC26wEDw. Checked against GitHub and the diff, not against the report (relayed on the card, 6221311745). The seat opened this PR from the dev's drafts (degraded channel), and the body is the dev's verbatim.

Checklist

  • Form:
    • Draft → main, assignee os-sales.
    • The first line is Fixes #12701, and the second is Clause-②: yes, as the claim 6217878223 declares.
    • The base is 378896d, with origin/main merged in at 13a95b0. git merge-tree against current main c9e6742 is clean. 8 files, +689 / −53, no PNG on the branch.
  • Rulings, as bound:
    • Approvals, one read, one owner: useRecordApprovals keeps the read and its answer in one module-scoped store keyed by object + record, the shape hooks/sharedUserFeeds.ts already uses, without polling. No second store and no provider; the bell's shared feed is untouched. Every consumer joins the read in flight or is served the stored answer, the entry drops when the last consumer unmounts, and refresh() always reads.
    • Record, one fetch: record:details passes DetailView the new recordFromHost prop, so the body waits for the page's record and reads nothing. The page already expands every relation the viewer may read, so its list is the union; a definition that gains a relation reads once more with the full list (the objectui#11699 dependency), and that path is pinned. No parallel second fetch.
    • No behaviour change beyond the requests: the before/after network logs show the same page on a HotCRM lead and on an opportunity with a pending, record-locking request (decision bar, lock band, Recall, disabled Edit, details body). sys_activity is read once before and once after; it was not duplicated.
    • A host whose record read fails: checked by the seat on the branch. RecordDetailView returns its missing / forbidden / failed state before it mounts RecordContextProvider, so a body under recordFromHost never waits on a read that will not come. The binding host (recordBinding) and the page-designer preview supply data with recordId, or no recordId at all.
  • Mechanism assumptions:
    • 1 falsified (a good result): the three identical approvals reads came from the page's one hook, not three consumers. Two were StrictMode's replay of its mount effect, and the third was the skip-the-first-run invalidation effect walking through that replay. The nonce guard fixes the third; the store fixes the replay and the genuine multi-consumer case (an authored record:approvals), which a test pins.
    • 2 confirmed: the narrower second crm_lead read was record:details' DetailView, reading while ctx.data was still null.
    • 3 confirmed: useRecordApprovals is on hooks/index.ts and not on the app-shell entry, measured on current main.
  • Tests:
    • New pins: RecordDetailView.recordOpenRequests-12701.test.tsx (3; the real page, adapter and hook under StrictMode, with an authored record:approvals) and DetailView.recordFromHost-12701.test.tsx (3). The objectui#11699 pin now expects the body's own GET /data/OBJ/ID at 0, which is the read this card removes.
    • Six ablations, each red. A3 first stayed green because the fixture's page and body populate lists were equal; the dev widened the fixture in c085c47 (HotCRM's shape) and it went red. That is the right fix: the pin now counts what it claims to count.
    • Full @object-ui/app-shell suite as six sequential shards of the same file set: 1,471 files, 13,841 tests passed, 0 failed. @object-ui/plugin-detail in full: 2,666 passed, 0 failed. Both type-checks (with their test tsconfigs), the build of the dependency closure (29/29), eslint on touched files (0 errors), the i18n checks, line citations and changeset presence: all exit 0.
  • Changeset: @object-ui/app-shell and @object-ui/plugin-detail, patch, user-facing prose.
  • Contract faces: one optional prop on the exported DetailViewProps, covered by Clause-②: yes. The TIER_DEFAULT contract review follows this record.
  • Evidence: the card asks for no screenshot; the network logs are in the PR body.
  • CI: running at this record. Landing waits for green.

Rulings

  1. The populate union: A, as built. The page's expand list is the union by construction, because a record:details section reads the same object's relations under the same field security. The one-refetch path is the existing expand-list dependency, now pinned. A section-to-page registry (B) would be new @object-ui/react surface with no measured demand, and a second place for populate needs to drift.
  2. The contract conflict: A, as dispatched. The relay is refused from the dev's container, so the seat opened the PR and relayed the report.
  3. File surface: packages/plugin-detail/src/renderers/record-details.tsx is the one call site that passes the new prop, and the objectui#11699 test beside RecordDetailView had to change its count. Both are accepted as within the card's region.
  4. The suite as six shards: accepted, as on earlier cards. The container caps a single run.
  5. The test seam __resetRecordApprovalsStore is a module export only, on neither hooks/index.ts nor the package entry. Accepted.
  6. Out of scope, not filed:
    • The opportunity page's related-list previews read each relation with top=3 (rows) and top=1 (the filtered count). These are two different queries, not the duplicate this card removes.
    • In production builds without StrictMode, the approvals read was probably already one per open on synthesized pages. Not measured; the record's double read did not depend on StrictMode.
    • The HMR artifact after a git merge under a running dev server is not a code defect.

Implemented-by: cloud session session_01LbpjhSB18hFom4adrPRd4B, branch claude/issue-12701-record-page-one-fetch
Reviewed-by: seat domain:ui#4, session_01SJn1GEkrbbBQkhGC26wEDw


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 74 chunks) 3124.1 KB 3744.1 KB
Main entry chunk (gzip) 75.4 KB 350 KB
Entry file index-D3FsXt0M.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) 21.42KB 7.85KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
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) 20.39KB 6.02KB
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.62KB 0.83KB
auth (authStyles.js) 5.27KB 1.86KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
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.26KB 8.03KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (commentAuthorship.js) 1.39KB 0.78KB
collaboration (index.js) 1.93KB 0.85KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 600.27KB 144.97KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 248.96KB 69.45KB
fields (index.js) 290.81KB 74.35KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
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.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.67KB 11.85KB
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) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
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.04KB 3.92KB
plugin-calendar (index.js) 54.94KB 16.05KB
plugin-charts (index.js) 84.97KB 23.36KB
plugin-chatbot (index.js) 201.89KB 48.13KB
plugin-dashboard (index.js) 146.15KB 39.55KB
plugin-designer (index.js) 233.95KB 50.08KB
plugin-detail (index.js) 263.62KB 69.68KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 180.28KB 46.60KB
plugin-gantt (index.js) 178.36KB 45.08KB
plugin-grid (index.js) 270.35KB 74.86KB
plugin-kanban (index.js) 59.42KB 18.48KB
plugin-list (index.js) 132.94KB 34.07KB
plugin-map (index.js) 27.07KB 8.96KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 46.66KB 13.13KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 93.43KB 23.94KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
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) 120.23KB 39.46KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.81KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 1.13KB 0.65KB
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) 5.78KB 2.70KB
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 (node-slots.js) 7.18KB 2.34KB
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-approval-decision.js) 0.20KB 0.20KB
types (record-components.js) 0.20KB 0.19KB
types (record-panels.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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
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: TIER_DEFAULT
Head-sha: 13a95b0c558d806bcddc13c889517cc0bf3e1277
Local-runs: none

Face: the Clause-②: yes declaration of the claim 6217878223 on objectui#12701. The PR's file list carries no packages/spec/src/** path and no governed path. Inputs read: the card, the claim, the diff at the head above, the changeset and the PR body. Nothing was built or run locally.

① Derived judgments

  • DetailViewProps (exported from @object-ui/plugin-detail as a type, and DetailView as a value) gains one optional member, recordFromHost?: boolean, default false. A host that passes nothing reads its record as before; the third DetailView.recordFromHost-12701 test pins that path. That is an additive widening. Correct, and declared.
  • useRecordApprovals keeps its signature (objectName, recordId) => UseRecordApprovalsResult. It is exported from packages/app-shell/src/hooks/index.ts, and @object-ui/app-shell's src/index.ts does not name it (0 mentions on main c9e6742). The new __resetRecordApprovalsStore is a module export only. No package-entry change.
  • Behaviour: no authored key, view key or schema changes. The record page renders the same sections; only the requests change. Correct.

② Semver level

The changeset bumps @object-ui/app-shell and @object-ui/plugin-detail patch, in pre mode on tag next. An optional prop whose default keeps behaviour, and a request fix, fit patch. The repo's Changeset Bump Policy check rules on CI. Consistent.

③ Boundary flags

  • None to the maintainer.

Implemented-by: session_01LbpjhSB18hFom4adrPRd4B
Reviewed-by: session_01SJn1GEkrbbBQkhGC26wEDw

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Landing · head 13a95b0 · 2026-10-16T17:56Z

  • Head unchanged since the ACCEPT: 13a95b0.
  • Checks on 13a95b0: 36 runs, 33 green and 3 expected skips, none red. That includes Live E2E (informational), Build & E2E, all 16 test shards, Type Check, Lint, Changeset Bump Policy (the patch level holds) and Bundle Analysis at 3,124.1 KB against the 3,744.1 KB ceiling.
  • Merge state: clean against main c9e6742.
  • Contract review: TIER_DEFAULT PASS at this head (6221352844): one optional recordFromHost prop on the exported DetailViewProps; no app-shell entry change.
  • Action: mark it ready for review and enable auto-merge through the merge queue. Fixes #12701 closes the card on merge.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 16, 2026 17:57
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit e219a3d Oct 16, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12701-record-page-one-fetch branch October 16, 2026 18:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Record page: opening a record sends one approvals query and one record fetch, not three and two

2 participants