Repository navigation
record:reference_rail 的 entry 授权面比页面所需更窄,且该形状未被 spec 声明:filter 写了静默失效、标题拿不到 pluralLabel #986
Description
Activity
- addedmetadataDeclarative metadata — schema, security posture, UI surfacesDeclarative metadata — schema, security posture, UI surfacesupstream:objectstackBlocked on / caused by the ObjectStack platform — tracked upstreamBlocked on / caused by the ObjectStack platform — tracked upstream
on Aug 6, 2026 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/spec17.0.0 GA,@objectstack/console17.0.0 GA (this card measured rc.3). hotcrm @d4ddee0.
Probe — booted GA app on port 4003, Chromium 141 via Playwright, opportunity detailO587r0gc6QK0JL0p(Acme, has quotes / line items / tasks).
Static half — re-read on GA:
record:reference_railis still undeclaredComponentPropsMap (@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 = falsegrep 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 loudcomponent-props-unknown-keywarnings forrecord:related_list,record:activityandpage:accordion. The rail is simply not in that set.Live half — gap 1: a
filteron an entry parses, ships, and does nothingReverse 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
completedtask 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_taskrail entry:filter: [{ field: 'status', op: 'neq', value: 'completed' }]
stage result tsc --noEmitexit 0 — typechecks objectstack validatepassed, reference_railmentioned 0 times in the outputpnpm buildexit 0, reference_railmentioned 0 timesshipped artifact filter present verbatim in dist/objectstack.jsonrendered rail badge still 3; the completed task still listedThe same build run emitted warnings for
record:related_list's filter shape (opunrecognized,operatorinvalid 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.pngand986-rail-planted-filter.png— identicalTask 3badge and identical three rows. The planted filter was reverted immediately (page file restored byte-identical,git hash-objectmatches26b2cece…, artifact rebuilt clean).Live half — gap 2: the title takes
label, neverpluralLabelRendered rail headings on GA, English:
Quote 1 View All Opportunity Line Item 3 View All Task 3 View AllSingular, on cards that carry a total-count badge and list up to three rows.
objects.*.pluralLabelis 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:alertruns its title through the inline translation-map resolver while the rail rendersentry.titleas 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/specgap — the missingComponentPropsMaprow. 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 (
pluralLabelresolution, 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
- Gap 1 is a
Mirror filed for gap 1: objectstack-ai/objectstack#8691 — "spec:
record:reference_railhas no ComponentPropsMap row — an entryfilterparses, typechecks, validates, ships, and silently does nothing".Repo choice:
objectstack, notobjectui— deliberately the opposite call to the other two console cards in this batch. Gap 1's fix is a row inComponentPropsMapinpackages/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 reachpluralLabel; 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:v17on objectstack-ai/objectstack#8667 (Seat A — platform), which is the right desk for apackages/speccontract fix. No labels applied by me.
Generated by Claude Code
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/console17.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 ComponentPropsMaprow — an entryfilterparses, typechecks, validates, ships, does nothingreproduces yes 2. rail title resolves objects.*.label, neverpluralLabelreproduces no — named as out of scope 3. rail titleis a raw React child, so it cannot take an inline translation mapreproduces 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 (
AddressFieldsub-labels), #4163 (I18nLabelinline 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 (
grephits 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:
- The maintainer rules there is no business pull for a rail
filterchannel or a translatable rail title. Then gaps 2 and 3 arewontfixand can be recorded as such right here, and this card closes with the pointer to #8691 for gap 1. - The maintainer rules there is pull. Then an objectui card is filed for gaps 2 and 3 (console renderer surface —
pluralLabelresolution and title translation, the same seat as objectui#4644 / #4645), and this card closes with pointers to both.
Until then this card keeps
upstream:objectstackand stays open. Labels untouched; notarget:v17applied 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
- The maintainer rules there is no business pull for a rail
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
filterwas planted on thecrm_taskrail entry against data that would visibly change the answer (3 related rows, 2 not completed), and it passed every gate —tsc --noEmitexit 0,objectstack validatePASSED withreference_railmentioned 0 times,pnpm buildexit 0, filter present verbatim indist/objectstack.json— with the rendered badge still reading 3. Control: the same build emittedcomponent-props-unknown-keywarnings forrecord:related_list,record:activityandpage:accordionin the same file, so the toolchain can warn and simply has no row to check this component against.ComponentPropsMapon GA has 37 keys and norecord:reference_rail. The plant was reverted and proved byte-identical bygit hash-object.Disposition
Gap 1 — the shape is undeclared, so an entry
filtersilently 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.*.labeland can never reachpluralLabel; 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
ComponentPropsMaprow 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
来源:#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):标题解析是:
即实际被消费的只有
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 条。可能的上游修法(仅列出,不预判取舍)
ComponentPropsMap里给record:reference_rail补一行严格 schema。这样第 1 条的「静默失效」立刻变成发布时的响亮拒绝,代价最小,且不需要判断有没有业务拉力。filter,与record:related_list同款谓词形状,渲染侧并进$filter。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