Skip to content

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

Description

@claude

Found while funding the token budget for the Navigation Item Types rows (PR #14138); out of
scope there, filed unassigned for triage.

The contradiction, both halves inside one published file

skills/objectstack-ui/SKILL.md teaches $currentUser as a filter value in its
### Filtering section:

filter: [
  { field: 'status', operator: 'not_equals', value: 'closed' },
  { field: 'assigned_to', operator: 'equals', value: '$currentUser' },
]

followed by the note:

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

Roughly 1400 lines further down, the same file's ## Context Tokens section says the
opposite, and says it exhaustively:

Contract: CONTEXT_TOKENS in @objectstack/spec/data. These are the only two tokens
that resolve inside a filter value

naming {current_user_id} and {current_org_id}, resolved by resolveFilterPlaceholders
in @object-ui/core, and adding that os validate fails the build on an unresolvable
placeholder in any filter (rule filter-token-unknown) precisely because the runtime
failure is silent: an unresolved token reaches SQL as a literal, matches nothing, and the
surface renders empty.

Both claims cannot be true. One of the two sections is teaching a filter that does not work.

What the tree says

Repo-wide grep for the literal $currentUser (excluding node_modules):

  • skills/objectstack-ui/SKILL.md — the example and the note above (a third site, in
    Common Pitfalls, is deleted by PR docs(skills): document the action and component nav item types #14138 as ceiling funding);
  • packages/cli/src/commands/explain.ts — an { type: 'assignment', field: 'assigned_to', value: '$currentUser' } sample, i.e. a different surface (a default/assignment value,
    not a query filter);
  • docs/adr/0017-object-has-many-view.md — inside SQL-shaped prose, not a metadata example.

Nothing in packages/spec declares $currentUser as a filter token. The declared filter
vocabulary is CONTEXT_TOKENS (packages/spec/src/data/context-tokens.zod.ts), and its own
tests reject near-miss spellings.

Why it matters

This is a published, customer-loaded catalog. An AI author who reads ### Filtering — the
first place in the file that shows a "records assigned to me" filter — writes
value: '$currentUser' and gets either a red os validate (filter-token-unknown) or, if
the value slips past authoring, a list that silently renders nothing. The Context Tokens
section exists to prevent exactly that failure, and the Filtering section above it walks the
reader into it.

Decision the fix needs

Which half is wrong is not something this finding can settle from the docs alone:

  1. $currentUser is dead in filters — then the fix is to rewrite the ### Filtering
    example and its note to {current_user_id}, and the "only two tokens" claim stands.
  2. $currentUser really does resolve in some filter path (a legacy alias somewhere in
    the query layer) — then the Context Tokens section's "only two" is the false claim, and
    under contract-first the alias is what should go, with the declaration made to match.

Route 2 would be an ADR-0087 conversion-layer question rather than a docs edit, which is why
this is filed rather than fixed in passing. Note the file is at its token ceiling: any fix
here is size-neutral at best.

Generated by Claude Code


Generated by Claude Code

Activity

  1. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    Triage (R+89, triage seat, session session_019kDRpB7D2XzVzkaLp57T5D): pm:queue · priority:p2 · domain:skills · type Bug.

    The decision the card needs is answered by a reading, not a ruling: on origin/main (a39b02a6) the literal $currentUser has zero hits in packages/** outside tests and the explain.ts assignment sample (positive control: CONTEXT_TOKENS present in packages/spec/src/data/context-tokens.zod.ts). No legacy alias resolves it in any filter path ⇒ route 1: the ### Filtering example and its note in skills/objectstack-ui/SKILL.md are the false half; the ## Context Tokens section ("only two tokens") is true and stands. Fix = rewrite the example to {current_user_id} and the note to point at the Context Tokens contract; size-neutral or negative (the file is at its token ceiling — pay inside the file, per the ratchet). Governed surface (skills/**) ⇒ draft PR, human merge. Value gate (#13597): a published skill currently teaches a filter that either reds os validate or renders empty; correcting it changes what an author writes. p2: customer-loaded catalog, first "assigned to me" example in the file.

    ⛔ packages/cli/src/commands/explain.ts's $currentUser is a default-value sample on a different surface and is out of this card's scope; if it is also wrong it is a domain:cli card.

    Size/model suggestion: S, sonnet/opus.


    Generated by Claude Code

  2. added theissue type on Sep 2, 2026
  3. self-assigned this
    on Sep 3, 2026
  4. os-litant commented on Sep 3, 2026

    @os-litant
    Collaborator

    Claim: skills lane seat — pm:queue → pm:dispatched and the assignee set in the same write. Dispatching one os-dev flight now that the ui split (PR #14658) has merged and skills/objectstack-ui/SKILL.md is free.
    Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
    Branch: claude/issue-14139-currentuser-filter-token
    Clause-②: yes (conservatively — the fix states which tokens resolve in a filter value, a falsifiable contract claim; the conformance class is not mechanized, so the claim goes yes) ⇒ needs:contract-review on the PR, reviewed in-seat at CONTRACT_REVIEW_TIER before any human review request.

    Scope: settle the card's two-route question at source first — whether any filter path resolves $currentUser (resolveFilterPlaceholders in @object-ui/core, CONTEXT_TOKENS in packages/spec/src/data/context-tokens.zod.ts, the query layer, and the filter-token-unknown lint rule); if nothing does (route 1, the expected reading), rewrite the ### Filtering example and its note to {current_user_id} so the file agrees with its own Context Tokens section, size-neutral or smaller (the entry was re-locked at 3,815 by #14658, headroom 0). If a legacy alias does resolve it (route 2), stop and report with the evidence — that is an ADR-0087 conversion-layer question for a decision card, not a docs edit. ⛔ No other site; the explain.ts sample is a different surface. Governed skills/** ⇒ draft PR, first line Fixes #14139, skip-changeset.


    Generated by Claude Code

  5. os-litant commented on Sep 3, 2026

    @os-litant
    Collaborator

    os-dev-report

    {
      "issue": 14139,
      "status": "done",
      "branch": "claude/issue-14139-currentuser-filter-token",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/14781",
      "premise_still_valid": true,
      "summary": "Route 1, settled at source before any edit: nothing resolves the literal $currentUser in any filter path. Both resolvers recognise a placeholder only when the whole value is brace-wrapped (FILTER_TOKEN_WRAPPED_RE at packages/spec/src/data/context-tokens.zod.ts:146; WHOLE_TOKEN_RE at objectui packages/core/src/utils/filter-tokens.ts:107), so classifyFilterToken returns null for it and no alias table maps it anywhere. Fixed the example value and its note in skills/objectstack-ui/rules/list-views.md to {current_user_id}, keeping the example's shape; net -4 bytes (3011 -> 3010 tokens against an unchanged 3011 ceiling), so no payment and no restatement deletion was needed and the note itself shrank 71 -> 62 bytes. The card's CENTRAL premise held, but five incidental ones did not, all documented in the PR body: (1) the text lives in rules/list-views.md, not SKILL.md, since the ui split #14658 moved it; (2) the '## Context Tokens' section it quotes no longer exists anywhere under skills/ -- the surviving contract half is '## Date Macros - Filter Placeholders' in SKILL.md, so the contradiction spanned two files in one package rather than two halves of one file; (3) the 3,815/headroom-0 ceiling named in dispatch is SKILL.md's row, untouched here, while the governing row is ['skills/objectstack-ui/rules/list-views.md', 3011]; (4) the card's 'red os validate OR silent empty list' is only half true -- because $currentUser is UNBRACED it is not even a placeholder ATTEMPT, so filter-token-unknown never fires and resolveFilterTokens never throws, leaving only the silent-empty mode, which makes the defect strictly worse than filed; (5) the ### Filtering fence is NOT a typed block -- check:skill-examples extracts only fences carrying an os:check marker on the line directly above, and that fence has none (proved by fence census, 3 markers at lines 11/166/221 before and after, all ten fences at identical line numbers). One standing-clause conflict is flagged in open_questions: my dispatch template's skip-changeset closed list does not name skills/**, while the label's own criterion and merged precedent both say it applies here.",
      "tests": "All gates under scripts/pm/os-verify-lock.sh (slot issue-14139), every exit code captured by redirect BEFORE any pipe, each verdict quoted from the gate's own output. Union re-derived AFTER the last edit: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` reported 'gate list derived from the tree of objectstack-ai/objectstack at commit 482fb9c75' with a 1-path change set and named 15 commands; all 15 were run, plus every gate named in dispatch. The union and every gate were run at HEAD 482fb9c7, which is the final commit (git rev-parse HEAD == origin/claude/issue-14139-currentuser-filter-token == 482fb9c75f076beefcc7cdbe7ab80319cf694849). GREEN (exit 0): check-skills-token-ratchet ('36 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted', and per-file 'skills/objectstack-ui/rules/list-views.md is 3010 tokens (ceiling 3011; headroom 1)'); check-skills-token-ratchet --self-test ('64 cases pass'); check:skill-identifier-liveness ('Leg 1: 465 citation(s) over 46 published file(s) ...; Leg 2: 8 registered exhaustive section(s), 0 ledgered gap(s)' -- no bound heading moved, and none of the three ui Leg-2 bindings targets list-views.md); check:skill-examples ('256 prose examples type-check across 3 surface(s) -- every marked block parsed, so tsc ran the SEMANTIC pass on all of them'); check:skill-docs ('Skill docs in sync'); check:role-word ('OK, no new occurrences of the reserved word'); check:nul-bytes ('OK (scanned 8052 text file(s) ... no raw ASCII control bytes)'); check:doc-formula-expressions; check-ci-filter-parity; check-cross-package-test-inputs (both spellings); check-shard-attestation; check:agent-test-spelling; check:corpus-claim-drift; check:doc-authoring ('46 published skill files clean'); check:pm-governed-merges; check:skill-compatibility; check:skill-frame-sync. NOT MEASURED (not red, not green): check-test-completeness exit 3, its own text 'PREREQUISITE NOT MET -- this gate grades a saved turbo run test log, and no log was named' plus 'running the family locally, record this gate as NOT MEASURED'. check:skill-examples first returned exit 1 on a BUILD PREREQUISITE for an unrelated surface ('packages/client-react/dist holds no .d.ts declarations -- the package is not built'); I satisfied it (pnpm --filter '@objectstack/client-react...' build and '@objectstack/client...' build, both exit 0) and re-ran the gate, so its row above is a real measurement rather than a NOT MEASURED. Edit landed on disk by observation, not by an editor exit code: the replacement script refused unless both anchors matched exactly (it reported 'replacements: example=1 note=1'), and post-edit greps count 0 hits of the old literal and 2 of the new token, with bytes 12043 -> 12039. No ablation was performed -- the change is two whole-line replacements in a markdown file with no executable surface. Lock discipline: round 2 hit a queue-timeout (exit 99, 'never acquired ... NOT MEASURED'), the slot was kept, I did lock-free work and re-acquired in the same turn rather than ending on a wait.",
      "mcp_calls": "14 — CHANNEL SWITCH DECLARED: `gh` is absent from this container entirely (`command -v gh` empty), so no repo-scoped REST read was available and every GitHub read and write went through MCP. Breakdown: 2 card reads, 1 create_pull_request, 2 failed issue_read get_labels (that method cannot resolve a PR number: 'Could not resolve to an Issue with the number of 14781'), 1 pull_request_read get (full PR body read-back), 2 list_pull_requests (labels before and the comparative read-back after), 1 get_label (verifying needs:contract-review exists rather than creating a phantom label), 1 issue_write update (labels), 1 targeted search_issues (dedupe before filing the finding — non-vacuous: it returned #14139 itself as its only hit), 1 issue_write create (#14782), 1 add_issue_comment (this report), 1 issue_read get_comments (reading this comment back). No wide-table scans.",
      "open_questions": [
        {
          "question": "The os-dev standing clause gives skip-changeset a CLOSED list (docs/adr/**, .claude/**, scripts/pm/**, tests/workflow, comments) that does not name skills/**, while my dispatch, the claim comment, the label's own criterion and merged precedent all say a skills-only PR takes it. I applied it as dispatched. Which should move?",
          "options": [
            "A — the clause's closed list is under-enumerated: add skills/** to it. Evidence: the criterion is 'publishes nothing from any package', and skills/ appears in no package manifest's `files` and is copied into no published artifact; the Check Changeset job's only test is 'was a .changeset/*.md added', with no path exemption, so without the label a release-less PR is red by construction; and the three most recent skills-only merges (58ea39a5d #14658, 446117fc2 #14673, c985ae958 #14660) each carry no changeset.",
            "B — the closed list is right and skills/** should ship a changeset instead, because the published catalog is customer-loaded and a skill edit is user-visible even though no npm artifact moves. This would make the three merges above retroactively wrong and needs a maintainer ruling, not a seat decision."
          ],
          "recommendation": "A, and it is a documentation fix rather than a behaviour change — nothing about what ships would move. Long-term soundness (the axis that carries at least 50%) favours it: a closed list that disagrees with the criterion it claims to enumerate is the failure mode the clause exists to prevent, and every dev on a skills card must currently re-derive the answer from merged precedent, as I did. It also serves AI-authoring safety, since the alternative reading silently reds a whole lane. No real business need pulls toward B (no consumer reads a changeset for a skills edit), and startup scope discipline prefers correcting one list over adding a release artifact to every skills PR. B is not mine to choose in any case — it would invalidate three merged PRs, so it is a maintainer ruling."
        }
      ],
      "out_of_scope_findings": [
        "filed as #14782: `os explain flow`'s printed example (packages/cli/src/commands/explain.ts:128) teaches `$currentUser` as a flow `assignment` step value. Measured: that line is the ONLY occurrence of the literal in non-test, non-CHANGELOG source in the repository; the flow template vocabulary is brace-based (`{record.field}`, validate-flow-template-paths.ts) and the enumerated `$`-sentinels are `$root`/`$trigger`/`$record`/`$source` with no `$currentUser` among them; the acting-user token on the neighbouring value surface is bare `current_user` (DEFAULT_VALUE_TOKEN_CURRENT_USER, matched exactly). Filed unassigned, no labels, suggested domain:cli, with the unverified half marked explicitly — I did not execute an assignment node, so 'nothing resolves this spelling' is measured but 'therefore the literal string is stored into assigned_to' is not."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  6. os-litant commented on Sep 3, 2026

    @os-litant
    Collaborator

    Clause ② contract review — PASS — skills-lane seat (session session_01LraLgQVGq8egUwfYZpbYt1), done in-seat at CONTRACT_REVIEW_TIER (served model read via get_session at 01:13Z 09-03: session_context.model = last_served_model = the value CONTRACT_REVIEW_TIER names on main since PR #14644), verified at source on both repos' origin/main, not from the report.

    The claim under review (PR #14781 @ 482fb9c7): no filter path resolves the literal $currentUser; {current_user_id} is the declared token that resolves to the signed-in user's id. Read at objectstack df657d9df and objectui bf244f4, 01:13:59Z:

    • packages/spec/src/data/context-tokens.zod.ts:85 — current_user_id is in CONTEXT_TOKENS; :146 — FILTER_TOKEN_WRAPPED_RE = /^\$?\{([^{}]+)\}$/, so a value is a placeholder only when brace-wrapped; :176 — current_user_id: "The signed-in user's id (\sys_user.id`)."` — the new note's wording is the description's own.
    • objectui packages/core/src/utils/filter-tokens.ts:81-85 — the client alias table maps current_user, current_user_email, user_id, userid, me to current_user_id; no currentUser spelling, and :107 WHOLE_TOKEN_RE requires braces too. git grep -F '$currentUser' over objectui non-test source: zero hits.
    • objectstack non-test source: the only $currentUser outside skills/ is packages/cli/src/commands/explain.ts:128, an assignment default value — a different surface, now os explain flow's example teaches $currentUser as an assignment value, a spelling nothing resolves #14782 (dev-filed, bare; triage's).
    • Route 1 is settled at source; route 2 (an ADR-0087 conversion-layer alias) does not arise. The dev's sharper reading also holds: unbraced, the literal is not a placeholder attempt, so filter-token-unknown never fires — only the silent-empty failure mode is reachable.

    ⇒ Per the 2026-08-31 ruling the same seat strips needs:contract-review at PASS: stripped on PR #14781 in this round and read back. This card never carried the label (the dev's clause-② write landed on the PR only), so there is nothing to strip here. ACCEPT follows on CI green.


    Generated by Claude Code

  7. os-litant commented on Sep 3, 2026

    @os-litant
    Collaborator

    ACCEPT — skills-lane seat (session session_01LraLgQVGq8egUwfYZpbYt1), reviewer of record, verified against GitHub and both repos' origin/main, not the report. The clause ② contract review is the comment above (5518815530, PASS at source; needs:contract-review stripped on PR #14781 and read back at 01:16Z as documentation · size/xs · skip-changeset).


    Generated by Claude Code

  8. os-litant commented on Sep 3, 2026

    @os-litant
    Collaborator

    MERGED — PR #14781 @ 482fb9c7 landed on main as 521eaf9e41 (queue commit 02:33:16Z; this card closed by os-zhuang 2026-09-03T03:52:47Z). Session: session_01LraLgQVGq8egUwfYZpbYt1


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdocumentationImprovements or additions to documentationdomain:skillspriority:p2Medium: important, M3

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions