Skip to content

fix(cli): os lint and os i18n check ask for an approval flow's label and each approval step's label (#24916) - #25157

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-24916-approval-flow-i18n-coverage
Oct 22, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-24916-approval-flow-i18n-coverage

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24916

Clause-②: no
Metadata-protocol: none

What changes

The i18n extractor (packages/cli/src/utils/i18n-extract.ts) is the one walk behind both os lint's i18n/missing-* family and os i18n check. It asked for flows.FLOW.label only on a flow with a screen node, and it never walked approval nodes. So neither command ever listed an approval flow, or any approval step, as untranslated. That held even though the approvals door (plugin-approvals reader-locale-labels.ts, landed for #24863) reads flows.FLOW.label and flows.FLOW.approvals.NODE.label in the reader's locale, and the key was added to the contract in #24915.

  • Flow label. It is asked for on a flow with a screen node or an approval node (same deep node universe as the screens walk). A flow with neither still owes no label.
  • Approval steps. Every approval node with an id emits flows.FLOW.approvals.NODE.label, seeded with its FlowNode.label through the same pushOptional the flow label uses. APPROVAL_NODE_TYPE is 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).
  • Changeset: @objectstack/cli patch.

The mechanism hypotheses, measured (BASE 459afea1f)

  • H1, confirmed. The gate was the single condition if (hasScreen) in walkScreenFlows, and no approval-node arm existed. It is now hasScreen || approvalNodes.length > 0, plus the approvals walk.

  • H2, measured, and the answer is not "every flow". Flow.label is 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 of flows.FLOW.label were counted at objectui's .objectui-sha pin cf153eb2e and on origin/main. There are two:

    1. objectui FlowRunner (displayFlowLabel), which opens only on a run paused at a screen. Every other objectui consumer of the served flowLabel (FlowRunsPage, useConsoleActionRuntime, RecordDetailView, flowResponse.ts) hands it to that runner. flowLaunch.ts reads the flows bundle only for successMessage / errorMessage, and the refused arm names no flow.
    2. plugin-approvals readerLabelsFor / localizeRequestLabels (the inbox, the record's approval panel and the request page), for a request opened by an approval node.

    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. computeI18nCoverage builds its expected set from collectExpectedEntries, so one change serves both. Measured with the built CLI on a fixture: an approval-only autolaunched record-triggered flow expense_approval with two approval steps, a screen flow quick_note, a scheduled flow nightly_digest, and zh-CN.

    door before (BASE build) after (this branch's build)
    os lint --strict --json 3 i18n/missing-flow, all quick_note 6: expense_approval label, approvals.manager_review.label and approvals.finance_review.label, plus the same 3 quick_note
    os i18n check --show-keys zh-CN 2/5, missing 3 (quick_note only) zh-CN 2/8, missing 6 (the same six keys)
    either door, after translating expense_approval's three keys n/a only the 3 quick_note keys remain

    nightly_digest is asked for nothing in any leg.

  • H4, no row moves. authorWarnedProperties('translation') is empty, and flows.approvals / flows.approvals.label are live in packages/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, and lint-liveness-properties.ts needs no edit. See the acceptance notes for one evidence-text gap.

A file outside the dispatched surface: examples/app-showcase/src/system/translations/index.ts

check:i18n-coverage runs the built CLI over every example. Once the walk asked for the new keys, it measured examples/app-showcase/objectstack.config.ts at 414 → 437: 8 approval flow labels and 15 approval step labels, all zh-CN, with the identical list from os 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-coverage reads OK (14 configs, 621 baselined, none new), and showcase's os lint drops exactly those 23 findings and adds none. os validate on showcase exits 0 with zero translation-target-unknown, so every node id names an approval node. @objectstack/example-showcase is private, so it needs no changeset.

Coordination

Tests

New file packages/cli/test/i18n-flow-approval-coverage.test.ts (unit tier: no spawn). What it pins:

  • The walker emits flows.expense_approval.label and both approvals.NODE.label keys, seeded with the authored text, in the flow source. An id-less approval node is skipped. A flow with neither a screen nor an approval node gets no label.
  • The coverage gate lists the approval flow's label and steps in zh-CN as missing. The steps stay listed while only the flow label is translated, and everything goes quiet once all three are translated.
  • The screen flow's demand stays the same three keys as before, and the scheduled flow is asked for nothing.
  • The os i18n extract skeleton writes the authored labels in en and empty slots in zh-CN. Every locale parses through TranslationDataSchema with no issues, and so does a translated pack carrying the key.

The comment in i18n-flow-screen-coverage.test.ts that called FlowRunner "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 and git diff HEAD is empty.

  • A, predicate reverted to if (hasScreen) {: 4 failed, 6 passed. Failed: walker emits, id-less skip, gate lists, skeleton writes.
  • B, the approval-step pushOptional deleted: 4 failed, 6 passed. Failed: walker emits, gate lists, flow-label-only partial, skeleton writes.

Verification (final head f78411843, which merges origin/main 2aaee3b24; the 5 incoming commits touch none of this diff's paths)

All of these ran locally, on a shared box.

Tests and typecheck

run head result
@objectstack/cli unit tier, whole (vitest run --project unit) 7ed34f9d4 (the same fix as the final head; only an import line moved after it) 322 files, 4756 tests passed
the 28 test/*i18n*.test.ts files (non-e2e) 0a4800d41, then again on f78411843 28 files, 339 tests passed, both times
@objectstack/cli typecheck (tsc --noEmit plus check:test-typecheck) 0a4800d41 OK. The new test file is in the tsconfig.test.json program (--listFilesOnly).
@objectstack/example-showcase typecheck and test 0a4800d41 typecheck exit 0; tests 37 files, 443 passed
@objectstack/lint src/validate-translatable-sections.test.ts (imports the showcase bundle) 0a4800d41 33 passed
ESLint over the five changed source and test files (--no-inline-config) 0a4800d41 exit 0

The CLI integration tier 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 at f78411843, names 70 commands. Each ran with its exit code captured before any pipe.
  • --ran reconciliation: "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. On 0a4800d41 it 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


Generated by Claude Code

…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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 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
  • 1 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 — 30 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 23540582f4f7a783ee11337bbda12a8352a70f91 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7aafc132d8d91d3b36c24b62a2c9f7cb1feb67f1 — the merge of head f784118433e3dc44751026cd96f450feef3eddb7 into base 23540582f4f7a783ee11337bbda12a8352a70f91, 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 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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT (landing on green)

domain:cli seat 2 (#22648) · os-bill · session session_01AFrKwndmrCLtF5V9hNtrw3 · 2026-10-22T02:15Z · head f784118433e3dc44751026cd96f450feef3eddb7 · claim 6329730486 → report on #24916.

Check Reading
Shape Draft, base main. First line Fixes #24916; Clause-②: no, Metadata-protocol: none. One changeset, @objectstack/cli: patch. 6 files, +299 / −21. check-governed-merges.mjs --pr 25157: not governed, 320 lines ≤ 3000. No contract face (no Clause-②: yes, no narrowing declaration, no published schema, no governed path), so this ACCEPT is the review of record at the seat's tier.
The card's contract, read against the diff walkScreenFlows now asks for flows.FLOW.label on a flow with a screen node or an approval node (was hasScreen alone), and emits flows.FLOW.approvals.NODE.label for each approval node with an id, seeded with the node's label through pushOptional. The key is the one #24915 declared. A flow with neither node (scheduled, record-triggered, API) still owes nothing: measured, the label's two readers are the console's FlowRunner (screen-paused runs) and the approvals door, so "every flow" would ask for a string nothing reads. os i18n check's surface text names approval steps. The second @objectstack/spec/automation import line is deliberate: PR #24947 rewrites the first one.
Evidence New pin packages/cli/test/i18n-flow-approval-coverage.test.ts (10 tests); two ablations (the predicate reverted; the approvals push deleted) each red 4 of 10. CLI unit tier 322 files / 4756 tests; the 28 i18n test files 339/339 at the head. Both doors on a fixture: an approval-only flow is listed (label + steps) until translated, the screen flow keeps its 3 keys, the scheduled flow asks nothing. Local gates: 70/70 derived families exit 0, --ran reconciled.
Accepted deviations One file past the claimed surface: examples/app-showcase/src/system/translations/index.ts (+42, 23 zh-CN keys). The widened walk raised the showcase's untranslated count 414 → 437 in the required check:i18n-coverage, whose prescription is to translate, not to raise the baseline; the 23 strings read correctly. Repo-wide pnpm lint left to CI's Lint & Repo Gates (ESLint on the changed files exit 0).
Out of scope Noted, not filed: the flows.label liveness row cites only FlowRunner as evidence (ledger text); skills/objectstack-i18n/SKILL.md's sample list of os i18n check surfaces (governed; a sample, never complete); an approval node's decisionOutputs[].label has no key until a reader exists.
Landing CI was converging at this reading (18 success, 7 skipped, 13 in_progress, 0 failure). Not governed ⇒ pr_ready + automerge_enable through the relay once every check on this head is success or a by-design skip. #24993 (same file) is dispatched after this lands.

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 size/m tests tooling

Projects

None yet

2 participants