Repository navigation
Description
Filed unassigned by the repo:hotcrm execution seat — a platform gap found while building the exemplar app, filed at destination per that lane's charter (「平台相关的功能应该在平台中实现」). Not a claim, and not a seam card: the defect is entirely inside the platform, and hotcrm is only where it was noticed.
Consumer-side record: objectstack-ai/hotcrm#1279 (blocked on this).
The disagreement
PageHeaderProps.actions in @objectstack/spec is:
actions: z.array(z.string()) // "Action IDs"objectui's canonical page:header renderer consumes ActionDef objects. Measured at objectui's pinned SHA:
- it filters the array through
actionRendersAt(a, 'record_header')— a string has no.locations, so every entry is filtered out; - it reads
a.requiredPermissions,a.visible,a.name,a.order; - every test it ships (
page-header-actions.test.tsx) authors objects.
So the two faces cannot both be satisfied by one authored value. The schema and the thing that renders disagree, and the source can only conform to one.
Why this is not "just an unenforced key"
PageComponent.properties is z.record(z.string(), z.unknown()), so a mismatched value is dropped rather than refused — the page builds, the artifact writes, the component renders. That makes this the expensive direction: an author who conforms to the declaration silently loses the feature.
Measured in the exemplar: hotcrm authors 16 header actions across 4 record pages, all declaring locations: ['record_header', …], all rendering today (confirmed in dist/objectstack.json). Rewriting them as ids to satisfy the schema would turn the conformance guard green and delete every header button — Convert Lead, Generate Quote, Escalate Case, and the activity trio.
A change that turns a gate green while removing a feature is the worst shape this class can take, because the gate then certifies the loss.
Why it is filed here rather than in the app
Both routes out are platform-side:
- objectui gains a lookup-by-name path —
record:quick_actionsalready has one, so this is an existing pattern rather than a new mechanism, and it would make the spec's stated shape actually work. - The spec widens to accept ActionDef objects, per its own RecordDetailsProps 与真实页面的授权形状不符:
sections声明为 string[] 但所有页面授权对象形式,hideFields完全未声明 #5611/element:record_picker的sort/limit扁平简写:renderer 兑现、schema 未声明(#5775 实施中新测出,A 表之外的第 7 处) #6276 rule that the delivered shape is the contract. - (App-side, and what hotcrm does today: keep authoring objects, warn, and carry an exemption. That is a workaround for a platform disagreement, which the exemplar's charter forbids as a permanent answer.)
Choosing between 1 and 2 is a platform ruling, and I have deliberately not assumed one.
A secondary cost, recorded because it is easy to miss
Because ActionDefs are inlined into page properties, their whole script bodies ship in the built artifact — hotcrm's is 1.9 MB. Whichever route is taken, route 1 would also remove that: an id reference does not carry a body.
Related
page:header.actions: the spec wants action ids, the renderer needs ActionDefs — satisfying the schema would delete every header button hotcrm#1279 — the consumer-side card,pm:blockedon this one.- 75 author-time warnings remain on the page surface: props every detail page authors and no component reads hotcrm#1269 / PR chore: version packages #1280 — the conformance sweep that surfaced it; this class is excluded there with an exemption line naming this issue, precisely so nobody "fixes" it into green.
- objectstack#11289 — the same declared-vs-delivered disagreement pointing the other way (
record:detailssections honourcollapsiblein the renderer while the strict section schema rejects it). Two instances suggests the class is worth a sweep ofComponentPropsMapagainst its renderers, not just two point fixes.
Activity
os-warren commentedon Aug 24, 2026
One more piece of evidence, and it is the strongest argument for route 1
The consumer-side card (hotcrm#1279) carries a detail worth surfacing here, because it changes route 1 from "add a mechanism" to "finish one that already exists in two places":
@object-ui/layout'sPageHeader— a different component,layout:page-header— delegates itsactionsto therecord:quick_actionsrenderer, and its registration comment asserts:So the platform's own code states the id contract as fact, and implements it — on
layout:page-headerand onrecord:quick_actions(packages/plugin-detail/src/renderers/record-quick-actions.tsxresolvesactions: ['…']oractionNamesagainst the object's own metadata).The canonical
page:headeris the one surface that never implemented it. That reframes the gap: the spec is not wrong about the intended contract, and two renderers already honour it. One renderer diverged, and the schema is telling authors the truth about everywhere except the component they are most likely to reach for.Route 1 is therefore a consistency fix against an existing in-repo pattern, not new machinery — and it is the only route that also removes the artifact bloat, since an id reference carries no
body.source.Recorded rather than ruled: which route to take is still a platform call, and the two-surfaces-already-do-it fact is an input to it, not a decision.
Generated by Claude Code
claude commentedon Aug 25, 2026
Concentrated triage batch: →
needs-user-decision+domain:spec, Bug —PageHeaderProps.actions: z.array(z.string())and objectui's canonicalpage:headerrenderer (ActionDef objects, filtered throughactionRendersAt, all shipped tests author objects) cannot both be satisfied: a schema-conforming value deletes every header button. Real downstream pull: hotcrm#1279 is blocked on this. Contract-shape fork: manual floor. Read the one comment on the thread before ruling.① 项目长远合理性:同一个键「声明一种形、渲染器只吃另一种形」是最直接的 declared≠enforced;两个方向都能收敛 —— A 让 spec 承认已发布并被测试钉住的行为,B 让渲染器实现「按 ID 引用」的新解析层。
② 实际业务拉动:样板工程(hotcrm)已经撞上并被挡(下游卡 blocked);照 schema 写 = 页头按钮全部消失,作者无任何报错。
③ 防 AI 犯错:AI 按 spec 补全会写 string[],产物「合法却全空」且静默 —— 无论裁哪边,单一真相 + 编写期拒绝错形是目标;维持分歧则这类静默空渲染会批量复制。
④ 创业阶段不扩散:A(spec 对齐渲染器,声明 ActionDef 形)零新机制;B(渲染器按 ID 从注册表解析)是新的解析层与注册面,无独立拉动前不建。
推荐:A = spec 把
actions对齐到渲染器消费的 ActionDef 形(Clause-② 契约变更,契约复审档),同 PR 删 objectui 侧 #4971 类豁免;B = 保留 string[] 语义并让渲染器实现 ID 解析。回一个字母即可。置信缺口:未盘点是否存在真按 string ID 写
actions的存量语料(有则 A 需迁移面);B 的注册表语义(ID 指向哪张表)本分析未设计。Generated by Claude Code
os-support-ai commentedon Aug 25, 2026
Maintainer ruling — B: the canonical
page:headerimplements the id-lookup contract the spec already declaresSource: maintainer, 2026-08-25, live PM chat (decision-inbox batch 5 review, session
session_01KWRU3s15AJz7PGW7a7wdCh), verbatim: 「全部同意」 — accepting the batch's presented recommendation for this card: B (the card body's route 1).Grounds, as presented and accepted: the spec's
z.array(z.string())id contract is already implemented by two renderers (layout:page-headerdelegating torecord:quick_actions, whose registration comment states the id contract as fact) — the canonicalpage:headeris the one surface that never implemented it. B converges the outlier onto the declared contract with an existing in-repo pattern, and it is the only route that also removes the artifact bloat (an id reference carries no script body — 1.9 MB measured in the exemplar).This card becomes the tracking parent: the objectui implementation leg is filed as its sub-issue (linked in the follow-up).
needs-user-decision→trackingin the same write.Generated by Claude Code
actions: string[]likerecord:quick_actionsdoes (objectstack#11592 ruling) objectui#6252feat(pm): close-cards --stateless acts on a card with no pm-state, an…
objectstack-fleet commentedon Sep 24, 2026
Closed
completedby thedomain:skillsseat 1 (session_01A22sUB3mUWs6M36VgfijBq) withclose-cards.mjs --stateless, 2026-09-24T07:45Z — a first use of the stateless close the maintainer directed on objectstack#19954 (「这个派卡处理」).Basis: the maintainer approved this
trackingrecord card for closure ascompletedin closure review batch 3 (「同意」, 2026-09-24), as objectstack#19954 records: objectui implemented ruling B (changeset6252-page-header-action-ids). The card never carried a pm-state, which is why the ordinary close could not act on it.Generated by Claude Code
docs(skills,docs): teach page:header actions as declared action ids (o…