Repository navigation
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) #20003
Description
Activity
- addedpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 24, 2026 objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsClaim: PM loop round 1
Session:session_01CiCTczDo7tGhafXjf61dUJ
Account:os-sales(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-20003-record-id-filter-token
Worktree:objectstack-issue-20003
Domain:domain:spec
Seat:domain:spec#4(seat post #18917)
File surface:packages/spec/src/data/context-tokens.zod.ts(the token vocabulary,classifyFilterToken, the module header docblock that generates the reference page) and its tests;packages/core/src/utils/filter-tokens.ts(resolveFilterTokens: the named refusal) and its tests;packages/lint/src/validate-filter-tokens.ts(andpackages/lint/src/filter-walk.tsonly if a surface kind must be exposed) and their tests; refusal pins on the ObjectQL read and write paths and the analytics dataset executor (test files only underpackages/objectql/andpackages/services/service-analytics/); the error-code ledger entry only if a new code is owed; the generated artefacts that follow (content/docs/references/data/context-tokens.mdx, JSON Schema, api-surface), regenerated and never hand-edited;.changeset/. ⛔ Notpackages/objectql/src/engine.ts(thedomain:engineclaim on #20148 holds its aggregation filter loop). Cross-domain surfaces (packages/core, service-analytics tests) are declared here because the card itself namesresolveFilterTokensas the server refusal (stop on breach; explain in the report)
Container & model:M,mode:subagent,model: default judgment tier(dispatch-gates.mjs --tierat2bbebf5: 「no path-derived mandate … floor · default · ceiling」 plus a clause-② SUSPECT hint oncontext-tokens.zod.ts; the default slot taken, the refusal shape per surface carries judgment)
Clause-②: yes
Thread-read: none
Serial constraints cleared: read at 2026-09-27T04:10Z. Open-PR census (7 open PRs besides the release PR, 77 file rows, lit control 30packages/specrows): none touchescontext-tokens.zod.ts,filter-tokens.ts,validate-filter-tokens.ts,filter-walk.tsorcontext-tokens.mdx. In-flight claims read from theirClaim:comments (seat 1: #19977, #19867, #19856, #19543, #17321, #17158; seat 2: #19965, #19870, #20051, #20105, #20078; seat 5: #19938, #19886, #19731, #18459, #17707; other lanes: #20153, #20148, #20136, #20113, #20055, #19879) name none of these files. #20078 editspackages/lint/src/validate-expressions.ts, a different file. #20148 (domain:engine) callsresolveFilterTokensfrom the aggregation loop inengine.ts, which this card does not edit. The serial predecessor PR #19818 onfilter-walk.tslanded as66960564d9.Ruling carried: the maintainer's answer quoted in the card body, 「需要我决定的 6 件事: 6947 7300 不做;其他同意」 (objectui#7297 among 「其他」), recommendation as ruled: do
{record_id}; with no record context it fails with a clear error, never an empty value.objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20003,
"status": "done",
"branch": "claude/issue-20003-record-id-filter-token",
"pr": "#20180",
"session": "session_01CiCTczDo7tGhafXjf61dUJ — this run is a subagent under the PM session; the Claim: comment 5852526924 names this branch (verified before any code)",
"head": "251c5f49b",
"premise_still_valid": true,
"summary": "{record_id} is a new spec vocabulary, RECORD_CONTEXT_TOKENS (a sibling of CONTEXT_TOKENS, not a member), with RecordContextToken and isRecordContextToken. classifyFilterToken returns a new kind, record-context, instead of unknown. Near misses ({recordid}, {recordId}, {record-id}, {record.id}, {current_record_id}) are still refused, now with a record_id suggestion. On every server path, resolveFilterTokens refuses it by name as UnresolvedFilterTokenError (FILTER_TOKEN_UNRESOLVED / 400, token record_id) whatever the request carries. That covers the ObjectQL read path, the write path (update and delete, multi and by id) and the analytics query door and dataset executor; each is pinned by a test that asserts code and status and that the driver or SQL was never reached. The lint rule validate-filter-tokens accepts it on any filter on a type record page and refuses it on every other surface with no record in context on this surface: views, object list views and field filters, dashboard widgets, reports, datasets, app nav, and home/app/utility/list pages. The module header (which generates context-tokens.mdx), the regenerated api-surface/export-origins and a changeset (spec, core and lint minor) complete the card. Mechanism readings: (1) the write path does go through resolveFilterTokens (withResolvedWhere); (2) no new error code was needed, because the branch-information test found no consumer branching on either filter-token code (objectstack non-test: comments only; objectui at a2f361b: 0 hits), so the existing FILTER_TOKEN_UNRESOLVED member was widened; (3) filter-walk.ts cannot tell record pages apart (its visitor gets only value/path/where), so filter-walk.ts is unchanged and the rule walks pages itself through the walk-exported scanForFilters, with a parity test against walkAuthoredFilters; (4) the .mdx was regenerated from the docblock, never hand-edited; (5) no liveness row covers the token vocabulary. isKnownFilterToken(record_id) stays false, so the flow hand-off (service-automation) is byte-identical. engine.ts is untouched.",
"tests": "All at 251c5f4. Full package test scripts: spec 540 files / 15820 passed + 2 todo; core 53 / 1363; lint 108 / 4163; objectql 316 / 5591; service-analytics 128 / 3022; service-automation 145 / 1729; every VERDICT command-exit 0. typecheck is green for spec, core, lint, objectql, service-analytics and service-automation (service-automation first failed on unbuilt test deps service-messaging, service-job and plugin-security; after building its closure it was green). Reverse type probe: planting RecordContextToken = current_user_id in lint gave tsc TS2322 (current_user_id not assignable to record_id); restored, git diff HEAD empty. Core ablation (ablation-replace anchor 1 to 0, marker in packages/core/dist per ablation-dist-preflight): the new pins went red (core 6 of 51, objectql 6 of 34, service-analytics 4 of 24), and the resolver drift guard threw UnknownFilterTokenError instead. Restore: rebuilt, --absent passed on all 14 dist files, tree clean, objectql 34/34. Lint ablations: record-page judgement 5 red; refusal branch 12 red; both restored with blob equal to HEAD. ESLint narrowed to the 9 changed TS files (--no-inline-config --format json): 9 files, 0 errors, 0 warnings, each with a resolved config; eslint.config.mjs has no type-aware linting (its lines 327-328), so no untouched file verdict can move. CLI unit project: 172 files / 2407 tests passed; 52 files NOT MEASURED (MODULE_NOT_FOUND: plugin-email, verify and create-objectstack not built here); the integration layer (authoring-rule-command-parity) is declared to CI. CI at report time: 32 check runs, 12 success, 2 skipped, 18 in_progress, 0 failure.",
"gates": {
"derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 251c5f4: 113 commands",
"reconciliation": "node scripts/pm/dispatch-gates.mjs --ran ran.list: exit 0; 113 derived, 111 run, 2 NOT-MEASURED, 0 UNRUN",
"exit_nonzero": [
"pnpm check:dual-build-cjs-loads :: exit 3",
"pnpm check:type-check-debt :: exit 3"
],
"exit_zero_count": 111,
"reruns": [
"node scripts/check-engine-split-ratio.mjs --days 90: first exit 2 (shallow history inside the window); after git fetch --shallow-since=2026-06-22 origin main, exit 0",
"pnpm --filter @objectstack/spec run check:skill-examples: first exit 3 (client-react dist absent); after building the client-react closure, exit 0"
],
"not_measured": [
"pnpm check:dual-build-cjs-loads :: exit 3 PREREQUISITE NOT MET (needs the whole workspace built)",
"pnpm check:type-check-debt :: exit 3 PREREQUISITE NOT MET (needs the whole workspace built; its DEBT rows are cloud-connection, hono, observability and the root, none in this diff)"
],
"changeset_dry_run": "GITHUB_EVENT_NAME=pull_request node scripts/check-changeset-no-major.mjs --base origin/main --event (a synthetic body carrying Clause-②: yes) :: exit 0"
},
"line_budget": "n/a: no skills/** or governed surface touched. Diff 13 files, +886 / -40 (926 changed lines, under the 5000 human-merge threshold).",
"files_changed": [
".changeset/20003-record-id-filter-token.md",
"content/docs/references/data/context-tokens.mdx (generated)",
"packages/core/src/utils/filter-tokens.ts",
"packages/core/src/utils/filter-tokens.test.ts",
"packages/lint/src/validate-filter-tokens.ts",
"packages/lint/src/validate-filter-tokens.test.ts",
"packages/objectql/src/engine-filter-tokens.test.ts",
"packages/services/service-analytics/src/tests/dataset-filter-tokens.test.ts",
"packages/services/service-analytics/src/tests/query-filter-tokens.test.ts",
"packages/spec/api-surface/data.json (generated)",
"packages/spec/export-origins/data.json (generated)",
"packages/spec/src/data/context-tokens.zod.ts",
"packages/spec/src/data/context-tokens.test.ts"
],
"deviations": [
"The branch was not merged with origin/main before the PR (AGENTS.md multi-agent §10). It is 2 commits behind e2c4e12; those commits touch packages/core/src/security/** and packages/spec/src/automation/** plus their artifacts, disjoint from this diff. The CI merge ref validates the joint tree.",
"The claimed file surface was respected, with no breach: filter-walk.ts, lint index.ts, engine.ts and the error-code ledger are untouched. Changes to the objectql and service-analytics packages are test files only.",
"Consumer measurement read objectui source through a read-only sparse git clone (HEAD a2f361b) in this card's scratchpad. The objectui#7297 issue itself was not read, and nothing was written there.",
"Shared git history was deepened (git fetch --shallow-since=2026-06-22 origin main) to meet the check-engine-split-ratio prerequisite. It is a read and only extends the shared object store.",
"The first reverse-type-probe attempt was refused by ablation-replace (the replacement contained the anchor, so the count did not drop) and its command never ran. That reading is void; it was rerun with an anchor the replacement consumes.",
"Commit trailers use the AGENTS.md model-free pair (Claude-Session plus Co-authored-by: Claude), not the model-named Co-Authored-By from the harness reminder. The PR footer uses the AGENTS.md session-URL form, not the reminder line. AGENTS.md outranks the reminder, and the pre-push hook refuses a model id.",
"One status reply was sent to the coordinator through SendMessage in answer to its probe."
],
"mcp_calls": "0",
"api_writes": "3 relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches). (1) request fw-20260927T060315Z-87fb9d, run 36298969881: POST /repos/objectstack-ai/objectstack/pulls (draft #20180; body read back byte-identical). (2) request fw-20260927T060350Z-014d05 via scripts/pm/label-write.mjs, run 36298996937: POST /repos//issues/20180/assignees os-sales (read back: assignee os-sales, no label change). (3) this os-dev-report comment through scripts/pm/post-stamped.mjs: POST /repos//issues/20003/comments. There were also 4 git pushes, which are not REST writes. All other REST calls were reads.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none — packages/lint/src/validate-flow-filter-tokens.ts: its early-return comment names context/date-macro as the kinds ObjectQL owns after hand-off, and record-context now returns early there too. Behaviour is unchanged (a bare record_id is an open-oracle identifier in the flow dialect and was silent before too); only the comment is one kind short. Noted in the PR Acceptance notes, not filed.",
"carrier: whoever lands the objectui#7297 resolver and the spec bump that carries it — the published skills (skills/objectstack-ui/SKILL.md, skills/objectstack-query/rules/filters.md) teach only {current_user_id}/{current_org_id}. They should teach {record_id} once a renderer resolves it; teaching it earlier steers authors into a server refusal. This is a Tier H surface. Noted, not filed.",
"carrier: CI — the CLI integration test authoring-rule-command-parity covers filter-token-unknown; it lives in the integration project and was not run locally. Noted, not filed."
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsACCEPT — PR #20180 (2026-09-27T09:19Z)
domain:specseat 4 (session_01CiCTczDo7tGhafXjf61dUJ). This is the review of record for the round claimed in5852526924; the dev report is5853232187.Checklist, read on GitHub:
- PR form: draft, base
main, first lineFixes #20003, no other closing keyword. The bare lineClause-②: yesmatches the diff. Assigneeos-sales. - Scope: 13 files (+886 / −40).
- Source changes:
context-tokens.zod.ts,core/src/utils/filter-tokens.ts,lint/src/validate-filter-tokens.ts. - Also: test files in five packages, the regenerated api-surface, export-origins and
context-tokens.mdx, and a changeset. packages/objectql/src/engine.ts,filter-walk.tsandlint/src/index.tsare untouched, as the claim required.
- Source changes:
- At-tier contract review: PASS, record
5854583888on the PR, at head251c5f49b5.- The refusal is
FILTER_TOKEN_UNRESOLVED/ 400 by name on every server path that receives a filter: find, findOne, count, aggregate and its per-aggregation filters, update and delete includingwhere.id, the analytics query door,generateSql, and the dataset executor. - The lint accepts the token only beneath a
type: 'record'page (including the schema's default type) and refuses it by name on every other surface. - The only new exports are
RECORD_CONTEXT_TOKENS,RecordContextTokenandisRecordContextToken, each named in the changeset. - The reused code is already in the ledger (
error-code-ledger.zod.ts:848); the seat confirmed the row onorigin/main.
- The refusal is
- CI at
251c5f49b5: 33 success, 2 skipped (Console Pin Gate; the opt-in tarball smoke); all seven required contexts are success. - Merge:
mergeable_state: clean, and a no-drivermerge-treeonto today'smainexits 0. - Governed surface: 0 paths; 926 changed lines.
- Dev evidence: 111 of 113 derived gates exit 0. The other two are NOT MEASURED (exit 3, full-workspace prerequisite); CI's four Type Check jobs are green.
- Process note: the first review subagent for this PR stalled with no output and was stopped. A fresh one produced the record above.
Out-of-scope findings (3), each disposed:
validate-flow-filter-tokens.ts's early-return comment names two kinds where there are now three; behaviour is unchanged → Acceptance notes.- The published skills teach only the session tokens → carrier: whoever lands the objectui#7297 resolver. Teaching
{record_id}before a renderer resolves it would steer authors into the server refusal. Tier H, noted. - The CLI integration test
authoring-rule-command-paritywas not run locally → CI;Dogfood Verify CLIand the Test Core shards are green at the head.
Downstream: objectui#7297 unblocks when an objectui checkout can install a published
@objectstack/specthat carries the token, per this card's own closing section. That is the release lane, ⛔ not a merge here.Next: ready → auto-merge through the relay → merge queue. This card closes on the merge through
Fixes.- PR form: draft, base
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsLanded — PR #20180 →
e7f69dbba6(2026-09-27T09:41Z)domain:specseat 4 (session_01CiCTczDo7tGhafXjf61dUJ). PR #20180 merged through the merge queue on 2026-09-27T09:41:25Z, and this card closedcompletedthrough itsFixesline. Two readings confirm the landing:e7f69dbba6has one parent (e5cf27d65) and is an ancestor oforigin/main.- Its
git patch-id --stableequals the reviewed diff (4854da0146c2on both).
pm:dispatchedand the assignee come off in the same act.Downstream, unchanged by this merge: objectui#7297 unblocks only once an objectui checkout can INSTALL a published
@objectstack/speccarryingRECORD_CONTEXT_TOKENS. That is the release lane's job, per this card's own closing section. ⛔ A merge here is not the unblock.- added a commit that references this issue
on Sep 28, 2026 - added a commit that references this issue
on Oct 7, 2026
What is missing
On a
type: 'record'page, the only component that can scope itself to the record in view isrecord:related_list(viarelationshipField/relationshipValueField). Every other data-bearing component, includingelement:numberand any component-leveldataSource, takes aFilterCondition. The only dynamic values a filter can hold are theCONTEXT_TOKENS:Both tokens name the signed-in viewer. None names the record the page is bound to.
The result is a silent wrong answer. An author puts "open tasks" at the top of a person's record page and writes the obvious filter. The page shows a count over the whole organisation, under that person's name, and it reads as their number.
The same component can already bind the record in its
visibleWhenpredicate ("Page predicates bind the live page surface:record+current_user…"). So authors reasonably expect the filter to do the same.This has already cost a delivery.
objectstack-ai/duly#13shipped threerecord:related_listtables where three numbers were wanted. The full measurement is on objectui#7297.Maintainer ruling
The triage seat put this to the maintainer on 2026-09-24 as decision ② of six, recommending: do
{record_id}; with no record context it must fail with a clear error, never quietly become an empty value.The maintainer's answer, verbatim: 「需要我决定的 6 件事: 6947 7300 不做;其他同意」. #7297 is among "其他", so the recommendation stands as ruled.
What the spec has to say
This card owns the vocabulary and its contract. The objectui resolver is objectui#7297's own work, and it stays blocked on this card.
1. The token is recognised by name
classifyFilterToken('{record_id}')must stop returningkind: 'unknown'.You decide whether it joins
CONTEXT_TOKENSor sits in a sibling list. The case for a sibling list: the module header definesCONTEXT_TOKENSas "resolve against the caller's session", and{record_id}does not; it resolves against the surface. If it joinsCONTEXT_TOKENS, the header and the "Deliberately tiny … three surfaces and one lint pass" note must be rewritten to say so.2. The server refuses it, by name
The server resolver cannot know which record a page is showing. It is
resolveFilterTokens()inpackages/core/src/utils/filter-tokens.ts:376, which runs on the ObjectQL read and write paths and in the analytics dataset executor.So a filter that reaches the server with
{record_id}still in it must throw a named error. It must not resolve tonull,undefinedor the literal string. This matches the rule the header already sets for a request with nouserIdortenantId: "resolving tonulldegrades toIS NULLon most drivers and would hand back the rows the filter was written to exclude."It also closes the reverse trap. Today the number is "about everybody"; the fix must not turn it into a number "about nobody".
3. The lint pass allows it only where a record context exists
packages/lint/src/validate-filter-tokens.tsmust allow{record_id}in filters on components of atype: 'record'page. It must refuse it by name on:Each refusal must say why ("no record in context on this surface"), not the generic unknown-token message.
The pass can already do this:
walkAuthoredFilters(lint/src/filter-walk.ts) reports each filter'sFilterSurface, and the rule declares its own seven surfaces.4. The documentation says it is presentation scope
Like
{current_user_id},{record_id}scopes what a surface shows. It is not an access boundary.The "Where the tokens are honoured" section must list which surfaces honour it. It must also keep it apart from two look-alikes:
{recordId}URL / flow-template placeholders (action.zod.tsnewTabUrl,lint/src/flow-template-grammar.ts), which are a different mechanism;'record_id'(page.zod.ts:454).5. A changeset
The change adds a token, so it is additive. Per AGENTS.md changeset rules, the changeset must say which surfaces now accept the token and which refuse it.
Acceptance
classifyFilterTokenrecognises{record_id}. Tests pin a lit control ({current_user_id}still resolves as before) and a near-miss ({recordid}or{record-id}is still refused with a suggestion).resolveFilterTokensthrows a named error on{record_id}, on the read path, the write path and the dataset executor, each with a test.How this unblocks objectui#7297
objectui#7297 is unblocked when an objectui checkout can install a published
@objectstack/specthat carries the token. Merging here is not enough:@objectstack/speclatest is 17.4.0 today, and that is what objectui resolves.objectui#7297 carries the executable probe for that condition.
Out of scope
current_user.*expressions.AppContextSelectorids.titleFormatfield interpolation.{record_id}: related-record tokens, parent-of-record tokens and the like each need their own card and their own ruling.