Skip to content

fix(react): carry the 18 form-view spec keys the bridge silently dropped - #6365

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-5898-form-view-bridge-dropped-keys
Aug 25, 2026
Merged

fix(react): carry the 18 form-view spec keys the bridge silently dropped#6365
os-support-ai merged 1 commit into
mainfrom
claude/issue-5898-form-view-bridge-dropped-keys

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes #5898

The form-view bridge's docblock promises that "every serializable spec key is either mapped onto the object-form node or listed here with an explicit reason for being ignored — the bridge must never silently drop spec configuration (#2545)". Measured against the installed @objectstack/spec 17.2.0, that promise was false. Seventeen keys now reach the node; the eighteenth is an explained refusal, which is what "not silently" asks for.


1. The key census, re-derived — both directions

⚠️ The card's figure of 18 was not confirmed by the PM and was not inherited. Method below; it reproduces 18 exactly, and the per-key split matches the card's too.

Method. The contract's key set is read from the runtime Zod shape of the three schemas, not from a hand-list and not from keyof (which cannot distinguish a live key from a retiredKey() tombstone — z.never().optional() still shows up as a key in the type). FormSectionSchema and FormFieldSchema close with .transform(), so they are ZodPipes whose authoring shape lives on .in.shape; FormViewSchema closes with .superRefine() and stays a ZodObject with .shape. Reading .shape alone answers undefined for two of the three — silently — so the helper handles both and throws rather than returning an empty set. Tombstones are filtered by unwrapping optional → never, and that filter is itself pinned in both directions (below).

The comparison side is the bridge's actual output, not its declaration: a spec-valid fixture carrying every live key at every level is fed to SpecBridge.transformFormView, and each key is asked where it landed. Declarations were the proxy the old check used; output is the thing the promise is about.

Forward direction — contract → bridge (spec 17.2.0):

Level Live contract keys Retired tombstones Neither mapped nor explained (before)
FormViewSchema 23 2 (aria, defaultSort) 2buttons, defaults
FormSectionSchema 10 0 2pane, visibleOn
FormFieldSchema 29 0 14publicPicker, maxLength, minLength, min, max, precision, scale, multiple, immutable, span, language, keyField, disclosure, fields
18 total

Reverse direction — bridge → contract: 0. Every key the bridge reads (21 on the view, 8 on the section, 15 on the field, before this change) is a key the contract declares. There is no invented vocabulary here and nothing stale: the "no stale rows" half of the new gate asserts it mechanically, at all three levels, and it passes.

Where the card was wrong, and it changed the repair. The card suspected buttons / defaults might have no node slot, in which case "the honest fix is an entry in the ignore list". Measured, the opposite is true: ObjectFormSchema declares both slots, ObjectForm's foldButtonsAndDefaults reads them at render, and the spec's own descriptions name ObjectUI's ObjectForm as the consumer the keys exist for (framework#1894 / #2998). An ignore entry would have documented a drop that had a live destination three lines away.


2. Which existing test could have caught this, and why it did not — measured

FormViewSpecConformance.test.ts is the file that exists to enforce #2545, and it is the one that missed it. Not because an assertion was weak, but because the key set came from the wrong source:

for (const key of Object.keys(FULL_SPEC_FORM_VIEW)) {   // ← the FIXTURE, hand-listed
  if (key in IGNORED_SPEC_KEYS) continue;
  expect(node[key], `spec key "${key}" was silently dropped`).toBeDefined();
}

A key absent from the fixture is a key the loop never asks about, so the check could only ever confirm what its author already remembered. This is exactly the degenerate case the card describes: the fixture was assembled from keys the bridge already carried, so it passed identically before and after.

Measured, not asserted: the pre-existing pair (origin/main's test + origin/main's bridge) was run as a control — Test Files 7 passed (7) / Tests 125 passed (125), exit 0 — while 18 contract keys were being dropped.

The other two neighbours are innocent and were left untouched: FormViewRetiredKeys.test.ts pins the absence of aria / defaultSort (a different promise, and it holds), and FormViewWidenedArms.test.ts pins three key types end-to-end (columns, dependsOn, visibleWhen) — none of the 18 is one of them. Nothing was rewritten to match the code; the completeness loop was re-sourced so it is structurally unable to skip a key, and every pre-existing behavioural assertion in the file survives.


3. Per-key decisions — map it, or explain it

Nothing was mapped to a destination that had to be guessed.

Key Decision Destination / reason
FormView.buttons map ObjectFormSchema.buttons; ObjectForm folds it onto showSubmit/submitText/showCancel/cancelText/showReset at render
FormView.defaults map ObjectFormSchema.defaults; ObjectForm folds it into create-mode initialValues
FormSection.pane map ObjectFormSection.paneObjectForm's split branch → SplitForm's paneOf. Dropped, every spec-authored placement fell back to the legacy positional rule, so reordering sections moved them across the divider — the exact failure pane was added to prevent
FormSection.visibleOn map (fold) Folded onto visibleWhen, reproducing the contract's own FormSectionSchema.transform(normalizeVisibleWhen). Not a new lenient arm: it is a declared (deprecated) spec key, and the fold matches what the field path directly above already does — for the same never-parsed input class the bridge still reads groups for. Canonical wins when both are authored, same precedence as the contract
13 × FormFieldmaxLength, minLength, min, max, precision, scale, multiple, immutable, span, language, keyField, disclosure, fields map Same-name copies onto the runtime FormField, at the destinations normalizeSectionField already pins by name in plugin-form/src/sectionFields.spec-parity.test.ts. fields travels verbatim in the spec vocabulary (field, not name) because the runtime slot is a pass-through whose pinned row asserts { field: 'inner' } survives unrewritten — recursing it through mapField would rewrite the sub-field identity key
FormField.publicPicker explain A server-side authorization opt-in, not a presentation delta: it gates GET /forms/:slug/lookup/:field (403 LOOKUP_NOT_PUBLIC without it) and the public-form resolve route strips undeclared lookup fields before any renderer sees them. Zero read points in this repo. Carrying it would invent a client-side meaning for a capability only the server enforces — the same reasoned exemption the downstream chokepoint already records, on the same delegated ruling (objectui#4648 item 5, 2026-08-15)

Two details that would otherwise be silent bugs of their own: every new copy tests != null rather than truthiness (min: 0, precision: 0, multiple: false, immutable: false are all authored decisions, and a truthiness test would drop them exactly as the missing declaration did), and every new declaration binds its type to the contract (FormFieldInput['maxLength'], FormView['buttons'], …) rather than restating it, per this file's derivation policy.

No accept set moves. Every key here is already declared by @objectstack/spec and already read by a declared slot on ObjectFormSchema / ObjectFormSection / the runtime FormField. This is declared-≠-enforced repair; no new public surface was minted.


4. The new gate

The completeness loop now derives its key set from the contract's own shape at all three levels. Every key must be claimed by exactly one registry — a behavioural row (an assertion that the authored value arrives at its documented destination) or a reasoned ignore entry — and both directions are asserted: a key the spec adds fails as unclaimed, a key the spec retires fails as stale. The tombstone filter is pinned in both directions too, so it can neither shrink the census silently nor demand a mapping for a key the contract refuses.

The fixture is asserted spec-valid (FormViewSchema.safeParse) before any row runs — without that control a row could pass against metadata no author could publish. type: 'split' is load-bearing: section.pane is split-only vocabulary and the contract rejects it on any other form type, so a wizard fixture could not carry the key at all, which is the degenerate-fixture failure in its purest form. The deprecated visibility spellings get their own fixture, because the full one authors the canonical spelling beside them and that wins — a row asserted against it would pass whether or not the fallback exists.


5. Ablation — prediction first, then the measurement

Predicted before running: reverting bridges/form-view.ts to origin/main while keeping the new test turns exactly 17 behavioural rows red — 2 view (buttons, defaults), 2 section (pane, visibleOn), 13 field — and 0 census assertions, because the registries live in the test and are complete either way. The 18th key, publicPicker, is the explained refusal, so its row stays green.

Actual: Tests 17 failed | 181 passed (198), vitest exit 1. The 17 failing rows are exactly the predicted list, key for key. Zero census failures. publicPicker green. Prediction and measurement agreed in direction, count and identity; nothing was wrong.

Ablation hygiene, since a green ablation is the failure mode that reads like success:

  • Mutation confirmed on disk before any measurement, never from an editor's exit code: HEAD blob 21d47691 vs post-mutation blob 5a70ee02, plus zero-hit greps for each injected marker (objectui#5898, 'buttons',, mapped.pane, mapped.maxLength). The script aborts if the file comes back byte-identical to HEAD.
  • No rebuild step, and that is a property of the setup rather than a shortcut: the root vitest config aliases every @object-ui/* specifier to the package's src/, and this suite imports the bridge by relative path, so the run compiles the mutated source directly — there is no dist to go stale. (The tsc half is different and does read built .d.ts; its dependency closure was built first — pnpm --filter '@object-ui/react^...' build, exit 0.)
  • Restore proven by state, not by exit code, and pinned to HEAD rather than a bare checkout (which reads from the index): git diff HEAD empty, git status clean, worktree blob 21d47691 == HEAD:…/form-view.ts. The script carries a trap … EXIT INT TERM with absolute paths as the crash-path convenience; the blob comparison is the proof.

6. Gates — verdict lines as printed, exit codes captured before any pipe

Gate set derived from the CI job step lists under .github/workflows/ (ci.yml, lint.yml, changeset-presence.yml, changeset-guard.yml, control-bytes.yml) and narrowed to what a TypeScript-only change under packages/react/src/spec-bridge/ can reach. All run at 216e1357, the final commit, on a tree verified clean (git status --porcelain empty).

Gate Exit The gate's own verdict line
pnpm --filter @object-ui/react run type-check (tsc --noEmit && tsc -p tsconfig.test.json) 0 (silent on success; measured non-vacuous below)
tsc -p tsconfig.test.json --listFiles 0 FormViewSpecConformance.test.ts in program: 1 · bridges/form-view.ts in program: 1
pnpm exec vitest run packages/react/ 0 Test Files 55 passed (55) / Tests 817 passed (817)
pnpm exec vitest run packages/react/src/spec-bridge/ 0 Test Files 7 passed (7) / Tests 198 passed (198)
pnpm exec eslint (changed files) 0 files linted = 2 | errors = 0 | warnings = 16
pnpm check:spec-symbols 0 ✅ spec symbol derivation: 1306 files scanned against 4959 spec export names / ✅ spec alignment claims: 2 declared deliberate copies
node scripts/check-changeset-presence.mjs 0 ✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
node scripts/check-changeset-no-major.mjs 0 ✅ No changeset declares a 'major' bump.
node scripts/check-control-bytes.mjs 0 ✅ check-control-bytes: OK (scanned 5238 tracked text file(s); skipped 85 binary).
node scripts/check-lint-coverage.mjs 0 ✅ lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).
node scripts/check-type-check-coverage.mjs 0 ✅ type-check coverage: 45/46 via 'type-check' / ✅ test type-check coverage: 41/41 packages compile their tests
pnpm check:phantom-deps 0 ✅ Every in-scope import is declared by the package that publishes it.
pnpm check:self-import 0 ✅ No package names itself inside its own src/.
pnpm check:vi-mock-specifiers 0 ✅ check-vi-mock-specifiers: OK (3754 tracked source file(s) …)
pnpm check:i18n-keys 0 Every in-scope call-site key resolves against the en pack (2824 keys) …

Two things stated rather than left to inference:

  • type-check is measured, not merely silent. The package's tsconfig.json excludes **/*.test.ts, so "typecheck clean" would have said nothing about the new test file. tsconfig.test.json (chained from the same script) is what compiles it, and --listFiles confirms both edited files are program inputs — 1 hit each, not 0.
  • Lint was narrowed, and the narrowing is declared. pnpm lint is turbo run lint → per-package eslint .; only packages/react is touched, and eslint here is not type-aware (no parserOptions.project / projectService anywhere in eslint.config.js), so this diff cannot move the verdict on any file it does not contain. Population and count come from eslint's own --format json output: 2 files, 0 errors, 16 warnings — against a 13-warning baseline measured on the same two files at origin/main. All 16 are @typescript-eslint/no-explicit-any, a rule configured as a warning; the package script is a bare eslint . with no --max-warnings, so it is not gated. The full-farm run is CI's.

Out-of-scope observations from this sweep are recorded in the report to PM rather than fixed here; nothing outside packages/react/src/spec-bridge/ is touched.


Generated by Claude Code

`spec-bridge/bridges/form-view.ts` states in its own docblock that "every
serializable spec key is either mapped onto the `object-form` node or listed
here with an explicit reason for being ignored — the bridge must never silently
drop spec configuration (#2545)". Measured against `@objectstack/spec` 17.2.0
that was false for 18 keys: 2 on `FormViewSchema`, 2 on `FormSectionSchema`,
14 on `FormFieldSchema`.

Seventeen have a real destination the receiving layer already reads, so they are
mapped; the eighteenth is an explained refusal, which is what "not silently"
asks for:

  buttons / defaults      -> `ObjectFormSchema` declares both slots and
                             `ObjectForm` folds them at render.
  section.pane            -> `ObjectFormSection.pane`, read by `SplitForm`'s
                             `paneOf`; dropped, every authored placement fell
                             back to the positional rule.
  section.visibleOn       -> folded onto `visibleWhen`, reproducing the
                             contract's own `normalizeVisibleWhen` for the
                             never-parsed input class this bridge serves.
  13 field keys           -> same-name copies onto the runtime FormField, at the
                             destinations `normalizeSectionField` already pins.
  field.publicPicker      -> NOT carried: a server-side public-lookup
                             authorization opt-in with no client destination.

The conformance test is why it stayed green: its completeness loop iterated
`Object.keys(FIXTURE)`, so a key nobody remembered to write into the fixture was
a key it never asked about. Its key set is now derived from the contract's own
shape at all three levels, every key must be claimed by a behavioral row or a
reasoned ignore entry, and the fixture is asserted spec-valid before any row
runs.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 52 chunks) 3223.5 KB 3266.6 KB
Main entry chunk (gzip) 154.2 KB 350 KB
Entry file index-DWnMnNsv.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) 10.96KB 4.16KB
app-shell (runtime-config.js) 18.10KB 6.51KB
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) 5.13KB 2.35KB
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) 505.89KB 114.59KB
core (index.js) 5.30KB 2.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 171.74KB 47.48KB
fields (index.js) 238.89KB 60.02KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.95KB 10.97KB
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.55KB 0.62KB
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 (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.62KB 12.83KB
plugin-charts (index.js) 64.66KB 18.32KB
plugin-chatbot (index.js) 188.21KB 44.67KB
plugin-dashboard (index.js) 133.35KB 34.45KB
plugin-designer (index.js) 211.95KB 42.75KB
plugin-detail (index.js) 245.10KB 62.31KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 127.52KB 31.01KB
plugin-gantt (index.js) 164.14KB 39.87KB
plugin-grid (index.js) 201.21KB 54.43KB
plugin-kanban (index.js) 52.83KB 14.55KB
plugin-list (index.js) 111.94KB 27.24KB
plugin-map (index.js) 20.09KB 6.62KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.49KB 11.93KB
plugin-timeline (index.js) 26.70KB 7.69KB
plugin-tree (index.js) 9.26KB 3.13KB
plugin-view (index.js) 84.55KB 20.74KB
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) 54.84KB 18.43KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.35KB 0.70KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 12.13KB 3.65KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 7.54KB 2.63KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
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.49KB 2.14KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
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 (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
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-support-ai
os-support-ai marked this pull request as ready for review August 25, 2026 16:20
@os-support-ai
os-support-ai added this pull request to the merge queue Aug 25, 2026
Merged via the queue into main with commit f53a8d0 Aug 25, 2026
28 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-5898-form-view-bridge-dropped-keys branch August 25, 2026 16:32
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.

finding(react): the form-view bridge's #2545 "never silently drop spec configuration" promise is false — 18 spec keys are neither mapped nor explained

2 participants