Skip to content

fix(spec): grade action.onSuccess.navigate/openIn and translation.flows.screens live - #20328

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-20296-understated-planned-rows
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-20296-understated-planned-rows

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20296
Clause-②: no

Three liveness rows were graded planned, but objectui reads all three at the .objectui-sha pin this repo builds against (f8a9d0fb). This PR grades them live. Each row cites its mounted reader by file and symbol, with no line numbers, and names its producer leg. Each row's note says why it was planned and what flipped it.

Following the seat ruling on the card (comment 5860372033, answer B), the PR also corrects every sentence these flips made false. The flows container keeps its authorWarn, and its authorHint now tells the truth. Four prose sites are restated. ⛔ No schema, parse, .describe() or accept-set change. The three packages/spec/src/** edits are comments only, and a comment-stripped transpile proves it (below).

Files:

  • packages/spec/liveness/action.json: the two onSuccess rows.
  • packages/spec/liveness/translation.json: the flows.screens row, plus the flows container's authorHint and a dated sentence appended to its note.
  • packages/spec/liveness/README.md: the translation Notes cell.
  • packages/spec/liveness/state-counts.md: regenerated.
  • JSDoc and docblock comments in packages/spec/src/ui/action.zod.ts, packages/spec/src/system/translation.zod.ts and packages/spec/src/system/i18n-resolver.ts.
  • .changeset/20296-understated-planned-rows.md.

Row → reader, at objectui @f8a9d0fb

row mounted reader (file · symbol) what it reads producer leg control at objectui main 256b4c9e
action.onSuccess.navigate packages/core/src/actions/ActionRunner.ts · handlePostExecution → readOnSuccessNavigation → navigateOnSuccess; packages/app-shell/src/hooks/useConsoleActionRuntime.tsx · useConsoleActionRuntime (its navigateHandler) block.navigate, interpolated with ${param.*} / ${ctx.*} / ${result.*}, with the scheme guarded, then handed to the host's navigationHandler (a react-router navigate in the console) the forward onSuccess: schema.onSuccess in ActionButtonRenderer, ActionIconRenderer, ActionMenuRenderer and ActionGroupRenderer, and DeclaredActionButton's spread of the declared action same reads
action.onSuccess.openIn ActionRunner.ts · navigateOnSuccess; useConsoleActionRuntime (navigateHandler) block.openIn === 'newTab' becomes newTab, with no ?? 'self' of its own the spec's ActionSchema materializes .default('self'); the same renderer forward same reads
translation.flows.screens local packages/spec/src/system/i18n-resolver.ts · resolveFlowScreenTitle; objectui packages/app-shell/src/views/FlowRunner.tsx · localizeScreen, activeFlowsBundle and FlowRunner the heading through resolveFlowScreenTitle, and each field's label / placeholder from bundle[language]?.flows?.[flowName]?.screens?.[screen.nodeId]?.fields over FLOW_SCREEN_FIELD_COPY_KEYS this repo: packages/runtime/src/app-plugin.ts · loadTranslations and packages/runtime/src/domains/i18n.ts · handleI18nRequest serve the bundle whole. objectui: apps/console/src/loadLanguage.ts · loadLanguage → packages/i18n/src/utils/spec-translations.ts · transformSpecTranslations (forwards flows verbatim under app) → packages/i18n/src/provider.tsx · I18nProvider (addResourceBundle into translation) same reads

The per-read control. Each cited string was counted with git show REF:PATH | grep -cF at the pin and at objectui main. All 15 reads gave the same count at both: readOnSuccessNavigation(action.onSuccess) 1/1, private navigateOnSuccess( 1/1, block.navigate 2/2, block.openIn === 'newTab' 1/1, onSuccess: schema.onSuccess 1/1, consoleServerAction's readOnSuccessNavigation(action.onSuccess) 1/1, resolveFlowScreenTitle(bundle, flowName, screen 1/1, ?.screens?.[screen.nodeId]?.fields 1/1, function activeFlowsBundle 1/1, FlowRunner in useConsoleActionRuntime.tsx 1/1, useConsoleActionRuntime({ dataSource }) in ConsoleShell.tsx 1/1, ConsoleShell in apps/console/src/App.tsx 1/1, 'flows', in spec-translations.ts 1/1, transformSpecTranslations(translations) 1/1, and r.setNavigationHandler(onNavigate) 1/1. The ledger cites the historical form, "the .objectui-sha pin f8a9d0fb".

Mount chains, closed by hand at the pin

A reader inside a component that nothing mounts is not evidence. So each chain was walked down from the console root.

  • The onSuccess pair.
    • apps/console/src/App.tsx renders ConsoleShell.
    • ConsoleShell calls useConsoleActionRuntime({ dataSource }) and mounts ActionProvider with onNavigate={navigateHandler} and handlers = api, flow and script.
    • @object-ui/react's ActionContext builds the ActionRunner and calls setNavigationHandler(onNavigate).
    • A declared action can be rendered by action:button, action:icon, action:menu or action:group; ObjectView's list toolbar renders action:bar. It can also be rendered by DeclaredActionsBar. Each of these calls execute with onSuccess still on the def.
    • ActionRunner.execute dispatches to the registered api or script handler, then to handlePostExecution. That call follows both the custom-handler path and the built-in path. It then runs readOnSuccessNavigation and navigateOnSuccess.
    • For a script action, consoleServerAction.ts sees the declared block and only closes the pre-opened tab, so one success makes one navigation. The spec already refuses onSuccess together with opensInNewTab: true.
  • flows.screens.
    • FlowRunner is mounted in three places: in useConsoleActionRuntime's dialogs (rendered by ConsoleShell and by the runtimes that ObjectView and DeclaredActionsBar run), in RecordDetailView, and on the console route developer/flow-runs (FlowRunsPage).
    • When a type: 'flow' action's run pauses at a screen node, the host sets { flowName, runId, screen }.
    • FlowRunner computes shown = localizeScreen(...), draws shown.title, and hands shown to ScreenView.
    • The bundle comes from I18nProvider loadLanguage={loadLanguage} in apps/console/src/main.tsx.
  • Server leg, in this repo. AppPlugin.loadTranslations hands each bundle's locale data to the i18n service whole. The file adapter deep-merges it with no group filter. Both translations routes answer getTranslations(locale) whole.

The flows container: authorWarn kept, authorHint made true (seat ruling B)

None of the three rows carries an authorWarn. The one involved sits on the flows container. That container also covers flows.label, which nothing reads yet (#20318). The bit has two readers:

  • @objectstack/lint's warn map;
  • the CLI i18n coverage gate authorWarnedTranslationGroups in packages/cli/src/utils/i18n-extract.ts, which holds back the whole flows.* demand while the bit is set.

So the bit stays. When #20318 lands the label reader, the group becomes wholly live, the bit drops, and the CLI demand comes back by design. Only the hint text changes.

- No shipped screen-flow runner reads this group yet — until the objectui half of #7646 lands, a `type: 'screen'` flow
  renders the strings authored on the flow (`config.title`, `fields[].label`, `fields[].placeholder`) in every locale.
+ Only part of this group is read. The console's screen-flow runner reads `screens`: each screen's `title` and each
  field's `label` / `placeholder` render in the active locale. The flow's own `label` is read by nothing yet, so a
  translated flow label is stored and never shown, and the flow keeps the label authored on it in every locale.

The new hint carries no tracker number, because it is text an author reads. #20318 is cited in the container note instead, in a dated sentence appended to it. Measured with the built lint at the head: a zh-CN bundle authoring flows.lead_conversion.screens.s1.title still draws exactly one finding. Its rule is liveness-planned-property, its message is unchanged ("sets flows but this translation property is planned — …"), and its hint is the new text above. authorWarnedProperties('translation') is still ["flows"]. An action authoring onSuccess draws 0 findings, before and after.

Prose restated (comments and ledger text only)

Comment-only, proven. Each of the three src files was transpiled with typescript 6.0.3 transpileModule (removeComments: true, ES2022 / ESNext) at the merge base 4e8d48d9 and at the head 080df02a. At 4e8d48d9 these three files equal origin/main 10ea9eb2 (git diff --quiet exit 0). The output hashes (sha256, first 16 hex characters):

file raw base → head stripped base stripped head control (a one-token code mutation of head)
ui/action.zod.ts b2e1bac7… → 507f9472… (differ) 458e751a64bf8ce4 458e751a64bf8ce4 IDENTICAL 'self'→'selg' gives b56bf8b4…, which the instrument sees
system/translation.zod.ts 9a0c42ff… → fdf2c86e… (differ) a9fe3aa64c3a71e2 a9fe3aa64c3a71e2 IDENTICAL an inserted const gives 6844d95c…, which it sees
system/i18n-resolver.ts 237136a3… → 0b3352a5… (differ) 5680cc3e05cebe9a 5680cc3e05cebe9a IDENTICAL an inserted const gives 3029a546…, which it sees

Pins, and whether they moved

They did not move. Readings at 080df02a:

  • pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/lint-liveness-properties.test.ts src/component-field-specs-liveness.test.ts: exit 0, Tests 91 passed (91), the same count as round one. This includes authorWarnedProperties('translation').has('flows') === true.
  • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 test/i18n-flow-liveness-gate.test.ts test/i18n-flow-screen-coverage.test.ts: exit 0, Tests 29 passed (29), the same count. This includes the real-ledger equality authorWarnedTranslationGroups() == ['flows'] and the gated flows.* demand.

Verification (every reading at HEAD 080df02a, after merging origin/main 10ea9eb2)

The merge ran through bash scripts/pm/os-regen-merge.sh as merge commit 4e8d48d9. state-counts.md was kept on the branch side and then re-verified by the gate. The spec, lint, platform-objects and objectql closures were rebuilt with pnpm exec turbo run build --filter=@objectstack/lint --filter=@objectstack/platform-objects --filter=@objectstack/objectql --concurrency=2 under the verify lock (15 tasks successful).

  • check:liveness: exit 0. action 49 classified (live 46, dead 3), translation 24 classified (live 23, planned 1), and ✓ packages/spec/liveness/state-counts.md is current. Evidence paths: 617 local, all resolved; 223 foreign. Symbol anchors: 764 of 764. Container coverage: 109 / 564. At base 4e0f72e8 it read action 44 live / 2 planned and translation 22 / 2.
  • check:objectui-pin-citations: exit 0, 47 asserting objectui pin citation(s) match .objectui-sha (f8a9d0fb0), 53 historical. Round one read 50 historical; the three new ones are the .objectui-sha pin f8a9d0fb sentences above, in the historical form. Sources: 1653.
  • Spec package. --project local: exit 0, Test Files 553 passed (553), Tests 16326 passed | 1 todo. --project repo: exit 0, Test Files 33 passed (33), Tests 604 passed (604). The repo project includes the docblock pins that read action.zod.ts as text, such as action-requires-confirmation-docblock.pin.test.ts.
  • Spec typecheck. pnpm --filter @objectstack/spec run typecheck: exit 0.
  • Other suites that read the edited files as text. @objectstack/platform-objects src/apps/translations/action-body-panel-echo-decisions.test.ts: exit 0, 22 passed. @objectstack/sdui-parser src/__tests__/dashboard-widget-options.test.ts: exit 0, 21 passed.
  • Derived gate families. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, derived from the 8 actual changed paths at 080df02a, gave 83 commands, identical to the derivation at f6e3558d. 81 exited 0. Reconciled: ✓ dispatch-gates --ran: 83 derived famil(ies) accounted for — 81 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3). check:generated, check:api-surface, check:docs, check:authorable-surface and check-spec-docblock-symbol-anchors each exited 0.
  • Roster gates whose roster lives in a changed directory (check-changeset-fixed, check:meta-url-spelling, check:spec-changes, check:authz-resolver, check:error-code-casing and check:filter-alias-parity): each exit 0.
  • NOT MEASURED: pnpm check:dual-build-cjs-loads. Exit 3, PREREQUISITE NOT MET. It needs every publishable package built. This is a declared narrowing; CI builds the workspace and runs it.
  • NOT MEASURED: pnpm check:i18n-walk-parity. Exit 3, PREREQUISITE NOT MET. It needs @objectstack/cli built, a closure of about 45 packages. It is derived here only because i18n-resolver.ts changed, and that change is comment-only: the stripped transpile above is identical. This is a declared narrowing; CI builds and runs it.
  • ESLint, narrowed and proven. The config's population is files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'], and 3 of the 8 changed paths are in it: the three src files. pnpm exec eslint --no-inline-config --format json over all 8 paths gives exit 0, 8 files, 0 errors and 5 warnings. The 3 src files have 0 errors and 0 warnings. Each of the 5 warnings is File ignored because no matching configuration was supplied, on the JSON, markdown and changeset paths. Invariance: the config enables no type-aware linting (no parserOptions.project), so this diff cannot move the verdict on any untouched file.

Acceptance notes

  • Two more sentences are made false by these flips. Both sit outside the seat's bounded list, so they are left for the seat. They are routed in the dispatch report:
    • packages/spec/src/system/metadata-form-zod-reconciliation.test.ts, the action onSuccess omit entry. Its why reads "declared, not enforced yet — both of its children (navigate, openIn) carry the liveness verdict planned: no console consumer reads the block yet. No offer until it is enforced; whether to offer it then is a ruling for the enforcement, not for this gate". The key is now enforced. So this is no longer only text: whether the Studio action form should offer onSuccess is the ruling the entry defers.
    • packages/cli/src/utils/i18n-extract.ts authorWarnedTranslationGroups docblock ("no shipped runner reads the group, so a translated wizard string really is stored and never shown"), and the matching header prose in packages/cli/test/i18n-flow-liveness-gate.test.ts. The gating these describe is unchanged. Only the reason given ("no shipped runner reads the group") is now half-true.
  • The objectui main control 256b4c9e is this container's objectui origin/main ref (a read-only checkout, not re-fetched). That checkout is shallow, so pin → main ancestry is not asserted.

Changeset

.changeset/20296-understated-planned-rows.md, @objectstack/spec: patch, updated for the hint and prose corrections. liveness/ and src/**/*.zod.ts are in packages/spec/package.json files[], and @objectstack/lint reads the shipped ledgers at runtime, so this diff publishes. That is why this PR carries no skip-changeset.

Deviations from the claim's file surface

  • The amended surface is from seat comment 5860372033: the container authorHint, the README and the three comment sites. Two edits go beyond it, and both are text inside translation.json, which the claim already names:
    • a dated sentence appended to the flows container note, recording the flip and why the bit stays;
    • the flows.screens row's own closing sentence, which named the pre-ruling state.
  • undrilled-containers.baseline.json is untouched, because the gate named no container row.

Generated by Claude Code

…ws.screens live

objectui reads all three at the .objectui-sha pin f8a9d0fb: ActionRunner's
handlePostExecution -> readOnSuccessNavigation -> navigateOnSuccess for the
onSuccess pair, and FlowRunner's localizeScreen for flows.<flow>.screens.
Each row now cites its mounted reader by file and symbol, names the
producer leg, and records why it was planned and what flipped it.
translation.flows.label and the flows container's authorWarn are unchanged.
state-counts.md regenerated.

Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 3 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/spec/liveness/README.md, packages/spec/liveness/action.json, packages/spec/liveness/state-counts.md, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/releases/v17/17-1.mdx (via newTab (literal, a string literal in a comment in actionObject))

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
  • 5 changed file(s) yielded no anchor (packages/spec/liveness/README.md, packages/spec/liveness/action.json, packages/spec/liveness/state-counts.md, …) — pages documenting those are invisible to this run
  • 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 — 136 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 dfd8e398aacfa74b8f401a9186814fec093cf010 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from bf38320cf50ec95bd37c10e0250af8bb2a6d01cc — the merge of head 080df02a51b2ef5c3d2b91f78672d9f915dde622 into base dfd8e398aacfa74b8f401a9186814fec093cf010, 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 bf38320cf50ec95bd37c10e0250af8bb2a6d01cc && git checkout bf38320cf50ec95bd37c10e0250af8bb2a6d01cc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dfd8e398aacfa74b8f401a9186814fec093cf010 080df02a51b2ef5c3d2b91f78672d9f915dde622 && git checkout -B drift-repro dfd8e398aacfa74b8f401a9186814fec093cf010 && git merge --no-ff 080df02a51b2ef5c3d2b91f78672d9f915dde622

node scripts/docs-audit/affected-docs.mjs --json dfd8e398aacfa74b8f401a9186814fec093cf010

⚠️ 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 dfd8e398aacfa74b8f401a9186814fec093cf010 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…ade false

Text only: the flows container's authorHint now says FlowRunner reads
`screens` and only the flow `label` is unread (authorWarn kept); the
liveness README translation cell, the onSuccess JSDoc in action.zod.ts,
the flows JSDoc in translation.zod.ts and the translateFlow docblock in
i18n-resolver.ts name the landed readers. No schema, describe or
accept-set change.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 080df02a51b2ef5c3d2b91f78672d9f915dde622

Local-runs: check:liveness (head, plus two controls) · gen:liveness-counts · check:objectui-pin-citations (head, plus a base-src control) · check-liveness --producer-gap · check-spec-docblock-symbol-anchors · check:doc-authoring · check:issue-citations · check-changeset-no-major · check-empty-changeset · check-adr-0087-registration · check-changeset-fixed · comment-stripped transpileModule diff (3 files, 2 controls each) · structural walk of both ledgers base→head · lint warn-map derivation over both ledgers · the lint and CLI flow-liveness vitest pins (see ③ for how they were measured)

① Derived judgments

Reviewed at head 080df02a51b2ef5c3d2b91f78672d9f915dde622 (PR #20328, card #20296) against base 10ea9eb2ed29269fc98fa8bdee6e265906b2caad, which is also the merge-base, so the PR diff is exactly git diff base head: 8 files, +57/−27. .objectui-sha is f8a9d0fb0596f4521076628e2bbfe27e6ce67d52 at both base and head. Every objectui reading was taken with git show / git grep against that pin in the read-only clone at /home/user/objectui (its HEAD 5c61e524 on an unrelated branch, never switched, unchanged after every gate run), and controlled at objectui 256b4c9ef518dd124760825072c039c687d69113 (the PR's control) and at that clone's origin/main 972c1685ec06fa538f29c078f8c3c475089e7856. The clone is shallow, so pin → main ancestry is not asserted. Gates ran in a detached worktree at the head under the scratchpad (pnpm install --frozen-lockfile --offline, exit 0); heavy build and test went through scripts/pm/os-verify-lock.sh; the worktree was removed afterwards.

1. Each flipped row names a real, MOUNTED reader at the pin f8a9d0fb — RIGHT.

Mount chain, closed by hand at the pin, once for both onSuccess rows:

  • apps/console/src/main.tsx:155 renders the I18nProvider element with loadLanguage={loadLanguage} around the App element; apps/console/src/App.tsx:194–421 renders the ConsoleShell element around the app content.
  • packages/app-shell/src/console/ConsoleShell.tsx:86 calls useConsoleActionRuntime({ dataSource }) and at :88–105 mounts the ActionProvider element with onNavigate={runtime.actionProviderProps.onNavigate} and a handlers prop, with {runtime.dialogs} inside it.
  • packages/app-shell/src/hooks/useConsoleActionRuntime.tsx:129 is the hook; :338–344 defines navigateHandler (options?.external || options?.newTab → window.open(url, '_blank', 'noopener,noreferrer'), otherwise react-router navigate(url) from useNavigate() at :131); :723 passes it as onNavigate, and :727 registers handlers: { api, flow, script: serverActionHandler, modal }.
  • packages/react/src/context/ActionContext.tsx:113 builds new ActionRunner(normalizedContext) and :117 calls r.setNavigationHandler(onNavigate) (the same call in packages/react/src/hooks/useActionRunner.ts).
  • packages/core/src/actions/ActionRunner.ts: execute calls handlePostExecution(action, result) on every path — the update-operation path (:1214), the registered custom handler path (:1222, which is where the console's api and script handlers return), the built-in path (:1246) and the catch (:1250). handlePostExecution (:1302) at :1411–1416 does if (result.success && action.onSuccess) { const navigation = readOnSuccessNavigation(action.onSuccess); if (navigation) this.navigateOnSuccess(navigation, action, result); }. readOnSuccessNavigation (:2275, exported) accepts only an object whose navigate is a non-empty string. navigateOnSuccess (:1447–1483) interpolates block.navigate through interpolateTarget(block.navigate, action, readActionPayload(result.data)) (the ${result.*} scope is passed by this one caller, beside ${param.*} / ${ctx.*}), refuses a URL that is not http://, https://, / or ./ (isValidUrl), sets const newTab = block.openIn === 'newTab' with no fallback of its own, and calls this.navigationHandler(url, { external, newTab }), falling back to window.open / result.redirect only when no handler is registered.
  • Producer leg: packages/components/src/renderers/action/action-button.tsx:270 forwards onSuccess: schema.onSuccess into the def it hands to execute (:273); action-icon.tsx carries the same onSuccess: schema.onSuccess (1 hit); action-menu.tsx:284 and action-group.tsx:286 forward onSuccess: action.onSuccess per rendered item. packages/app-shell/src/views/DeclaredActionsBar.tsx:257 destructures const { params: rawParams, ...rest } = action, builds dispatch = { ...localizeActionTexts(objectName, rest), objectName, params } (:286–296; the localizer itself spreads ...action, packages/react/src/hooks/useActionTextLocalizer.ts:29, so onSuccess rides through) and calls execute(dispatch) (:322); DeclaredActionButton is defined at :114 and rendered at :502. DeclaredActionsBar is mounted by RecordDetailView.tsx:2500 and apps/console/src/pages/system/ApprovalsInboxPage.tsx:2488/2535; type: 'action:bar' is rendered by ObjectView.tsx:3106, InterfaceListPage.tsx:566 and environment/EnvironmentListToolbar.tsx:125/264.
  • The script handler's file at the pin is packages/app-shell/src/utils/consoleServerAction.ts; at :240 if (readOnSuccessNavigation(action.onSuccess)) it defers to the declared block instead of also following a handler-returned redirectUrl, which is what the navigate row's note says. The spec refuses onSuccess together with opensInNewTab: true (packages/spec/src/ui/action.zod.ts:1960).

action.onSuccess.navigate — the row's evidence (ActionRunner.ts#handlePostExecution, #navigateOnSuccess, useConsoleActionRuntime.tsx#useConsoleActionRuntime) and producer (the four renderers plus DeclaredActionsBar.tsx#DeclaredActionButton) all resolve to the symbols and reads above. RIGHT.

action.onSuccess.openIn — evidence ActionRunner.ts#navigateOnSuccess (block.openIn === 'newTab', 1 hit) and useConsoleActionRuntime (options?.newTab → new window, otherwise navigate); producer packages/spec/src/ui/action.zod.ts#ActionSchema (openIn: z.enum(['self', 'newTab'], …).default('self') at :1649–1655, materialized) plus the renderer forward. RIGHT.

translation.flows.screens — mount chain, closed by hand at the pin:

  • packages/app-shell/src/views/FlowRunner.tsx imports FLOW_SCREEN_FIELD_COPY_KEYS and resolveFlowScreenTitle from the spec (:138–139). activeFlowsBundle (:185) reads i18n.getResourceBundle(language, 'translation'), walks its namespaces and returns { [language]: { flows } } when flows[flowName] exists. localizeScreen (:219) computes the title through resolveFlowScreenTitle(bundle, flowName, screen, { locale: language }) (:226) and fieldCopy = bundle[language]?.flows?.[flowName]?.screens?.[screen.nodeId]?.fields (:227), overlaying each field over FLOW_SCREEN_FIELD_COPY_KEYS (label, placeholder; spec i18n-resolver.ts:3378), each key falling back to the authored value. FlowRunner (:294) computes shown = localizeScreen(screen, flowName, activeFlowsBundle(i18n, language, flowName), language) (:324), draws shown.title as the DialogTitle (:462) and hands screen={shown} to ScreenView (:495–496).
  • The FlowRunner element is mounted (non-test JSX, git grep at the pin) in exactly three places: useConsoleActionRuntime.tsx:798 inside the dialogs element (:734–818) that ConsoleShell renders, and that ObjectView.tsx:1506 and DeclaredActionsBar.tsx:552 render through their own useConsoleActionRuntime(…) calls; RecordDetailView.tsx:2628 (RecordDetailView is mounted by app-shell console/AppContent.tsx:1096, ObjectView.tsx:3326, ObjectDataPage.tsx:626, InterfaceListPage.tsx:588); and apps/console/src/pages/developer/FlowRunsPage.tsx:410, routed at apps/console/src/AppContent.tsx:169 (developer/flow-runs) and registered as developer:flow-runs (registerDeveloperComponents.tsx:49–54). The flow handler sets screenFlow = { flowName, runId, screen } when a run pauses at a screen node (useConsoleActionRuntime.tsx:612).
  • Bundle leg: apps/console/src/loadLanguage.ts:16–19 fetches /api/v1/i18n/translations/${lang} and runs transformSpecTranslations(translations); packages/i18n/src/utils/spec-translations.ts lists 'flows' among the recognised spec groups (:84) and its generic forward loop (:211–214) copies every top-level key it did not flatten — flows is not in PASSTHROUGH_TOP_LEVEL_KEYS (:122–128) and not objects, so it is copied verbatim — into appNs, returned as { app: appNs }; packages/i18n/src/provider.tsx:669–671 (I18nProvider, :565) adds the loaded payload with addResourceBundle(currentLang, 'translation', resources, true, true), which is the tree activeFlowsBundle walks. Server leg in this repo at the head: packages/runtime/src/app-plugin.ts#loadTranslations (:1989) hands each locale's data whole to i18nService.loadTranslations(locale, data) (:2131); packages/runtime/src/domains/i18n.ts#handleI18nRequest (:35) answers getTranslations(locale) whole (:76, :85). The local evidence packages/spec/src/system/i18n-resolver.ts#resolveFlowScreenTitle exists at :3458.

Per-read control (git show REF:PATH | grep -cF), pin / 256b4c9e / origin/main 972c1685: readOnSuccessNavigation(action.onSuccess) 1/1/1, private navigateOnSuccess( 1/1/1, block.navigate 2/2/2, block.openIn === 'newTab' 1/1/1, onSuccess: schema.onSuccess (action-button) 1/1/1, resolveFlowScreenTitle(bundle, flowName, screen 1/1/1, ?.screens?.[screen.nodeId]?.fields 1/1/1, function activeFlowsBundle 1/1/1, FlowRunner in useConsoleActionRuntime.tsx 4/4/4, useConsoleActionRuntime({ dataSource }) in ConsoleShell.tsx 1/1/1, ConsoleShell in App.tsx 4/4/4, 'flows', in spec-translations.ts 1/1/1, transformSpecTranslations(translations) 1/1/1, setNavigationHandler(onNavigate) in ActionContext.tsx and useActionRunner.ts 1/1/1 each. Every cited file exists at the pin. The history citations resolve: objectui#5493, #5934 and #5920 are closed issues with the titles the notes describe; objectui#5221 is cited by ActionRunner.ts:1395 itself at the pin as "the console half of objectstack#9566/#9474", its events and timeline resolve (labelled domain:ui, closed 2026-08-24, landed by objectui PR #5933 "read ActionSchema.onSuccess for post-success navigation"), though the issue body itself answers 404 to this token; objectstack #9566, #9474, #7646 and #11287 are closed; #20318 is open (domain:spec, priority:p3, area:i18n).

2. The flows container stays planned + authorWarn, its new authorHint is true, translation.flows.label stays planned — RIGHT.

Structural walk of translation.json base → head: the flows container changed only authorHint and note; status planned, authorWarn: true and verifiedAt 2026-08-11 are unchanged. The flows.label row is byte-identical to base (status: planned, no authorWarn). The new hint carries no tracker number (regex #\d+ over the string: none); #20318 is cited in the appended note sentence. Its two claims were checked at the pin: "the console's screen-flow runner reads screens: each screen's title and each field's label / placeholder render in the active locale" is the localizeScreen → DialogTitle / ScreenView path above; "the flow's own label is read by nothing yet" — a census over packages/*/src and apps/*/src at the pin for any flows…label read finds none (the only .flows reads are FlowRunner :194 / :227 and two unrelated flow-list payloads in packagedFlows.ts and StudioDesignSurface.tsx), and in this repo the only reader of flows.FLOW.label is the unexported lookupFlowLabel (i18n-resolver.ts:3382), reachable only through translateFlow (:3536), which is not in the translateMetadataDocument dispatch table (view, action, object, app, dashboard, dataset, page, …; no flow) and has no caller outside its own test. Lint's shouldWarn (packages/lint/src/lint-liveness-properties.ts:157–159) warns on authorWarn === true || status === 'experimental' only, and its hint is entry.authorHint ?? entry.note ?? defaultHint (:390), so the bit's readers see the new text and the same warn set: a walk of both ledgers for authorWarn === true gives translation: ["flows"], action: [] at base and at head alike.

3. check:liveness at the head exits 0, state-counts.md is current, no row lost a reader or became live without one — RIGHT.

pnpm --filter @objectstack/spec run check:liveness at the head: exit 0; action 49 classified (live 46, dead 3); translation 24 classified (live 23, planned 1); evidence paths: 617 repo-local … 617 resolved … 223 attributed to another repo; symbol anchors: 764 … 764; container coverage: 109 … 564; ✓ packages/spec/liveness/state-counts.md is current — the same 40 row(s). gen:liveness-counts rewrote state-counts.md byte-identically (git diff empty). Control 1, base action.json / translation.json / state-counts.md swapped in: exit 0, translation 24 (live 22, planned 2), 616/616 local, 218 foreign, 760/760 anchors. Control 2, base ledgers with the head state-counts.md: exit 1, state-counts.md is STALE, so the freshness check is lit. --producer-gap: 96 live entries cite a producer (93 before this PR per the round-one report: the three flipped rows). CI Spec property liveness at the head (job 108723199441): success, with the same action 49 classified (live 46, dead 3), translation 24 classified (live 23, planned 1), 617/617, 764/764, 96 producer-citing and state-counts.md is current lines.

Structural walk base → head: action.json 42 leaf rows, live 37 / dead 3 / planned 2 → live 39 / dead 3; exactly two rows changed, onSuccess.navigate and onSuccess.openIn, each planned → live with evidence and producer added, verifiedAt 2026-08-18 → 2026-09-27, evidenceScope cross-repo kept, authorWarn absent before and after. translation.json 17 leaf rows, live 15 / planned 2 → live 16 / planned 1; flows.screens planned → live with evidence, producer, evidenceScope: cross-repo added and verifiedAt 2026-08-11 → 2026-09-27; the flows container changed text only. No row lost an evidence field, no live row became anything else, no authorWarn changed, no row was added or removed, undrilled-containers.baseline.json is untouched and the container coverage figure did not move.

pnpm --filter @objectstack/spec run check:objectui-pin-citations at the head, with ../objectui a symlink to the read-only clone: exit 0, 47 asserting objectui pin citation(s) match .objectui-sha (f8a9d0fb0), 53 historical citation(s) recorded and not checked, across 1653 spec source(s), 7 anchor content assertion(s) verified against objectui at f8a9d0fb0. Control with the three base src files restored: 47 asserting / 50 historical, and the diff adds exactly three sentences of the historical form (the ".objectui-sha pin f8a9d0fb" prose in the three JSDoc blocks), so the +3 is accounted for and no asserting citation was added or moved.

4. The three src files change only in comments, and the new prose is true at the pin — RIGHT.

typescript 6.0.3 transpileModule with removeComments: true, ES2022 / ESNext, over base and head: ui/action.zod.ts raw b2e1bac74f25c62c → 507f9472d8594d49 (differ), stripped 458e751a64bf8ce4 = 458e751a64bf8ce4 (44830 bytes both); system/translation.zod.ts raw 9a0c42ff0deefd8b → fdf2c86eb835e160, stripped a9fe3aa64c3a71e2 = a9fe3aa64c3a71e2 (43247 bytes); system/i18n-resolver.ts raw 237136a3535d48ac → 0b3352a545656c2d, stripped 5680cc3e05cebe9a = 5680cc3e05cebe9a (55520 bytes). Controls the instrument sees, per file: an appended export const changes every stripped hash (1df4dfb2…, bf6fbf3d…, 2d7def94…); a one-token code mutation changes it too (translation.zod.ts b8c4425e…, i18n-resolver.ts e59daf5b…, action.zod.ts 'self' → 'selg' on the code line at :1655 gives 5b572270fa883170 — see the control note in ③).

Prose, checked at the pin: the onSuccess JSDoc names ActionRunner (handlePostExecution → readOnSuccessNavigation → navigateOnSuccess), the three interpolation scopes, the handoff to the console's router and a new tab only on openIn: 'newTab' — each is a line quoted in ①. The flows JSDoc says FlowRunner reads screens (title, label, placeholder), the flow's own label is read by nothing, screens is live and label planned — true per ② and the ledger at the head. The translateFlow docblock says the #11287 runner card took the client side (objectui#5920, "the runner half of objectstack#11287 (client-side pick)", closed), that FlowRunner overlays flows.FLOW.screens at the pin, that this function stays unregistered (not in the dispatch table, no callers) and that flows.FLOW.label has no reader on either side yet — true per ②. check-spec-docblock-symbol-anchors at the head: exit 0, 3768 anchors resolve, 3 cross-repo. check:doc-authoring exit 0.

5. The lint and CLI flow-liveness pins do not move; authorWarnedProperties('translation') is still ["flows"] — RIGHT.

Static half: the warn set is derived from the shipped ledgers alone (authorWarnedProperties = keys of loadWarnMap(dir, type), lint-liveness-properties.ts:369–373; authorWarnedTranslationGroups() = its top-level members, packages/cli/src/utils/i18n-extract.ts:1082–1085, consumed at :1455 to hold back the flows.* demand), and the walk in ② gives the same ["flows"] for translation and [] for action at base and head, so the assertions authorWarnedProperties('translation').has('flows') (packages/lint/src/lint-liveness-properties.test.ts:1316) and [...authorWarnedTranslationGroups()] equals ['flows'] (packages/cli/test/i18n-flow-liveness-gate.test.ts:123) cannot change on this diff. Dynamic half, run at the head under scripts/pm/os-verify-lock.sh (VERDICT command-exit 0, held 60s, waited 285s on the second attempt; the first attempt exited 99 after its 540s budget), after OS_SKIP_DTS=1 pnpm exec turbo run build --filter=@objectstack/lint --filter=@objectstack/platform-objects --filter=@objectstack/metadata-core --concurrency=2 (8 tasks successful: types, spec, core, formula, sdui-parser, metadata-core, lint, platform-objects): pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/lint-liveness-properties.test.ts src/component-field-specs-liveness.test.ts exit 0, Test Files 2 passed (2), Tests 91 passed (91); pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 test/i18n-flow-liveness-gate.test.ts test/i18n-flow-screen-coverage.test.ts exit 0, Test Files 2 passed (2), Tests 29 passed (29). Both counts equal the dev's, so no pin moved. Built-lint probe from the lint source over the built spec: authorWarnedProperties('translation') is ["flows"] and authorWarnedProperties('action') is []; a stack whose zh-CN bundle authors flows.lead_conversion.screens.s1.title and a field label draws exactly one finding, rule liveness-planned-property, message unchanged ("sets flows but this translation property is planned — declared, and a consumer is being built against it (not read YET)."), hint byte-equal to the head translation.json flows.authorHint; the same stack against a copy of the ledger directory with the base action.json / translation.json swapped in draws the same one finding with the old hint ("No shipped screen-flow runner reads this group yet …"). An api action authoring onSuccess: { navigate, openIn: 'newTab' } draws 0 findings against either ledger set.

6. Changeset / semver — RIGHT. See ②.

② Semver level

patch on @objectstack/spec (.changeset/20296-understated-planned-rows.md) is right, on the #20302 precedent. packages/spec/package.json files[] is ["dist","json-schema","liveness","prompts","llms.txt","README.md","src/**/*.zod.ts","CHANGELOG.md","api-surface","spec-changes.json"], so liveness/action.json, liveness/translation.json, liveness/state-counts.md, liveness/README.md and the two *.zod.ts comment changes publish; @objectstack/lint resolves @objectstack/spec/package.json and reads the shipped liveness/ directory at runtime (resolveLivenessDir, lint-liveness-properties.ts:80–89), so the ledger is consumed data and skip-changeset would have been false. Nothing in the parsed surface, .describe() text, refusal text or accept set moves (the comment-stripped transpile is identical for all three source files), so neither minor nor major is owed. Clause-②: no is on the PR body's second line and is consistent. Every statement in the changeset was checked: files[] includes liveness (true); @objectstack/lint warns on a planned row only when it sets authorWarn (true — shouldWarn), none of the three flipped rows does (true), so the warning set does not change (true — same warn map) and one hint text changes (true); the onSuccess bullet's interpolation scopes, URL refusal ("neither http(s) nor relative": isValidUrl allows http://, https://, /, ./), new tab only on openIn: 'newTab', renderer / declared-actions-bar forward and router wiring (all true per ①); the flows.screens bullet's heading and label / placeholder reads with per-key fallback and the route → loader path (true); the flows.label bullet (true per ②); the prose bullet (true per ④); the counts action 46/0 (was 44/2), translation 23/1 (was 22/2) (true, measured). check-changeset-no-major --base exit 0; check-empty-changeset --base exit 0 (one declaring changeset added, none from the merge base modified); check-adr-0087-registration --base exit 0 (no declared-breaking changeset); check-changeset-fixed exit 0. No model identifier appears anywhere in the diff (0 hits).

③ Boundary flags

  • Cross-repo evidence: all three flipped rows cite objectui @f8a9d0fb, the .objectui-sha pin at the head, with evidenceScope: cross-repo and verifiedAt: 2026-09-27; each read answers identically at objectui 256b4c9e and at the clone's origin/main 972c1685, so the next pin bump does not strand them. Pin → main ancestry is not asserted (shallow clone), as the PR says.
  • Carrier: i18n: the flow launcher and runner header read translation.flows.<flow>.label (1 key) #20318 (open, domain:spec, p3) is named in the flows container note and the flows.screens row for the unread label half; the ENFORCE / RETIRE word for flows.label stays there.
  • The flows container's authorWarn is kept on purpose (seat ruling 5860372033, answer B): dropping it would switch on the CLI's whole-group flows.* demand for the unread flows.FLOW.label (i18n-extract.ts:1455). The bit's two readers see only a new hint.
  • Wording, non-blocking: the PR body's control table says the four renderers forward "onSuccess: schema.onSuccess"; measured, action-button.tsx and action-icon.tsx spell it schema.onSuccess and action-menu.tsx:284 / action-group.tsx:286 spell it action.onSuccess per item. The ledger rows say "the same forward", which is the true statement. The script handler file sits at packages/app-shell/src/utils/consoleServerAction.ts at the pin; the ledger note names it without a path.
  • History citation, non-blocking: objectui#5221 answers 404 on the issue endpoint to this token while its events and timeline resolve and ActionRunner.ts:1395 cites it at the pin; the landing PR is objectui#5933. It is a history pointer in a note, not an evidence or producer field, so no gate reads it.
  • Control note on ④: my first one-token control for action.zod.ts replaced the first .default('self') in the file, which is inside the JSDoc at :1593, so the instrument correctly saw nothing; the rerun on the code line :1655 is the control recorded above, and the appended-export const control was seen in all three files on the first run.
  • Pre-existing, not this PR's: 16 action.json leaf rows (label, icon, component, variant, confirmText, successMessage, errorMessage, refreshAfter, resultDialog, visible, recordIdParam, recordIdField, mode, opensInNewTab, newTabUrl, aria) are live with a note and no evidence field, identical at base and head; the gate has nothing to resolve on them and does not fail. Noted so the count of "live without evidence" is not misread as this PR's doing.
  • Deviations from the seat-amended claim surface, both text inside a claimed file and both measured true: the dated sentence appended to the flows container note, and the flows.screens row's closing sentence. undrilled-containers.baseline.json untouched; the gate named no container row.
  • Two further sentences the flips make false sit outside the bounded list — the onSuccess omit reason in packages/spec/src/system/metadata-form-zod-reconciliation.test.ts and the authorWarnedTranslationGroups docblock in packages/cli/src/utils/i18n-extract.ts (plus the header of packages/cli/test/i18n-flow-liveness-gate.test.ts). The seat's ACCEPT (5860916647) routes them to a follow-up card; they do not block this PR.
  • Not measured locally: pnpm check:dual-build-cjs-loads and pnpm check:i18n-walk-parity (whole-workspace and CLI-closure builds; both are inside the required Lint & Repo Gates context, success at the head); the spec typecheck (comment-only change; TypeScript Type Check aggregator success at the head with all four lanes ok); the spec --project local and --project repo suites (unchanged source, comment-only; Test Core and its six shards success at the head). Everything lock-bound that this review needed — the lint and CLI pin suites and the build under them — was measured locally after the lock granted on the second attempt, so nothing in ①–⑥ rests on a CI reading alone; the CI figures quoted are cross-checks.
  • CI at the head at review time: 35 distinct check names, 30 success and 5 skipped (Auto Label, Check PR Size, Packed-tarball smoke (opt-in), Console Pin Gate, Build Docs — path-filtered or opt-in), 0 failed, 0 in progress; all seven required contexts (Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard) are success. The PR is a draft with no auto-merge, labelled needs:contract-review; no governed surface is in its file list.

Implemented-by: claude/issue-20296-understated-planned-rows
Reviewed-by: session_01Rjy9MeetSfq34PKn81CRiN

VERDICT: PASS

Blocking items: none.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 00:23
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 826f327 Sep 28, 2026
51 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20296-understated-planned-rows branch September 28, 2026 00:44
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…bjectstack-ai#20383)

Fixes objectstack-ai#20339
Clause-②: no

## What this changes

Comment text only, in `packages/cli`. Four sentences there said nothing
reads the `flows` translation group, and two named an objectui runner's
landing as the flip trigger. Since PR objectstack-ai#20328, the shipped liveness
ledger's `flows` row (`packages/spec/liveness/translation.json`) says
otherwise: `children.screens` is `live`, because the console's
screen-flow runner reads it, and only `children.label` is still read by
nothing (objectstack-ai#20318). The container keeps `planned` + `authorWarn: true`,
and that warn bit is group-level. So the gating these comments describe
is unchanged and still correct. Only the reason they gave was stale.

| where | before | after |
|---|---|---|
| `packages/cli/src/utils/i18n-extract.ts`, the
`authorWarnedTranslationGroups` docblock (site 2) | "no shipped runner
reads the group, so a translated wizard string really is stored and
never shown" | "Only part of the group is read: the console's
screen-flow runner reads `screens`, but the flow's own `label` is read
by nothing yet (objectstack-ai#20318), so a translated flow label really is stored and
never shown. The warn is group-level, so it still covers the whole
group." |
| the same docblock, its "Shape" paragraph | "the day the objectui
screen-flow runner lands and the row flips to `live`" | "the day the row
flips to `live` (dropping its `authorWarn`; for `flows` that waits on
objectstack-ai#20318)" |
| `packages/cli/test/i18n-flow-liveness-gate.test.ts`, the header's
ledger sentence (site 3) | "no shipped runner reads the group" | "only
part of the group is read: the console's screen-flow runner reads
`screens`, but the flow's own `label` is read by nothing yet (objectstack-ai#20318)" |
| the same header, its warn-side paragraph (site 3) | "Nothing reads the
group, so a translated wizard string really is stored and never shown" |
"The flow's own `label` is read by nothing yet (objectstack-ai#20318), so a translated
flow label really is stored and never shown [...] The warn is
group-level, so it still covers the whole group." |
| the same file, the comment inside the cell `never lets both rules
speak about the same keys` | "nothing reads the group, so this copy is
stored and never shown" | "the flow's own `label` is read by nothing
yet, so its translated copy is stored and never shown" |
| `packages/cli/test/i18n-flow-screen-coverage.test.ts`, the header's
flip-trigger sentence (patch round 1) | "it turns itself back on the day
an objectui screen-flow runner lands and the row flips" | "it turns
itself back on the day the row flips to `live` (dropping its
`authorWarn`; for `flows` that waits on objectstack-ai#20318)" |

The "Shape" sentence is changed because it sits in the same docblock and
named the wrong trigger. The screen-flow runner has already shipped
(objectui at the `.objectui-sha` pin `f8a9d0fb`), and the row did not
flip. The ledger's `screens` note says the container bit drops when
objectstack-ai#20318 lands. The in-cell comment is changed because the claim's file
surface names every `packages/cli` sentence that gives this reason. Only
its comment lines move, and the cell's code is unchanged (proof below).
The header of `i18n-flow-screen-coverage.test.ts` named the same wrong
trigger under the same rule, so patch round 1 gives it the Shape
clause's wording. `packages/cli` now states one trigger. The file is the
one the gate header points readers to, and the claim names it.

## Evidence

- **Premise.** Measured on base `5a6267f48`, and unchanged through the
merge of `862b6ce86`. The `flows` row is `status: planned` and
`authorWarn: true`; `children.label` is `planned`; `children.screens` is
`live` (`verifiedAt: 2026-09-27`). Its `authorHint` reads: "Only part of
this group is read. The console's screen-flow runner reads `screens`
[...] The flow's own `label` is read by nothing yet, so a translated
flow label is stored and never shown". The new sentences follow that
row.
- **Comment-only proof.** Instrument: the TypeScript parser, printing
both files with `removeComments: true`. The print at base and the print
at HEAD are identical: 48261 of 48261 characters for `i18n-extract.ts`,
5544 of 5544 for the gate test, and 18534 of 18534 for
`i18n-flow-screen-coverage.test.ts` (patch round 1; measured against
both `bd7b473e2` and merge base `c577e6663`). Control legs: the same
instrument detects a one-identifier rename of the exported function, and
a one-identifier rename of the screen-coverage file's mocked
`authorWarnedProperties`. No assertion, fixture or behaviour moves.
- **Published surface.** After `pnpm --filter @objectstack/cli build`,
both `dist/utils/i18n-extract.d.ts` and `dist/utils/i18n-extract.js`
carry the new sentence (1 hit each) and not the old one (0 hits each).
Positive control: the `authorWarnedTranslationGroups` export line gets 1
hit in each. `files: ["dist"]` ships both files, so this PR carries an
`@objectstack/cli` `patch` changeset.

## Local verification (HEAD `bd7b473e2`; patch round 1 at `dbff132d7`)

- **Patch round 1**, at `dbff132d7`, after merging `origin/main`
`c577e6663`:
  - `pnpm lint` exit 0;
- `node scripts/check-issue-citations.mjs --base origin/main` exit 0: 2
added citations judged, both live;
- `vitest run --project unit --maxWorkers=2` over
`i18n-flow-screen-coverage.test.ts` and
`i18n-flow-liveness-gate.test.ts`: 2 files and 29 tests passed;
- the 63 derived gates, re-run on this head: 63 run, 0 NOT-MEASURED, 0
UNRUN.

  The bullets below are the first round, at `bd7b473e2`.
- `pnpm --workspace-concurrency=2 --filter '@objectstack/cli^...' build`
exit 0. `pnpm --filter @objectstack/cli build` exit 0.
- Targeted unit run (`vitest run --project unit --maxWorkers=2`) over
the 18 test files that import `i18n-extract`: 18 files and 250 tests
passed. The gate file on its own, verbose: 8 of 8 cells passed.
- The full `unit` layer of `@objectstack/cli`: 231 files and 3309 tests
passed. The `integration` layer is left to CI, because no spawn entry or
integration-tier file is touched.
- `pnpm --filter @objectstack/cli run typecheck` exit 0 (`tsc --noEmit`,
then `check:test-typecheck` OK).
- `pnpm --filter @objectstack/spec run check:liveness` exit 0.
- `pnpm lint` (the full `eslint . --no-inline-config`) exit 0, no
findings.
- `node scripts/check-issue-citations.mjs --base origin/main` exit 0. It
judged the 2 added citations (objectstack-ai#20318, objectstack-ai#20339), and both are live issue
numbers.
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` derived 63 commands, and all 63 ended at exit 0. The `--ran`
reconciliation found 63 run, 0 NOT-MEASURED and 0 UNRUN. Three first
attempts measured nothing and were re-run green after their
prerequisites were built:
- `check:dual-build-cjs-loads` (exit 3, PREREQUISITE NOT MET): 104 entry
points load.
- `check:i18n-coverage` (exit 3, PREREQUISITE NOT MET): 13 configs, none
new.
- `check:type-check-debt` (hit a 300 s per-command cap): 4 entries, none
above its record.
- The roster gates whose roster directory holds one of these paths all
exit 0: `check-changeset-fixed`, `check:authz-resolver`,
`check:error-code-casing`, `check:filter-alias-parity`. So does
`check:nul-bytes`.

## Acceptance notes

- **Site 1 is not in this PR.** Site 1 is
`packages/spec/src/system/metadata-form-zod-reconciliation.test.ts`, the
`why` of the `onSuccess` omit entry. Triage routed it to the spec lane.
It is still stale on `origin/main` `c577e6663`. objectstack-ai#20362 was a spec-lane
PR that touched that file after triage, and it did not carry the fix.
- **Other copies of the same stale wording, not edited here:**
- `packages/cli/CHANGELOG.md`, the same entry in
`packages/lint/CHANGELOG.md`, and `packages/spec/CHANGELOG.md`. These
are release-owned, and each sentence was true when it was released.
- The `flows` bullet in `content/docs/ui/translations.mdx`. This
customer-facing page still says "no shipped screen-flow runner reads the
group yet, so a wizard still renders the strings authored on the flow".
It is outside `packages/cli`, so it goes to the seat.
- **One sentence left as it is.** The test header's "(a) it
self-activates when the row flips to `live` with the objectui runner"
stays byte-identical, as the dispatch directs. It holds under objectstack-ai#20318's
ENFORCE proposal, whose reader surface is objectui `FlowRunner.tsx` plus
the launcher. objectstack-ai#20318 still waits for the maintainer's ENFORCE-or-RETIRE
decision.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants