Skip to content

fix(skills): teach {current_user_id}, not $currentUser, in the objectstack-ui Filtering example - #14781

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-14139-currentuser-filter-token
Sep 3, 2026
Merged

fix(skills): teach {current_user_id}, not $currentUser, in the objectstack-ui Filtering example#14781
os-zhuang merged 1 commit into
mainfrom
claude/issue-14139-currentuser-filter-token

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes #14139

skills/objectstack-ui taught $currentUser as a filter value. Nothing resolves
it. The example and its note now teach {current_user_id}, the declared token,
so the rule file agrees with the package's own filter-placeholder contract.

Step 1 — the route, settled at source

Route 1. No filter path resolves the literal $currentUser, on either side
of the wire, and no legacy alias maps it. Route 2 (an ADR-0087 conversion-layer
question) does not arise.

Where the literal was searched — every place, with a positive control

# Search Tree Result Positive control on the same tree
1 git grep -n -F '$currentUser' (all tracked paths) objectstack @ 224f8ea4 4 hits, none in a filter resolution path: docs/adr/0017-object-has-many-view.md:216 (SQL-shaped prose), packages/cli/src/commands/explain.ts:128 (an assignment default value — a different surface), and the two sites fixed here same grep finds the 4 hits, so the pattern is live
2 git grep -ni 'currentuser' excluding *CHANGELOG.md objectstack @ 224f8ea4 118 hits; zero are a filter-token vocabulary entry packages/spec/src/data/context-tokens.test.ts:44 REJECTS currentUserId among near-misses; packages/spec/src/data/default-value-shape.test.ts:54 pins currentUser (camelCase) is a literal
3 git grep -n -F '$currentUser' objectui @ 67dadd6 2 hits, both packages/types/src/__tests__/phase2-schemas.test.ts:791,808ListViewSchema.safeParse(...) shape assertions on a tab's filter array. Neither asserts resolution current_user_id on the same tree: 14 hits in packages/core/src/utils/filter-tokens.ts and 14 in its test
4 git grep -n -E "'\$[a-zA-Z]" over packages/core/src, packages/objectql, packages/lint/src, packages/spec/src objectstack @ 224f8ea4 only Mongo-style operators ($in, $and, $or, $not, $gte …) and $source / $rootno $-prefixed session-token alias table anywhere the same regex over packages/ does return explain.ts:128, so it can see the shape it is looking for

Why nothing can resolve it — the mechanism, not an absence of hits

A filter value is recognised as a placeholder only when the whole value is
brace-wrapped
. $currentUser carries no braces, so it is not classified as a
placeholder at all — not even as an unknown one.

Layer Source Recognition
Vocabulary packages/spec/src/data/context-tokens.zod.ts:84 CONTEXT_TOKENS = ['current_user_id', 'current_org_id'] — the complete set
Recognition grammar packages/spec/src/data/context-tokens.zod.ts:146 FILTER_TOKEN_WRAPPED_RE = /^\$?\{([^{}]+)\}$/
Classifier packages/spec/src/data/context-tokens.zod.ts:239-253 classifyFilterToken returns null when that regex does not match — i.e. "not a placeholder", pass through verbatim
Client resolver objectui packages/core/src/utils/filter-tokens.ts:107,135,209 WHOLE_TOKEN_RE = /^\$?\{([a-zA-Z0-9_]+)\}$/, used by resolveContextTokens and resolveFilterPlaceholders
Server resolver packages/core/src/utils/filter-tokens.ts:364 resolveFilterTokens walks with the same classifyFilterToken; its hasFilterToken pre-pass returns false for a tree holding only $currentUser
Lint rule packages/lint/src/validate-filter-tokens.ts:65,123-124 filter-token-unknown pushes a finding only when classifyFilterToken(node)?.kind === 'unknown'

Consequence, and it is sharper than the card assumed: because $currentUser is
unbraced it is not a placeholder attempt, so filter-token-unknown never fires
on it and resolveFilterTokens never throws. The value reaches the data engine
as a literal string, matches no record, and the list renders empty — with no
build error and no runtime warning anywhere. The card offered "a red
os validate or a silent empty list"; only the silent half is reachable.

The change

Two lines in skills/objectstack-ui/rules/list-views.md, in the ### Filtering
section. The example keeps its shape.

Example — before

  { field: 'assigned_to', operator: 'equals', value: '$currentUser' },

Example — after

  { field: 'assigned_to', operator: 'equals', value: '{current_user_id}' },

Note — before (71 bytes)

> **`$currentUser`** is a runtime variable — the logged-in user's ID.

Note — after (62 bytes, i.e. no more than the old note)

> **`{current_user_id}`** resolves to the signed-in user's id.

Every claim in the new note is traceable to a source line:

Claim Source line
{current_user_id} is a declared filter token packages/spec/src/data/context-tokens.zod.ts:84-87
it resolves (it is not a literal) packages/core/src/utils/filter-tokens.ts:364 (server) and objectui packages/core/src/utils/filter-tokens.ts:209 (client)
it is "the signed-in user's id" packages/spec/src/data/context-tokens.zod.ts:176CONTEXT_TOKEN_DESCRIPTIONS.current_user_id = "The signed-in user's id (\sys_user.id`)."`

After the change the rule file agrees with its own package entry:
skills/objectstack-ui/SKILL.md## Date Macros — Filter Placeholders names
{current_user_id} / {current_org_id} and delegates the vocabulary to
objectstack-queryrules/filters.md.

Token budget — shrink-only, ceiling untouched

The ratchet convention is ceil(utf8 bytes / 4).

Reading Before After Delta
rules/list-views.md bytes 12,043 12,039 −4
rules/list-views.md tokens 3,011 3,010 −1
rules/list-views.md lines 306 306 0
package skills/objectstack-ui/ — hand-authored (ratcheted) tokens 24,189 24,188 −1
package skills/objectstack-ui/ — whole package tokens (incl. generator-owned) 33,899 33,898 −1
package skills/objectstack-ui/ — hand-authored lines 1,980 1,980 0

The honest rewrite is net negative, so no payment was needed and no
restatement was deleted. No ceiling was changed: the row stays
['skills/objectstack-ui/rules/list-views.md', 3011], and the gate's own line is

✓ check-skills-token-ratchet: skills/objectstack-ui/rules/list-views.md is 3010 tokens (ceiling 3011; headroom 1).

No re-wrap was used as payment — the diff is two whole-line replacements, and
the line count is unchanged.

Gates

All run under scripts/pm/os-verify-lock.sh, each exit code captured by
redirect before any pipe, each verdict quoted from the gate's own output.

Gate Exit The gate's own verdict line
node scripts/check-skills-token-ratchet.mjs 0 ✓ check-skills-token-ratchet: 36 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted.
node scripts/check-skills-token-ratchet.mjs --self-test 0 ✓ check-skills-token-ratchet self-test: 64 cases pass.
pnpm check:skill-identifier-liveness 0 check-skill-identifier-liveness OK — Leg 1: 465 citation(s) over 46 published file(s) …; Leg 2: 8 registered exhaustive section(s), 0 ledgered gap(s).
pnpm --filter @objectstack/spec run check:skill-examples 0 ✅ 256 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them
pnpm --filter @objectstack/spec run check:skill-docs 0 ✅ Skill docs in sync
pnpm check:role-word 0 check-role-word: OK, no new occurrences of the reserved word. (Scanned: 224 .md/.mdx file(s) … skills 34)
pnpm check:nul-bytes 0 check-nul-bytes: OK (scanned 8052 text file(s) … no raw ASCII control bytes).
pnpm --filter @objectstack/lint run check:doc-formula-expressions 0 ran clean (no findings emitted)

The re-derived union

Re-derived after the last edit with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands,
which reported gate list derived from the tree of 'objectstack-ai/objectstack' at commit 482fb9c75
and a change set of 1 path. Every command it named was run:

Gate Exit The gate's own verdict line
node scripts/check-ci-filter-parity.mjs 0 OK: all 130 declared cross-package glob(s) (92 unique) are covered …
node scripts/check-cross-package-test-inputs.mjs 0 OK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
node scripts/check-shard-attestation.mjs 0 ✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
node scripts/check-skills-token-ratchet.mjs 0 (above)
node scripts/check-test-completeness.mjs 3 NOT MEASUREDcheck-test-completeness: PREREQUISITE NOT MET — this gate grades a saved 'turbo run test' log, and no log was named. The gate's own text adds: "running the family locally, record this gate as NOT MEASURED. ⛔ It is not a red, and there is nothing here to fix."
pnpm --filter @objectstack/lint run check:doc-formula-expressions 0 (above)
pnpm check:agent-test-spelling 0 ran clean
pnpm check:corpus-claim-drift 0 check-corpus-claim-drift: OK, no new claim sites beside a pinned spelling.
pnpm check:cross-package-test-inputs 0 All 117 self-test cases passed. + OK: 25 package(s) read outside themselves, all declared …
pnpm check:doc-authoring 0 ✓ doc authoring guard: 46 published skill files clean — no internal issue-id references.
pnpm check:pm-governed-merges 0 ✓ check-governed-merges --self-test: 243 assertions … + live: the real generator declared 9 output(s) and certified this tree
pnpm check:role-word 0 (above)
pnpm check:skill-compatibility 0 ✓ check-skill-compatibility-version: 11 SKILL.md file(s) reconciled against 79 workspace packages
pnpm check:skill-frame-sync 0 ✓ check-skill-frame-sync: 4 copies of the decision frame are structurally isomorphic across 3 files
pnpm check:skill-identifier-liveness 0 (above)

The union derivation also notes that 9 further families "apply once this card's
changeset exists" — this PR carries skip-changeset (below), so those paths do
not exist and those families do not apply.

check:skill-examples needed a build prerequisite on its client SDK surface
(packages/client-react/dist holds no .d.ts declarations — the package is not built), which is unrelated to this diff. It was satisfied
(pnpm --filter '@objectstack/client-react...' build, then
pnpm --filter '@objectstack/client...' build, both exit 0) and the gate re-run,
so the row above is a real measurement and not a NOT MEASURED.

Fence census — the ### Filtering example is not a typed block

check:skill-examples extracts a fence only when the line directly above it
carries an os:check marker (an HTML comment — written here as the bare token so
this body survives GitHub's sanitizer). Census of
skills/objectstack-ui/rules/list-views.md:

Reading Before (224f8ea4) After (482fb9c7)
os:check marker lines 11, 166, 221 11, 166, 221
marker count 3 3
fence open/close lines (all 10 blocks) 12/27, 38/47, 57/73, 92/97, 111/128, 167/184, 222/234, 245/254, 264/276, 288/302 identical
file lines 306 306

No marker moved and no fence moved. The ### Filtering fence opens at line 92
and line 91 is blank — no marker — so that block is not among the gate's
extracted examples, before or after. The gate's skills+docs surface count is
unchanged at 224 blocks.

skip-changeset

This PR publishes nothing from any package: skills/ is not listed in any
package manifest's files, and no build step copies it into a published
artifact. Merged precedent on this exact surface — 58ea39a5d (#14658),
446117fc2 (#14673), c985ae958 (#14660) — are all skills-only merges and none
carries a .changeset/*.md. Check Changeset requires an added .changeset/*.md
unless the label is present, so the label is what keeps a release-less PR green.

Premises that did not hold

# The premise What the tree says
1 The example and note live in skills/objectstack-ui/SKILL.md They live in skills/objectstack-ui/rules/list-views.md. The ui split (#14658) moved them. Same published package, different file — and the fix landed on the file that actually holds the text
2 The file has a ## Context Tokens section stating "the only two tokens" That heading exists nowhere under skills/ any more. The surviving contract half is ## Date Macros — Filter Placeholders in skills/objectstack-ui/SKILL.md, which delegates the vocabulary to objectstack-queryrules/filters.md. So the contradiction spanned two files in one package, not two halves of one file
3 The governing ceiling is 3,815 with headroom 0 3,815 is SKILL.md's row, untouched here. The row governing this edit is ['skills/objectstack-ui/rules/list-views.md', 3011] — headroom 0 before, 1 after
4 An author writing $currentUser gets a red os validate (filter-token-unknown) or a silent empty list Only the silent half is reachable. filter-token-unknown fires on kind === 'unknown', which requires a brace-wrapped value; $currentUser classifies as null (not a placeholder) and is passed through verbatim
5 The ### Filtering example is a typed block checked by check:skill-examples It is not. That gate extracts only fences carrying an os:check marker on the line directly above (packages/spec/scripts/check-skill-examples.ts:398,601). The ### Filtering fence has none — see the fence census above

Nothing else in the card's scope changed: packages/cli/src/commands/explain.ts
and docs/adr/0017-object-has-many-view.md are untouched, as dispatched.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1


Generated by Claude Code

…tering example

`skills/objectstack-ui/rules/list-views.md` taught `$currentUser` as a filter
value. No filter path resolves it: both resolvers recognise a placeholder only
when the whole value is brace-wrapped (`FILTER_TOKEN_WRAPPED_RE` in
`packages/spec/src/data/context-tokens.zod.ts`, `WHOLE_TOKEN_RE` in
`@object-ui/core`), so an unbraced `$currentUser` is not even classified as a
placeholder attempt — it reaches the data engine as a literal, matches nothing,
and the list renders empty with no diagnostic anywhere.

Rewrite the example value and its note to `{current_user_id}`, the declared
token, so the rule file agrees with the package's own
`## Date Macros — Filter Placeholders` contract in SKILL.md.

Net -4 bytes (3011 -> 3010 tokens, ceiling 3011 unchanged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-zhuang
os-zhuang marked this pull request as ready for review September 3, 2026 02:32
@os-zhuang
os-zhuang enabled auto-merge September 3, 2026 02:32
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33710778194 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test:  FAIL   integration  test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
      ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 50 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33710294958 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test:  FAIL   integration  test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
      ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1800
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 48 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xs skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

skills/objectstack-ui teaches $currentUser as a filter value, contradicting its own "only two tokens resolve in a filter" contract

3 participants