Skip to content

fix(app-shell): the flow inspector's fallback form writes only keys the node contract declares (objectui#11968) - #11979

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11968-flow-fallback-keys
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11968-flow-fallback-keys

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11968

Clause-②: no — this removes one written key and renames another to the key the contract already declares; no published surface widens (triage's reading on the card).

What changes

Studio's flow-node inspector renders the hand-written FLOW_NODE_CONFIG groups (the fallback form) whenever the engine has published no configSchema for a node type: while GET /automation/actions is loading, and when it answers non-ok or throws. Two groups wrote config keys their node's contract does not declare:

  • HTTP request offered an Output variable row writing config.outputVariable. The http executor never reads it, HttpConfigSchema is strict and refuses it by name, and the engine's registration refuses a flow carrying it, so a fallback edit saved a flow that then never registered. The row is removed; the online form never offered it.
  • Notify's Click-through URL row wrote config.url, the converted spelling of actionUrl. It now writes actionUrl, the key NotifyConfigSchema declares and the online form writes.

A flow that already stores either key still opens: the key shows in the Advanced (JSON) block, an unrelated edit keeps it unchanged, and clearing it there leaves a config the contract accepts. Until the author clears it, registration refuses that flow exactly as it did before.

The pin

flow-node-config.contractKeys-11968.test.tsx holds every fallback group to its node's contract, read from the installed @objectstack/spec at test time — no contract is hand-copied:

  • Every key a group can introduce is judged: each field path, plus each objectList column under the list element. A config key is judged against the config contract of every spec node type the inspector resolves to that group (fieldsForNodeType is alias-aware, so http_request is judged as the http node it renders for). Any other root (a sibling block, node-level timeoutMs) is judged against FlowNodeSchema. A [REMOVED] tombstone counts as undeclared.
  • Contract sources: getBuiltinNodeConfigContracts(), SCHEMALESS_NODE_CONFIG_SCHEMAS, LEDGER_DECLARED_NODE_CONFIG_SCHEMAS. Two associations are made by name because the spec exports no table for them, with the key sets still read from the spec: end to EndConfigSchema, and the approval node type to ApprovalNodeConfigSchema.
  • Exempt, each re-measured so an exemption cannot outlive its reason: fields behind the never-matching __legacy__ controller (render-only, they cannot introduce a key); the decision target column (virtual, stripped by applyDecisionBranches); and the two groups no published contract judges, start and legacy_action.
  • Lit controls in the same file show the walk fires at the top level, in a nested block, in an array element, in a sibling block and on a tombstone, and an accounting case requires every group to be judged, write no config key, or sit in the no-contract register. Each finding names the group, the node type and the key.
  • The rendered half reads a flow that already stores a removed key (above), and that the zh overlay follows the renamed notify row.

Files outside the claim's original surface

Seat ruling A on my report (comment 6062596737), recorded in the seat's ACCEPT, extended claim 6060906675 by these three, as a bounded in-place amendment. They are in their own commit 8f32f89:

  • FlowNodeInspector.requiredMarkers-10948.test.tsx: its http row listed the removed Output variable label, and its aria-required case used that row's placeholder as the optional sibling. The sibling is now the Body text control.
  • FlowNodeInspector.declaredUnmapped-11788.test.tsx: its "not brought back online" case read the offline notify url row, which no longer exists. It reads a fixture schema that leaves out topic instead, with the full schema as its lit control.
  • metadata-admin/i18n.ts, FLOW_FIELD_ZH hunks only: the notify overlay is re-keyed url to actionUrl so the renamed row keeps its zh label on both forms, and the dead http outputVariable overlay goes. objectui#11977 adds rows to this file in another region; whichever lands second merges main.

Evidence, at head 1aaf06f

  • Every test importing flow-node-config or naming FlowNodeInspector (57 files, the new pin included) plus packages/core/src/utils/__tests__/column-identity.ratchet.test.ts, repo-root pnpm exec vitest run, under the verify lock: Test Files 58 passed (58), Tests 904 passed | 1 skipped (905). Before the out-of-surface commit the same set read 2 files and 3 tests failed, the three cases named above.
  • Ablation, committed state, through ablation-replace.mjs in wrap mode. A first attempt was a no-op: the replacement contained its anchor, the tool refused and restored, and no test ran. That reading is void. It was redone with another anchor:
    • Leg A (re-add the http Output variable row): Tests 4 failed | 12 passed (16), and the main case printed http_request (node type 'http') writes config.outputVariable: 'outputVariable' is not declared by its config contract.
    • Leg B (notify row back to url): Tests 5 failed | 11 passed (16), naming notify (node type 'notify') writes config.url.
    • Each leg was restored with the blob equal to HEAD and git diff HEAD empty.
    • Those legs ran at 8f32f89, where flow-node-config.ts is byte-identical to the head. The head commit then changed the findings' wording from "by" to "in".
  • turbo run build --filter='@object-ui/app-shell^...': Tasks: 28 successful, 28 total. pnpm --filter @object-ui/app-shell type-check exits 0; it runs tsc --noEmit and tsc -p tsconfig.test.json, and --listFilesOnly lists all three touched test files.
  • Gates, all exit 0: check:spec-symbols, check:metadata-write-doors, check:new-line-citations (0 new citations), check:control-bytes, check-changeset-presence.mjs, check-changeset-no-major.mjs, check:test-path-roots, the three check:vi-mock-*, check:changeset-claims, check:i18n-designer-parity, check:designer-field-key-parity and check:unreferenced-sources. eslint on the 5 touched ts/tsx files: 0 errors, 0 warnings.
  • NOT MEASURED locally:
    • check:spec-floors: it needs a whole-workspace build and reported no-artifact for every package, so it judged nothing here.
    • The eager-closure budget reading is left to CI.

Acceptance notes

None of these was filed:

  • flow-scope nodeOutputRefs adds config.outputVariable for every node type, and its comment lists http and end among the binders. A stored http or end node carrying the undeclared key therefore offers a data-picker variable the engine never binds, and the flow simulator binds it the same way. It is a reader of a key that registration refuses, not a writer, and its reach was not measured. Carrier: none.
  • FlowNodeInspector.endOutputVariable-9335.test.tsx has a comment saying the spec declares the key on the http group. That was never true, and the row is now gone. Test comment only. Carrier: none.
  • apps/console/src/preview-samples.ts, a dev-only sample, still carries an http node with outputVariable. It does not write stored metadata. Carrier: none.
  • Read-only inference, NOT MEASURED: HttpConfigSchema declares config.timeoutMs, while both forms write node-level timeoutMs. mergeServerFlowFields drops the served config field in favour of the node-level one. Which of the two the http executor reads was not measured. Carrier: none.
  • The spec publishes no contract for start-node config, and exports no table for the end and approval associations, so this pin names those two by hand. This is an observation for the spec lane, not a claimed defect.

Generated by Claude Code

claude added 3 commits October 8, 2026 13:49
…he node contract declares (objectui#11968)

The hand-written FLOW_NODE_CONFIG groups render whenever the engine has
published no configSchema for a node type. Two of them wrote config keys
their contract does not declare:

- http_request offered an Output variable row writing config.outputVariable.
  The http executor never reads it, HttpConfigSchema is strict and refuses
  it by name, and registration refuses a flow carrying it, so a fallback
  edit saved a flow that never registered. The row is removed.
- notify's Click-through URL row wrote config.url, the converted spelling
  of actionUrl. It now writes actionUrl.

The new flow-node-config.contractKeys-11968 test judges every key every
fallback group can introduce (field paths and objectList columns) against
the contract of each spec node type the inspector resolves to that group,
read from the installed @objectstack/spec: getBuiltinNodeConfigContracts(),
SCHEMALESS_NODE_CONFIG_SCHEMAS, LEDGER_DECLARED_NODE_CONFIG_SCHEMAS, plus
EndConfigSchema and ApprovalNodeConfigSchema; non-config paths are judged
against FlowNodeSchema. Legacy render-only fields, the decision Target
column and the two groups no contract judges (start, legacy_action) are
exempt, each re-measured. It also reads a flow that already stores a
removed key: shown in Advanced (JSON), kept through an unrelated edit,
clearable there.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU
…-key fix moves (objectui#11968)

Outside the claim's declared file surface, committed separately so the
addition reads on its own:

- FlowNodeInspector.requiredMarkers-10948: the http row listed the removed
  Output variable label among the unmarked ones, and the aria-required case
  used its placeholder as the optional sibling. The sibling is now the Body
  text control.
- FlowNodeInspector.declaredUnmapped-11788: the "not brought back online"
  case read the offline notify `url` row, which now writes `actionUrl`. It
  reads a fixture schema that leaves out `topic` instead, with the full
  schema as its lit control.
- metadata-admin i18n.ts: the zh overlay keyed the notify row `url`; it is
  re-keyed `actionUrl`, so the renamed row keeps its translation on both
  forms, and the http `outputVariable` overlay goes with the row. The pin
  file gains the zh reading for it.

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

Wording only: a tombstone finding read "is a retired tombstone by its
config contract". Every finding now says "in".

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

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 337 chunks) 3311.1 KB 3312.0 KB
Main entry chunk (gzip) 71.4 KB 350 KB
Entry file index-B4owtrSD.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) 18.33KB 6.73KB
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) 587.49KB 141.58KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 268.74KB 68.04KB
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) 41.50KB 11.82KB
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.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
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) 53.43KB 15.54KB
plugin-charts (index.js) 84.71KB 23.25KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 248.57KB 65.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.11KB 45.88KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 248.22KB 68.84KB
plugin-kanban (index.js) 52.22KB 16.38KB
plugin-list (index.js) 117.68KB 29.40KB
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.10KB 11.81KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.09KB 22.94KB
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) 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.25KB
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
objectstack-fleet Bot marked this pull request as ready for review October 8, 2026 15:16
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 8, 2026 15:16
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 0ab2b98 Oct 8, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11968-flow-fallback-keys branch October 8, 2026 15:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants