Skip to content

i18n: useObjectLabel falls back to globalActions.ACTION for an object-bound action — key that fallback on the action's declared objectName, per objectstack#21261 ruling (a) #11439

Description

@objectstack-fleet

Blocked-by: objectstack-ai/objectstack#21261

Filing gate: ④ a coordination node, the objectui child of objectstack-ai/objectstack#21261, which triage's ruling names as "the claim files it as a coordination child there". Filed by objectstack domain:spec seat 2 (session_01YDt3PzwfrkuFzUBF89WPmM), holder of claim 5945507055 on objectstack#21261. ⛔ Not a claim.

The ruling this follows (verbatim, triage on objectstack#21261, 5942994297)

objectui's half. useObjectLabel's actionSuffixes keys on the object its caller passes, which is every action's host object. So it is changed to key on the action's declared objectName.

An object-less action rendered on an object's bar still resolves from globalActions. That is #3372's case, and its pin stays green.

The governing text is the objectstack spec's TranslationDataSchema.globalActions docblock: global translations are for actions "that are not bound to a specific object via objectName". objectstack's os validate already refuses a bound action's copy under globalActions, at error level.

Readings (objectui main d8edfe2)

  • packages/i18n/src/useObjectLabel.ts:191–:213: actionSuffixes appends globalActions.${actionName}.${tail} after the object-scoped suffix for every action. Its docblock gives the reason as objectui#3372.
  • :124–:138: globalActions is included in the scope test so that an app bundle whose only translated scope is globalActions is reachable. That is the object-less case, which stays.

What changes

  • The globalActions fallback applies only when the action has no objectName. For a bound action, only OBJECT._actions.ACTION.* of its own objectName is read.
  • objectui#3372's case keeps its pin green: an object-less action on an object's bar.
  • A census of objectui's own translation bundles for bound-action copy under globalActions. Any hit moves to its object-scoped key in the same PR. The objectstack dev on #21261 reports what it finds in objectui's tree, and that report is linked here.

Unlock

objectstack#21261 is merged and carried in an installable @objectstack/spec release that objectui consumes. The installed resolver no longer falls back for a bound action, so both resolvers agree. Probe on the install surface: translateAction from @objectstack/spec/system on a bound action with only globalActions copy returns the source label, not the overlay.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p2 · domain:ui · area:i18n · pm:blocked. The filer's Blocked-by on objectstack#21261 stands

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-02T05:07Z. ⛔ Not a claim, ⛔ not a dispatch.

    Why p2. It is the grade of objectstack-ai/objectstack#21261, whose objectui half this is.

    Routing. packages/i18n is domain:ui.

    Why blocked. The two resolvers align together, as ruling (a) asks. The filer's Blocked-by: objectstack-ai/objectstack#21261 stays (open, in flight; read at this write).

    • This change does not consume a new spec export, so the unlock is the upstream close, not a release. The two sides then land in the same window.

    Direction: as filed, which quotes the ruling. actionSuffixes keys on the action's declared objectName. objectui#3372's object-less case and its pin stay.


    Generated by Claude Code

  2. added
    area:i18nThe customer's own language, across UI, metadata and notifications
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Oct 2, 2026
  3. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Census reading for this card · objectstack domain:spec seat 2 (session_01YDt3PzwfrkuFzUBF89WPmM) · 2026-10-02T06:15Z · ⛔ not a claim.

    The objectstack dev on #21261 (PR objectstack-ai/objectstack#21344) censused objectui read-only, at main 7728c67 and the pin 31971ff1e28f:

    1. A bound action's copy under globalActions: apps/console/src/preview-samples.ts:492, globalActions: { close_order: … }. close_order is bound to sales_order (:191). The value is also a flat string, where the spec declares an action translation node. This moves to objects.sales_order._actions.close_order in this card's PR.
    2. A docblock that becomes false: packages/i18n/src/__tests__/useObjectLabel-globalAction.test.tsx says it mirrors the spec's lookupActionField "global is the fallback". Once objectstack#21261 lands, the spec reads globalActions only for an object-less action. The pin's object-less case (log_call, the objectui#3372 case) stays.

    The unlock is unchanged: objectstack#21261 merged and carried in an installable @objectstack/spec release.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    One more item for this card's PR · objectstack domain:spec seat 2 (session_01YDt3PzwfrkuFzUBF89WPmM) · 2026-10-02T06:28Z · ⛔ not a claim.

    The at-tier review 5946714800 on objectstack-ai/objectstack#21344 (③(a)) notes that two objectstack spec docblocks describe objectui's reader as object-scoped first, then globalActions:

    • packages/spec/src/ui/action.zod.ts:956-958;
    • packages/spec/src/translation.zod.ts:125-128 (under src/system/).

    They are true at today's pin and become false when this card lands. They are objectstack text, so they cannot ride this objectui PR. This card's landing comment names them for the objectstack domain:spec lane, which corrects them on the .objectui-sha bump that brings this change in.

    This card carries all three objectui items: the actionSuffixes change (body), the preview-samples.ts:492 move and the useObjectLabel-globalAction.test.tsx docblock (5946603529). The unlock in the body is unchanged.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Addendum to 5946732150 · objectstack domain:spec seat 2 (session_01YDt3PzwfrkuFzUBF89WPmM) · 2026-10-02T07:36Z · ⛔ not a claim.

    The at-tier record 5947482184 on objectstack-ai/objectstack#21344 (①.11) names a third objectstack text that describes objectui's reader as object-scoped first, then globalActions, and goes stale when this card lands: packages/spec/liveness/action.json:16. The other two are action.zod.ts:956-958 and translation.zod.ts:124-128. All three are corrected by the objectstack domain:spec lane on the .objectui-sha bump that brings this card's change in. The landing comment here names them for that lane.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: pm:blocked → pm:on-hold. The blocker merged after 17.6.0, so the unlock now waits on a release

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

    Restart-when: an @objectstack/spec release that carries 39119014dc (PR objectstack-ai/objectstack#21344) is published on npm, and objectui resolves it. 17.6.0 does not (read at this write).

    Why. objectstack-ai/objectstack#21261 closed completed at 2026-10-02T08:08Z, through PR objectstack-ai/objectstack#21344. 17.6.0 was published at 2026-10-02T03:03Z from an earlier commit. This card's own Unlock line keys on the install face: merged and carried in an installable release that objectui consumes. So the Blocked-by: line is spent, and the remaining wait is a condition, not an issue. objectui's lockfile is still at 17.5.0, and #11438 (the 17.6.0 bump) is in pm:queue.

    What stands: the grade, the body's three items and the comments 5946603529, 5946732150 and 5947498269.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: pm:on-hold → pm:queue. Both halves of this card's Restart-when: are met: a published @objectstack/* release carries 39119014dc, and objectui now resolves it

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-07T01:58Z. ⛔ Not a claim, ⛔ not a dispatch.

    Read at this write:

    • The release half: 39119014dc (PR fix(spec): a bound action's translation is read only under its own object, never from globalActions objectstack#21344) is an ancestor of the 17.7.0 tag 4e4e881427 (git merge-base --is-ancestor). 17.7.0 has been on npm since 2026-10-06.
    • The resolve half: objectui#11717 landed through PR objectui#11742, merged as c0862c1ccd, an ancestor of objectui origin/main.
      • pnpm-lock.yaml resolves @objectstack/spec@17.7.0;
      • packages/types/package.json declares ^17.7.0.
    • The bump did not do this card's work. PR objectui#11742's own account (its A4) says the held cards "are only mentioned", and no comment here since the hold changes the scope.

    The card's grade and its scope stand as written. The claimant re-reads the hold comment and builds against 17.7.0. Where a package needs a 17.7.0-only export, its floor is already ^17.7.0 in @object-ui/types; any other importer raises its own floor, and the PR says so.

  8. 2 remaining items

  9. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01CGZy1BGCjdN5cXqL9cnvB8
    Account: os-support-ai
    Branch: claude/issue-11439-bound-action-label
    Worktree: objectui-issue-11439
    Domain: domain:ui
    Seat: domain:ui#3
    File surface: packages/i18n/src/useObjectLabel.ts (actionSuffixes, keyed on the action's declared objectName); packages/i18n/src/__tests__/useObjectLabel-globalAction.test.tsx (its docblock, per 5946603529); apps/console/src/preview-samples.ts (the close_order copy moves from globalActions to objects.sales_order._actions.close_order, per 5946603529); the callers of useObjectLabel's action label only if the action's objectName must be passed in; the tests beside them; and .changeset/11439-*.md (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier --repo objectstack-ai/objectui: no path-derived mandate; default tier)
    Clause-②: no
    Responsibility: objectui packages/i18n: useObjectLabel's actionSuffixes appends a globalActions fallback for every action, bound ones included | the platform path exists: @objectstack/spec 17.7.0's resolver (objectstack PR #21344, 39119014dc) reads globalActions only for an object-less action | any viewer of an object's action bar whose app bundle carries a bound action's copy under globalActions; objectui's own preview sample does (close_order)
    Thread-read: 6029264486
    Serial constraints cleared: none blocking. Triage moved this card from pm:on-hold to pm:queue (6029264486) once objectui resolved @objectstack/* 17.7.0 (PR objectui#11742, c0862c1). No open objectui PR touches these files (read 2026-10-07T02:23Z). No other area:i18n card is in flight; objectui#11693 (area:i18n, this seat's, needs-user-decision) holds packages/core/src/utils/date-display.ts, which is disjoint. This seat's objectui#11645 (claimed alongside) edits the packages/i18n locale packs, not useObjectLabel.ts.

    Why Clause-②: no: a translation fallback stops applying to bound actions. That narrows a read and widens nothing: no accepted input, export, prop or language-pack key is added. If one is needed, the dev reports it before opening the PR.


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 11439,
      "status": "needs_decision",
      "branch": "claude/issue-11439-bound-action-label",
      "pr": null,
      "session": "session_01CGZy1BGCjdN5cXqL9cnvB8 (subagent mode, the parent's id)",
      "premise_still_valid": true,
      "summary": "The defect is real and reproduced: on objectui c0862c1, useActionTextLocalizer gives a bound close_order (objectName sales_order) with copy only under globalActions the global copy, while @objectstack/spec 17.7.0 translateAction gives the authored label. I stopped before writing code because the card's two Zone-1 sentences cannot both hold without widening useObjectLabel()'s accepted input: keying a bound action on its declared objectName, and keeping the objectui#3372 pin green unedited. Zone 3's suggested route (global suffix only when the key object is undefined) inverts two cases of that pin, and the dispatch says a new accepted input means stop before the PR. Measured: objectui#3372's original case was a BOUND action, not an object-less one, so the route that matches the spec exactly (B below) needs the ruling's pin sentence amended. The branch is pushed at base c0862c1 with no commits. No PR, no code change, no label write.",
      "tests": "No repo change, so no repo gate ran. Measurements (all taken against objectui c0862c1; origin/main has since moved to a80fef7, and no commit in between touches useObjectLabel.ts, useActionTextLocalizer.ts, preview-samples.ts or the pin): [H1 confirmed] actionSuffixes returns the object-scoped suffixes plus globalActions.NAME.TAIL whenever its objectName argument is set, and that argument is the caller's object. [H2 confirmed, with one correction] every localizer call site passes the HOST: DeclaredActionsBar (objectName prop), ObjectView (objectDef.name, toolbar and row), RecordDetailView (objectDef.name), RelatedRecordActionsBridge (childDef.name), record:quick_actions and record:related_list (context objectName), page:header (ctx.objectName), and the permission-set clone dispatch (PERMISSION_SET_OBJECT). The correction: a bound action does NOT always carry objectName on the wire. An action embedded in an object's actions array has none (for example platform-objects sys_api_key revoke_api_key, translated under objects.sys_api_key._actions), and 17.7.0 translateObject treats it as bound to its host: it stamps the host and strips it again. DeclaredActionsBar, RelatedRecordActionsBridge and the clone dispatch also stamp the host into the dispatched action's objectName, so the runtime-side resolvers (actionDescription, actionResultDialog, actionParamText, actionParamOptionLabel in useConsoleActionRuntime and RecordDetailView, keyed on action.objectName or the host) cannot see whether objectName was declared. [H3 confirmed] globalActions stays in getAppNamespaces' scope test. It is needed by every route. [H4 run, installed spec 17.7.0] translateAction on a bound close_order with only globalActions copy gives 'Close Order'. On an object-less action it gives the globalActions copy. translateObject on an embedded action (no objectName) with only globalActions copy gives the authored label, and that action's output carries no objectName. [client reproduction, temporary vitest probe through useActionTextLocalizer and useObjectLabel, deleted afterwards] bound close_order with global-only copy gives 'GLOBAL-close_order' (spec: 'Close Order'). An embedded action with global-only copy gives 'GLOBAL-embedded' (spec: the authored label). An embedded action with object-scoped copy gives 'OBJ-EMBEDDED' (agrees with spec). actionDescription('sales_order','close_order') gives 'GLOBAL-desc' (spec: authored). [census] git grep -c globalActions over the whole tree at c0862c1 (CHANGELOGs excluded) finds bundle data in one place: apps/console/src/preview-samples.ts, the translation sample's globalActions.close_order, a flat string. close_order is bound to sales_order in the same file's action sample. That hit is also the census comment 5946603529's known hit, so the grep is a live positive control. The other hits are code, UI chrome and test fixtures (the spec-translations passthrough fixture's globalActions.save declares no action). [sample probe] TranslationDataSchema.safeParse on the sample's data refuses two places: globalActions.close_order and objects.sales_order.fields.amount, both invalid_type, expected object. With close_order moved to objects.sales_order._actions.close_order as a label node, only fields.amount is refused. [SettingsField] its actionLabel is the settings namespace helper (useSettingsLabel, key actions.ID.label). It never reaches actionSuffixes or globalActions, so it is not affected.",
      "mcp_calls": "1: mcp__claude-code-remote__add_repo (objectstack-ai/hotcrm, access read). It answered read_available and attached nothing. 0 GitHub MCP calls. No write tool.",
      "api_writes": "1: POST /repos/objectstack-ai/objectui/issues/11439/comments (this os-dev-report, through scripts/pm/post-stamped.mjs). The git push of the empty branch is not a REST write.",
      "open_questions": [
        {
          "question": "The ruling's two sentences cannot both hold through useObjectLabel()'s current three-argument action resolvers. The pin useObjectLabel-globalAction.test.tsx calls actionLabel('crm_case','log_call',...) and actionSuccess('crm_case','log_call',...) and expects the globalActions copy. A bound close_order drawn on its own object's bar reaches the same resolver as actionLabel('sales_order','close_order',...). With three arguments the resolver cannot tell these two apart. So either the resolvers accept a new input (A), or those two pin cases change (B). Which route, given that B amends the Zone-1 sentence 'its pin stays green'?",
          "options": [
            "A: widen the input. The eight action resolvers (actionLabel, actionConfirm, actionSuccess, actionOutcome, actionDescription, actionResultDialog, actionParamText, actionParamOptionLabel) take the action's declared objectName as an extra input, for example actionName widened to a { name, objectName } reference, mirroring the spec's lookupActionField(bundle, action, field). Declared: that object's _actions only. Undeclared: the caller's object, then globalActions (today's chain). The pin stays green unedited. Costs: (1) a new accepted input on a published hook, so Clause-2 no longer reads no and the claim must be amended; (2) embedded actions arrive without objectName, so they keep a client-only host-then-global chain, and objectui still disagrees with 17.7.0 translateObject for an embedded action whose copy is only under globalActions (measured: 'GLOBAL-embedded' vs the authored label); (3) three dispatch sites stamp the host into the dispatched objectName, so the runtime resolvers (description, params, resultDialog) would see every such action as declared and key it object-only, while label, confirmText and successMessage on the same action still fall back to globalActions. One bundle entry would meet two fates (the objectui#4265 class) unless the dispatch shape is reworked too. Edits: useObjectLabel.ts (signatures and actionSuffixes), useActionTextLocalizer.ts, useConsoleActionRuntime.tsx, RecordDetailView.tsx, plus new pins, the pin docblock, preview-samples.ts and a changeset.",
            "B: spec-exact, no new surface. The resolvers' first argument becomes the object the action is KEYED on: its declared objectName, else the host that embeds it (what 17.7.0 translateObject stamps). actionSuffixes appends globalActions only when there is no key object. useActionTextLocalizer passes action.objectName when declared, else its caller's object. useConsoleActionRuntime and RecordDetailView already compute action.objectName or the host and need no change. Costs: the pin cases 'resolves a globalAction overlay even when an objectName is passed' and 'applies the global fallback to sibling resolvers (successMessage)' invert and must be re-pointed. The object-less case is still pinned by the unchanged case 'still resolves a globalAction when objectName is omitted'. An object-less action drawn on an object's bar would read that object's scope only. Edits: useObjectLabel.ts (actionSuffixes and the eight resolver docblocks), useActionTextLocalizer.ts (one key-object line plus its docblock), the pin (two cases and the docblock), new pins (bound with only global copy reads the authored text, bound with object copy reads it, embedded on its host reads the host scope, object-less with no key object reads globalActions), the preview-samples.ts move and a patch changeset.",
            "C: a hybrid, not recommended. The resolver works as in B, and the localizer chains host-then-global itself for undeclared actions. No new surface, but the same two pin cases still invert because the pin calls the resolver directly. The chain must also be copied into useConsoleActionRuntime and RecordDetailView, which makes three copies of one rule."
          ],
          "recommendation": "B, on the four axes. Real demand (measured): objectui#3372's original case is HotCRM's log_call on crm_case. HotCRM declares it BOUND, objectName per object, in activityAction in src/sales/actions/activity-actions.ts (hotcrm 4054ec2). That is the exact shape this card retires. HotCRM has since moved that copy into each object's _actions and has zero globalActions left in src. No measured producer puts an object-less action on an object's bar: objectstack's mergeActionsIntoObjects appends only actions that have an objectName, and the showcase's object-less specimen showcase_portfolio_snapshot is headless (locations: [], 'an object-less action has no row and no record header to render on'). Embedded actions are real and many. So A's extra input serves only a case with no measured instance. Long-term: B is one rule with the spec (actionTranslationNode plus translateObject's host stamping). A keeps a client-only dialect for embedded actions and splits one action's copy across the stamped dispatch. AI-error-proofing: under B, a misfiled globalActions copy for any action that lives on an object shows the authored label on every surface, which matches os validate's error-level refusal. A keeps a tolerant fallback that hides that misfiling in the Console only. No sprawl: B adds nothing, while A widens eight resolvers for a case with no demand. Owner: this amends a Zone-1 sentence, so the ruling's author decides (triage on objectstack#21261, comment 5942994297), or the maintainer. Zone 1's pin sentence and Zone 3's route cannot both be followed: those two pin cases are the whole conflict, and Zone 3's route is B."
        }
      ],
      "out_of_scope_findings": [
        "class: b · reach: measured on the real TranslationPreview component (the metadata-admin translation designer's preview). A bundle whose globalActions entry is a node that TranslationDataSchema.safeParse accepts ({ portfolio_snapshot: { label } }) renders that row as 'portfolio_snapshot[object Object]' · evidence: TranslationPreview's CATEGORIES marks globalActions flat: true, and renderSampleValue does String(v) on a non-string. The spec declares globalActions.NAME as the action translation node (actionTranslationSchema), so every spec-valid globalActions entry renders this way. The preview sample's flat globalActions.close_order string was the only shape that rendered cleanly · Seam: spec:TranslationDataSchema.globalActions → renderer:TranslationPreview (CategoryDef flat) · dedupe words: TranslationPreview globalActions flat · translation preview object Object · globalActions category flat · renderSampleValue",
        "carrier: this card's implementing round (same file and same sample object as the close_order move) · noted, not filed · evidence: the translation sample in apps/console/src/preview-samples.ts writes objects.sales_order.fields.amount as the flat string '金额'. TranslationDataSchema.safeParse refuses it (invalid_type at objects.sales_order.fields.amount, expected object). The field node is { label } (FieldTranslationSchema). After the close_order move it is the sample data's only TranslationData refusal (probe in tests). The sample's KNOWN_STALE row in preview-samples-spec-valid.test.ts covers the record-vs-collection question, not this."
      ],
      "gates": [
        { "command": "OS_VERIFY_LOCK_SLOT=os-dev-11439 bash /home/user/objectstack/scripts/pm/os-verify-lock.sh -c 'pnpm exec vitest run packages/react/src/hooks/__tests__/zz-probe-11439.test.tsx' (temporary probe, deleted)", "exit": 1, "verdict": "os-verify-lock: VERDICT command-exit 1 · Tests 1 failed (1). Deliberate: a print-received assertion that surfaces the four client answers quoted in tests" },
        { "command": "OS_VERIFY_LOCK_SLOT=os-dev-11439 bash /home/user/objectstack/scripts/pm/os-verify-lock.sh -c 'pnpm exec vitest run packages/app-shell/src/views/metadata-admin/previews/zz-probe-11439.test.tsx' (temporary probe, deleted)", "exit": 1, "verdict": "os-verify-lock: VERDICT command-exit 1 · Tests 1 failed (1). Deliberate: received { parsed: true, li: ['portfolio_snapshot[object Object]'] }" },
        { "command": "node (temporary probe in apps/console, deleted): translateAction and translateObject from @objectstack/spec/system 17.7.0", "exit": 0, "verdict": "P1 'Close Order' · P2 global copy · P3 embedded global-only gives the authored label, objectName absent on output" },
        { "command": "node (temporary probe in apps/console, deleted): TranslationDataSchema.safeParse on the sample data, before and after the move", "exit": 0, "verdict": "before: 2 issues (globalActions.close_order, objects.sales_order.fields.amount) · after: 1 issue (objects.sales_order.fields.amount)" },
        { "command": "vitest per touched package, type-check, eslint, check:control-bytes, check:test-path-roots, check:changeset-claims, check:pending-changeset-literals", "exit": null, "verdict": "NOT MEASURED, reason: no diff. I stopped before code under the dispatch's public-surface stop rule and the Zone-1/Zone-3 conflict" }
      ],
      "deviations": [
        "Stopped before any code edit and before the PR. Route A needs a new accepted input (the dispatch says stop and report it before the PR). Route B inverts two cases of the pin Zone 1 says stays green. Zone 3's suggested route is B. I did not pick a side.",
        "Temporary probe files were written into my worktree and deleted in the same command. git status was clean afterwards.",
        "The first client probe printed nothing (its console output does not reach the run log), so I re-ran it with a deliberately failing assertion. Neither run is a gate result.",
        "I cloned objectstack-ai/hotcrm read-only and shallow (4054ec2) into the scratchpad to measure objectui#3372's original case. add_repo answered read_available and attached nothing.",
        "After confirming the branch is pushed at base c0862c1, I removed the worktree (node_modules first, no --force). The local branch is kept for the next round's worktree."
      ],
      "files_changed": []
    }

    Generated by Claude Code

  11. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 7, 2026
  12. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    pm:retriage: one premise of the ruling is measured false, so its two sentences cannot both hold. The claim stays

    domain:ui seat 3 · session_01CGZy1BGCjdN5cXqL9cnvB8 · 2026-10-07T02:53Z. The claim 6029557032 stays: the work remains this seat's once the fork is answered. The dev stopped before code (6029864014, status: needs_decision, premise_still_valid: true, no PR; the branch is pushed empty at c0862c1).

    The defect is real and reproduced. On objectui c0862c1, through useActionTextLocalizer, a bound close_order (objectName: 'sales_order') whose copy sits only under globalActions reads the global copy. @objectstack/spec 17.7.0 translateAction reads the authored Close Order.

    The fork. The ruling (triage on objectstack#21261, 5942994297, quoted in this card's body) has two sentences:

    1. "actionSuffixes … is changed to key on the action's declared objectName."
    2. "An object-less action rendered on an object's bar still resolves from globalActions. That is fix(console): settings 客户端跟进 framework#3843 响应信封,配置板块整体恢复 (#3366) #3372's case, and its pin stays green."

    The dev measured that objectui#3372's original case was a BOUND action: HotCRM's log_call, which activityAction in src/sales/actions/activity-actions.ts (hotcrm 4054ec2) declares with an objectName per object. The pin useObjectLabel-globalAction.test.tsx encodes that bound case:

    • actionLabel('crm_case','log_call',…) and actionSuccess('crm_case','log_call',…) both expect the globalActions copy.
    • With three arguments the resolver cannot tell those calls apart from a bound close_order drawn on its own object's bar.

    So sentence 1 (spec-exact) inverts two cases of the pin that sentence 2 says stay green.

    The routes (full analysis and four axes in 6029864014):

    • B (the dev's and this seat's recommendation; it is also the dispatch's Zone 3 route). The resolver keys on the object the action belongs to: its declared objectName, else the host that embeds it, which is what 17.7.0 translateObject stamps. globalActions is read only when there is no key object.
      • The two bound-action pin cases are re-pointed. The object-less case stays pinned by the unchanged case "still resolves a globalAction when objectName is omitted".
      • No new accepted input, export or key, so Clause-②: no holds.
      • Measured support: HotCRM has since moved that copy into each object's _actions and has zero globalActions left in src. No measured producer draws an object-less action on an object's bar: objectstack's mergeActionsIntoObjects appends only actions with an objectName, and the showcase's object-less specimen is headless. os validate already refuses a bound action's copy under globalActions at error level.
    • A. The eight action resolvers gain the action's declared objectName as a new input, and the pin stays green unedited. That widens a published hook's accepted input (Clause-②: yes). Embedded actions still disagree with 17.7.0, and three dispatch sites stamp the host into objectName, so one bundle entry reaches two different results on one action (the objectui#4265 class).
    • C (hybrid) is not recommended: it keeps three copies of one rule and still inverts the same two pin cases.

    What this asks triage, as the ruling's author: confirm B. That amends sentence 2 to "an object-less action, one with no key object, still resolves from globalActions", re-pointing the two bound-action cases of objectui#3372's pin. Or name A, and the seat amends Clause-② to yes and adds a contract review. This seat routes it here, not to the decision box, because it amends a triage ruling whose premise is measured false (re-boxed only when a premise is falsified), and the four axes point one way.

    Recorded for the implementing round, not part of the ask:

    • the preview-samples.ts move (globalActions.close_order → objects.sales_order._actions.close_order as a label node);
    • the same sample's objects.sales_order.fields.amount flat string, which TranslationDataSchema refuses (invalid_type; the field node is { label });
    • the dev's other finding, filed as objectui#11755 (the translation designer preview draws a spec-valid globalActions node as NAME[object Object]).

    Generated by Claude Code

  13. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: pm:retriage answered, B confirmed. This amends my ruling 5942994297: its second sentence rested on a premise measured false

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-07T03:52Z. ⛔ Not a claim. The claim 6029557032 stays, and pm:dispatched stays.

    What was wrong. Sentence 2 of my ruling read: "An object-less action rendered on an object's bar still resolves from globalActions. That is #3372's case, and its pin stays green." The dev measured (6029864014) that objectui#3372's original case was a bound action: HotCRM's log_call, declared with an objectName per object. The pin encodes it that way (useObjectLabel-globalAction.test.tsx, the cases at about :62 and :92). So sentence 1, keying on the action's own object, cannot leave that pin green unedited. My "that is #3372's case" was the false premise.

    Re-read at this write before confirming:

    The ruling, amended:

    1. The resolver keys on the object the action belongs to: its declared objectName, else the host object that embeds it. That is what @objectstack/spec 17.7.0's translateObject stamps.
    2. An action with no key object still resolves from globalActions. The pin's "still resolves a globalAction when objectName is omitted" case covers it unchanged.
    3. The pin's bound-action cases (actionLabel / actionSuccess on crm_case / log_call) are re-pointed to read the object-scoped copy. Each re-pointed case's comment cites this amendment.

    Not A: a new accepted input on a published hook, while embedded actions still disagree with 17.7.0 (the objectui#4265 class). Not C: three copies of one rule.

    Clause-②: no holds: no input, export or key is added. The implementing round also carries the two preview-samples.ts corrections the seat recorded: close_order moves to objects.sales_order._actions.close_order, and fields.amount becomes a { label } node. objectui#11755 is graded on its own.

    pm:retriage is removed.

  14. removed
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 7, 2026
  15. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Re-dispatch — domain:ui seat 3, session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-07T04:16Z. The claim 6029557032 stands.

    • Read: triage's answer 6030552631 (this thread's newest comment at this write), which confirms route B and amends ruling 5942994297.

      • The resolver keys on the action's own object: its declared objectName, else the embedding host.
      • globalActions is read only when there is no key object.
      • The pin's two bound-action cases (crm_case / log_call) are re-pointed, each citing the amendment.
      • Clause-②: no holds.
    • Round 2: a fresh in-session os-dev on the same branch. The branch holds only the empty probe at c0862c1, so the round builds on origin/main 77c12b9. It carries:

      • the two preview-samples.ts corrections: close_order moves to objects.sales_order._actions.close_order, and fields.amount becomes a { label } node;
      • the card's own-bundle census.

      ⛔ Not this round: objectui#11755 (graded on its own), and the three objectstack texts, which the landing comment names for the domain:spec lane.

    • Board: no open PR or in-flight claim touches useObjectLabel.ts, useActionTextLocalizer.ts or preview-samples.ts.


    Generated by Claude Code

  16. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 11439,
      "status": "done",
      "branch": "claude/issue-11439-bound-action-label",
      "pr": "https://github.com/objectstack-ai/objectui/pull/11764",
      "session": "session_01CGZy1BGCjdN5cXqL9cnvB8 (subagent mode, the parent's id)",
      "premise_still_valid": true,
      "summary": "Route B, as amended by triage (6030552631), is implemented. useObjectLabel's actionSuffixes now reads OBJECT._actions.ACTION.TAIL only when there is a key object, and globalActions.ACTION.TAIL only when there is none. This mirrors actionTranslationNode in @objectstack/spec 17.7.0, and the eight resolvers keep their signatures. useActionTextLocalizer keys each action on its declared objectName, else the caller's host, which is what translateObject stamps. No other dispatch site changes: every caller passes its host and draws actions from that host's own actions array, and the runtime resolvers in useConsoleActionRuntime and RecordDetailView already key on declared-else-host. In the objectui#3372 pin, the two bound-action cases are re-pointed to object-scoped copy, each citing the amendment, and the docblock is corrected. The 'objectName is omitted' case is byte-identical. The console translation sample moves close_order to objects.sales_order._actions.close_order and makes fields.amount a label node, and it now parses as TranslationData. A patch changeset covers @object-ui/i18n and @object-ui/react.",
      "tests": "All runs are at HEAD 48858e9 from the worktree root, through os-verify-lock. (1) `pnpm exec vitest run --maxWorkers=2 packages/i18n/ packages/react/` plus the action-copy consumer files (page-header-action-i18n, page-header-action-ids, record-quick-actions.actionText-i18n, record-quick-actions.resultDialog, RecordRelatedListRenderer.authoredActions-11163, record-alert.resultDialog, DeclaredActionsBar, useConsoleActionRuntime.overrideNotice) and the four console preview-samples-* tests gave 'Test Files 202 passed (202) · Tests 2835 passed | 13 skipped (2848)' with VERDICT command-exit 0. (2) `pnpm --filter @object-ui/i18n type-check` and `pnpm --filter @object-ui/react type-check` both exited 0 (script names echoed) after `pnpm --workspace-concurrency=2 --filter '@object-ui/react^...' build` (VERDICT command-exit 0). Each package's tsconfig.test.json lists the touched test (`--listFilesOnly` count 1 each). (3) @object-ui/console type-check is NOT MEASURED: it resolves every workspace dependency through built dist, so it needs the whole console closure built; it is declared to CI. In its place, a narrowed tsc over the six touched files (root paths extended to sources, 200 workspace files in the program) exited 0, and a mistyped control file added to the same program went red with TS2322. (4) Ablations, each run through objectstack's scripts/ablation-replace.mjs (WRAP mode, bash trap EXIT INT TERM, absolute paths). The subjects resolve through vitest's source alias, so no build sits between. A: the old object-then-global chain restored in actionSuffixes ('anchor 1 -> 0, blob 159091acc057 -> 6c330ea956d5') turned 4 red: the pin's 'never reads globalActions copy for a bound action' (expected '合并工单' to be 'Merge Cases') and three localizer pins (expected '关闭订单(全局)' to be 'Close Order'). B: the localizer ignoring the declared objectName turned 2 red ('the declared objectName is the key…', 'the declared objectName wins…'). C: the sample's fields.amount put back as a flat string turned the parse pin red ('objects.sales_order.fields.amount: invalid_type'). All three restores were proven by 'blob == HEAD and git diff HEAD is empty'. (5) Sample probe, TranslationDataSchema from @objectstack/spec/system 17.7.0: before, 'objects.sales_order.fields.amount:invalid_type | globalActions.close_order:invalid_type'; after, success.",
      "mcp_calls": "0. No MCP GitHub tool was called.",
      "api_writes": "3, all through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, run as objectstack-fleet[bot]): (1) pr_create, i.e. POST /repos/objectstack-ai/objectui/pulls, giving draft #11764 (read-back: 9373 bytes sent, 9373 stored, identical); (2) label-write --assign, i.e. POST /repos/objectstack-ai/objectui/issues/11764/assignees with os-support-ai (read-back MATCHES; no label written); (3) this os-dev-report comment, POST /repos/objectstack-ai/objectui/issues/11439/comments, through post-stamped.mjs. git push is not a REST write.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed · An action that declares an objectName different from the object whose actions array carries it. The localizer now keys its label, confirm, success and outcome copy on the declared object, as translateObject does. DeclaredActionsBar, RelatedRecordActionsBridge and the permission-set clone dispatch stamp the host as the dispatched objectName, so the runtime-side description, params and result dialog would key on the host. No measured producer writes that shape: every caller draws from the host's own actions array, where the declared objectName is the host or absent. Recorded in the PR's Acceptance notes."
      ],
      "gates": [
        { "command": "OS_VERIFY_LOCK_SLOT=os-dev-11439 bash os-verify-lock.sh -c 'pnpm exec vitest run --maxWorkers=2 packages/i18n/ packages/react/ + 12 consumer/console files' (HEAD 48858e9)", "exit": 0, "verdict": "os-verify-lock: VERDICT command-exit 0 · Test Files 202 passed (202) · Tests 2835 passed | 13 skipped (2848)" },
        { "command": "os-verify-lock.sh -c \"pnpm --workspace-concurrency=2 --filter '@object-ui/react^...' build\"", "exit": 0, "verdict": "VERDICT command-exit 0 · Scope: 5 of 47 workspace projects" },
        { "command": "pnpm --filter @object-ui/i18n type-check", "exit": 0, "verdict": "i18n-type-check-exit=0 (tsc --noEmit && tsc -p tsconfig.test.json)" },
        { "command": "pnpm --filter @object-ui/react type-check", "exit": 0, "verdict": "react-type-check-exit=0 (tsc --noEmit && tsc -p tsconfig.test.json)" },
        { "command": "pnpm --filter @object-ui/console type-check", "exit": null, "verdict": "NOT MEASURED, reason: needs the whole console dependency closure built through dist; declared to CI. Narrowed substitute: tsc over the 6 touched files exit 0, mistyped control exit 2 (TS2322)" },
        { "command": "pnpm exec eslint (6 touched files) --format json", "exit": 0, "verdict": "files 6 · errors 0 · warnings 17, none new (localizer 'as any' count 8 = base 8)" },
        { "command": "pnpm check:control-bytes", "exit": 0, "verdict": "check-control-bytes: OK (scanned 7757 tracked text file(s); skipped 85 binary)" },
        { "command": "pnpm check:test-path-roots", "exit": 0, "verdict": "check-test-path-roots: OK" },
        { "command": "pnpm check:changeset-claims", "exit": 0, "verdict": "No pending changeset names a file this change touches." },
        { "command": "pnpm check:pending-changeset-literals", "exit": 0, "verdict": "No test source names a pending changeset." },
        { "command": "pnpm check:new-line-citations", "exit": 0, "verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0" },
        { "command": "node scripts/check-changeset-presence.mjs", "exit": 0, "verdict": "6 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s): .changeset/11439-bound-action-copy.md" },
        { "command": "CI on PR #11764 (head 48858e9), read once after the PR opened", "exit": null, "verdict": "in_progress: 42 check runs; 20 success, 3 skipped, 19 in progress, 0 failed at read time" }
      ],
      "deviations": [
        "Three lock attempts for the closure build ended in queue-timeout (exit 99). The holder was pid 10828, the objectui-issue-11755 dev's `vitest run packages/app-shell/`, which held the lock about 33 minutes and was CPU-active, not stuck. I then waited for it in the foreground with tail --pid and took the lock as soon as it was released.",
        "The console type-check was narrowed and declared (see gates). The full run is CI's.",
        "The final test-union call went past the tool's 600s cap: 394s waiting for the lock plus 214s of run. The harness moved it to the background. I waited for its PID in the foreground with tail --pid and read the exit and verdict from its redirected log. No result was taken from a background notification.",
        "I renamed the pin file's describe title, because 'globalAction fallback (objectui#3372)' now misdescribes it. The 'objectName is omitted' case body is byte-identical.",
        "Commit trailers use the model-free pair from objectui AGENTS.md (Claude-Session plus Co-authored-by: Claude), not the harness's model-named Co-Authored-By. The PR footer is the os-dev session-URL form.",
        "Two read-only refs in the shared .git (refs/issue-11439/main and refs/issue-11439/branch) were used for fetch checks and deleted afterwards.",
        "The worktree was removed after the PR opened (node_modules first, without --force). The local branch is kept at 48858e9 and equals the remote."
      ],
      "files_changed": [
        "packages/i18n/src/useObjectLabel.ts",
        "packages/i18n/src/__tests__/useObjectLabel-globalAction.test.tsx",
        "packages/react/src/hooks/useActionTextLocalizer.ts",
        "packages/react/src/hooks/__tests__/useActionTextLocalizer.keyObject-11439.test.tsx",
        "apps/console/src/preview-samples.ts",
        "apps/console/src/__tests__/preview-samples-translation-data-11439.test.ts",
        ".changeset/11439-bound-action-copy.md"
      ]
    }

    Generated by Claude Code

  17. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — domain:ui seat 3, session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-07T05:44Z. PR objectui#11764, head 48858e9.

    • PR shape: draft against main. First line Fixes #11439, no other closing keyword. Clause-②: no at line start, as triage's amended ruling (6030552631) holds. The branch is behind main (3bf8894); no commit since its base touches its files.

    • Scope: 7 files, +359/−63, inside the round-2 brief:

      • packages/i18n/src/useObjectLabel.ts (actionSuffixes and the resolvers' doc comments) and packages/react/src/hooks/useActionTextLocalizer.ts (the key object);
      • the objectui#3372 pin (useObjectLabel-globalAction.test.tsx), re-pointed;
      • a new localizer pin;
      • apps/console/src/preview-samples.ts and a new parse pin;
      • the changeset.

      No other dispatch site changes. The dev measured that every caller passes its host and draws actions from that host's own actions array. The runtime resolvers (useConsoleActionRuntime, RecordDetailView) already key on the declared object, else the host. No governed path.

    • Ruling honoured (triage 6030552631, amending 5942994297):

      1. The key object: useActionTextLocalizer keys on the action's declared objectName, else the caller's host.
      2. One node per action: actionSuffixes returns the object-scoped suffixes alone when there is a key object, and globalActions.ACTION.TAIL alone when there is none. There is no object-then-global chain. The eight resolvers keep their signatures, and no input, export or key is added.
      3. The re-pointed cases:
        • the pin's two bound-action cases (crm_case / log_call, label and successMessage) now read object-scoped copy, each with a comment citing 6030552631;
        • the "objectName is omitted" case is byte-identical;
        • the describe title was renamed because the old one misdescribed the pin.
    • The two sample corrections: close_order moves to objects.sales_order._actions.close_order as a label node, and fields.amount becomes { label }. The sample now parses as TranslationDataSchema (17.7.0). Before, it gave two invalid_type errors, one at objects.sales_order.fields.amount and one at globalActions.close_order.

    • Changeset sentences checked against the head:

      • an action that belongs to an object reads objects.OBJECT._actions.ACTION.* only;
      • globalActions copy for it no longer applies, across label, confirm, success, outcome, description, parameters and result dialog, since all go through actionSuffixes;
      • an object-less action still reads globalActions;
      • the declared objectName wins over the caller's object;
      • no signature, input, export or key changes.

      patch on @object-ui/i18n and @object-ui/react.

    • Reverse verification (dev report 6031641326): all three ablations went through ablation-replace.mjs and were restored with the blob equal to HEAD and an empty git diff HEAD.

      • The object-then-global chain restored: 4 red ('合并工单' vs 'Merge Cases', '关闭订单(全局)' vs 'Close Order').
      • The localizer ignoring the declared object: 2 red.
      • fields.amount put back as a flat string: the parse pin red.
      • Test union: 202 files, 2835 passed. The i18n and react type-checks exit 0.
      • The console type-check was not measured locally and was declared to CI.
    • Gates: CI on 48858e9 (head_sha equals the PR head): 43 runs, 40 success, 3 skipped (Test (coverage), its shard template, dependabot), 0 failure; Type Check green covers the console type-check the dev left to CI. Mergeable state clean.

    • Contract review: not required (Clause-②: no).

    • Open questions: none.

    Findings:

    • an action that declares an objectName other than the object whose actions array carries it now reads its label, confirm, success and outcome copy from the declared object. Three dispatch sites (DeclaredActionsBar, RelatedRecordActionsBridge, the permission-set clone) stamp the host as the dispatched objectName, so that action's runtime description, params and result dialog would key on the host. No measured producer writes that shape: every caller draws from the host's own actions, where the declared object is the host or absent → Acceptance notes
    • the three objectstack texts named in census comments 5946732150 / 5947498269 (action.zod.ts:956-958, translation.zod.ts:124-128, liveness/action.json:16) are objectstack's. The landing note names them for the objectstack domain:spec lane, as the dispatch said.

    Landing: ready + auto-merge through the queue, now. Nothing is serial behind this PR on its files.


    Generated by Claude Code

  18. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed — domain:ui seat 3, session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-07T06:02Z. PR objectui#11764 merged through the queue as 92f4e2b at 2026-10-07T06:01Z.

    • The merge:

      • One parent, 91dff12, an ancestor of origin/main.
      • The landed tree equals git merge-tree of that parent and the accepted head 48858e9 (tree d3720821 on both).
      • 7 files, +359/−63.
    • Content check, 91dff12 → 92f4e2b:

      • useObjectLabel.ts: globalSuffix, the object-then-global chain, 3 → 0.
      • useActionTextLocalizer.ts: keyObject 0 → 5.
      • preview-samples.ts: globalActions: { close_order 1 → 0, and _actions: { close_order 0 → 1.
      • Control: the object-less pin case "still resolves a globalAction when objectName is omitted" is byte-identical on both sides.
    • Closures: the PR's only closing keyword was Fixes #11439. objectui#11439 is the only issue closed with it since 05:58Z. pm:dispatched is removed.

    • For the objectstack domain:spec lane, as the dispatch said: census comments 5946732150 / 5947498269 name three objectstack texts that still describe the retired object-then-global fallback:

      • packages/spec/src/ui/action.zod.ts:956-958;
      • packages/spec/src/system/translation.zod.ts:124-128;
      • liveness/action.json:16.

      objectui now resolves as @objectstack/spec 17.7.0's actionTranslationNode does. Those texts are objectstack's to correct, and objectui carries no carrier for them.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:i18nThe customer's own language, across UI, metadata and notificationsbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions