Skip to content

feat(spec,lint,cli): a screen field's option labels and a flow's terminal toasts have keys in the flows translation face - #22555

Merged
objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-22507-flow-user-read-slots
Oct 10, 2026
Merged

objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-22507-flow-user-read-slots

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Part of #22507
Clause-②: yes (narrowing)

This PR keys two of the three user-read flow slots that had no key in the flows translation face: a screen field's option labels and the flow's terminal toasts. It also adds the family pin. The third slot, a screen's description, 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 is Part of rather than the closing keyword the dispatch named: merging this must leave the card open for the description half.

What changes

@objectstack/spec

  • TranslationDataSchema.flows gains three keys.
    • flows.FLOW.successMessage and flows.FLOW.errorMessage: the terminal toasts. Aliases success / successText / error / errorText rename onto them.
    • flows.FLOW.screens.NODE_ID.fields.FIELD.options.VALUE: a screen field's option labels. choices / values rename onto options.
  • Each option is keyed by its value coerced to a string, 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's ScreenView), and the coercion translateAction already applies to an action param's inline options.
  • The one narrowing. ScreenFieldConfigSchema refuses two options of one field whose values collide as text, such as 1 and "1", true and "true", or a repeated value.
    • The refusal lands at the second option's value. At the flow door it reads nodes[1].config.fields[0].options[1].value.
    • It leads with SCREEN_FIELD_OPTION_VALUE_COLLISION, names both options by position and value, and gives the remedy.
    • Measured reach: 0 collisions. The examples hold 35 flows, 6 screens, 2 option fields and 10 options, all with string values (measured at 37c711449 by loading the app-crm, app-todo and app-showcase flows). hotcrm at f0afcbda0 and hotclm at 14c899fc4 were read by inspection: their option values are string literals or come from an object field's string options.
  • translateFlow applies 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 served ScreenFieldSpec.
    • FLOW_TERMINAL_MESSAGE_KEYS is new, the key list the extractor and the schema pin share.
  • The exclusion texts that no longer hold were rewritten: FLOW_SCREEN_FIELD_NO_OPTIONS is removed, the toast guidance is removed, and the description guidance now says why it is unkeyed and what the screen face carries.
  • The liveness ledger gains translation/flows.successMessage and flows.errorMessage, both planned (contract-first split, as screens was), and dated notes on flows and flows.screens for the option map.

@objectstack/lint: validateTranslationReferences judges both new key families.

  • A translated toast over a flow that declares none is translation-target-unknown (error).
  • An option key that names no declared option, or names one by its label, is translation-option-key-unknown (warning). This is the same rule and severity as the object-field and action-param legs.

@objectstack/cli: os i18n extract and the coverage gate scaffold and demand the new keys in the flow bucket.

  • The option keys go through the same key function.
  • The completion toast is demanded for a flow with a screen, and the failure toast for every flow. Each predicate mirrors the reader that will draw it (see below).

Examples: app-crm (zh-CN toasts) and app-todo (zh-CN and ja-JP option labels) are translated, so check:i18n-coverage holds 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.ts walks 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) or owed (pinned exactly, shrink-only).

  • Tier 1 covers every text leaf of FlowSchema (minus nodes), FlowNodeSchema (minus config), ScreenConfigSchema and EndConfigSchema.
  • Tier 2 covers the copy-named text leaves of every other declared node contract (the builtin executor contracts and approval), plus every FLOW_NODE_TEXT_SLOTS entry.

It also fails on a stale row, and on a face key that no slot fills.

Slot Disposition
Flow.label keyed label
Flow.successMessage / errorMessage keyed (new)
screen title, and a screen node's label as its fallback heading keyed screens.NODE_ID.title
screen field label / placeholder / inlineHelpText keyed
screen field options[].label keyed …options.VALUE (new)
end message (refused) keyed refusals.NODE_ID.message
screen description owed: server-rendered, decision pending
notify title / message elsewhere: the notify template path (sys_email_template, per recipient locale)
approval decisionOutputs[].label owed: read in the approval decision dialog, no face at all
Flow.description, edge label, variables, ids, predicates, defaultValue, defaults, objectName, framework/lock fields, … noReader, each with its reason

noReader means that no person running the flow reads the text. Flow.description is 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.mjs with an anchor hit of 1 → 0, the pin run under the verify lock, then restored with blob == HEAD and an empty git diff HEAD). Predicted: red. Observed: red.

  • Planting subtitle: z.string().optional() on ScreenConfigSchema (blob 5b4cf3fe → ac16dd5a → 5b4cf3fe) gives 1 failed / 6 passed: expected [ 'screen.subtitle' ] to deeply equal [].
  • Renaming the face's successMessage key (blob 8d5dd093 → 05480fb1 → 8d5dd093) gives 2 failed / 5 passed: the keyed-row check and the face-fill check.

Where this PR stops: a screen's description is server-rendered

The dispatch orders a stop before touching service-automation if a slot proves server-rendered, and description does. service-automation's screen executor renders config.description per run through renderTextSlot ({{ }} holes over the flow's variables) and serves the result as ScreenSpec.description.

An overlay on the served string would draw a translated hole literally. The face's existing title key already has that boundary, as FlowRunner's own header records. Templated descriptions are real: app-todo's success_screen (Task "{{ subject }}" created successfully!) and hotclm's contract_intake "First version" screen (description: '{templateNote}').

So the key is not added. The options for the seat:

  • A. The engine picks the translated template in the run's locale before renderTextSlot, the refusals route. This touches service-automation, and the same change should move title too.
  • B. A client overlay, with the schema refusing a {{ }} hole in a translated description. A templated description then stays untranslatable.
  • C. A client overlay with title's silent boundary.

The pin records description as 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: in FlowRunner.tsx localizeScreen, set each field's options to resolveFlowScreenFieldOptions(bundle, flowName, screen.nodeId, field, { locale: language }) before ScreenView draws its SelectItems.
    • Not through FLOW_SCREEN_FIELD_COPY_KEYS: the option map is not a string key and is deliberately not in that list.
  • flows.FLOW.successMessage: in FlowRunner.tsx, the terminal-success toast (outcome.successMessage || t('flowRunner.completed', …)), via translateFlow({ name: flowName, successMessage: outcome.successMessage }, bundle, { locale: language }).successMessage.
  • flows.FLOW.errorMessage: in utils/flowResponse.ts flowFailureMessage, which reads data.errorMessage, from error.details.errorMessage on the 400 FLOW_FAILED path. It is read by FlowRunner's resume and by both launch hosts through judgeFlowLaunch (useConsoleActionRuntime, RecordDetailView).
    • The launch path has no bundle or flow name there today, so the card threads them.
  • The runner's header notes that say description / inlineHelpText / FlowSchema.successMessage are off the surface are stale.
  • ⛔ No runner-side guessing from an object field's options: a screen field declares no such binding.

Verification (final head c17df03f0, which merges origin/main 4638625e0)

  • Typecheck: pnpm --filter PKG run typecheck exited 0 for @objectstack/spec (source, scripts, and the test layer: 52 files / 246 errors / 135 pinned signatures held), @objectstack/lint and @objectstack/cli.
  • Spec tiers in full, under the verify lock:
    • local: 637 files, 18977 passed and 1 todo.
    • repo: 54 files, 915 passed.
  • Lint: the full suite, 133 files / 6096 passed. validate-translation-references.test.ts alone: 162 passed.
  • CLI: the unit-tier test/i18n-* files, 24 files / 285 passed. The new i18n-flow-toast-option-coverage.test.ts is among them. The integration tier is declared to CI.
  • Generated: pnpm --filter @objectstack/spec check:generated reports "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 existing ScreenFieldConfigSchema refinement.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands derived 115 commands at this head. All 115 were run with their exit codes recorded, and --ran reports "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). They exited 0 after building the named packages.
    • pnpm check:platform-checklist exits 1, and it is not this diff. It reads ABSENT SYMBOL … attachment-access-hooks.ts#canEdit and SYMBOL ANCHORS LOST — 27 … floor is 28 in areas/attachments-storage.json. A clean worktree of origin/main 4638625e0 exits 1 with the same two lines. This diff touches neither file.
  • ESLint, narrowed to the 17 changed .ts files: eslint --no-inline-config --format json reports 17 files, 0 errors and 0 warnings.
    • None of the files is ignored; an ignored file would be a warning.
    • eslint.config.mjs enables no type-aware linting, so this diff cannot move a verdict on an untouched file.
  • Size: about 1,690 changed lines, generated files included.

Acceptance notes

  • The two new key families are 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 window screens had.
  • The screen title key has the same server-render boundary as description (a translated {{ }} hole draws literally). Option A above would fix both.
  • Bulk-action params still exclude option labels with the old "value is unconstrained" reasoning (BULK_PARAM_NO_OPTIONS). An action param's inline options are keyed by String(value) with no collision refusal. Neither is in this card.

Generated by Claude Code

claude added 9 commits October 9, 2026 23:33
…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>
… 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>
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cli, @objectstack/lint, @objectstack/spec, touching 33 documentable anchor(s). ⚠️ 7 changed file(s) yielded no anchor (packages/spec/api-surface/automation.json, packages/spec/api-surface/system.json, packages/spec/export-origins/automation.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via errorMessage (symbol, a field of interface FlowLike), errorMessage (literal, a string literal in FLOW_TERMINAL_MESSAGE_KEYS; a string literal in appTranslationDataShape))
  • content/docs/automation/flows.mdx (via errorMessage (symbol, a field of interface FlowLike), successMessage (symbol, a field of interface FlowLike), errorMessage (literal, a string literal in FLOW_TERMINAL_MESSAGE_KEYS; a string literal in appTranslationDataShape), successMessage (literal, a string literal in FLOW_TERMINAL_MESSAGE_KEYS; a string literal in appTranslationDataShape; a string literal in walkScreenFlows))
  • content/docs/getting-started/build-with-claude-code.mdx (via successMessage (symbol, a field of interface FlowLike), successMessage (literal, a string literal in FLOW_TERMINAL_MESSAGE_KEYS; a string literal in appTranslationDataShape; a string literal in walkScreenFlows))
  • content/docs/protocol/objectui/actions.mdx (via errorMessage (symbol, a field of interface FlowLike), successMessage (symbol, a field of interface FlowLike), errorMessage (literal, a string literal in FLOW_TERMINAL_MESSAGE_KEYS; a string literal in appTranslationDataShape), successMessage (literal, a string literal in FLOW_TERMINAL_MESSAGE_KEYS; a string literal in appTranslationDataShape; a string literal in walkScreenFlows))
  • content/docs/protocol/objectui/record-alert.mdx (via successMessage (symbol, a field of interface FlowLike), successMessage (literal, a string literal in FLOW_TERMINAL_MESSAGE_KEYS; a string literal in appTranslationDataShape; a string literal in walkScreenFlows))
  • content/docs/ui/actions.mdx (via errorMessage (symbol, a field of interface FlowLike), successMessage (symbol, a field of interface FlowLike), errorMessage (literal, a string literal in FLOW_TERMINAL_MESSAGE_KEYS; a string literal in appTranslationDataShape), successMessage (literal, a string literal in FLOW_TERMINAL_MESSAGE_KEYS; a string literal in appTranslationDataShape; a string literal in walkScreenFlows))
  • content/docs/ui/translations.mdx (via successMessage (symbol, a field of interface FlowLike), successMessage (literal, a string literal in FLOW_TERMINAL_MESSAGE_KEYS; a string literal in appTranslationDataShape; a string literal in walkScreenFlows))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-1.mdx (via errorMessage (symbol, a field of interface FlowLike), successMessage (symbol, a field of interface FlowLike), errorMessage (literal, a string literal in FLOW_TERMINAL_MESSAGE_KEYS; a string literal in appTranslationDataShape), successMessage (literal, a string literal in FLOW_TERMINAL_MESSAGE_KEYS; a string literal in appTranslationDataShape; a string literal in walkScreenFlows))
  • content/docs/releases/v17/17-5.mdx (via errorMessage (symbol, a field of interface FlowLike), translateFlow (symbol, a top-level function), errorMessage (literal, a string literal in FLOW_TERMINAL_MESSAGE_KEYS; a string literal in appTranslationDataShape))
  • content/docs/releases/v17/17-7.mdx (via successMessage (symbol, a field of interface FlowLike), successMessage (literal, a string literal in FLOW_TERMINAL_MESSAGE_KEYS; a string literal in appTranslationDataShape; a string literal in walkScreenFlows))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 7 changed file(s) yielded no anchor (packages/spec/api-surface/automation.json, packages/spec/api-surface/system.json, packages/spec/export-origins/automation.json, …) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 145 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6a3f82efa7d588267eb2e52c06a73c56315c636a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 70790eb170e4c3088b6ed0b9f5decb42229f58b7 — the merge of head 63f2467e48d0b5a9d450d8eb3ae6ebcb8c4e0669 into base 6a3f82efa7d588267eb2e52c06a73c56315c636a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 6a3f82efa7d588267eb2e52c06a73c56315c636a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…rowing, with its ADR-0087 disposition

Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 63f2467e48d0b5a9d450d8eb3ae6ebcb8c4e0669
Local-runs: none

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 origin/main at 4638625; main has since moved (6a3f82e) and only the PR's own delta is judged. Nothing was built, run or re-run.

