Skip to content

finding(app-shell): four flow-node fields declare a defaultValue the installed spec applies none of — the reconciliation ledger stops at the escalation block #9109

Description

@os-tesla

Measured on origin/main 9f5c017 against installed @objectstack/spec 17.4.0, while implementing objectui#6830 arm A's select half (branch claude/issue-6830-select-declared-default). Filed unassigned by that execution seat — recording, not claiming, and deliberately not ridden into that PR, whose fence is the render path.

The observation

FlowConfigField.defaultValue (packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts) is documented in its own doc comment as the spec default for this key, and it is acted on as one at three sites: controllerAdmits resolves an unset showWhen controller through it, the boolean control seeds its checked state from it (objectui#8451), and — as of objectui#6830 arm A's select half — a select control states it on the trigger.

Four of the ten declarations have no spec counterpart at all. Re-derived field by field, each row read from the installed Zod rather than from the table:

declaring field kind declares installed spec (17.4.0) for an omitted key
approval.behavior select first_response .default('first_response') — agrees
approval.onEmptyApprovers select admin_rescue .default('admin_rescue') — agrees
approval.escalation.action select notify .default('notify') — agrees
approval.escalation.enabled boolean true .default(true) — agrees (objectui#6620)
approval.escalation.notifySubmitter boolean true .default(true) — agrees
approval.maxRevisions number 3 .default(3) — agrees
http_request.method select GET z.string().optional()applies none
screen.mode select create optional enum, applies none
wait.waitEventConfig.eventType select timer REQUIRED enum, applies none
boundary_event.boundaryConfig.eventType select error REQUIRED enum, applies none

Probe, reproducible in a worktree with the workspace installed:

node --input-type=module -e "
import * as A from '@objectstack/spec/automation';
console.log(A.HttpConfigSchema.safeParse({ url: 'https://x' }).data);            // no method
console.log(A.ScreenConfigSchema.safeParse({}).data);                            // no mode
console.log(A.ApprovalNodeConfigSchema.safeParse({ approvers: [{ type: 'user', value: 'u1' }] }).data);
console.log(A.ApprovalEscalationSchema.safeParse({ timeoutHours: 24 }).data);
console.log(A.FlowNodeSchema.safeParse({ id: 'n1', type: 'wait', label: 'L', waitEventConfig: {} }).error.issues[0]);
"

Positive control is in the same output: the four agrees rows come back with their materialised values from the very calls whose other keys come back absent, so the absences are real absences and not a dead probe.

Why it is a defect and not a nit

The repo already rules on this shape. flow-node-config.spec-reconciliation.test.ts carries an assertion named and the form declares no default the spec does not apply, whose own comment reads:

The other direction, and not symmetric decoration: a defaultValue with no spec counterpart is a claim about the contract with nothing behind it, and it is ACTED ON — it resolves a showWhen controller and seeds a boolean control off a value the runtime never applies.

That ledger is scoped to the approval-escalation block. It walks field.path[0] === 'config' && field.path[1] === 'escalation' only, so all four unbacked declarations sit outside it and nothing reddens. The scoping was deliberate at the time (objectui#6620 was on hold and the author did not want to arm it from an unrelated PR); objectui#6620 is closed and the reason is spent.

Two of the four are worse than merely unbacked: waitEventConfig.eventType and boundaryConfig.eventType are required by the spec. There is no runtime default to state — an omitted key does not behave as timer or error, it fails to parse. The declaration currently tells the designer that leaving the key alone is a working configuration, in two ways the author can see:

  • controllerAdmits reveals waitEventConfig.timerDuration (gated equals: ['timer']) on a fresh wait node that has stored no eventType at all — live today, predating arm A;
  • since arm A's select half the trigger states Timer as its placeholder, which is the same claim in words.

http_request.method and screen.mode are the milder pair: optional keys whose executor very likely does apply GET / create-mode, but that behaviour lives in the framework and the spec's Zod does not carry it, so the table is asserting something this repo cannot check.

Not proposing the fix, since the right end is a product call

Two ends, and they point in opposite directions:

  • Widen the ledger and delete the unbacked declarations. Cheapest, and it is what the existing assertion's reasoning implies. Cost: wait loses the showWhen reveal of timerDuration on a key-less node, which may be worth keeping on usability grounds even though the spec does not back it.
  • Widen the ledger and back the declarations upstream — make the spec .default() what the executors actually do, then the table is honest and the reveal survives. Needs a framework change for method, and for the two required enums it is a real product question (should a wait node default its event type?).

Either way the mechanical half is the same and is the part worth filing: flow-node-config.spec-reconciliation.test.ts's two default-direction assertions should walk every declaring field rather than the escalation block, with per-node-type spec schemas, so a declaration outside that block cannot go unchecked. Today four of them do.

Secondary observation, same family

approval.maxRevisions declares defaultValue: '3' and a hand-written placeholder: '3'. The number control reads the placeholder, never defaultValue, so the two agree today only because someone typed 3 twice. The spec is the source for one of them and for neither in the code. Not worth its own card; it dissolves if the number control reads defaultValue the way the boolean and select controls now do.

Generated by Claude Code in session session_01UzHd6hDYatoDn17BuwKxnZ, domain:ui execution seat, out of objectui#6830's fence.


Generated by Claude Code

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpackage: app-shellpriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions