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
Measured on
origin/main9f5c017against installed@objectstack/spec17.4.0, while implementing objectui#6830 arm A's select half (branchclaude/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:controllerAdmitsresolves an unsetshowWhencontroller through it, thebooleancontrol seeds its checked state from it (objectui#8451), and — as of objectui#6830 arm A's select half — aselectcontrol 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:
approval.behaviorfirst_response.default('first_response')— agreesapproval.onEmptyApproversadmin_rescue.default('admin_rescue')— agreesapproval.escalation.actionnotify.default('notify')— agreesapproval.escalation.enabledtrue.default(true)— agrees (objectui#6620)approval.escalation.notifySubmittertrue.default(true)— agreesapproval.maxRevisions3.default(3)— agreeshttp_request.methodGETz.string().optional()— applies nonescreen.modecreatewait.waitEventConfig.eventTypetimerboundary_event.boundaryConfig.eventTypeerrorProbe, reproducible in a worktree with the workspace installed:
Positive control is in the same output: the four
agreesrows 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.tscarries an assertion named and the form declares no default the spec does not apply, whose own comment reads: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.eventTypeandboundaryConfig.eventTypeare required by the spec. There is no runtime default to state — an omitted key does not behave astimerorerror, 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:controllerAdmitsrevealswaitEventConfig.timerDuration(gatedequals: ['timer']) on a freshwaitnode that has stored noeventTypeat all — live today, predating arm A;Timeras its placeholder, which is the same claim in words.http_request.methodandscreen.modeare the milder pair: optional keys whose executor very likely does applyGET/ 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:
waitloses theshowWhenreveal oftimerDurationon a key-less node, which may be worth keeping on usability grounds even though the spec does not back it..default()what the executors actually do, then the table is honest and the reveal survives. Needs a framework change formethod, and for the two required enums it is a real product question (should awaitnode 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.maxRevisionsdeclaresdefaultValue: '3'and a hand-writtenplaceholder: '3'. The number control reads the placeholder, neverdefaultValue, so the two agree today only because someone typed3twice. 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 readsdefaultValuethe way the boolean and select controls now do.Generated by Claude Code in session
session_01UzHd6hDYatoDn17BuwKxnZ,domain:uiexecution seat, out of objectui#6830's fence.Generated by Claude Code