Skip to content

test(app-shell): measure and pin what a declared flow-node defaultValue renders (objectui#6830) - #8431

Merged
os-justin merged 3 commits into
mainfrom
claude/issue-6830-flownode-inspector-defaultvalue
Sep 7, 2026
Merged

test(app-shell): measure and pin what a declared flow-node defaultValue renders (objectui#6830)#8431
os-justin merged 3 commits into
mainfrom
claude/issue-6830-flownode-inspector-defaultvalue

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Part of #6830 — this PR carries the measurement half only. No render-path
change is in it: the direction is a product call and it is named under
"What is still open".

Base 0203a29e9 · head 09c44a863 · installed @objectstack/spec 17.3.0.

What was measured

The card's claim is a claim about a rendered document, so it was settled by
rendering FlowNodeInspector and reading the control the author sees — the
checkbox's checked, the select trigger's text — not by grepping the source.

question measured answer
does a declared defaultValue seed the control's value? no, on both control kinds
is it delivered by a MECHANISM (prop spread, DOM pass-through)? no — the boolean draws value === true and the select value != null ? String(value) : ''; nothing else in the path reads the property
does the ONLINE writer fare better? nojson-schema-to-fields converts JSON-Schema default into defaultValue, and that field renders identically blank
is the property inert, then? nocontrollerAdmits resolves an unset showWhen controller through it, so a declared default changes which fields are ON SCREEN. That is a visibility effect, never a value effect
can a boolean control tell "key absent" from "key stored as false"? the data can; the control cannot. getFieldValue returns undefined vs false, and value === true flattens both to an identical DOM node (asserted by outerHTML equality)

Two smaller measurements fell out of the same run:

  • An unset select renders completely blank — not the em-dash placeholder the
    card and InspectorSelectField both assume. The field always passes a
    controlled value (the ''-to-sentinel bridge), and Radix renders its
    placeholder only for an undefined one.
  • The hand-written escalation.enabled: defaultValue 'false' is inert: an
    absent default and a 'false' default both fail equals: ['true'], so the
    group hides either way. The ablation confirms it — neutralising the read site
    reds only the online case.

Three of the card's numbers moved; the headline did not

  • Holds: ten fields declare a default; the boolean and select render paths
    are verbatim as quoted; the reproduction still reproduces on 17.3.0.
  • Moved: the declaring field the card lists as assignment create is
    screen.mode ("Form mode") — assignment declares nothing. The measured spec
    version is 17.3.0, not 17.2.0. And FlowNodeInspector.tsx:236 (named in a
    card comment as the sole read site) reads flow variable defaults from
    draft.variables, a different property on a different type; the showWhen
    read is flow-node-config.ts's controllerAdmits, as the card body itself says.

The pin, and the proof it can fail

FlowNodeInspector.declaredDefault.test.tsx — 13 cases, all asserting rendered
values. Three ablations, each mutating a READ SITE from a committed tree, each
proved to reach disk by git hash-object against the HEAD blob, each restored
by state (git diff HEAD empty and hashes equal):

leg mutation red rows
A controllerAdmits stops resolving through controller.defaultValue a gate whose default is true reveals its siblings for a node that omits the key (1 failed / 12 passed)
B the boolean control seeds from field.defaultValue — i.e. someone implements the repair boolean: an unset key draws an UNCHECKED box…, renders identically whether the key is missing or explicitly false, the boolean control exists, is a checkbox, and commits the author edit (3 failed / 10 passed)
C the boolean control is deleted outright — an implementation strictly WORSE than the bug 5 failed / 8 passed, including the non-regression row

Leg C is the point of the non-regression describe: every negative in this file
("the control shows no default") is also satisfied by an inspector that renders
nothing, so the controls' existence, their stored-value behaviour, the offered
vocabulary and the deprecated-value fallback are pinned beside them. A feature
deletion cannot pass.

What is still open

Triage already ruled direction A ("show, do not write") on this card, and set
a precondition: establish whether the inspector can tell absent from stored-false
before designing the affordance
. That precondition is now answered — the
distinction survives to the component boundary and is destroyed by value === true,
so A's boolean half is implementable and its cost is known.

Two things the implementer of A needs, neither of which this PR decides:

  1. A's select half needs a shared primitive fixed first. Rendering the
    effective default as placeholder text does not work today: InspectorSelectField's
    placeholder prop is unreachable for every caller (measured — an explicit
    placeholder renders as the empty string). That primitive has 61 call sites
    across the designer, so the blast radius is wider than flow nodes.
  2. One declared default is wrong. On 17.3.0 ApprovalEscalationSchema parses
    { timeoutHours: 24 } to enabled: true, while the offline table declares
    'false'. Showing declared defaults would show a wrong one on that field.
    That divergence is flow-node inspector: escalation.enabled toggle defaults to 'false' while @objectstack/spec now defaults the key to true — omitted-key blocks render OFF but are ON #6620's subject and is not addressed here; flow-node inspector: escalation.enabled toggle defaults to 'false' while @objectstack/spec now defaults the key to true — omitted-key blocks render OFF but are ON #6620 remains
    open on its own terms.

⛔ Retirement is not available as the other arm: the property has a live
reader (controllerAdmits) and a second writer (the engine-published
configSchema), so ADR-0049 enforce-or-remove resolves to enforce by
elimination. Only the doc comment's "spec default" wording could be retired,
which is triage's arm B — already refused as insufficient.

Verification

  • pnpm exec vitest run packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.declaredDefault.test.tsx — 13 passed.
  • pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build — exit 0 (closure built before type-checking).
  • pnpm --filter @object-ui/app-shell type-check — exit 0 (tsc --noEmit and tsc -p tsconfig.test.json). The new file is in the test program: tsc -p tsconfig.test.json --listFiles lists it once, with a lit control and a zero-hit negative control.
  • pnpm --filter @object-ui/app-shell lint — exit 0, 0 errors; the new file contributes 0 warnings.
  • check:vi-mock-specifiers, check:vi-mock-inherit, check:control-bytes — all ✅ OK.
  • check-governed-queue-guard --test on both changed paths — ✅ NOT GOVERNED.
  • check-changeset-presence✅ 1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)… Every one of them has an EMPTY frontmatter. check-changeset-no-major.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S


Generated by Claude Code

…objectui#6830)

Measures the rendered control rather than the declaration table: an unset key
draws an unchecked box / a blank select whatever the field declares, on both
writers of the property (the hand-written table and the engine-published
configSchema). Pins the one read site that does reach the DOM — a declared
default on a `showWhen` controller changes visibility, not value — and the
non-regression half, so an inspector that renders nothing cannot pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
…830)

Keeps the new pin at zero lint warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3472.5 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-DzK6MxpQ.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) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
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) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 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) 498.55KB 114.03KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 189.15KB 52.56KB
fields (index.js) 243.15KB 61.40KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
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) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.52KB 46.34KB
plugin-dashboard (index.js) 131.48KB 34.45KB
plugin-designer (index.js) 213.21KB 43.63KB
plugin-detail (index.js) 248.68KB 63.94KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.58KB 56.63KB
plugin-kanban (index.js) 55.38KB 15.72KB
plugin-list (index.js) 113.38KB 27.73KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.46KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
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) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
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) 0.20KB 0.18KB
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) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
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) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
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

@os-justin
os-justin marked this pull request as ready for review September 7, 2026 23:22
@os-justin
os-justin added this pull request to the merge queue Sep 7, 2026
Merged via the queue into main with commit 75b1b51 Sep 7, 2026
34 checks passed
@os-justin
os-justin deleted the claude/issue-6830-flownode-inspector-defaultvalue branch September 7, 2026 23:39
os-justin pushed a commit that referenced this pull request Sep 8, 2026
…defaultValue

objectui#6830 arm A ("show, do not write", triage 2026-09-04), the boolean
half — objectui#8451. The select half stays untouched: it is blocked on
objectui#8450.

The control drew `value === true`, which flattened an ABSENT key and a stored
`false` onto one unchecked box although the runtime treats them oppositely on a
key the spec defaults to true. It now shows the value in effect — the stored
boolean, else the declared `defaultValue` in the same 'true'/'false' spelling
`controllerAdmits` already resolves an unset controller against. Nothing is
written; the first author edit still commits an explicit boolean.

`isUnsetFieldValue` is exported from `flow-node-config.ts` so the three sites
that have to answer "has the author stored anything here" (the re-show rule,
the `showWhen` resolver, and now the control) share one definition instead of
three copies of `=== undefined || === null || === ''`.

⛔ No caption naming the default is rendered, deliberately. Of the two boolean
fields the offline table declares a default for, `escalation.enabled` declares
the OPPOSITE of what installed `@objectstack/spec` 17.3.0 applies to the
omitted key (objectui#6620), so a "(default)" label would have shipped that
wrong claim to authors in words. The seed alone leaves that field rendering
byte-identically to before, which is pinned.

PR #8431's pin is turned green against the repaired behaviour rather than
weakened: its three leg-B rows now assert the new states, and the stored-`false`
rows are kept load bearing so an always-checked control — an implementation
strictly worse than the bug — still fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
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.

2 participants