Repository navigation
fix(app-shell): the SLA escalation switch no longer writes a refused { enabled: false } block, and keeps entered values when switched off (objectui#11660) - #11661
Merged
objectstack-fleet[bot] merged 4 commits intoOct 5, 2026
Conversation
…es the escalation block (objectui#11660)
The flow inspector wrote `escalation: { enabled: false }` when an author
turned escalation off. `ApprovalNodeConfigSchema` refuses that block for its
missing `timeoutHours`, so the flow saved and then failed at the approval node.
The spec names the block's existence as the switch, so off is now no block:
- committing `false` to `config.escalation.enabled` removes `config.escalation`
(siblings untouched, an emptied `config` pruned as before);
- the switch reads an absent block as off instead of applying its declared
default with nothing to apply it to;
- a write that would leave the bare `{ enabled: false }` stub removes it.
The rule is keyed by node type and path (`BLOCK_SWITCHES`), so it holds for
the offline field table and the engine-published configSchema alike.
Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL
Co-authored-by: Claude <noreply@anthropic.com>
…not after a spec export (objectui#11660) `check:spec-symbols` refused `isPlainRecord`: `@objectstack/spec/data` exports a symbol of that name, and a local declaration under it reads as the spec's own definition. Renamed to `isBlockObject`; behaviour unchanged. Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…ase on an existing block (objectui#11660)
The select-half sweep rendered every declaring field on an EMPTY config,
which drew `approval.escalation.action` only because the escalation switch
read its default over no block. No block now reads off and hides the block's
fields, so that one case renders on `{ timeoutHours: 24 }` — the smallest
block the spec accepts, with `action` still unset.
Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL
Co-authored-by: Claude <noreply@anthropic.com>
…ide enabled: false (objectui#11660)
Triage amended the direction under objectui#6499 Option C. Switching off no
longer removes a block that holds values: the ordinary write stores
`enabled: false` beside them, every value kept, including a block with no
`timeoutHours` yet (nothing filled in, nothing deleted). The only removal left
is the bare `{ enabled: false }` stub `ApprovalNodeConfigSchema` refuses, so
switching off with nothing entered, or clearing the last retained value,
writes no block. No block still reads off.
The pins flip to match: the full-block switch-off keeps every value
byte-identical and parses; the round trip covers on and off-with-values; the
nothing-entered, emptied-config, no-hours edge and rule-3-then-Clear cases are
pinned on both descriptor sources. The changeset and docs drop the claim that
no block is the only conforming off.
Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL
Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
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
Size Limits
|
Contributor
Author
ACCEPT: PR objectui#11661, head
|
objectstack-fleet
Bot
deleted the
claude/issue-11660-escalation-off-removes-block
branch
October 5, 2026 23:18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #11660
Clause-②: no
What changes
The flow designer's SLA escalation switch on an approval node no longer writes the refused
escalation: { enabled: false }block, and switching it off keeps the values the author entered. This follows triage's amended direction on the card (comment 6003792818, which replaced the first grade under objectui#6499 Option C):escalationblock used to draw the switch checked and reveal all four escalation fields, because the inspector applied the declared default ('true') with no block for it to apply to. It now reads off, and rendering writes nothing. A block that omitsenabledstill reads on (the objectui#6620 pin is unchanged), and a storedenabled: falsestill reads off.{ enabled: true }alone. Switching back off before anything is entered removes the block instead of writing the{ enabled: false }stub.enabled: falseand keeps every value byte-identical. The existing "inactive values retained" display and its Clear action carry them (objectui#6499 Option C).{ enabled: false }behind. A block that still holds another value is kept.timeoutHours(for example{ enabled: true, action: 'reassign' }before hours are typed) keeps its values when switched off, gainsenabled: false, and has no hours filled in. It was refused atescalation.timeoutHoursbefore the switch and stays refused after it, with the field named.Why the stub was the defect:
ApprovalEscalationSchema.enabledis described as "an escalation block carrying timeoutHours is live unless this is explicitly false — the feature-level switch is whether the escalation block exists at all", andtimeoutHoursis required whateverenabledsays. So both no block and{ enabled: false, timeoutHours: N }are a conforming off, and the runtime skips escalation on an explicitfalse. The bare{ enabled: false }is refused. The inspector wrote that stub when an author switched off a node with nothing entered, because the switch drew on over a node that had no block.Where the change sits
The designer renders the engine-published
configSchemawhen one is served, and falls back to the offline table otherwise. Both sources emit the gate at['config', 'escalation', 'enabled'], so the rule is keyed by node type and path inflow-node-config.ts(BLOCK_SWITCHES). A descriptor member was not needed. Three internal helpers implement it:switchedBlockOf(node, field)says whether a field is the switch or a value inside the switched block.readFieldValue(node, field)is the stored read, except that the switch over an absent block readsfalse(rule 1).controllerAdmitsandFlowNodeInspector's control value both go through it.isBareSwitchedOffBlock(node, switched)detects the stub after any write;FlowNodeInspector.setFieldthen removes the block (rules 2 and 4).Everything else is the ordinary write, which is what keeps entered values on switch-off (rules 3 and 5).
FlowNodeConfigFieldandjson-schema-to-fields.tsare unchanged.Clause-②: noholds: no export, prop, type member or i18n key is added to anything a package entry reaches. The helpers are new exports of the internalinspectors/flow-node-config.tsmodule, which the@object-ui/app-shellentry does not reach. That was measured on the builtdist: walking every relativeimport/export … fromout ofdist/index.d.tsreaches 164 declaration files. The control,inspector-registry.d.ts, is among them. None offlow-node-config.d.ts,FlowNodeInspector.d.tsorFlowNodeConfigField.d.tsis, althoughflow-node-config.d.tsis emitted and declares the new helpers.Measured
Live, this branch (objectstack
mainatcab63967)Setup: the showcase app on a
--freshdatabase, the console from this branch, Playwright Chromium, and a user-authored record-triggered flow with one approval node. The designer rendered the engine-published descriptors (the gate is labelledEscalation).{ enabled: true, timeoutHours: 24, action: 'reassign', escalateTo: 'sre_lead', notifySubmitter: false }sits besidebehavior,lockRecord: falseandmaxRevisions: 2. Switching it off autosaves{ enabled: false, timeoutHours: 24, action: 'reassign', escalateTo: 'sre_lead', notifySubmitter: false }with every sibling key, and gets 200. The node then reads off with all four values on screen and four "kept, not in effect" notices, both right away and when reopened. After publish (200), a new trigger record starts a run withstartsuccess,gatesuccess, statuspaused.{ enabled: true }, which is refused 422 atnodes.1.config.escalation.timeoutHours(see the acceptance notes). Switching back off leaves the designer at "No changes to save", because the node again equals the stored one with noescalationkey. A forced save (a label edit) sends the node with noescalationkey and gets 200.Reassignwithout typing hours: autosave sends{ enabled: true, action: 'reassign' }and gets 422 atnodes.1.config.escalation.timeoutHours. Switching off sends{ enabled: false, action: 'reassign' }, again 422 at the same field. No hours are filled in, and the node reads off with Action retained (one notice).Live, earlier (objectstack
mainat866683f9, unchanged paths)846f982in the worktree, then restored and blob-verified): a node with no block drew the switch checked with all four fields revealed. Switching it off autosavedescalation: { enabled: false }and got 422INVALID_METADATAatnodes.1.config.escalation.timeoutHours.{ enabled: false, timeoutHours: 24 }read off and showed Timeout Hours with the notice. The page made zero metadata writes while open.Unit pins
FlowNodeInspector.escalationOff-11660.test.tsxruns every case on both descriptor sources. The first is the offline table. The second isgetApprovalNodeConfigJsonSchema()from the installed@objectstack/spec/automation, which is what objectstack'splugin-approvalshands its approval node descriptor asconfigSchema. The file pins:enabledreads on (lit control). A storedfalsereads off.enabled: falsewith every value byte-identical (JSON.stringifyequal, key order included). The sibling config keys (approvers,behavior,minApprovals,onEmptyApprovers,lockRecord,maxRevisionsand an Advanced extra) are byte-identical, and the block parses. Four values show as retained. A block that omitsenabledkeeps its value and gainsenabled: false.{ enabled: true }alone, with noactionornotifySubmitterdefault. Switching back off with nothing entered leaves noescalationkey, and an emptiedconfigis pruned.{ enabled: true, action: 'reassign' }switched off becomes{ enabled: false, action: 'reassign' }. It gains notimeoutHours, andApprovalNodeConfigSchemarefuses it atescalation.timeoutHoursboth before and after.{ enabled: true, timeoutHours: 24 }switched off, then Clear on the hours, leaves no block.ApprovalNodeConfigSchemawith every value. Switched off, it carriesenabled: falsewith every value identical, and it parses.Two existing pins moved:
FlowNodeInspector.inactiveRetained.test.tsx, "offers a clear button that removes the retained key", used to pin the refused{ enabled: false }left after Clear. It now pins no block (rule 4).FlowNodeInspector.declaredDefault.test.tsx, "every select-kind declaring field renders its declared default", used to renderapproval.escalation.actionon an empty config. That only drew because the gate read on over no block. That one case now renders on{ timeoutHours: 24 }, withactionstill unset.Ablation
Each write branch was ablated separately on the committed state, through
ablation-replace.mjs(anchor hit once, blob changed, restore proven equal to the HEAD blob withgit diff HEADempty). The runs cover the new file plusFlowNodeInspector.inactiveRetained.test.tsx, 37 tests.inactiveRetainedpin.false, the behaviour this round retires): 10 failed | 27 passed. The failures are the two rule-3 cases, rule 5, rule 4 on a rule-3 block, and the round trip, each on both sources.The read half (
readFieldValue) is unchanged from the first round. Its ablation there turned 6 pins red.Gates
Run from the repo root at
2f7d398, the branch head.origin/mainis still846f982, this branch's base, so the pre-PR merge oforigin/mainwas a no-op (Already up to date), and there is no merge commit.pnpm exec vitest run packages/app-shell/Test Files 1008 passed | 1 skipped (1009),Tests 10019 passed | 9 skipped (10028)pnpm --filter @object-ui/app-shell type-check(dependency closure built first)tsc -p tsconfig.test.json --listFileslists the new test file (1 hit)pnpm --filter @object-ui/app-shell lint0 errors, and no warning on a line this branch addspnpm --filter @object-ui/app-shell builddist completeness: 1 package(s) completecheck:spec-symbols,check:new-line-citations(0 new citation(s)),check:control-bytes,check:changeset-claims,check:pending-changeset-literals,check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:test-path-roots,check:metadata-write-doors,check:unreferenced-sources,check:installed-pin-claims,check:designer-field-key-parity,check:i18n-designer-paritynode scripts/check-changeset-presence.mjs/check-changeset-no-major.mjs@object-ui/app-shell: patchThe full lint farm and every other
check:*script are left to CI.Acceptance notes
{ enabled: true }, and with autosave on that save is refused 422 atnodes.1.config.escalation.timeoutHoursuntil hours are typed. Timeout Hours shows no required marker, because the objectui#10948 probe andclientValidation.tsask the installed@objectstack/spec17.6.0, whoseflowNodeConfigRefusalsdoes not judge the block yet. Built at objectstack866683f9, that judge namesescalation.timeoutHoursfor{ enabled: true },{ enabled: false }and{}, and nothing for no block. So the existing probe should light the marker once objectui installs a spec release containing feat(spec)!: the build doors judge an approval node config against its declared contract, whole — an undeclared key or a refused value is refused with a location objectstack#21893. No gate was built here.{ enabled: false }that the old inspector already stored reads off and is not rewritten on render or by an edit elsewhere on the node. After feat(spec)!: the build doors judge an approval node config against its declared contract, whole — an undeclared key or a refused value is refused with a location objectstack#21893, such a flow's next save is refused atescalation.timeoutHoursuntil the author touches the escalation block. Switching on and back off (rule 2), or filling the hours, clears it.Generated by Claude Code