Skip to content

finding(plugin-detail): record:alert's CTA action.label is the third narrow slot in the same local props type — renderer resolves an inline locale map, the type says string #4998

Description

@yinlianghui

发现于 #4970 的实施(PR 见下)。#4970 的范围裁定点名的是 RecordAlertProps 的 title / body 两处,这是同一个 interface 里第三处同形的槽位,按不扩围单独记档。

机制

packages/plugin-detail/src/renderers/record-alert.tsx 的未导出局部 RecordAlertProps 里,CTA 那一支两处都写作:

action?: { actionName: string; label?: string; variant?: string };

(properties.* 一份 + flat 兼容一份,与 title / body 完全并列。)

而同一个文件的读点是:

const ctaLabel = pickLocalized(props.action?.label, language);

即渲染器对 CTA 文案做的是内联 locale 映射解析,和它上面两行对 title / body 做的事逐字相同 —— 只有类型声明说这里只能是字符串。#4970 已经把 title / body 放宽成 string | I18nLabel(绑定 spec 符号),这一处留在原样。

今天不咬人(observation-class 的依据)

  • interface 没有导出(只做 React.FC< RecordAlertProps > 的参数),所以没有任何消费者读到这个窄声明;
  • pickLocalized 收 unknown,所以 TS 不报错、运行时也照样解析映射;
  • 作者面(manifest / sdui-intrinsics.d.ts)根本没有声明这个成员的类型:plugin-detail/src/index.tsx:569 是 { name: 'action', type: 'object', … },成员形状只写在 description 的散文里({ actionName, label?, variant? })。

这也是它与 title / body 的判据差别,值得单独记而不是并进 #4970:那两个键的第二条臂有已落地的发布面声明(['string', 'object'],#3832 / PR #4975)可以对齐,所以「声明比自己的发布面窄」是个可指认的矛盾;action.label 的发布面对成员类型什么都没说,窄声明的唯一对照物是渲染器本身。要把这一处说成契约不一致,得先有成员形状可声明 —— 那是 ComponentInput 的成员形状缺口(PR #3795 的 open question),不是这一行能解决的。

修复面(尺度最小的一档)

两行:label?: string | I18nLabel(I18nLabel 已在同文件 import,#4970 引入)。如果同时想让发布面能表达这个事实,则要等成员形状那张卡。

关联:#4970(title / body 两处 + elements.tsx 两个标本)、#3832 / PR #4975(机制 + 五标本)、#4581(声明窄于渲染器的同族,已关闭,范围是 BaseSchema)、PR #3795(ComponentInput 无成员形状,本卡发布面那一半的前提)。

Activity

  1. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    Triage first-touch — concentrated round #2 (triage seat, session session_019ZyCHcjXeTFPiS8RWRk4nN, 2026-08-19): promoted finding → pm:queue · Task. Two-line fix (label?: string | I18nLabel in both copies of the local RecordAlertProps CTA slot), aligning the declaration with what the renderer already does (pickLocalized) — the same move #4970 landed for title/body in the same interface. The publish-surface half (declaring the member shape in the manifest) stays out of scope pending the ComponentInput member-shape question (#3795), exactly as the body reasons. Size/model suggestion: S-mechanical, sonnet.


    Generated by Claude Code

  2. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 21, 2026
  3. self-assigned this
    on Aug 21, 2026
  4. os-sales commented on Aug 21, 2026

    @os-sales
    Collaborator

    Claim: PM loop round 3 (batch raised to 5 at the maintainer's instruction — 「任务很多,并发保持5」)
    Session: session_012u2pRjcqAYtoEjgr3wwhnK
    Branch: claude/issue-4998-record-alert-label-i18n
    Worktree: objectui-issue-4998
    Domain: domain:ui
    File surface: packages/plugin-detail/src/renderers/record-alert.tsx and the second copy of the local RecordAlertProps CTA slot wherever it lives in packages/plugin-detail/** (+ tests, + a changeset) (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: sonnet — the floor, and this is the card that earns it: triage measured it as S-mechanical with a landed precedent (#4970 made the identical move for title/body in the same interface), the change is a type widening the renderer's runtime behaviour already implements, and correctness is decided by type-check rather than by judgment. The one search task — finding both copies, not one — is bounded and gate-checked.
    Clause-②: no — widening a local props type to match what the renderer already does aligns a declaration with existing behaviour; it neither changes the accepted set nor widens the published surface. ⚠️ The publish-surface half (declaring the member shape in the manifest) is deliberately out of scope, parked on the ComponentInput member-shape question in #3795 — do not pull it in.
    Serial constraints cleared: none — packages/plugin-detail/** has no in-flight claim in this lane and no PR this session touched it. Disjoint from round 3's other four (apps/console/**, packages/app-shell/src/views/studio-design/**, packages/components/src/renderers/layout/grid.tsx, packages/core/**).

    Located on origin/main this round: packages/plugin-detail/src/renderers/record-alert.tsx with __tests__/record-alert.test.tsx and __tests__/record-alert.visibleWhen.evidence.test.tsx beside it.


    Generated by Claude Code

  5. added a commit that references this issue on Aug 21, 2026
    dc14b64
  6. os-sales commented on Aug 21, 2026

    @os-sales
    Collaborator

    os-dev-report
    {
    "issue": 4998,
    "status": "done",
    "branch": "claude/issue-4998-record-alert-label-i18n",
    "pr": "#5572",
    "premise_still_valid": true,
    "summary": "Widened the local, unexported RecordAlertProps CTA slot's action.label from string to string | I18nLabel in both copies in packages/plugin-detail/src/renderers/record-alert.tsx (schema.properties.action.label line 98, flat-compat schema.action.label line 108), matching the pickLocalized(props.action?.label, language) resolution the renderer already performs at runtime. This is the same move #4970 landed for title/body in the same interface. Added a test asserting the case the type previously refused (a locale-map action.label rendering the resolved string via a real I18nProvider); the publish-surface manifest half stays out of scope per the ruling, parked on #3795.",
    "tests": "All at commit dc14b64. pnpm --filter @object-ui/plugin-detail type-check: exit 0 (tsc --noEmit && tsc -p tsconfig.test.json, 0 errors). pnpm exec vitest run packages/plugin-detail/src/renderers/tests/record-alert.test.tsx: 21/21 passed (1 test file). pnpm exec vitest run packages/plugin-detail/: 90 test files / 848 tests passed. pnpm --filter @object-ui/plugin-detail lint: exit 0, 0 errors (809 pre-existing no-explicit-any/react-hooks warnings, none on touched lines, confirmed by grep against changed files). node scripts/check-control-bytes.mjs: OK, 4629 tracked text files scanned. node scripts/check-changeset-presence.mjs: 2 source files changed under src/, 1 changeset declared -> pass. node scripts/check-changeset-no-major.mjs: pass, no major bump. node scripts/check-changeset-fixed.mjs: pass, all workspace packages in fixed group. Reverse verification: reverted only the type declaration in record-alert.tsx to the pre-fix label?: string (source-only, confirmed on disk via grep), reran type-check -> exit 2, tsc reported TS2322 at exactly the new test's locale-map literal (record-alert.test.tsx:219:17, "Type '{ en: string; zh-CN: string; }' is not assignable to type 'string'"); the pre-existing plain-string CTA test stayed unaffected. Restored the fix via git checkout from the committed branch state (git status clean afterward), reran type-check -> exit 0 again. This confirms the new locale-map test -- not the pre-existing plain-string CTA test -- is the case the narrow declaration refused.",
    "open_questions": [],
    "out_of_scope_findings": []
    }


    Generated by Claude Code

  7. os-sales commented on Aug 21, 2026

    @os-sales
    Collaborator

    PM · review of PR #5572 — the ablation is the right one for a type-level fix

    Most reverse verifications this session mutated behaviour and watched tests go red. That would have proved nothing here, because the renderer already resolved locale maps at runtime — pickLocalized takes unknown, so every vitest assertion is green with the narrow declaration too. You ablated the thing that actually carries the fix:

    reverted only the type declaration to label?: string (source-only, confirmed on disk via grep), reran type-check → exit 2, TS2322 at exactly the new test's locale-map literal (record-alert.test.tsx:219:17, "Type '{ en: string; zh-CN: string; }' is not assignable to type 'string'"); the pre-existing plain-string CTA test stayed unaffected. Restored, re-ran → exit 0.

    That establishes two separate things, and the second is the one reviewers usually skip: not just that the pin fails without the fix, but which test carries the discrimination — the new locale-map case, not the plain-string case sitting three lines above it. I had flagged exactly this doubt on my own read before your report arrived; you had already closed it.

    Checked independently, not taken from the report

    • Both copies widened — schema.properties.action.label (:98) and the flat compat mirror schema.action.label (:108). Finding one and shipping was this card's failure mode; triage said "both copies" for that reason.
    • The stale docblock was rewritten, not left to rot. It previously read "deliberately NOT widened here (objectui#4998)" — a sentence that would have become false the moment this merged. It now states the widening, why (pickLocalized parity with title/body), and that it is type-only.
    • The out-of-scope half stayed out. The published surface still declares action as a bare object with the member shape in prose, so there is no manifest arm to align; parked on fix(plugin-detail): record:highlights 的 fields 声明补上 readonly,让 manifest 能被作者读到 (#3407) #3795 and said so in both the code comment and the changeset, which is where a future reader will actually look.
    • Unprompted care worth naming: wrapping the new case in a real I18nProvider pinned to en so it does not ride whatever language another test file left on the global i18next singleton. Nothing asked for that; a locale test that silently depends on file execution order is a flake waiting for a busy CI runner.

    This was dispatched at the sonnet floor — triage measured it S-mechanical with a landed precedent (#4970) and correctness decided by type-check rather than judgment. That call holds: the floor was the right tier, and the work came back with a better ablation than the tier required.

    Status

    Path surface (get_files): 3 files, all packages/plugin-detail/** plus the changeset. No governed surface → ordinary ready → merge-queue path.

    CI on #5572: Build & E2E, Build Docs and all three changeset gates green; the four Test shards and Type Check still in_progress, nothing red. Type Check is the gate that decides this card, so no verdict and no enqueue until it reads completed: success by name.


    Generated by Claude Code

  8. os-sales commented on Aug 21, 2026

    @os-sales
    Collaborator

    ACCEPT — PR #5572

    CI converged. Read by name: Type Check (the gate that decides this card), Test (shard 1/4) (2/4) (3/4) (4/4), Lint, Build & E2E, Bundle Analysis, Control Byte Scan, all three changeset gates, both doc type checks, Internal Docs Link Check, Skill Guide Path Check, Build Docs, Live E2E → every one completed: success. 19 success / 3 skipped / 0 failures / 0 in progress.

    Path surface (get_files): 3 files, all packages/plugin-detail/** plus the changeset. No governed surface → ordinary ready → merge-queue path.

    Substantive review is in the comment above and does not depend on this run.

    Incidental corroboration of the "type-only" claim: plugin-detail reads 60.89 KB gzipped in Bundle Analysis — byte-identical to the pre-change figure, which is exactly what a type widening should produce.

    Driving to landing.


    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

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions