refactor(plugin-form): retire the form-view section className/gridClassName reads - #7198

Merged
os-support-ai merged 3 commits into
mainfrom
claude/issue-13626-formview-classname-reads
Sep 1, 2026
Merged

refactor(plugin-form): retire the form-view section className/gridClassName reads#7198
os-support-ai merged 3 commits into
mainfrom
claude/issue-13626-formview-classname-reads

Conversation

@os-support-ai

@os-support-aios-support-ai commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Part of objectstack-ai/objectstack#13626

Executes the maintainer ruling of 2026-09-01 (director decision batch C, verbatim 「同意」): retire the reads.

className / gridClassName on the form-view section family sit on the SDUI-only side of the authorable boundary — @objectstack/spec deliberately does not declare either on the form-view/section surface (its component.zod.ts says so in as many words) and the authorable-surface ledger carries no entry for them. The renderer nevertheless reached them off the parsed view through as any at seven sites: the boundary declared on one side and crossed on the other, with the two repos each deliberate and in opposite directions.

Declaring the keys was weighed and not adopted: it would formally invite free Tailwind strings into authored metadata, the exact class the boundary exists to keep out — and per ADR-0065 / ADR-0080 (rev. 2026-06-30) utility classNames in runtime metadata are never scanned by the build-time Tailwind, so they silently produce no CSS anyway.

Clause-②: no — no contract-schema accept/reject change. Nothing in @objectstack/spec moves; this PR only stops the renderer reading two keys the spec already declines to declare, so no parse verdict changes in either direction. The path limb does not apply (objectui repo).

Authored by Claude Code, session https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1 (recorded in prose because a body edit rewrites the footer link).

① Census first — the ruling's own gate

The ruling ordered a census before any narrowing, with heavy usage being evidence back to the card rather than a change. Measured across all three corpora, zero authored uses:

CorpusCommitclassName/gridClassName on form-view section nodesLiveness control (columns: in the same section literals)
objectstacka7002ce50115
objectui899730e014
hotcrma6be39a072

Method. A structural walker bracket-matches each sections: array literal and reports the two keys inside it, so the count is scoped to the form-view section family rather than to the string className. The identical script ran against all three corpora.

How authored metadata was told apart from React props — the distinction the ruling asked for:

  • Object-property syntax (className:) counts; JSX attribute syntax (className=) does not.
  • Every candidate was then classified by the node it sits on. All non-section hits resolved to something else: page-component (UIComponent / PageComponent) trees, which have their own declared ledger entry ui/PageComponent:className; component prop declarations in sdui.manifest.json; buttonVariants({ className }) cva calls in hotcrm's docs site; and two prose comments in objectstack examples that warn against authoring the key.
  • objectstack: 21 JSON/YAML hits and 7 object-property hits, none on a form-view section. hotcrm: 2 hits, both React cva props; 0 in its 167-file src/ metadata tree. objectui: gridClassName has zero authored occurrences repo-wide — every hit is a component prop declaration, renderer plumbing, or prose.

Why the zeros are readings and not absences. Each corpus carries a positive control: the same walker finds 115 / 14 / 72 columns: keys inside the very same section literals, so it demonstrably reaches section nodes. hotcrm is the strongest case — 14 view files author form sections, the exact node type in question, and none of them carries either style key.

② The retired reads

Seven cast sites, re-derived on origin/main at dispatch (line numbers had drifted from the triage comment):

FileSites
packages/plugin-form/src/ObjectForm.tsxtabbed 293/294, wizard 325/326, split 359/360, drawer 392, modal 423/424, stacked section-divider 1293
packages/plugin-form/src/DrawerForm.tsxits own section-divider 598

Behaviour after the change is the natural result of the existing code path — the key is not read, so it does not take effect. FormSection already falls back to gridCols[columns] when gridClassName is absent, so section layout is unchanged. No existing test asserted the styles flow through, so none needed updating (all 821 pass unchanged).

The pin is behavioural, and that is load-bearing

⚠️ The retired reads did not actually need their casts. ObjectFormSection (this repo's own @object-ui/types) still declares both keys, so a later "cleanup" writing a plain className: s.className — no as any in sight — type-checks and silently restores consumption. A source grep for as any would stay green through exactly the regression the pin exists to catch.

packages/plugin-form/src/__tests__/sectionStyleKeysRetired-13626.test.tsx therefore authors the keys and asserts the strings never reach the DOM, across all seven arms, each row waiting on the section's own label first so that "absent" is a verdict rather than a form that failed to render.

Reverse verification (direction predicted before running)

Restoring the tabbed arm's two reads turned exactly one row red — the TabbedForm row — in the predicted PRESENT direction, with the other seven green. The assertion that fired was the sentinel-absence check reporting that the rendered markup DID contain os13626-authored-grid-class: under the mutation the authored grid class reappears as the class attribute of the wrapper element around the field grid. That is also a precise record of the pre-change behaviour this PR removes.

The mutation was proven on disk before the run (injected marker = 1, restored cast read = 1, worktree blob 13197ac0 differs from HEAD blob 777bbed4), and the restore leg proven after it (git diff HEAD empty, worktree blob back to 777bbed4, 0 residual markers).

③ Changeset and semver

minor on @object-ui/plugin-form, with the breaking semantics spelled out in the body — this repo's version-alignment rule reserves major for the objectstack cross-major sync and labels its own breaking changes minor.

No ADR-0087 disposition: no declared surface moves. The keys were never on the authorable-surface ledger, which is the whole premise of the finding; check:changeset-presence and check:changeset-no-major both pass.

Scope boundaries held

  • objectstack is untouched — the ledger and the "deliberately NOT declared" note in component.zod.ts were already correct.
  • No declaration added, no schema changed. Route (b) "declare them" and route (c) "write the status quo down" were both ruled out.
  • The form ROOT className (ObjectFormSchema.className, read as a plain uncast schema.className and forwarded to the form wrapper) is a different key on a different node, was not part of the ruling, and is deliberately untouched and unpinned.

Docs

The README stated the renderer reads a section className "explicitly on exactly one pseudo-field, type: 'section-divider'" — the read this PR retires, so the sentence was false as of this change. Corrected in place, plus a new Section styling is not authorable section recording the boundary, why declaring was rejected, and what still works. The plugin-form.mdxclassName rows are about field-level and form-root className and remain true, so they are left alone.

Verification

All on final head 4174960, after git merge origin/main (which brought packages/components changes, so the dependency closure was rebuilt before re-running):

CheckResult
pnpm exec vitest run packages/plugin-form/PASS — 81 files, 821 passed, 8 skipped
the new pin alonePASS — 8/8
pnpm --filter @object-ui/plugin-form type-checkPASS — exit 0 (tsc --noEmit plus tsc -p tsconfig.test.json)
pnpm --filter @object-ui/plugin-form lintPASS — 0 errors (751 pre-existing warnings; this PR removes 7 as any casts)
check:changeset-presence / check:changeset-no-majorPASS / PASS
check:control-bytesPASS — 5955 files scanned
check:doc-fencesPASS
check:governed-queue-guard --test PATHSNOT GOVERNED — ordinary PR

Notes on measurement honesty:

  • The three edited files were confirmed present in the typecheck program via tsc --listFiles, so "typecheck clean" actually covers the new test rather than excluding it.
  • check:readme-exports exits 1 locally for a whole-repo build precondition (dist/index.d.ts missing for ~30 packages this PR never touches, plus the derived floor packagesRead: found 12, floor is 25). Isolated rather than assumed: building plugin-form alone took its README failures from 58 to 0. CI builds before linting.
  • Lint is the affected package's complete eslint . job — the same unit turbo run lint runs — not a narrowed subset. Type-aware linting is off (no parserOptions.project), so this diff cannot move verdicts on untouched files.

…ssName reads
The renderer reached `className` / `gridClassName` off the parsed form view
through `as any` at seven sites, while `@objectstack/spec` deliberately does
not declare either key on the form-view/section authorable surface — the
boundary declared on one side and crossed on the other.
Per the maintainer ruling of 2026-09-01 (objectstack#13626, director decision
batch C): retire the reads. Declaring the keys was weighed and not adopted —
it would formally invite free Tailwind strings into authored metadata, the
exact class the boundary exists to keep out.
Six sites in ObjectForm (the tabbed/wizard/split/drawer/modal section maps and
the stacked section-divider) and one in DrawerForm (its own divider) stop
copying the keys. A behavioural pin covers all seven arms; it is behavioural
rather than a source grep because `ObjectFormSection` still declares both keys,
so a plain uncast read would type-check and silently restore consumption.
…hangeset
The README stated the renderer reads a section `className` "explicitly on
exactly one pseudo-field, `type: 'section-divider'`" — the read this branch
retires, so the sentence is now false. It is corrected in place and a new
"Section styling is not authorable" section states the boundary, why declaring
the keys was rejected, and what still works (the form ROOT `className`,
section `columns`, host CSS).
The changeset declares the behaviour narrowing. `minor`, not `major`, per this
repo's version-alignment rule; the breaking semantics are spelled out in the
body.
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation plugin tests labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-DAJmHplV.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)514.13KB117.19KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)68.45KB19.20KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.19KB56.03KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
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)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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)81.07KB26.86KB
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

Labels

documentationImprovements or additions to documentationplugintests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

