Repository navigation
fix(cli): os lint and os i18n check ask for an approval flow's label and each approval step's label (#24916) - #25157
Conversation
…proval step's label The extractor demanded `flows.FLOW.label` only on a flow with a screen node and never walked approval nodes, so `os lint` and `os i18n check` never listed an approval flow or its steps as untranslated, though the approvals door reads `flows.FLOW.label` and `flows.FLOW.approvals.NODE.label` in the reader's locale. The label predicate now mirrors both readers (a screen node or an approval node); a flow with neither still owes no label. Each approval node with an id emits `flows.FLOW.approvals.NODE.label`, seeded with its authored label. Claude-Session: https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3 Co-authored-by: Claude <noreply@anthropic.com>
…idened i18n coverage now asks for; changeset for the cli fix `check:i18n-coverage` measured app-showcase at 437 against its baselined 414 once the extractor asked for approval flow labels and approval step labels: eight flow labels and fifteen steps, all zh-CN. They are translated here, as the gate prescribes, so the baseline stays at 414 rather than freezing new debt. Claude-Session: https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3 Co-authored-by: Claude <noreply@anthropic.com>
…e automation import an in-flight PR rewrites Claude-Session: https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3 Co-authored-by: Claude <noreply@anthropic.com>
…proval-flow-i18n-coverage
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 30 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 7aafc132d8d91d3b36c24b62a2c9f7cb1feb67f1 && git checkout 7aafc132d8d91d3b36c24b62a2c9f7cb1feb67f1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 23540582f4f7a783ee11337bbda12a8352a70f91 f784118433e3dc44751026cd96f450feef3eddb7 && git checkout -B drift-repro 23540582f4f7a783ee11337bbda12a8352a70f91 && git merge --no-ff f784118433e3dc44751026cd96f450feef3eddb7
node scripts/docs-audit/affected-docs.mjs --json 23540582f4f7a783ee11337bbda12a8352a70f91 |
席位核对 · ACCEPT (landing on green)
Generated by Claude Code |
Fixes #24916
Clause-②: no
Metadata-protocol: none
What changes
The i18n extractor (
packages/cli/src/utils/i18n-extract.ts) is the one walk behind bothos lint'si18n/missing-*family andos i18n check. It asked forflows.FLOW.labelonly on a flow with ascreennode, and it never walkedapprovalnodes. So neither command ever listed an approval flow, or any approval step, as untranslated. That held even though the approvals door (plugin-approvalsreader-locale-labels.ts, landed for #24863) readsflows.FLOW.labelandflows.FLOW.approvals.NODE.labelin the reader's locale, and the key was added to the contract in #24915.screennode or anapprovalnode (same deep node universe as the screens walk). A flow with neither still owes no label.approvalnode with anidemitsflows.FLOW.approvals.NODE.label, seeded with itsFlowNode.labelthrough the samepushOptionalthe flow label uses.APPROVAL_NODE_TYPEis imported from@objectstack/spec/automation, not restated.os i18n check --help. The flow surface noun now reads "flow screens, approval steps, toasts and refusals" (SOURCE_SURFACE.flow, which the help-surface parity pin derives from).@objectstack/clipatch.The mechanism hypotheses, measured (BASE
459afea1f)H1, confirmed. The gate was the single condition
if (hasScreen)inwalkScreenFlows, and no approval-node arm existed. It is nowhasScreen || approvalNodes.length > 0, plus the approvals walk.H2, measured, and the answer is not "every flow".
Flow.labelis required by the schema, so "carries a label" would mean every flow. The extractor's own rule is to ask only where a reader shows the string, so the readers offlows.FLOW.labelwere counted at objectui's.objectui-shapincf153eb2eand onorigin/main. There are two:FlowRunner(displayFlowLabel), which opens only on a run paused at a screen. Every other objectui consumer of the servedflowLabel(FlowRunsPage,useConsoleActionRuntime,RecordDetailView,flowResponse.ts) hands it to that runner.flowLaunch.tsreads theflowsbundle only forsuccessMessage/errorMessage, and therefusedarm names no flow.plugin-approvalsreaderLabelsFor/localizeRequestLabels(the inbox, the record's approval panel and the request page), for a request opened by anapprovalnode.A record-triggered, scheduled or API flow with neither node has no surface that shows its translated label, so asking for it would be a stored-never-read demand. The code comment already said the predicate "widens in the same change as that reader", and this change widens it to that second reader.
H3, confirmed on both doors.
computeI18nCoveragebuilds its expected set fromcollectExpectedEntries, so one change serves both. Measured with the built CLI on a fixture: an approval-onlyautolaunchedrecord-triggered flowexpense_approvalwith two approval steps, a screen flowquick_note, a scheduled flownightly_digest, and zh-CN.os lint --strict --jsoni18n/missing-flow, allquick_noteexpense_approvallabel,approvals.manager_review.labelandapprovals.finance_review.label, plus the same 3quick_noteos i18n check --show-keysquick_noteonly)expense_approval's three keysquick_notekeys remainnightly_digestis asked for nothing in any leg.H4, no row moves.
authorWarnedProperties('translation')is empty, andflows.approvals/flows.approvals.labelareliveinpackages/spec/liveness/translation.json(flipped by spec: the translation liveness row flows.approvals still reads planned and ApprovalRequestRow's label docblocks still name the authored label, although PR #24977 is the reader that serves them in the reader's locale #24979). So the demand side and the warn side still agree, andlint-liveness-properties.tsneeds no edit. See the acceptance notes for one evidence-text gap.A file outside the dispatched surface:
examples/app-showcase/src/system/translations/index.tscheck:i18n-coverageruns the built CLI over every example. Once the walk asked for the new keys, it measuredexamples/app-showcase/objectstack.config.tsat 414 → 437: 8 approval flow labels and 15 approval step labels, all zh-CN, with the identical list fromos lint. The gate's own prescription for growth is to translate them, and raising the baseline would freeze 23 new strings of debt. So they are translated in the showcase's zh-CN pack.After that,
check:i18n-coveragereads OK (14 configs, 621 baselined, none new), and showcase'sos lintdrops exactly those 23 findings and adds none.os validateon showcase exits 0 with zerotranslation-target-unknown, so every node id names an approval node.@objectstack/example-showcaseis private, so it needs no changeset.Coordination
i18n-extract.ts, and rewrites the@objectstack/spec/automationimport line. This branch leaves that line byte-identical and importsAPPROVAL_NODE_TYPEon a separate line two unchanged lines below it. Agit merge-fileof this file against feat(spec,lint): a bulk action's confirmText is a template slot over count and its params, judged at validate, compile and the publish gate #24947's head over BASE exits 0 with no conflict markers.collectExpectedEntries) is not touched.Tests
New file
packages/cli/test/i18n-flow-approval-coverage.test.ts(unit tier: no spawn). What it pins:flows.expense_approval.labeland bothapprovals.NODE.labelkeys, seeded with the authored text, in theflowsource. An id-less approval node is skipped. A flow with neither a screen nor an approval node gets no label.os i18n extractskeleton writes the authored labels inenand empty slots inzh-CN. Every locale parses throughTranslationDataSchemawith no issues, and so does a translated pack carrying the key.The comment in
i18n-flow-screen-coverage.test.tsthat calledFlowRunner"the one reader" of the label now names the second reader. No assertion changed.Ablations (one per changed behaviour, through
scripts/ablation-replace.mjs). The subject is imported by relative source path, so no build is involved. Each restore was proven: blob equals HEAD andgit diff HEADis empty.if (hasScreen) {: 4 failed, 6 passed. Failed: walker emits, id-less skip, gate lists, skeleton writes.pushOptionaldeleted: 4 failed, 6 passed. Failed: walker emits, gate lists, flow-label-only partial, skeleton writes.Verification (final head
f78411843, which mergesorigin/main2aaee3b24; the 5 incoming commits touch none of this diff's paths)All of these ran locally, on a shared box.
Tests and typecheck
@objectstack/cliunit tier, whole (vitest run --project unit)7ed34f9d4(the same fix as the final head; only an import line moved after it)test/*i18n*.test.tsfiles (non-e2e)0a4800d41, then again onf78411843@objectstack/clitypecheck(tsc --noEmitpluscheck:test-typecheck)0a4800d41tsconfig.test.jsonprogram (--listFilesOnly).@objectstack/example-showcasetypecheckandtest0a4800d41@objectstack/lintsrc/validate-translatable-sections.test.ts(imports the showcase bundle)0a4800d41--no-inline-config)0a4800d41The CLI
integrationtier is left to CI. This diff touches no spawn entry and no integration-tier file.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, derived atf78411843, names 70 commands. Each ran with its exit code captured before any pipe.--ranreconciliation: "70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN", every exit 0.check:i18n-coverage: OK (14 configs, 621 baselined, none new).check:i18n: OK (10 packages in sync).check:i18n-walk-parity: 11 declared groups, 9 walked, 2 exempted.check:nul-bytes: OK.check:type-check-debt: OK.check:dual-build-cjs-loads: OK. On0a4800d41it first answered PREREQUISITE NOT MET (exit 3), because 8 unrelated packages were unbuilt in this worktree. After a turbo build of those 8 (all cache hits) it was re-read green.Acceptance notes
packages/spec/liveness/translation.jsonflows.labelstill cites only objectuiFlowRunneras evidence. Its status (live) is right, but the evidence omits the second reader,plugin-approvalsreader-locale-labels.ts#flowLabelKey. This is a ledger-text gap, not a defect. Carrier: none.skills/objectstack-i18n/SKILL.md(a governed surface, untouched here) lists the surfacesos i18n checkcovers as "… pages, flow screens". It was already a sample, not the set (no datasets, picklists, toasts or refusals), and now also omits approval steps. Carrier: none.walkScreenFlowsnow also walks approval flows, as it already walked toasts and refusals of flows with no screen. The name was kept to stay off the lines cli/i18n: translation coverage never expects a contributed navigation item's label — deleting its zh-CN key leaves os lint --strict green, though translation-target-unknown accepts that key #24993 touches.approvalnode (inside an ADR-0031 region) is refused bydefineStack(the durable-pause-in-region rule), so the deep walk reaches one only on a stack no public door admits. That is consistent withos validateaccepting the key "at any depth".decisionOutputs[].label(the decision dialog's output labels) still has no translation key. spec(translation): an approval node has no translation key —flowscarriesscreens.NODEandrefusals.NODEonly, so a pack's approval-step label is refusedtranslation-target-unknown(Seam: plugin-approvals reads it; split from #24863) #24915 recorded it as an owed row, waiting on a reader.Generated by Claude Code