Skip to content

test: audit the nine recorder-wait sites — one repaired, eight measured sound - #8702

Closed
os-justin wants to merge 2 commits into
mainfrom
claude/issue-8690-recorder-wait-audit
Closed

test: audit the nine recorder-wait sites — one repaired, eight measured sound#8702
os-justin wants to merge 2 commits into
mainfrom
claude/issue-8690-recorder-wait-audit

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Part of objectui#8690 — the audit it asks for. Nine strict-shape sites read and classified: 1 repaired, 8 sound. Every verdict is a measurement, not an inspection: for each site the unfavourable ordering was forced on disk and the outcome observed.

Base: da5e4f69e (the card's own base, so its line numbers land unchanged).

The nine readings

site (line on da5e4f69e) shape verdict what makes it so
app-shell/…/PermissionMatrixEditor.packageDoorFacets.test.tsx:241 (and :191, same construction) waits server.saved, reads server.savedOpts[0] sound the two pushes are consecutive synchronous statements in one save double
plugin-charts/src/ObjectChart.optionColors.test.tsx:285 waits host.calls, reads host.inits[0] sound same construction, one fetch double
plugin-dashboard/…/DatasetWidget.relabel.test.tsx:206 waits host.calls, reads host.inits[0] sound same construction, one fetch double
plugin-designer/…/MetadataObjectsPage.lookupKeying.test.tsx:380 waits puts, reads the delete log — an absence REPAIRED the absence was dated to the FIRST write; a write after it is invisible
plugin-list/…/ListView.objectProviderBinding-7477.test.tsx:161, :191, :231 waits one renderer's props, asserts the other is empty — absence sound the anchor is post-settle: the view surface mounts only after loading flips false
plugin-list/…/ListView.timeline-binding.test.tsx:82 waits findCalls, reads captured sound captured is itself awaited two lines above; the detector only inspects the nearest wait
plugin-tree/…/ObjectTree.settledSchemaKeying-6481.test.tsx:169 waits findCalls, reads schemaRequests sound the request record is causally upstream of the query it gates

The one repair

MetadataObjectsPage.lookupKeying — the constructor round-trip case asserted "no delete was issued" right after waitFor(puts.length === 1). That covers the delete scan only because the scan happens to run before the save loop in handleObjectsChange — a property of the page, not one the file asserts. The pin now waits for the manager to receive the re-read list (reload() is the handler's last statement) and reads the delete log after that. label and not name, because a rename leaves the name unchanged: only the relabelled row is a value the pre-reload state cannot answer with. This is the anchor C0/H1/H2 in the same file already use.

Evidence (all three legs, from a committed tree, mutation proved on disk in both directions — anchor counts, git hash-object against the HEAD blob, and a line-total gate — restored by state with git diff HEAD empty):

  1. Leg 1 — the defect the old pin cannot see. Inject a stray client.reset('object', 'ghost_mut8690') at the end of handleObjectsChange, 50ms late (RTL's asyncWrapper drains one macrotask, so a 0ms deferral would sit inside its own window). Pre-repair pin: PASSES. The C0/H1/H2 cases in the same file, already anchored on the reload: RED (expected [ 'contact', 'ghost_mut8690' ] to deeply equal [ 'contact' ]).
  2. Leg 2 — the new pin under the same forced ordering: RED. expected [ 'ghost_mut8690', 'ghost_mut8690' ] to deeply equal [].
  3. Leg 3 — the new pin under the ordinary ordering: green (whole package, 182 tests with the audited files).

The control that separates a strengthening from a relocation (copied from PR #8689): under the identical mutation, the pre-repair pin restored from da5e4f69e passes. Provenance was checked by blob, not by eye — file on disk 76b074a1…, da5e4f69e blob 76b074a1…, shipped blob 53ea1edd…, and zero occurrences of the anchor comment in the restored copy.

And the reachable defect class is still caught: breaking the delete scan itself so it misses every name reds the repaired pin (expected [ 'constructor' ] to deeply equal []). Nothing was traded away.

Why the other eight were left alone

The three calls/inits-shaped pairs (app-shell, charts, dashboard). Both recorders are filled by one statement pair inside the same double, so no ordering exists in between — single-threaded, no interleaving point. Forced two ways:

  • a slow channel — the realistic production-side hazard: the double's response deferred 50ms with the pushes left co-located. All 15 tests stay green. The ordering is not reachable from anything the component does.
  • the only way to create it is to edit the double: split the pair with a 50ms gap and all four reads go RED (expected undefined to match object { mode: 'draft', packageId: 'app.a' }, and the same for both inits[0] reads). That is also the vacuity check the card warns about — these reads discriminate, they do not degrade to comparing undefined with undefined.

ListView.timeline-binding:82. The read's own recorder is awaited two lines above the flagged wait; the detector only looks at the nearest one. Measured: defer the spy's record by 50ms and the file stays green (17 passed) — the wait absorbs it; remove that wait under the same forced ordering and the read is undefined (TypeError: Cannot read properties of undefined (reading 'schema')). The guard is real and it is load-bearing.

ObjectTree.settledSchemaKeying-6481:169. A query for object X can only be issued after X's schema request was recorded — the record is the first statement of the getter whose promise gates the query. Defer that push by 50ms and the pin goes RED (expected [] to deeply equal [ 'business_unit', 'territory' ]), so it is non-vacuous and genuinely depends on the recorder being filled. The residual — a duplicate arriving after the read — was measured too, and it is not the detector's shape: a late duplicate is invisible on the WAITED recorder (findCalls, whose exact-equality assertion sits one line above) exactly as it is on the unwaited one. Rewriting the wait would change nothing.

The three objectProviderBinding-7477 absences. These are the kind objectui#8664 solved for ObjectTree.contractEnvelope-6839 by anchoring on the "No records" panel, which only renders after loading flips false. The anchor here already has that property, and it was measured rather than assumed: ListView renders list-loading while loading && data.length === 0, so the sibling renderer's first props arrive only after the load settles. Probe inserted at each of the three anchors — kanbanProps[0].loading / gridProps[0].loading — all false. The emptiness is therefore read on a settled render, not on an unrendered one.

Two forcing attempts, reported as measured:

  • VOID. Applying the author's kind late (currentView starts grid, flips 50ms after the load settles) was intended to make the grid mount before the anchor. It never mounted at all — a diagnostic probe waiting up to 3s for gridProps timed out. The wrong kind is replaced before the surface is ever shown, so the leg did not produce the scenario and proves nothing about the pin. Reported VOID, not green.
  • The residual, landed. A competing renderer forced to mount 50ms AFTER the settled one is invisible to the pin — the diagnostic probe confirms it really mounted, and the absence assertion passed anyway. In the shipped component that flip needs a user action on the view switcher, and sealing it would need a sleep in the test, which this repo's test discipline rules out. Recorded, not repaired.

The detector

Kept as scripts/census-recorder-wait-shape.mjs, re-derived from the card's four-step description. Not wired into CI, deliberately — it answers where to look, never what is wrong, and a gate on this list would institutionalise the batch repair the card exists to prevent.

Its header records what it measured rather than quoting the card as reproduced. On da5e4f69e, 2776 tracked test files: --recorder-match=ident gives 159 flags / 15 strict in 10 files, --recorder-match=path gives 167 / 18 in 12. The card reported 160 / 9. One flag off the total, wider in the strict bucket, and the two modes' strict buckets do not contain each other — ident misses a wait written as a member of a host object, path misses a recorder pushed bare and read through one. The card's nine sit inside the union.

The repair also removes its own flag from the census: MetadataObjectsPage.lookupKeying no longer appears in the strict list.

Scope

Test-only plus one repo script. packages/fields and packages/permissions are untouched (objectui#8434 and PR #8689 are live there). The 151 flags whose wait named no recorder were not re-litigated. The census sites outside the card's nine were not audited and not folded in — reported separately as a finding.

Verification

  • pnpm exec vitest run from the repo root with paths, per-test classification from the JSON reporter (--reporter=json), never the text one.
  • Whole touched package plus the seven audited files: 182 passed, 0 failed.
  • pnpm --filter @object-ui/plugin-designer type-check green, with the dependency closure built first (an unbuilt tree reports TS2307 for every workspace import — a precondition, not a result). tsc -p tsconfig.test.json --listFiles confirms the edited file is in the checked set, so this is a measurement and not an empty pass.
  • node scripts/check-changeset-presence.mjs, check-changeset-no-major.mjs, check-control-bytes.mjs, eslint on the new script: all green. The changeset declares an empty frontmatter — the real exemption; skip-changeset is a phantom label here.

Generated by Claude Code

…dler's reload

`expect(deletes).toEqual([])` was dated to the first PUT by `waitFor(puts)`.
That covers the delete scan only because the scan happens to run before the
save loop in `handleObjectsChange` — a property of the page, not one the file
asserts. Anchor it on `reload()`, the handler's last statement, so the
emptiness is read after every write the handler makes.

Measured: with a stray `reset` issued after the saves, the `puts` wait leaves
this assertion green while C0/H1/H2 in the same file — which already anchor on
the reload — go red.

objectui#8690

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
The detector answers where to look, never what is wrong, so it is a census
tool and NOT wired into CI: a gate on this list would institutionalise the
batch repair the card exists to prevent. Two recorder-matching modes, because
the choice moves the numbers, and the header records what each one measured on
the card's base commit rather than quoting the card's counts as reproduced.

Changeset declares an empty frontmatter: test-only plus one repo script,
nothing published moves.

objectui#8690

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

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3477.6 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-BkOatu4S.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) 15.67KB 5.75KB
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) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
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.41KB 1.23KB
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.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
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) 26.08KB 7.56KB
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) 499.42KB 114.32KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 198.39KB 55.29KB
fields (index.js) 244.36KB 61.61KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
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.39KB 3.10KB
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) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.53KB 46.34KB
plugin-dashboard (index.js) 131.43KB 34.44KB
plugin-designer (index.js) 215.51KB 44.29KB
plugin-detail (index.js) 251.25KB 65.00KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.18KB 56.62KB
plugin-kanban (index.js) 55.44KB 15.73KB
plugin-list (index.js) 112.73KB 27.69KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.54KB 20.84KB
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) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
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) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
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) 14.27KB 5.47KB
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

@os-justin
os-justin marked this pull request as ready for review September 8, 2026 23:27
@os-justin
os-justin enabled auto-merge September 8, 2026 23:27

Copy link
Copy Markdown
Collaborator Author

Closing: every file this PR delivers is already on main, and merging it now would deliver nothing but a conflict. Measured, not inferred:

1. All three files entered main in one commit — 28efdcb37, the squash of #8706. #8706 branched from this branch, so its squash carried this PR's commits with it:

$ git log --oneline --diff-filter=A origin/main -- scripts/census-recorder-wait-shape.mjs
28efdcb37 chore(scripts): record what the recorder-wait census's two matchers disagree about (#8706)
$ git log --oneline --diff-filter=A origin/main -- .changeset/issue-8690-recorder-wait-audit.md
28efdcb37 …(#8706)
$ git log --oneline -1 origin/main -- packages/plugin-designer/src/MetadataObjectsPage.lookupKeying.test.tsx
28efdcb37 …(#8706)

Two of the three are byte-identical to this branch, by blob hash rather than by eye:

file blob on main blob on this branch
.changeset/issue-8690-recorder-wait-audit.md 99df3700 99df3700 same
…/MetadataObjectsPage.lookupKeying.test.tsx 53ea1edd 53ea1edd same
scripts/census-recorder-wait-shape.mjs 3732191f 6a6906ac main's is later#8706's annotations (+62/−4)

2. Merging would conflict, not add. The script is absent at the merge base (da5e4f69e), so both sides add it:

$ git merge-tree --write-tree origin/main claude/issue-8690-recorder-wait-audit
CONFLICT (add/add): Merge conflict in scripts/census-recorder-wait-shape.mjs

Resolving that conflict has exactly one correct answer — take main's side, which is this branch's file plus #8706's annotations — leaving a PR whose merged diff is empty.

3. The red check is not this PR's. Test (shard 2/4) failed on:

TypeError: Cannot read properties of undefined (reading 'userId')
 ❯ packages/permissions/src/__tests__/providerCtxIdentity.discarded.test.tsx:619:41

That is the defect #8689 fixed — "wait on the array the assertion reads" — landed on main as 3fed306c3, after this branch's base da5e4f69e. Nothing on this branch touches packages/permissions/. Rebuilding this branch to clear a failure it did not cause, in order to merge a diff that is already merged, is work with no product at the end of it.

The audit itself — nine recorder-wait sites, one repaired, eight measured sound — is on main and stays there. The follow-up repair of the census matcher is #8719, stacked on this work; it is unaffected by this close, since what it stacks on is now main itself.


Generated by Claude Code

@os-justin os-justin closed this Sep 9, 2026
auto-merge was automatically disabled September 9, 2026 01:54

Pull request was closed

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.

2 participants