Skip to content

feat(app-shell): the screen-flow dialog renders a screen field's declared bound, help text and lookup target (objectui#9248) - #11188

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-9248-screen-field-bound-help-reference
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-9248-screen-field-bound-help-reference

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #9248
Clause-②: no

The three screen-field keys that objectstack-ai/objectstack#17306 (ruling A, 「声明即强制」) added now render in the console's screen-flow dialog. They are read straight off the installed @objectstack/spec 17.5.0 ScreenFieldSpec, with the spec's own spellings: min / max, inlineHelpText, reference. There is no local spelling and no alias. That card holds itself open until these keys render, so this PR supplies the rendering half.

Implemented by the domain:ui seat-2 dispatch of session https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec, branch claude/issue-9248-screen-field-bound-help-reference.

What changed

  • ScreenView / ScreenFieldInput
    • A numeric input (number or currency) gets min / max as native attributes. They are never written onto a date or text input.
    • inlineHelpText is drawn as its own element under the control. Every control arm names it in aria-describedby: text, number, textarea, checkbox, select and lookup. The placeholder is left alone, because the browser clears it on the first keystroke.
    • A type: 'lookup' field with a non-empty reference renders the object form's own LookupField, fed the screen's dataSource. It is reached through getLazyFieldWidget('lookup') from @object-ui/fields, the same seam ActionParamDialog uses, so no new picker is written. It has its own Suspense boundary, because the Studio preview renders this body with none around it.
    • A lookup with no reference keeps today's plain input. The spec's ScreenFieldSpec.reference doc names that fallback for a run suspended before the key existed.
    • New pure helper screenFieldBoundViolations(screen, values).
  • FlowRunner: the submit check refuses an out-of-bound value beside the required check and names the field. The message uses the existing pack keys validation.min / validation.max / validation.formInvalidJoiner, so no i18n key is added. The header's "deliberately not localized" list gains inlineHelpText (premise 6).
  • One widening outside the dispatch's two named files, declared here: metadata-admin/previews/screen-spec.ts (toScreenFields). It now carries the same four keys into the Studio preview's ScreenSpec, each only when it holds the spec's declared type. Premise 5 below explains why.
  • One changeset, '@object-ui/app-shell': patch.

Premises re-measured

Measured at objectui BASE 3ab51503b, against the installed @objectstack/spec 17.5.0 and objectstack tag commit 0f6dcac5e9.

  1. Spec. ScreenFieldConfigSchema parses {type:'number',min:1,max:10}, {inlineHelpText} and {type:'lookup',reference:'account'}. It refuses a lookup with no reference (a custom issue on path reference). help and helpText are refusals carrying a rename prescription (unrecognized_keys, "Did you mean helpText → inlineHelpText"), not accepted aliases. objectui reads none of them.
  2. Type. The 17.5.0 ScreenFieldSpec in @objectstack/spec/contracts declares min, max, inlineHelpText and reference, all optional. They are read off the verbatim re-export, with no local widening.
  3. Rendering reach. All three FlowRunner hosts pass dataSource: useConsoleActionRuntime, RecordDetailView, and the console's FlowRunsPage, which passes its adapter. The Studio preview passes its adapter too. So the lookup reaches a data source through ScreenView's existing prop, and the FlowRunner lookup pin runs through that path.
  4. Engine rule. Measured on validateScreenInputs in service-automation's screen-input-contract.ts at 0f6dcac5e9. The bound is compared only when the value is a present finite number, whatever the field's type. A value below min or above max is refused, so the bounds themselves pass (inclusive). A field hidden by visibleWhen is skipped. screenFieldBoundViolations applies exactly this comparison, and a pin covers each clause.
  5. Falsified. The Studio preview does reuse ScreenView, but buildScreenSpec projects each authored field down to name / label / type / required / visibleWhen in toScreenFields. The new keys therefore never reached the preview's renderer. The evidence is the preview-projection ablation leg below: with the BASE projection restored, the three preview pins go red. Following the ruling's intent that the keys ship with their rendering, the projection now carries the four keys. This is the only file outside the dispatch's surface.
  6. Translation overlay. inlineHelpText is not overlaid. FLOW_SCREEN_FIELD_COPY_KEYS is ['label', 'placeholder'] at 17.5.0 and on objectstack main. The spec's translation face also refuses an inlineHelpText entry loudly (unrecognized_keys on flows.F.screens.S.fields.Q), and the spec documents this as a not-yet that needs a ruled widening of that face. The runner walks the constant, so it will follow with no edit here.

Tests

All of these ran on HEAD 7d9c24c28, from the worktree root.

  • New pins: ScreenView.boundHelpReference-9248.test.tsx, __tests__/FlowRunner.bounds-9248.test.tsx and metadata-admin/previews/ScreenPreview.boundHelpReference-9248.test.tsx. Together they hold 24 tests, one lit leg per key, each with its control:
    • min: 1, max: 10 puts both native bounds on the input. Submitting 11 or 0 is refused naming the field, and no resume request is sent. Submitting 10 passes and resumes with {qty: 10}. An empty optional field is not refused and resumes with {}. CONTROL: the same field without the keys resumes with 11.
    • inlineHelpText: 'Shown below' renders below the input (document order), is linked by aria-describedby, becomes the accessible description, and stays on screen while the input holds a value. This holds on every control arm.
    • type: 'lookup', reference: 'account': the picker lists account records from the data source. The pin asserts the queried object name, and it renders with no SchemaRendererProvider, so only ScreenView's dataSource prop can feed it. The same test runs through FlowRunner's host dataSource and in the Studio preview. CONTROL: a lookup without reference stays a text box.
    • CONTROL: a field carrying none of the keys has no bounds, no aria-describedby, no help element and no picker.
  • Consumer suites: every test file that names ScreenView / FlowRunner / screen-spec / buildScreenSpec / ScreenPreview, enumerated with git grep -l.
    • Group A: Test Files 31 passed (31), Tests 378 passed (378).
    • Group B (the RecordDetailView suites): Test Files 37 passed (37), Tests 228 passed (228).
  • pnpm --filter @object-ui/app-shell type-check, run after the closure build turbo run build --filter='@object-ui/app-shell^...' (28 of 28 tasks), exits 0. Both tsc --noEmit and tsc -p tsconfig.test.json ran, and --listFilesOnly shows all three new pins in the test program.
  • Root scripts/__tests__/ (run once because the diff adds a markdown changeset): Test Files 177 passed | 2 skipped (179), Tests 5370 passed | 2 skipped.

Ablations

These were one-shot runs; no permanent file came from them. Each leg went through objectstack's scripts/ablation-replace.mjs in WRAP mode: the anchor had to hit exactly once, the mutation was proven on disk by a blob change, the three pin files were run, and the restore was proven (blob equals HEAD and git diff HEAD is empty). An outer trap restored the tree on EXIT, INT and TERM. No dist is involved: the pins import the subject by relative path from src.

leg mutation pins red controls
bound-attr delete the min= / max= props on the Input 4 of 24 (ScreenView x2, FlowRunner x1, preview x1) green
submit-refusal if (outOfBounds.length) becomes always-false 2 of 24 (11 refused, 0 refused) green
help-text helpId becomes always undefined 4 of 24 green
lookup the lookup branch is unreachable 3 of 24 (ScreenView, FlowRunner, preview) green, including "no reference stays a text box"
preview-projection remove the four carried keys in toScreenFields (the BASE projection) 3 of 24 (all preview pins) green

Every leg ended with ok restored and a clean tree.

The control, measured byte-for-byte. A throwaway probe rendered key-less fields covering every arm, with empty and with filled values, plus the whole FlowRunner dialog. It ran against HEAD and against BASE's ScreenView.tsx / FlowRunner.tsx, with the blob verified equal to 3ab51503b before the run. Result: cmp byte-identical, 21501 bytes each. The files were restored to their HEAD blobs, and git diff HEAD came back empty.

Gates

  • eslint on the 6 lintable changed files exits 0 (0 errors). This is a declared narrowing, and all three pieces of evidence are here:
    • The population comes from eslint's own config. The 7th changed file, the .changeset markdown, answers "File ignored because no matching configuration was supplied."
    • The count, 6, comes from the --format json result length.
    • Invariance: --print-config shows no parserOptions.project / projectService, so linting is not type-aware. No rule under eslint-rules/ reads the filesystem. So this diff cannot move any untouched file's verdict.
  • node scripts/check-changeset-presence.mjs exits 0 ("6 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)").
  • check:control-bytes exits 0.
  • check:new-line-citations exits 0 (0 new citation(s)).
  • check:i18n-keys exits 0.
  • check:changeset-claims exits 0 and is report-only. It names two pending changesets that mention screen-spec.ts. Both paragraphs are about that file's spec imports, which this change leaves alone, so both still hold.
  • check:pending-changeset-literals, markdown-test-inputs.mjs --audit and check-changeset-no-major exit 0.
  • check-governed-queue-guard --test over the changed paths answers NOT GOVERNED.
  • check:designer-field-key-parity exits 0.
  • NOT MEASURED: check:eager-closure, because it needs a full console build, and CI owns it. ScreenView's one new static import is @object-ui/fields, which both runtime hosts already import statically through ActionParamDialog. LookupField itself stays behind the registry's lazy chunk.

Acceptance notes

  • Out of scope, reported to the seat and not changed here: the Studio preview's toScreenFields also drops options, placeholder and defaultValue, all keys that ScreenView renders.
    • A one-shot probe on the real ScreenPreview gave, for a select field with options and a text field with placeholder: preview combobox 0 | textbox 1 | placeholder 0 against runtime combobox 1 | textbox 0 | placeholder 1.
    • Named producer: objectstack examples/app-todo flow task.flow.ts, screen node screen_1, fields priority (select with options and defaultValue: 'normal') and category.
    • It is left alone here because defaultValue carries the engine's {token} interpolation, which makes the class fix non-mechanical. It also predates this change: the byte-identical control above covers the key-less path.
  • inlineHelpText is drawn untranslated until the spec's per-field translation face lists it (premise 6). The spec owns that step. Noted, not filed.
  • ScreenView.tsx eslint warnings go from 12 to 13: one more react-refresh/only-export-components, for the new exported helper. The file's existing exported helpers already trip the same rule, and lint.yml sets no --max-warnings.
  • Docs: no objectui guide page documents the flow screen-field contract. That contract is the spec's.
  • Not measured: whether the Studio flow designer's screen-field editor offers columns for the new keys. That is an authoring surface outside this card.

Generated by Claude Code

…ared bound, help text and lookup target (objectui#9248)

The spec's screen field carries `min` / `max`, `inlineHelpText` and
`reference` (object-field spellings). `ScreenView` now puts the bound on the
numeric input as native attributes, draws the help text under the control and
names it in `aria-describedby`, and resolves a `type: 'lookup'` field's picker
from `reference` through the shared `LookupField` widget fed the screen's
data source. `FlowRunner`'s submit check refuses an out-of-bound value naming
the field, with the engine's resume comparison (`screenFieldBoundViolations`).

The Studio preview's `buildScreenSpec` projection carries the same four keys,
so the preview draws them as the runtime does.

Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
Co-authored-by: Claude <noreply@anthropic.com>
…ectui#9248)

`react-hooks/static-components` refuses a component created during render;
the shared registry caches one lazy `LookupField` per type, so the module-scope
constant is the same component `ActionParamDialog` renders.

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

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 2 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/10864-app-shell-spec-floor.md

  • names packages/app-shell/src/views/metadata-admin/previews/screen-spec.ts → packages/app-shell/src/views/metadata-admin/previews/screen-spec.ts — edited by this change

    packages/app-shell/src/views/metadata-admin/previews/screen-spec.ts imports predicateSlotRefusal from @objectstack/spec/automation; packages/app-shell/src/views/metadata-admin/previews/simulator/flow-sim-validate.ts imports structuralConditionRefusal from the same subpath plus EVALUATED_EXPRESSION_SOURCE_REQUIRED and EvaluatedExpressionSchema from @objectstack/spec/shared. @objectstack/spec was declared as ^17.3.0, so any resolution landing on 17.3.0 (which the range allows) gets a module that names exports its own declared spec dependency does not have.

.changeset/5920-flow-runner-translation-overlay.md

  • names screen-spec.ts → packages/app-shell/src/views/metadata-admin/previews/screen-spec.ts — edited by this change

    ⚠️ Dated note, 2026-09-28 — the declared floor has since moved again — objectui#10864. At this change @object-ui/app-shell declared ^17.3.0, and the paragraph above is kept as the reading of that change: 17.3.0 was in fact the first release exporting the resolver family this fix uses. Later in this same release the floor was raised again, to ^17.4.0, because two other shipped app-shell sources (screen-spec.ts, flow-sim-validate.ts) import four symbols (predicateSlotRefusal, structuralConditionRefusal, EVALUATED_EXPRESSION_SOURCE_REQUIRED, EvaluatedExpressionSchema) that 17.3.0 does not export — a defect this changeset's own range did not introduce and did not need to guard against. @object-ui/app-shell now declares ^17.4.0.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with e978ed5ea (merge-base with origin/main): 6 file(s) changed outside .changeset/, read against 1794 pending declaration(s) that publish a body (2401 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3570.6 KB 3607.4 KB
Main entry chunk (gzip) 149.6 KB 350 KB
Entry file index-DfFOUYC-.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) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
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.17KB 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) 40.70KB 10.94KB
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.13KB 7.95KB
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) 559.46KB 134.18KB
core (index.js) 9.94KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.28KB 63.04KB
fields (index.js) 261.19KB 66.27KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
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.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.32KB 11.09KB
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) 13.58KB 4.90KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.25KB 2.17KB
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.01KB 3.93KB
plugin-calendar (index.js) 52.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 138.73KB 37.05KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 234.78KB 62.36KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 172.30KB 44.19KB
plugin-gantt (index.js) 172.43KB 42.85KB
plugin-grid (index.js) 230.25KB 63.22KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.82KB 28.67KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 32.26KB 9.42KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 90.43KB 22.76KB
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.81KB 3.58KB
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) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.17KB 2.73KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
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) 22.61KB 7.40KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
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.26KB
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 (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) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.82KB 7.15KB
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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7d9c24c282675af3c35b0a06f71dd5b8d745b280
Local-runs: none

Inputs: card #9248 (body and its five comments: unlock 5852969116, unlock 5904174295, claim 5906696061, dev report 5907589570, claim amendment 5907646725); PR #11188 (body, the 7-file list, the net diff against main); the 43 check-runs on the head read with filter=latest: 40 success, 3 skipped, 0 failed, 0 in progress at judgment (the eight test shards, Spec Main Shape Gate and the Test aggregate all completed while this review waited). Context reads at the head and at objectstack 0f6dcac5e9 went through contents; nothing was built, run or re-run.

① Derived judgments

  1. The four keys read straight off the installed 17.5.0 ScreenFieldSpec, no widened local copy — RIGHT. ScreenView.tsx still re-exports ScreenFieldSpec verbatim from @objectstack/spec/contracts (that hunk is untouched) and the diff reads field.min / field.max / field.inlineHelpText / field.reference on that type; the contract at 0f6dcac5e9 (packages/spec/src/contracts/automation-service.ts) declares exactly those four, all optional. The local ScreenSpec alias (only fields widened) is unchanged. No alias and no screen-local vocabulary. help / helpText / hint / tooltip occur once, in a header comment naming them as spec refusals, and are read nowhere; ScreenFieldConfigSchema maps them onto inlineHelpText in the aliases table of a strict object, so they are refusals carrying a rename prescription, as the dev's premise 1 says.
  2. Native min / max on the numeric input — RIGHT. Written only when htmlType === 'number' (number and currency); a date or text input receives undefined, so the attribute is omitted. Pinned, including a lone min: 0.
  3. screenFieldBoundViolations against the engine — RIGHT, parity read on validateScreenInputs in packages/services/service-automation/src/screen-input-contract.ts at 0f6dcac5e9. The engine's bound pass skips unless typeof value === 'number' && Number.isFinite(value), refuses a value below min or above max (the limits themselves pass), skips a visibleWhen field whose visibility probe is not true, and leaves an absent value to required. The helper applies the same four clauses over visibleScreenFields, and a pin covers each. One nuance, pre-existing and not introduced here: the engine treats an UNEVALUABLE predicate as hidden while the client's visibleScreenFields fails open (renders, so compares) — the same split required already has at base, and it follows the card's "same on-screen list" rule. The engine's separate invalid_type pass on type: 'number' needs no client twin: the numeric input emits Number(...) or undefined, never a string.
  4. inlineHelpText below the control, named in aria-describedby — RIGHT. Drawn as its own paragraph element after the control (document order pinned) with id ff-NAME-help, and describedBy is threaded to the Input, the Textarea, the Checkbox, the SelectTrigger and the lookup widget. Pinned as the accessible description on the text, number, textarea, boolean and select arms. The lookup arm receives the prop through the same toDomProps spread that carries its pinned aria-required, but has no description pin of its own — noted, not a defect.
  5. The lookup — RIGHT. getLazyFieldWidget('lookup') returns the cached React.lazy loader of LookupField from the static loader map in @object-ui/fields, the seam ActionParamDialog reads; it is resolved once at module scope, after the registry module has evaluated, and rendered behind its own Suspense boundary because ScreenPreview renders ScreenView with none. dataSource is passed as the explicit prop, which LookupField ranks first (explicit prop, then the field, then SchemaRendererContext), fed from ScreenView's existing dataSource prop; LookupField reads field.reference as its target object. Pinned with no SchemaRendererProvider, through ScreenView, through FlowRunner's host dataSource, and in the Studio preview, each asserting the queried object NAME.
  6. A lookup with no reference keeps a text input — RIGHT, and the brief's question is answered by the spec itself: the ScreenFieldSpec.reference doc on the wire contract says the key stays optional there because the shape also describes a ScreenSpec rehydrated from a run suspended before the key existed, and that a client meeting one "falls back to a plain input — the pre-#17306 behaviour, kept reachable on purpose rather than turned into a client-side crash". The REQUIRED-when-lookup rule lives on the authoring side (ScreenFieldConfigSchema's superRefine, refusing on path reference), so a parsed flow never reaches the client without it. The fallback is the contract's prescription, not a local choice.
  7. The ratified screen-spec.ts projection carry — RIGHT. toScreenFields carries the four keys typeof-guarded to the spec's declared types (number, number, string, string); a half-typed row adds nothing (pinned). Ratified at 5907646725; the file is on the PR's list.
  8. The byte-identical key-less control — RIGHT on the code read. With no key present, helpId is undefined so no help element renders, aria-describedby / min / max are undefined and omitted, and the lookup branch is not entered; a type: 'lookup' field WITHOUT reference keeps the text Input it always had. The CONTROL pins (two children per field block, no -help id, no lookup trigger, no bounds, no aria-describedby) run in the green shards; the dev's byte-for-byte cmp (21501 bytes at HEAD and at BASE) is its own local run and is consistent with the read.
  9. The 24 pins and the 5 ablation legs — CONSISTENT. 14 in ScreenView.boundHelpReference-9248.test.tsx, 7 in __tests__/FlowRunner.bounds-9248.test.tsx, 3 in previews/ScreenPreview.boundHelpReference-9248.test.tsx; all three files are in the test program CI ran. The reported red counts (4 / 2 / 4 / 3 / 3) match which pins assert each mutated site; the legs are the dev's local runs and are not re-derived here.
  10. i18n — RIGHT. validation.min, validation.max and validation.formInvalidJoiner exist in packages/i18n/src/locales/en.ts with the {{field}} / {{min}} / {{max}} holes the runner fills; no key is added; check:i18n-keys and Lint are green.

② Semver level

Changeset .changeset/9248-screen-field-bound-help-reference.md, '@object-ui/app-shell': patch — ACCEPTED. What the diff publishes is behaviour on an existing surface. @object-ui/app-shell's index exports FlowRunner and the types FlowRunnerProps / ScreenFlowState / ScreenSpec / ScreenFieldSpec (the last two being the spec's re-exports, unchanged); ScreenView, ScreenFieldInput, the new screenFieldBoundViolations and ScreenFieldBoundViolation are not on the package index and the package's exports map is the root entry only, so the new helper widens no public surface, and FlowRunnerProps is untouched. Clause-②: no is on the PR body and is RIGHT: no objectui-owned accept set or public surface moves; the accept set is the spec's. The feat title with a patch bump is the weaker of two legitimate readings (among the pending changesets 35 feat( entries declare minor and 4 declare patch; no rule binds the pair, the fixed group versions together, and the body frames the change as declared-but-inert keys made to render). Changeset Declaration, Changeset Bump Policy, Changeset Claim Re-read and Changeset Fixed Group Check are green.

③ Boundary flags

Deviations named in dev report 5907589570:

  • The widening to metadata-admin/previews/screen-spec.ts — ANSWERED: ratified by claim amendment 5907646725. The projection dropped the keys at base (premise 5 falsified), the carry is typeof-guarded, and the preview-projection leg with the three preview pins is its evidence.
  • git fetch origin main in the shared /home/user/objectstack checkout — ANSWERED as a process deviation, not a contract defect. No file was edited and no branch moved, but it advanced the shared origin/main (41dcf1188 to df67985b0) and the common-dir FETCH_HEAD, the exact hazard the multi-agent rules list for anyone restoring from a moving ref in that checkout. A safe spelling existed (a contents/...?ref=main read, or a fetch into a self-named ref). Declared, bounded, and of no effect on this diff; the seat should re-derive any origin/main-relative reading it made in that checkout during that window.
  • The lookup reuse — ANSWERED: not a deviation; it is the seam the dispatch named.

open_questions: none declared. The brief's own question (the fallback for a lookup without reference) is answered in ① item 6.

Out-of-scope findings:

  • The Studio preview also drops options / placeholder / defaultValue — CONFIRMED and ESCALATED to the seat. The named producer exists at 0f6dcac5e9: examples/app-todo/src/flows/task.flow.ts, screen node screen_1, field priority (select with options and defaultValue: 'normal') and field category (select with options). The gap predates this PR (the base projection already dropped them) and defaultValue carries the engine's suspend-time interpolation, so it is a separate card, which the amendment says the seat handles at accept.
  • inlineHelpText is untranslated — ANSWERED: FLOW_SCREEN_FIELD_COPY_KEYS is the spec's face and lists label and placeholder; overlayFieldCopy walks that constant, so the runner follows the day the spec lists the key, and the runner header's "deliberately not localized" list now names it. Nothing to do in objectui; the carrier is a spec-side widening in objectstack, for the seat to file if it wants one.
  • The flow designer's screen-field editor was not measured — ANSWERED: an authoring surface outside this rendering card. Note that check:designer-field-key-parity (green) gates the EMIT side only, keys a designer writes that the spec refuses; its header says under-coverage stays a separate card, so its green says nothing about whether the editor offers the four keys.

Implemented-by: claude/issue-9248-screen-field-bound-help-reference
Reviewed-by: session_011p7ikEivgXefNDaE5S5Uec

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 30, 2026 09:05
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit 81778b9 Sep 30, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-9248-screen-field-bound-help-reference branch September 30, 2026 09:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants