Skip to content

fix(console): do not seed a runtime-default defaultValue into the form control - #5885

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5727-formpage-runtime-default-seed
Aug 23, 2026
Merged

fix(console): do not seed a runtime-default defaultValue into the form control#5885
os-zhuang merged 1 commit into
mainfrom
claude/issue-5727-formpage-runtime-default-seed

Conversation

@claude

@claudeclaudeBot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Fixes#5727

readPrefill in apps/console/src/components/FormPage.tsx seeded every declared defaultValue unconditionally. A defaultValue may be a literal, or an instruction the server resolves per insert — a DEFAULT_VALUE_TOKENS token (NOW(), current_user) or a CEL Expression envelope. Seeding one of those literally put the text NOW() into a datetime input on both console form routes, and the submit sent that string as the field's value; it is neither absent nor null, so ObjectQL.applyFieldDefaults never resolved the declared default and the column stored the token text.

The seed is now guarded by isRuntimeDefault, imported from @object-ui/core.

Premise re-measured

The card is static evidence taken at c7cd2b60b. Re-measured on today's origin/main (1939c9610) before writing anything — the premise still holds:

  • readPrefill still seeds unconditionally: if (f.defaultValue !== undefined) out[f.name] = f.defaultValue;
  • isRuntimeDefault still returns zero hits in FormPage.tsx (grep -c = 0).

#5627 has since landed, and its serverOwnedValue carve-out is present — but it governs required, not seeding, exactly as the card predicted.

The triage question is the PM's call, not this PR's

The card asked whether this hand-rolled prefill should import the classifier or grow a shared helper. The dispatching PM ruled: import isRuntimeDefault from @object-ui/core directly, as @object-ui/plugin-form's schemaDefaults.ts already does. Restating the rationale rather than re-deciding it: the classifier is already the single published authority for this distinction, a third helper wrapping it would be a new indirection with one consumer, and the sibling chain's shape is the precedent this repo has been converging on (#4047 / #4068 / #4069 / #4085).

Two consequences of that ruling worth recording, because they are not free choices:

  • isRuntimeDefault, not schemaDefaults.ts's isSeedableDefault wrapper. The wrapper additionally rejects null, which would quietly change what a declared defaultValue: null does on this route. The card is about runtime instructions; the null contract stays as it was (!== undefined). Pinned as a control below.
  • The skip is not gated on create mode, unlike the sibling chain's caller-side gate. On an edit form a record that names the field already outranks any default, so gating would change nothing there — while a record that leaves the column UNSET is exactly where the literal NOW() would still reach a control. One unconditional rule means a runtime token never reaches an input on this route.

No dependency change was needed: @object-ui/core is already declared by apps/console, and this file already imports isServerOwnedValue from it. check:phantom-deps verdict below.

Expectations, with controls

Every specimen is the spec's own declaration, put through the spec's own three-way classifier (discriminateDefaultValueShape from @objectstack/spec/data) before the behaviour is asserted — a hand-typed 'NOW()' would keep passing after the token family moved on.

defaultValuespec classifies asreaches the control?why
'NOW()' (from DEFAULT_VALUE_TOKENS)tokenno — key absentserver resolves at insert
'current_user' (from DEFAULT_VALUE_TOKENS)tokenno — key absentserver resolves at insert
{ dialect: 'cel', source: 'today()' }expressionno — key absentserver evaluates it
'now()' / ' NOW() 'tokenno — key absentthe token predicate is case-insensitive and whitespace-tolerant; same instruction
control'Acme'literalyesa literal default is a value
control'NOW' (no parens)literalyesa genuinely intended literal; the predicate is deliberately not widened
control{ source: 'today()' } (no dialect)literalyesthe engine falls through and stores it verbatim
controlnullyes (null)unchanged by this guard — see the isSeedableDefault note above
control stored record value, default 'NOW()'yes (the stored value)edit mode: the token was resolved at insert
control?prefill_x=, default 'NOW()'yes (the param)a producer supplying a value is not a declaration awaiting resolution

The key is asserted absent (not.toHaveProperty), not empty: absent is precisely the case applyFieldDefaults resolves.

Verification

All legs below ran on d62fb621d, which is this PR's head.

Reverse verification — discriminating. The guard alone was ablated back to the unconditional seed (the mutation was confirmed on disk by grep counts on both the injected and the deleted text: injected marker 1, removed guard 0 — an editor's exit code proves nothing). No rebuild step is involved or claimed: the test imports the subject as ./FormPage, a relative source specifier, so nothing resolves through a package exports to dist. The mutation script carried a trap ... EXIT INT TERM restore.

  • Ablated: Tests 5 failed | 70 passed (75) — the two token legs, the envelope leg, and both tolerated spellings went red. Predicted direction, observed direction.
  • Restored: git diff HEAD --name-only empty and sha256sum byte-identical to git show HEAD:...; re-run Test Files 1 passed (1) / Tests 75 passed (75).
  • Legs that do NOT discriminate, named honestly: all ten control rows stay green under the ablation, by design — they exist to catch an over-broad guard, not this one. Two of them do not test the guard at all: the stored-record and prefill_ rows pin precedence, and the later rungs overwrite whatever source 1 put there either way. The null row does not discriminate this ablation either; it discriminates a different candidate implementation (isSeedableDefault).

Tests and types

  • pnpm exec vitest run apps/console/src/components/FormPage.test.ts -> Test Files 1 passed (1) / Tests 75 passed (75)
  • pnpm exec vitest run apps/console/ -> Test Files 72 passed (72) / Tests 805 passed (805)
  • pnpm --filter @object-ui/console type-check -> exit 0, script echoed (> @object-ui/console type-check -> tsc --noEmit && tsc -b tsconfig.node.json --force)

Gates — each quoted from the gate's own verdict line, with exit codes captured before any pipe:

  • check-changeset-presence.mjs -> 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
  • check-changeset-no-major.mjs -> No changeset declares a 'major' bump.
  • check-control-bytes.mjs -> OK (scanned 4891 tracked text file(s); skipped 85 binary)
  • check-phantom-dependencies.mjs -> Every in-scope import is declared by the package that publishes it.

Re-derived from the paths actually touched rather than trusting the dispatch list — the new @objectstack/spec/data import in the test pulls in more than the four named:

  • check-spec-symbol-derivation.mjs -> spec symbol derivation: 1300 files scanned against 4959 spec export names
  • check-package-self-import.mjs -> No package names itself inside its own src/.

Lint, and the narrowing declared. Ran the whole @object-ui/console turbo lint unit (eslint . from apps/console, the package's own script verbatim) rather than all 40 units: 171 files, 0 errors, 202 warnings — the file count read from --format json, and the population from eslint's own resolution of ., not from a hand-picked list. No warning on any line this PR adds; the ones on the touched files are pre-existing (react-refresh/only-export-components on this file's many exported helpers, no-explicit-any on existing casts). Soundness of not running the other units: eslint.config.js extends tseslint.configs.recommended, notrecommendedTypeChecked, and sets no parserOptions.project / projectService; the local eslint-rules/* are per-file AST rules with no filesystem reads. No rule reads across file boundaries, and this diff touches no config file — so it cannot move the verdict of any file it does not contain.

One gate deliberately not run, declared:check:eager-closure needs apps/console/dist/eager-closure.json, emitted by vite build; without it the script reports a broken gauge, not a verdict. It cannot move here, and that is checkable rather than assumed: this PR adds a named symbol to an import statement that already existed, so the bundled module's package-specifier set is byte-identical to origin/main's (diff of the sorted from '...' sets is empty). CI builds the console and runs it.

Out of scope, filed rather than fixed

Neither is touched here. The conditional-rule surfaces (#5627's territory) and the required-ness half (#4069 / #4085) are likewise untouched.


Generated by Claude Code

`readPrefill` in apps/console/src/components/FormPage.tsx seeded every declared
`defaultValue` unconditionally. A `defaultValue` may be a literal, or an
instruction the server resolves per insert — a DEFAULT_VALUE_TOKENS token
(`NOW()`, `current_user`) or a CEL Expression envelope. Seeding those literally
put the text `NOW()` into a datetime input on both console form routes, and the
submit sent that string as the value; it is neither absent nor null, so
`ObjectQL.applyFieldDefaults` never resolved the declared default.
The seed is now guarded by `isRuntimeDefault` from `@object-ui/core` — the same
published classifier `@object-ui/plugin-form`'s `schemaDefaults.ts` guards its
seeding with, and the one this file already reads once removed via
`isServerOwnedValue`. A runtime default leaves the key absent rather than empty,
because absent is the case the engine resolves. Skipping is unconditional
rather than create-gated: on an edit form a record that names the field already
outranks the default, while a record leaving the column unset is exactly where
the literal would still reach a control.
The later precedence rungs are untouched: a stored value and an explicit
`prefill_` param still fill a runtime-default field.
Fixes#5727
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3229.4 KB3990.2 KB
Main entry chunk (gzip)153.4 KB350 KB
Entry fileindex-Bs_SuCBS.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.13KB3.77KB
app-shell (runtime-config.js)13.57KB4.78KB
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)504.16KB114.08KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)164.55KB45.67KB
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)186.91KB44.22KB
plugin-dashboard (index.js)133.35KB34.44KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)242.34KB60.99KB
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)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.80KB27.20KB
plugin-map (index.js)20.06KB6.62KB
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)8.50KB2.88KB
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)3.77KB1.33KB
react (SchemaRenderer.js)52.40KB17.45KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
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)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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.88KB1.85KB
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-zhuang
os-zhuang marked this pull request as ready for review August 23, 2026 17:42
@os-zhuang
os-zhuang added this pull request to the merge queueAug 23, 2026
Merged via the queue into main with commit 07cd685Aug 23, 2026
23 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5727-formpage-runtime-default-seed branch August 23, 2026 17:42
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-zhuang@claude