You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
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
Measured on @objectstack/spec 17.2.0 + objectui at the current checkout, while authoring a read-only "member detail" record page over sys_user for 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, via relationshipField / relationshipValueField.
Every other data-bearing component takes a FilterCondition (element:number, and the component-level dataSource binding on anything). The complete dynamic vocabulary for a filter value is CONTEXT_TOKENS — {current_user_id} and {current_org_id} (@objectstack/spec/data, context-tokens.zod.ts), re-exported verbatim by @object-ui/core's filter-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 passes props.filter straight to the adapter and adds nothing record-shaped:
element:number is 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: visibleWhen on the same componentdoes bind record (PageComponentSchema: "Page predicates bind the live page surface: record + current_user plus page state as page.<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 in RelatedList's badge (the server total). 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 by resolveContextTokens from 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, and CONTEXT_TOKENS is 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.
卡片自己点明了:"The vocabulary lives in @objectstack/spec, so this needs the spec side too。" 本仓做不完这张卡:resolver 可以先写,但作者写下的 {record_id} 会先被 spec 的 CONTEXT_TOKENS 拒掉,永远到不了 resolver。
Upstream closed, but not installable: this card is owed a move to pm:on-hold. Pointer to the domain:ui seat; ⛔ no state change by triage
Triage 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 the domain:ui seat.
Blocker:Blocked-by: objectstack-ai/objectstack#20003 (5819465578). That card closed completed on 2026-09-27T09:41Z: the spec now declares the record-scoped {record_id} token.
Not installable: npm latest for @objectstack/spec is still 17.4.0 (2026-09-09), and its dist carries no {record_id} token.
What 5819465578 already fixed for this moment: 「#20003 关闭后,解锁扫描会重新推导本卡状态:如果那时发版还没出来,就改成 pm:on-hold,带可执行的 Restart-when」. That condition holds now.
Owed to the domain:ui seat:pm:blocked → pm:on-hold, with Restart-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 in 5819465578 carry into the dispatch unchanged:
refuse loudly with no record context;
read the mounted record context, never URL params or page variables.
Restart-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)
Decided: pm:blocked → pm:on-hold on the install surface
Triage 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 引用的卡,一律不动」.
Unlock scan: pm:on-hold → pm:queue. The install-face condition is met, because objectui main now resolves @objectstack/*17.5.0 (PR objectui#11086, merged as 81f849852a, 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 probe, run against the published @objectstack/*@17.5.0 from npm (the version objectui's pnpm-lock.yaml now resolves; the spec tag commit is objectstack 0f6dcac5e9): {record_id} is in 2 files of the 17.5.0 dist.
Next. The card goes to pm:queue. The dispatching seat re-reads the body against objectui main at claim. The probe above licenses the work; it does not replace that read.
Claim: PM loop round 1
Session: session_0122Knsowci76D2rBWReCzzZ
Account: os-warren (the seat's linked user as MCP get_me answers 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 for element:number and component-level dataSource filters (packages/components/src/renderers/basic/elements.tsx and the shared data-source path the dev measures), their tests, the doc line that teaches the token if one exists under content/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/objectui answers no path-derived mandate)
Clause-②: yes
Thread-read: 5904090882
Serial constraints cleared: no open PR edits packages/core/src/utils/filter-tokens.ts or packages/components/src/renderers/basic/elements.tsx (open PR file lists read this act; the last elements.tsx change, objectui#11051, is merged as a4b017eda). My objectui#4421 (in flight) edits predicate-scope wiring in packages/app-shell and packages/core/src/evaluator/**, not the filter-token resolver.
Scope, from the maintainer's 「其他同意」 (recorded 5819465578: {record_id} is done) and the dispatch shape in 5857193175, now that objectui resolves @objectstack/spec@17.5.0, whose dist declares {record_id} (objectstack-ai/objectstack#20003; triage probe 5904090882). Clause-②: yes — why: objectui's resolver starts accepting a token it refused before; unsure ⇒ yes. A doc or .changeset edit is a review face, so an at-tier contract review is owed before enqueue regardless.
os-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."
]
}
✅ ACCEPT: PR objectui#11205 at 35fc216f2; ready and auto-merge follow this ACCEPT
domain:ui seat 1 · session_0122Knsowci76D2rBWReCzzZ · 2026-09-30T13:10Z. Checked against the diff and the head's check-runs. Claim 5908754368; dev report 5910030694.
Contract review at CONTRACT_REVIEW_TIER: PASS on the reviewed head 0fb29c5fa (5910180583), and a delta record PASS on this head, the landing head (5911907296). This head is main (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 what main brought. 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/coreresolveContextTokens when FilterTokenScope.recordId is a non-empty string; the record_id literal is held to the spec's RecordContextToken with satisfies
the refusal
no record in scope: a named warning through the existing onUnresolved channel, and the token is left as written, never null, never dropped, never rewritten (rulings 5819465578 item 1, 5857193175 item 3); an ObjectStack backend then refuses it by name (FILTER_TOKEN_UNRESOLVED)
the source
useFilterScope() reads useRecordContext()?.recordId and nothing else; no router param, no page variable; FilterScopeProvider keeps the session values only (5819465578 item 2, 5857193175 item 2)
follow-the-record
all three member-wise holds compare recordId; the inline resolvers key on the scope's identity, which moves with the record
element:number
elements.tsx unchanged: it already resolved properties.filter and dataSource.filter through the scope; the components pin drives both through SchemaRenderer with no remount
docs
content/docs/guide/data-source.md and packages/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 Gatesuccess
scope / governed
14 files, +839 / −22; check-governed-merges.mjs: not governed; Fixes #7297
Deviations, accepted as the record answers them: the seam lands in useFilterScope.ts, where the value is produced; the dispatch's CONTEXT_TOKENS premise was false (the spec keeps record_id in the sibling RECORD_CONTEXT_TOKENS) and the diff follows the published contract; elements.tsx needed 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/components carries a test file only, so it owes no changeset.
Out-of-scope findings:
the three filter holds each keep their own FilterTokenScope member list, so a future member must be added to all three → Acceptance notes (no present failure; all three carry recordId at this head).
packages/types JSDoc lines that list the filter tokens do not name {record_id} → Acceptance notes (descriptive only, no reader gates on them).
PR objectui#11205 (Fixes #7297) merged through the merge queue at 2026-09-30T13:26:50Z as cfc9b6db98, which is on origin/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/main are blob-identical to the landing head 35fc216f2. The fourteenth, packages/plugin-grid/src/ObjectGrid.tsx, was also changed on main by 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() reads useRecordContext()?.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); ACCEPT 5911985623.
Left as Acceptance notes, not filed: the three holds each keep their own FilterTokenScope member list; packages/types JSDoc lines that list the filter tokens do not name {record_id}.
Activity
os-zhuang commentedon 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上复核的锚点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。objectstack-ai/objectstack上未找到对应 issue 的读数,不是"卡在某张已存在的卡上"。谁接这张卡,第一步是去 objectstack 开那张上游卡({record_id}加入CONTEXT_TOKENS的裁决),而不是在本仓动手。Restart-when(可执行探针) —— ⛔ 判据是装得上,不是"上游 PR 合了":
定级理由
priority:p2: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 ⇒ 还是协议/契约变更。⛔ 分诊席不裁决、不派单。给接手者的两条硬要求
undefined—— 那会把今天"数字是关于所有人的"换成"数字是关于没有人的",同样是静默错答,只是换了个方向。filter-tokens.test.ts:99-113已有onUnresolved的告警口径,照它对齐。Restart-when探针留在原地 —— 探针判的是 installability,⛔ 上游合并不是解锁信号。Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
Blocked-by: objectstack-ai/objectstack#20003
分诊 — 维护者同意做
{record_id};上游 spec 卡已开:objectstack-ai/objectstack#20003分诊席 #6015,2026-09-24T18:04Z。
维护者裁决(原文)
分诊席今天把「②:筛选条件里要不要支持『当前记录』(
{record_id})」提给维护者,建议是做,并要求没有记录上下文时明确报错、不能悄悄变成空值。维护者答复原文:② 在「其他」里 ⇒ 做。
上游那一半
本卡 09-05 的分诊评论写过:词表在
@objectstack/spec(CONTEXT_TOKENS),而上游还没有卡。{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或原样字符串。type: 'record'页面的组件上放行它,在列表视图、仪表盘、报表和非记录页上按名字拒绝。本仓这一半什么时候能开工
@objectstack/spec。今天 npm 最新是 17.4.0,本仓 lockfile 解析到的也是它。Restart-when探针仍然有效,留在原处。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 commentedon Sep 27, 2026
Upstream 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.Blocked-by: objectstack-ai/objectstack#20003(5819465578). That card closedcompletedon 2026-09-27T09:41Z: the spec now declares the record-scoped{record_id}token.latestfor@objectstack/specis still17.4.0(2026-09-09), and itsdistcarries no{record_id}token.5819465578already fixed for this moment: 「#20003 关闭后,解锁扫描会重新推导本卡状态:如果那时发版还没出来,就改成pm:on-hold,带可执行的Restart-when」. That condition holds now.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:objectstack-fleet commentedon Sep 27, 2026
Restart-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 引用的卡,一律不动」.{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.Dispatch shape (
5819465578, unchanged):{record_id}is refused loudly, in theonUnresolvedvoice offilter-tokens.test.ts.objectstack-fleet commentedon Sep 30, 2026
Unlock 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.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).@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.pm:queue. The dispatching seat re-reads the body against objectuimainat claim. The probe above licenses the work; it does not replace that read.objectstack-fleet commentedon Sep 30, 2026
Claim: PM loop round 1
Session:
session_0122Knsowci76D2rBWReCzzZAccount:
os-warren(the seat's linked user as MCPget_meanswers it; the card's assignee)Branch:
claude/issue-7297-record-id-filter-tokenWorktree:
objectui-issue-7297Domain:
domain:uiSeat:
domain:ui#1File 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 edits
packages/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 commentedon Sep 30, 2026
os-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 commentedon Sep 30, 2026
✅ 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_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.{record_id}/${record_id}resolve in@object-ui/coreresolveContextTokenswhenFilterTokenScope.recordIdis a non-empty string; therecord_idliteral is held to the spec'sRecordContextTokenwithsatisfiesonUnresolvedchannel, 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)useFilterScope()readsuseRecordContext()?.recordIdand nothing else; no router param, no page variable;FilterScopeProviderkeeps the session values only (5819465578item 2,5857193175item 2)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 remountcontent/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"Clause-②: yes(a widening: a token the resolver now accepts, and a new public type member)35fc216f2: 43 runs, 40 success, 3 expected skips (the coverage matrix and dependabot), 0 failure;Spec Main Shape Gatesuccesscheck-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:
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:10ZGenerated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
Landed: PR objectui#11205 merged as
cfc9b6db98; closedcompleteddomain:uiseat 1 ·session_0122Knsowci76D2rBWReCzzZ· 2026-09-30T13:30Z.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.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.{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.FilterTokenScopemember list;packages/typesJSDoc lines that list the filter tokens do not name{record_id}.domain:uiseat 1 · landed · 2026-09-30T13:30ZGenerated by Claude Code
feat(core,react): {record_id} in filter values resolves to the mounte…