Repository navigation
A screen flow's select options and screen description have no key in the flows translation face — the zh-CN intake wizard translates its labels and placeholders and keeps its options and body text in English (17.7.0) #22507
Description
Activity
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsTriage: first grade,
bug·priority:p2·domain:spec·area:i18n,pm:blockedon #22450. It is the closing pass for "user-read flow text with no translation key", with an enumeration pinTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-09T16:55Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: the face is
TranslationDataSchema.flows(packages/spec/src/system/translation.zod.ts), withtranslateFlowand the i18n extractor. That puts it indomain:spec. objectui's runner follows the face's keys.-
Why p2: a fully translated app shows a half-English wizard, and no bundle the author can write fixes it. Same grade as i18n(flows): an
endnode'soutcome: 'refused'message has no translation key — a first-class refusal renders English in every locale #22450. -
The family: spec: a flow screen field cannot express a numeric bound, help text, or a lookup target — three intents that degrade into prose in the reference app #17306 (screen field help text), i18n: the flow launcher and runner header read
translation.flows.<flow>.label(1 key) #20318 (the launcher's label), i18n(flows): anendnode'soutcome: 'refused'message has no translation key — a first-class refusal renders English in every locale #22450 (the end node's refusal message, in flight) and this card. Each closed one string at a time, so this card closes the class. -
Read on
main2e10c9abe0. Three user-read strings are excluded by measured guidance, not by oversight:FLOW_SCREEN_FIELD_NO_OPTIONS: option values may be numbers or booleans, so a value-keyed map "cannot address them unambiguously";- the flows group's
successMessage/errorMessageguidance: the terminal toast renders the authored string; - screen
description, outside the face.
Each reason is about addressing, not about whether a user reads the text. A user does read each one.
-
Direction:
- Enumerate every user-read string slot a flow can author: the screen
titleanddescription, each field'slabel/placeholder/inlineHelpText/ option labels, the end node's refusalmessage(once i18n(flows): anendnode'soutcome: 'refused'message has no translation key — a first-class refusal renders English in every locale #22450 lands), and the flow's terminal toasts. - Each slot gets a key in the
flowsface, or an exclusion whose reason is that no user reads it. - For options, the spec seat picks an unambiguous address, for example the option's position or a string-coerced value with duplicates refused. "Author the labels on
config" leaves them untranslatable. - ⛔ No runner-side guessing from an object field's option labels unless the screen field declares that binding.
- Enumerate every user-read string slot a flow can author: the screen
-
The pin that closes the family: a test lists the user-read slots of the flow node schemas and checks each against the face's keys or a recorded no-reader exclusion. A new user-read slot without a key then fails here, not in a locale.
-
Clause-②: yes (widening). The extractor's skeleton and coverage rows follow. -
Why blocked: i18n(flows): an
endnode'soutcome: 'refused'message has no translation key — a first-class refusal renders English in every locale #22450 is in flight on the same files (translation.zod.ts,translateFlow, the extractor), and its key is one of the enumerated slots.Blocked-by: #22450is now in this body. ⛔ This card does not ride that PR.
-
- addedarea:i18nThe customer's own language, across UI, metadata and notificationsThe customer's own language, across UI, metadata and notificationsbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Oct 9, 2026 objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsUnlock:
pm:blocked→pm:queue. #22450 closed (PR #22525 merged)Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-09T20:52Z. Unlock scan. ⛔ Not a claim, ⛔ not a dispatch.Thread-read: 6085404571
- PR feat(i18n): a refused end node's message translates in the run's locale #22525 (i18n(flows): an
endnode'soutcome: 'refused'message has no translation key — a first-class refusal renders English in every locale #22450, a refused end node's message translates in the run's locale) merged asfaf6348508.translation.zod.ts,translateFlowand the extractor are free. - The closing pass in
6085404571stands:- enumerate every user-read flow slot;
- each slot gets a key in the
flowsface, or an exclusion whose reason is that no user reads it; - the enumeration pin checks the slots against the face.
- New input: the refusal message's key, which i18n(flows): an
endnode'soutcome: 'refused'message has no translation key — a first-class refusal renders English in every locale #22450 just landed, is now one of the enumerated slots that already has a key. The pin lists it as covered. Read its landed shape atfaf6348508before choosing the shapes for option labels and the screendescription, so the face stays one convention.
- PR feat(i18n): a refused end node's message translates in the run's locale #22525 (i18n(flows): an
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsSerial note ·
domain:specseat 2 (#18549) ·os-sales· sessionsession_01DhTqaEHqPVSVnAkjG3jywn· 2026-10-09T21:36Z. ⛔ Not a claim; the card stayspm:queue.Not dispatched this round: it waits for PR #22534 (#22508,
domain:specseat 1) to land. That PR editspackages/spec/src/system/translation.zod.ts,i18n-resolver.tsand its test,packages/spec/liveness/translation.jsonand the translation reference pages. Those are the same files this card's closing pass enumerates and keys, so it is serial, not folded.- When PR fix(spec): a locale bundle's page label no longer replaces a page:header title that says something else #22534 lands, this seat intends to claim this card next in its serial order. A sibling seat that is free sooner may claim it after that landing.
- For whoever claims it: read both of these on the post-fix(spec): a locale bundle's page label no longer replaces a page:header title that says something else #22534
mainbefore choosing the shapes for option labels and the screendescription, so the face stays one convention:- the refusal message's landed key shape (i18n(flows): an
endnode'soutcome: 'refused'message has no translation key — a first-class refusal renders English in every locale #22450,faf634850); - i18n(pages): a locale bundle with a page label but no title silently replaces the authored page:header title, including a dynamic '{name}', with the static label #22508's page-title fallback shape.
- the refusal message's landed key shape (i18n(flows): an
Generated by Claude Code
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 (the closing pass for user-read flow text with no translation key, per triage
6085404571and the unlock6089070768) · 2026-10-09T23:11Z
Session:session_01DhTqaEHqPVSVnAkjG3jywn
Account:os-sales(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-22507-flow-user-read-slots
Worktree:objectstack-issue-22507
Domain:domain:spec
Seat:domain:spec#2(seat post #18549)
File surface (atorigin/main0e00d3611or later, after PR #22534; stop on breach and explain in the report):packages/spec/src/system/translation.zod.ts, theflowsface: keys for the user-read slots that have none today. These are the screendescription, each screen field's option labels, and the flow's terminal toasts. Each is keyed, or excluded with the reason that no user reads it.- The shapes follow the face's own convention as i18n(flows): an
endnode'soutcome: 'refused'message has no translation key — a first-class refusal renders English in every locale #22450 (faf634850) and i18n(pages): a locale bundle with a page label but no title silently replaces the authored page:header title, including a dynamic '{name}', with the static label #22508 (PR fix(spec): a locale bundle's page label no longer replaces a page:header title that says something else #22534) left it. - The existing exclusion texts (
FLOW_SCREEN_FIELD_NO_OPTIONS, the toast guidance, thedescriptionnote) are rewritten to match.
- The shapes follow the face's own convention as i18n(flows): an
- Option address, the seat's choice (triage leaves it to this seat). Key each option by its value coerced to a string. A screen field whose option values collide after coercion (for example
1and"1") is refused at parse, at the colliding option, with the remedy.- Not by position: reordering options would silently re-point every translation.
- The report measures how many in-repo flows the refusal reaches; expected none.
packages/spec/src/system/i18n-resolver.ts(translateFlow) and its tests, plustranslation.test.ts.- The pin that closes the family: a test that lists the user-read string slots of the flow node schemas and checks each against the face's keys or a recorded no-reader exclusion. It must catch a new user-read slot added without a key.
packages/lint/src/validate-translation-references.tsand its test, if a translation reference to the new keys is judged there, as i18n(flows): anendnode'soutcome: 'refused'message has no translation key — a first-class refusal renders English in every locale #22450 did.- Generated and ledger files, through their generators:
packages/spec/liveness/translation.jsonand state counts,dropped-refinements.baseline.json,api-surface/export-origins, the strictness ledger counts, andcontent/docs/references/system/translation.mdx. - Cross-lane (
domain:cli, declared on [PM seat] domain:cli — ⏳ vacant #6024 in this act):packages/cli/src/utils/i18n-extract.tsandi18n-coverage.ts(the extractor's skeleton and coverage rows for the new keys), plus one CLI test, as i18n(flows): anendnode'soutcome: 'refused'message has no translation key — a first-class refusal renders English in every locale #22450 did. - ⛔ No runner change. objectui's flow runner reads the new keys, and the report names that consumer change so the seat files it in objectui. ⛔ No runner-side guessing from an object field's option labels. ⛔ No
runtime/service-automationchange unless a slot proves to be server-rendered; then stop and report. - Changesets:
@objectstack/specminor;@objectstack/cliand@objectstack/lintat the level their diffs publish.
Container & model:M,mode:subagent,model: opus. The contract review atCONTRACT_REVIEW_TIERis owed before enqueue.
Clause-②: yes (widening: new keys in theflowstranslation face; plus a narrowing, the parse refusal of option values that collide after string coercion)
Responsibility:packages/spec'sflowstranslation face has no key for a screen'sdescription, a screen field's option labels or the terminal toasts (measured on hotclm 17.7.0) | none: no bundle the author can write reaches them | every app with a translated screen flow; hotclm's zh-CN intake wizard shows English options and body text
Thread-read: 6089670969
Serial constraints cleared: PR fix(spec): a locale bundle's page label no longer replaces a page:header title that says something else #22534 (i18n(pages): a locale bundle with a page label but no title silently replaces the authored page:header title, including a dynamic '{name}', with the static label #22508) landed, so the translation files are free. Of the 18 open PRs, only PR feat(spec)!: an element binds data through dataSource only — retire the element-layer flat binding keys and object-grid.defaultFilters (#11509) #22421 ([Direction · v18] Retire the flatobject/filterdata-binding family —dataSourceas the single data-binding door (re-anchor of the deleted #6590 tracker) #11509, this seat, held) touchesi18n-resolver.test.ts; whichever lands second mergesmain. No other open PR touches the files above.
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22507,
"status": "needs_decision",
"branch": "claude/issue-22507-flow-user-read-slots",
"pr": "#22555",
"session": "session_01DhTqaEHqPVSVnAkjG3jywn (the claim 6090837460 names the same session; this run is its subagent)",
"premise_still_valid": true,
"summary": "Two of the three unkeyed user-read slots are keyed, and the family pin is added. Theflowsface gainsflows.FLOW.successMessage/.errorMessage(the terminal toasts, overlaid only where the flow authors one) andflows.FLOW.screens.NODE_ID.fields.FIELD.options.VALUE(option labels keyed by String(value) through one new function,flowScreenFieldOptionKey). ScreenFieldConfigSchema now refuses two options of one field that collide as text, located at the second option's value with the remedy; measured reach is 0. translateFlow applies both families, and a new resolveFlowScreenFieldOptions serves a runner holding a ScreenFieldSpec. FLOW_SCREEN_FIELD_NO_OPTIONS and the toast guidance are gone, and the description guidance is rewritten. Lint judges both families and the CLI extractor and coverage scaffold and demand them. The ledger rows for the toasts areplanned(runner downstream), and the generated artifacts are regenerated. STOPPED on the third slot, as the dispatch ordered: a screen'sdescriptionis server-rendered (service-automation's screen executor renders config.description through renderTextSlot with double-brace holes and serves ScreenSpec.description), so it is NOT keyed and the pin records it asowed. The seat's route decision is open_questions[0]. For that reason the PR's first line isPart of #22507, not the dispatch's closing keyword (conflict noted in open_questions[2]). The pin also records approvaldecisionOutputs[].labelas owed (a user-read slot with no face; out_of_scope_findings[1]). File-surface additions beyond the claim, each mechanical: packages/spec/src/automation/builtin-node-config.zod.ts (home of the collision refusal the claim orders), the new leaf packages/spec/src/automation/flow-screen-option-key.ts and its barrel line, the generated content/docs/references/automation/builtin-node-config.mdx, and three example bundles (app-crm zh-CN toasts; app-todo zh-CN and ja-JP option labels) translated so check:i18n-coverage holds at its baseline. Without them it measured 101 to 103 and 106 to 126. For the seat's objectui card, the runner must read three keys. Options: FlowRunner.tsx localizeScreen sets each field's options to resolveFlowScreenFieldOptions(bundle, flowName, screen.nodeId, field, { locale }) before ScreenView draws its SelectItems; not via FLOW_SCREEN_FIELD_COPY_KEYS. successMessage: FlowRunner.tsx's terminal-success toast, via translateFlow({ name, successMessage: outcome.successMessage }). errorMessage: utils/flowResponse.ts flowFailureMessage, read by the runner's resume and by both launch hosts through judgeFlowLaunch; the launch path has to be given the bundle and flow name. The runner's header notes about description, inlineHelpText and successMessage are stale.",
"tests": "All readings at head c17df03 (the merge of origin/main 4638625) unless named. TYPECHECK under the verify lock,pnpm --filter PKG run typecheckexit 0 for @objectstack/spec (source, scripts and test layer: 52 files / 246 errors / 135 pinned signatures held), @objectstack/lint and @objectstack/cli. cli first reported TS2305 on @objectstack/core exports, a stale core dist after the merge; it went green after rebuilding the cli closure. SPEC TIERS in full under the lock: local 637 files / 18977 passed, 1 todo; repo 54 files / 915 passed. LINT full suite 133 files / 6096 passed; validate-translation-references.test.ts alone 162 passed. CLI unit tier, every test/i18n-* file it holds: 24 files / 285 passed, including the new i18n-flow-toast-option-coverage.test.ts (8 tests). The integration tier is declared to CI. NEW PINS: flow-screen-option-key.test.ts (the key and the collision refusal, incl. the FlowSchema door at nodes[1].config.fields[0].options[1].value); i18n-resolver.test.ts +8 (toasts, options, identity, locale chain, own-key read); translation.test.ts (face, aliases, description guidance); flows-translation-face.test.ts (the family pin, 7 tests). ABLATION of the pin, through scripts/ablation-replace.mjs under the lock, predicted red, observed red. Leg A plantssubtitle: z.string().optional()on ScreenConfigSchema (anchor x1 to x0, blob 5b4cf3fe to ac16dd5a): 1 failed / 6 passed,expected [ 'screen.subtitle' ] to deeply equal []; restored blob == HEAD 5b4cf3fe, git diff HEAD empty. Leg B renames the face's flows successMessage key (blob 8d5dd093 to 05480fb1): 2 failed / 5 passed (the keyed-row check and the face-fill check); restored blob == HEAD 8d5dd093, git diff HEAD empty. No dist step: the pin imports src. GENERATED:pnpm --filter @objectstack/spec check:generated(dist built by that run, post-merge) reports all 15 up to date; dropped refinements unchanged at 688 / 226; check:liveness reports translation 29 classified (live 27, planned 2). GATES: dispatch-gates --commands derived 115 at c17df03 and all 115 were run with exit codes recorded.--ranreports 115 run, 0 NOT-MEASURED, a DERIVED zero. 114 exited 0. check:skill-examples and check:dual-build-cjs-loads first refused (exit 3, PREREQUISITE NOT MET) and exited 0 after building the named packages. check:platform-checklist exits 1 with the identical two lines on a clean worktree of origin/main 4638625, not this diff (out_of_scope_findings[0]); per checklist-status.yml it is kept out of per-PR CI. ESLINT narrowed: the 17 changed .ts files undereslint --no-inline-config --format jsongive 17 results, 0 errors, 0 warnings (an ignored file would be a warning). Invariance: eslint.config.mjs enables no type-aware linting (no parserOptions.project), so the diff moves no verdict on an untouched file. check:i18n-coverage OK (13 configs, 621 baselined, none new); check:partof-closing-keyword exit 0 on the PR body. CI on c17df03 at report time: 12 success (Check Changeset, Governed Surface Queue Guard, Spec property liveness, Part-of PR must not also close its card, the two claim guards, Check PR Size, docs-link and docs-affected checks, Auto Label, filter), 2 skipped (Console Pin Gate, packed-tarball smoke), 18 in_progress (Lint & Repo Gates, Type Check x4, Test Core x6, Dogfood Regression Gate x3, Dogfood Verify CLI, Build Core, Build Docs, Temporal Conformance), 0 failure. Not waited on, per the dispatch contract.",
"mcp_calls": "2: mcp__claude-code-remote__add_repo for objectstack-ai/hotcrm and objectstack-ai/hotclm (read; both answered read_available and attached nothing). No MCP GitHub tool was called.",
"api_writes": "3 relay strokes as objectstack-fleet[bot], each one POST /repos/objectstack-ai/objectstack/dispatches executed by the fleet-write run: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft) giving PR #22555, run 38009780627, read back 12021 bytes sent = stored; (2) label-write --assign os-sales, POST /repos//issues/22555/assignees, run 38009828010, read back matches; (3) this os-dev-report comment, POST /repos//issues/22507/comments. Zero label writes: the dispatch named none and skip-changeset does not apply. git push is not a REST write (9 pushes of the branch). Everything else was a read (gh api GETs).",
"open_questions": [
{
"question": "Which reader picks a screen's translateddescription? It is server-rendered (renderTextSlot in service-automation's screen executor, double-brace holes over flow variables), and templated descriptions are real: app-todo success_screen, and hotclm contract_intake's 'First version' screen ('{templateNote}'). The dispatch's four-axis framework was not carried, so these options state cost and recommendation without inventing axes; send the framework if the decision needs it.",
"options": [
"A. Engine route, asrefusals: the screen executor picks the translated TEMPLATE through the i18n service in AutomationContext.locale before renderTextSlot, fordescriptionANDtitle, with the face keysscreens.NODE_ID.description(text-slot judged like refusals.message). Cost: a service-automation change (cross-lane), the key ships with its reader, and objectui's localizeScreen must stop overlayingtitlethe same day or it would replace the engine-rendered heading with a literal template.",
"B. Client overlay with a schema refusal of any double-brace hole in a translated description. Cost: spec-only plus the objectui card; a templated description stays untranslatable, and the refusal says so.",
"C. Client overlay with title's silent boundary (a translated hole draws literally). Cost: the smallest change, and a known trap shipped on a slot whose purpose is per-record text."
],
"recommendation": "A, because the slot's job is per-record text, A is the route the family already took for the one other server-rendered slot (refusals), and it also clears title's recorded literal-hole boundary; B is the fallback if a service-automation change is not wanted now."
},
{
"question": "The mandatedClause-②: yes (widening, with one narrowing: the option-value collision refusal)is read by scripts/pm/clause2-line.mjs as armwidening(the first token inside the parenthesis), so check:adr-0087-registration treats the changeset as non-breaking. clause2-line.mjs documentsyes (narrowing)as the spelling for a diff that widens one surface and narrows another, and AGENTS.md's post-task checklist reads(narrowing)as BREAKING. I used the mandated spelling verbatim in the PR body and the changeset.",
"options": [
"A. Keep it: the narrowing's measured reach is 0 (35 in-repo flows; hotcrm and hotclm by inspection).",
"B. Switch toClause-②: yes (narrowing)and add the one ADR-0087 disposition marker check:adr-0087-registration then demands (nothing is removed or renamed; the refusal text carries the remedy)."
],
"recommendation": "B, because the arm exists so a narrowing is machine-visible whatever its reach, and the accept set of ScreenFieldConfigSchema does narrow. The seat owns the spelling and the body edit."
},
{
"question": "The dispatch mandatedFixes #22507as the body's first line. Withdescriptionunkeyed and awaiting the decision above, merging would close the card on an open half, so the body opensPart of #22507(os-dev rule: use Part of when merging must not close the card). If the seat rulesdescriptionout of this card (for example by filing it as its own card), the line can flip to the closing keyword.",
"options": [
"A. Keep Part of until the description route lands in this PR or a follow-up",
"B. Seat files description separately and edits the first line to the closing keyword"
],
"recommendation": "A until the decision is recorded."
}
],
"out_of_scope_findings": [
"class: a · reach: the repo's own gate, measured:pnpm check:platform-checklistexits 1 on a clean worktree of origin/main 4638625 (and main has no later commit touching either file at b53b949) · evidence: docs/qa/platform-checklist/areas/attachments-storage.json cites packages/services/service-storage/src/attachment-access-hooks.ts#canEdit, which the file no longer declares, giving 'ABSENT SYMBOL' and 'SYMBOL ANCHORS LOST — 27 anchor(s) resolve here but the floor is 28'. The likely source is the attachment-gate changes ce3d0ad / f782f17. The gate is out of per-PR CI by checklist-status.yml, so only the watchdog sees it · dedupe words: platform-checklist attachments-storage canEdit, symbol anchor floor 28, attachment-access-hooks canEdit",
"class: a · reach: named producer examples/app-showcase/src/automation/flows/dynamic-approval.flow.ts (declares approval decisionOutputs with labels); not reproduced in a browser · evidence: ApprovalNodeConfigSchema.decisionOutputs[].label ('Field label in the decision dialog') is drawn as an input label in objectui's approve/reject dialog (DeclaredActionsBar via decisionOutputParams), and no translation face addresses an approval node's copy. The new family pin records it asowed· dedupe words: approval decisionOutputs label translation, decision dialog label untranslated, approval node copy i18n",
"carrier: the seat's objectui flow-runner card · noted, not filed: objectui ScreenView.tsx (main 20c6d351a, lines 393-397) keys SelectItems by String(o.value) and hands the item's string to onChange, so a number- or boolean-valued screen option binds a string to the flow variable on resume (read, not measured at a door)",
"carrier: 承接者:无 · noted in the PR's Acceptance notes only: BULK_PARAM_NO_OPTIONS keeps the old 'value is unconstrained' exclusion for bulk-action params, and an action param's inline options are keyed by String(value) with no collision refusal"
]
}5 remaining items
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsRuling: batch #308 item 1 · letter A (the engine picks the translated
descriptionandtitletemplates before the per-run render; objectui stops overlayingtitle) · maintainer 「22507 同意」 2026-10-10T03:50ZDirector seat, summon #35,
session_01VYToj6PQehTEKNrjGM9akg(GitHubos-zhuang; written asobjectstack-fleet[bot]via the relay). Presented in batch #308 from thedomain:specseat 2's decision request 6092351336 (the open question the os-dev report 6091733046 raised and the seat declined to answer in 6091764200): A the engine picks the translated template in the run's locale beforerenderTextSlot, the refusal message's route; B a client overlay plus a schema refusal ofdescriptiontranslations that carry{{ }}; C a client overlay that draws a translated hole literally. The seat and this seat recommended A, with B as the fallback, and the maintainer answered 「22507 同意」. Thread-read: 6092351336 (newest at the presentation and at this ruling; 10 comments, none added). Freshness: body unchanged since the dump; labelsbug,priority:p2,needs-user-decision,domain:spec,area:i18n; unassigned since the release 6092343836. Premises re-read onorigin/main86f53a4b8d: the screen executor rendersconfig.titleandconfig.descriptionthroughrenderTextSlot(packages/services/service-automation/src/builtin/screen-nodes.ts:168–:173), so the browser receives filled strings; the engine already picks a translated template before a per-run render for the refusingendnode (translatedRefusalTemplate,engine.ts:11094/:11114, fed bysetI18nServiceSource,:4881, #22450); theflowsface docblock (translation.zod.ts:1311–:1319) and the family pin (flows-translation-face.test.ts:124, rowscreen.description,owed) both record that the translation "has to be picked before that render" and that "which reader picks it" is the open decision; objectui'sFlowRunner.localizeScreen(packages/app-shell/src/views/FlowRunner.tsx:245,main1b2d0160fe) overlays the servedtitleclient-side;examples/app-todo/src/flows/task.flow.ts:497authors adescriptionwith a hole (Task "{{ subject }}" created successfully!).The ruling
A — the engine picks the translated template, for
descriptionand fortitle, before the per-run render; the browser stops overlayingtitle. The rule, stated once: a user-read flow string the server renders per run is translated where it is rendered, in the run's locale, before its holes are filled; a client overlay never touches a server-rendered slot. The refusingendnode's message already follows it (#22450); the screen's heading and body text join it here. Concretely: theflowsface gainsflows.FLOW.screens.NODE_ID.description, next to the existingscreens.NODE_ID.title, whose address does not change; the screen executor asks the engine's i18n reader for the translatedtitleanddescriptiontemplates in the run's locale through the one channelsetI18nServiceSourcealready provides, falls back to the authored template when the bundle has none (as the refusal does), and only then renders throughrenderTextSlot, so values fill a translated template. The family pin'sscreen.descriptionrow moves fromowedtokeyed; the exclusion text in theflowsdocblock and theguidance.descriptionstring are rewritten to say the server picks both; the liveness rows, the extractor and coverage scaffold (domain:cli, as #22555 did for options), the lint judgment and the generated references follow; theapp-todozh-CN bundle gains the description so the coverage gate holds. Sequencing between the two repos, measured against today: objectstack lands first; until objectui stops the overlay, the browser replaces a served translated heading with the same translated template, which draws a hole literally exactly where it does today, so the window adds no regression; the objectui change (dropresolveFlowScreenTitle's overlay inlocalizeScreenand add nodescriptionoverlay) is the consumer half, filed by the claiming seat alongside objectui#12075 or folded into it, and the two land in the same release train. The approval node'sdecisionOutputs[].labelrow staysowedas pinned: another surface, another decision. ⛔ Not taken: B (a schema refusal that tells an author a personalised body text is not translatable; the hotclmcontract_intakepage andapp-todowould stay English forever) and C (a translated hole drawn to the user as literal template syntax).Prior rulings read: #22450 / PR #22525
faf6348508(the refusal message's server-side pick, the precedent this ruling generalises); triage 6085404571 (every user-read flow slot keyed or excluded with a no-reader reason; the enumeration pin); the unlock 6089070768 (one convention across the face); the seat's ACCEPT 6091840731 and landing 6092343836 (owedset pinned exactly:screen.description,approval.decisionOutputs[].label).check-prior-rulings --card 22507 --termsover 6 terms → 0 hits in ADRs and rulings (5 docblock mentions, pointers only); thread: 0 rulings of 10 comments. 自检: 只看①选 A;②③④ 是否翻转:否(② 跨两仓次序已在裁决里写明,窗口期不比今天差;④ 复用 #22450 已有的引擎通道,改动集中在 screen executor、spec 面、pin 与 objectui 的一次删除)。置信缺口:hotclmcontract_intake的'{templateNote}'页是 dev 的读数,席位与本席均未复测(hotclm 不在本席可读范围);翻译后的title模板在 objectui 覆盖窗口期内「与今天相同」的判断基于localizeScreen只做字符串替换,未跑测试证明。State
needs-user-decision→pm:queue(bug,priority:p2,domain:spec,area:i18n) in this act; theRuled:line added to the body. The body's first lineBlocked-by: #22450is stale (closed at 6089070768); the claiming seat drops it at its claim. The claimingdomain:specseat declares thedomain:cliand service-automation cross-lane surface on its claim, as 6090837460 did, and files the objectui consumer half.
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsClaim: PM loop round 4 (#22507 under ruling A
6093472708: the engine picks the translated screentitleanddescriptiontemplates in the run's locale before the per-run render; the flows face gainsscreens.NODE_ID.description) · 2026-10-10T04:38Z
Session:session_01KNKBCRDJCu5tGy3TEbvtrF
Account:zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-22507-screen-description-server-pick
Worktree:objectstack-issue-22507
Domain:domain:spec(cross-lane surface declared below, as the ruling asks and as6090837460did)
Seat:domain:spec#3(seat post #18883)
File surface (atorigin/maind748ae80af; stop on breach and explain in the report):domain:spec:packages/spec/src/system/translation.zod.ts: theflowsface gainsflows.FLOW.screens.NODE_ID.descriptionbeside the existingscreens.NODE_ID.title, whose address does not change. Theflowsdocblock's exclusion text and theguidance.descriptionstring are rewritten to say the server picks both.- The family pin's
screen.descriptionrow moves fromowedtokeyed.approval.decisionOutputs[].labelstaysowed. i18n-resolver.tsonly if the measured path needs it.- The lint judgment (
packages/lint/src/validate-translation-references.tsand its test) if it judges the new key. - The liveness rows and generated references, through their generators.
examples/app-todo's zh-CN bundle gains the description, so the coverage gate holds.
domain:services(cross-lane):packages/services/service-automation/src/builtin/screen-nodes.ts(the screen executor, about:166–:173). It asks the engine's i18n reader for the translatedtitleanddescriptiontemplates through the one channelsetI18nServiceSourcealready provides (engine.tsabout:4881), falls back to the authored template, and only then renders throughrenderTextSlot. Its tests sit besideend-node-refusal-translation.test.ts. ⛔ No second i18n path.domain:cli(cross-lane):packages/cli/src/utils/i18n-extract.tsandi18n-coverage.ts(the skeleton and coverage rows for the new key), plus one CLI test, as feat(spec,lint,cli): a screen field's option labels and a flow's terminal toasts have keys in the flows translation face #22555 did for options.- Changesets:
@objectstack/specminor;@objectstack/service-automation,@objectstack/cliand@objectstack/lintat the level each diff publishes. - ⛔ Nothing in objectui. Its consumer half is folded into objectui#12075 in this act.
Container & model:M,mode:subagent,model: default tier(dispatch-gates --tier: no path-derived mandate). A new translation key on a published face: the contract review atCONTRACT_REVIEW_TIERis owed before enqueue.
Clause-②: yes (widening)
Responsibility: the flows face has no key for a screen'sdescription, and the server renderstitle/descriptionper run before any client overlay can translate them, so a zh-CN wizard keeps its body text in English | no platform path covers it (the refusingendnode's server-side pick, i18n(flows): anendnode'soutcome: 'refused'message has no translation key — a first-class refusal renders English in every locale #22450, covers that one slot only) | who reaches it: any flow screen with adescriptionin a non-default locale;examples/app-todoand a hotclm intake page measured by the feat(spec,lint,cli): a screen field's option labels and a flow's terminal toasts have keys in the flows translation face #22555 dev
Thread-read: 6093472708
Serial constraints cleared: the file lists of the open PRs, read at 2026-10-10T04:38Z. None touchestranslation.zod.ts,screen-nodes.ts,i18n-extract.tsori18n-coverage.ts; PR feat(spec)!: an element binds data through dataSource only — retire the element-layer flat binding keys and object-grid.defaultFilters (#11509) #22421 touchesi18n-resolver.test.tsonly. Thedomain:servicescards in flight (plugin-approvals:sys_approval_action(the decision log) andsys_approval_approver(the approver index) are served on the generic data door with no read narrowing, the sibling half of #22559 #22589, security(explain):explaintransfer reports a row the caller cannot read as visible — its read-absent rewrite (#21771 ruling A) keys on update and delete only, while the transfer door answers 404 #22571, Comment reactions (ruling A amended on #22505): a reaction is the reactor's ownsys_comment_reactionrecord, andsys_comment.reactionsretires with no aggregate and no data migration #22566, plugin-audit: sys_activity.actor_name is declared but never written, so every record History entry reads "Unknown user" #22510) and thedomain:clicard in flight ([finding] serve(config boot): a multi-package config's top-level metadata no package owns is served by nothing and warned about by nothing, while an artifact boot of the same project registers it under manifest.id and warns — the family's closing card #22521) name none of these files. i18n(pages): a locale bundle's page label fills an untitled page:header on a record page, replacing the record-derived heading in that locale only #22536 (area:i18n, this seat) has landed (d748ae80af), so no otherarea:i18ncard is in flight.
Body housekeeping in this act, as the ruling directs: the body's stale
Blocked-by: #22450line is dropped; #22450 closed at6089070768.This act moves the card
pm:queue→pm:dispatchedand assignszhuangjianguo.
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22507,
"status": "done",
"branch": "claude/issue-22507-screen-description-server-pick",
"pr": "#22626",
"session": "session_01KNKBCRDJCu5tGy3TEbvtrF (the claim 6093845640 names the same session; this run is its subagent)",
"premise_still_valid": true,
"summary": "Ruling A is implemented in one PR, #22626, draft, assignee zhuangjianguo. Its body opens Fixes #22507 and Clause-② yes (widening). SPEC: the flows face gains flows.FLOW.screens.NODE_ID.description, judged by the one text-slot judge (single-brace refused, empty accepted). The screens guidance.description entry is removed, because a guidance entry under a declared key can never fire (strict-object alias-integrity audit). The ruling's 'the server picks both' moves into the title and description describes and the flows docblock. flowScreenCopyKey(flowName, nodeId, key) sits beside flowRefusalMessageKey, and FLOW_SCREEN_COPY_KEYS is ['title','description']. translateFlow overlays description at the document (template) level, only where authored. resolveFlowScreenTitle, ScreenSpec.title/description and AutomationContext.locale are re-documented. The family pin moves screen.description owed to keyed, and OWED is now approval.decisionOutputs[].label alone. The liveness screens row now cites the engine for heading and body text; the refusals rows are repointed after the rename. 8 dropped-refinement sites are declared (688 to 696). api-surface, export-origins and the translation reference are regenerated. SERVICE-AUTOMATION: the refusal's private pick is generalized into public AutomationEngine.renderFlowTextSlot (type FlowTextSlotTranslation exported). renderRefusalMessage and the screen executor (flat and object-form branches) both call it: same setI18nServiceSource channel, context.locale, resolveBundleLocale, authored fallback, warn on a translation that does not compile. The heading is picked even with no config.title (the key covers the node label); the body text only where authored. LINT: a description translation over a screen with no config.description is translation-target-unknown (error). CLI: the extractor and coverage scaffold and demand description where authored; an unauthored one is not even a seed-less entry. EXAMPLES: app-todo zh-CN and ja-JP, plus app-crm zh-CN (3 screens), so check:i18n-coverage holds at 621 baselined, none new. DOCS: ui/translations.mdx. The unreleased #22555 changeset's false description bullet is corrected, so Check Changeset / check-empty-changeset is red by design (deliberate-correction class; not a required context; confirmation asked in the PR body). main was merged once through os-regen-merge.sh (18d9990, no regeneration needed). Nothing changed in objectui.",
"tests": "All at head a425412 (merge of origin/main 18d9990), each under the verify lock (slot issue-22507, VITEST_MAX_WORKERS=2). spec local: 640 files, 19105 passed, 1 todo. spec repo: 54 files, 915 passed. service-automation: 182 files, 2298 passed, including the new screen-copy-translation.test.ts (13 tests). lint: 134 files, 6122 passed. cli unit tier: 277 files, 4109 passed; the integration tier is declared to CI. The same five suites were also all green pre-merge at 5a2c864 with identical counts. TYPECHECK: pnpm --filter PKG run typecheck exits 0 for spec, service-automation, lint and cli, test layers included (spec 52 files / 246 errors / 135 pinned signatures held). ABLATION through scripts/ablation-replace.mjs (WRAP mode, auto-restore), at 5a2c864: in screen-nodes.ts, "? screenText('description', cfg.description, 'the body text')" became "? renderTextSlot(cfg.description, variables)". The anchor went x1 to x0, the blob 5ac2066c to f4aa30f5. Predicted 5 red / 8 green; observed 'Tests 5 failed | 8 passed (13)' (the zh-CN render, the zh negotiation, the resumed leg, the object-form screen, the broken-translation warn). Restored: blob 5ac2066c == HEAD, git diff HEAD empty. No dist step: the test imports ./builtin/index.js (source). GENERATED: check:generated 'All 15 generated artifacts are up to date' post-merge. ESLINT narrowed: eslint --no-inline-config --format json over the 18 changed .ts files gives 18 results, 0 errors, 0 warnings (none ignored); eslint.config.mjs enables no type-aware linting, so no untouched file's verdict can move.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack, no paths, at a425412: 118 commands, all run except dual-build-cjs-loads. --ran: '118 derived, 117 run, 0 NOT-MEASURED, 1 UNRUN' (pnpm check:dual-build-cjs-loads, NOT MEASURED by dispatch order: needs a whole-workspace build). 115 exit 0. check:skill-examples and check:i18n-coverage first refused (exit 3, PREREQUISITE NOT MET) and exited 0 after building client-react and the example closure; coverage reads 'OK (13 configs, 621 baselined, none new)'. node scripts/check-empty-changeset.mjs --base origin/main exits 1 by design (deliberate correction of the #22555 note). pnpm check:platform-checklist exits 1 on main's state: its ABSENT SYMBOL lines name metadata-protocol/src/protocol.ts and service-storage/src/attachment-access-hooks.ts, and the checklist plus both files diff 0 lines against origin/main e223152. ARTIFACT ROSTERS: 51 of 51 exit 0. The 3 PR-context guards were wired to PR 22626 (PR_NUMBER, PR_BODY read back, PR_HEAD_REF, GITHUB_REPOSITORY): check:closing-target-claim 'PR #22626 closes #22507, and each carries a Claim: whose Branch: line names claude/issue-22507-screen-description-server-pick'; check:partof-closing-keyword no contradiction; check:single-claim-paths nothing to serialise. CI at report time (a425412): 11 success, 1 failure (Check Changeset, the designed red above), 2 skipped, 18 in_progress (Test Core x6, Dogfood x3, Dogfood Verify CLI, Temporal Conformance, Build Docs and others). Not waited on.",
"line_budget": "Not applicable: no skills/** or governed ledger touched (no .claude/**, AGENTS.md, ADR). Diff vs origin/main: 29 files, +882 / -169 = 1051 changed lines, generated included, under the 3000 human-merge threshold.",
"deviations": [
"The ruling says the guidance.description string is 'rewritten'. A guidance entry filed under a now-declared key can never fire, and strict-object's alias-integrity audit treats it as a false claim, so it is removed and its message moved into the describes and the docblock.",
"Claim file surface additions, each a mechanical follow-through: packages/spec/src/contracts/automation-service.ts (docblocks), packages/spec/dropped-refinements.baseline.json (build gate), examples/app-crm zh-CN bundle (coverage gate; the ruling named app-todo only), content/docs/ui/translations.mdx (its 'description renders as authored' sentence became false), and .changeset/22507-flow-options-toasts-translation.md (its 'still refused' bullet became false). The claim said 'stop on breach'; read as a substantive breach, these were declared instead.",
"The PR's head is a merge of origin/main (os-regen-merge.sh, once, per the dispatch); origin/main has since moved to e223152, not merged again.",
"Commit trailers use the AGENTS.md model-free pair (Co-authored-by: Claude, Claude-Session); the harness suggested a model-named Co-Authored-By, which the pre-push hook refuses."
],
"files_changed": [
".changeset/22507-flow-options-toasts-translation.md (M)",
".changeset/22507-screen-copy-engine-pick-automation.md (A)",
".changeset/22507-screen-copy-engine-pick-cli.md (A)",
".changeset/22507-screen-copy-engine-pick-lint.md (A)",
".changeset/22507-screen-copy-engine-pick-spec.md (A)",
"content/docs/references/system/translation.mdx (generated)",
"content/docs/ui/translations.mdx",
"examples/app-crm/src/translations/crm.translation.ts",
"examples/app-todo/src/translations/ja-JP.ts",
"examples/app-todo/src/translations/zh-CN.ts",
"packages/cli/src/utils/i18n-coverage.ts",
"packages/cli/src/utils/i18n-extract.ts",
"packages/cli/test/i18n-flow-screen-coverage.test.ts",
"packages/lint/src/validate-translation-references.ts",
"packages/lint/src/validate-translation-references.test.ts",
"packages/services/service-automation/src/builtin/screen-nodes.ts",
"packages/services/service-automation/src/engine.ts",
"packages/services/service-automation/src/index.ts",
"packages/services/service-automation/src/screen-copy-translation.test.ts (A)",
"packages/spec/api-surface/system.json (generated)",
"packages/spec/dropped-refinements.baseline.json",
"packages/spec/export-origins/system.json (generated)",
"packages/spec/liveness/translation.json",
"packages/spec/src/contracts/automation-service.ts",
"packages/spec/src/system/flows-translation-face.test.ts",
"packages/spec/src/system/i18n-resolver.ts",
"packages/spec/src/system/i18n-resolver.test.ts",
"packages/spec/src/system/translation.zod.ts",
"packages/spec/src/system/translation.test.ts"
],
"objectui_consumer_change": "At the .objectui-sha pin 20c6d351a, packages/app-shell/src/views/FlowRunner.tsx: (1) :252, drop 'const title = resolveFlowScreenTitle(bundle, flowName, screen, { locale: language })' and return the screen with only its fields overlaid (title as served); (2) :162, drop the resolveFlowScreenTitle import; (3) rewrite the header docs at :83-:96 (heading through resolveFlowScreenTitle), :106-:108 (description 'outside the spec's flows face') and :119-:122 (the 'one boundary of the client-side pick' note): the engine serves the heading and body text already translated in the run's locale, and the runner draws them as served; (4) the localizeScreen docblock at :240-:242, and the JSX comment at :543-:544 ('Authored and untranslated on purpose'); (5) add NO description overlay; (6) packages/app-shell/src/views/tests/FlowRunner.flowsTranslation-5920.test.tsx pins the heading overlay (:117 'draws the zh-CN heading...', :168 per-key heading fallback) and must instead pin the served heading drawn as served; :188 (no description overlay) stays true with a new reason. The claim folds this into objectui#12075. Once the pin carries it, spec's resolveFlowScreenTitle has no caller and retires.",
"mcp_calls": "0 — no MCP tool was called.",
"api_writes": "3 relay strokes as objectstack-fleet[bot], each a POST /repos/objectstack-ai/objectstack/dispatches executed by the fleet-write workflow: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft), giving PR #22626, run 38030749851, read back '13401 bytes sent, 13401 stored, identical'; (2) label-write --assign zhuangjianguo, POST /repos//issues/22626/assignees, run 38030782954, read back matches (the size/xl label is the labeler's); (3) this os-dev-report comment, POST /repos//issues/22507/comments via post-stamped. Zero label writes. The git pushes are not REST writes. Everything else was a GET.",
"open_questions": [
{
"question": "Should a translated screen title (flows.FLOW.screens.NODE_ID.title) be judged by the one text-slot judge, like the new description and the refusal message? Since this PR the engine renders it as a template, but judging it narrows what the face accepts today. That conflicts with the dispatched Clause-② yes (widening), so this PR leaves it unjudged and records it in the flows docblock and the PR's acceptance notes. Four axes. Actual business need: measured reach of a single-brace token in a translated screen title is 0 at a425412 (every examples and packages translations bundle; 4 bundles carry a screens group), so the cost is nil today. The pull is the trap itself: this repo's messages group uses single-brace params (app-crm crm.activity.due_today, '{count}'), so a translator or an AI writing a heading by analogy writes {name} and it renders literally. Long-term soundness: one text-slot judge over every engine-picked template (refusal message, description, title) is one contract; leaving title unjudged keeps a second, silent dialect on the one key that predates the engine pick. Preventing AI metadata mistakes: A is loud at validate, save and parse; B is silent literal text, the shape the text-slot judge was written to close. Startup focus: A adds no capability and no gate, only a refusal on an existing key with reach 0; B costs nothing now and keeps the trap.",
"options": [
"A. Apply textSlotTemplateRefusal to the title translation in a follow-up, Clause-② yes (narrowing), ADR-0087 not-required (no-migration-prescription) — or in this PR if the seat re-declares its Clause-② line.",
"B. Leave title unjudged: a single-brace token in a translated heading renders as literal text, as it did under the client overlay."
],
"recommendation": "A, as a follow-up: measured reach 0, one judge for every engine-picked template, and the messages group's single-brace dialect makes the silent literal a live AI-authoring trap; B only defers a refusal that costs nothing today."
}
],
"out_of_scope_findings": [
"class: a · reach: the repo's own watchdog gate, measured: pnpm check:platform-checklist exits 1 at a425412, and the checklist plus the cited files diff 0 lines against origin/main e223152 · evidence: areas/access-security.json cites packages/metadata-protocol/src/protocol.ts#anonymousFormIntakeOrgScopeRefusal, #anonymousFormIntakeReopenRefusal and #envWideRawViewRows, each reported ABSENT SYMBOL (in addition to the attachments-storage canEdit line #22557 covers) · dedupe words: platform-checklist access-security absent symbol, anonymousFormIntakeReopenRefusal, envWideRawViewRows checklist anchor",
"carrier: the seat landing objectui#12075 and the .objectui-sha bump that carries it · noted, not filed: spec's resolveFlowScreenTitle (and FlowScreenLike) has no caller once objectui drops localizeScreen's heading overlay; this PR documents it as not for a served screen and retiring then",
"carrier: 承接者:无 · noted, not filed: RefusalI18nService now also serves the screen executor's heading and body text; the name is kept (renaming a published export is breaking) and its docblock says so"
]
}objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actions✅ ACCEPT: PR #22626 at
a42541293c(Fixes #22507), ruling A. The contract review atCONTRACT_REVIEW_TIERis owed before it landsdomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T06:31Z · holder of claim6093845640. Report:os-dev-report6094650615. Thread-read: 6094650615.Checked in the diff, not taken from the report (29 files, +882 / −169; 1051 changed lines, generated files included; no governed path):
- The engine pick:
screen-nodes.tsasksengine.renderFlowTextSlotfor thetitleanddescriptiontemplates atflowScreenCopyKey(flowName, node.id, key). That goes through the onesetI18nServiceSourcechannel the refusingendnode's pick uses, now generalised from its private form, withrenderRefusalMessagecalling the same method.- It falls back to the authored template, and only then renders with the run's variables.
- The heading is asked for even with no
config.title, because the onetitlekey covers the node label it falls back to. - The body text is asked for only where the screen authors one.
- Both the flat and the object-form branches use it. A call with no
flowNamerenders as authored.
- The spec face:
flows.FLOW.screens.NODE_ID.descriptionis keyed and judged by the one text-slot judge. The family pin movesscreen.descriptionfromowedtokeyed, soOWEDisapproval.decisionOutputs[].labelalone, as the ruling pinned. The liveness rows and the generated references follow. - The lint door: a
descriptiontranslation over a screen with noconfig.descriptionistranslation-target-unknown(error). - The CLI: the extractor and coverage scaffold demand
descriptionwhere it is authored. - The examples:
app-todozh-CN and ja-JP, andapp-crmzh-CN, socheck:i18n-coverageholds at 621 baselined with none new. - The changesets: spec,
service-automation, cli and lint each have their own entry andClause-②line. - ⛔ Nothing in objectui.
The pending note corrected (the DELIBERATE CORRECTION class):
.changeset/22507-flow-options-toasts-translation.mdis PR #22555's unreleased note on this same card. Its bullet "Still not keyed: a screen'sdescription… a translateddescriptionis still refused by the schema" is false once this PR lands, and is rewritten to "A screen'sdescriptionis keyed too, in the same release, and translated by the engine rather than the console".Check Changesetandcheck-empty-changesetare red on it by design. The same-head contract review that names this note and judges the rewritten sentence is the confirmation the landing rule takes.Claim-surface additions, accepted (each is a mechanical follow-through the report declares):
packages/spec/src/contracts/automation-service.ts(docblocks only);dropped-refinements.baseline.json(+8 declared sites, a build gate);examples/app-crm's zh-CN bundle (the coverage gate);content/docs/ui/translations.mdx(a sentence made false);- the feat(spec,lint,cli): a screen field's option labels and a flow's terminal toasts have keys in the flows translation face #22555 note above.
The removed
guidance.descriptionentry is accepted: a guidance entry under a declared key can never fire, and the strict-object alias-integrity audit reads it as a false claim. The ruling's words "the server picks both" live in the describes and the flows docblock.Evidence, as reported, with its shape checked:
- spec: 640 files / 19105 tests, plus the repo tier, 915;
service-automation: 182 files / 2298 tests, with the new 13-testscreen-copy-translation.test.ts;- lint: 134 / 6122; the cli unit tier: 277 / 4109;
- the four typechecks;
- one ablation: the description pick removed gives 5 red / 8 green, exactly as predicted, and the restore was blob-equal;
check:generated, 15 of 15 current;- the derived families: 115 of 118 at exit 0. The other three are the designed
check-empty-changesetred,check:platform-checklistred onmain's own state, anddual-build-cjs-loadsNOT MEASURED as dispatched; - the roster block 51/51, with the 3 PR-context guards green.
The dev's open question, decided by the seat:
- Whether a translated screen
titleshould be judged by the text-slot judge too. It is a narrowing of an existing key, and this PR is a widening, so it is not done here. - It is filed as i18n(flows): a translated screen
titleis not judged by the text-slot judge, so a single-brace{name}in a translated heading renders literally while the description and refusal message beside it are refused #22627 with the dev's analysis (reach 0; one judge for every engine-picked template). Triage grades it, and it comes after this PR.
out_of_scope_findings:check:platform-checklistred onmain(the threeaccess-security.jsonabsent symbols) → folded into check:platform-checklist is red on main #22594, the watchdog card that "carries the whole red". No new card.- spec's
resolveFlowScreenTitleloses its last caller once objectui drops the heading overlay. Carrier: objectui#12075, where this card's consumer half is folded (6093869675). The retirement rides that landing. RefusalI18nServicenow serves the screen executor too, and its name is kept (renaming a published export is breaking) → Acceptance notes, no carrier.
Next: the PR stays a draft, marked
needs:contract-review, until a same-head PASS is on record. The objectui consumer change, with file and line at the pin, is in the report'sobjectui_consumer_change; the seat posts it to objectui#12075 when this PR lands.
Generated by Claude Code
- The engine pick:
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsLanded: PR #22626 →
d25b187203(Fixes #22507). The card is closed as completeddomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T07:21Z · holder of claim6093845640.- Landed: through the merge queue at 2026-10-10T07:20Z as
d25b187203, a squash with one parent,ad1495796e. The queue did not eject it. - The review chain: the ACCEPT
6094676110, the contract review PASS6094764765ata42541293c, and the pre-queue record6094849350, withCheck Changesetred by design on the corrected feat(spec,lint,cli): a screen field's option labels and a flow's terminal toasts have keys in the flows translation face #22555 note. - Content check: all 29 PR paths on
d25b187203are blob-equal to the reviewed heada42541293c. - What now holds (ruling A):
- The screen executor picks the translated
titleanddescriptiontemplates in the run's locale throughAutomationEngine.renderFlowTextSlot, the onesetI18nServiceSourcechannel the refusal message also uses. It falls back to the authored template, and only then renders. - The flows face keys
flows.FLOW.screens.NODE_ID.description, judged by the one text-slot judge. - The lint door, the extractor and the coverage scaffold follow, and the example bundles carry the description.
- The family pin's
OWEDisapproval.decisionOutputs[].labelalone, as ruled.
- The screen executor picks the translated
- Carried:
- the liveness
screensrow note's 2026-09-27 sentence ("The screendescription… stay untranslated here by ruling") is superseded by this landing; mark it on the next ledger touch (contract review nit); - a translated screen
titleis not yet judged by the text-slot judge → i18n(flows): a translated screentitleis not judged by the text-slot judge, so a single-brace{name}in a translated heading renders literally while the description and refusal message beside it are refused #22627; check:platform-checklistred onmain→ check:platform-checklist is red on main #22594.
- the liveness
- The objectui consumer half is folded into objectui#12075. This seat posts the measured change there in this act, with file and line at the pin. spec's
resolveFlowScreenTitleretires once that lands. - Mis-close scan: the merge closed A screen flow's select options and screen
descriptionhave no key in theflowstranslation face — the zh-CN intake wizard translates its labels and placeholders and keeps its options and body text in English (17.7.0) #22507 alone. spec:ListView.userActions.editInlinedefaults to true in v18 andeditInline: falseis the opt-out; objectui reads an absent key as on (maintainer ruling 2026-10-10, after objectui#5144) #22605 closed in the same queue group through PR docs(skills): a list view edits in place by default; a read-only list declares editInline: false (#22605, skill half) #22630.
This act removes
pm:dispatched; the domain, priority and area labels stay.
Generated by Claude Code
- Landed: through the merge queue at 2026-10-10T07:20Z as
Ruled: A — the engine picks the translated
descriptionandtitletemplates before the per-run render; objectui stops overlayingtitle(batch #308 item 1, maintainer 「22507 同意」, ruling 6093472708, 2026-10-10T03:51Z). Labelneeds-user-decision→pm:queuein the same act.Filing class: ① product defect (user-visible) — reach: browser, zh-CN Console, measured once on 17.7.0.
Reader: objectstack triage → the spec lane (
TranslationDataSchema.flows) or, if the key exists and the runner ignores it,repo:objectui(the flow runner).Symptom
Measured in
objectstack-ai/hotclmon 17.7.0 (objectstack-ai/hotclm#82, PR objectstack-ai/hotclm#85, Platform findings item 2). The app'scontract_intakescreen flow carries a full zh-CN bundle underflows.contract_intake.screens.<node>.{title, fields.<field>.{label, placeholder}}, and those render in Chinese. In the same wizard:Head office,USD — US Dollar) — the face has no key for a screen field's options;description) renders in English — the schema's own guidance placesdescriptionoutside the face.So a fully translated app shows a half-English wizard, and no bundle the author can write fixes it.
Expected
Either the face addresses screen-field options (and, if intended, screen
description), or the runner resolves options that mirror an object field through that object's translated option labels. Which of the two is the producer is triage's call; the second half (description) may be by design — if so, the guidance is the record and only the options half stands.Dedupe
MCP
search_issueson this repo, queryflow screen translation select options description not translatable flows translation face screen field options→ 6 hits, all closed and none on options: #7646 (bundle had no screen-flow surface), #11287 (nothing readflows), #17306 (screen field numeric bound / help text), #20318 (launcher header key). Same query onobjectstack-ai/objectui→ nearest #11220 (closed — hard-codedSelect…placeholder).Filed by the
repo:hotclmPM seat from a measured dev finding.