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

#SearchTreeResultPositive control on the same tree
1git grep -n -F '$currentUser' (all tracked paths)objectstack @ 224f8ea44 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 heresame grep finds the 4 hits, so the pattern is live
2git grep -ni 'currentuser' excluding *CHANGELOG.mdobjectstack @ 224f8ea4118 hits; zero are a filter-token vocabulary entrypackages/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
3git grep -n -F '$currentUser'objectui @ 67dadd62 hits, both packages/types/src/__tests__/phase2-schemas.test.ts:791,808ListViewSchema.safeParse(...)shape assertions on a tab's filter array. Neither asserts resolutioncurrent_user_id on the same tree: 14 hits in packages/core/src/utils/filter-tokens.ts and 14 in its test
4git grep -n -E "'\$[a-zA-Z]" over packages/core/src, packages/objectql, packages/lint/src, packages/spec/srcobjectstack @ 224f8ea4only Mongo-style operators ($in, $and, $or, $not, $gte …) and $source / $rootno $-prefixed session-token alias table anywherethe 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.

LayerSourceRecognition
Vocabularypackages/spec/src/data/context-tokens.zod.ts:84CONTEXT_TOKENS = ['current_user_id', 'current_org_id'] — the complete set
Recognition grammarpackages/spec/src/data/context-tokens.zod.ts:146FILTER_TOKEN_WRAPPED_RE = /^\$?\{([^{}]+)\}$/
Classifierpackages/spec/src/data/context-tokens.zod.ts:239-253classifyFilterToken returns null when that regex does not match — i.e. "not a placeholder", pass through verbatim
Client resolverobjectui packages/core/src/utils/filter-tokens.ts:107,135,209WHOLE_TOKEN_RE = /^\$?\{([a-zA-Z0-9_]+)\}$/, used by resolveContextTokens and resolveFilterPlaceholders
Server resolverpackages/core/src/utils/filter-tokens.ts:364resolveFilterTokens walks with the same classifyFilterToken; its hasFilterToken pre-pass returns false for a tree holding only $currentUser
Lint rulepackages/lint/src/validate-filter-tokens.ts:65,123-124filter-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 validateor 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:

ClaimSource line
{current_user_id} is a declared filter tokenpackages/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).

ReadingBeforeAfterDelta
rules/list-views.md bytes12,04312,039−4
rules/list-views.md tokens3,0113,010−1
rules/list-views.md lines3063060
package skills/objectstack-ui/ — hand-authored (ratcheted) tokens24,18924,188−1
package skills/objectstack-ui/ — whole package tokens (incl. generator-owned)33,89933,898−1
package skills/objectstack-ui/ — hand-authored lines1,9801,9800

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.

GateExitThe gate's own verdict line
node scripts/check-skills-token-ratchet.mjs0✓ 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-test0✓ check-skills-token-ratchet self-test: 64 cases pass.
pnpm check:skill-identifier-liveness0check-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-examples0✅ 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-docs0✅ Skill docs in sync
pnpm check:role-word0check-role-word: OK, no new occurrences of the reserved word. (Scanned: 224 .md/.mdx file(s) … skills 34)
pnpm check:nul-bytes0check-nul-bytes: OK (scanned 8052 text file(s) … no raw ASCII control bytes).
pnpm --filter @objectstack/lint run check:doc-formula-expressions0ran 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:

GateExitThe gate's own verdict line
node scripts/check-ci-filter-parity.mjs0OK: all 130 declared cross-package glob(s) (92 unique) are covered …
node scripts/check-cross-package-test-inputs.mjs0OK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
node scripts/check-shard-attestation.mjs0✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
node scripts/check-skills-token-ratchet.mjs0(above)
node scripts/check-test-completeness.mjs3NOT 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-expressions0(above)
pnpm check:agent-test-spelling0ran clean
pnpm check:corpus-claim-drift0check-corpus-claim-drift: OK, no new claim sites beside a pinned spelling.
pnpm check:cross-package-test-inputs0All 117 self-test cases passed. + OK: 25 package(s) read outside themselves, all declared …
pnpm check:doc-authoring0✓ doc authoring guard: 46 published skill files clean — no internal issue-id references.
pnpm check:pm-governed-merges0✓ check-governed-merges --self-test: 243 assertions … + live: the real generator declared 9 output(s) and certified this tree
pnpm check:role-word0(above)
pnpm check:skill-compatibility0✓ check-skill-compatibility-version: 11 SKILL.md file(s) reconciled against 79 workspace packages
pnpm check:skill-frame-sync0✓ check-skill-frame-sync: 4 copies of the decision frame are structurally isomorphic across 3 files
pnpm check:skill-identifier-liveness0(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:

ReadingBefore (224f8ea4)After (482fb9c7)
os:check marker lines11, 166, 22111, 166, 221
marker count33
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/302identical
file lines306306

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 premiseWhat the tree says
1The example and note live in skills/objectstack-ui/SKILL.mdThey 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
2The 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
3The governing ceiling is 3,815 with headroom 03,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
4An author writing $currentUser gets a red os validate (filter-token-unknown) or a silent empty listOnly 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
5The ### Filtering example is a typed block checked by check:skill-examplesIt 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 added this pull request to the merge queueSep 3, 2026
@github-actions

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

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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xsskip-changesetPR 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

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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

#SearchTreeResultPositive control on the same tree
1git grep -n -F '$currentUser' (all tracked paths)objectstack @ 224f8ea44 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 heresame grep finds the 4 hits, so the pattern is live
2git grep -ni 'currentuser' excluding *CHANGELOG.mdobjectstack @ 224f8ea4118 hits; zero are a filter-token vocabulary entrypackages/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
3git grep -n -F '$currentUser'objectui @ 67dadd62 hits, both packages/types/src/__tests__/phase2-schemas.test.ts:791,808ListViewSchema.safeParse(...)shape assertions on a tab's filter array. Neither asserts resolutioncurrent_user_id on the same tree: 14 hits in packages/core/src/utils/filter-tokens.ts and 14 in its test
4git grep -n -E "'\$[a-zA-Z]" over packages/core/src, packages/objectql, packages/lint/src, packages/spec/srcobjectstack @ 224f8ea4only Mongo-style operators ($in, $and, $or, $not, $gte …) and $source / $rootno $-prefixed session-token alias table anywherethe 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.

LayerSourceRecognition
Vocabularypackages/spec/src/data/context-tokens.zod.ts:84CONTEXT_TOKENS = ['current_user_id', 'current_org_id'] — the complete set
Recognition grammarpackages/spec/src/data/context-tokens.zod.ts:146FILTER_TOKEN_WRAPPED_RE = /^\$?\{([^{}]+)\}$/
Classifierpackages/spec/src/data/context-tokens.zod.ts:239-253classifyFilterToken returns null when that regex does not match — i.e. "not a placeholder", pass through verbatim
Client resolverobjectui packages/core/src/utils/filter-tokens.ts:107,135,209WHOLE_TOKEN_RE = /^\$?\{([a-zA-Z0-9_]+)\}$/, used by resolveContextTokens and resolveFilterPlaceholders
Server resolverpackages/core/src/utils/filter-tokens.ts:364resolveFilterTokens walks with the same classifyFilterToken; its hasFilterToken pre-pass returns false for a tree holding only $currentUser
Lint rulepackages/lint/src/validate-filter-tokens.ts:65,123-124filter-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 validateor 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:

ClaimSource line
{current_user_id} is a declared filter tokenpackages/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).

ReadingBeforeAfterDelta
rules/list-views.md bytes12,04312,039−4
rules/list-views.md tokens3,0113,010−1
rules/list-views.md lines3063060
package skills/objectstack-ui/ — hand-authored (ratcheted) tokens24,18924,188−1
package skills/objectstack-ui/ — whole package tokens (incl. generator-owned)33,89933,898−1
package skills/objectstack-ui/ — hand-authored lines1,9801,9800

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.

GateExitThe gate's own verdict line
node scripts/check-skills-token-ratchet.mjs0✓ 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-test0✓ check-skills-token-ratchet self-test: 64 cases pass.
pnpm check:skill-identifier-liveness0check-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-examples0✅ 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-docs0✅ Skill docs in sync
pnpm check:role-word0check-role-word: OK, no new occurrences of the reserved word. (Scanned: 224 .md/.mdx file(s) … skills 34)
pnpm check:nul-bytes0check-nul-bytes: OK (scanned 8052 text file(s) … no raw ASCII control bytes).
pnpm --filter @objectstack/lint run check:doc-formula-expressions0ran 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:

GateExitThe gate's own verdict line
node scripts/check-ci-filter-parity.mjs0OK: all 130 declared cross-package glob(s) (92 unique) are covered …
node scripts/check-cross-package-test-inputs.mjs0OK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
node scripts/check-shard-attestation.mjs0✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
node scripts/check-skills-token-ratchet.mjs0(above)
node scripts/check-test-completeness.mjs3NOT 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-expressions0(above)
pnpm check:agent-test-spelling0ran clean
pnpm check:corpus-claim-drift0check-corpus-claim-drift: OK, no new claim sites beside a pinned spelling.
pnpm check:cross-package-test-inputs0All 117 self-test cases passed. + OK: 25 package(s) read outside themselves, all declared …
pnpm check:doc-authoring0✓ doc authoring guard: 46 published skill files clean — no internal issue-id references.
pnpm check:pm-governed-merges0✓ check-governed-merges --self-test: 243 assertions … + live: the real generator declared 9 output(s) and certified this tree
pnpm check:role-word0(above)
pnpm check:skill-compatibility0✓ check-skill-compatibility-version: 11 SKILL.md file(s) reconciled against 79 workspace packages
pnpm check:skill-frame-sync0✓ check-skill-frame-sync: 4 copies of the decision frame are structurally isomorphic across 3 files
pnpm check:skill-identifier-liveness0(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:

ReadingBefore (224f8ea4)After (482fb9c7)
os:check marker lines11, 166, 22111, 166, 221
marker count33
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/302identical
file lines306306

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 premiseWhat the tree says
1The example and note live in skills/objectstack-ui/SKILL.mdThey 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
2The 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
3The governing ceiling is 3,815 with headroom 03,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
4An author writing $currentUser gets a red os validate (filter-token-unknown) or a silent empty listOnly 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
5The ### Filtering example is a typed block checked by check:skill-examplesIt 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 added this pull request to the merge queueSep 3, 2026
@github-actions

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

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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xsskip-changesetPR 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

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

#SearchTreeResultPositive control on the same tree
1git grep -n -F '$currentUser' (all tracked paths)objectstack @ 224f8ea44 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 heresame grep finds the 4 hits, so the pattern is live
2git grep -ni 'currentuser' excluding *CHANGELOG.mdobjectstack @ 224f8ea4118 hits; zero are a filter-token vocabulary entrypackages/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
3git grep -n -F '$currentUser'objectui @ 67dadd62 hits, both packages/types/src/__tests__/phase2-schemas.test.ts:791,808ListViewSchema.safeParse(...)shape assertions on a tab's filter array. Neither asserts resolutioncurrent_user_id on the same tree: 14 hits in packages/core/src/utils/filter-tokens.ts and 14 in its test
4git grep -n -E "'\$[a-zA-Z]" over packages/core/src, packages/objectql, packages/lint/src, packages/spec/srcobjectstack @ 224f8ea4only Mongo-style operators ($in, $and, $or, $not, $gte …) and $source / $rootno $-prefixed session-token alias table anywherethe 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.

LayerSourceRecognition
Vocabularypackages/spec/src/data/context-tokens.zod.ts:84CONTEXT_TOKENS = ['current_user_id', 'current_org_id'] — the complete set
Recognition grammarpackages/spec/src/data/context-tokens.zod.ts:146FILTER_TOKEN_WRAPPED_RE = /^\$?\{([^{}]+)\}$/
Classifierpackages/spec/src/data/context-tokens.zod.ts:239-253classifyFilterToken returns null when that regex does not match — i.e. "not a placeholder", pass through verbatim
Client resolverobjectui packages/core/src/utils/filter-tokens.ts:107,135,209WHOLE_TOKEN_RE = /^\$?\{([a-zA-Z0-9_]+)\}$/, used by resolveContextTokens and resolveFilterPlaceholders
Server resolverpackages/core/src/utils/filter-tokens.ts:364resolveFilterTokens walks with the same classifyFilterToken; its hasFilterToken pre-pass returns false for a tree holding only $currentUser
Lint rulepackages/lint/src/validate-filter-tokens.ts:65,123-124filter-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 validateor 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:

ClaimSource line
{current_user_id} is a declared filter tokenpackages/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).

ReadingBeforeAfterDelta
rules/list-views.md bytes12,04312,039−4
rules/list-views.md tokens3,0113,010−1
rules/list-views.md lines3063060
package skills/objectstack-ui/ — hand-authored (ratcheted) tokens24,18924,188−1
package skills/objectstack-ui/ — whole package tokens (incl. generator-owned)33,89933,898−1
package skills/objectstack-ui/ — hand-authored lines1,9801,9800

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.

GateExitThe gate's own verdict line
node scripts/check-skills-token-ratchet.mjs0✓ 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-test0✓ check-skills-token-ratchet self-test: 64 cases pass.
pnpm check:skill-identifier-liveness0check-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-examples0✅ 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-docs0✅ Skill docs in sync
pnpm check:role-word0check-role-word: OK, no new occurrences of the reserved word. (Scanned: 224 .md/.mdx file(s) … skills 34)
pnpm check:nul-bytes0check-nul-bytes: OK (scanned 8052 text file(s) … no raw ASCII control bytes).
pnpm --filter @objectstack/lint run check:doc-formula-expressions0ran 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:

GateExitThe gate's own verdict line
node scripts/check-ci-filter-parity.mjs0OK: all 130 declared cross-package glob(s) (92 unique) are covered …
node scripts/check-cross-package-test-inputs.mjs0OK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
node scripts/check-shard-attestation.mjs0✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
node scripts/check-skills-token-ratchet.mjs0(above)
node scripts/check-test-completeness.mjs3NOT 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-expressions0(above)
pnpm check:agent-test-spelling0ran clean
pnpm check:corpus-claim-drift0check-corpus-claim-drift: OK, no new claim sites beside a pinned spelling.
pnpm check:cross-package-test-inputs0All 117 self-test cases passed. + OK: 25 package(s) read outside themselves, all declared …
pnpm check:doc-authoring0✓ doc authoring guard: 46 published skill files clean — no internal issue-id references.
pnpm check:pm-governed-merges0✓ check-governed-merges --self-test: 243 assertions … + live: the real generator declared 9 output(s) and certified this tree
pnpm check:role-word0(above)
pnpm check:skill-compatibility0✓ check-skill-compatibility-version: 11 SKILL.md file(s) reconciled against 79 workspace packages
pnpm check:skill-frame-sync0✓ check-skill-frame-sync: 4 copies of the decision frame are structurally isomorphic across 3 files
pnpm check:skill-identifier-liveness0(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:

ReadingBefore (224f8ea4)After (482fb9c7)
os:check marker lines11, 166, 22111, 166, 221
marker count33
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/302identical
file lines306306

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 premiseWhat the tree says
1The example and note live in skills/objectstack-ui/SKILL.mdThey 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
2The 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
3The governing ceiling is 3,815 with headroom 03,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
4An author writing $currentUser gets a red os validate (filter-token-unknown) or a silent empty listOnly 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
5The ### Filtering example is a typed block checked by check:skill-examplesIt 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 added this pull request to the merge queueSep 3, 2026
@github-actions

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

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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xsskip-changesetPR 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

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

#SearchTreeResultPositive control on the same tree
1git grep -n -F '$currentUser' (all tracked paths)objectstack @ 224f8ea44 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 heresame grep finds the 4 hits, so the pattern is live
2git grep -ni 'currentuser' excluding *CHANGELOG.mdobjectstack @ 224f8ea4118 hits; zero are a filter-token vocabulary entrypackages/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
3git grep -n -F '$currentUser'objectui @ 67dadd62 hits, both packages/types/src/__tests__/phase2-schemas.test.ts:791,808ListViewSchema.safeParse(...)shape assertions on a tab's filter array. Neither asserts resolutioncurrent_user_id on the same tree: 14 hits in packages/core/src/utils/filter-tokens.ts and 14 in its test
4git grep -n -E "'\$[a-zA-Z]" over packages/core/src, packages/objectql, packages/lint/src, packages/spec/srcobjectstack @ 224f8ea4only Mongo-style operators ($in, $and, $or, $not, $gte …) and $source / $rootno $-prefixed session-token alias table anywherethe 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.

LayerSourceRecognition
Vocabularypackages/spec/src/data/context-tokens.zod.ts:84CONTEXT_TOKENS = ['current_user_id', 'current_org_id'] — the complete set
Recognition grammarpackages/spec/src/data/context-tokens.zod.ts:146FILTER_TOKEN_WRAPPED_RE = /^\$?\{([^{}]+)\}$/
Classifierpackages/spec/src/data/context-tokens.zod.ts:239-253classifyFilterToken returns null when that regex does not match — i.e. "not a placeholder", pass through verbatim
Client resolverobjectui packages/core/src/utils/filter-tokens.ts:107,135,209WHOLE_TOKEN_RE = /^\$?\{([a-zA-Z0-9_]+)\}$/, used by resolveContextTokens and resolveFilterPlaceholders
Server resolverpackages/core/src/utils/filter-tokens.ts:364resolveFilterTokens walks with the same classifyFilterToken; its hasFilterToken pre-pass returns false for a tree holding only $currentUser
Lint rulepackages/lint/src/validate-filter-tokens.ts:65,123-124filter-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 validateor 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:

ClaimSource line
{current_user_id} is a declared filter tokenpackages/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).

ReadingBeforeAfterDelta
rules/list-views.md bytes12,04312,039−4
rules/list-views.md tokens3,0113,010−1
rules/list-views.md lines3063060
package skills/objectstack-ui/ — hand-authored (ratcheted) tokens24,18924,188−1
package skills/objectstack-ui/ — whole package tokens (incl. generator-owned)33,89933,898−1
package skills/objectstack-ui/ — hand-authored lines1,9801,9800

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.

GateExitThe gate's own verdict line
node scripts/check-skills-token-ratchet.mjs0✓ 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-test0✓ check-skills-token-ratchet self-test: 64 cases pass.
pnpm check:skill-identifier-liveness0check-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-examples0✅ 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-docs0✅ Skill docs in sync
pnpm check:role-word0check-role-word: OK, no new occurrences of the reserved word. (Scanned: 224 .md/.mdx file(s) … skills 34)
pnpm check:nul-bytes0check-nul-bytes: OK (scanned 8052 text file(s) … no raw ASCII control bytes).
pnpm --filter @objectstack/lint run check:doc-formula-expressions0ran 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:

GateExitThe gate's own verdict line
node scripts/check-ci-filter-parity.mjs0OK: all 130 declared cross-package glob(s) (92 unique) are covered …
node scripts/check-cross-package-test-inputs.mjs0OK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
node scripts/check-shard-attestation.mjs0✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
node scripts/check-skills-token-ratchet.mjs0(above)
node scripts/check-test-completeness.mjs3NOT 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-expressions0(above)
pnpm check:agent-test-spelling0ran clean
pnpm check:corpus-claim-drift0check-corpus-claim-drift: OK, no new claim sites beside a pinned spelling.
pnpm check:cross-package-test-inputs0All 117 self-test cases passed. + OK: 25 package(s) read outside themselves, all declared …
pnpm check:doc-authoring0✓ doc authoring guard: 46 published skill files clean — no internal issue-id references.
pnpm check:pm-governed-merges0✓ check-governed-merges --self-test: 243 assertions … + live: the real generator declared 9 output(s) and certified this tree
pnpm check:role-word0(above)
pnpm check:skill-compatibility0✓ check-skill-compatibility-version: 11 SKILL.md file(s) reconciled against 79 workspace packages
pnpm check:skill-frame-sync0✓ check-skill-frame-sync: 4 copies of the decision frame are structurally isomorphic across 3 files
pnpm check:skill-identifier-liveness0(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:

ReadingBefore (224f8ea4)After (482fb9c7)
os:check marker lines11, 166, 22111, 166, 221
marker count33
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/302identical
file lines306306

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 premiseWhat the tree says
1The example and note live in skills/objectstack-ui/SKILL.mdThey 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
2The 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
3The governing ceiling is 3,815 with headroom 03,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
4An author writing $currentUser gets a red os validate (filter-token-unknown) or a silent empty listOnly 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
5The ### Filtering example is a typed block checked by check:skill-examplesIt 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 added this pull request to the merge queueSep 3, 2026
@github-actions

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

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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xsskip-changesetPR 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

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

#SearchTreeResultPositive control on the same tree
1git grep -n -F '$currentUser' (all tracked paths)objectstack @ 224f8ea44 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 heresame grep finds the 4 hits, so the pattern is live
2git grep -ni 'currentuser' excluding *CHANGELOG.mdobjectstack @ 224f8ea4118 hits; zero are a filter-token vocabulary entrypackages/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
3git grep -n -F '$currentUser'objectui @ 67dadd62 hits, both packages/types/src/__tests__/phase2-schemas.test.ts:791,808ListViewSchema.safeParse(...)shape assertions on a tab's filter array. Neither asserts resolutioncurrent_user_id on the same tree: 14 hits in packages/core/src/utils/filter-tokens.ts and 14 in its test
4git grep -n -E "'\$[a-zA-Z]" over packages/core/src, packages/objectql, packages/lint/src, packages/spec/srcobjectstack @ 224f8ea4only Mongo-style operators ($in, $and, $or, $not, $gte …) and $source / $rootno $-prefixed session-token alias table anywherethe 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.

LayerSourceRecognition
Vocabularypackages/spec/src/data/context-tokens.zod.ts:84CONTEXT_TOKENS = ['current_user_id', 'current_org_id'] — the complete set
Recognition grammarpackages/spec/src/data/context-tokens.zod.ts:146FILTER_TOKEN_WRAPPED_RE = /^\$?\{([^{}]+)\}$/
Classifierpackages/spec/src/data/context-tokens.zod.ts:239-253classifyFilterToken returns null when that regex does not match — i.e. "not a placeholder", pass through verbatim
Client resolverobjectui packages/core/src/utils/filter-tokens.ts:107,135,209WHOLE_TOKEN_RE = /^\$?\{([a-zA-Z0-9_]+)\}$/, used by resolveContextTokens and resolveFilterPlaceholders
Server resolverpackages/core/src/utils/filter-tokens.ts:364resolveFilterTokens walks with the same classifyFilterToken; its hasFilterToken pre-pass returns false for a tree holding only $currentUser
Lint rulepackages/lint/src/validate-filter-tokens.ts:65,123-124filter-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 validateor 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:

ClaimSource line
{current_user_id} is a declared filter tokenpackages/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).

ReadingBeforeAfterDelta
rules/list-views.md bytes12,04312,039−4
rules/list-views.md tokens3,0113,010−1
rules/list-views.md lines3063060
package skills/objectstack-ui/ — hand-authored (ratcheted) tokens24,18924,188−1
package skills/objectstack-ui/ — whole package tokens (incl. generator-owned)33,89933,898−1
package skills/objectstack-ui/ — hand-authored lines1,9801,9800

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.

GateExitThe gate's own verdict line
node scripts/check-skills-token-ratchet.mjs0✓ 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-test0✓ check-skills-token-ratchet self-test: 64 cases pass.
pnpm check:skill-identifier-liveness0check-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-examples0✅ 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-docs0✅ Skill docs in sync
pnpm check:role-word0check-role-word: OK, no new occurrences of the reserved word. (Scanned: 224 .md/.mdx file(s) … skills 34)
pnpm check:nul-bytes0check-nul-bytes: OK (scanned 8052 text file(s) … no raw ASCII control bytes).
pnpm --filter @objectstack/lint run check:doc-formula-expressions0ran 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:

GateExitThe gate's own verdict line
node scripts/check-ci-filter-parity.mjs0OK: all 130 declared cross-package glob(s) (92 unique) are covered …
node scripts/check-cross-package-test-inputs.mjs0OK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
node scripts/check-shard-attestation.mjs0✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
node scripts/check-skills-token-ratchet.mjs0(above)
node scripts/check-test-completeness.mjs3NOT 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-expressions0(above)
pnpm check:agent-test-spelling0ran clean
pnpm check:corpus-claim-drift0check-corpus-claim-drift: OK, no new claim sites beside a pinned spelling.
pnpm check:cross-package-test-inputs0All 117 self-test cases passed. + OK: 25 package(s) read outside themselves, all declared …
pnpm check:doc-authoring0✓ doc authoring guard: 46 published skill files clean — no internal issue-id references.
pnpm check:pm-governed-merges0✓ check-governed-merges --self-test: 243 assertions … + live: the real generator declared 9 output(s) and certified this tree
pnpm check:role-word0(above)
pnpm check:skill-compatibility0✓ check-skill-compatibility-version: 11 SKILL.md file(s) reconciled against 79 workspace packages
pnpm check:skill-frame-sync0✓ check-skill-frame-sync: 4 copies of the decision frame are structurally isomorphic across 3 files
pnpm check:skill-identifier-liveness0(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:

ReadingBefore (224f8ea4)After (482fb9c7)
os:check marker lines11, 166, 22111, 166, 221
marker count33
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/302identical
file lines306306

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 premiseWhat the tree says
1The example and note live in skills/objectstack-ui/SKILL.mdThey 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
2The 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
3The governing ceiling is 3,815 with headroom 03,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
4An author writing $currentUser gets a red os validate (filter-token-unknown) or a silent empty listOnly 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
5The ### Filtering example is a typed block checked by check:skill-examplesIt 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 added this pull request to the merge queueSep 3, 2026
@github-actions

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

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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xsskip-changesetPR 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

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

#SearchTreeResultPositive control on the same tree
1git grep -n -F '$currentUser' (all tracked paths)objectstack @ 224f8ea44 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 heresame grep finds the 4 hits, so the pattern is live
2git grep -ni 'currentuser' excluding *CHANGELOG.mdobjectstack @ 224f8ea4118 hits; zero are a filter-token vocabulary entrypackages/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
3git grep -n -F '$currentUser'objectui @ 67dadd62 hits, both packages/types/src/__tests__/phase2-schemas.test.ts:791,808ListViewSchema.safeParse(...)shape assertions on a tab's filter array. Neither asserts resolutioncurrent_user_id on the same tree: 14 hits in packages/core/src/utils/filter-tokens.ts and 14 in its test
4git grep -n -E "'\$[a-zA-Z]" over packages/core/src, packages/objectql, packages/lint/src, packages/spec/srcobjectstack @ 224f8ea4only Mongo-style operators ($in, $and, $or, $not, $gte …) and $source / $rootno $-prefixed session-token alias table anywherethe 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.

LayerSourceRecognition
Vocabularypackages/spec/src/data/context-tokens.zod.ts:84CONTEXT_TOKENS = ['current_user_id', 'current_org_id'] — the complete set
Recognition grammarpackages/spec/src/data/context-tokens.zod.ts:146FILTER_TOKEN_WRAPPED_RE = /^\$?\{([^{}]+)\}$/
Classifierpackages/spec/src/data/context-tokens.zod.ts:239-253classifyFilterToken returns null when that regex does not match — i.e. "not a placeholder", pass through verbatim
Client resolverobjectui packages/core/src/utils/filter-tokens.ts:107,135,209WHOLE_TOKEN_RE = /^\$?\{([a-zA-Z0-9_]+)\}$/, used by resolveContextTokens and resolveFilterPlaceholders
Server resolverpackages/core/src/utils/filter-tokens.ts:364resolveFilterTokens walks with the same classifyFilterToken; its hasFilterToken pre-pass returns false for a tree holding only $currentUser
Lint rulepackages/lint/src/validate-filter-tokens.ts:65,123-124filter-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 validateor 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:

ClaimSource line
{current_user_id} is a declared filter tokenpackages/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).

ReadingBeforeAfterDelta
rules/list-views.md bytes12,04312,039−4
rules/list-views.md tokens3,0113,010−1
rules/list-views.md lines3063060
package skills/objectstack-ui/ — hand-authored (ratcheted) tokens24,18924,188−1
package skills/objectstack-ui/ — whole package tokens (incl. generator-owned)33,89933,898−1
package skills/objectstack-ui/ — hand-authored lines1,9801,9800

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.

GateExitThe gate's own verdict line
node scripts/check-skills-token-ratchet.mjs0✓ 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-test0✓ check-skills-token-ratchet self-test: 64 cases pass.
pnpm check:skill-identifier-liveness0check-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-examples0✅ 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-docs0✅ Skill docs in sync
pnpm check:role-word0check-role-word: OK, no new occurrences of the reserved word. (Scanned: 224 .md/.mdx file(s) … skills 34)
pnpm check:nul-bytes0check-nul-bytes: OK (scanned 8052 text file(s) … no raw ASCII control bytes).
pnpm --filter @objectstack/lint run check:doc-formula-expressions0ran 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:

GateExitThe gate's own verdict line
node scripts/check-ci-filter-parity.mjs0OK: all 130 declared cross-package glob(s) (92 unique) are covered …
node scripts/check-cross-package-test-inputs.mjs0OK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
node scripts/check-shard-attestation.mjs0✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
node scripts/check-skills-token-ratchet.mjs0(above)
node scripts/check-test-completeness.mjs3NOT 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-expressions0(above)
pnpm check:agent-test-spelling0ran clean
pnpm check:corpus-claim-drift0check-corpus-claim-drift: OK, no new claim sites beside a pinned spelling.
pnpm check:cross-package-test-inputs0All 117 self-test cases passed. + OK: 25 package(s) read outside themselves, all declared …
pnpm check:doc-authoring0✓ doc authoring guard: 46 published skill files clean — no internal issue-id references.
pnpm check:pm-governed-merges0✓ check-governed-merges --self-test: 243 assertions … + live: the real generator declared 9 output(s) and certified this tree
pnpm check:role-word0(above)
pnpm check:skill-compatibility0✓ check-skill-compatibility-version: 11 SKILL.md file(s) reconciled against 79 workspace packages
pnpm check:skill-frame-sync0✓ check-skill-frame-sync: 4 copies of the decision frame are structurally isomorphic across 3 files
pnpm check:skill-identifier-liveness0(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:

ReadingBefore (224f8ea4)After (482fb9c7)
os:check marker lines11, 166, 22111, 166, 221
marker count33
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/302identical
file lines306306

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 premiseWhat the tree says
1The example and note live in skills/objectstack-ui/SKILL.mdThey 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
2The 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
3The governing ceiling is 3,815 with headroom 03,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
4An author writing $currentUser gets a red os validate (filter-token-unknown) or a silent empty listOnly 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
5The ### Filtering example is a typed block checked by check:skill-examplesIt 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 added this pull request to the merge queueSep 3, 2026
@github-actions

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

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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xsskip-changesetPR 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

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

#SearchTreeResultPositive control on the same tree
1git grep -n -F '$currentUser' (all tracked paths)objectstack @ 224f8ea44 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 heresame grep finds the 4 hits, so the pattern is live
2git grep -ni 'currentuser' excluding *CHANGELOG.mdobjectstack @ 224f8ea4118 hits; zero are a filter-token vocabulary entrypackages/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
3git grep -n -F '$currentUser'objectui @ 67dadd62 hits, both packages/types/src/__tests__/phase2-schemas.test.ts:791,808ListViewSchema.safeParse(...)shape assertions on a tab's filter array. Neither asserts resolutioncurrent_user_id on the same tree: 14 hits in packages/core/src/utils/filter-tokens.ts and 14 in its test
4git grep -n -E "'\$[a-zA-Z]" over packages/core/src, packages/objectql, packages/lint/src, packages/spec/srcobjectstack @ 224f8ea4only Mongo-style operators ($in, $and, $or, $not, $gte …) and $source / $rootno $-prefixed session-token alias table anywherethe 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.

LayerSourceRecognition
Vocabularypackages/spec/src/data/context-tokens.zod.ts:84CONTEXT_TOKENS = ['current_user_id', 'current_org_id'] — the complete set
Recognition grammarpackages/spec/src/data/context-tokens.zod.ts:146FILTER_TOKEN_WRAPPED_RE = /^\$?\{([^{}]+)\}$/
Classifierpackages/spec/src/data/context-tokens.zod.ts:239-253classifyFilterToken returns null when that regex does not match — i.e. "not a placeholder", pass through verbatim
Client resolverobjectui packages/core/src/utils/filter-tokens.ts:107,135,209WHOLE_TOKEN_RE = /^\$?\{([a-zA-Z0-9_]+)\}$/, used by resolveContextTokens and resolveFilterPlaceholders
Server resolverpackages/core/src/utils/filter-tokens.ts:364resolveFilterTokens walks with the same classifyFilterToken; its hasFilterToken pre-pass returns false for a tree holding only $currentUser
Lint rulepackages/lint/src/validate-filter-tokens.ts:65,123-124filter-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 validateor 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:

ClaimSource line
{current_user_id} is a declared filter tokenpackages/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).

ReadingBeforeAfterDelta
rules/list-views.md bytes12,04312,039−4
rules/list-views.md tokens3,0113,010−1
rules/list-views.md lines3063060
package skills/objectstack-ui/ — hand-authored (ratcheted) tokens24,18924,188−1
package skills/objectstack-ui/ — whole package tokens (incl. generator-owned)33,89933,898−1
package skills/objectstack-ui/ — hand-authored lines1,9801,9800

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.

GateExitThe gate's own verdict line
node scripts/check-skills-token-ratchet.mjs0✓ 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-test0✓ check-skills-token-ratchet self-test: 64 cases pass.
pnpm check:skill-identifier-liveness0check-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-examples0✅ 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-docs0✅ Skill docs in sync
pnpm check:role-word0check-role-word: OK, no new occurrences of the reserved word. (Scanned: 224 .md/.mdx file(s) … skills 34)
pnpm check:nul-bytes0check-nul-bytes: OK (scanned 8052 text file(s) … no raw ASCII control bytes).
pnpm --filter @objectstack/lint run check:doc-formula-expressions0ran 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:

GateExitThe gate's own verdict line
node scripts/check-ci-filter-parity.mjs0OK: all 130 declared cross-package glob(s) (92 unique) are covered …
node scripts/check-cross-package-test-inputs.mjs0OK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
node scripts/check-shard-attestation.mjs0✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
node scripts/check-skills-token-ratchet.mjs0(above)
node scripts/check-test-completeness.mjs3NOT 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-expressions0(above)
pnpm check:agent-test-spelling0ran clean
pnpm check:corpus-claim-drift0check-corpus-claim-drift: OK, no new claim sites beside a pinned spelling.
pnpm check:cross-package-test-inputs0All 117 self-test cases passed. + OK: 25 package(s) read outside themselves, all declared …
pnpm check:doc-authoring0✓ doc authoring guard: 46 published skill files clean — no internal issue-id references.
pnpm check:pm-governed-merges0✓ check-governed-merges --self-test: 243 assertions … + live: the real generator declared 9 output(s) and certified this tree
pnpm check:role-word0(above)
pnpm check:skill-compatibility0✓ check-skill-compatibility-version: 11 SKILL.md file(s) reconciled against 79 workspace packages
pnpm check:skill-frame-sync0✓ check-skill-frame-sync: 4 copies of the decision frame are structurally isomorphic across 3 files
pnpm check:skill-identifier-liveness0(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:

ReadingBefore (224f8ea4)After (482fb9c7)
os:check marker lines11, 166, 22111, 166, 221
marker count33
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/302identical
file lines306306

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 premiseWhat the tree says
1The example and note live in skills/objectstack-ui/SKILL.mdThey 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
2The 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
3The governing ceiling is 3,815 with headroom 03,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
4An author writing $currentUser gets a red os validate (filter-token-unknown) or a silent empty listOnly 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
5The ### Filtering example is a typed block checked by check:skill-examplesIt 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 added this pull request to the merge queueSep 3, 2026
@github-actions

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

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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xsskip-changesetPR 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

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

#SearchTreeResultPositive control on the same tree
1git grep -n -F '$currentUser' (all tracked paths)objectstack @ 224f8ea44 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 heresame grep finds the 4 hits, so the pattern is live
2git grep -ni 'currentuser' excluding *CHANGELOG.mdobjectstack @ 224f8ea4118 hits; zero are a filter-token vocabulary entrypackages/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
3git grep -n -F '$currentUser'objectui @ 67dadd62 hits, both packages/types/src/__tests__/phase2-schemas.test.ts:791,808ListViewSchema.safeParse(...)shape assertions on a tab's filter array. Neither asserts resolutioncurrent_user_id on the same tree: 14 hits in packages/core/src/utils/filter-tokens.ts and 14 in its test
4git grep -n -E "'\$[a-zA-Z]" over packages/core/src, packages/objectql, packages/lint/src, packages/spec/srcobjectstack @ 224f8ea4only Mongo-style operators ($in, $and, $or, $not, $gte …) and $source / $rootno $-prefixed session-token alias table anywherethe 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.

LayerSourceRecognition
Vocabularypackages/spec/src/data/context-tokens.zod.ts:84CONTEXT_TOKENS = ['current_user_id', 'current_org_id'] — the complete set
Recognition grammarpackages/spec/src/data/context-tokens.zod.ts:146FILTER_TOKEN_WRAPPED_RE = /^\$?\{([^{}]+)\}$/
Classifierpackages/spec/src/data/context-tokens.zod.ts:239-253classifyFilterToken returns null when that regex does not match — i.e. "not a placeholder", pass through verbatim
Client resolverobjectui packages/core/src/utils/filter-tokens.ts:107,135,209WHOLE_TOKEN_RE = /^\$?\{([a-zA-Z0-9_]+)\}$/, used by resolveContextTokens and resolveFilterPlaceholders
Server resolverpackages/core/src/utils/filter-tokens.ts:364resolveFilterTokens walks with the same classifyFilterToken; its hasFilterToken pre-pass returns false for a tree holding only $currentUser
Lint rulepackages/lint/src/validate-filter-tokens.ts:65,123-124filter-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 validateor 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:

ClaimSource line
{current_user_id} is a declared filter tokenpackages/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).

ReadingBeforeAfterDelta
rules/list-views.md bytes12,04312,039−4
rules/list-views.md tokens3,0113,010−1
rules/list-views.md lines3063060
package skills/objectstack-ui/ — hand-authored (ratcheted) tokens24,18924,188−1
package skills/objectstack-ui/ — whole package tokens (incl. generator-owned)33,89933,898−1
package skills/objectstack-ui/ — hand-authored lines1,9801,9800

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.

GateExitThe gate's own verdict line
node scripts/check-skills-token-ratchet.mjs0✓ 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-test0✓ check-skills-token-ratchet self-test: 64 cases pass.
pnpm check:skill-identifier-liveness0check-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-examples0✅ 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-docs0✅ Skill docs in sync
pnpm check:role-word0check-role-word: OK, no new occurrences of the reserved word. (Scanned: 224 .md/.mdx file(s) … skills 34)
pnpm check:nul-bytes0check-nul-bytes: OK (scanned 8052 text file(s) … no raw ASCII control bytes).
pnpm --filter @objectstack/lint run check:doc-formula-expressions0ran 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:

GateExitThe gate's own verdict line
node scripts/check-ci-filter-parity.mjs0OK: all 130 declared cross-package glob(s) (92 unique) are covered …
node scripts/check-cross-package-test-inputs.mjs0OK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
node scripts/check-shard-attestation.mjs0✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
node scripts/check-skills-token-ratchet.mjs0(above)
node scripts/check-test-completeness.mjs3NOT 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-expressions0(above)
pnpm check:agent-test-spelling0ran clean
pnpm check:corpus-claim-drift0check-corpus-claim-drift: OK, no new claim sites beside a pinned spelling.
pnpm check:cross-package-test-inputs0All 117 self-test cases passed. + OK: 25 package(s) read outside themselves, all declared …
pnpm check:doc-authoring0✓ doc authoring guard: 46 published skill files clean — no internal issue-id references.
pnpm check:pm-governed-merges0✓ check-governed-merges --self-test: 243 assertions … + live: the real generator declared 9 output(s) and certified this tree
pnpm check:role-word0(above)
pnpm check:skill-compatibility0✓ check-skill-compatibility-version: 11 SKILL.md file(s) reconciled against 79 workspace packages
pnpm check:skill-frame-sync0✓ check-skill-frame-sync: 4 copies of the decision frame are structurally isomorphic across 3 files
pnpm check:skill-identifier-liveness0(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:

ReadingBefore (224f8ea4)After (482fb9c7)
os:check marker lines11, 166, 22111, 166, 221
marker count33
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/302identical
file lines306306

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 premiseWhat the tree says
1The example and note live in skills/objectstack-ui/SKILL.mdThey 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
2The 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
3The governing ceiling is 3,815 with headroom 03,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
4An author writing $currentUser gets a red os validate (filter-token-unknown) or a silent empty listOnly 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
5The ### Filtering example is a typed block checked by check:skill-examplesIt 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 added this pull request to the merge queueSep 3, 2026
@github-actions

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

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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xsskip-changesetPR 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

@os-litant@os-zhuang@claude