Skip to content

retire(metadata-admin): remove the standalone validation resource, move its preview to the embedded path - #4248

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-4132-retire-standalone-validation
Aug 11, 2026
Merged

retire(metadata-admin): remove the standalone validation resource, move its preview to the embedded path#4248
yinlianghui merged 2 commits into
mainfrom
claude/issue-4132-retire-standalone-validation

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Fixes#4132

Verdict: shape B landed — the drawer hosts the preview with no structural surgery

The PM ruling was shape B with A as the measured fallback: retire the standalone door (non-negotiable), and wire ValidationPreview into the embedded path if the drawer can host it; fall back to A (retire the preview with the door) only if hosting it is measurably structural surgery.

Measurement of the drawer's structure, taken before writing any wiring:

  • MetadataDetailDrawer.tsx:122-132 renders exactly one thing for kind: 'embedded'EmbeddedItemEditor, inside a plain flex-1 min-h-0 overflow-auto column. No tab strip to extend, no layout to redesign.
  • EmbeddedItemEditor already owns the live draft state (:64) and already resolves the sub-type by editAs (:54). MetadataPreviewProps needs { type, name, draft } — all three were in hand.
  • So hosting the preview is 6 lines: one getMetadataPreview(editAs) lookup and one conditional mount above the SchemaForm. Diff for the wiring: EmbeddedItemEditor.tsx, +28 lines including the header paragraph explaining why.

Not structural surgery, so the fallback was not taken and no follow-up card is needed for the wiring.

What was retired

anchors.ts registered a standalone validation resource anchored by anchorByField('object'), with createFields / createSchema / createDefaults. That gave every object's Related tab a "Validations" group whose + navigated to validation/_new (RelatedPanel.tsx:257 → the generic metadata/:type/new route at AppContent.tsx:642). Gone, together with the comment that justified it — "usually embedded in the object, but standalone variants do exist" — which the framework contradicts.

Kept: the embedded anchor __object_validation (editAs: 'validation', embeddedPath: 'validations'). That is the path the framework evaluates, and it is the control in the new pin.

Premise check (requirement 1) — all three items hold

itemresult
the registrations still exist on my tipyes — anchors.ts:347 standalone resource + previews/index.ts:45 preview registration, both present at 2776b110b
the installed rc.6 spec has no validation kindconfirmed by parsing the shipped dist, not grep
metadata/validation/new reachable todayconfirmed — the red-first run measured listAnchorsFor('object') returning object → validation, a non-embedded group, which is what RelatedPanel renders the + for

The spec probe imported @objectstack/spec/dist/kernel/index.mjs directly. Note the shipped rc.6 names differ from the issue body's: the registry is DEFAULT_METADATA_TYPE_REGISTRY (an array of 27 entries), not METADATA_TYPE_SCHEMAS, and the unregistered-kind list is behind listUnregisteredKindSchemaTypes().

DEFAULT_METADATA_TYPE_REGISTRY (27 kinds) includes 'validation' => false
action, agent, api, app, book, capability, dashboard, dataset, datasource, doc,
email_template, external_catalog, field, flow, hook, job, mapping, object, page,
permission, position, report, seed, skill, tool, translation, view
listUnregisteredKindSchemaTypes() -> 3; includes 'validation' => false
connector, sharing_rule, webhook

What the embedded path renders now

EmbeddedItemEditor looks up getMetadataPreview(editAs) and mounts it on the live draft above the SchemaForm, read-only (no onPatch, editing={false}). The lookup is generic, not a validation special case — today validation is the only embedded sub-type with both an editAs anchor and a registered preview, and the control in the pin asserts a sub-type without one (index) is unchanged and grows no empty preview chrome.

This makes objectstack#7427's validation.label / .description / .tags read points reachable on the evaluated path; those rows were graded dead precisely because the render only existed behind the retired door.

One read retired with the door

ValidationPreview painted an object: < name > pill, explicitly exempted from the #3275 cleanup because "anchors.ts registers a standalone validation resource … so a standalone rule really does carry it". With that registration gone the read has no producer — and it never had one on the governed path either. Measured against the installed spec:

ValidationRuleSchema.safeParse({ ...rule, object: 'sales_order' })
-> unrecognized_keys: ['object']
ValidationRuleSchema.safeParse({ ...rule }) # same draft, no `object`
-> success

So the pill could only ever confirm a key that makes the rule unsaveable — the AGENTS.md #0.1 shape this file already deleted expression and pattern for. Its test case was replaced, not re-spelled (re-spelling would have kept a green test for a key nothing can produce): the schema's own verdict is the instrument, plus a negative render assertion.

Pins, and their reverse verification

Three files reverted one at a time against HEAD, direction predicted first:

1. anchors.ts reverted → anchors.validation-retired.test.ts: 7 red, controls green (predicted red)

x has no entry in the resource registry
x offers no create affordance - createFields / createSchema are gone with it
x the object Related tab has no standalone "Validations" group
x no anchored group anywhere routes standalone validation authoring
x declares no standalone validation resource registration
x does not carry the stale "standalone variants do exist" justification
x records the retirement where the registration used to be
/ non-vacuity: the registry IS populated and other standalone kinds are in it
/ `__object_validation` is still anchored at object
/ its group is reachable from the object Related tab
/ the drawer still hands the embedded editor its `editAs`
/ previews/index.ts still registers ValidationPreview

⚠️The first pass of this reverse verification was 5 red, not 7 — and the two that stayed green were my own vacuous assertions, fixed in the second commit:

  • not.toMatch(/standalone variants do exist/) never matched, because the comment it guards wraps across two lines in the source.
  • toMatch(/ADR-0088/) was already satisfied by the neighbouring trigger retirement note, so it passed on a file that still registered the door.

Both now assert wording unique to this retirement. This is the whole reason the fix is taken out and the pins re-run rather than reasoned about.

2. EmbeddedItemEditor.tsx reverted → EmbeddedItemEditor.preview.test.tsx: 4 red, 4 controls green (predicted red)

x renders the rule label - objectstack#7427 `validation.label`
x renders the rule description - `validation.description`
x renders the rule tags - `validation.tags`
x renders the type-specific body, not just the envelope
/ `validation` still has a registered preview after the standalone retirement
/ control: a sub-type with no preview is genuinely unregistered
/ ALONGSIDE, not instead of: the SchemaForm is still mounted and editable
/ renders the editor with no preview chrome (the `index` control)

3. ValidationPreview.tsx reverted → ValidationPreview.test.tsx: 1 red, 2 partners green — a deliberately partial direction

Only the render assertion flips. The two schema-probe assertions stay green on the reverted file by design: they measure ValidationRuleSchema, not this repo's file, so restoring the pill cannot move them. Reporting them as "should have gone red" would misdescribe what they pin.

x the preview paints no object pill even when a draft carries the key
/ the schema rejects `object` by name - this is why the read is gone
/ non-vacuity: the same draft WITHOUT `object` is not rejected for its keys

Two test-harness notes worth the next reader's time

Both cost a lap here and neither is a product defect:

  • The pin cannot go through MetadataDetailDrawer. The drawer wraps the editor in a Radix Sheet, which never settles under the light DOM setup — the identical assertions through < MetadataDetailDrawer > ran past 540s with no output, while EmbeddedItemEditor alone renders in ~6s. EmbeddedItemEditoris the entirety of the drawer's embedded branch, so the pin mounts it directly and the drawer's half (that it forwards editAs) is asserted off source text in the sibling pin. Both halves of the path are covered, neither by a hanging render.
  • The pin's form schema must not declare a condition property.SchemaForm's CONDITION_FIELD_NAMES routes that name to the CEL ConditionWidget, whose formula loader is a dynamic import — the unbounded-module-load trap AGENTS.md §测试纪律 describes. Bisected: schemas with name / label render in ~44ms, the same schema plus condition never settles. The rule's condition is asserted on the preview instead, where it is read straight off the draft.

Gates

Orphaned artifacts — swept, nothing stranded

  • ValidationPreview.tsx / .test.tsx: not orphaned under shape B — the renderer moved to the embedded path, so both stay (with the object case replaced).
  • i18n: no key was used only by the standalone route, so none went dead. TYPE_LABELS_EN/ZH.validation ('Validation Rule' / 校验规则) stays live — the kind still exists as an embedded sub-type via editAs: 'validation'. No i18n change, so no i18n gate applies.
  • Docs: swept content/docs/** for the standalone validation resource (metadata/validation, Validations, registerMetadataResource). No page documents it; the hits for "validation" are all form-field validation. Nothing to correct, and content/docs/releases/ untouched.
  • No other in-repo consumer references the retired registration (createConformance.test.ts reads the registry live, so it simply stops seeing the type).

Generated by Claude Code

…ts preview to the embedded path (#4132)
`anchors.ts` registered a standalone `validation` resource anchored by
`anchorByField('object')` with `createFields` / `createSchema`, so every
object's Related tab carried a "Validations" group whose `+` routed to
`validation/_new`. ADR-0088 / objectstack#4509 retired the kind; the framework
ledger records that a standalone rule never reached any object's write path —
it "saved cleanly and gated nothing". Re-measured by parsing the installed
`@objectstack/spec` 17.0.0-rc.6 registries (not grep): `validation` is in
neither `DEFAULT_METADATA_TYPE_REGISTRY` nor the unregistered-kind list.
The embedded anchor `__object_validation` stays — that is the path the
framework evaluates.
`ValidationPreview` moved rather than retiring with the door: the drawer's
`EmbeddedItemEditor` now mounts the preview registered for the anchor's
`editAs`, on the live draft, above the SchemaForm. The lookup is generic, so a
sub-type with no registered preview (`index`) is unchanged. This makes
objectstack#7427's `validation.label` / `.description` / `.tags` read points
reachable on the governed path.
The `object: <name>` pill goes with the door: its stated justification was that
registration, and `ValidationRuleSchema` rejects `object` by name
(`unrecognized_keys`), so it could only confirm an unsaveable key. Its test case
is replaced with the schema's own verdict plus a negative render assertion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
…nt guard actually red on revert
Found by the reverse verification, not by review: with `anchors.ts` restored to
`origin/main`, five of the seven retirement assertions went red as predicted and
two stayed GREEN on a file that still registers the door.
• "does not carry the stale justification" searched for
`standalone variants do exist`, but the comment it guards WRAPS
(`… but standalone variants` / `// do exist. Match anything …`), so the flat
spelling matched nothing and passed for free. Now tolerates the newline +
comment prefix between the halves.
• "records the retirement" matched a bare `/ADR-0088/`, which the neighbouring
`trigger` retirement note already satisfies. Now asserted on wording unique
to the validation note plus the `objectui#4132` provenance marker.
Re-measured after the fix: 7 red on the reverted file, controls (embedded
anchor, populated registry, drawer `editAs`, preview barrel) still green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercelBot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 11, 2026 7:37am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.3 KB350 KB
Entry fileindex-BMyfcbic.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.88KB3.25KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.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)488.60KB108.25KB
core (index.js)3.04KB1.15KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)144.34KB37.61KB
fields (index.js)228.43KB56.61KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.98KB10.85KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.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.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)45.23KB12.45KB
plugin-charts (index.js)61.52KB17.49KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)118.58KB30.71KB
plugin-designer (index.js)210.91KB42.67KB
plugin-detail (index.js)238.86KB59.70KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.58KB27.68KB
plugin-gantt (index.js)164.14KB39.98KB
plugin-grid (index.js)187.97KB49.90KB
plugin-kanban (index.js)48.60KB13.41KB
plugin-list (index.js)110.31KB26.76KB
plugin-map (index.js)17.00KB5.32KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)26.21KB7.52KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)23.71KB7.96KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.23KB0.66KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
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 (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
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 11, 2026 07:47
@yinlianghui
yinlianghui added this pull request to the merge queueAug 11, 2026
Merged via the queue into main with commit 3032107Aug 11, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4132-retire-standalone-validation branch August 11, 2026 07:48
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

metadata-admin still offers a standalone validation resource (+ preview) for a kind the framework retired in ADR-0088 / objectstack#4509

2 participants

@yinlianghui@claude