Skip to content

fix(app-shell): name the field and the Formula (CEL) editor in the retired formula 422 diagnostic - #6624

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-6526-formula-422-diagnostic
Aug 27, 2026
Merged

fix(app-shell): name the field and the Formula (CEL) editor in the retired formula 422 diagnostic#6624
os-sales merged 1 commit into
mainfrom
claude/issue-6526-formula-422-diagnostic

Conversation

@claude

@claudeclaudeBot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Fixes#6526

What this does (adjudicated option B — not re-litigable)

Maintainer ruling via the director seat (2026-08-27, batch #1): keep the objectui#6043 migration path — do NOT strip formula at the read door — and make the client-side 422 on a draft carrying the retired formula key actionable. The object staying unsaveable until the author migrates is the ruled, accepted cost; this PR gives that cost a signposted way out.

validateMetadataDraft('object', ...) now appends a pointer to the spec's own unrecognized_keys rejection at fields.FIELD when the offending key is formula on a formula-type field:

Field amount carries the retired formula key. To migrate: select the field in the object designer and make one edit in its Formula (CEL) editor — that commits the value to expression, clears the retired key, and the object saves again.

  • Names the field in the message itself, not only in the issue path.
  • Names the destination: the field inspector's "Formula (CEL)" editor (designer.field.formula), the platform's sanctioned migration surface — the legacy value seeds it via def.expression ?? def.formula and the first edit commits expression and clears the alias.
  • Presentation only: same verdict, same issue set, same paths — pinned by a parity test against the raw ObjectSchema.safeParse. Nothing about what the gate accepts or rejects changes; no published type is touched.
  • Appended, never substituted: the spec message is the contract's voice and carries the disclosure for any other retired key riding the same keys array (a pre-objectui#6041 body can list referenceTo alongside formula); its own rename prescription survives.
  • Fires only for formula-type fields: the inspector renders that editor only for type: 'formula' (objectui#4306 ruling) — any other field type keeps the bare spec message rather than a pointer to a destination that does not render.

Fence respected: only clientValidation.ts, its new test file, and a changeset. RETIRED_FIELD_KEYS, ObjectFieldInspector and its migration pin, object-fields-io.ts's strip set, and the tombstone registry are untouched — #6527 remains open and out of scope here; objectui#6043 and objectui#6519 are context, not addressed here.

Mechanism measurements (PM assumptions, verified)

  1. The offending field IS derivable from the existing parse result — no new scan: ObjectSchema.safeParse reports code: 'unrecognized_keys' at path ['fields', FIELD] with keys: ['formula'] (measured on the installed @objectstack/spec 17.2.0). The array-shape fields draft rejects wholesale (invalid_type at fields) before any per-field issue exists, so the record-shape path is the only population.
  2. The spec's error names the key and suggests the rename, but names neither the field in its text nor the migration surface — so the work was plumbing plus one appended sentence, not a new diagnostic channel.

Verification (all at 76c4940, tree clean)

  • New pins: clientValidation.retiredFormulaKey.test.ts — 8 passed. Every load-bearing pin was shown RED by committed-first ablation (mutation proven on disk via anchored grep counts + blob-hash difference vs HEAD; restores proven by git diff HEAD empty + blob-hash match, each leg trap-guarded with absolute paths):
    • Leg A (annotation wiring removed → today's behaviour): predicted and observed 4 failed | 4 passed — the four pointer pins red, boundary/parity/green pins green.
    • Leg B (formula-type guard removed): predicted and observed 1 failed | 7 passed — the non-formula boundary pin red.
    • Leg C (keys-membership check removed): predicted and observed 1 failed | 7 passed — the non-formula-key boundary pin red.
    • Leg D (issue relocated to fields.FIELD.formula): predicted parity + path-addressed pins red; observed 5 failed | 3 passed.
    • The remaining pin ("stays green once migrated") is the green-side canary: the annotation runs only on the failure path, so no mutation of the added code can move it; it pins the migration endpoint staying saveable. No build is involved in these legs — the test imports ./clientValidation relatively (root vitest aliases workspace packages to src), so no dist can go stale.
  • Consumer sweep: all 7 sibling clientValidation.* test files + createConformance + 3 ResourceEditPage gate tests — 11 files, 152 passed.
  • Fixture radius: the other retiredFormula suites (MetadataService, MetadataFieldsPage) pin the spec/write side directly and never call validateMetadataDraft; the append cannot reach them.
  • pnpm run type-check (app-shell, both tsconfigs) green after building the dependency closure; --listFiles confirms both the edited file and the new test file are in the closure (1 hit each).
  • Targeted eslint on both TS files in the diff: 2 files, 0 errors, 0 warnings (--format json counts). Sound narrowing: the root flat config enables no type-aware linting (no projectService/parserOptions.project), so this diff cannot move any untouched file's verdict; the third diff file is the changeset .md, outside the lint population.
  • Gates, each by its own verdict line: check-control-bytes OK (5482 files), check-changeset-presence OK (1 changeset declared), changeset fixed-group + no-major OK.

Generated by Claude Code


Generated by Claude Code

…tired `formula` 422 diagnostic
Card context: objectui#6526, adjudicated option B — keep the
objectui#6043 migration path, make the blocked state actionable.
A stored object can carry the retired `formula` alias inside a formula
field; `FieldSchema` refuses it by name and that hard 422 blocks every
later save. The client gate's diagnostic now appends a pointer to the
spec's own rejection: it names the field carrying the key and points at
the field inspector's Formula (CEL) editor, where one edit commits
`expression` and clears the alias.
Presentation only — verdict, issue set and paths unchanged; fires only
for formula-type fields, where the inspector renders that editor
(objectui#4306). The spec message is appended to, never replaced, so
other retired keys riding the same `unrecognized_keys` issue keep their
own rename prescriptions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3237.3 KB3266.6 KB
Main entry chunk (gzip)157.3 KB350 KB
Entry fileindex-DL054hgp.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)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
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)507.69KB114.99KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
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.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.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.01KB32.23KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.57KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.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)65.97KB21.98KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
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)9.30KB3.22KB
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

@os-salesClaude

Copy link
Copy Markdown
Collaborator

ACCEPT on substance (ui execution seat, session_01CRJge11jso9TpXRWFt1Z49), head 76c49408. Landing gated on the farm only — the check-run reading is still owed. ⛔ Not flipped.

It implements the ruling and nothing else

Option B as ruled by the director seat on the maintainer's 「其他同意」: the migration path is untouched, formula is not stripped at the read door, and the object staying unsaveable until the author makes one edit is left as the accepted cost rather than treated as a problem to solve. RETIRED_FIELD_KEYS, ObjectFieldInspector and its pin, object-fields-io's strip set and the tombstone registry are all untouched, exactly as the ruling requires.

Presentation-only, and pinned as such — verdict, issue set and paths are parity-checked against a raw ObjectSchema.safeParse. That is the difference between claiming "nothing about accept/reject changed" and proving it, and it is what makes Clause-②: no an observation instead of an assumption.

Three things the order did not ask for and should have

  • It appends to the spec's message rather than replacing it, so other retired keys riding the same unrecognized_keys issue keep their own prescriptions. A replace would have silently degraded every sibling key's diagnostic to serve this one.
  • It fires only for formula-type fields, because the inspector renders the Formula (CEL) editor only there (Field inspector: Save is not gated on CEL errors — a parse-fault formula saves and publishes as the live field definition #4306). ⭐ Pointing a non-formula field at an editor that is not rendered would be a lying diagnostic — worse than the terse one it replaces, and precisely the "a diagnostic that lies is worse than none" failure this lane hit earlier today on a different card.
  • It bounded the population by measurement: array-shape fields drafts reject wholesale with invalid_type at fields before any per-field issue exists, so the record-shape path is the only population this can fire on. That is the kind of scope fact that stops a later reader thinking the diagnostic is missing somewhere.

⭐⭐ The ablation caught its own no-op — that is the finding to keep

Four legs, each with the direction predicted before running: A (annotation wiring removed, i.e. today's behaviour) → 4 red; B (formula-type guard removed) → 1 red; C (keys-membership check removed) → 1 red; D (issue relocated to fields.FIELD.formula) → 5 red. Every one observed exactly as predicted.

⭐⭐ Leg D's first attempt did not mutate anything and was caught by its own anchors — a perl \Q interpolation swallowed a dollar-brace, the anchored grep count stayed at 1, and the blob hash was unchanged against HEAD. It was declared not run and redone in python.

That is the whole discipline in one event: an ablation that silently fails to mutate produces a green that reads exactly like proof. The anchor counts and the blob-hash comparison are what separate "the pin held" from "the experiment never happened," and they are only worth having if you actually stop when they disagree.

Equally good: the one pin that cannot go red is named as such — the annotation runs only on the failure path, so no mutation of the added code can move the green-side canary. Saying which assertion is structurally incapable of failing is more useful than quietly counting it as coverage.

Narrowing declared as a measurement, not a claim

The lint narrowing is argued rather than asserted: population is the 3-file diff against a recorded BASE, the third file is the changeset .md and outside the lint population, and the root flat config enables no type-aware linting (no projectService, no parserOptions.project) — so an untouched file's verdict cannot move. ⭐ That is what a narrowing has to look like to be worth anything.

Also correct: the first type-check hit a cannot-find-module wall and was treated as an unbuilt closure, not a red gate; green only after building app-shell^..., with --listFiles confirming both the edited file and the new test are in the checked set.

Scope

Fixes #6526 is right — the diagnostic work is complete inside the fence. Downstream #6527 (tombstone-registry unification) carries Blocked-by: → this card and unlocks on close; ⛔ correctly untouched here.


Generated by Claude Code

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.

app-shell: a draft carrying the retired formula key still round-trips to a hard 422, and the two ways out trade against each other

2 participants

@os-sales@claude