Skip to content

record:reference_rail 的 entry 授权面比页面所需更窄,且该形状未被 spec 声明:filter 写了静默失效、标题拿不到 pluralLabel #986

Description

@yinlianghui

来源:#972 / PR #983 实施过程的测量结论。#972 的「Open Tasks 徽章计入 Completed」那一半按 measure-first 判定为平台能力缺口、本仓不硬改,故把测量结果单独记在这里,免得随 #972 关闭一起丢失。PR #983 已在本仓侧加了守卫挡住写法,但守卫只能挡,不能补能力。

测量对象是本仓实际装的 @objectstack/console@17.0.0-rc.3 与 @objectstack/spec@17.0.0-rc.3 的产物字符串,未做浏览器实测。

实况:rail entry 只有三个键被读

参考栏对每个 entry 只发这一条查询(node_modules/@objectstack/console/dist/assets/plugins-views-CJKuyDvq.js):

find(objectName, { $filter: { [relationshipField]: parentId }, $top: limit ?? 3, $count: true })

标题解析是:

entry.title || (i18n.objectLabel ? i18n.objectLabel({ name: entry.objectName, label: humanize(entry.objectName) }) : humanize(entry.objectName))

即实际被消费的只有 objectName / relationshipField / limit / title(外加 icon、displayField 用于卡片内的行渲染)。组件注册的 inputs 只声明了 hideEmpty 一项。

三条各自独立的缺口

1. entry 没有 filter 通道,而写了不会报错

参考栏的卡片只能表达「关联的全部记录」,无法表达「关联的、且满足某谓词的记录」。商机详情页想要的「未完成任务」在 Related 页签的关联列表里是一行 filter: [{ field: 'status', op: 'neq', value: 'completed' }],在 rail entry 上则无处可写。

比「不支持」更值得记的是它不报错:@objectstack/spec 的 ComponentPropsMap 里没有 record:reference_rail 这一行,页面组件的 properties 是 z.record(z.string(), z.unknown()),.d.ts 里也不存在 ReferenceRailEntry 类型。所以在 entry 上补 filter 会解析通过、typecheck 通过、objectstack validate 通过、构建进产物、发布上线,然后什么也不做 —— 徽章照旧计入全部记录,而源码从此声称自己过滤了。这正是 AI 写元数据最容易踩、也最难在 review 里看出来的一类错。

2. 标题只能拿 label,拿不到 pluralLabel

objectLabel 查的键是 objects.*.label。一张带「关联记录总数」徽章、列最多三条记录的卡片,语义上要的是复数标签(objects.*.pluralLabel,本仓四个语言包都写齐了),拿到的却是单数。PR #983 删掉硬编码 title 之后,英文界面的三张卡片就是 Quote / Opportunity Line Item / Task —— 本地化对了,但数词不对。这条是纯观感,不影响任何功能。

3. title 是不可翻译的字面量

同一份产物里 record:alert 的 title 走 gt(n.title, language)(内联译文映射解析器,支持 { en, 'zh-CN', … }),参考栏则把 entry.title 当作原始 React child 直接渲染。所以 rail entry 的 title 只有两种状态:要么不写(回落到本地化对象标签),要么写一个压过所有语言包的字面量。没有「写一个会被翻译的标题」这个选项。PR #983 选择了不写,代价见第 2 条。

可能的上游修法(仅列出,不预判取舍)

  1. 只补声明不补能力:在 spec 的 ComponentPropsMap 里给 record:reference_rail 补一行严格 schema。这样第 1 条的「静默失效」立刻变成发布时的响亮拒绝,代价最小,且不需要判断有没有业务拉力。
  2. 补 filter:entry 加可选 filter,与 record:related_list 同款谓词形状,渲染侧并进 $filter。
  3. 补标题通道:entry 的 title 接受内联译文映射(与 record:alert 对齐),或让无 title 时按「是列表卡片」取 pluralLabel。

按「初创聚焦、能力扩张要有真实业务拉力」的口径,第 1 条与第 2、3 条的性质不同:第 1 条是把已经存在的形状说清楚(收紧,不扩张),第 2、3 条才是新增授权面 —— 目前的真实消费方只有本仓商机详情页这一个参考栏(全仓 grep 命中一处),拉力是否够,请维护者判断。

本仓侧已做的挡板

PR #983 在 test/metadata-references.test.ts 加了三道守卫(entry 必须指向真实对象与关系字段、不得声明字面 title、不得声明 filter),并在 test/i18n-references.test.ts 加了一道(参考栏涉及的对象在四个语言包里必须都有 label —— 因为 rail 的兜底不是英文 label 而是 humanize(objectName),缺译文比没有 i18n 还糟)。第三道在干净树上是空过的,已用「植入 filter → 转红」单独证伪过,不是靠什么也没产出而通过。

重复检查

search_issues 在本仓 open issue 上查 reference rail filter、reference_rail —— 命中的只有 #972(本条来源)与 #733(同一栏 Products 卡片显示原始记录 ID,是 entries 的另一种失真,与本条不同)。无既有单。若与他人同小时立单重复,烦请 PM race-close。

Refs #972 #733

Activity

  1. added
    metadataDeclarative metadata — schema, security posture, UI surfaces
    upstream:objectstackBlocked on / caused by the ObjectStack platform — tracked upstream
    on Aug 6, 2026
  2. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    GA re-test on 17.0.0 — all three gaps REPRODUCE · stays OPEN

    Both halves this card was missing are now measured: the static read redone on GA, and the live half confirmed on a rendered rail in a browser — which the original filing explicitly did not do (「未做浏览器实测」).

    Version — @objectstack/spec 17.0.0 GA, @objectstack/console 17.0.0 GA (this card measured rc.3). hotcrm @ d4ddee0.
    Probe — booted GA app on port 4003, Chromium 141 via Playwright, opportunity detail O587r0gc6QK0JL0p (Acme, has quotes / line items / tasks).


    Static half — re-read on GA: record:reference_rail is still undeclared

    ComponentPropsMap (@objectstack/spec/ui, 17.0.0 GA)
      total component keys = 37
      record:* keys = ["record:details","record:related_list","record:highlights",
                       "record:activity","record:chatter","record:path"]
      has record:reference_rail = false
    

    grep reference_rail dist/ui/index.d.ts → 0 hits. Unchanged from the rc.3/rc.6 reading.

    Control, so "undeclared" is a measurement and not an assumption: a declared sibling really is parsed — ComponentPropsMap['record:related_list'].safeParse({...}) rejects, and this app's own build emits loud component-props-unknown-key warnings for record:related_list, record:activity and page:accordion. The rail is simply not in that set.

    Live half — gap 1: a filter on an entry parses, ships, and does nothing

    Reverse verification, with the expected direction fixed before running: plant a filter that should visibly change the badge, and see whether it does.

    Baseline — 3 tasks related to this opportunity, of which 2 are not completed (I created one completed task precisely so a working filter would be observable; with all tasks in one state the probe could not have failed):

    Task | 3 | View All
      Acme — schedule AI governance workshop   (not_started)
      PROBE-1154 completed task                (completed)
      Follow up with Acme on proposal          (not_started)
    

    Then planted on the crm_task rail entry:

    filter: [{ field: 'status', op: 'neq', value: 'completed' }]
    stage result
    tsc --noEmit exit 0 — typechecks
    objectstack validate passed, reference_rail mentioned 0 times in the output
    pnpm build exit 0, reference_rail mentioned 0 times
    shipped artifact filter present verbatim in dist/objectstack.json
    rendered rail badge still 3; the completed task still listed

    The same build run emitted warnings for record:related_list's filter shape (op unrecognized, operator invalid option) — so the toolchain is loud about the declared component and silent about the undeclared one in the very same file. That contrast is the defect: it parses, typechecks, validates, builds, publishes, and does nothing, while the source now claims it filters.

    Screenshots 986-rail-baseline.png and 986-rail-planted-filter.png — identical Task 3 badge and identical three rows. The planted filter was reverted immediately (page file restored byte-identical, git hash-object matches 26b2cece…, artifact rebuilt clean).

    Live half — gap 2: the title takes label, never pluralLabel

    Rendered rail headings on GA, English:

    Quote                  1   View All
    Opportunity Line Item  3   View All
    Task                   3   View All
    

    Singular, on cards that carry a total-count badge and list up to three rows. objects.*.pluralLabel is authored in all four locale bundles here and is unreachable. Exactly as described — cosmetic, no functional impact.

    Gap 3: title is still an untranslatable literal

    Unchanged in the GA bundle: record:alert runs its title through the inline translation-map resolver while the rail renders entry.title as a raw React child. The two states remain "omit it and get the localized singular label" or "write a literal that overrides every locale". PR #983's choice to omit still stands, and gap 2 is its price.

    Bonus confirmation, already tracked

    The Opportunity Line Item card lists raw record IDs (3hSgFDn-R3twK3mR, CrZ5ffsjr27zBaiw, YQVNhPOBIelR7i8v) instead of a display label — that is #733, still live on GA. Recorded here as corroboration, not as a new finding.


    Routing — this card splits across two repos, and the split matters

    • Gap 1 is a packages/spec gap — the missing ComponentPropsMap row. That is objectstack (Seat A). It is also the option this card ranked first: it tightens an existing shape rather than expanding the authorization surface, needs no judgement about business pull, and converts a silent no-op into a loud publish-time rejection — the structural fix that stops AI-authored metadata from confidently declaring configuration that does nothing.
    • Gaps 2 and 3 are console renderer behaviour (pluralLabel resolution, translatable title) and would land in objectui (Seat B). Both are genuine capability expansion, and this card already flagged that the only consumer in this repo is a single rail — the pull question the maintainer was asked to judge is still unanswered, so I am not pre-empting it.

    I am filing one mirror, in objectstack, for gap 1 only, and naming gaps 2–3 in it as objectui-side follow-ups to be split out if and when the maintainer rules there is pull. Filing all three in one repo would have put a spec contract fix and two undecided console features on one desk.

    Probed under #1154 (parent #1150). Probe scaffolding deleted; no product code changed.


    Generated by Claude Code

  3. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    Mirror filed for gap 1: objectstack-ai/objectstack#8691 — "spec: record:reference_rail has no ComponentPropsMap row — an entry filter parses, typechecks, validates, ships, and silently does nothing".

    Repo choice: objectstack, not objectui — deliberately the opposite call to the other two console cards in this batch. Gap 1's fix is a row in ComponentPropsMap in packages/spec, which is objectstack source; nothing in the console renderer has to change for the silent no-op to become a loud publish-time rejection. Gaps 2 and 3 (title cannot reach pluralLabel; title is an untranslatable literal) are console/objectui surface and are named in the mirror as explicitly out of its scope, to be split to the console seat only if the maintainer rules there is business pull — this card already measured that the sole consumer repo-wide is one rail on one detail page.

    Duplicate check before filing: searched objectstack for reference_rail / ComponentPropsMap / undeclared-props — only #1894 ("Inverse drift — renderers read undeclared props", closed). No open card.

    Nominated for target:v17 on objectstack-ai/objectstack#8667 (Seat A — platform), which is the right desk for a packages/spec contract fix. No labels applied by me.


    Generated by Claude Code

  4. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    NOT closed — the pointer does not cover this card. Staying open, flagged to the maintainer.

    This card was scheduled for close-with-pointer in the transfer sweep #1156 (part 2), alongside nine siblings. The other nine closed. This one did not pass the pre-close check, so it is being left open deliberately rather than closed on a partial pointer.

    What the check found

    The mirror objectstack-ai/objectstack#8691 exists, is open, is labelled bug, and is a good card. But it deliberately carries one of this card's three gaps, and says so in its own words:

    Scope note up front: the downstream card records three gaps in record:reference_rail. This mirror carries only the first — the missing spec declaration.

    Meanwhile this card's GA reading (2026-08-14, @objectstack/spec + @objectstack/console 17.0.0 GA, static read plus a rendered rail in a real browser) measured all three as reproducing:

    gap GA state covered by #8691?
    1. no ComponentPropsMap row — an entry filter parses, typechecks, validates, ships, does nothing reproduces yes
    2. rail title resolves objects.*.label, never pluralLabel reproduces no — named as out of scope
    3. rail title is a raw React child, so it cannot take an inline translation map reproduces no — named as out of scope

    Why that blocks the close rather than being a detail

    Closing here would leave gaps 2 and 3 with no open card in any repo. I searched objectui before writing this: the nearest hits are #4028 (AddressField sub-labels), #4163 (I18nLabel inline map audit, needs-user-decision) and #4024 (closed) — none covers the reference rail's title. The only other record of gaps 2 and 3 is prose inside #8691's "Explicitly not in this card" section, and that card is scoped to the spec declaration; when it is fixed and closed, that prose closes with it.

    There is also a live maintainer question attached to those two gaps, and this card is currently its only open holder. Both are capability expansion rather than a tightening, and this card already measured that the sole consumer repo-wide is one rail on one detail page (grep hits once) — so whether there is real business pull is genuinely open under the startup-focus principle. The probe round deliberately did not pre-empt that ruling by filing an objectui card, and neither will I: filing one now would answer the maintainer's question on their behalf, and this task is routing and bookkeeping only.

    So the choice was between closing this card and losing two measured findings plus an unanswered ruling request, or leaving one card of ten open and saying why. Per #1156's own instruction — a pointer to a card that describes a different defect is worse than no pointer, because it closes the hotcrm card and loses the finding — this is the second.

    What would unblock it

    Either of these, and this card can close with a pointer immediately:

    1. The maintainer rules there is no business pull for a rail filter channel or a translatable rail title. Then gaps 2 and 3 are wontfix and can be recorded as such right here, and this card closes with the pointer to #8691 for gap 1.
    2. The maintainer rules there is pull. Then an objectui card is filed for gaps 2 and 3 (console renderer surface — pluralLabel resolution and title translation, the same seat as objectui#4644 / #4645), and this card closes with pointers to both.

    Until then this card keeps upstream:objectstack and stays open. Labels untouched; no target:v17 applied anywhere; #8691's and #8667's lists untouched.

    The maintainer ruling this sweep runs under, quoted verbatim and untranslated, since it is what sends gap 1 upstream in the first place:

    hotcrm 席位的原则是用平台的能力做元数据应用的开发,平台的需求应该转给平台

    Authority: #1156.


    Generated by Claude Code


    Generated by Claude Code

  5. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    Closing — gap 1 transferred, gaps 2 and 3 deliberately not worked

    PM decision, session session_01XAK3brMLjd4ykF4QAhFnuo, closing this card under the GA close-out sweep (#1150) and the transfer ruling (#1156). #1156 part 2 correctly refused to close this card on a bare pointer, because #8691 covers gap 1 only. That refusal is what forced this to be an explicit decision instead of a silent loss, and it was the right call.

    GA reading (17.0.0, measured 2026-08-14 under #1154)

    All three gaps reproduce. Gap 1 was established by ablation rather than inspection: a real filter was planted on the crm_task rail entry against data that would visibly change the answer (3 related rows, 2 not completed), and it passed every gate — tsc --noEmit exit 0, objectstack validate PASSED with reference_rail mentioned 0 times, pnpm build exit 0, filter present verbatim in dist/objectstack.json — with the rendered badge still reading 3. Control: the same build emitted component-props-unknown-key warnings for record:related_list, record:activity and page:accordion in the same file, so the toolchain can warn and simply has no row to check this component against. ComponentPropsMap on GA has 37 keys and no record:reference_rail. The plant was reverted and proved byte-identical by git hash-object.

    Disposition

    Gap 1 — the shape is undeclared, so an entry filter silently does nothing. Transferred: objectstack#8691. That card is the system of record and it is the load-bearing half — declaring the row converts a silent no-op into a loud publish-time rejection.

    Gaps 2 and 3 — the rail title resolves objects.*.label and can never reach pluralLabel; the title renders as a raw React child so it cannot take an inline translation map. ⛔ Not worked, and no card is being filed. This is a decision to lose two measured findings on purpose, recorded here rather than left to evaporate.

    Both are capability expansion on the console renderer, and all three evaluation axes agree:

    • Measured business pull — none. A repo-wide grep finds exactly one rail, on one detail page. Gap 2 is cosmetic (singular "Task" on a card that already carries a count badge). Gap 3 describes a title nobody writes: this app's own PR fix(pages): 商机参考栏卡片标题回落到本地化对象标签,并去掉 Open Tasks 的不实过滤声明 (#972) #983 deliberately chose the omit-the-title path, which inherits the localized object label and works today.
    • Platform long-term coherence. Gap 1's fix is independent of both. If a filter or title channel is ever granted, the declared ComponentPropsMap row is precisely where it gets enforced — so deciding this now costs nothing and forecloses nothing.
    • AI-agent error-resistance. Gap 1 does all the work on this axis: an AI-authored rail filter starts failing loudly at publish time. A translatable rail title neither helps nor hurts it, and gap 3 would create a second way to spell a title — new authoring surface that removes no failure mode.

    Decided on the standing startup-focus principle (capability expansion defaults tight absent demonstrated pull), not on a fresh product ruling.

    This is a veto window, not a closed door

    ⚠️ If the maintainer knows of a consumer this repo cannot see, the remedy is one objectui card for gaps 2–3 — cheap, low-risk, same seat as objectui#4644 / #4645 — and this card reopens on that ruling. Nothing here is irreversible; the findings are preserved in full above precisely so that reversal needs no re-measurement.

    Closing with the pointer to objectstack#8691. Authority: maintainer ruling 2026-08-14, verbatim and untranslated:

    hotcrm 席位的原则是用平台的能力做元数据应用的开发,平台的需求应该转给平台


    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

No one assigned

    Labels

    findingmetadataDeclarative metadata — schema, security posture, UI surfacesupstream:objectstackBlocked on / caused by the ObjectStack platform — tracked upstream

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions