Skip to content

test(console): resolve the binding-reach probe's reading-'map' error cards as a fixture defect, and guard render on both branches - #5943

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-3840-binding-reach-form-crash
Aug 24, 2026
Merged

test(console): resolve the binding-reach probe's reading-'map' error cards as a fixture defect, and guard render on both branches#5943
os-warren merged 2 commits into
mainfrom
claude/issue-3840-binding-reach-form-crash

Conversation

@os-warren

@os-warrenos-warren commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Fixes#3840

The reading 'map' error card that object-form and object-master-detail-form paint under the binding-reach probe is a fixture defect (a), not a product defect (b). No renderer is touched.

How the fork was resolved

Stopping at "the crash went away when I changed the sample" does not discriminate — that is true under both readings. So the discriminating step was the input's spec shape:

  1. Bisected the generated schema. Printing what sampleFor builds and dropping inputs one at a time isolates sections. The stack names the read exactly — ObjectForm.tsx:1166, inside SimpleObjectForm:

    section.fields.map(f=>[typeoff==='string' ? f : ((fasany).field??f.name),f]),

    The generic array sample is the bare string ['name'], so section is a string and section.fields is undefined.

  2. Read whether the spec permits that value — the step that decides it. It does not:

    sourceverdict on sections: ['name']
    @objectstack/specFormViewSchema.sectionsREJECT"Invalid input: expected object, received string"; and [{}]"0.fields: expected array, received undefined"
    packages/types/src/objectql.tsObjectFormSectionfields: (string | FormField)[]required, on an object entry
    every renderer path (:242:266:293:325:355:1166)reads .fields / .name / .label off each entry

    So ['name'] is not metadata any author could publish — publish-time validation rejects it. The fixture was handing the form family a plausible value that is not a plausible configuration, which is the lesson this file already counted four times.

The block-props overlay (ComponentPropsMap['object-form'].sections) is z.array(z.unknown()) and accepts [42], [null], anything — worth stating because it is the schema nearest to hand and it cannot answer this question. FormViewSchema is the one that carries the authoring contract.

object-master-detail-form no longer crashed on today's main — but only by accident

The card's premise is half stale, and the stale half is the more interesting one. That block declares formType as a bare string (not the enum object-form declares), so the generic sample was 'x' — and the crashing loop is gated on ObjectForm.tsx:1134:

if(effectiveSections?.length&&(!schema.formType||schema.formType==='simple')){

'x' skips it. Measured: with sections left malformed, forcing formType: 'simple' reproduces the identical reading 'map' crash. The block was green for a reason unrelated to its correctness, so its formType sample is corrected too — a value outside a prop's vocabulary routes around the block instead of exercising it. Filed separately as #5939.

What changed — one file, plus the changeset

  • sampleFor gets two name-keyed samples: sections[{ name, label, fields: ['name'] }], and formType'simple'. Keyed by name because the type carries no information here (sections and fields are both array; only one is an array of objects).
  • The lesson list gets its fifth and sixth entries, with the spec quotes inline so the next reader can check the claim instead of trusting it, and the file header now points at the running list and its count.
  • The crash guard is extracted as assertRendered and moved ahead of the branch split, covering all 14 candidates — the shape the sibling probe record-block-record-reach.test.tsx has had from the start. The in-code comment that deferred this to [finding] object-formobject-master-detail-form 在 binding-reach 探针的自动 fixture 下渲染成错误卡(reading 'map')—— 断言仍成立,但两个分支的崩溃守卫因此没法一起开 #3840 is gone, replaced by the reasoning for why a crash mis-reads on both branches (on the ledgered branch it confirms the ledger entry; on the other it points at wiring that is fine).

Reverse-verification — the guard is load-bearing

Direction predicted before running: reverting only the sections sample should redden exactly two tests, via the new guard rather than the reach assertion. Observed exactly that:

× object-form asks the data layer for its objectName
× object-master-detail-form asks the data layer for its objectName
AssertionError: object-form threw during render — that is a crash, not a binding verdict
Component "object-form" failed to render / Cannot read properties of undefined (reading 'map')
Tests 2 failed | 14 passed (16)

(The assertion message wraps the block name in angle brackets; they are stripped here because GitHub's body sanitizer eats short angle-bracket fragments.)

Both failures come from assertRendered, not from the reach assertion — which confirms the card's "no false green today" claim: those blocks were always earning their data-reach verdicts, and it is the crash that was invisible. The mutation was confirmed on disk before the run (probe_section occurrences 1 → 0, marker present, non-empty git diff --stat) and restored by an EXIT/INT/TERM trap.

Gates — by name, at 58dda7a (the pushed commit; tree clean)

gateverdict
vitest run apps/console/src/__tests__/Test Files 19 passed (19) / Tests 242 passed (242)
@object-ui/console type-checkexit 0 (tsc --noEmit && tsc -b tsconfig.node.json --force; script name echoed, so not a zero-match pass)
@object-ui/console lintexit 0 — 171 files, 0 errors, 202 pre-existing warnings; probe file 0 errors / 5 warnings, all pre-existing (0 occurrences of any in the added lines)
check-changeset-presence.mjsexit 0 — "declares 1 changeset(s) … Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption"
check-changeset-fixed.mjsexit 0 — "All workspace packages are in the changeset fixed group."
check-changeset-no-major.mjsexit 0 — "No changeset declares a major bump."
check-control-bytes.mjsexit 0 — "scanned 4929 tracked text file(s); skipped 85 binary"

Every exit code was captured before any pipe. The console's dependency closure was built first (pnpm --filter '@object-ui/console^...' build) — without it type-check reports 40+ phantom TS2882 module-resolution errors.

Declared narrowing: lint was run for @object-ui/console rather than repo-wide turbo run lint. The narrowing is measured, not assumed: the surveyed population comes from eslint's own config resolution (eslint . in that package), the count is read from --format json (171 files), and eslint.config.js enables no type-aware linting (no projectService, no parserOptions.project) — so a diff confined to one file in this package cannot move any untouched file's verdict. CI runs the full farm regardless.

Scope

One test file and one changeset. No renderer, no apps/console/src/sdui-workbench-preview.tsx, no packages/app-shell/src/views/** — the fork resolved to (a), so the conditional renderer half of the file surface never opened, and nothing here reaches #5458's files.

Two out-of-scope findings were filed unassigned and both remain open, addressed by no change in this PR: #5939 (the formType declaration divergence above) and #5940 (object-master-detail-form calls getObjectSchema(undefined) for a detail collection it never resolved).


Generated by Claude Code

…ample and guard render on both branches
The two blocks that painted a `reading 'map'` error card under this probe were
a FIXTURE defect, not a product one. Resolved by measurement rather than by
"the crash went away when I changed the sample", which is true either way:
- Bisected to the `sections` input. `ObjectForm.tsx:1166` reads
`section.fields.map(...)` off each entry; the generic `array` sample is the
bare string `['name']`, so `section.fields` is `undefined`.
- Read the spec shape for that input, which is the step that discriminates:
`FormViewSchema.sections` REJECTS `['name']` at parse ("expected object,
received string") and requires `fields` on every entry. `ObjectFormSection`
in this repo agrees — `fields` is required. So no author could publish the
configuration the fixture was handing the form family.
`object-master-detail-form` no longer crashes on today's main, but only by
accident: it declares `formType` as a bare `string`, so the generic sample was
`'x'`, which skips the gate at `ObjectForm.tsx:1134`
(`!schema.formType || schema.formType === 'simple'`) that guards the crashing
loop. Forcing `formType: 'simple'` reproduces the identical crash, so that
sample is corrected too — a value outside a prop's vocabulary routes around the
block instead of exercising it.
Both are recorded as the fifth and sixth instances of the lesson this file
counts (a plausible value for every input is not a plausible configuration),
with the spec quotes inline so the next reader can check rather than trust.
With the blocks green, the crash guard moves ahead of the branch split as
`assertRendered`, matching the sibling probe `record-block-record-reach.test.tsx`
and covering all 14 candidates: a crash is not a binding verdict on either
branch.
…asing nothing
Test-only: the probe's fixture samples and its render guard. No renderer,
no published surface, so the frontmatter is empty by design rather than
scored — the explicit exemption this repo's changeset-presence gate names.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3230.7 KB3990.2 KB
Main entry chunk (gzip)153.6 KB350 KB
Entry fileindex-BL8yIXhK.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)10.13KB3.77KB
app-shell (runtime-config.js)13.57KB4.78KB
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)504.18KB114.10KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)164.55KB45.67KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
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.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.32KB34.42KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)243.38KB61.72KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.86KB27.22KB
plugin-map (index.js)20.06KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.57KB20.74KB
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)3.77KB1.33KB
react (SchemaRenderer.js)52.40KB17.45KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
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)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
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)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.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 (index.js)3.88KB1.85KB
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

Labels

Projects

None yet

2 participants

@os-warren@claude