① Derived judgments

Every accept-set and public-surface change the diff implies, each named right or wrong.

  1. Right — flows.FLOW.successMessage / .errorMessage join the face (translation.zod.ts), replacing the guidance refusal. translateFlow overlays a toast only where typeof flow[key] === 'string', so a bundle cannot add one, and a flow nothing resolves comes back as the same reference. The engine carries both strings verbatim on the terminal result (engine.ts at the merge base: successMessage: flow.successMessage, errorMessage: flow.errorMessage), so the client-side overlay is the right route — the one label already takes. The four new aliases (success, successText, error, errorText) refuse with a rename, not a tolerance.
  2. Right — …fields.FIELD.options.VALUE joins the field face as a string-to-string record keyed by String(value) through one new function, flowScreenFieldOptionKey, read by the schema refusal, translateFlow, resolveFlowScreenFieldOptions, the lint and the extractor alike. Not by position (a reorder would re-point every translation); not from an object field (a screen field declares no such binding). options is deliberately NOT in FLOW_SCREEN_FIELD_COPY_KEYS (a map over an array, not a string key), and the lookup reads own keys only. The aliases choices / values refuse with a rename. The key is the console select's own identity for an option (ScreenView.tsx at the objectui pin f0268ad78 keys each SelectItem by String(o.value)).
  3. Right — the one accept-set narrowing. ScreenFieldConfigSchema now refuses two options of one field whose values read the same as text, at the second option's value, leading with SCREEN_FIELD_OPTION_VALUE_COLLISION, naming both options by position and value, closing with the remedy; the refusal reaches the FlowSchema door. Reach measured 0 over the in-repo examples at 37c7114 (35 flows / 6 screens / 2 option fields / 10 options); hotcrm and hotclm by inspection, declared as inspection.
  4. Right — the exclusion texts. FLOW_SCREEN_FIELD_NO_OPTIONS is deleted with no leftover reference at the head; the toast guidance is gone; the screen description guidance now says "has no key … yet", why (the server renders it per run), and what the face does carry. BULK_PARAM_NO_OPTIONS keeps its own measured exclusion and says this change did not touch it.
  5. Right — public surface, additive. @objectstack/spec gains SCREEN_FIELD_OPTION_VALUE_COLLISION and flowScreenFieldOptionKey (automation), FLOW_TERMINAL_MESSAGE_KEYS, FlowTerminalMessageKey, FlowScreenFieldOptionLike and resolveFlowScreenFieldOptions (system); FlowLike and FlowScreenFieldLike gain optional members. api-surface/ and export-origins/ carry each one in the diff.
  6. Right — @objectstack/lint. A toast over a flow that declares none is translation-target-unknown (error, the orphan rule); an option key naming no declared value, naming a label, or sitting under a field that declares no options is translation-option-key-unknown (warning) — the rule and severity of the object-field and action-param legs — judged against the field's own options[] read through the same key function, never an object field's options.
  7. Right — @objectstack/cli. The extractor scaffolds exactly the keys the resolver reads (option keys through the same function); the coverage gate demands errorMessage for every flow that authors one and successMessage only for a flow with a screen. Both predicates mirror the readers at the objectui pin: FlowRunner.tsx:439 draws outcome.successMessage in the screen runner alone; flowResponse.ts:217 prefers data.errorMessage for any failed run.
  8. Right — the family pin flows-translation-face.test.ts walks the flow schemas mechanically, classifies every text-bearing leaf as keyed / noReader / elsewhere / owed, pins the owed set exactly (screen.description, approval.decisionOutputs[].label), fails on a stale row and on a face key no slot fills. The reported ablation (a planted subtitle, a renamed face key — red, then restored blob == HEAD) is the dev's reading; the pin's own green is CI's to confirm (Test Core, pending below).
  9. Right — the ledger. Two new child rows, flows.successMessage / flows.errorMessage, are planned with their reader named (contract-first, the shape screens took); the container stays live; the option map stays inside the screens row by the one-drill-level boundary; state counts 27 live / 2 planned / 29 classified. Spec property liveness is green on the head.
  10. Right, and declared — files beyond the claim's surface: builtin-node-config.zod.ts (the home of the refusal the claim orders), the new key leaf and its barrel line, the regenerated builtin-node-config.mdx, and three example bundles translated so check:i18n-coverage holds at its baseline rather than widening the ratchet. Each mechanical or implied by the claim; the report declared each.
  11. Right — the references under content/docs/references/ are regenerated through the generator, not hand-edited; the refusal prose, the guidance and the lint messages carry no tracker number.

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

  • The Clause-② declaration reads yes (narrowing) on both carriers — the changeset and the PR body's line 2 — and that is what the diff publishes: a widening (new face keys, new exports) AND an accept-set narrowing (item 3). The earlier spelling, which hid the narrowing from check-adr-0087-registration, was corrected in fix round 1 (6091764200); that correction is the only change in 63f2467e4 (+6/−2 on the changeset, no source).
  • Levels minor for @objectstack/spec, @objectstack/lint and @objectstack/cli — right: the launch-window grade for a breaking change, and at least minor for the two additive packages.
  • The changeset carries the BREAKING banner naming the narrowing, the remedy (give each option a value that stays distinct as text, or drop the duplicate), the measured reach, and the one ADR-0087 disposition not-required (no-migration-prescription) — honest: nothing is removed, renamed or re-shaped, so objectstack migrate meta has no rewrite and no mapping applies; the refusal carries the fix at every door that parses a flow.
  • Check Changeset (check-empty-changeset, check-adr-0087-registration, check-changeset-no-major) is green on the head.

③ Boundary flags

Each dev flag and open_questions entry, answered or escalated.

  • Q1 — which reader picks a screen's translated description: ESCALATED to the maintainer, deliberately not answered here (seat 6091764200, 6091840731). The server-render claim holds at the merge base: service-automation/src/builtin/screen-nodes.ts serves description: text(cfg.description) through renderTextSlot, flat and object-form screens alike, so an overlay on the served string would draw a translated hole literally. Leaving the slot owed is recorded honestly in every carrier: the face's TSDoc ("Not keyed: a screen's description … owed, not excluded … a decision of its own, not taken here"), the runtime guidance ("has no key … yet … no reader does that today"), the pin's owed row naming the decision it waits on, the i18n-resolver.ts docblock, the ledger container note, the changeset's "Still not keyed" bullet, and the PR body. One nit, not blocking: the older sentence in the ledger's screens row ("stay untranslated here by ruling") now reads against the owed framing; the PR that routes description rewrites it when it edits that row.
  • Q2 — the Clause-② spelling: answered B, done (verified in ②).
  • Q3 — the first line: answered A, Part of #22507; Part-of PR must not also close its card is green; the card stays open for the owed rows.
  • approval.decisionOutputs[].label recorded owed — outside the triage enumeration, pinned, held on this card by the seat as remaining work. ESCALATED beside Q1: the maintainer's decision act on 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 should also say whether it stays on this card or becomes its own; the pin's OWED list is edited in the same PR that routes it.
  • BULK_PARAM_NO_OPTIONS, and action-param inline options keyed by String(value) with no collision refusal — declared in the Acceptance notes, not this card; noted as a convention asymmetry across the three option maps, for a follow-up card if the maintainer wants one convention.
  • The objectui runner card (three reads: resolveFlowScreenFieldOptions in localizeScreen, successMessage in the success toast, errorMessage in flowFailureMessage through both launch hosts) — named in the PR body; the two planned rows flip when it lands at a pin. Two items go into that card: the runner's header note at the pin ("FlowSchema.successMessage — off the translation surface by design", FlowRunner.tsx:117) is stale, and ScreenView submits String(value) for a select.
  • check:platform-checklist is red on main itself ([finding] check:platform-checklist is red on main: attachments-storage.json anchors attachment-access-hooks.ts#canEdit, which #22513 (ce3d0ad41) moved to checkEdit #22557), kept out of per-PR CI; not this diff.

Checks on the head, read 2026-10-10T00:59Z: 23 success, 4 skipped, 0 failure, 12 in progress. Pending, and ⛔ not read as green: Lint & Repo Gates; Test Core (1/6) through (6/6); Dogfood Regression Gate (1/3) and (2/3); Temporal Conformance (live PG + MySQL); Type Check · workspace and Type Check · consumer gates (so the TypeScript Type Check aggregate has not reported). Green among the required set so far: Build Core, Governed Surface Queue Guard. This verdict is on the contract; landing waits for every required check to conclude success.

Implemented-by: claude/issue-22507-flow-user-read-slots
Reviewed-by: session_01DhTqaEHqPVSVnAkjG3jywn

VERDICT: PASS


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants