Skip to content

refactor(types,core)!: retire CRUDSchema and the crud node type (ADR-0049) - #6117

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-5373-retire-crud-schema
Aug 24, 2026
Merged

refactor(types,core)!: retire CRUDSchema and the crud node type (ADR-0049)#6117
yinlianghui merged 1 commit into
mainfrom
claude/issue-5373-retire-crud-schema

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#5373

Retires CRUDSchema and the type: 'crud' node spelling under ADR-0049 enforce-or-remove, per the maintainer's ruling of 2026-08-20 (route 2, provenance 「其他接受你的建议。」). Routes 1 (register a renderer) and 3 (demote off the node union) were rejected; only the mechanics are in this PR.

Verified before writing: this is not #5861's situation

#5861 was stopped because its ruled key turned out to be declared and enforced in @objectstack/spec, so no objectui edit could produce the required refusal. Checked here, on /home/user/objectstack @ 3637731:

  • grep -rn "CRUDSchema" packages/spec/srczero hits.
  • grep -rn "crud" packages/spec/src/ui/zero hits.
  • The 'crud' literals that do exist in the spec belong to unrelated vocabularies: PublicAuthFeatureSurface = 'crud' | 'login' | 'status' (kernel/public-auth-features.ts, a REST auth-surface enum), the flow-node CRUD quartet in automation/, and one JSON example key in api/rest-server.zod.ts. None is a UI node type.

All four declaration faces are objectui-owned, so the retirement is complete in this repo.

Re-measured "zero renderers" on this merge base, with controls

The card measured a literal register('crud') grep on f2e11ae6f. That method is not sound on its own — the identical grep for register('table') also returns zero, because registrations are namespaced and some are lazy. So the measurement here uses the doc gate's own registry derivation (deriveRegistryKeys), which reads registerandregisterLazy, plus the loop, indirect and open-site forms:

universe size = 659 derivation findings = 0
crud -> 0 registration key(s)
object-grid -> 2 : ["plugin-grid:object-grid","object-grid"] (control)
object-form -> 2 : ["plugin-form:object-form","object-form"] (control)
detail -> 2 : ["view:detail","detail"] (control)
table -> 4 : ["ui:table","table",...] (control)
any key containing "crud": []

The lazy path was checked separately: 47 registerLazy( call sites across packages/ + apps/, none mentioning crud; apps/console/src/register-plugins.ts has no crud reference.

What an authored type: 'crud' does now — quoted

Deleting the validator branch on its own would not have produced a refusal. validateSchema has no unknown-type check: validateBaseSchema only asks that type be a non-empty string, so crud would have fallen through to a silent valid: true#5861's forbidden silence, one layer quieter than before. So the branch is replaced by a tombstone table that refuses the spelling by name. Measured through @object-ui/core's built dist/ (the layer a downstream consumer resolves through exports):

valid : false
isValidSchema : false
errors : [
{
"path": "schema.type",
"message": "Node type `crud` was RETIRED (objectui#5373, ADR-0049 enforce-or-remove). `CRUDSchema` declared it in four places and no renderer ever registered it, so a node spelling it painted the OBJUI-001 \"Unknown component type\" panel. Compose the shapes that DO render instead: `object-grid` for the record table with its toolbar, filters, pagination and row/batch actions, `object-form` for the create/edit form, and `detail` for the record view.",
"type": "error",
"code": "RETIRED_TYPE"
}
]
assertValidSchema THREW:
Schema validation failed:
schema.type: Node type `crud` was RETIRED (objectui#5373, ...)

Counter-probes in the same run: object-grid still validates (valid: true, 0 errors), and a crud node nested under children is refused with its own path (schema.children[1].type) — so "crud is refused" is not satisfiable by validation being broken outright.

What changed

@object-ui/types — the CRUDSchema interface and its zod mirror; the four shapes that existed only to type its keys (CRUDOperation, CRUDFilter, CRUDToolbar, CRUDPagination), their zod mirrors and ...SchemaType aliases; both barrels; and CRUDSchema as a member of CRUDComponentSchema, which is what takes it off the node union AnySchema (assumption 2 — not optional). The four sub-shapes are removed because they are orphaned by the retirement, measured, not assumed: after CRUDSchema goes they have zero consumers in crud.ts, and outside it only the barrel and the parity/builder tests that also go. ActionSchema / DetailSchema / CRUDDialogSchema are unchanged and remain the union's members.

@object-ui/core — the validateCRUDSchema branch becomes RETIRED_NODE_TYPES + validateRetiredNodeType; CRUDBuilder and the crud() factory are removed.

Docs — the ### CRUDSchema section of api/schema-reference.md is replaced with a retirement note plus a mapping table onto shapes that render; five dangling [CRUDSchema](#crudschema) links are re-pointed; the import example and api/index.md are updated. The note deliberately carries no fenced block spelling the retired type — the doc gate judges every type literal inside a fence, and re-teaching it there is the thing this landing removes.

The DOC_TYPE_EXEMPTIONS entry and the doc rewrite land together, as #5342 asked. Verified rather than asserted: re-adding the crud entry with the rewrite in place turns the gate red with exactly

content/docs/api/schema-reference.md -> crud [stale-exemption]
no code block in that file spells this type any more.

The neighbouring action exemption's reason cited CRUDSchema's toolbar.actions[] / rowActions[] / batchActions[] as its carriers; those keys went with CRUDSchema, so the reason is re-pointed at the sites that survive (ActionSchema.dialog.actions[] / chain[], DetailSchema.actions[], CRUDDialogSchema.actions[]) with re-derived line numbers.

Which defaultSort is which

Disambiguated mechanically, not by eye — every site tagged with its owning section:

537 [### CRUDSchema] "defaultSort": "name", <- this card
538 [### CRUDSchema] "defaultSortOrder": "asc", <- this card
603 [### CRUDSchema] | `defaultSort` / `defaultSortOrder` | ...
756 [### ObjectGridSchema] "defaultSort": { "field": ..., "order": ... } <- #5861, UNTOUCHED

CRUDSchema.defaultSort is a flat string plus a separate defaultSortOrder; ObjectGridSchema.defaultSort is an object with field and order — a different key on a different carrier. Only the first three lines are removed.

Pins: flipped vs. retired

  • Flippedpackages/cli/.../check-known-types.test.ts:84, from "warns about crud, which four declaration faces describe..." to "warns about crud, a RETIRED spelling that must never re-enter the key set". Not deleted. Its comment now states plainly what it can and cannot witness: KNOWN_SCHEMA_TYPES is derived from registrations, and crud never had one, so this assertion held before the retirement, holds after it, and would still hold on a revert. It is a regression pin against crud being registered back in, not a witness of the declarations being gone.
  • Flippedscripts/__tests__/known-schema-types-derivation-5115.test.ts:82, whose comment asserted CRUDSchema "still has its interface, zod mirror, validator branch and builder" — now false.
  • Flipped — the two schema-validator.test.ts tests that asserted a well-formed crud node was valid and an ill-formed one merely warned. They now assert the refusal, its code, its path and its message.
  • Retired — the describe('crud()') block and the operationOf helper in schema-builder.test.ts. They exercised a builder that no longer exists; there is nothing left to pin. Replaced by a pin on the builder's absence.
  • Retired — the five crud.zod.ts#CRUD* entries in the zod-mirror parity test, plus the CRUDSchema: 'operations' known-drift entry (the same staleness path that surfaced objectui#5855).
  • Newpackages/types/src/__tests__/crud-retirement-5373.test.ts.

Which assertions would still pass on a revert

Stated because three phantom-class defects this round had exactly this root.

Would still pass on a revert (and are labelled as such in-tree):

  • the CLI warns about crud pin, for the structural reason above;
  • known-schema-types-derivation-5115.test.ts's rejects crud assertions — same reason, both read the registry, not the declarations.

Would go red on a revert — these are the actual witnesses:

  • schema-validator.test.ts — the refusal, its code, path and message;
  • crud-retirement-5373.test.ts — the zod union refusing a crud node (restore the mirror as a union member and safeParse succeeds again), and the barrel not exporting the mirror or its four sub-shapes;
  • schema-builder.test.tsexpect(builder).not.toHaveProperty('crud');
  • check-doc-component-types.mjs — demonstrated above by re-adding the entry.

Every negative carries a control drawn from the same object in the same run (detail must still parse, DetailSchema must still be exported, form and grid must still be functions, object-grid must still validate), so none of them is satisfiable by the thing under test failing to load.

Reverse verification

Predicted before running: removing the crud entry from RETIRED_NODE_TYPES while leaving the machinery should turn the two refusal tests red, leave the counter-probe green, and make an authored crud node come back silently valid. Observed exactly that.

The edit was committed first, so the trap restore could not eat it. Mutation proven on disk by grepping the removed text (the refusal message → 0) and separately the injected marker (ABLATION-MARKER-5373 → 1), against a control present in both versions (RETIRED_NODE_TYPES → 3). @object-ui/core was rebuilt and the mutation proven to have reached dist/ (refusal text → 0, control → 3) before any result was read — an unbuilt ablation stays green and would have read as "the refusal never fired".

Tests 2 failed | 14 passed (16)
x REFUSES `type: 'crud'` by name ... AssertionError: expected true to be false
x refuses a retired spelling nested in children ...

expected true to be falseis the silence: with the table entry gone, an authored crud node validates clean. The restore leg was run too, not just armed — source restored (git diff HEAD --stat empty), rebuilt, and dist/ re-checked (marker → 0, refusal text → 1), then 16/16 green again.

Gates

Derived by enumerating each CI job's own step list, not from top-level script names. All run on ec71e390d; the tree is byte-identical to 439c54c87, which everything above was measured on (message-only amend, git diff empty).

gateexita red would have meant
turbo run build --filter '@object-ui/types...' '@object-ui/core...' '@object-ui/cli...'0stale .d.ts; every judgement after it unreadable
pnpm --filter @object-ui/types type-check (tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json)0a surviving reference to a removed type — incl. examples/
pnpm --filter @object-ui/core type-check0same, plus the new tombstone module not compiling
pnpm --filter @object-ui/cli type-check0the flipped pin not compiling
pnpm type-check:scripts0the edited scripts/ files not compiling
turbo run type-check --filter '...@object-ui/types' (78 tasks, 43 pkgs)0a downstream consumer still importing CRUDSchema
pnpm exec vitest run over every touched package (144 files, 2808 tests)0a behavioural regression from the removal
pnpm lint (turbo run lint, 47/47 packages)0a lint error; 0 errors, warnings all pre-existing
node scripts/check-doc-component-types.mjs0a doc snippet teaching an unregistered type, or a stale exemption
node scripts/check-doc-snippet-types.mjs (157/157 blocks)0a snippet importing the removed type
node scripts/check-doc-links.mjs0a dangling #crudschema anchor
pnpm check (the CLI over this repo, from lint.yml)0an authored schema in-repo spelling crud
node scripts/check-changeset-presence.mjs0a published change with no changeset
node scripts/check-changeset-no-major.mjs0a major bump; policy is minor
node scripts/check-control-bytes.mjs (5057 files)0a raw control byte
node scripts/check-lint-coverage.mjs / check-type-check-coverage.mjs0 / 0a package dropping out of coverage
pnpm check:phantom-deps / check:self-import / check:esm-specifiers / check:spec-symbols / check:action-forward-parity0an export-surface regression from the removal

Exit codes were captured before any pipe (cmd > log 2>&1; EXIT=$?), never from a | tail. half-state-patrol.yml is a scheduled board patrol, not a per-PR gate, and it returned exit 3 ("no reading at all") here — deliberately excluded.

The downstream sweep uses the prefix form, with the direction demonstrated rather than asserted: ...@object-ui/types matches 43 packages (consumers + itself); the suffix form @object-ui/types... matches 1 (types has no workspace dependencies). Prefix is the correct direction for a contract tightening.

Version

minor for both packages, per AGENTS.md 版本号策略 — never major. The changeset names CRUDSchema and the crud spelling verbatim and states the external caveat: in-repo zero is not npm zero, so a published consumer that imported the type, called crud() / CRUDBuilder, or authored type: 'crud' gets a compile error or a validation error respectively.


Generated by Claude Code

…R-0049)
`crud` carried four declaration faces — the TS interface, the zod mirror, a
dedicated branch in `validateSchema` that affirmatively passed it, and
`CRUDBuilder` — and never had a registered renderer, so a node spelling it
painted the OBJUI-001 "Unknown component type" panel while
`api/schema-reference.md` published it as reference material.
Per the maintainer's 2026-08-20 ruling (route 2), all four faces are removed
and the spelling is now refused BY NAME: `validateSchema` returns a
`RETIRED_TYPE` error on `schema.type` at any depth, naming the shapes that do
render (`object-grid`, `object-form`, `detail`). The reference page is
rewritten around those, and #5342's `DOC_TYPE_EXEMPTIONS` entry — which asked
to be deleted when this landed — goes in the same commit.
Card relation is declared once, in the PR body.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3221.0 KB3990.2 KB
Main entry chunk (gzip)153.7 KB350 KB
Entry fileindex-DJ2hJGsR.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.38KB3.90KB
app-shell (runtime-config.js)18.10KB6.51KB
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)505.15KB114.53KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)168.39KB46.47KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.45KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)244.08KB61.86KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)201.05KB54.38KB
plugin-kanban (index.js)52.89KB14.59KB
plugin-list (index.js)111.86KB27.22KB
plugin-map (index.js)20.11KB6.64KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.57KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)52.40KB17.45KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.49KB2.14KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 24, 2026 18:19
@yinlianghui
yinlianghui added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit 105f3c5Aug 24, 2026
23 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-5373-retire-crud-schema branch August 24, 2026 18:31
yinlianghui pushed a commit that referenced this pull request Aug 25, 2026
…base
The measurement objectui#5392's 2026-08-22 ruling required before the A/B/C
fork is decided. No generator, no drift gate, no published artifact.
Re-run on origin/main @ 50d0d6c, which contains objectui#6117 (CRUDSchema
retired). Converter: zod 4.4.3's built-in z.toJSONSchema (already a dependency).
Population: AnyComponentSchema — 14 union members, 105 leaf node schemas.
Headline: the generated artifact is a strict relaxation of the zod source. Over
475 root-type JSON files it agrees with safeValidateSchema on 461 (97.1%), is
weaker on 14, and is stricter on none. 0/105 node types convert under default
options; 105/105 convert lenient; 0/105 faithful. 276 of 333 objects are open
because BaseSchema is .passthrough() and .strict() is called zero times, so the
artifact reads closed and is not. Size: 651,908 bytes pretty / 369,028
minified, or +21.5% / +12.2% of the published dist.
New since the pre-#6117 run: all 5 refinement sites isolated with passing
controls (was 2 of 5); @objectstack/spec already ships generated JSON Schema
(23.4 MB, 156 UI documents, 118/124 closed at the root) as both a precedent and
the closedness contrast; the JSONC-only files were checked and carry no root
type, so the 475 census is complete against check.ts's own reader.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationpackage: corepackage: typesplugintests

Projects

None yet

2 participants

@yinlianghui@claude