') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); fix(components,plugin-detail,plugin-view): read find() answers as QueryResult declares them — remove the seven surviving `records` arms by os-sales · Pull Request #6841 · objectstack-ai/objectui · GitHub
Skip to content

fix(components,plugin-detail,plugin-view): read find() answers as QueryResult declares them — remove the seven surviving records arms - #6841

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6726-records-tolerant-reader
Aug 30, 2026
Merged

fix(components,plugin-detail,plugin-view): read find() answers as QueryResult declares them — remove the seven surviving records arms#6841
os-sam merged 1 commit into
mainfrom
claude/issue-6726-records-tolerant-reader

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#6726

QueryResult (packages/types/src/data.ts) declares exactly one rows member — data — alongside total, page, pageSize, hasMore, cursor, metadata. records is not a member: it is the spelling the server envelope and the client SDK use, which ObjectStackAdapter.normalizeQueryResult maps to data before returning. #5945 removed it from two app-shell readers; this removes it from the seven more the same producer sweep turned up, one module at a time, each with its own pin.

QueryResult is not widened to bless records. That is a published-type change and a maintainer decision.

What changed, per module

modulearm removedpin
components/src/hooks/related-count-store.tsrecords tried beforedatacomponents/src/__tests__/related-count-store.contractEnvelope-6726.test.ts
components/src/renderers/basic/data-list.tsxres?.data ?? res?.records ?? bare-arrayrenderers/basic/__tests__/data-list.contractEnvelope-6726.test.tsx
components/src/renderers/basic/elements.tsxsame chain (element:number client-side aggregate)renderers/basic/__tests__/element-number.contractEnvelope-6726.test.tsx
components/src/renderers/basic/record-picker.tsxsame chainrenderers/basic/__tests__/record-picker.contractEnvelope-6726.test.tsx
plugin-detail/src/renderers/record-activity.tsxres?.data ?? res?.records ?? []renderers/__tests__/record-activity.contractEnvelope-6726.test.tsx
plugin-detail/src/renderers/record-history.tsxsamerenderers/__tests__/record-history.contractEnvelope-6726.test.tsx
plugin-view/src/ObjectView.tsxthe else if records branch of the unwrap ladder__tests__/ObjectView.contractEnvelope-6726.test.tsx

related-count-store.ts is the one that was actively wrong: it read records ahead of data, so a find() answer carrying both would have been counted from the key the contract does not declare. Its pin asserts that case directly ({ records: 3 rows, data: 1 row } counts 1, not 3). The other six read data first, so their records arm was dead — and a dead tolerant arm is still where a non-conforming producer keeps working unrejected (AGENTS.md #0.1).

One repeated shape, not seven different treatments. Every module got the same two steps #5945 used — measure, then delete and pin — and each pin has the same three legs where the module has them: the contract read, the live arms, the refusal. Nothing here argues for splitting the card.

The measurement, and its control

Repo-wide over every tracked file (git ls-files, 5,732 files — sources, tests, examples, skills/, scripts/, fixture data), for an object-literal records: key plus .records = assignment: 112 sites, zero of them a producer at the DataSource.find() seam.

Read directly, every find() implementation in the repo returns data or a bare array:

  • ObjectStackAdapter.normalizeQueryResult (data-objectstack/src/index.ts:3246) consumes the server/SDK records (and value) envelope and returns { data, total, page, pageSize, hasMore }.
  • ApiDataSource.normalizeQueryResult (core/src/adapters/ApiDataSource.ts:382) reads records off the raw HTTP payload and likewise emits data.
  • ValueDataSource, runner's MockDataSource, apps/site's gallery source, examples/byo-backend-console's mock, the gantt demo source and the @object-ui/types REST example all return data (or a bare array).
  • related-count-store.ts's own ProbeFn never declared records in the first place — its return type is { total?, data? } | unknown[] | { length? }. Only the res: any cast let the arm through.

Control for the zero (a zero counts only if a term that must hit is run in the same query and does): the same sweep DOES find every legitimate records producer, on other seams — ViewDataProvider.ts (12), its test (8), the client-SDK doubles below normalizeQueryResult (apiAccessDenied.test.ts, consoleUserPreferenceBudget.test.tsx, v3-compat.test.ts), the raw HTTP payload doubles (objectListApiDisabled-4408.test.tsx, packagedActions.test.ts), plus #5945's own two asRecords envelopes. 45+ hits. A producer at the find() seam would have been caught by the identical query.

A second control lives inside each pin: the data leg and (where the module has one) the bare-array leg push the same rows through the same mount and both render. So a records leg that renders nothing rendered nothing because the envelope was refused, not because the harness was dead.

Ablation — each pin red with the arm restored, green without it

Per module: restore the pre-fix expression, prove the mutation reached disk (the exact pre-fix text 0 → 1 and the post-fix text 1 → 0 — never an editor exit code), run that module's pin, restore with git checkout HEAD --, then prove the restore (git diff HEAD empty andgit hash-object equal to the HEAD blob). Every module passed both proofs.

moduleablatedgreen (this PR)
related-count-store2 failed / 3 passed (5)5 passed
data-list1 failed / 2 passed (3)3 passed
elements (element:number)1 failed / 2 passed (3)3 passed
record-picker1 failed / 2 passed (3)3 passed
record-activity1 failed / 1 passed (2)2 passed
record-history1 failed / 1 passed (2)2 passed
ObjectView1 failed / 2 passed (3)3 passed

In every case the reds are exactly the records assertions and the live-arm legs stay green — the pins discriminate the deleted arm, they do not merely fail. related-count-store reds twice because it is the only module with a precedence case to assert as well.

No rebuild leg is owed here, and here is why rather than an assumption: the root vitest.config.mts aliases every @object-ui/* specifier to packages/*/src (lines 268-284), and each pin imports its module under test by relative path. Nothing in these runs resolves through a dist/, so a stale build cannot make an ablation falsely green. The console was built for the two bundle-artifact gates below, and those read dist/ as intended.

Exclusions — confirmed, not assumed, and untouched

  • app-shell/src/console/marketplace/marketplaceApi.ts:354,403 — raw fetch() of Cloud's /api/v1/data/...; its own comment states the API returns { object, records, total, hasMore }. A raw HTTP payload, not a QueryResult. Correct as it stands.
  • app-shell/src/views/setup/packagedActions.ts:108readDataRecords reads the transport envelope's FindDataResponse = { object, records, total?, hasMore? }, wrapped or bare. Its header already says these are the two forms of one response, ⛔ not a tolerant alias. Correct as it stands.
  • core/src/data-scope/ViewDataProvider.tsDataFetcher.fetchRecords is declared Promise of { records, total } and ResolvedData declares records. Its own interface, its own seam. Correct as it stands.

What moved

Nothing. No existing test was edited, renamed or deleted; the seven pins are all new files. Full suites for the three touched packages: 354 files, 3,276 tests, 0 failures.

Verification — all on b2a4ca1c9, this branch's head

Run from the repo root, never pnpm --filter PKG test (objectui#3378 / #3288 — that re-roots vitest at the package and reports another package's files passing while running none of yours).

  • Pins, executed file names shown by --reporter=verbose: pnpm exec vitest run over the seven *.contractEnvelope-6726.* files — Test Files 7 passed (7) / Tests 21 passed (21).
  • Package suites: pnpm exec vitest run packages/plugin-view/ packages/plugin-detail/Test Files 141 passed (141) / Tests 1302 passed (1302); pnpm exec vitest run packages/components/Test Files 213 passed (213) / Tests 1974 passed (1974).
  • pnpm --filter @object-ui/components --filter @object-ui/plugin-detail --filter @object-ui/plugin-view run type-check — 3 of 47 projects, all Done. Each package's type-check is tsc --noEmit && tsc -p tsconfig.test.json, and --listFiles confirms all seven new test files are in those programs (4 / 2 / 1), so the green covers them rather than merely passing beside them.
  • pnpm lint (turbo run lint, eslint . --no-inline-config) — repo-wide, Tasks: 47 successful, 47 total, 0 errors. Not narrowed.
  • Gates: check:control-bytes (OK, 5,655 files), check:vi-mock-specifiers (OK — the ObjectView pin adds one), check:phantom-deps, check:self-import, check:side-effects-array, check:esm-specifiers, check:element-data-source-declaration, check:entry-guard, check:i18n-keys, check:i18n-drift, check:spec-symbols, check:action-forward-parity, check:designer-field-key-parity, check:icon-record-names, pnpm check — all exit 0.
  • Changeset guards: check-changeset-no-major.mjs (no major declared — the fixed-group rule), check-changeset-overwrite.mjs (1 added, 0 modified, 0 deleted).
  • Bundle-artifact gates, after turbo run build --filter='./packages/*' (39/39) and pnpm --filter @object-ui/console build: check:sdui-registration-pins — all 12 registrations present, 513 chunks weighed; check:eager-closure — 3174.0 KB / 3222.7 KB, 48.7 KB headroom, all per-chunk ceilings green.
  • Changed files self-scanned for raw control bytes: none.

Out of fence — filed, not repaired here

Neither is addressed by this PR, and neither is closed by it.

Generated by Claude Code


Generated by Claude Code

…ryResult declares them
Remove the `records` arm from the seven `DataSource.find()` consumers
objectui#5945 did not name, and pin the contract read per module.
`QueryResult` declares exactly one rows member, `data`. `records` is the
below-the-adapter spelling (server envelope + client SDK) that
`ObjectStackAdapter.normalizeQueryResult` maps to `data` before returning.
`related-count-store.ts` read it AHEAD of `data` — the precedence inversion
objectui#5945 was filed about; the other six read it behind `data`, where it
was dead but still legitimised a second de-facto contract.
Measured repo-wide: no producer emits `records` at this seam. The producers
that do exist are on other seams (ViewDataProvider's own `ResolvedData`, raw
Cloud HTTP payloads, client-SDK doubles below the adapter) and are untouched.
`QueryResult` is NOT widened.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3174.0 KB3222.7 KB
Main entry chunk (gzip)148.2 KB350 KB
Entry fileindex-mRwjtTyn.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)511.41KB116.30KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)240.93KB60.76KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.03KB32.64KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.57KB54.55KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

Sign up for freeto 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.

The same records-before-data tolerant reader survives in ~7 more find() consumers that #5945 did not name

2 participants

@os-sales@os-sam