refactor(plugin-form): retire the form-view section className/gridClassName reads - #7198

Merged
os-support-ai merged 3 commits into
mainfrom
claude/issue-13626-formview-classname-reads
Sep 1, 2026
Merged

refactor(plugin-form): retire the form-view section className/gridClassName reads#7198
os-support-ai merged 3 commits into
mainfrom
claude/issue-13626-formview-classname-reads

Conversation

@os-support-ai

@os-support-aios-support-ai commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Part of objectstack-ai/objectstack#13626

Executes the maintainer ruling of 2026-09-01 (director decision batch C, verbatim 「同意」): retire the reads.

className / gridClassName on the form-view section family sit on the SDUI-only side of the authorable boundary — @objectstack/spec deliberately does not declare either on the form-view/section surface (its component.zod.ts says so in as many words) and the authorable-surface ledger carries no entry for them. The renderer nevertheless reached them off the parsed view through as any at seven sites: the boundary declared on one side and crossed on the other, with the two repos each deliberate and in opposite directions.

Declaring the keys was weighed and not adopted: it would formally invite free Tailwind strings into authored metadata, the exact class the boundary exists to keep out — and per ADR-0065 / ADR-0080 (rev. 2026-06-30) utility classNames in runtime metadata are never scanned by the build-time Tailwind, so they silently produce no CSS anyway.

Clause-②: no — no contract-schema accept/reject change. Nothing in @objectstack/spec moves; this PR only stops the renderer reading two keys the spec already declines to declare, so no parse verdict changes in either direction. The path limb does not apply (objectui repo).

Authored by Claude Code, session https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1 (recorded in prose because a body edit rewrites the footer link).

① Census first — the ruling's own gate

The ruling ordered a census before any narrowing, with heavy usage being evidence back to the card rather than a change. Measured across all three corpora, zero authored uses:

CorpusCommitclassName/gridClassName on form-view section nodesLiveness control (columns: in the same section literals)
objectstacka7002ce50115
objectui899730e014
hotcrma6be39a072

Method. A structural walker bracket-matches each sections: array literal and reports the two keys inside it, so the count is scoped to the form-view section family rather than to the string className. The identical script ran against all three corpora.

How authored metadata was told apart from React props — the distinction the ruling asked for:

  • Object-property syntax (className:) counts; JSX attribute syntax (className=) does not.
  • Every candidate was then classified by the node it sits on. All non-section hits resolved to something else: page-component (UIComponent / PageComponent) trees, which have their own declared ledger entry ui/PageComponent:className; component prop declarations in sdui.manifest.json; buttonVariants({ className }) cva calls in hotcrm's docs site; and two prose comments in objectstack examples that warn against authoring the key.
  • objectstack: 21 JSON/YAML hits and 7 object-property hits, none on a form-view section. hotcrm: 2 hits, both React cva props; 0 in its 167-file src/ metadata tree. objectui: gridClassName has zero authored occurrences repo-wide — every hit is a component prop declaration, renderer plumbing, or prose.

Why the zeros are readings and not absences. Each corpus carries a positive control: the same walker finds 115 / 14 / 72 columns: keys inside the very same section literals, so it demonstrably reaches section nodes. hotcrm is the strongest case — 14 view files author form sections, the exact node type in question, and none of them carries either style key.

② The retired reads

Seven cast sites, re-derived on origin/main at dispatch (line numbers had drifted from the triage comment):

FileSites
packages/plugin-form/src/ObjectForm.tsxtabbed 293/294, wizard 325/326, split 359/360, drawer 392, modal 423/424, stacked section-divider 1293
packages/plugin-form/src/DrawerForm.tsxits own section-divider 598

Behaviour after the change is the natural result of the existing code path — the key is not read, so it does not take effect. FormSection already falls back to gridCols[columns] when gridClassName is absent, so section layout is unchanged. No existing test asserted the styles flow through, so none needed updating (all 821 pass unchanged).

The pin is behavioural, and that is load-bearing

⚠️ The retired reads did not actually need their casts. ObjectFormSection (this repo's own @object-ui/types) still declares both keys, so a later "cleanup" writing a plain className: s.className — no as any in sight — type-checks and silently restores consumption. A source grep for as any would stay green through exactly the regression the pin exists to catch.

packages/plugin-form/src/__tests__/sectionStyleKeysRetired-13626.test.tsx therefore authors the keys and asserts the strings never reach the DOM, across all seven arms, each row waiting on the section's own label first so that "absent" is a verdict rather than a form that failed to render.

Reverse verification (direction predicted before running)

Restoring the tabbed arm's two reads turned exactly one row red — the TabbedForm row — in the predicted PRESENT direction, with the other seven green. The assertion that fired was the sentinel-absence check reporting that the rendered markup DID contain os13626-authored-grid-class: under the mutation the authored grid class reappears as the class attribute of the wrapper element around the field grid. That is also a precise record of the pre-change behaviour this PR removes.

The mutation was proven on disk before the run (injected marker = 1, restored cast read = 1, worktree blob 13197ac0 differs from HEAD blob 777bbed4), and the restore leg proven after it (git diff HEAD empty, worktree blob back to 777bbed4, 0 residual markers).

③ Changeset and semver

minor on @object-ui/plugin-form, with the breaking semantics spelled out in the body — this repo's version-alignment rule reserves major for the objectstack cross-major sync and labels its own breaking changes minor.

No ADR-0087 disposition: no declared surface moves. The keys were never on the authorable-surface ledger, which is the whole premise of the finding; check:changeset-presence and check:changeset-no-major both pass.

Scope boundaries held

  • objectstack is untouched — the ledger and the "deliberately NOT declared" note in component.zod.ts were already correct.
  • No declaration added, no schema changed. Route (b) "declare them" and route (c) "write the status quo down" were both ruled out.
  • The form ROOT className (ObjectFormSchema.className, read as a plain uncast schema.className and forwarded to the form wrapper) is a different key on a different node, was not part of the ruling, and is deliberately untouched and unpinned.

Docs

The README stated the renderer reads a section className "explicitly on exactly one pseudo-field, type: 'section-divider'" — the read this PR retires, so the sentence was false as of this change. Corrected in place, plus a new Section styling is not authorable section recording the boundary, why declaring was rejected, and what still works. The plugin-form.mdxclassName rows are about field-level and form-root className and remain true, so they are left alone.

Verification

All on final head 4174960, after git merge origin/main (which brought packages/components changes, so the dependency closure was rebuilt before re-running):

CheckResult
pnpm exec vitest run packages/plugin-form/PASS — 81 files, 821 passed, 8 skipped
the new pin alonePASS — 8/8
pnpm --filter @object-ui/plugin-form type-checkPASS — exit 0 (tsc --noEmit plus tsc -p tsconfig.test.json)
pnpm --filter @object-ui/plugin-form lintPASS — 0 errors (751 pre-existing warnings; this PR removes 7 as any casts)
check:changeset-presence / check:changeset-no-majorPASS / PASS
check:control-bytesPASS — 5955 files scanned
check:doc-fencesPASS
check:governed-queue-guard --test PATHSNOT GOVERNED — ordinary PR

Notes on measurement honesty:

  • The three edited files were confirmed present in the typecheck program via tsc --listFiles, so "typecheck clean" actually covers the new test rather than excluding it.
  • check:readme-exports exits 1 locally for a whole-repo build precondition (dist/index.d.ts missing for ~30 packages this PR never touches, plus the derived floor packagesRead: found 12, floor is 25). Isolated rather than assumed: building plugin-form alone took its README failures from 58 to 0. CI builds before linting.
  • Lint is the affected package's complete eslint . job — the same unit turbo run lint runs — not a narrowed subset. Type-aware linting is off (no parserOptions.project), so this diff cannot move verdicts on untouched files.

…ssName reads
The renderer reached `className` / `gridClassName` off the parsed form view
through `as any` at seven sites, while `@objectstack/spec` deliberately does
not declare either key on the form-view/section authorable surface — the
boundary declared on one side and crossed on the other.
Per the maintainer ruling of 2026-09-01 (objectstack#13626, director decision
batch C): retire the reads. Declaring the keys was weighed and not adopted —
it would formally invite free Tailwind strings into authored metadata, the
exact class the boundary exists to keep out.
Six sites in ObjectForm (the tabbed/wizard/split/drawer/modal section maps and
the stacked section-divider) and one in DrawerForm (its own divider) stop
copying the keys. A behavioural pin covers all seven arms; it is behavioural
rather than a source grep because `ObjectFormSection` still declares both keys,
so a plain uncast read would type-check and silently restore consumption.
…hangeset
The README stated the renderer reads a section `className` "explicitly on
exactly one pseudo-field, `type: 'section-divider'`" — the read this branch
retires, so the sentence is now false. It is corrected in place and a new
"Section styling is not authorable" section states the boundary, why declaring
the keys was rejected, and what still works (the form ROOT `className`,
section `columns`, host CSS).
The changeset declares the behaviour narrowing. `minor`, not `major`, per this
repo's version-alignment rule; the breaking semantics are spelled out in the
body.
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation plugin tests labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-DAJmHplV.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)514.13KB117.19KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)68.45KB19.20KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.19KB56.03KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
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)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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)81.07KB26.86KB
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

Labels

documentationImprovements or additions to documentationplugintests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor(plugin-form): retire the form-view section className/gridClassName reads - #7198

Merged
os-support-ai merged 3 commits into
mainfrom
claude/issue-13626-formview-classname-reads
Sep 1, 2026
Merged

refactor(plugin-form): retire the form-view section className/gridClassName reads#7198
os-support-ai merged 3 commits into
mainfrom
claude/issue-13626-formview-classname-reads

Conversation

@os-support-ai

@os-support-aios-support-ai commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Part of objectstack-ai/objectstack#13626

Executes the maintainer ruling of 2026-09-01 (director decision batch C, verbatim 「同意」): retire the reads.

className / gridClassName on the form-view section family sit on the SDUI-only side of the authorable boundary — @objectstack/spec deliberately does not declare either on the form-view/section surface (its component.zod.ts says so in as many words) and the authorable-surface ledger carries no entry for them. The renderer nevertheless reached them off the parsed view through as any at seven sites: the boundary declared on one side and crossed on the other, with the two repos each deliberate and in opposite directions.

Declaring the keys was weighed and not adopted: it would formally invite free Tailwind strings into authored metadata, the exact class the boundary exists to keep out — and per ADR-0065 / ADR-0080 (rev. 2026-06-30) utility classNames in runtime metadata are never scanned by the build-time Tailwind, so they silently produce no CSS anyway.

Clause-②: no — no contract-schema accept/reject change. Nothing in @objectstack/spec moves; this PR only stops the renderer reading two keys the spec already declines to declare, so no parse verdict changes in either direction. The path limb does not apply (objectui repo).

Authored by Claude Code, session https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1 (recorded in prose because a body edit rewrites the footer link).

① Census first — the ruling's own gate

The ruling ordered a census before any narrowing, with heavy usage being evidence back to the card rather than a change. Measured across all three corpora, zero authored uses:

CorpusCommitclassName/gridClassName on form-view section nodesLiveness control (columns: in the same section literals)
objectstacka7002ce50115
objectui899730e014
hotcrma6be39a072

Method. A structural walker bracket-matches each sections: array literal and reports the two keys inside it, so the count is scoped to the form-view section family rather than to the string className. The identical script ran against all three corpora.

How authored metadata was told apart from React props — the distinction the ruling asked for:

  • Object-property syntax (className:) counts; JSX attribute syntax (className=) does not.
  • Every candidate was then classified by the node it sits on. All non-section hits resolved to something else: page-component (UIComponent / PageComponent) trees, which have their own declared ledger entry ui/PageComponent:className; component prop declarations in sdui.manifest.json; buttonVariants({ className }) cva calls in hotcrm's docs site; and two prose comments in objectstack examples that warn against authoring the key.
  • objectstack: 21 JSON/YAML hits and 7 object-property hits, none on a form-view section. hotcrm: 2 hits, both React cva props; 0 in its 167-file src/ metadata tree. objectui: gridClassName has zero authored occurrences repo-wide — every hit is a component prop declaration, renderer plumbing, or prose.

Why the zeros are readings and not absences. Each corpus carries a positive control: the same walker finds 115 / 14 / 72 columns: keys inside the very same section literals, so it demonstrably reaches section nodes. hotcrm is the strongest case — 14 view files author form sections, the exact node type in question, and none of them carries either style key.

② The retired reads

Seven cast sites, re-derived on origin/main at dispatch (line numbers had drifted from the triage comment):

FileSites
packages/plugin-form/src/ObjectForm.tsxtabbed 293/294, wizard 325/326, split 359/360, drawer 392, modal 423/424, stacked section-divider 1293
packages/plugin-form/src/DrawerForm.tsxits own section-divider 598

Behaviour after the change is the natural result of the existing code path — the key is not read, so it does not take effect. FormSection already falls back to gridCols[columns] when gridClassName is absent, so section layout is unchanged. No existing test asserted the styles flow through, so none needed updating (all 821 pass unchanged).

The pin is behavioural, and that is load-bearing

⚠️ The retired reads did not actually need their casts. ObjectFormSection (this repo's own @object-ui/types) still declares both keys, so a later "cleanup" writing a plain className: s.className — no as any in sight — type-checks and silently restores consumption. A source grep for as any would stay green through exactly the regression the pin exists to catch.

packages/plugin-form/src/__tests__/sectionStyleKeysRetired-13626.test.tsx therefore authors the keys and asserts the strings never reach the DOM, across all seven arms, each row waiting on the section's own label first so that "absent" is a verdict rather than a form that failed to render.

Reverse verification (direction predicted before running)

Restoring the tabbed arm's two reads turned exactly one row red — the TabbedForm row — in the predicted PRESENT direction, with the other seven green. The assertion that fired was the sentinel-absence check reporting that the rendered markup DID contain os13626-authored-grid-class: under the mutation the authored grid class reappears as the class attribute of the wrapper element around the field grid. That is also a precise record of the pre-change behaviour this PR removes.

The mutation was proven on disk before the run (injected marker = 1, restored cast read = 1, worktree blob 13197ac0 differs from HEAD blob 777bbed4), and the restore leg proven after it (git diff HEAD empty, worktree blob back to 777bbed4, 0 residual markers).

③ Changeset and semver

minor on @object-ui/plugin-form, with the breaking semantics spelled out in the body — this repo's version-alignment rule reserves major for the objectstack cross-major sync and labels its own breaking changes minor.

No ADR-0087 disposition: no declared surface moves. The keys were never on the authorable-surface ledger, which is the whole premise of the finding; check:changeset-presence and check:changeset-no-major both pass.

Scope boundaries held

  • objectstack is untouched — the ledger and the "deliberately NOT declared" note in component.zod.ts were already correct.
  • No declaration added, no schema changed. Route (b) "declare them" and route (c) "write the status quo down" were both ruled out.
  • The form ROOT className (ObjectFormSchema.className, read as a plain uncast schema.className and forwarded to the form wrapper) is a different key on a different node, was not part of the ruling, and is deliberately untouched and unpinned.

Docs

The README stated the renderer reads a section className "explicitly on exactly one pseudo-field, type: 'section-divider'" — the read this PR retires, so the sentence was false as of this change. Corrected in place, plus a new Section styling is not authorable section recording the boundary, why declaring was rejected, and what still works. The plugin-form.mdxclassName rows are about field-level and form-root className and remain true, so they are left alone.

Verification

All on final head 4174960, after git merge origin/main (which brought packages/components changes, so the dependency closure was rebuilt before re-running):

CheckResult
pnpm exec vitest run packages/plugin-form/PASS — 81 files, 821 passed, 8 skipped
the new pin alonePASS — 8/8
pnpm --filter @object-ui/plugin-form type-checkPASS — exit 0 (tsc --noEmit plus tsc -p tsconfig.test.json)
pnpm --filter @object-ui/plugin-form lintPASS — 0 errors (751 pre-existing warnings; this PR removes 7 as any casts)
check:changeset-presence / check:changeset-no-majorPASS / PASS
check:control-bytesPASS — 5955 files scanned
check:doc-fencesPASS
check:governed-queue-guard --test PATHSNOT GOVERNED — ordinary PR

Notes on measurement honesty:

  • The three edited files were confirmed present in the typecheck program via tsc --listFiles, so "typecheck clean" actually covers the new test rather than excluding it.
  • check:readme-exports exits 1 locally for a whole-repo build precondition (dist/index.d.ts missing for ~30 packages this PR never touches, plus the derived floor packagesRead: found 12, floor is 25). Isolated rather than assumed: building plugin-form alone took its README failures from 58 to 0. CI builds before linting.
  • Lint is the affected package's complete eslint . job — the same unit turbo run lint runs — not a narrowed subset. Type-aware linting is off (no parserOptions.project), so this diff cannot move verdicts on untouched files.

…ssName reads
The renderer reached `className` / `gridClassName` off the parsed form view
through `as any` at seven sites, while `@objectstack/spec` deliberately does
not declare either key on the form-view/section authorable surface — the
boundary declared on one side and crossed on the other.
Per the maintainer ruling of 2026-09-01 (objectstack#13626, director decision
batch C): retire the reads. Declaring the keys was weighed and not adopted —
it would formally invite free Tailwind strings into authored metadata, the
exact class the boundary exists to keep out.
Six sites in ObjectForm (the tabbed/wizard/split/drawer/modal section maps and
the stacked section-divider) and one in DrawerForm (its own divider) stop
copying the keys. A behavioural pin covers all seven arms; it is behavioural
rather than a source grep because `ObjectFormSection` still declares both keys,
so a plain uncast read would type-check and silently restore consumption.
…hangeset
The README stated the renderer reads a section `className` "explicitly on
exactly one pseudo-field, `type: 'section-divider'`" — the read this branch
retires, so the sentence is now false. It is corrected in place and a new
"Section styling is not authorable" section states the boundary, why declaring
the keys was rejected, and what still works (the form ROOT `className`,
section `columns`, host CSS).
The changeset declares the behaviour narrowing. `minor`, not `major`, per this
repo's version-alignment rule; the breaking semantics are spelled out in the
body.
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation plugin tests labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-DAJmHplV.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)514.13KB117.19KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)68.45KB19.20KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.19KB56.03KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
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)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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)81.07KB26.86KB
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

Labels

documentationImprovements or additions to documentationplugintests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor(plugin-form): retire the form-view section className/gridClassName reads - #7198

Merged
os-support-ai merged 3 commits into
mainfrom
claude/issue-13626-formview-classname-reads
Sep 1, 2026
Merged

refactor(plugin-form): retire the form-view section className/gridClassName reads#7198
os-support-ai merged 3 commits into
mainfrom
claude/issue-13626-formview-classname-reads

Conversation

@os-support-ai

@os-support-aios-support-ai commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Part of objectstack-ai/objectstack#13626

Executes the maintainer ruling of 2026-09-01 (director decision batch C, verbatim 「同意」): retire the reads.

className / gridClassName on the form-view section family sit on the SDUI-only side of the authorable boundary — @objectstack/spec deliberately does not declare either on the form-view/section surface (its component.zod.ts says so in as many words) and the authorable-surface ledger carries no entry for them. The renderer nevertheless reached them off the parsed view through as any at seven sites: the boundary declared on one side and crossed on the other, with the two repos each deliberate and in opposite directions.

Declaring the keys was weighed and not adopted: it would formally invite free Tailwind strings into authored metadata, the exact class the boundary exists to keep out — and per ADR-0065 / ADR-0080 (rev. 2026-06-30) utility classNames in runtime metadata are never scanned by the build-time Tailwind, so they silently produce no CSS anyway.

Clause-②: no — no contract-schema accept/reject change. Nothing in @objectstack/spec moves; this PR only stops the renderer reading two keys the spec already declines to declare, so no parse verdict changes in either direction. The path limb does not apply (objectui repo).

Authored by Claude Code, session https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1 (recorded in prose because a body edit rewrites the footer link).

① Census first — the ruling's own gate

The ruling ordered a census before any narrowing, with heavy usage being evidence back to the card rather than a change. Measured across all three corpora, zero authored uses:

CorpusCommitclassName/gridClassName on form-view section nodesLiveness control (columns: in the same section literals)
objectstacka7002ce50115
objectui899730e014
hotcrma6be39a072

Method. A structural walker bracket-matches each sections: array literal and reports the two keys inside it, so the count is scoped to the form-view section family rather than to the string className. The identical script ran against all three corpora.

How authored metadata was told apart from React props — the distinction the ruling asked for:

  • Object-property syntax (className:) counts; JSX attribute syntax (className=) does not.
  • Every candidate was then classified by the node it sits on. All non-section hits resolved to something else: page-component (UIComponent / PageComponent) trees, which have their own declared ledger entry ui/PageComponent:className; component prop declarations in sdui.manifest.json; buttonVariants({ className }) cva calls in hotcrm's docs site; and two prose comments in objectstack examples that warn against authoring the key.
  • objectstack: 21 JSON/YAML hits and 7 object-property hits, none on a form-view section. hotcrm: 2 hits, both React cva props; 0 in its 167-file src/ metadata tree. objectui: gridClassName has zero authored occurrences repo-wide — every hit is a component prop declaration, renderer plumbing, or prose.

Why the zeros are readings and not absences. Each corpus carries a positive control: the same walker finds 115 / 14 / 72 columns: keys inside the very same section literals, so it demonstrably reaches section nodes. hotcrm is the strongest case — 14 view files author form sections, the exact node type in question, and none of them carries either style key.

② The retired reads

Seven cast sites, re-derived on origin/main at dispatch (line numbers had drifted from the triage comment):

FileSites
packages/plugin-form/src/ObjectForm.tsxtabbed 293/294, wizard 325/326, split 359/360, drawer 392, modal 423/424, stacked section-divider 1293
packages/plugin-form/src/DrawerForm.tsxits own section-divider 598

Behaviour after the change is the natural result of the existing code path — the key is not read, so it does not take effect. FormSection already falls back to gridCols[columns] when gridClassName is absent, so section layout is unchanged. No existing test asserted the styles flow through, so none needed updating (all 821 pass unchanged).

The pin is behavioural, and that is load-bearing

⚠️ The retired reads did not actually need their casts. ObjectFormSection (this repo's own @object-ui/types) still declares both keys, so a later "cleanup" writing a plain className: s.className — no as any in sight — type-checks and silently restores consumption. A source grep for as any would stay green through exactly the regression the pin exists to catch.

packages/plugin-form/src/__tests__/sectionStyleKeysRetired-13626.test.tsx therefore authors the keys and asserts the strings never reach the DOM, across all seven arms, each row waiting on the section's own label first so that "absent" is a verdict rather than a form that failed to render.

Reverse verification (direction predicted before running)

Restoring the tabbed arm's two reads turned exactly one row red — the TabbedForm row — in the predicted PRESENT direction, with the other seven green. The assertion that fired was the sentinel-absence check reporting that the rendered markup DID contain os13626-authored-grid-class: under the mutation the authored grid class reappears as the class attribute of the wrapper element around the field grid. That is also a precise record of the pre-change behaviour this PR removes.

The mutation was proven on disk before the run (injected marker = 1, restored cast read = 1, worktree blob 13197ac0 differs from HEAD blob 777bbed4), and the restore leg proven after it (git diff HEAD empty, worktree blob back to 777bbed4, 0 residual markers).

③ Changeset and semver

minor on @object-ui/plugin-form, with the breaking semantics spelled out in the body — this repo's version-alignment rule reserves major for the objectstack cross-major sync and labels its own breaking changes minor.

No ADR-0087 disposition: no declared surface moves. The keys were never on the authorable-surface ledger, which is the whole premise of the finding; check:changeset-presence and check:changeset-no-major both pass.

Scope boundaries held

  • objectstack is untouched — the ledger and the "deliberately NOT declared" note in component.zod.ts were already correct.
  • No declaration added, no schema changed. Route (b) "declare them" and route (c) "write the status quo down" were both ruled out.
  • The form ROOT className (ObjectFormSchema.className, read as a plain uncast schema.className and forwarded to the form wrapper) is a different key on a different node, was not part of the ruling, and is deliberately untouched and unpinned.

Docs

The README stated the renderer reads a section className "explicitly on exactly one pseudo-field, type: 'section-divider'" — the read this PR retires, so the sentence was false as of this change. Corrected in place, plus a new Section styling is not authorable section recording the boundary, why declaring was rejected, and what still works. The plugin-form.mdxclassName rows are about field-level and form-root className and remain true, so they are left alone.

Verification

All on final head 4174960, after git merge origin/main (which brought packages/components changes, so the dependency closure was rebuilt before re-running):

CheckResult
pnpm exec vitest run packages/plugin-form/PASS — 81 files, 821 passed, 8 skipped
the new pin alonePASS — 8/8
pnpm --filter @object-ui/plugin-form type-checkPASS — exit 0 (tsc --noEmit plus tsc -p tsconfig.test.json)
pnpm --filter @object-ui/plugin-form lintPASS — 0 errors (751 pre-existing warnings; this PR removes 7 as any casts)
check:changeset-presence / check:changeset-no-majorPASS / PASS
check:control-bytesPASS — 5955 files scanned
check:doc-fencesPASS
check:governed-queue-guard --test PATHSNOT GOVERNED — ordinary PR

Notes on measurement honesty:

  • The three edited files were confirmed present in the typecheck program via tsc --listFiles, so "typecheck clean" actually covers the new test rather than excluding it.
  • check:readme-exports exits 1 locally for a whole-repo build precondition (dist/index.d.ts missing for ~30 packages this PR never touches, plus the derived floor packagesRead: found 12, floor is 25). Isolated rather than assumed: building plugin-form alone took its README failures from 58 to 0. CI builds before linting.
  • Lint is the affected package's complete eslint . job — the same unit turbo run lint runs — not a narrowed subset. Type-aware linting is off (no parserOptions.project), so this diff cannot move verdicts on untouched files.

…ssName reads
The renderer reached `className` / `gridClassName` off the parsed form view
through `as any` at seven sites, while `@objectstack/spec` deliberately does
not declare either key on the form-view/section authorable surface — the
boundary declared on one side and crossed on the other.
Per the maintainer ruling of 2026-09-01 (objectstack#13626, director decision
batch C): retire the reads. Declaring the keys was weighed and not adopted —
it would formally invite free Tailwind strings into authored metadata, the
exact class the boundary exists to keep out.
Six sites in ObjectForm (the tabbed/wizard/split/drawer/modal section maps and
the stacked section-divider) and one in DrawerForm (its own divider) stop
copying the keys. A behavioural pin covers all seven arms; it is behavioural
rather than a source grep because `ObjectFormSection` still declares both keys,
so a plain uncast read would type-check and silently restore consumption.
…hangeset
The README stated the renderer reads a section `className` "explicitly on
exactly one pseudo-field, `type: 'section-divider'`" — the read this branch
retires, so the sentence is now false. It is corrected in place and a new
"Section styling is not authorable" section states the boundary, why declaring
the keys was rejected, and what still works (the form ROOT `className`,
section `columns`, host CSS).
The changeset declares the behaviour narrowing. `minor`, not `major`, per this
repo's version-alignment rule; the breaking semantics are spelled out in the
body.
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation plugin tests labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-DAJmHplV.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)514.13KB117.19KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)68.45KB19.20KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.19KB56.03KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
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)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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)81.07KB26.86KB
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

Labels

documentationImprovements or additions to documentationplugintests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

refactor(plugin-form): retire the form-view section className/gridClassName reads - #7198

Merged
os-support-ai merged 3 commits into
mainfrom
claude/issue-13626-formview-classname-reads
Sep 1, 2026
Merged

refactor(plugin-form): retire the form-view section className/gridClassName reads#7198
os-support-ai merged 3 commits into
mainfrom
claude/issue-13626-formview-classname-reads

Conversation

@os-support-ai

@os-support-aios-support-ai commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Part of objectstack-ai/objectstack#13626

Executes the maintainer ruling of 2026-09-01 (director decision batch C, verbatim 「同意」): retire the reads.

className / gridClassName on the form-view section family sit on the SDUI-only side of the authorable boundary — @objectstack/spec deliberately does not declare either on the form-view/section surface (its component.zod.ts says so in as many words) and the authorable-surface ledger carries no entry for them. The renderer nevertheless reached them off the parsed view through as any at seven sites: the boundary declared on one side and crossed on the other, with the two repos each deliberate and in opposite directions.

Declaring the keys was weighed and not adopted: it would formally invite free Tailwind strings into authored metadata, the exact class the boundary exists to keep out — and per ADR-0065 / ADR-0080 (rev. 2026-06-30) utility classNames in runtime metadata are never scanned by the build-time Tailwind, so they silently produce no CSS anyway.

Clause-②: no — no contract-schema accept/reject change. Nothing in @objectstack/spec moves; this PR only stops the renderer reading two keys the spec already declines to declare, so no parse verdict changes in either direction. The path limb does not apply (objectui repo).

Authored by Claude Code, session https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1 (recorded in prose because a body edit rewrites the footer link).

① Census first — the ruling's own gate

The ruling ordered a census before any narrowing, with heavy usage being evidence back to the card rather than a change. Measured across all three corpora, zero authored uses:

CorpusCommitclassName/gridClassName on form-view section nodesLiveness control (columns: in the same section literals)
objectstacka7002ce50115
objectui899730e014
hotcrma6be39a072

Method. A structural walker bracket-matches each sections: array literal and reports the two keys inside it, so the count is scoped to the form-view section family rather than to the string className. The identical script ran against all three corpora.

How authored metadata was told apart from React props — the distinction the ruling asked for:

  • Object-property syntax (className:) counts; JSX attribute syntax (className=) does not.
  • Every candidate was then classified by the node it sits on. All non-section hits resolved to something else: page-component (UIComponent / PageComponent) trees, which have their own declared ledger entry ui/PageComponent:className; component prop declarations in sdui.manifest.json; buttonVariants({ className }) cva calls in hotcrm's docs site; and two prose comments in objectstack examples that warn against authoring the key.
  • objectstack: 21 JSON/YAML hits and 7 object-property hits, none on a form-view section. hotcrm: 2 hits, both React cva props; 0 in its 167-file src/ metadata tree. objectui: gridClassName has zero authored occurrences repo-wide — every hit is a component prop declaration, renderer plumbing, or prose.

Why the zeros are readings and not absences. Each corpus carries a positive control: the same walker finds 115 / 14 / 72 columns: keys inside the very same section literals, so it demonstrably reaches section nodes. hotcrm is the strongest case — 14 view files author form sections, the exact node type in question, and none of them carries either style key.

② The retired reads

Seven cast sites, re-derived on origin/main at dispatch (line numbers had drifted from the triage comment):

FileSites
packages/plugin-form/src/ObjectForm.tsxtabbed 293/294, wizard 325/326, split 359/360, drawer 392, modal 423/424, stacked section-divider 1293
packages/plugin-form/src/DrawerForm.tsxits own section-divider 598

Behaviour after the change is the natural result of the existing code path — the key is not read, so it does not take effect. FormSection already falls back to gridCols[columns] when gridClassName is absent, so section layout is unchanged. No existing test asserted the styles flow through, so none needed updating (all 821 pass unchanged).

The pin is behavioural, and that is load-bearing

⚠️ The retired reads did not actually need their casts. ObjectFormSection (this repo's own @object-ui/types) still declares both keys, so a later "cleanup" writing a plain className: s.className — no as any in sight — type-checks and silently restores consumption. A source grep for as any would stay green through exactly the regression the pin exists to catch.

packages/plugin-form/src/__tests__/sectionStyleKeysRetired-13626.test.tsx therefore authors the keys and asserts the strings never reach the DOM, across all seven arms, each row waiting on the section's own label first so that "absent" is a verdict rather than a form that failed to render.

Reverse verification (direction predicted before running)

Restoring the tabbed arm's two reads turned exactly one row red — the TabbedForm row — in the predicted PRESENT direction, with the other seven green. The assertion that fired was the sentinel-absence check reporting that the rendered markup DID contain os13626-authored-grid-class: under the mutation the authored grid class reappears as the class attribute of the wrapper element around the field grid. That is also a precise record of the pre-change behaviour this PR removes.

The mutation was proven on disk before the run (injected marker = 1, restored cast read = 1, worktree blob 13197ac0 differs from HEAD blob 777bbed4), and the restore leg proven after it (git diff HEAD empty, worktree blob back to 777bbed4, 0 residual markers).

③ Changeset and semver

minor on @object-ui/plugin-form, with the breaking semantics spelled out in the body — this repo's version-alignment rule reserves major for the objectstack cross-major sync and labels its own breaking changes minor.

No ADR-0087 disposition: no declared surface moves. The keys were never on the authorable-surface ledger, which is the whole premise of the finding; check:changeset-presence and check:changeset-no-major both pass.

Scope boundaries held

  • objectstack is untouched — the ledger and the "deliberately NOT declared" note in component.zod.ts were already correct.
  • No declaration added, no schema changed. Route (b) "declare them" and route (c) "write the status quo down" were both ruled out.
  • The form ROOT className (ObjectFormSchema.className, read as a plain uncast schema.className and forwarded to the form wrapper) is a different key on a different node, was not part of the ruling, and is deliberately untouched and unpinned.

Docs

The README stated the renderer reads a section className "explicitly on exactly one pseudo-field, type: 'section-divider'" — the read this PR retires, so the sentence was false as of this change. Corrected in place, plus a new Section styling is not authorable section recording the boundary, why declaring was rejected, and what still works. The plugin-form.mdxclassName rows are about field-level and form-root className and remain true, so they are left alone.

Verification

All on final head 4174960, after git merge origin/main (which brought packages/components changes, so the dependency closure was rebuilt before re-running):

CheckResult
pnpm exec vitest run packages/plugin-form/PASS — 81 files, 821 passed, 8 skipped
the new pin alonePASS — 8/8
pnpm --filter @object-ui/plugin-form type-checkPASS — exit 0 (tsc --noEmit plus tsc -p tsconfig.test.json)
pnpm --filter @object-ui/plugin-form lintPASS — 0 errors (751 pre-existing warnings; this PR removes 7 as any casts)
check:changeset-presence / check:changeset-no-majorPASS / PASS
check:control-bytesPASS — 5955 files scanned
check:doc-fencesPASS
check:governed-queue-guard --test PATHSNOT GOVERNED — ordinary PR

Notes on measurement honesty:

  • The three edited files were confirmed present in the typecheck program via tsc --listFiles, so "typecheck clean" actually covers the new test rather than excluding it.
  • check:readme-exports exits 1 locally for a whole-repo build precondition (dist/index.d.ts missing for ~30 packages this PR never touches, plus the derived floor packagesRead: found 12, floor is 25). Isolated rather than assumed: building plugin-form alone took its README failures from 58 to 0. CI builds before linting.
  • Lint is the affected package's complete eslint . job — the same unit turbo run lint runs — not a narrowed subset. Type-aware linting is off (no parserOptions.project), so this diff cannot move verdicts on untouched files.

…ssName reads
The renderer reached `className` / `gridClassName` off the parsed form view
through `as any` at seven sites, while `@objectstack/spec` deliberately does
not declare either key on the form-view/section authorable surface — the
boundary declared on one side and crossed on the other.
Per the maintainer ruling of 2026-09-01 (objectstack#13626, director decision
batch C): retire the reads. Declaring the keys was weighed and not adopted —
it would formally invite free Tailwind strings into authored metadata, the
exact class the boundary exists to keep out.
Six sites in ObjectForm (the tabbed/wizard/split/drawer/modal section maps and
the stacked section-divider) and one in DrawerForm (its own divider) stop
copying the keys. A behavioural pin covers all seven arms; it is behavioural
rather than a source grep because `ObjectFormSection` still declares both keys,
so a plain uncast read would type-check and silently restore consumption.
…hangeset
The README stated the renderer reads a section `className` "explicitly on
exactly one pseudo-field, `type: 'section-divider'`" — the read this branch
retires, so the sentence is now false. It is corrected in place and a new
"Section styling is not authorable" section states the boundary, why declaring
the keys was rejected, and what still works (the form ROOT `className`,
section `columns`, host CSS).
The changeset declares the behaviour narrowing. `minor`, not `major`, per this
repo's version-alignment rule; the breaking semantics are spelled out in the
body.
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation plugin tests labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-DAJmHplV.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)514.13KB117.19KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)68.45KB19.20KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.19KB56.03KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
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)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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)81.07KB26.86KB
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

Labels

documentationImprovements or additions to documentationplugintests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor(plugin-form): retire the form-view section className/gridClassName reads - #7198

Merged
os-support-ai merged 3 commits into
mainfrom
claude/issue-13626-formview-classname-reads
Sep 1, 2026
Merged

refactor(plugin-form): retire the form-view section className/gridClassName reads#7198
os-support-ai merged 3 commits into
mainfrom
claude/issue-13626-formview-classname-reads

Conversation

@os-support-ai

@os-support-aios-support-ai commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Part of objectstack-ai/objectstack#13626

Executes the maintainer ruling of 2026-09-01 (director decision batch C, verbatim 「同意」): retire the reads.

className / gridClassName on the form-view section family sit on the SDUI-only side of the authorable boundary — @objectstack/spec deliberately does not declare either on the form-view/section surface (its component.zod.ts says so in as many words) and the authorable-surface ledger carries no entry for them. The renderer nevertheless reached them off the parsed view through as any at seven sites: the boundary declared on one side and crossed on the other, with the two repos each deliberate and in opposite directions.

Declaring the keys was weighed and not adopted: it would formally invite free Tailwind strings into authored metadata, the exact class the boundary exists to keep out — and per ADR-0065 / ADR-0080 (rev. 2026-06-30) utility classNames in runtime metadata are never scanned by the build-time Tailwind, so they silently produce no CSS anyway.

Clause-②: no — no contract-schema accept/reject change. Nothing in @objectstack/spec moves; this PR only stops the renderer reading two keys the spec already declines to declare, so no parse verdict changes in either direction. The path limb does not apply (objectui repo).

Authored by Claude Code, session https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1 (recorded in prose because a body edit rewrites the footer link).

① Census first — the ruling's own gate

The ruling ordered a census before any narrowing, with heavy usage being evidence back to the card rather than a change. Measured across all three corpora, zero authored uses:

CorpusCommitclassName/gridClassName on form-view section nodesLiveness control (columns: in the same section literals)
objectstacka7002ce50115
objectui899730e014
hotcrma6be39a072

Method. A structural walker bracket-matches each sections: array literal and reports the two keys inside it, so the count is scoped to the form-view section family rather than to the string className. The identical script ran against all three corpora.

How authored metadata was told apart from React props — the distinction the ruling asked for:

  • Object-property syntax (className:) counts; JSX attribute syntax (className=) does not.
  • Every candidate was then classified by the node it sits on. All non-section hits resolved to something else: page-component (UIComponent / PageComponent) trees, which have their own declared ledger entry ui/PageComponent:className; component prop declarations in sdui.manifest.json; buttonVariants({ className }) cva calls in hotcrm's docs site; and two prose comments in objectstack examples that warn against authoring the key.
  • objectstack: 21 JSON/YAML hits and 7 object-property hits, none on a form-view section. hotcrm: 2 hits, both React cva props; 0 in its 167-file src/ metadata tree. objectui: gridClassName has zero authored occurrences repo-wide — every hit is a component prop declaration, renderer plumbing, or prose.

Why the zeros are readings and not absences. Each corpus carries a positive control: the same walker finds 115 / 14 / 72 columns: keys inside the very same section literals, so it demonstrably reaches section nodes. hotcrm is the strongest case — 14 view files author form sections, the exact node type in question, and none of them carries either style key.

② The retired reads

Seven cast sites, re-derived on origin/main at dispatch (line numbers had drifted from the triage comment):

FileSites
packages/plugin-form/src/ObjectForm.tsxtabbed 293/294, wizard 325/326, split 359/360, drawer 392, modal 423/424, stacked section-divider 1293
packages/plugin-form/src/DrawerForm.tsxits own section-divider 598

Behaviour after the change is the natural result of the existing code path — the key is not read, so it does not take effect. FormSection already falls back to gridCols[columns] when gridClassName is absent, so section layout is unchanged. No existing test asserted the styles flow through, so none needed updating (all 821 pass unchanged).

The pin is behavioural, and that is load-bearing

⚠️ The retired reads did not actually need their casts. ObjectFormSection (this repo's own @object-ui/types) still declares both keys, so a later "cleanup" writing a plain className: s.className — no as any in sight — type-checks and silently restores consumption. A source grep for as any would stay green through exactly the regression the pin exists to catch.

packages/plugin-form/src/__tests__/sectionStyleKeysRetired-13626.test.tsx therefore authors the keys and asserts the strings never reach the DOM, across all seven arms, each row waiting on the section's own label first so that "absent" is a verdict rather than a form that failed to render.

Reverse verification (direction predicted before running)

Restoring the tabbed arm's two reads turned exactly one row red — the TabbedForm row — in the predicted PRESENT direction, with the other seven green. The assertion that fired was the sentinel-absence check reporting that the rendered markup DID contain os13626-authored-grid-class: under the mutation the authored grid class reappears as the class attribute of the wrapper element around the field grid. That is also a precise record of the pre-change behaviour this PR removes.

The mutation was proven on disk before the run (injected marker = 1, restored cast read = 1, worktree blob 13197ac0 differs from HEAD blob 777bbed4), and the restore leg proven after it (git diff HEAD empty, worktree blob back to 777bbed4, 0 residual markers).

③ Changeset and semver

minor on @object-ui/plugin-form, with the breaking semantics spelled out in the body — this repo's version-alignment rule reserves major for the objectstack cross-major sync and labels its own breaking changes minor.

No ADR-0087 disposition: no declared surface moves. The keys were never on the authorable-surface ledger, which is the whole premise of the finding; check:changeset-presence and check:changeset-no-major both pass.

Scope boundaries held

  • objectstack is untouched — the ledger and the "deliberately NOT declared" note in component.zod.ts were already correct.
  • No declaration added, no schema changed. Route (b) "declare them" and route (c) "write the status quo down" were both ruled out.
  • The form ROOT className (ObjectFormSchema.className, read as a plain uncast schema.className and forwarded to the form wrapper) is a different key on a different node, was not part of the ruling, and is deliberately untouched and unpinned.

Docs

The README stated the renderer reads a section className "explicitly on exactly one pseudo-field, type: 'section-divider'" — the read this PR retires, so the sentence was false as of this change. Corrected in place, plus a new Section styling is not authorable section recording the boundary, why declaring was rejected, and what still works. The plugin-form.mdxclassName rows are about field-level and form-root className and remain true, so they are left alone.

Verification

All on final head 4174960, after git merge origin/main (which brought packages/components changes, so the dependency closure was rebuilt before re-running):

CheckResult
pnpm exec vitest run packages/plugin-form/PASS — 81 files, 821 passed, 8 skipped
the new pin alonePASS — 8/8
pnpm --filter @object-ui/plugin-form type-checkPASS — exit 0 (tsc --noEmit plus tsc -p tsconfig.test.json)
pnpm --filter @object-ui/plugin-form lintPASS — 0 errors (751 pre-existing warnings; this PR removes 7 as any casts)
check:changeset-presence / check:changeset-no-majorPASS / PASS
check:control-bytesPASS — 5955 files scanned
check:doc-fencesPASS
check:governed-queue-guard --test PATHSNOT GOVERNED — ordinary PR

Notes on measurement honesty:

  • The three edited files were confirmed present in the typecheck program via tsc --listFiles, so "typecheck clean" actually covers the new test rather than excluding it.
  • check:readme-exports exits 1 locally for a whole-repo build precondition (dist/index.d.ts missing for ~30 packages this PR never touches, plus the derived floor packagesRead: found 12, floor is 25). Isolated rather than assumed: building plugin-form alone took its README failures from 58 to 0. CI builds before linting.
  • Lint is the affected package's complete eslint . job — the same unit turbo run lint runs — not a narrowed subset. Type-aware linting is off (no parserOptions.project), so this diff cannot move verdicts on untouched files.

…ssName reads
The renderer reached `className` / `gridClassName` off the parsed form view
through `as any` at seven sites, while `@objectstack/spec` deliberately does
not declare either key on the form-view/section authorable surface — the
boundary declared on one side and crossed on the other.
Per the maintainer ruling of 2026-09-01 (objectstack#13626, director decision
batch C): retire the reads. Declaring the keys was weighed and not adopted —
it would formally invite free Tailwind strings into authored metadata, the
exact class the boundary exists to keep out.
Six sites in ObjectForm (the tabbed/wizard/split/drawer/modal section maps and
the stacked section-divider) and one in DrawerForm (its own divider) stop
copying the keys. A behavioural pin covers all seven arms; it is behavioural
rather than a source grep because `ObjectFormSection` still declares both keys,
so a plain uncast read would type-check and silently restore consumption.
…hangeset
The README stated the renderer reads a section `className` "explicitly on
exactly one pseudo-field, `type: 'section-divider'`" — the read this branch
retires, so the sentence is now false. It is corrected in place and a new
"Section styling is not authorable" section states the boundary, why declaring
the keys was rejected, and what still works (the form ROOT `className`,
section `columns`, host CSS).
The changeset declares the behaviour narrowing. `minor`, not `major`, per this
repo's version-alignment rule; the breaking semantics are spelled out in the
body.
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation plugin tests labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-DAJmHplV.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)514.13KB117.19KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)68.45KB19.20KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.19KB56.03KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
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)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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)81.07KB26.86KB
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

Labels

documentationImprovements or additions to documentationplugintests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor(plugin-form): retire the form-view section className/gridClassName reads - #7198

Merged
os-support-ai merged 3 commits into
mainfrom
claude/issue-13626-formview-classname-reads
Sep 1, 2026
Merged

refactor(plugin-form): retire the form-view section className/gridClassName reads#7198
os-support-ai merged 3 commits into
mainfrom
claude/issue-13626-formview-classname-reads

Conversation

@os-support-ai

@os-support-aios-support-ai commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Part of objectstack-ai/objectstack#13626

Executes the maintainer ruling of 2026-09-01 (director decision batch C, verbatim 「同意」): retire the reads.

className / gridClassName on the form-view section family sit on the SDUI-only side of the authorable boundary — @objectstack/spec deliberately does not declare either on the form-view/section surface (its component.zod.ts says so in as many words) and the authorable-surface ledger carries no entry for them. The renderer nevertheless reached them off the parsed view through as any at seven sites: the boundary declared on one side and crossed on the other, with the two repos each deliberate and in opposite directions.

Declaring the keys was weighed and not adopted: it would formally invite free Tailwind strings into authored metadata, the exact class the boundary exists to keep out — and per ADR-0065 / ADR-0080 (rev. 2026-06-30) utility classNames in runtime metadata are never scanned by the build-time Tailwind, so they silently produce no CSS anyway.

Clause-②: no — no contract-schema accept/reject change. Nothing in @objectstack/spec moves; this PR only stops the renderer reading two keys the spec already declines to declare, so no parse verdict changes in either direction. The path limb does not apply (objectui repo).

Authored by Claude Code, session https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1 (recorded in prose because a body edit rewrites the footer link).

① Census first — the ruling's own gate

The ruling ordered a census before any narrowing, with heavy usage being evidence back to the card rather than a change. Measured across all three corpora, zero authored uses:

CorpusCommitclassName/gridClassName on form-view section nodesLiveness control (columns: in the same section literals)
objectstacka7002ce50115
objectui899730e014
hotcrma6be39a072

Method. A structural walker bracket-matches each sections: array literal and reports the two keys inside it, so the count is scoped to the form-view section family rather than to the string className. The identical script ran against all three corpora.

How authored metadata was told apart from React props — the distinction the ruling asked for:

  • Object-property syntax (className:) counts; JSX attribute syntax (className=) does not.
  • Every candidate was then classified by the node it sits on. All non-section hits resolved to something else: page-component (UIComponent / PageComponent) trees, which have their own declared ledger entry ui/PageComponent:className; component prop declarations in sdui.manifest.json; buttonVariants({ className }) cva calls in hotcrm's docs site; and two prose comments in objectstack examples that warn against authoring the key.
  • objectstack: 21 JSON/YAML hits and 7 object-property hits, none on a form-view section. hotcrm: 2 hits, both React cva props; 0 in its 167-file src/ metadata tree. objectui: gridClassName has zero authored occurrences repo-wide — every hit is a component prop declaration, renderer plumbing, or prose.

Why the zeros are readings and not absences. Each corpus carries a positive control: the same walker finds 115 / 14 / 72 columns: keys inside the very same section literals, so it demonstrably reaches section nodes. hotcrm is the strongest case — 14 view files author form sections, the exact node type in question, and none of them carries either style key.

② The retired reads

Seven cast sites, re-derived on origin/main at dispatch (line numbers had drifted from the triage comment):

FileSites
packages/plugin-form/src/ObjectForm.tsxtabbed 293/294, wizard 325/326, split 359/360, drawer 392, modal 423/424, stacked section-divider 1293
packages/plugin-form/src/DrawerForm.tsxits own section-divider 598

Behaviour after the change is the natural result of the existing code path — the key is not read, so it does not take effect. FormSection already falls back to gridCols[columns] when gridClassName is absent, so section layout is unchanged. No existing test asserted the styles flow through, so none needed updating (all 821 pass unchanged).

The pin is behavioural, and that is load-bearing

⚠️ The retired reads did not actually need their casts. ObjectFormSection (this repo's own @object-ui/types) still declares both keys, so a later "cleanup" writing a plain className: s.className — no as any in sight — type-checks and silently restores consumption. A source grep for as any would stay green through exactly the regression the pin exists to catch.

packages/plugin-form/src/__tests__/sectionStyleKeysRetired-13626.test.tsx therefore authors the keys and asserts the strings never reach the DOM, across all seven arms, each row waiting on the section's own label first so that "absent" is a verdict rather than a form that failed to render.

Reverse verification (direction predicted before running)

Restoring the tabbed arm's two reads turned exactly one row red — the TabbedForm row — in the predicted PRESENT direction, with the other seven green. The assertion that fired was the sentinel-absence check reporting that the rendered markup DID contain os13626-authored-grid-class: under the mutation the authored grid class reappears as the class attribute of the wrapper element around the field grid. That is also a precise record of the pre-change behaviour this PR removes.

The mutation was proven on disk before the run (injected marker = 1, restored cast read = 1, worktree blob 13197ac0 differs from HEAD blob 777bbed4), and the restore leg proven after it (git diff HEAD empty, worktree blob back to 777bbed4, 0 residual markers).

③ Changeset and semver

minor on @object-ui/plugin-form, with the breaking semantics spelled out in the body — this repo's version-alignment rule reserves major for the objectstack cross-major sync and labels its own breaking changes minor.

No ADR-0087 disposition: no declared surface moves. The keys were never on the authorable-surface ledger, which is the whole premise of the finding; check:changeset-presence and check:changeset-no-major both pass.

Scope boundaries held

  • objectstack is untouched — the ledger and the "deliberately NOT declared" note in component.zod.ts were already correct.
  • No declaration added, no schema changed. Route (b) "declare them" and route (c) "write the status quo down" were both ruled out.
  • The form ROOT className (ObjectFormSchema.className, read as a plain uncast schema.className and forwarded to the form wrapper) is a different key on a different node, was not part of the ruling, and is deliberately untouched and unpinned.

Docs

The README stated the renderer reads a section className "explicitly on exactly one pseudo-field, type: 'section-divider'" — the read this PR retires, so the sentence was false as of this change. Corrected in place, plus a new Section styling is not authorable section recording the boundary, why declaring was rejected, and what still works. The plugin-form.mdxclassName rows are about field-level and form-root className and remain true, so they are left alone.

Verification

All on final head 4174960, after git merge origin/main (which brought packages/components changes, so the dependency closure was rebuilt before re-running):

CheckResult
pnpm exec vitest run packages/plugin-form/PASS — 81 files, 821 passed, 8 skipped
the new pin alonePASS — 8/8
pnpm --filter @object-ui/plugin-form type-checkPASS — exit 0 (tsc --noEmit plus tsc -p tsconfig.test.json)
pnpm --filter @object-ui/plugin-form lintPASS — 0 errors (751 pre-existing warnings; this PR removes 7 as any casts)
check:changeset-presence / check:changeset-no-majorPASS / PASS
check:control-bytesPASS — 5955 files scanned
check:doc-fencesPASS
check:governed-queue-guard --test PATHSNOT GOVERNED — ordinary PR

Notes on measurement honesty:

  • The three edited files were confirmed present in the typecheck program via tsc --listFiles, so "typecheck clean" actually covers the new test rather than excluding it.
  • check:readme-exports exits 1 locally for a whole-repo build precondition (dist/index.d.ts missing for ~30 packages this PR never touches, plus the derived floor packagesRead: found 12, floor is 25). Isolated rather than assumed: building plugin-form alone took its README failures from 58 to 0. CI builds before linting.
  • Lint is the affected package's complete eslint . job — the same unit turbo run lint runs — not a narrowed subset. Type-aware linting is off (no parserOptions.project), so this diff cannot move verdicts on untouched files.

…ssName reads
The renderer reached `className` / `gridClassName` off the parsed form view
through `as any` at seven sites, while `@objectstack/spec` deliberately does
not declare either key on the form-view/section authorable surface — the
boundary declared on one side and crossed on the other.
Per the maintainer ruling of 2026-09-01 (objectstack#13626, director decision
batch C): retire the reads. Declaring the keys was weighed and not adopted —
it would formally invite free Tailwind strings into authored metadata, the
exact class the boundary exists to keep out.
Six sites in ObjectForm (the tabbed/wizard/split/drawer/modal section maps and
the stacked section-divider) and one in DrawerForm (its own divider) stop
copying the keys. A behavioural pin covers all seven arms; it is behavioural
rather than a source grep because `ObjectFormSection` still declares both keys,
so a plain uncast read would type-check and silently restore consumption.
…hangeset
The README stated the renderer reads a section `className` "explicitly on
exactly one pseudo-field, `type: 'section-divider'`" — the read this branch
retires, so the sentence is now false. It is corrected in place and a new
"Section styling is not authorable" section states the boundary, why declaring
the keys was rejected, and what still works (the form ROOT `className`,
section `columns`, host CSS).
The changeset declares the behaviour narrowing. `minor`, not `major`, per this
repo's version-alignment rule; the breaking semantics are spelled out in the
body.
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation plugin tests labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-DAJmHplV.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)514.13KB117.19KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)68.45KB19.20KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.19KB56.03KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
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)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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)81.07KB26.86KB
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

Labels

documentationImprovements or additions to documentationplugintests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

refactor(plugin-form): retire the form-view section className/gridClassName reads - #7198

Merged
os-support-ai merged 3 commits into
mainfrom
claude/issue-13626-formview-classname-reads
Sep 1, 2026
Merged

refactor(plugin-form): retire the form-view section className/gridClassName reads#7198
os-support-ai merged 3 commits into
mainfrom
claude/issue-13626-formview-classname-reads

Conversation

@os-support-ai

@os-support-aios-support-ai commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Part of objectstack-ai/objectstack#13626

Executes the maintainer ruling of 2026-09-01 (director decision batch C, verbatim 「同意」): retire the reads.

className / gridClassName on the form-view section family sit on the SDUI-only side of the authorable boundary — @objectstack/spec deliberately does not declare either on the form-view/section surface (its component.zod.ts says so in as many words) and the authorable-surface ledger carries no entry for them. The renderer nevertheless reached them off the parsed view through as any at seven sites: the boundary declared on one side and crossed on the other, with the two repos each deliberate and in opposite directions.

Declaring the keys was weighed and not adopted: it would formally invite free Tailwind strings into authored metadata, the exact class the boundary exists to keep out — and per ADR-0065 / ADR-0080 (rev. 2026-06-30) utility classNames in runtime metadata are never scanned by the build-time Tailwind, so they silently produce no CSS anyway.

Clause-②: no — no contract-schema accept/reject change. Nothing in @objectstack/spec moves; this PR only stops the renderer reading two keys the spec already declines to declare, so no parse verdict changes in either direction. The path limb does not apply (objectui repo).

Authored by Claude Code, session https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1 (recorded in prose because a body edit rewrites the footer link).

① Census first — the ruling's own gate

The ruling ordered a census before any narrowing, with heavy usage being evidence back to the card rather than a change. Measured across all three corpora, zero authored uses:

CorpusCommitclassName/gridClassName on form-view section nodesLiveness control (columns: in the same section literals)
objectstacka7002ce50115
objectui899730e014
hotcrma6be39a072

Method. A structural walker bracket-matches each sections: array literal and reports the two keys inside it, so the count is scoped to the form-view section family rather than to the string className. The identical script ran against all three corpora.

How authored metadata was told apart from React props — the distinction the ruling asked for:

  • Object-property syntax (className:) counts; JSX attribute syntax (className=) does not.
  • Every candidate was then classified by the node it sits on. All non-section hits resolved to something else: page-component (UIComponent / PageComponent) trees, which have their own declared ledger entry ui/PageComponent:className; component prop declarations in sdui.manifest.json; buttonVariants({ className }) cva calls in hotcrm's docs site; and two prose comments in objectstack examples that warn against authoring the key.
  • objectstack: 21 JSON/YAML hits and 7 object-property hits, none on a form-view section. hotcrm: 2 hits, both React cva props; 0 in its 167-file src/ metadata tree. objectui: gridClassName has zero authored occurrences repo-wide — every hit is a component prop declaration, renderer plumbing, or prose.

Why the zeros are readings and not absences. Each corpus carries a positive control: the same walker finds 115 / 14 / 72 columns: keys inside the very same section literals, so it demonstrably reaches section nodes. hotcrm is the strongest case — 14 view files author form sections, the exact node type in question, and none of them carries either style key.

② The retired reads

Seven cast sites, re-derived on origin/main at dispatch (line numbers had drifted from the triage comment):

FileSites
packages/plugin-form/src/ObjectForm.tsxtabbed 293/294, wizard 325/326, split 359/360, drawer 392, modal 423/424, stacked section-divider 1293
packages/plugin-form/src/DrawerForm.tsxits own section-divider 598

Behaviour after the change is the natural result of the existing code path — the key is not read, so it does not take effect. FormSection already falls back to gridCols[columns] when gridClassName is absent, so section layout is unchanged. No existing test asserted the styles flow through, so none needed updating (all 821 pass unchanged).

The pin is behavioural, and that is load-bearing

⚠️ The retired reads did not actually need their casts. ObjectFormSection (this repo's own @object-ui/types) still declares both keys, so a later "cleanup" writing a plain className: s.className — no as any in sight — type-checks and silently restores consumption. A source grep for as any would stay green through exactly the regression the pin exists to catch.

packages/plugin-form/src/__tests__/sectionStyleKeysRetired-13626.test.tsx therefore authors the keys and asserts the strings never reach the DOM, across all seven arms, each row waiting on the section's own label first so that "absent" is a verdict rather than a form that failed to render.

Reverse verification (direction predicted before running)

Restoring the tabbed arm's two reads turned exactly one row red — the TabbedForm row — in the predicted PRESENT direction, with the other seven green. The assertion that fired was the sentinel-absence check reporting that the rendered markup DID contain os13626-authored-grid-class: under the mutation the authored grid class reappears as the class attribute of the wrapper element around the field grid. That is also a precise record of the pre-change behaviour this PR removes.

The mutation was proven on disk before the run (injected marker = 1, restored cast read = 1, worktree blob 13197ac0 differs from HEAD blob 777bbed4), and the restore leg proven after it (git diff HEAD empty, worktree blob back to 777bbed4, 0 residual markers).

③ Changeset and semver

minor on @object-ui/plugin-form, with the breaking semantics spelled out in the body — this repo's version-alignment rule reserves major for the objectstack cross-major sync and labels its own breaking changes minor.

No ADR-0087 disposition: no declared surface moves. The keys were never on the authorable-surface ledger, which is the whole premise of the finding; check:changeset-presence and check:changeset-no-major both pass.

Scope boundaries held

  • objectstack is untouched — the ledger and the "deliberately NOT declared" note in component.zod.ts were already correct.
  • No declaration added, no schema changed. Route (b) "declare them" and route (c) "write the status quo down" were both ruled out.
  • The form ROOT className (ObjectFormSchema.className, read as a plain uncast schema.className and forwarded to the form wrapper) is a different key on a different node, was not part of the ruling, and is deliberately untouched and unpinned.

Docs

The README stated the renderer reads a section className "explicitly on exactly one pseudo-field, type: 'section-divider'" — the read this PR retires, so the sentence was false as of this change. Corrected in place, plus a new Section styling is not authorable section recording the boundary, why declaring was rejected, and what still works. The plugin-form.mdxclassName rows are about field-level and form-root className and remain true, so they are left alone.

Verification

All on final head 4174960, after git merge origin/main (which brought packages/components changes, so the dependency closure was rebuilt before re-running):

CheckResult
pnpm exec vitest run packages/plugin-form/PASS — 81 files, 821 passed, 8 skipped
the new pin alonePASS — 8/8
pnpm --filter @object-ui/plugin-form type-checkPASS — exit 0 (tsc --noEmit plus tsc -p tsconfig.test.json)
pnpm --filter @object-ui/plugin-form lintPASS — 0 errors (751 pre-existing warnings; this PR removes 7 as any casts)
check:changeset-presence / check:changeset-no-majorPASS / PASS
check:control-bytesPASS — 5955 files scanned
check:doc-fencesPASS
check:governed-queue-guard --test PATHSNOT GOVERNED — ordinary PR

Notes on measurement honesty:

  • The three edited files were confirmed present in the typecheck program via tsc --listFiles, so "typecheck clean" actually covers the new test rather than excluding it.
  • check:readme-exports exits 1 locally for a whole-repo build precondition (dist/index.d.ts missing for ~30 packages this PR never touches, plus the derived floor packagesRead: found 12, floor is 25). Isolated rather than assumed: building plugin-form alone took its README failures from 58 to 0. CI builds before linting.
  • Lint is the affected package's complete eslint . job — the same unit turbo run lint runs — not a narrowed subset. Type-aware linting is off (no parserOptions.project), so this diff cannot move verdicts on untouched files.

…ssName reads
The renderer reached `className` / `gridClassName` off the parsed form view
through `as any` at seven sites, while `@objectstack/spec` deliberately does
not declare either key on the form-view/section authorable surface — the
boundary declared on one side and crossed on the other.
Per the maintainer ruling of 2026-09-01 (objectstack#13626, director decision
batch C): retire the reads. Declaring the keys was weighed and not adopted —
it would formally invite free Tailwind strings into authored metadata, the
exact class the boundary exists to keep out.
Six sites in ObjectForm (the tabbed/wizard/split/drawer/modal section maps and
the stacked section-divider) and one in DrawerForm (its own divider) stop
copying the keys. A behavioural pin covers all seven arms; it is behavioural
rather than a source grep because `ObjectFormSection` still declares both keys,
so a plain uncast read would type-check and silently restore consumption.
…hangeset
The README stated the renderer reads a section `className` "explicitly on
exactly one pseudo-field, `type: 'section-divider'`" — the read this branch
retires, so the sentence is now false. It is corrected in place and a new
"Section styling is not authorable" section states the boundary, why declaring
the keys was rejected, and what still works (the form ROOT `className`,
section `columns`, host CSS).
The changeset declares the behaviour narrowing. `minor`, not `major`, per this
repo's version-alignment rule; the breaking semantics are spelled out in the
body.
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation plugin tests labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-DAJmHplV.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)514.13KB117.19KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)68.45KB19.20KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.19KB56.03KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
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)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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)81.07KB26.86KB
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

Labels

documentationImprovements or additions to documentationplugintests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-support-ai@claude