fix(app-shell): give the flow end node a typed control for config.message - #9444
Merged
Conversation
…message`
`EndConfigSchema` cross-validates `outcome` and `message` in BOTH directions
through a `superRefine`: `outcome: 'refused'` REQUIRES a `message`, and
`outcome: 'completed'` — including an omitted `outcome`, since the declared
default resolves to `completed` — REFUSES one.
The inspector's `end` group offered a typed control for `outcome` and none for
`message`. Since the Outcome field became a closed two-option dropdown,
`refused` is one click away, and picking it with nothing else done produced a
flow that fails to load: the one key that outcome requires was authorable only
through the Advanced (JSON) block. A blocked authoring path, not a missing
nicety.
The group now offers "Why the run was refused" — a `textarea` gated on
`showWhen: { field: 'outcome', equals: ['refused'] }`. Kind, label, placeholder
and help are derived from the installed spec: it describes `message` as a
`{token}` template "interpolated at run time exactly like a screen
`description`", and that field is a `textarea` in this same table, as is the
sibling `message` key on `notify`. No `defaultValue` — the key is `.optional()`
with no `.default()`.
The gate keeps BOTH directions authorable. An unset `outcome` resolves through
the declared `completed` default, so the field stays off screen until the author
picks `refused`; a STORED `message` re-shows it regardless, which is the only
way to clear a stale message after switching back. Clearing deletes the key
rather than storing `''`, which this schema refuses under both outcomes.
The Outcome help (English and the zh-CN overlay) no longer says the message is
set in Advanced, since it no longer is. The `inactiveRetained` census was
re-measured at landing rather than copied: `end` becomes a new gated bucket,
8 -> 9 buckets, 33 -> 34 gated fields, 24 -> 23 ungated types.
The same group's `outputVariable` row is deliberately untouched.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011QreXiyMEqKLN4U5daMPVa
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
|
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 #9336
EndConfigSchemacross-validatesoutcomeandmessagein both directions through asuperRefine. The inspector'sendgroup offered a typed control foroutcomeand nonefor
message— so an author who pickedrefusedin the UI could not produce a valid flow,because the one key that outcome requires was authorable only through the Advanced (JSON) block.
A blocked authoring path, not a missing nicety.
The probe, re-run at this branch's head
Measured at
1d1ca96(basee2feb13) against the installed@objectstack/spec17.4.0 —not inherited from the card. Accepted rows are printed beside the refusals, so the refusals are
a reading rather than a dead probe.
Three rows the card did not have, each of which changed the repair:
{message:'x'}— a message with nooutcomeat all is refused too. The completed-siderule bites on the effective outcome, so the declared
completeddefault carries it. This iswhy the new field is gated on the controller rather than shown always: an always-on box would
trade this defect for the contract's other direction.
{outcome:'refused', message:''}and{outcome:'completed', message:''}— the emptystring is refused under both outcomes (
min(1)on one side, the no-op rule on the other).So a control that committed
''on clear would ship an unloadable flow either way. It does not:setAtPathdeletes the leaf on an empty commit, and that is now pinned.{outcome:'refused', outputVariable:'result'}reports onlyunrecognized_keys— thesuperRefinenever runs while an unrecognized key is present. See the separability note below.The repair
One descriptor in the
endgroup:Every choice is derived from the installed spec, not picked:
textarea— the spec's ownmessagedescription says it is "interpolated at run timeexactly like a screen
description", and that field is atextareain this same table; so isthe sibling
messagekey onnotify. Two independent derivations agreeing..describe()opens "Why the run was refused", and boththe describe and the refusal message give that exact example string.
defaultValue—messageisz.string().min(1).optional()with no.default(),and a declaration there is read as a claim about the installed spec.
The gate keeps both directions authorable. An unset
outcomeresolves through the declaredcompleteddefault, so the field stays off screen until the author actually picksrefused; astored
messagere-shows it regardless (objectui#6499's stored-value rule), which is the onlyway an author can clear a stale message after switching back to
completed.The Outcome field's
help(English and the zh-CN overlay) no longer says the message is setin Advanced — this PR makes that sentence false, so it goes.
Census, re-measured at landing
flow-node-config.inactiveRetained.test.tscarries three exact counts.endgains its firstshowWhenfield and so becomes a new bucket. Re-measured here rather than copied, as triage ruledfor the counts objectui#9277 / objectui#9278 share:
gatedByType.sizetotalGated/checkedungatedTypes.lengthAblation — prediction written before the run, then the run
Predicted, in writing, before running (recorded in the working notes): direction turn-red
(not diagnostics-grow, not inversion); 9 failed tests across 2 files — 7 of the new pin's 10,
plus the 2 census tests. The 3 predicted to stay green were named in advance: the two rows that
read only the installed spec, and the
outputVariableseparability row.Mutation: delete only the four-line
messagedescriptor. Proven on disk before the run —HEADblob4ea15bf4f6cd51087ef442e63da6ea4a2acfc3c1, mutated blob2f832f385ae62ba5575a7f689d637c8347ebb8b0, marker count 1 → 0.Observed:
Tests 9 failed | 15 passed (24)— the predicted 7 + 2, and the three namedsurvivors survived. Prediction matched exactly.
Restore proven by state, never by re-running the test:
git checkout HEAD -- PATH(never abare
git checkout --, which would restore the mutation out of the index), then blob hash4ea15bf4f6cd51087ef442e63da6ea4a2acfc3c1==HEADblob andgit diff HEADempty, working treeclean. The mutation script carried
trap … EXIT INT TERMthroughout.Separability from objectui#9335 — the answer is YES, they are separable
objectui#9335 is a live, unclaimed defect in this very group: the same form writes
config.outputVariable, a keyEndConfigSchemarefuses by name. This PR does not touch itand claims no repair of it; that card stays open. Evidence that
messagegot its typed controlwithout touching or breaking that row:
outputVariableline is byte-identical to its state onorigin/main—git diffshows it onlyas unchanged context.
messageis gated byshowWhenonoutcome;outputVariableisungated and reads no controller. They share no descriptor, no controller and no code path.
{outcome:'refused', message:'why', outputVariable:'result'}is still refused, foroutputVariablealone. Note the direction: an unrecognized key masks thesuperRefine(itnever runs), so objectui#9335 hides this card's defect, never the reverse. Repairing
messagetherefore cannot make objectui#9335 harder to see or harder to repair; on a node where an author
used the Output variable box, objectui#9335's refusal simply arrives first.
outputVariablerow is still present, stilltext, still ungated, and that the three-key configis still refused — so a future repair of objectui#9335 has to come past that assertion on purpose.
objectui#9335 remains open and deliberately untouched by this PR.
Clause-②: no — re-declared against the actual diff
The seat predicted
no; the diff is the fact, and the diff agrees.flow-node-config.tsis notexported from
packages/app-shell/src/index.ts(its only occurrence there is inside a comment —a reading, not a dead grep: that file is 470 lines with 92
exportoccurrences and 84 relativeimports). The package publishes one subpath. The diff adds no exported symbol and moves no
signature: one element appended to an internal descriptor array, one key added to an internal zh
overlay, plus tests and a changeset. The changeset gate confirms mechanically: "0 of them a
manifest whose published contract moved". And
messageis a key the published contract alreadydeclares and requires — teaching the form to author it is pulling back to the declared
contract. No
needs:contract-review.Verification
vitest, repo root)metadata-admintreepnpm --filter @object-ui/app-shell type-checktsc --noEmit+tsc -p tsconfig.test.json;--listFilesconfirms the new suite is in the checked set — 1 hit, so the green covers it)pnpm --filter @object-ui/app-shell lintcheck-changeset-presence/check-changeset-no-majorcheck-control-bytescheck-governed-queue-guard --testDependency closure built first (
pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build);the first type-check attempt reported unbuilt siblings, which is NOT MEASURED, not a red gate.
Repo-wide
pnpm lint/ fullpnpm testare CI's runs, not claimed here.Generated by Claude Code