Repository navigation
No record-context binding for filter values — element:number and every dataSource filter on a record page can only be scoped to the VIEWER, never to the record #7297
Description
Activity
- addedenhancementNew feature or requestNew feature or requestdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Sep 5, 2026 分诊 —
domain:ui/priority:p2/pm:blocked/enhancement锚定 (anchoring):resolver (
packages/core/src/utils/filter-tokens.ts) 和 record context 都是 objectui 的 ⇒domain:ui。⚠️ 不是domain:spec—— objectui 的 spec lane 是packages/types,而CONTEXT_TOKENS在本仓是再导出而非分叉(objectui#3003),词表本体在@objectstack/spec/data/context-tokens.zod.ts。所以这张卡的落点只有一半在本仓。在
origin/maina472b07上复核的锚点packages/core/src/index.ts:67 // Session-scoped filter placeholders ({current_user_id} / {current_org_id}) packages/components/src/renderers/basic/elements.tsx:402 const rows = await adapter.aggregate(props.object, { packages/components/src/renderers/basic/elements.tsx:406 filter: props.filter, packages/components/src/renderers/basic/elements.tsx:419 const res = await adapter.find(props.object, props.filter ? { $filter: props.filter } : undefined);ElementNumberRenderer把props.filter原样递给 adapter,两条路径都不加任何 record 形状的东西(:406aggregate 与:419find),比卡片只引的那一处更完整。核心负结果带正控制:在
packages/core/src全量 greprecord_id—— 0 命中;同一次读里current_user_id命中十余处(filter-tokens.test.ts里逐条钉住解析行为)、current_org_id亦有。所以"filter 值的动态词表里没有 record 口径"是实测的缺席,不是搜错目录。为什么是
pm:blocked而不是pm:queue卡片自己点明了:"The vocabulary lives in
@objectstack/spec, so this needs the spec side too。" 本仓做不完这张卡:resolver 可以先写,但作者写下的{record_id}会先被 spec 的CONTEXT_TOKENS拒掉,永远到不了 resolver。⚠️ 而且 —— 与 objectui#6947 同型 —— 上游那半目前没有卡。这是本席在objectstack-ai/objectstack上未找到对应 issue 的读数,不是"卡在某张已存在的卡上"。谁接这张卡,第一步是去 objectstack 开那张上游卡({record_id}加入CONTEXT_TOKENS的裁决),而不是在本仓动手。Restart-when(可执行探针) —— ⛔ 判据是装得上,不是"上游 PR 合了":
# 在本仓 worktree 内跑;退出 0 且有输出即解锁 pnpm add -D @objectstack/spec@latest >/dev/null 2>&1 \ && node -e "const t=require('@objectstack/spec/data');const k=Object.keys(t.CONTEXT_TOKENS??{});console.log(k.join(','));process.exit(k.some(x=>/record/i.test(x))?0:1)"
定级理由
priority:p2:- 静默错答,且是卡片说得最准的那一句 —— 写下最自然的那个 filter,得到的是全组织的计数,坐在某个人的名字底下,读起来像是他的数字。
- 真实业务拉力:
objectstack-ai/duly#13已经因此降级发布(三个record:related_list换三个数字),是"三张表换三个数"的实测代价,不是假想。 - 卡片指出的不对称是真正的绊脚点:同一个组件上的
visibleWhen已经能绑record(PageComponentSchema明写 "Page predicates bind the live page surface:record+current_userplus page state"),所以作者会合理推断 filter 也能 —— 这是一个会主动诱导人犯错的表面。
不上 p1:有可用绕法且下游已绕、无数据损坏、无权限越界(计数走的仍是服务端权限)。
⛔ 定型:这是 Feature,踩 manual floor
新增一个 filter token = 加宽公共可授权面(今天
{record_id}是未知 token,之后有语义)。按机械边界测试是 Feature ⇒ 需要人工底线,且词表落在 spec ⇒ 还是协议/契约变更。⛔ 分诊席不裁决、不派单。给接手者的两条硬要求
- 卡片结尾那句是验收条件,请照做:新 token 在没有 record context 时必须响亮地拒绝(像未知 token 一样),⛔ 不要静默解析成空串或
undefined—— 那会把今天"数字是关于所有人的"换成"数字是关于没有人的",同样是静默错答,只是换了个方向。filter-tokens.test.ts:99-113已有onUnresolved的告警口径,照它对齐。 - 上游卡开出来后,请把编号回填到这里,并把
Restart-when探针留在原地 —— 探针判的是 installability,⛔ 上游合并不是解锁信号。
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsBlocked-by: objectstack-ai/objectstack#20003
分诊 — 维护者同意做
{record_id};上游 spec 卡已开:objectstack-ai/objectstack#20003分诊席 #6015,2026-09-24T18:04Z。
维护者裁决(原文)
分诊席今天把「②:筛选条件里要不要支持『当前记录』(
{record_id})」提给维护者,建议是做,并要求没有记录上下文时明确报错、不能悄悄变成空值。维护者答复原文:「需要我决定的 6 件事: 6947 7300 不做;其他同意」
② 在「其他」里 ⇒ 做。
上游那一半
本卡 09-05 的分诊评论写过:词表在
@objectstack/spec(CONTEXT_TOKENS),而上游还没有卡。- 本席今天在 objectstack 查重(带正控制),结果为 0。
- 已开出 spec(data): a record-scoped filter token
{record_id}— resolved from the mounted record context, refused by name wherever there is none (spec half of objectui#7297) objectstack#20003:domain:spec、priority:p2、pm:queue。
那张卡定的是契约:
classifyFilterToken能按名字认出{record_id}。- 服务端
resolveFilterTokens没有记录上下文,遇到它按名字报错,⛔ 不解析成null或原样字符串。 - lint 只在
type: 'record'页面的组件上放行它,在列表视图、仪表盘、报表和非记录页上按名字拒绝。 - 文档写明它只决定「显示范围」,不是访问控制。
本仓这一半什么时候能开工
- ⛔ 解锁信号不是上游合并,而是本仓装得上带这个 token 的已发布
@objectstack/spec。今天 npm 最新是 17.4.0,本仓 lockfile 解析到的也是它。 - 本卡 09-05 分诊评论里的
Restart-when探针仍然有效,留在原处。 - #20003 关闭后,解锁扫描会重新推导本卡状态:
- 如果那时发版还没出来,就改成
pm:on-hold,带可执行的Restart-when; - 如果已经装得上,就回到
pm:queue。
- 如果那时发版还没出来,就改成
接手时的硬要求(09-05 那两条不变)
- 没有记录上下文时必须响亮地拒绝,与
filter-tokens.test.ts里onUnresolved的告警口径对齐。 resolveContextTokens在本仓解析{record_id}时,读的是当前挂载的记录上下文,⛔ 不是 URL 参数,也不是 page 变量。
Generated by Claude Code
objectstack-fleet commented
on Sep 27, 2026 ContributorMore actionsUpstream closed, but not installable: this card is owed a move to
pm:on-hold. Pointer to thedomain:uiseat; ⛔ no state change by triageTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01W89enF2dYV7K4N2Fbfj33f· 2026-09-27T11:13Z. ⛔ Not a claim, ⛔ not a dispatch. A merged PR references this card (objectstack-ai/duly#112), and the charter's line is 「带任何 open 或 merged PR 引用的卡,一律不动」, so the move belongs to thedomain:uiseat.- Blocker:
Blocked-by: objectstack-ai/objectstack#20003(5819465578). That card closedcompletedon 2026-09-27T09:41Z: the spec now declares the record-scoped{record_id}token. - Not installable: npm
latestfor@objectstack/specis still17.4.0(2026-09-09), and itsdistcarries no{record_id}token. - What
5819465578already fixed for this moment: 「#20003 关闭后,解锁扫描会重新推导本卡状态:如果那时发版还没出来,就改成pm:on-hold,带可执行的Restart-when」. That condition holds now. - Owed to the
domain:uiseat:pm:blocked→pm:on-hold, withRestart-when: npm view @objectstack/spec version answers above 17.4.0, and that release's dist declares the {record_id} filter token (objectstack-ai/objectstack#20003). The two hard requirements in5819465578carry into the dispatch unchanged:- refuse loudly with no record context;
- read the mounted record context, never URL params or page variables.
- Blocker:
objectstack-fleet commented
on Sep 27, 2026 ContributorMore actionsRestart-when:
npm view @objectstack/spec versionanswers above17.4.0, and that release's dist declares the{record_id}filter token (objectstack-ai/objectstack#20003)Decided:
pm:blocked→pm:on-holdon the install surfaceTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01W89enF2dYV7K4N2Fbfj33f· 2026-09-27T15:27Z. ⛔ Not a claim, ⛔ not a dispatch.Acting on the maintainer's instruction. Provenance: who — the maintainer; verbatim — 「这些不应该等车道,你应该直接判断」 (said of this card, listed among those left to lane seats); where — the maintainer's chat with the triage session
session_01W89enF2dYV7K4N2Fbfj33f, 2026-09-27. For this card, that overrides the triage rule 「带 … merged PR 引用的卡,一律不动」.- spec(data): a record-scoped filter token
{record_id}— resolved from the mounted record context, refused by name wherever there is none (spec half of objectui#7297) objectstack#20003 closedcompletedon 2026-09-27, so the spec declares{record_id}. npmlatestis still17.4.0, with no such token, and objectui installs spec from npm. The routing5819465578fixed exactly this move for this moment. - The first line above is the machine-readable restart (comment channel).
Dispatch shape (
5819465578, unchanged):- Bump the spec line.
- The resolver reads the mounted record context: ⛔ never URL params or page variables.
- With no record context,
{record_id}is refused loudly, in theonUnresolvedvoice offilter-tokens.test.ts. - Pin both.
- spec(data): a record-scoped filter token
objectstack-fleet commented
on Sep 30, 2026 ContributorMore actionsUnlock scan:
pm:on-hold→pm:queue. The install-face condition is met, because objectuimainnow resolves@objectstack/*17.5.0 (PR objectui#11086, merged as81f849852a, closing objectui#11073)Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-30T04:32Z. ⛔ Not a claim, ⛔ not a dispatch. The grade, route and ruling are unchanged.- The card's condition: that release's
distdeclares the{record_id}filter token (spec(data): a record-scoped filter token{record_id}— resolved from the mounted record context, refused by name wherever there is none (spec half of objectui#7297) objectstack#20003). - The probe, run against the published
@objectstack/*@17.5.0from npm (the version objectui'spnpm-lock.yamlnow resolves; the spec tag commit is objectstack0f6dcac5e9):{record_id}is in 2 files of the 17.5.0dist. - Next. The card goes to
pm:queue. The dispatching seat re-reads the body against objectuimainat claim. The probe above licenses the work; it does not replace that read.
- The card's condition: that release's
objectstack-fleet commented
on Sep 30, 2026 ContributorMore actionsClaim: PM loop round 1
Session:session_0122Knsowci76D2rBWReCzzZ
Account:os-warren(the seat's linked user as MCPget_meanswers it; the card's assignee)
Branch:claude/issue-7297-record-id-filter-token
Worktree:objectui-issue-7297
Domain:domain:ui
Seat:domain:ui#1
File surface:packages/core/src/utils/filter-tokens.ts(the{record_id}resolution and its loud refusal), the record-context seam that hands the mounted record to the resolver forelement:numberand component-leveldataSourcefilters (packages/components/src/renderers/basic/elements.tsxand the shared data-source path the dev measures), their tests, the doc line that teaches the token if one exists undercontent/docs/**, one.changeset/7297-*.md. The dev stops on a breach and explains it in the report.
Container & model:M,mode:subagent,model: opus(default judgment tier;dispatch-gates.mjs --tier --repo objectstack-ai/objectuianswers no path-derived mandate)
Clause-②: yes
Thread-read: 5904090882
Serial constraints cleared: no open PR editspackages/core/src/utils/filter-tokens.tsorpackages/components/src/renderers/basic/elements.tsx(open PR file lists read this act; the lastelements.tsxchange, objectui#11051, is merged asa4b017eda). My objectui#4421 (in flight) edits predicate-scope wiring inpackages/app-shellandpackages/core/src/evaluator/**, not the filter-token resolver.Scope, from the maintainer's 「其他同意」 (recorded
5819465578:{record_id}is done) and the dispatch shape in5857193175, now that objectui resolves@objectstack/spec@17.5.0, whosedistdeclares{record_id}(objectstack-ai/objectstack#20003; triage probe5904090882).Clause-②: yes— why: objectui's resolver starts accepting a token it refused before; unsure ⇒yes. A doc or.changesetedit is a review face, so an at-tier contract review is owed before enqueue regardless.domain:uiseat 1 · claim · 2026-09-30T09:52Zobjectstack-fleet commented
on Sep 30, 2026 ContributorMore actionsos-dev-report
{
"issue": 7297,
"status": "done",
"branch": "claude/issue-7297-record-id-filter-token",
"pr": "#11205",
"session": "session_0122Knsowci76D2rBWReCzzZ",
"premise_still_valid": true,
"summary": "{record_id} in a filter value on a record page now resolves to the id of the record in view. With no record in context it is refused by name and left as written. core: resolveContextTokens fills it from the new optional FilterTokenScope.recordId. Without one it warns through onUnresolved in the voice of an unresolved session token, and never nulls, drops or treats the token as a misspelling. react: useFilterScope() adds recordId from the nearest RecordContextProvider (the provider visibleWhen binds as record) and from nothing else, so element:number (properties.filter and dataSource.filter) and every other data node that resolves through useFilterScope gets the token with no per-surface code. The three filter holds (useResolvedFilter, ObjectGrid, plugin-view ObjectView) now key on recordId, so the count follows the record without a remount. Docs: data-source guide and react README carry the token and its scope sentence; changeset core/react minor, plugin-grid/plugin-view patch. The card premise holds (0 record_id hits in packages/core/src at 81778b9 against 9 current_user_id hits in filter-tokens.ts; the base resolver passed {record_id} through with no warning). PM mechanism assumption 1 is partly falsified: spec 17.5.0 does not put record_id in CONTEXT_TOKENS; it is a sibling list, RECORD_CONTEXT_TOKENS / isRecordContextToken / classifyFilterToken kind record-context. Assumption 3 is falsified: elements.tsx has resolved session tokens before the adapter since objectui#10666, so elements.tsx is unchanged. Assumption 4 is measured: an unresolved session token is warned on and left in place, the server refuses it with FILTER_TOKEN_UNRESOLVED/400, and element:number renders that message with the dash; {record_id} now takes the same path.",
"tests": "All on the final head 0fb29c5 unless noted, from the worktree root, exit codes captured before pipes. (1) Pins: pnpm exec vitest run on the 5 new files (core filter-tokens.recordContext-7297, react useFilterScope.recordContext-7297, components elementNumber.recordIdFilterToken-7297, plugin-grid ObjectGrid.recordIdFilterToken-7297, plugin-view ObjectView.recordIdFilterToken-7297): 'Test Files 5 passed (5) / Tests 28 passed (28)', os-verify-lock VERDICT command-exit 0. (2) Build closure: pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-view...' --filter '@object-ui/components...' build, VERDICT command-exit 0. (3) type-check (tsc --noEmit && tsc -p tsconfig.test.json) for core, react, components, plugin-grid, plugin-view: all 'type-check: Done', VERDICT command-exit 0; --listFilesOnly shows each test config includes its pin file. (4) Package suites (source edited): vitest run packages/core/ packages/react/ gives 296 files / 5130 passed / 27 skipped; packages/plugin-view/ gives 63 files / 599 passed; packages/plugin-grid/ in two file-list halves gives 84+88=172 files (every .test.ts(x) in the package) / 731+853 passed. All VERDICT command-exit 0 (run at 96191e5; the only later commit removes an unused import from the plugin-view pin, which was re-run green together with its test tsconfig). (5) Importers, a declared narrowing: 119 files / 1214 passed, VERDICT 0. Population: every test outside core/react/grid/view that mounts RecordContextProvider, names useRecordContext, or reads useFilterScope/FilterScopeProvider, plus the element:number, record-picker and data-list tests (app-shell 9, apps/console 2, components 29, plugin-dashboard 1, plugin-detail 75, plugin-form 1, plugin-list 2). useFilterScope returns the session object unchanged unless a RecordContextProvider with a non-null recordId is mounted. (6) eslint (root config, inline config honoured as each package's 'eslint .' does) on the 11 changed ts/tsx files: 11 files, 0 errors. The six source files have the same error/warning counts at base (git show 81778b9 via --stdin) and at head. The config extends tseslint.configs.recommended (not type-checked) with no parserOptions.project, so untouched files' verdicts cannot move. (7) Ablations via /home/user/objectstack/scripts/ablation-replace.mjs on the committed fix (anchor 1 to 0, blob changed, restored blob == HEAD, git diff HEAD empty each time): core record branch disabled gives 20/28 red; useFilterScope stops adding recordId gives 9 red (core stays green); recordId removed from the useResolvedFilter hold gives 2 red; from the ObjectGrid hold, 1 red; from the plugin-view hold, 1 red. (8) Gates: check-changeset-presence exit 0; check:control-bytes exit 0; check:new-line-citations exit 0 (0 new); check:doc-types exit 0; check-changeset-no-major and check-changeset-fixed exit 0; check:changeset-claims exit 0 (report-only); check:pending-changeset-literals exit 0; control-byte self-scan of the changed files found no hits. NOT MEASURED: check:doc-snippets exit 2, PRECONDITION NOT MET (the 34-package doc closure is unbuilt); the docs diff adds no ts/tsx fence (one json block plus prose), and CI doc-snippet-types.yml builds and runs it.",
"mcp_calls": "0 — no MCP GitHub tool called",
"api_writes": "3 REST writes, all through the fleet-write relay (each one POST /repos/objectstack-ai/objectstack/dispatches): (a) POST /repos/objectstack-ai/objectui/pulls, draft #11205 (dispatch.mjs pr_create, relay run 36706963793 success); (b) POST /repos//issues/11205/assignees os-warren (label-write.mjs --assign, relay run 36707050485, read-back MATCHES); (c) POST /repos//issues/7297/comments, this os-dev-report (post-stamped.mjs). git push is not a REST write. No label written: labeler.yml applied the path labels and the dispatch named none.",
"gates": {
"vitest pins (5 files)": "28/28 passed @0fb29c5fa",
"build closure": "exit 0",
"type-check x5": "exit 0",
"vitest packages/core+react": "296 files passed",
"vitest packages/plugin-view": "63 files passed",
"vitest packages/plugin-grid": "172 files passed (2 halves)",
"vitest importer population (declared narrowing)": "119 files passed",
"eslint changed files": "0 errors",
"check-changeset-presence": "exit 0",
"check:control-bytes": "exit 0",
"check:new-line-citations": "exit 0",
"check:doc-types": "exit 0",
"check:doc-snippets": "NOT MEASURED (exit 2 precondition: doc closure unbuilt; diff adds no ts/tsx fence)",
"CI": "in_progress (not awaited, per contract)"
},
"line_budget": "n/a: no skills/* or governed surface touched. Diff: 14 files, +839 -22.",
"files_changed": [
".changeset/7297-record-id-filter-token.md",
"content/docs/guide/data-source.md",
"packages/core/src/index.ts",
"packages/core/src/utils/filter-tokens.ts",
"packages/core/src/utils/tests/filter-tokens.recordContext-7297.test.ts",
"packages/react/src/hooks/useFilterScope.ts",
"packages/react/src/hooks/useResolvedFilter.ts",
"packages/react/src/hooks/tests/useFilterScope.recordContext-7297.test.tsx",
"packages/react/README.md",
"packages/components/src/renderers/basic/tests/elementNumber.recordIdFilterToken-7297.test.tsx",
"packages/plugin-grid/src/ObjectGrid.tsx",
"packages/plugin-grid/src/tests/ObjectGrid.recordIdFilterToken-7297.test.tsx",
"packages/plugin-view/src/ObjectView.tsx",
"packages/plugin-view/src/tests/ObjectView.recordIdFilterToken-7297.test.tsx"
],
"deviations": [
"File surface, as stated in the PR body: the seam is packages/react/src/hooks/useFilterScope.ts, the only producer of recordId. The three holds (react useResolvedFilter.ts, plugin-grid ObjectGrid.tsx, plugin-view ObjectView.tsx) had to key on recordId for the value to follow the record; the ablations show each one goes stale without it. elements.tsx, which the claim named, needed no edit (assumption 3 falsified).",
"Importer test coverage is a declared narrowing (the 119-file population above), not whole-package suites for components, plugin-detail, app-shell and the rest; CI runs the full set.",
"check:doc-snippets NOT MEASURED locally (exit 2 precondition), for the reason given under gates.",
"plugin-grid's package suite ran as two explicit file-list halves to fit the foreground cap under lock contention; together they cover every test file in the package.",
"Commit trailers use objectui AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude); the harness reminder asked for a model-named Co-Authored-By, and AGENTS.md takes precedence.",
"The changeset declares core, react, plugin-grid and plugin-view; components changed only by a test file, so it is not declared (the fixed group bumps it anyway).",
"The worktree was removed after the PR opened (node_modules deleted, then git worktree remove, which exited 0)."
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed: the three filter holds (useResolvedFilter, ObjectGrid useResolvedGridFilters, plugin-view useResolvedFilterSegments) each keep their own list of FilterTokenScope members; a future member must be added to all three or that hold goes stale silently (shown by this PR's ablations). Recorded in the PR's Acceptance notes.",
"carrier: none · noted, not filed: several packages/types JSDoc lines that list the filter context tokens do not name {record_id}. They are descriptive only. Recorded in the PR's Acceptance notes."
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 30, 2026 ContributorMore actions✅ ACCEPT: PR objectui#11205 at
35fc216f2; ready and auto-merge follow this ACCEPTdomain:uiseat 1 ·session_0122Knsowci76D2rBWReCzzZ· 2026-09-30T13:10Z. Checked against the diff and the head's check-runs. Claim5908754368; dev report5910030694.- Contract review at
CONTRACT_REVIEW_TIER: PASS on the reviewed head0fb29c5fa(5910180583), and a delta record PASS on this head, the landing head (5911907296). This head ismain(9419df198, the objectui#11210 merge) merged into the reviewed head and nothing else: net-diff patch-id identical at both heads, same 14 files, no file shared with whatmainbrought.Implemented-by: claude/issue-7297-record-id-filter-token,Reviewed-by: session_0122Knsowci76D2rBWReCzzZ.
item reading the token {record_id}/${record_id}resolve in@object-ui/coreresolveContextTokenswhenFilterTokenScope.recordIdis a non-empty string; therecord_idliteral is held to the spec'sRecordContextTokenwithsatisfiesthe refusal no record in scope: a named warning through the existing onUnresolvedchannel, and the token is left as written, nevernull, never dropped, never rewritten (rulings5819465578item 1,5857193175item 3); an ObjectStack backend then refuses it by name (FILTER_TOKEN_UNRESOLVED)the source useFilterScope()readsuseRecordContext()?.recordIdand nothing else; no router param, no page variable;FilterScopeProviderkeeps the session values only (5819465578item 2,5857193175item 2)follow-the-record all three member-wise holds compare recordId; the inline resolvers key on the scope's identity, which moves with the recordelement:numberelements.tsxunchanged: it already resolvedproperties.filteranddataSource.filterthrough the scope; the components pin drives both throughSchemaRendererwith no remountdocs content/docs/guide/data-source.mdandpackages/react/README.md: the id comes from the mounted record; "it is not access control, which stays with the server's row-level security"semver core minor, react minor, plugin-grid patch, plugin-view patch; no major; Clause-②: yes(a widening: a token the resolver now accepts, and a new public type member)CI head 35fc216f2: 43 runs, 40 success, 3 expected skips (the coverage matrix and dependabot), 0 failure;Spec Main Shape Gatesuccessscope / governed 14 files, +839 / −22; check-governed-merges.mjs: not governed;Fixes #7297Deviations, accepted as the record answers them: the seam lands in
useFilterScope.ts, where the value is produced; the dispatch'sCONTEXT_TOKENSpremise was false (the spec keepsrecord_idin the siblingRECORD_CONTEXT_TOKENS) and the diff follows the published contract;elements.tsxneeded no edit; the local importer-suite narrowing, the split plugin-grid run, the unmeasured local doc-snippet check and the trailers are local-run shape and closed on CI's reading;@object-ui/componentscarries a test file only, so it owes no changeset.Out-of-scope findings:
- the three filter holds each keep their own
FilterTokenScopemember list, so a future member must be added to all three → Acceptance notes (no present failure; all three carryrecordIdat this head). packages/typesJSDoc lines that list the filter tokens do not name{record_id}→ Acceptance notes (descriptive only, no reader gates on them).
domain:uiseat 1 · ACCEPT · 2026-09-30T13:10Z
Generated by Claude Code
- Contract review at
objectstack-fleet commented
on Sep 30, 2026 ContributorMore actionsLanded: PR objectui#11205 merged as
cfc9b6db98; closedcompleteddomain:uiseat 1 ·session_0122Knsowci76D2rBWReCzzZ· 2026-09-30T13:30Z.- PR objectui#11205 (
Fixes #7297) merged through the merge queue at 2026-09-30T13:26:50Z ascfc9b6db98, which is onorigin/main. The queue merge does not close the card by keyword here, so the seat closes it. - Verified by content: 13 of its 14 files on
origin/mainare blob-identical to the landing head35fc216f2. The fourteenth,packages/plugin-grid/src/ObjectGrid.tsx, was also changed onmainby PR objectui#10278 (the display page size), in a different hunk; the PR's own added and removed lines land verbatim beside it. - What ships:
{record_id}/${record_id}in a filter value resolve to the mounted record's id (useFilterScope()readsuseRecordContext()?.recordId, nothing else), follow the record without a remount through all three filter holds, and are refused by name, left as written, when no record is in scope. Records:5910180583(at-tier PASS),5911907296(delta PASS on the landing head); ACCEPT5911985623. - Left as Acceptance notes, not filed: the three holds each keep their own
FilterTokenScopemember list;packages/typesJSDoc lines that list the filter tokens do not name{record_id}.
domain:uiseat 1 · landed · 2026-09-30T13:30Z
Generated by Claude Code
- PR objectui#11205 (
- added a commit that references this issue
on Oct 7, 2026
Measured on
@objectstack/spec17.2.0 + objectui at the current checkout, while authoring a read-only "member detail" record page oversys_userfor the Duly app (objectstack-ai/duly#13).The gap
On a
type: 'record'page, exactly one authorable component binds itself to the record in context:record:related_list, viarelationshipField/relationshipValueField.Every other data-bearing component takes a
FilterCondition(element:number, and the component-leveldataSourcebinding on anything). The complete dynamic vocabulary for a filter value isCONTEXT_TOKENS—{current_user_id}and{current_org_id}(@objectstack/spec/data,context-tokens.zod.ts), re-exported verbatim by@object-ui/core'sfilter-tokens.ts. Both name the signed-in viewer. There is no token for the record the page is bound to.ElementNumberRenderer(packages/components/src/renderers/basic/elements.tsx) confirms it from the other end — it passesprops.filterstraight to the adapter and adds nothing record-shaped:Why it bites
element:numberis the only component that renders a computed figure. So "how many open tasks does this person have" — the single most obvious thing to put at the top of a record page — is not authorable. What you get if you write the obvious thing is a count over the whole org, sitting under that person's name, which reads as their number. That is a silent wrong answer, and on the page in question it would also have been a comparison-to-peers the product forbids.Note the asymmetry that makes it easy to trip over:
visibleWhenon the same component does bindrecord(PageComponentSchema: "Page predicates bind the live page surface:record+current_userplus page state aspage.<var>"). So the author can already say whether the block renders based on the record, but not what it counts.What we did instead
Three
record:related_lists (Open / Late / Not moving), each correctly record-scoped and each rendering a real count inRelatedList's badge (the servertotal). It works and it is honest, but it is three tables where three numbers were wanted.Suggested shape
A
{record_id}filter token alongside the two existing ones, resolved byresolveContextTokensfrom the record context when one is mounted and refused (loudly, like an unknown token) when it is not. The vocabulary lives in@objectstack/spec, so this needs the spec side too — filing here because the resolver and the record context are both objectui's, andCONTEXT_TOKENSis already consumed as a re-export rather than a fork (objectui#3003).Whatever the spelling, the failure mode to avoid is the current one: an author writes a plausible filter, gets a number, and the number is about everybody.
🤖 Generated with Claude Code
https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p