Repository navigation
feat(spec,lint,cli): a screen field's option labels and a flow's terminal toasts have keys in the flows translation face - #22555
Conversation
…e terminal toasts WIP: schema, option key, collision refusal and translateFlow overlay. Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
… the option collision refusal Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
…keys Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
… liveness ledger Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
…s for the flows face keys Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
…he flows face now keys Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
…n and toast keys Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
…ow-user-read-slots
📓 Docs Drift CheckThis PR changes 3 package(s): 7 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 — 145 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 70790eb170e4c3088b6ed0b9f5decb42229f58b7 && git checkout 70790eb170e4c3088b6ed0b9f5decb42229f58b7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6a3f82efa7d588267eb2e52c06a73c56315c636a 63f2467e48d0b5a9d450d8eb3ae6ebcb8c4e0669 && git checkout -B drift-repro 6a3f82efa7d588267eb2e52c06a73c56315c636a && git merge --no-ff 63f2467e48d0b5a9d450d8eb3ae6ebcb8c4e0669
node scripts/docs-audit/affected-docs.mjs --json 6a3f82efa7d588267eb2e52c06a73c56315c636a
|
…rowing, with its ADR-0087 disposition Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Rendered 2026-10-10T01:01Z on the read-only shape: card #22507 (body and all eight comments), PR #22555 (body, the 27-file list, and the net diff against the merge base 4638625 — file list and diff agree at +1592/−97), and the check-runs on the head. The head's parent is the merge of ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each named right or wrong.
No governed path in the 27 files; head repo equals base repo; about 1,689 changed lines (under the 5,000 line and the 3,000 line). ② Semver level
③ Boundary flagsEach dev flag and
Checks on the head, read 2026-10-10T00:59Z: 23 success, 4 skipped, 0 failure, 12 in progress. Pending, and ⛔ not read as green: Implemented-by: VERDICT: PASS Generated by Claude Code |
Part of #22507
Clause-②: yes (narrowing)
This PR keys two of the three user-read flow slots that had no key in the
flowstranslation face: a screen field's option labels and the flow's terminal toasts. It also adds the family pin. The third slot, a screen'sdescription, is server-rendered, so this PR stops there and reports it for a decision (see "Where this PR stops"). That is why the first line isPart ofrather than the closing keyword the dispatch named: merging this must leave the card open for thedescriptionhalf.What changes
@objectstack/specTranslationDataSchema.flowsgains three keys.flows.FLOW.successMessageandflows.FLOW.errorMessage: the terminal toasts. Aliasessuccess/successText/error/errorTextrename onto them.flows.FLOW.screens.NODE_ID.fields.FIELD.options.VALUE: a screen field's option labels.choices/valuesrename ontooptions.String(value), through one function:flowScreenFieldOptionKey(@objectstack/spec/automation). This is the seat's choice in the claim. It is also the console select's own key for an option (SelectItem value={String(o.value)}in objectui'sScreenView), and the coerciontranslateActionalready applies to an action param's inline options.ScreenFieldConfigSchemarefuses two options of one field whose values collide as text, such as1and"1",trueand"true", or a repeated value.value. At the flow door it readsnodes[1].config.fields[0].options[1].value.SCREEN_FIELD_OPTION_VALUE_COLLISION, names both options by position and value, and gives the remedy.37c711449by loading theapp-crm,app-todoandapp-showcaseflows). hotcrm atf0afcbda0and hotclm at14c899fc4were read by inspection: their option values are string literals or come from an object field's string options.translateFlowapplies the toasts, only where the flow authors one, so a bundle cannot add a toast. It also applies the option labels, option by option, falling back to the authored label.resolveFlowScreenFieldOptions(bundle, flowName, nodeId, field, opts)is new, for a runner that holds a servedScreenFieldSpec.FLOW_TERMINAL_MESSAGE_KEYSis new, the key list the extractor and the schema pin share.FLOW_SCREEN_FIELD_NO_OPTIONSis removed, the toast guidance is removed, and thedescriptionguidance now says why it is unkeyed and what the screen face carries.translation/flows.successMessageandflows.errorMessage, bothplanned(contract-first split, asscreenswas), and dated notes onflowsandflows.screensfor the option map.@objectstack/lint:validateTranslationReferencesjudges both new key families.translation-target-unknown(error).translation-option-key-unknown(warning). This is the same rule and severity as the object-field and action-param legs.@objectstack/cli:os i18n extractand the coverage gate scaffold and demand the new keys in theflowbucket.Examples:
app-crm(zh-CN toasts) andapp-todo(zh-CN and ja-JP option labels) are translated, socheck:i18n-coverageholds at its baseline. Without this it grew 101 → 103 and 106 → 126. These files are outside the claim's file surface; the report declares it.The enumeration, and the family pin
packages/spec/src/system/flows-translation-face.test.tswalks the flow schemas mechanically and lists every text-bearing leaf. Each leaf is held against one recorded disposition:keyed(a face key that must parse and survive),noReader,elsewhere(another declared route, which must exist) orowed(pinned exactly, shrink-only).FlowSchema(minusnodes),FlowNodeSchema(minusconfig),ScreenConfigSchemaandEndConfigSchema.approval), plus everyFLOW_NODE_TEXT_SLOTSentry.It also fails on a stale row, and on a face key that no slot fills.
Flow.labellabelFlow.successMessage/errorMessagetitle, and a screen node'slabelas its fallback headingscreens.NODE_ID.titlelabel/placeholder/inlineHelpTextoptions[].label…options.VALUE(new)message(refused)refusals.NODE_ID.messagedescriptiontitle/messagetemplatepath (sys_email_template, per recipient locale)decisionOutputs[].labelFlow.description, edgelabel, variables, ids, predicates,defaultValue,defaults,objectName, framework/lock fields, …noReadermeans that no person running the flow reads the text.Flow.descriptionis drawn by Studio's metadata viewer only, so it is recorded that way, with that reason.It bites (ablation, each leg through
scripts/ablation-replace.mjswith an anchor hit of 1 → 0, the pin run under the verify lock, then restored with blob == HEAD and an emptygit diff HEAD). Predicted: red. Observed: red.subtitle: z.string().optional()onScreenConfigSchema(blob5b4cf3fe→ac16dd5a→5b4cf3fe) gives 1 failed / 6 passed:expected [ 'screen.subtitle' ] to deeply equal [].successMessagekey (blob8d5dd093→05480fb1→8d5dd093) gives 2 failed / 5 passed: the keyed-row check and the face-fill check.Where this PR stops: a screen's
descriptionis server-renderedThe dispatch orders a stop before touching
service-automationif a slot proves server-rendered, anddescriptiondoes.service-automation's screen executor rendersconfig.descriptionper run throughrenderTextSlot({{ }}holes over the flow's variables) and serves the result asScreenSpec.description.An overlay on the served string would draw a translated hole literally. The face's existing
titlekey already has that boundary, as FlowRunner's own header records. Templated descriptions are real: app-todo'ssuccess_screen(Task "{{ subject }}" created successfully!) and hotclm'scontract_intake"First version" screen (description: '{templateNote}').So the key is not added. The options for the seat:
renderTextSlot, therefusalsroute. This touchesservice-automation, and the same change should movetitletoo.{{ }}hole in a translateddescription. A templated description then stays untranslatable.title's silent boundary.The pin records
descriptionas owed, so whichever lands must delete that row.What the objectui runner must read (for the seat's objectui card)
flows.FLOW.screens.NODE_ID.fields.FIELD.options.VALUE: inFlowRunner.tsxlocalizeScreen, set each field'soptionstoresolveFlowScreenFieldOptions(bundle, flowName, screen.nodeId, field, { locale: language })beforeScreenViewdraws itsSelectItems.FLOW_SCREEN_FIELD_COPY_KEYS: the option map is not a string key and is deliberately not in that list.flows.FLOW.successMessage: inFlowRunner.tsx, the terminal-success toast (outcome.successMessage || t('flowRunner.completed', …)), viatranslateFlow({ name: flowName, successMessage: outcome.successMessage }, bundle, { locale: language }).successMessage.flows.FLOW.errorMessage: inutils/flowResponse.tsflowFailureMessage, which readsdata.errorMessage, fromerror.details.errorMessageon the 400FLOW_FAILEDpath. It is read by FlowRunner's resume and by both launch hosts throughjudgeFlowLaunch(useConsoleActionRuntime,RecordDetailView).description/inlineHelpText/FlowSchema.successMessageare off the surface are stale.Verification (final head
c17df03f0, which mergesorigin/main4638625e0)pnpm --filter PKG run typecheckexited 0 for@objectstack/spec(source, scripts, and the test layer: 52 files / 246 errors / 135 pinned signatures held),@objectstack/lintand@objectstack/cli.local: 637 files, 18977 passed and 1 todo.repo: 54 files, 915 passed.validate-translation-references.test.tsalone: 162 passed.test/i18n-*files, 24 files / 285 passed. The newi18n-flow-toast-option-coverage.test.tsis among them. The integration tier is declared to CI.pnpm --filter @objectstack/spec check:generatedreports "All 15 generated artifacts are up to date", measured against a dist built by that run after the merge. The dropped-refinements count is unchanged at 688 / 226: the collision check lives in the existingScreenFieldConfigSchemarefinement.node scripts/pm/dispatch-gates.mjs --commandsderived 115 commands at this head. All 115 were run with their exit codes recorded, and--ranreports "115 run, 0 NOT-MEASURED (a DERIVED zero)".check:skill-examplesandcheck:dual-build-cjs-loadsfirst refused (exit 3, PREREQUISITE NOT MET). They exited 0 after building the named packages.pnpm check:platform-checklistexits 1, and it is not this diff. It readsABSENT SYMBOL … attachment-access-hooks.ts#canEditandSYMBOL ANCHORS LOST — 27 … floor is 28inareas/attachments-storage.json. A clean worktree oforigin/main4638625e0exits 1 with the same two lines. This diff touches neither file..tsfiles:eslint --no-inline-config --format jsonreports 17 files, 0 errors and 0 warnings.eslint.config.mjsenables no type-aware linting, so this diff cannot move a verdict on an untouched file.Acceptance notes
planned. The objectui runner reads neither yet, so until its card lands a translated option or toast is stored and the authored text is drawn. Meanwhile the extractor demands them, the same contract-first windowscreenshad.titlekey has the same server-render boundary asdescription(a translated{{ }}hole draws literally). Option A above would fix both.BULK_PARAM_NO_OPTIONS). An action param's inline options are keyed byString(value)with no collision refusal. Neither is in this card.Generated by Claude Code