Repository navigation
feat(spec,lint): flows translation face gains approvals.NODE.label for an approval node's step label - #24943
Conversation
…tion key (repro) Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…r an approval node's step label Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…or flows.approvals Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…proval-node-translation-key
📓 Docs Drift Check5 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 143 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4af783b671c18753da240e73b6a38756953ce8a1 && git checkout 4af783b671c18753da240e73b6a38756953ce8a1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 71696121a64d8bd057fac969a72a84295890ed37 606faa06ee40f4f065ade6632f939476269f20bd && git checkout -B drift-repro 71696121a64d8bd057fac969a72a84295890ed37 && git merge --no-ff 606faa06ee40f4f065ade6632f939476269f20bd
node scripts/docs-audit/affected-docs.mjs --json 71696121a64d8bd057fac969a72a84295890ed37 |
Contract reviewServed-tier: Read at 2026-10-20T17:44Z: card #24915 (body, the claim ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each named:
Reverse verification was reported by the dev (restoring the two source files to base failed exactly the 6 + 6 new/pin cases); I did not re-run it, and it is not a gate. ② Semver level
③ Boundary flagsDev
Nothing escalated. Head check-runs at the time of this record: every check-run on Implemented-by: VERDICT: PASS |
Closes #24915
Clause-②: yes (widening)
What changes
TranslationDataSchema.flows.FLOWcarriedlabel,successMessage,errorMessage,screens.NODE(screen nodes) andrefusals.NODE(refusingendnodes), and nothing for anapprovalnode. A language pack had nowhere to put an approval step's name. HotCRM measured this:flows.opportunity_approval.screens.manager_review.titleis refusedtranslation-target-unknown, becausemanager_reviewis anapprovalnode and not a screen.packages/spec/src/system/translation.zod.ts):flows.FLOW.approvals.NODE.labelis a declared key. It is addressed by theapprovalnode'sFlowNode.idand overlays that node's ownlabel. The entry is closed (strictObject).title,name,headingandstepLabelare renames ontolabel, andapprovalon the flow level is a rename ontoapprovals.objectstack validate(packages/lint/src/validate-translation-references.ts): the translation-reference check collects the flow'sapprovalnode ids at any depth, using the samewalkFlowNodesuniverse, so regions are included. Anapprovals.NODEkey over an approval node resolves. Over anything else it is refusedtranslation-target-unknown, with the existing diagnosis style:decisionnode, not anapproval, so nothing resolves this key."screennode: "...declares as ascreennode; its heading is keyedscreens.NODE.title."screens.NODEorrefusals.NODEkey written over an approval node now says "...declares as anapprovalnode; its step label is keyedapprovals.NODE.label." The old message only said the node is not a screen. It also read "as aapprovalnode".Why this spelling
approvals, keyed by node id: this follows the grammar the face already has.screensaddressesscreennodes andrefusalsaddresses refusingendnodes, each byFlowNode.id, the identifier the approvals service already holds (sys_approval_requeststores it asflow_node_id).label, nothing else. I readApprovalNodeConfigSchema(packages/spec/src/automation/approval.zod.ts). Its keys areapprovers,behavior,minApprovals,lockRecord,approvalStatusField,onEmptyApprovers,fallbackApprovers,decisionOutputs,escalationandmaxRevisions. None of them is a heading, instructions text or a comment prompt. The one person-facing string an approval step carries isFlowNode.label.plugin-approvalscopies that label ontocurrent_step_labelwhen a request opens (approval-service.ts,labelOrMachineName(input.nodeLabel, ...)). A screen spells its leaftitlebecause it overlaysScreenConfigSchema.title. This leaf overlays the node label, so it is spelledlabel, andtitleis an alias onto it.{{ }}text slot: the node label is plain text that is copied, not rendered throughrenderTextSlot. This is the same as the flow's ownlabel.decisionOutputs[]entry'slabel(a field label in the decision dialog). It stays anowedrow in the family pin, with its reason updated. It is a separate surface that needs its own reader.Tests
Measured at head
606faa06ee(merged withorigin/main).ebb460a714added the tests before the fix.translation.zod.tsandvalidate-translation-references.tsto the base81e6edd463(tree only,git restore --source). The mutation was confirmed on disk (approvals: z.recordcount 0,APPROVAL_NODE_TYPEcount 0). I rebuilt spec and ran the suites:6 failed | 158 passed. The four new approvals cases failed, and so did the two face-pin cases ("every keyed row ... exists in the face", "every face key is filled").6 failed | 180 passed. The five new refusal/redirect cases failed, and so did the one-line-verdict arm coverage.git checkout HEAD -- PATHS.git diff HEADwas empty andgit statuswas clean. I rebuilt spec, andablation-dist-preflightfound the marker present in 20 built files.screenskey over adecisionnode keeps its own diagnosis;instructions) is refused at parse.flows-translation-face.test.ts):node.labelis nowkeyedto bothscreens.NODE.titleandapprovals.NODE.label(newalsofield), so the face-key-filled check accounts for the new key.pnpm --filter @objectstack/spec test: 703 files, 21365 passed.pnpm --filter @objectstack/spec typecheck: exit 0.pnpm --filter @objectstack/lint test: 151 files, 7002 passed (after the merge).pnpm --filter @objectstack/lint typecheck: OK.pnpm --filter @objectstack/spec check:generated: all 14 artifacts are up to date. These were regenerated with--fix:content/docs/references/system/translation.mdx, the strictness-ledger counts and the liveness state counts.pnpm check:i18n: OK, andcheck:nul-bytes: OK.dispatch-gates --commands, reconciled with--ran): 113 derived, 113 run, allexit 0, 0 NOT-MEASURED (a derived zero: every family recorded an exit code), re-run on the merged head606faa06ee.--no-inline-config --format json): 5 files, 0 errors, 0 warnings. All 5 are inside the config (--print-configresolves each). The config declares no type-aware linting (noparserOptions.project), so this diff cannot change the verdict on any untouched file.Liveness
packages/spec/liveness/translation.jsongains aflows.approvalsrow (drilled tolabel) with statusplanned, notlive. The reader that renders the step label in the reader's locale is theplugin-approvalshalf of #24863, which this PR does not touch. It is a contract-first split, the same wayscreensand the toasts landed. #24863 remains open.Review tier
packages/specis a published schema, so this PR owes aCONTRACT_REVIEW_TIERreview of record before it enters the queue (pm-dispatchreferences/contract-review.md). It touches no governed surface (.claude/**,docs/adr/**,AGENTS.md, workflows,skills/**), so no Tier S or Tier H landing rule applies.Acceptance notes
packages/cli/src/utils/i18n-extract.ts) does not emitapprovals.NODE.labelslots in a skeleton. Asking apps to translate a key with no reader would be a declared-but-not-honoured demand. That demand belongs with the reader in Approvals show their flow and step names in the authoring language only: the request snapshots Flow.label / node.label at open, and the translation contract has no key for an approval step #24863.check:i18n-walk-parityis green with this diff.steps: 'screens'is left as it is. An author who writesstepsfor an approval step is told it renames toscreens, and validate then points them atapprovals.flowApprovalLabelKeyresolver helper is added. With no reader it would be an export with no consumer. The reader card can add it next toflowRefusalMessageKey.Generated by Claude Code