Skip to content

feat(components,i18n): a create form says when the server will fill a field (objectui#12108) - #12120

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-12108-cel-default-hint
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-12108-cel-default-hint

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12108
Clause-②: yes
Refs: #11914

What changes

A create form now says when the server will fill a field. A field whose defaultValue is a CEL Expression envelope ({ dialect: 'cel', source: 'today()' }) or a runtime token (NOW(), current_user) opens empty: the client cannot evaluate it, and the submit leaves the key out so the server resolves it at insert. The form renderer now draws Set automatically when saved. in that field's description slot while the field is empty. One locale key, form.serverDefaultHint, in all ten packs.

Where it lands, and why not where the claim expected

The claim's file surface named plugin-form or fields. Measured, the code that draws the field row is packages/components/src/renderers/form/form.tsx (renderFormField), so the hint lands there, per the dispatch's producer-side clause. schemaDefaults.ts gets docblock lines only.

  • Not a placeholder. The registered field widgets read their placeholder off the field metadata (fieldData?.placeholder in TextField, config?.placeholder in SelectField), not off the host's placeholder prop, and DateField, DateTimeField, TimeField, BooleanField and UserField draw none. A placeholder could not reach the date field a today() default sits on.
  • One draw site for every layout. ObjectForm, ModalForm, DrawerForm, TabbedForm, SplitForm and WizardForm all render through the form renderer, which already computes isServerOwnedValue(field, isCreateForm) per field for the create-mode required suppression, and holds the live value.
  • One classifier. serverOwned is computed once in renderFormField and read by both the required suppression and the hint. Emptiness is isMissingForRequired, the predicate omitServerResolvedDefaults drops a key by, so the hint shows exactly while the submit will leave the key out. A typed value removes it.
  • Announced. The hint sits inside the one FormDescription, after any authored description; FormControl already names that element's id in the control's aria-describedby.
  • Never wrong, sometimes absent. It is not drawn on a control that takes no input. The containers pass previousValues in edit mode only, so the renderer reads a mode: 'view' form as create; every field there is disabled, and this gate keeps the hint off it. The cost: a locked field on a real create form gets no hint, which is a missing hint, not a wrong one.

Coverage, measured

serverDefaultHint-12108.test.tsx drives ObjectForm through all seven routes (flat, sections, modal, drawer, tabbed, split, wizard) with the real registered widgets:

Field Widget that draws the empty control Hint
text, CEL default TextField yes
date, CEL default, required DateField yes
select, CEL default, required SelectField; the trigger still reads "Select an option" yes, under the trigger
number, CEL default NumberField yes
datetime, NOW() token DateTimeField yes
text, CEL default plus an authored description TextField yes, after the description, in the same element
select, literal default (CONTROL) SelectField, seeded "Open" as today no
text, no default TextField no

Also pinned: the hint goes when a value is typed and comes back when it is cleared; it is in the control's aria-describedby; edit and view forms (flat and modal) show none.

Not covered: a boolean (see Acceptance notes), and the console's second form renderer, apps/console/src/components/FormPage.tsx (/forms/:name, /f/:slug).

The required-field measurement

A required field with a CEL default does not block Save. Pinned for date and select in six routes (flat, sections, modal, drawer, tabbed, split): no required marker, create is called, both keys are absent from the payload. createDefaults.test.tsx already pinned the same rule on text fields. The wizard's submit is its last-step button and is not driven here; its hint is pinned.

  • Client predicate: isRequiredInForm in the builders and resolveFieldRuleState's serverOwnedValue in the renderer, both on isRuntimeDefault. This PR does not change either.
  • Server half (objectstack origin/main a18c5149): the insert path runs applyFieldDefaults before validateRecordInScope (the [#4633] note in validateData states it), and applyFieldDefaults evaluates a { dialect, source } envelope for a field that arrives absent or null. The server fills the field; it does not refuse it.
  • Bound: if the server's CEL evaluation fails, applyFieldDefaults logs a warning and leaves the field unset, and a required field is then refused by the server. Nothing on the client can see that case.

Ablation

At 662a0ba, through ablation-replace.mjs (anchor hit once, blob changed, restored blob equals HEAD, git diff HEAD empty):

Mutation Result
hint text removed at the draw site 17 failed, 17 passed (every hint-presence assertion)
disabled gate removed 2 failed (both view-mode pins)
emptiness check removed 1 failed (the typed-value pin)

Gates

Head 662a0ba unless noted.

  • type-check: @object-ui/components exit 0 at 662a0ba; @object-ui/i18n, @object-ui/core, @object-ui/plugin-form exit 0 at ad59f8b (ad59f8b..662a0ba touches form.tsx only, a module-private change). Each output echoes type-check. --listFiles on plugin-form's tsconfig.test.json lists the new test.
  • vitest, from the repo root: packages/plugin-form/ 173 files, 2127 passed, 1 skipped; packages/components/src/renderers/form/ 71 files, 495 passed, 17 skipped; packages/i18n/ 82 files, 1316 passed, 13 skipped; packages/core/src/validation/ 9 files, 152 passed.
  • Exit 0: check:i18n-keys, check:i18n-drift, check:i18n-dead-keys (the new key is not a candidate), check:i18n-designer-parity, check:new-line-citations (0 new), check:control-bytes, check-changeset-presence, check-changeset-no-major, check:changeset-claims, check:pending-changeset-literals, check:test-path-roots, the three check:vi-mock-*, check:esm-specifiers, check:self-import, check:phantom-deps, check:doc-types, check:doc-example-ids, check:doc-fences, check-doc-links, check:doc-example-readers.
  • check:eager-closure and check:eager-locale-catalogues: exit 0 at head and at base 5f75cfa, from two console vite builds. Delta: +128 B gzipped (+301 B raw) over the same 290 eager chunks: ui-components +110 B, i18n-locale-en +21 B, the rest one-digit hash-name noise. en stays the only eager catalogue.
  • Lint, narrowed to the 14 touched lintable files: 0 errors. Population: eslint --print-config resolves a config for each of the 14, none ignored. Count: 14 files in the --format json output. Invariance: eslint.config.js sets no parserOptions.project or projectService, and no rule in eslint-rules/ reads the disk, so this diff cannot move an untouched file's verdict. form.tsx carries 70 warnings, equal to its base blob; the new test adds 5 no-explicit-any warnings, the as any pattern createDefaults.test.tsx uses.
  • NOT MEASURED: check:doc-snippets and check:doc-examples (prerequisite: a 34-package build). Neither doc's diff touches a fenced block: 0 of 33 changed lines sit inside a fence, and the same detector finds 341 and 204 fenced lines in the two files. check:readme-exports needs every package built; this README diff touches no export listing. Repo-wide pnpm lint, the full pnpm test and a live browser pass are CI's or were not run.

Clause-② surface

  • Exports added: none. Props added: none (renderFieldDescription is module-private).
  • Locale key added: form.serverDefaultHint, in ten packs. TranslationKeys widens, so @object-ui/i18n is minor.
  • DOM: the hint span carries data-server-default-hint="true", a locator in the manner of data-required-marker.

Acceptance notes

  • A boolean with a CEL default: the default never applies from these forms. The renderer starts an unsupplied two-state control at false (the cloud#972 seed in form.tsx's defaultValues), so the submit sends false and the server stores it instead of evaluating the declaration. Measured in this pin's first run: the payload held { status: 'open', active: false, … }. No hint is drawn there, which is the true answer. Not changed here: exempting server-owned booleans from that seed is a UX decision (what a switch shows while undecided), not a mechanical one. Reported to the seat.
  • FormPage, the console's second form renderer, already leaves server-owned blanks out and lowers required on them, but draws no hint. It is outside this card's file surface; reported.
  • The README's six cross-file line addresses are re-cited by symbol or quoted text, as AGENTS.md rule 11 asks of a touched file.
  • The hint says that the server sets the value, never what: no client-side CEL evaluation, no new server door.

Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz


Generated by Claude Code

… field (objectui#12108)

A field whose defaultValue is a CEL expression or a runtime token is left
empty on a create form, and the submit omits it so the server resolves the
default at insert. The form said nothing about it: the field opened empty,
or on "Select an option".

The form renderer now draws "Set automatically when saved." in the field's
description slot while the field is server-owned (isServerOwnedValue, the
classifier the create-mode required suppression already reads) and its live
value is empty by isMissingForRequired, the predicate
omitServerResolvedDefaults drops the key by. A typed value removes it. It is
not drawn on a control that takes no input, so a view-mode form, which the
renderer reads as create, shows none.

One locale key, form.serverDefaultHint, in all ten packs.

Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz
Co-authored-by: Claude <noreply@anthropic.com>
…changeset (objectui#12108)

The plugin-form README's create-form table and the CRUD guide now say a
field left empty for the server carries "Set automatically when saved.",
and when it does not (edit, a control that takes no input, a boolean). The
classifier's consumer list in server-owned-value.ts and the schemaDefaults.ts
docblocks name the hint as the reader it now has.

The README's six cross-file line addresses are re-cited by symbol or quoted
text, as AGENTS.md #11 asks of a touched file.

Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz
Co-authored-by: Claude <noreply@anthropic.com>
… (objectui#12108)

The help line under a field was a module-level `FieldDescription`
component, which added a react-refresh/only-export-components warning to
form.tsx. It is now `renderFieldDescription`, the shape of
`withReadonlyHostGroup` beside it; the DOM it draws is unchanged.

Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 290 chunks) 3171.2 KB 3204.6 KB
Main entry chunk (gzip) 74.1 KB 350 KB
Entry file index-BGpLDLC9.js —
Status PASS —

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


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 19.75KB 7.29KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 586.97KB 141.32KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 269.55KB 68.24KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 54.31KB 15.86KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.52KB 47.99KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.53KB 49.80KB
plugin-detail (index.js) 249.33KB 65.74KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.59KB 46.00KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 249.52KB 69.17KB
plugin-kanban (index.js) 53.23KB 16.71KB
plugin-list (index.js) 122.77KB 31.19KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 92.11KB 23.27KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 662a0ba4408f7de23eddc3cc03b4293f40be2385
Local-runs: none

Inputs read: card objectui#12108 (body and both comments: the Claim and the os-dev-report), PR objectui#12120 (body, the 17-file list, the net diff against main), the check-runs on the head, and file contents at the head wherever the diff leans on something it does not show (form.tsx, server-owned-value.ts, required-presence.ts, schemaDefaults.ts, ObjectForm.tsx, ui/form.tsx, i18n.ts, .changeset/config.json). Nothing built, run or re-run.

Check-runs on the head, read at 2026-10-11T05:43Z: 31 success, 3 skipped (the two coverage jobs and dependabot), 8 still in_progress: Test shards 1, 2, 3, 5, 6, 7 and 8 of 8, and Type Check. Among the successes: Build and E2E, Lint, Doc Snippet Type Check, Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Claim Re-read, Line Citation Gate, README Export Check, Bundle Analysis, Governed Surface Queue Guard, Spec Main Shape Gate, Test (dist pins), Test (shard 4/8). An honest reading, not a verdict on the eight: the seat confirms they land green before the queue. Sections ①②③ are judged on the diff.

① Derived judgments

Every accept-set and public-surface change the diff implies, each judged:

  1. The hint's firing set: right. renderFormField in packages/components/src/renderers/form/form.tsx draws "Set automatically when saved." inside the field's one FormDescription element when serverOwned && !readonly && !fieldDisabled && isMissingForRequired(formField.value). Verified at the head: isServerOwnedValue (@object-ui/core) is isCreateForm && isRuntimeDefault(field.field.defaultValue), the same classifier the create-mode required suppression reads, now computed once (const serverOwned, line 2991) and read twice; isMissingForRequired is undefined, null, blank string or empty array (required-presence.ts:32), so a typed 0 keeps its value and drops the hint; omitServerResolvedDefaults (schemaDefaults.ts:411) drops a key on exactly isRuntimeDefault(defaultValue) && isMissingForRequired(value). Whenever the hint fires, the submit omits the key and the server resolves the declaration: the sentence is true every time it is shown. The one bound the client cannot see, a server-side CEL evaluation failure on a required field, is named in the PR body.
  2. The hint's silent set: right, disclosed, narrower than the card's sentence. The !readonly && !fieldDisabled gate makes the hint a strict subset of the omit-set. It exists because the renderer cannot tell view from create: ObjectForm.tsx passes previousValues in edit mode only (line 1517) and disables every field in view mode (line 1031: schema.readOnly || schema.mode === 'view' || field.readonly). The cost, named in the PR body and in the README: a readonly or disabled field with a runtime default on a real create form gets no hint. A missing hint, never a wrong one; the card's "Done when" is met for every field a user can type into. Follow-up candidate, not a defect: once a container hands the renderer a view signal, the gate can lift for locked create-form fields.
  3. Booleans: right as a reading, a defect elsewhere. A boolean with a CEL default is seeded false by the renderer (the cloud#972 seed), so it is never empty, the submit sends false, and the server never evaluates the declaration. No hint there is the true answer today; the underlying defect is F1 in ③.
  4. Locale surface: right. form.serverDefaultHint is added to all ten packs; TranslationKeys is the key set of en (i18n.ts:14, re-exported at line 268 and from index.ts), so the published type widens by one key. The useSafeFormTranslation fallback map in form.tsx gains the same key byte-identical to en. Ten-pack parity holds on the diff (one key, ten files).
  5. DOM surface: right. New locator data-server-default-hint="true" on the hint span, after the data-required-marker pattern. A field with no authored description that now carries a hint gains a FormDescription element it did not have; that element's id was already the target of FormControl's aria-describedby (ui/form.tsx:124-128), so the hint is announced with the field and no id is duplicated. Additive DOM; nothing removed or renamed.
  6. Required-field measurement: right, unchanged on purpose. The card asked for a measurement: a required field with a CEL default must not block Save. The diff changes neither isRequiredInForm (schemaDefaults.ts:248) nor what the renderer hands resolveFieldRuleState as serverOwnedValue; the new pin drives date and select through six routes and asserts no marker, create called, both keys absent from the payload. Pinned as behaviour, not fixed, which is what the card's "either way" asked for.
  7. Seeding: unchanged. isSeedableDefault, schemaDefaultValues, seedCreateValues and omitServerResolvedDefaults take docblock lines only; no behaviour moves in plugin-form. CONTROL pinned: the literal status: 'open' default seeds and carries no hint.
  8. @object-ui/core: comment only. server-owned-value.ts gains a fourth consumer in the JSDoc of isRuntimeDefault; no signature or export moves.
  9. Docs: right. The README table rows and the new "set on save" paragraph state the firing and silent sets exactly as the code does (edit forms, locked controls, booleans); the CRUD guide gains one sentence. The six README line addresses re-cited by symbol or quoted text all resolve at the head (D7 in ③). Line Citation Gate: success.
  10. Test: right, placed by precedent. packages/plugin-form/src/__tests__/serverDefaultHint-12108.test.tsx drives the public ObjectForm entry through seven routes with the real registered widgets, beside createDefaults.test.tsx; it pins the firing set, the CONTROL, the live-value toggle, aria-describedby, description ordering, edit and view silence, and the required measurement. The wizard's submit is not driven (its hint is), stated in the file.

Not judged here: the live-browser look of the hint; the eager-closure delta (Bundle Analysis success; the Console Performance Budget comment on the PR thread reads PASS).

② Semver level

.changeset/12108-cel-default-hint.md: @object-ui/components: minor, @object-ui/i18n: minor; its body carries Clause-②: yes (widening). PR body line: Clause-②: yes. Judged right:

  • What publishes: @object-ui/components (new renderer behaviour, new DOM locator) and @object-ui/i18n (one key; TranslationKeys widens). @object-ui/core and @object-ui/plugin-form carry comment, docblock and README edits only. All four sit in the one 40-package fixed group (.changeset/config.json), so every released package takes the same minor whichever two the file names.
  • yes takes at least minor: met. (widening) is the well-formed arm for yes; nothing is removed or renamed, so no BREAKING text, no migration and no ADR-0087 marker is owed. No major, per the fixed group's rule: Changeset Bump Policy success.
  • The PR body's yes and the changeset's yes (widening) agree on the verdict; the arm is the optional refinement. Both match the Claim's Clause-②: yes and its stated reason (the locale key). The Claim's conditional, "if the measurement also changes the required-field check, the report says so", is discharged: the report says it did not.
  • skip-changeset would have been wrong here and is not used.

③ Boundary flags

open_questions: none declared. The dev flags from the os-dev-report, each answered or escalated.

Deviations

  • D1, file surface: the hint landed in packages/components/src/renderers/form/form.tsx, outside the Claim's "plugin-form or fields"; the Claim said "stop on breach; explain in the report"; the dev explained and did not stop. Answered on the diff: the renderer is the one draw site all six containers route through (the pin's seven routes show it), it already holds the classifier and the live value, and a placeholder cannot reach a date, time, boolean or user widget. The landing site is the right producer; the Claim's surface was a prediction of where the draw site would be, and the prediction was wrong. Disclosed in the PR body and the report. Accepted. Escalated to the seat on one procedural point: the Claim's serial-constraint read ("none in flight on these files") did not name form.tsx; re-read in-flight PRs against packages/components/src/renderers/form/form.tsx before queueing. The comment-only edit to packages/core/src/validation/server-owned-value.ts and the two doc files are accepted as the consumer list and the docs that follow the code.
  • D2, commit attribution uses the model-free trailer pair rather than the harness line. Answered: that is the rule in both repos' AGENTS.md (objectui AGENTS.md line 328; objectstack's "Commit message" paragraph), and the harness reminder yields to it. All three commits comply. Not a deviation.
  • D3, a stray /tc.pid in the dev container. Answered: not in the 17-file list; a container artefact, nothing lands. No action here.
  • D4, the ablation ran objectstack's ablation-replace.mjs from the objectui worktree. Answered: the ablation is the dev's own evidence, neither re-run nor relied on by this record; the diff is judged on its text and on the head's check-runs. No bearing.
  • D5, objectui has no dispatch-gates.mjs; the gate set was derived by hand. Answered: the check-runs on the head are the gate verdicts here, 31 success and 8 pending as named above.
  • D6, a comparison worktree and a scratch ref created and removed. Answered: no residue in the file list. No action.
  • D7, the README's line addresses re-cited by symbol or quoted text. Verified at the head: renderFormField (form.tsx:2929) holds const rules: any = { with ...validation (3055-3056); await onSubmitProp(formData) (2743) sits in the submit handler; dataSource: _dataSource sits in stripRendererOnlyProps (644) and in the renderer's own props destructure (3615); const contextDataSource = schemaCtx?.dataSource ?? null (2006); dataSource: contextDataSource (3457); isElementDataSourceConfig in element-data-source.ts (169) refuses a value carrying find (173); useElementDataSource.ts exists. One pre-existing nit this PR leaves as it found it: the README quotes the rules spread on one line where the source spells it over two. Right.

Out-of-scope findings

  • F1, a boolean declaring a CEL defaultValue submits active: false on create, so applyFieldDefaults never evaluates the declaration (class a, measured in-process, no production producer found). Escalated: a real defect at a public door, outside this card's "Done when". The seat files it as its own card, or rules it out by producer absence; it does not hold this PR. The PR's "no hint on a boolean" is the correct reading of today's behaviour and is documented as such.
  • F2, apps/console/src/components/FormPage.tsx omits server-owned blanks and lowers required on them but draws no hint (noted, not filed). Escalated: the same defect class on the console's second renderer, outside the Claim's surface. The seat decides whether it becomes a follow-up card.
  • F3, the renderer reads a mode: 'view' form as create because the containers pass previousValues in edit mode only. Answered: verified at the head (ObjectForm.tsx:1517 and 1031); no wrong answer is observable today, and it is the reason for the disabled gate in ①-2. Noted; no action beyond the follow-up note there.

Implemented-by: claude/issue-12108-cel-default-hint
Reviewed-by: session_01AswpQDLCKiZos2jCXknwKz

VERDICT: PASS


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

forms: a field whose defaultValue is a CEL expression opens empty on a new record with no sign that the server will fill it on save

2 participants