feat(spec): offer onSuccess and outcomeMessages in Studio's action form; field.picklist joins the own-editor class - #21901
Conversation
…form; picklist joins the own-editor class Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…and outcomeMessages, in four locales Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…ws move Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…'s two new rows Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c31b008230ed57024eb74ee271523f72c0a89681 && git checkout c31b008230ed57024eb74ee271523f72c0a89681
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e6dc7a240617eaeef9a64e788bf6e5561c107f1b 1bd3d8e20922291b8e526dfed7ed04bc3f19c04a && git checkout -B drift-repro e6dc7a240617eaeef9a64e788bf6e5561c107f1b && git merge --no-ff 1bd3d8e20922291b8e526dfed7ed04bc3f19c04a
node scripts/docs-audit/affected-docs.mjs --json e6dc7a240617eaeef9a64e788bf6e5561c107f1b
|
The lint shipped-corpus pins move 83 to 85 predicates and 58 to 62 literal comparisons (outcomeMessages and onSuccess each carry a two-literal type gate). The cli outcome-messages equality now leaves out the metadataForms subtree, where the new form row's own strings match the substring. Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…conciliation-rows Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…conciliation-rows Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21863
Clause-②: no
This lands the director seat's ruling 1A / 2B on the card (record
5995552118, maintainer 「同意」). Studio's action form now offersonSuccessandoutcomeMessages, using the controls ruling5861442317already gave their shapes. Thefield.picklistledger row moves into the ruled class "authored through its own editor". No Zod key, liveness row or gate logic changes.What changed
The form rows (
packages/spec/src/ui/action.form.ts), in the Behavior section aftersuccessMessage/errorMessage:outcomeMessagesis onewidget: 'json'row, spelled like the file'spatchandbodyExtrarows (G1: a map-shaped value takesjson). Its helpText says each key is a snake_caseoutcomevalue the handler returns, and each value is the label shown for that outcome. It also says what happens with no entry, and that a key the handler never returns is never shown.onSuccessis onecompositerow (G2: an object-shaped value takes curated sub-rows), the face thebodyrow above already uses. It has two sub-rows:navigateistype: 'text',required: true, because the Zod requires it inside the block. Its helpText names the route or URL template and its three interpolation scopes (${param.*},${ctx.*},${result.*}), taken from the key's describe.openInistype: 'select'with no inlineoptions. The membernewTabhas a capital letter, andFormSelectOptionSchema.valueis a lowercase system identifier, so an inline list cannot spell it. The select reads its two members (self,newTab) off the served schema instead. Their meanings are in the helpText, and nothing is retyped.visibleWhen: "data.type == 'api' || data.type == 'script'". These are the two types the parse accepts them on (theonSuccesstype refinement andrefuseInertOutcomeMessagesinaction.zod.ts), so the control is shown where the value is accepted.The ledger (
packages/spec/src/system/metadata-form-zod-reconciliation.test.ts):action.onSuccessandaction.outcomeMessagesomitrows are deleted from the "Declared, not enforced yet" group.field.picklistis added to the "authored through its own editor" key list inRULED_ROOT_REASONS, andRULED_EDITORSgains'field.picklist': { surface: "the object designer's shared-picklist picker" }.whyis rewritten. It states theliveverdict (the server resolves the named picklist onto the served field'soptionsand judges a write against that set). It names the surface, and gives the class's citation with the placement ruling inside the same parenthetical:(ruling record 5861442317, #19332; the picker placed there by ruling record 5755653853, #18164 batch #209 item 1 A).5861442317's keys now namefield.picklistand the ruling that admitted it (5995552118).object.externalSharingModelandview.groupsare untouched.The catalogs: the four
packages/platform-objects/src/apps/translations/*.metadata-forms.generated.tsfiles were regenerated withnode scripts/check-i18n-bundles.mjs --write --filter=platform-objects, following PR feat(spec): offer object.imageField in Studio's object form beside nameField #21854:--writeadded the four new keys (outcomeMessages,onSuccess,onSuccess.navigate,onSuccess.openIn). The zh-CN / ja-JP / es-ESlabelandhelpTextleaves were then written in place.--writekept those leaves and dropped the eight provenance entries per locale that the first extract had added to the*.source-hashes.generated.tscompanions.BASE: blob2decb02bf6f1(es-ES),a96e8eb503d5(ja-JP) and31882e433b83(zh-CN), before and after.Three count pins in
platform-objects, each re-taken with one comment line naming the rows:object-lifecycle-panel-echo-decisions.test.ts: 661 → 665 per locale (four new row labels).action-body-panel-echo-decisions.test.ts, the count of composite children the repeater pin does not walk: 5 → 7 (onSuccess.navigate,onSuccess.openIn).['body']→['body', 'onSuccess'].The same file's echo rule ("no leaf on this panel reads its
ensource unless the ledger decided it is an echo") stays green, so all eight new leaves are authored in all three locales.Changeset:
@objectstack/specpatch and@objectstack/platform-objectspatch, in one file (see What ships).Controls (reverse verification, from the committed state)
Both legs ran against HEAD
a5106c1c78, in a throwaway detached worktree at that commit, throughscripts/ablation-replace.mjsin wrap mode. Each leg also had its owntrapthat restores fromHEAD. Command:pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/system/metadata-form-zod-reconciliation.test.ts, throughscripts/pm/os-verify-lock.sh.onSuccessform row deleted (anchor x1 → x0, blobda0842c71614→0c90358ff49f)action.(root): accepted by the Zod but unauthorable in the form — offer it, or add a root ledger entry that records why it is not offered: expected [ 'onSuccess' ] to deeply equal []. Restored: blobda0842c71614== HEAD,git diff HEADemptyfield.picklistremoved from the class's key list (anchor x1 → x0, injected text x0 → x1 read on disk during the run, blobc3bc2007ee93→aa5ba299af81)"authored through its own editor": the root rows giving this reason are not the keys ruling record 5861442317 put in it. A key the ruling did not name needs a ruling of its own; …: expected [ 'field.picklist', …(4) ] to deeply equal [ 'object.actions', …(3) ]. Restored: blobc3bc2007ee93== HEAD,git diff HEADempty,git status --porcelain0 linesThe first run of leg 1 timed out in the lock queue (exit 99, never acquired). The tool restored the file then (blob == HEAD), and the leg was re-run once the lock freed; the reading above is the re-run. The test imports only relative
srcpaths and the spec package has no workspace dependencies, so nodist/is involved in either leg.What ships
Measured with
npm pack --dry-run --json --ignore-scripts, with a positive control:@objectstack/spec:src/ui/action.form.tsis not infiles[](0 entries;src/ui/action.zod.ts, the control, is 1). The form ships compiled, though. The newonSuccessandoutcomeMessageshelpTexts are each in 6 packeddist/files, and the control,successMessage's helpText, is in 6.@objectstack/platform-objects: the new en helpText is in 6 packed files, against 6 for the control. The zh-CN and ja-JP labels are in 6 each, matched as unicode-escaped text, and the es-ES label is in 6. The control, zh-CNsuccessMessage, is in 6.Both packages publish the change, so
skip-changesetdoes not apply.Verification (head
a5106c1c78)All suites ran through
scripts/pm/os-verify-lock.sh.pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: 618 files passed, 18450 tests passed, 1 todo.pnpm --filter @objectstack/spec exec vitest run --project repo --maxWorkers=2: 53 files passed, 902 tests passed.pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2: 59 files, 949 passed. Run before the pins were re-taken, it gave exactly the three failures above (expected 7 to be 5,expected [ 'body', 'onSuccess' ] to deeply equal [ 'body' ],zh-CN positive control: expected 665 to be 661).pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2: 215 files passed, 3 skipped; 27906 tests passed, 19 skipped. No served-schema count moved: the change adds no Zod key.pnpm --filter @objectstack/spec run typecheck: exit 0.tsc -p tsconfig.test.json --listFilesOnlycompiles both changed spec files.pnpm --filter @objectstack/platform-objects run typecheck: exit 0. Itstsconfig.test.jsoncompiles both changed test files.pnpm check:i18n: exit 0,platform-objects in sync (11 bundle(s)).pnpm check:i18n-coverage: exit 0,13 config(s), 621 baselined untranslated string(s), none new. The first attempt exited 3 (PREREQUISITE NOT MET, the example closure unbuilt), and the reading here is from after the build.pnpm --filter @objectstack/spec check:generated: exit 0 (15 artifacts up to date).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 87 commands from 9 paths against merge base607463d73. All 87 ran ona5106c1c78.--ranreconciliation:87 derived famil(ies) accounted for — 87 run, 0 NOT-MEASURED.pnpm check:dual-build-cjs-loadsfirst exited 3 (PREREQUISITE NOT MET, nodist/for nine packages), and exited 0 after a full workspace build. The record keeps that last reading.--no-inline-config --format jsonon the 8 changed TS files gave 8 files, 0 errors, 0 warnings. The population iseslint.config.mjs:971(**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}). The config never enables type-aware linting (eslint.config.mjs:326-328: noparserOptions.project), so the verdict on an untouched file cannot move.bff24d6190(claim revisions5999807822and6000249318; one commit ona5106c1c78): the two red shards ona5106c1c78were this PR's own pins.Test Core (4/6):packages/lint/src/validate-predicate-path-refs.test.tsre-takes the shipped-corpus pins 83 → 85 (predicates) and 58 → 62 (==/!=literal comparisons), each with a history comment. Measured against merge base607463d736by<form>::<field>::<source>: added exactlyaction :: outcomeMessagesandaction :: onSuccess, bothdata.type == 'api' || data.type == 'script'; none removed.Test Core (2/6):packages/cli/test/i18n-extract-outcome-messages.test.tsleaves themetadataForms.subtree out of its.outcomeMessages.selection. The new row'smetadataForms.action.fields.outcomeMessages.{label,helpText,placeholder}paths are form-row text, so the equality stays over the four action-outcome keys. Ablation control: with the clause made a no-op, the file reddens exactly as CI did (expected [ …(7) ] to deeply equal [ …(4) ]).@objectstack/cli352 files, 4683 passed, 2 skipped;@objectstack/lint119 files, 5629 passed; plusobjectql,service-automation,plugin-approvals,cloud-connection,http-conformance,plugin-dev,example-embed-objectql,connector-rest,connector-slackandrest, all green.dispatch-gatesderived 89 commands from 11 paths, and all 89 exited 0. ESLint 0 / 0 on the 2 edited files. No source line moves inpackages/lintorpackages/cli.Acceptance notes
5861442317("cite the ruling record that decided the reason"). The dispatch asked that the row name the placement ruling (5755653853). So the row carries both, in the one parenthetical the class's citation form uses. The two-ruling spelling follows theobject.listViewsrow in the same class (ruling record 5861442317, #19332; per-arm view forms under the #19330 ruling, letter A). The ruling that admitted the key to the class (5995552118) is cited in code comments only.0abd4f9f87,ObjectFieldInspectorhas the inline option editor and no shared-picklist picker; the picker is objectui#10202 phase 2. The row'swhytherefore says the offer "was decided as that picker", not that the picker is there. Until it lands,field.picklistis authorable only in source.SchemaForm.tsxresolves a composite sub-row's schema from the parent'sproperties.selectis a passthrough widget, and with nofieldSpec.optionsthe select falls back to the sub-schema'senum, soopenInshould renderself/newTab. The JSON Schema this head emits (packages/spec/json-schema/ui/Action.json) carriesonSuccessas a plain object, withnavigaterequired andopenIn.enumequal toselfandnewTab, which is the shape that fallback reads.jsonis a passthrough widget too, sooutcomeMessagestakes the same face aspatchandbodyExtra. NOT MEASURED: a browser run of either face.action.form.ts, the four catalogs, the reconciliation test, anyplatform-objectscount pin, and a changeset.action-body-panel-echo-decisions.test.tsis the secondplatform-objectspin the new composite moves. It falls under the claim's "anyplatform-objectscount pin" and under the cross-lane declaration5998582591on [PM seat] domain:engine — 🟢 os-project-manager · session_017ErfyP2Rx7XWHJA27QjyUi #6367. No metadata-protocol pin moved.origin/maingained one commit after the branch point (87712ab823, global search inmetadata-protocol). It touches none of this PR's paths, so it was not merged.Generated by Claude Code