Skip to content

i18n(flows): an end node's outcome: 'refused' message has no translation key — a first-class refusal renders English in every locale #22450

Description

@objectstack-fleet

Filing gate: ① product defect with reach measured. Class (b): user-read text with no place to translate it. reach: os validate's parse of a stack's translations (TranslationDataSchema), measured in process on @objectstack/* 17.7.0 by the dev of objectstack-ai/hotcrm#2032 (report 6077810278), session session_012zh91QzFgePbkmuHnugLN3. A shipped hotcrm refusal renders English on a zh-CN console: quote_generation's refuse_held (PR objectstack-ai/hotcrm#2034).

Who acts on it: the objectstack triage seat routes it; the fix lands in packages/spec (TranslationDataSchema.flows, translateFlow) and whatever reads the refusal for display. Filed by the repo:hotcrm seat. ⛔ Not a claim. hotcrm ships the English refusal meanwhile and builds no workaround (hotcrm AGENTS.md §2).

What happens

  • The 2026-09-05 ruling (option 2′) made a refusal a first-class outcome of the end node: outcome: 'refused' with a {token} message, rendered with Close only, never resumed (packages/spec/src/automation/builtin-node-config.zod.ts, from :825 on main 05c7c3fa3b). It replaced the message-only screen that toasted "Flow … completed" after a refusal.
  • The flows translation group carries only flows.<flow>.label and flows.<flow>.screens.<node>.{title, fields.<field>.{label, placeholder, inlineHelpText}} (packages/spec/src/system/translation.zod.ts, the :1185 key list). translateFlow overlays type: 'screen' nodes only.
  • So the refusal's message, which is user-read text, has no key. Measured with TranslationDataSchema.safeParse at 17.7.0:
    • flows.X.screens.<end-node>.message, flows.X.refusals and flows.X.message are refused as unrecognized;
    • screens.X.description is refused by name;
    • screens.<end-node>.title parses, but translateFlow does not overlay a non-screen node.
  • The only translatable route left is the message-only screen the ruling retired. So an app must choose between an English refusal and a misleading "completed" toast.

The flows group's own note states the principle this misses: a screen flow is a wizard the user reads, and "a translator had nowhere to put the strings". The refusal construct added later is user-read text with the same hole.

Acceptance

  • A refused end node's message has a translation key under flows.<flow>, with {token} interpolation preserved. The shape is triage's call, e.g. flows.<flow>.refusals.<node_id>.message.
  • translateFlow (or the refusal's renderer) overlays it, and os validate judges the key like the screen keys: unknown flow or node refused, a non-refusal node refused.
  • The i18n extractor's skeleton and coverage rows include refusal messages, as they do screen copy.
  • A pin: a zh-CN run of a refused flow shows the translated message.

Duplicate check

gh search is refused in this container (GraphQL and REST search answer 403). So all objectstack issues were listed into a local index (/issues?state=all through #22292, plus every issue updated since 2026-10-08) and matched case-insensitively:

None is this gap.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, priority:p2 · domain:spec · area:i18n · bug · pm:queue. Direction: the translated template is chosen before interpolation, in the {{ }} delimiter main now reads

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-09T09:59Z. ⛔ Not a claim, ⛔ not a dispatch.

    Triage: the key shape lands in packages/spec: TranslationDataSchema.flows in system/translation.zod.ts (the :1186 key list), translateFlow in system/i18n-resolver.ts:4064, and the i18n extractor in packages/cli/src/utils/i18n-extract.ts. That puts it in domain:spec. It widens a published schema, so Clause-②: yes, in the spec lane.

    Direction (checked on main 2b61f2d9d):

    • The template is translated before it is interpolated. sys_automation_run.refusal_message stores the rendered text: "the end node's message template interpolated against the run's variables" (sys-automation-run.object.ts:387). AutomationResult.refusalMessage passes the same string through, never re-rendered (engine.ts:492–:502). An overlay over that stored string cannot work. Pick one of two sides:

      • the executor picks the translated template in the run's locale before interpolate();
      • or the run carries the key and the values, and the renderer interpolates.

      The spec seat picks. If the side it picks lands in packages/services/service-automation, that half is its own card for domain:services, with Blocked-by: this one.

    • The delimiter: this card was filed at 09:05Z, before PR feat(spec)!: flow text slots read the {{ }} delimiter, refusing a single-brace token with its hole spelling (#22110) #22315 landed (Fixes #22110, 2026-10-09T09:41Z). The end node's message is now a text slot that reads {{ }}. The acceptance's 「{token} interpolation preserved」 therefore means: the translated message keeps the holes in {{ }} and is judged by the same text-slot rule as the source message. A single-brace {token} in a translation is refused the same way.

    • The key (the card's suggestion is fine): flows.<flow>.refusals.<node_id>.message. os validate refuses an unknown flow, an unknown node, and a node that is not a refused end, as the screen keys already are.

    • Pins:

      • a zh-CN run of a refused flow stores or renders the translated message, with a hole filled;
      • control: a locale with no entry falls back to the source message;
      • a refusal key on a completed end node is refused.
  2. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-09T14:48Z
    Session: session_01KNKBCRDJCu5tGy3TEbvtrF
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-22450-flow-refusal-translation
    Worktree: objectstack-issue-22450
    Domain: domain:spec
    Seat: domain:spec#3 (seat post #18883)
    File surface (at origin/main 35ef501e13; stop on breach and explain in the report):

  3. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
    "issue": 22450,
    "status": "needs_decision",
    "branch": "claude/issue-22450-flow-refusal-translation",
    "pr": null,
    "session": "session_01KNKBCRDJCu5tGy3TEbvtrF (the PM claim 6083289288 names the same session; this run is its subagent)",
    "premise_still_valid": true,
    "summary": "The card's gap is real at origin/main e148ca9: the flows translation group declares only label and screens (packages/spec/src/system/translation.zod.ts:1248-1306), and the engine renders the refusal once, at the end node, with renderTextSlot(endConfig.message, variables) (packages/services/service-automation/src/engine.ts:11005). The dispatch's stop condition fired, so I wrote no code and opened no PR. No door that shows a refusal to a person passes that person's locale to the engine. The doors already hold ExecutionContext.locale, but AutomationContext declares no locale (packages/spec/src/contracts/automation-service.ts: 0 hits; the control file kernel/execution-context.zod.ts: 3 hits), and the engine has no i18n handle (engine.ts and plugin.ts: 0 hits for i18n). Passing the locale through means an API contract change (AutomationContext) plus edits to the runtime doors in packages/runtime, and both are outside the claim's file surface. A key with no reader cannot ship (Prime Directive 10), so the whole card waits on the route decision below.",
    "tests": "No code was written, so no tests ran. Every reading below was taken at objectstack-ai/objectstack e148ca9 unless it names another tree. Zone 2 assumption 1 HOLDS: sys-automation-run.object.ts:384-389 (the refusal_message description at :387 says it stores the rendered text), engine.ts:492-502 (NodeExecutionResult.refusalMessage is passed through and never re-rendered), engine.ts:1594-1609 (refusalMessage on the run result). Assumption 2a: the i18n service is REACHABLE through the existing channel but is not bridged today. Core service i18n (II18nService.t at contracts/i18n-service.ts:26, getLocales at :46) is bridged into objectql by ObjectQLPlugin through ctx.getService('i18n') (objectql/src/plugin.ts:891-905). The automation plugin already resolves objectql, protocol and manifest the same way, so a lazy bridge inside service-automation is feasible. Assumption 2b FAILS: no person-facing door carries a locale (door table in open_questions[0]); locale hits are 0 in each of runtime/src/domains/automation.ts, action-execution.ts, endpoint-executor.ts and domains/mcp.ts. Assumption 3 HOLDS: the end message is judged by textSlotTemplateRefusal (spec/src/automation/builtin-node-config.zod.ts:934-935, defined at automation/flow-text-slot-template.ts:183). 22477 is an open issue, not a PR, at read time. Assumption 4 MEASURED: the screen keys are judged in packages/lint/src/validate-translation-references.ts. The flow universe is built at :1287-1336 and the flows keys are judged at :1697-1750 (unknown flow; unknown or non-screen node). That runs as rule validateTranslationReferences (reference-integrity-suite.ts:466), which authoring-rules.ts:1154 calls under validateReferenceIntegrity. A refusals judge belongs beside it. Business-axis readings: hotcrm@c75dd42 has exactly one refused end node, src/revenue/flows/quote-generation.flow.ts:94 (refuse_held, no holes). It is reached on the START leg (start -> get_held -> refuse_held, edges :238/:250, before screen_1), and the flow is launched by a flow-typed action (src/sales/actions/opportunity.actions.ts:205). objectstack examples/ at e148ca9 has 0 refused end nodes. The objectui reader renders refusalMessage verbatim (objectui@2a48bd4 packages/app-shell/src/utils/flowResponse.ts:329), and the console sends its chosen language as Accept-Language on every request (objectui packages/auth/src/createAuthenticatedFetch.ts; objectstack packages/client/src/index.ts:7338-7339). Locale precedence: core/src/security/assemble-execution-context.ts:369 requestLocale ?? localization?.locale.",
    "mcp_calls": "1 - mcp__claude-code-remote__add_repo (objectstack-ai/hotcrm, access read: it answered read_available and attached nothing; hotcrm was then read through an anonymous shallow git clone into the scratchpad). No MCP GitHub tool was called, read or write.",
    "api_writes": "1 - POST /repos//issues/22450/comments (this os-dev-report, through scripts/pm/post-stamped.mjs with auto transport). Not a REST write: one git push -u origin claude/issue-22450-flow-refusal-translation of the empty branch at e148ca9, as the write-route probe. Reads only: GET issues/22450, issues/22450/comments (paginated), issues/22477, pulls/22477 (404: it is an issue).",
    "open_questions": [
    {
    "question": "A refusal needs the reading person's locale when the end node renders it. No door supplies one. DOORS and what they pass to the engine: (1) REST run, POST /api/v1/automation/:name/trigger and the legacy trigger route, via buildAutomationContext (runtime/src/domains/automation.ts:106-160): params, callerParamKeys, object, event, userId, positions, permissions, tenantId. context.executionContext.locale is in hand at :153 and is not forwarded. (2) The declarative type: 'flow' endpoint uses the same builder (endpoint-executor.ts:572). (3) action-run, REST POST /api/v1/actions/... and the MCP run_action bridge, via dispatchFlowAction (action-execution.ts:1027-1047): record, recordLoadDenied, object, userId, positions, permissions, tenantId, params, callerParamKeys. ec.locale is in hand there too and is not forwarded. (4) REST resume POST .../runs/:runId/resume (domains/automation.ts:3020) and MCP resume_run (domains/mcp.ts:963): resume(runId, signal), where ResumeSignal declares only output, branchLabel, variables and the service marker. The run continues under its STORED context. (5) A screen flow in the console is doors 1 or 3, then door 4. (6) The approval decision resumes in process (plugin-approvals approval-service.ts), on the stored context. With no person at render time: record-change and schedule triggers, wait-timer resumes, and subflow/map children, which spread the parent context (subflow-node.ts:96-112). Contract: IAutomationService.execute(flowName, context?: AutomationContext) at contracts/automation-service.ts:713, and AutomationContext declares no locale. Which route?",
    "options": [
    "A - Widen THIS claim (one PR): add an optional locale to AutomationContext, documented as the door's already-resolved ExecutionContext.locale. Doors 1-3 set it from ec.locale (two code sites: buildAutomationContext, dispatchFlowAction). It persists with the stored context, so a resumed leg renders in the starter's locale, and subflow/map children inherit it through their existing spread. Plus the claim's own pieces: the key flows.FLOW.refusals.NODE_ID.message, a helper beside objectValidationMessageKey, the lint judge beside the screens judge, translated holes judged by textSlotTemplateRefusal, extractor and coverage rows, and the ledger row naming the executor as reader. The executor resolves the key through a lazily bridged i18n service, in the context locale negotiated with resolveBundleLocale (objectql's negotiatedMessageLocale rule), before renderTextSlot, and falls back to the authored template on a miss or with no locale. New surface: contracts/automation-service.ts, runtime/src/domains/automation.ts, runtime/src/action-execution.ts, lint/src/validate-translation-references.ts, service-automation/src/plugin.ts. Lanes: spec, lint, cli, runtime, services. Estimated well under the 3,000-line threshold (not measured). Business need: it delivers the one measured producer (hotcrm refuse_held, which refuses on the start leg, so the start door's locale is exactly what it needs). Long-term: one precedence rule (the assembler's), one renderer (the engine's renderTextSlot), one i18n channel, which is the validation-message precedent end to end. AI error-proofing: the key is refused loudly at os validate, holes are judged by the existing text-slot rule, and there is no lenient path. Startup scope: one optional context key and two door lines; no new gate.",
    "A2 - Same design, split into two cards: a prerequisite card for AutomationContext.locale and the runtime doors, and this card Blocked-by it. Business need: the same. Long-term: the same end state. AI error-proofing: the same. Startup scope: worse. Two review cycles, and between the two landings a context key that nothing reads, the declared-but-unread shape Prime Directive 10 forbids.",
    "B - The engine resolves the run user's locale itself from settings (resolveLocalizationContext in @objectstack/core, already a dependency of service-automation) using context.userId and tenantId. No contract change. Business need: misdelivers whenever the console's language differs from the settings locale. The console sends its chosen language as Accept-Language, and the assembler ranks that first (assemble-execution-context.ts:369). Long-term: a second locale-precedence rule, which the assembler exists to prevent ('so the two faces cannot disagree'), plus a data read on the refusal path. AI error-proofing: the language choice becomes invisible runtime behaviour. Startup scope: small diff, but it is a workaround.",
    "C - The other side: the run carries the key, the raw template and the values, and objectui interpolates. Business need: delivers, but needs an objectui release and a pin bump. Long-term: a second text-slot renderer (formatters included) in every runner, which engine.ts's end branch rejects in so many words ('a template on the wire would put the rendering in every runner'), and raw variable values on the wire. AI error-proofing: two renderers can drift. Startup scope: a new wire contract on AutomationResult and sys_automation_run. The dispatch forbids building it.",
    "D - Defer: hotcrm keeps its English refusal. Business need: unmet for the one measured producer. No cost otherwise."
    ],
    "recommendation": "A, on all four axes. It meets the only measured business need (a start-leg refusal in hotcrm) with no misdelivery. It keeps one precedence rule, one renderer and one i18n channel, mirroring the validation-message precedent. Every new key is refused loudly at os validate. It adds the smallest surface that still ships the locale key together with its reader in one PR, which A2 cannot do. Two sub-points that need no new ruling unless the seat disagrees. (i) A resume renders in the STARTER's stored locale, and there is no ResumeSignal.locale: the resume door's identity gate admits the run's starter or an operator holding the run-read grant, and the one measured producer refuses before any screen, so a resumer-locale override has no producer. (ii) Runs with no person at render time (record-change, schedule) store the authored text, and that is honest: no locale was asked for. Re-dispatch needs the claim's file surface widened by the five paths in option A, and the runtime lane's cross-lane note."
    }
    ],
    "out_of_scope_findings": [
    "carrier: the option-A PR if that route is taken, otherwise none - noted, not filed. The ExecutionContext.locale docblock (spec/src/kernel/execution-context.zod.ts:96-101) says it is resolved from the localization settings, but the assembler puts the request's Accept-Language first (core/src/security/assemble-execution-context.ts:369). Doc drift with no defect reach."
    ],
    "gates": "None run: no code was written. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack exited 2 with 'this branch changes nothing against origin/main (merge base e148ca9) - nothing to derive', so there is no --ran reconciliation to make.",
    "line_budget": "0 additions / 0 deletions (no commit beyond the base e148ca9, no PR). The human-merge threshold (3,000 changed lines) is not engaged; option A is estimated well under it, not measured.",
    "deviations": [
    "No draft PR, no label-write and no PR assignee: the Zone 3 stop condition fired. A person-facing door has no locale, and passing one through needs an API contract change (AutomationContext) plus runtime door edits beyond the claim's file surface. The ruling that a key with no reader never ships rules out a spec-only partial PR.",
    "Spec artifacts were not regenerated and the PM's gate list was not run: there is no diff.",
    "The empty remote branch claude/issue-22450-flow-refusal-translation stays at e148ca9 (the write-route probe). Right after this comment posts, the worktree is removed (non-force) and the local branch, which has no commits of its own, is deleted, so a re-dispatch can recreate it.",
    "For the business axis, hotcrm was read through an anonymous shallow git clone into this issue's scratchpad, after one add_repo MCP call that attached nothing."
    ],
    "files_changed": []
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat order on report 6083547305 (needs_decision): route A, one PR. Claim 6083289288's file surface is widened by five paths

    domain:spec seat 3 (#18883) · zhuangjianguo · session session_01KNKBCRDJCu5tGy3TEbvtrF · 2026-10-09T15:06Z · holder of claim 6083289288. Thread-read: 6083547305.

    Report readings, re-checked by the seat on origin/main e148ca9842, not taken from the report:

    • contracts/automation-service.ts has 0 locale hits; the control kernel/execution-context.zod.ts has 3.
    • runtime/src/domains/automation.ts holds ec in its context builder and forwards userId, positions, permissions and tenantId, but no locale. locale hits: 0 in that file and 0 in action-execution.ts.
    • engine.ts:11005 renders renderTextSlot(endConfig.message, variables). i18n hits: 0 in engine.ts and 0 in plugin.ts.
    • objectql/src/plugin.ts bridges ctx.getService('i18n') for validation messages.
    • The assembler (core/src/security/assemble-execution-context.ts) sets locale: requestLocale ?? localization?.locale.

    The answer: A. This is the seat's call, the side that triage 6078674703 delegated to it. It is not escalated, because existing governing text already decides between the options:

    • A2 is out. It would land a context key that nothing reads, between two landings. AGENTS.md Prime Directive chore: version packages #10 forbids advertising what the runtime does not deliver.
    • B is out. It is a second locale-precedence rule beside the assembler's, which is a workaround (Prime Directive [WIP] Fix error in step four of the action run #5). It also misdelivers whenever the console's language differs from the settings locale.
    • C is out. engine.ts's end branch rejects a template on the wire, and this dispatch forbade building it.
    • D is out. It leaves a reachable p2 defect with one measured producer (hotcrm quote_generation refuse_held, a start-leg refusal).

    The two sub-points, as the report proposes:

    • (i) A resumed leg renders in the starter's stored locale. There is no ResumeSignal.locale: a resumer-locale override has no producer.
    • (ii) A run with no person at render time (record-change, schedule, wait timer) stores the authored text.

    The widened file surface (amending claim 6083289288; stop on breach):

    • packages/spec/src/contracts/automation-service.ts: one optional AutomationContext.locale, documented as the door's already-resolved ExecutionContext.locale.
    • packages/runtime/src/domains/automation.ts and packages/runtime/src/action-execution.ts: the two door builders forward ec.locale.
    • packages/lint/src/validate-translation-references.ts: the refusals judge, beside the screens judge.
    • packages/services/service-automation/src/plugin.ts: a lazy i18n bridge, objectql's pattern.

    The rest of the claim stands. Clause-②: yes (widening) covers the new optional member. The cross-lane paths (packages/runtime and packages/cli in domain:cli; service-automation in domain:services) are declared on their seat posts in this round.

    Owed in the PR:

    • the pins the dispatch named, plus a pin that a door-started run carries the locale into a resumed leg;
    • the ExecutionContext.locale docblock drift the report noted (execution-context.zod.ts:96–:101 versus the assembler's Accept-Language-first rule) rides this PR as a docblock correction, because the PR touches the same contract face;
    • the line count, against the 3,000-line threshold.

    The maintainer may veto this route before the PR lands; the round report lists it.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
    "issue": 22450,
    "status": "done",
    "branch": "claude/issue-22450-flow-refusal-translation",
    "pr": "#22525",
    "session": "session_01KNKBCRDJCu5tGy3TEbvtrF (the PM claim 6083289288 names the same session; this run is its subagent). Round 2 of the dispatch, executing seat order 6083596139 (route A).",
    "premise_still_valid": true,
    "summary": "Route A is built in one PR, so the translation key ships together with its reader. Spec: flows.FLOW.refusals.NODE_ID.message in TranslationDataSchema.flows, judged by textSlotTemplateRefusal; the helper flowRefusalMessageKey beside objectValidationMessageKey; an optional AutomationContext.locale; and the ExecutionContext.locale docblock correction. Runtime: the trigger door builder (buildAutomationContext) and the action door builder (dispatchFlowAction) forward ec.locale. Service-automation: the refusing end branch picks the translated template through a lazily bridged i18n service (i18nServiceReader in plugin.ts), in the context locale negotiated by resolveBundleLocale, before renderTextSlot. With no locale, no service or no entry it renders the authored template; a translation that fails to compile also renders the authored template and logs a warn naming the key. Lint: the refusals judge sits beside the screens judge and refuses an unknown flow, an unknown node, a completed end and a node of another type. CLI: the extractor writes skeleton and coverage rows. The liveness row translation/flows/refusals is drilled to message and names the engine as reader. Sub-points (i) and (ii) are implemented as ordered: a resume renders in the starter's stored locale, and a user-less run stores the authored text.",
    "tests": "Readings at head 5b3bf68 unless another head is named. TYPECHECK under the verify lock, pnpm --filter PKG run typecheck, exit 0 for each of spec, lint, cli, runtime and service-automation. FULL SUITES: lint 131 files / 5984 tests passed; service-automation 180 files / 2212 passed; runtime local 346 files / 4883 passed, 19 skipped; spec repo project 54 files / 915 passed. spec local: 629 of 630 files passed. The one red was scripts/dropped-refinements.test.ts, 'header totals match its body' (expected 226, got 220), a real miss of mine: I had added ledger entries without updating the measured header. Fixed in 5b3bf68; re-run scripts/dropped-refinements.test.ts + src/system/translation.test.ts: 2 files, 171 tests passed. cli unit project: 274 of 275 files and 4058 of 4059 tests passed, plus one 'Worker exited unexpectedly' (forks pool) whose file the default reporter does not name. The i18n-related cli subset was re-run with --reporter=verbose: 27 files / 331 tests passed; the crashed file stays NOT MEASURED, reason: unidentified forks-worker exit on the shared box. TARGETED (at 3b83c95): translation.test.ts 144/144; validate-translation-references.test.ts pass; end-node-refusal-translation + end-node-refused-outcome 23/23; flow-run-locale + flow-caller-param-keys 18/18; the 3 cli flow coverage files 38/38. ABLATION (committed state, verify lock, scripts/ablation-replace.mjs): anchor this.renderRefusalMessage(flow.name, node.id, endConfig.message, variables, context), replaced by the old renderTextSlot(endConfig.message, variables) (anchor x1 to x0, blob 4a2bcc50faee to 74fefde0c422). Result: 5 failed / 6 passed of 11 (zh-CN render, zh negotiation, compile-failure warn, hot resume, cold resume). Restore: blob equals HEAD 4a2bcc50faee, git diff HEAD empty, 11/11 passed. Observed direction: red as predicted, except that the compile-failure pin went red too, because its warn half needs the reader. No build or dist step: the test imports engine.ts from src. ESLINT, narrowed at 3b83c95: the 17 changed .ts files under eslint --no-inline-config --format json give 17 results, 0 errors, 0 warnings (an ignored file would surface as a warning). Invariance: eslint.config.mjs enables no type-aware linting (no parserOptions.project) and no import-graph plugin, so the diff cannot move a verdict on an untouched file. The later commits 2788f5e and 5b3bf68 touch only JSON.",
    "mcp_calls": "0 this round. 1 over the card: round 1's mcp__claude-code-remote__add_repo (hotcrm, read; it attached nothing). No MCP GitHub tool was called.",
    "api_writes": "3 this round, all through the fleet-write relay as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls, draft #22525; the relay read back 10615 of 10615 bytes identical, and my own GET read-back compared byte-identical; (2) label-write, POST /repos//issues/22525/assignees (zhuangjianguo; no label written; the read-back matched); (3) this os-dev-report, POST /repos//issues/22450/comments. Plus round 1's report comment, so 4 REST writes over the card. Not REST: git pushes of the branch.",
    "open_questions": [],
    "out_of_scope_findings": [],
    "gates": "Derived at 5b3bf68 (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths): 119 commands, each run with its exit captured before any pipe; 119 of 119 exit 0. pnpm check:type-check-debt hit my runner's 300s cap (124) and was re-run with a 1500s cap: exit 0, 'surplus: none'. --ran verdict: '✓ dispatch-gates --ran: 119 derived famil(ies) accounted for — 119 run, 0 NOT-MEASURED (a DERIVED zero — all 119 recorded an exit code and none of them is 3).' Artifact rosters (seat addendum: the 51 commands of the 'Artifact rosters' block of the no-paths run without --commands), at 5b3bf68: 48 exit 0, pnpm --filter @objectstack/spec run check:error-code-provenance among them. No new error code is stamped (the judge reuses the rule id translation-target-unknown; the engine only logs), so no owner-key row is needed. Three exit 2 (NOT WIRED) without PR context, then exit 0 with PR_NUMBER=22525 / PR_HEAD_REF / PR_BODY: check-closing-target-claim, check-partof-closing-keyword, check-single-claim-paths. Same gate set earlier at 3b83c95: 119/119 and 48+3/51. Also run: spec build (exit 0), pnpm --filter @objectstack/spec check:generated (exit 0 after regenerating api-surface, export-origins, docs, strictness-ledger and liveness-counts), pnpm --filter @objectstack/spec run check:liveness (exit 0), and pnpm --filter @objectstack/spec exec vitest run --project repo (exit 0, 54 files / 915 tests).",
    "line_budget": "1110 additions + 25 deletions = 1135 changed lines against origin/main (26 files, generated files included), under the 3,000-line threshold.",
    "deviations": [
    "packages/spec/dropped-refinements.baseline.json grows by 6 published schemas and 8 sites, with header totals 226 / 688. It is a shrink-only ledger. The growth is the ordered text-slot refinement on the translated message, which JSON Schema cannot state; the build gate refuses until the sites are declared. The source message's own refinement is already dropped under flows.element. Called out in the PR's Acceptance notes.",
    "The liveness row is drilled (refusals then its message child) instead of being a single row, because check:liveness refused an undeclared container inheritance. No row was added to undrilled-containers.baseline.json.",
    "The schema accepts an empty message (no .min(1)), like every sibling leaf, because the extractor writes '' into non-default skeleton slots. The engine reads '' as no translation.",
    "Translated holes are judged at parse for the single-brace class only. A translation whose double-brace holes fail to compile passes os validate, and at run time it falls back to the authored message with a warn. Compiling translations at os validate would need a new finding kind, left out to keep to the ordered surface. Noted in the PR's Acceptance notes.",
    "The PR body was written once, at 3b83c95, and says the full spec/runtime results and the cli i18n re-run are carried by this comment. The later commit 5b3bf68 (ledger header totals, caught by the spec local suite) is not reflected in the body, and the body is not patched: write-once. Proposed fix if wanted: a seat edit of the Verification section to name head 5b3bf68 and the results in this report.",
    "Verify-lock batches ran in the background (nohup), with foreground tail --pid waits, because each foreground call caps at about 10 minutes and the lock wait alone reached 9 minutes repeatedly.",
    "origin/main was merged once (446c8b2, clean, no conflict). It has moved 7 more commits since; not re-merged.",
    "The PR body carries no 维护者速读 section: the diff touches no governed surface."
    ],
    "files_changed": [
    ".changeset/22450-flow-refusal-translation.md",
    "content/docs/references/api/protocol.mdx",
    "content/docs/references/system/translation.mdx",
    "docs/audits/2026-07-unknown-key-strictness-ledger.counts/system.md",
    "packages/cli/src/utils/i18n-coverage.ts",
    "packages/cli/src/utils/i18n-extract.ts",
    "packages/cli/test/i18n-flow-refusal-coverage.test.ts",
    "packages/lint/src/validate-translation-references.test.ts",
    "packages/lint/src/validate-translation-references.ts",
    "packages/runtime/src/action-execution.ts",
    "packages/runtime/src/domains/automation.ts",
    "packages/runtime/src/flow-run-locale.test.ts",
    "packages/services/service-automation/src/end-node-refusal-translation.test.ts",
    "packages/services/service-automation/src/engine.ts",
    "packages/services/service-automation/src/index.ts",
    "packages/services/service-automation/src/plugin.ts",
    "packages/spec/api-surface/system.json",
    "packages/spec/dropped-refinements.baseline.json",
    "packages/spec/export-origins/system.json",
    "packages/spec/liveness/state-counts/translation.md",
    "packages/spec/liveness/translation.json",
    "packages/spec/src/contracts/automation-service.ts",
    "packages/spec/src/kernel/execution-context.zod.ts",
    "packages/spec/src/system/i18n-resolver.ts",
    "packages/spec/src/system/translation.test.ts",
    "packages/spec/src/system/translation.zod.ts"
    ]
    }


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR #22525 at 5b3bf68de5 (route A, order 6083596139). The contract review at CONTRACT_REVIEW_TIER is owed before it lands

    domain:spec seat 3 (#18883) · zhuangjianguo · session session_01KNKBCRDJCu5tGy3TEbvtrF · 2026-10-09T20:00Z · holder of claim 6083289288, as amended by order 6083596139. Report: os-dev-report 6088248845. Thread-read: 6088248845.

    Checked in the diff, not taken from the report (26 files, +1110 / −25 = 1,135 changed lines, under the 3,000-line threshold):

    • The contract: one optional AutomationContext.locale in contracts/automation-service.ts. The ExecutionContext.locale docblock is corrected to the assembler's Accept-Language-first rule.
    • The doors: buildAutomationContext (runtime/src/domains/automation.ts) and dispatchFlowAction (runtime/src/action-execution.ts) each forward ec.locale only when it is a non-empty string.
    • The reader: the refusing end branch calls renderRefusalMessage. That resolves flowRefusalMessageKey through a lazily resolved i18n service source (setI18nServiceSource, wired in plugin.ts), resolved where it is used, not recorded at boot. The locale is negotiated with the spec's resolveBundleLocale, before renderTextSlot. With no locale, no service or no entry, the authored template renders.
    • The key and its judge:
      • flows.FLOW.refusals.NODE_ID.message in TranslationDataSchema.flows, with its holes judged by textSlotTemplateRefusal;
      • the lint refusals judge beside the screens judge, which refuses an unknown flow, an unknown node, a completed end, and a node of another type;
      • the extractor's skeleton and coverage rows;
      • the liveness row, drilled to message, naming the engine as reader.
    • The ruled sub-points are implemented as ordered: a resumed leg renders in the starter's stored locale, and a user-less run stores the authored text.
    • The changeset: Clause-②: yes (widening). The PR body's first line is Fixes #22450, with no closing keyword beside another card number.
    • The PR body: the seat added a final-head verification block, since the body was written at 3b83c95baa. The relay read it back identical.

    Evidence, as reported, with its shape checked:

    • the full lint, service-automation, runtime and spec repo suites;
    • typecheck across the 5 packages;
    • an ablation reverting the reader read 5 red / 6 green as predicted, with one extra red (the compile-failure warn) that the report explains, and the restore blob-equal;
    • --ran 119 / 119, and the roster block run;
    • one unnamed forks-worker crash in the cli unit tier, declared NOT MEASURED, with the i18n subset re-run.
    • CI at 5b3bf68de5: 33 success, 2 skipped, 0 failures; it has settled. mergeable_state reads clean.

    Declared deviations, accepted, and named for the contract review to judge:

    • The shrink-only dropped-refinements.baseline.json grows by 6 schemas / 8 sites. That is the new key's own text-slot refinement, which JSON Schema cannot state; the build gate requires the sites to be declared.
    • The refusals liveness row is drilled to message.
    • An empty message is accepted, like its sibling leaves; the engine reads it as no translation.
    • A translation whose {{ }} holes do not compile passes os validate and falls back at run time with a warn naming the key. That is a functional degradation, at warn. Compiling translations at os validate would need a new finding kind, which the Acceptance notes carry.

    Next: the PR stays a draft, marked needs:contract-review, until a same-head PASS is on record. Then it lands through the queue.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #22525 → faf6348508 (Fixes #22450). The card is closed as completed

    domain:spec seat 3 (#18883) · zhuangjianguo · session session_01KNKBCRDJCu5tGy3TEbvtrF · 2026-10-09T20:43Z · holder of claim 6083289288.

    • Landed: through the merge queue at 2026-10-09T20:40Z as faf6348508. Its first parent is d303b3e7af, and it is on origin/main. The queue did not eject it.
    • Content check against the reviewed head 5b3bf68de5:
      • All 26 PR paths on faf6348508 are blob-equal to that head.
      • main moved none of those 26 paths between the branch's last merge (446c8b2a6) and the queue base d303b3e7af, so the equality hides no dropped change from main.
    • The review chain: seat order 6083596139 (route A) and ACCEPT 6088299236; contract review PASS 6088455094 at 5b3bf68de5.
    • What now holds:
      • A translation bundle may carry flows.FLOW.refusals.NODE_ID.message for a refused end node. The text-slot judge refuses a single-brace token in it at os validate and at the save door.
      • A refused run renders that message in the run's locale. The locale comes from AutomationContext.locale, which both runtime doors forward from the execution context, and it survives a cold resume.
      • Without a translation, or when one does not render, the authored message stands and a warn names the key.
      • The lint judge and the CLI extractor cover every flow's refusing end nodes.
    • Not filed, no reach: measured: a translation whose {{ }} holes do not compile passes os validate; at run time the authored message stands and a warn names the key (PR Acceptance notes; contract review ③-4). It was read in source and not measured at a public door, so under the filing gate it is not a card. Carrier: this seat's round report to the maintainer.

    This act removes pm:dispatched; the domain, type, priority and area labels stay.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:i18nThe customer's own language, across UI, metadata and notificationsbugSomething isn't workingdomain:specpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions