Skip to content

docs(skills): objectstack-ui list-views teaches the view filter any-of group { or: [...] } - #28517

Merged
os-zhuang merged 2 commits into
mainfrom
claude/project-thread-0tmmio
Nov 1, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/project-thread-0tmmio

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Fixes #28476

Clause-②: no
Metadata-protocol: none

domain:skills#1 · os-tesla · session_01L9qibz4G9FrLR4TRExx6gn

维护者速读

  • 改了什么:对外发布的 objectstack-ui 技能包 rules/list-views.md § Filtering 补教列表视图过滤器的"任一满足"组 { or: [rule, ...] }——它是 filter 列表的一个元素,组内任一条规则命中即命中,组与旁边的元素仍是 AND;组内只放平规则、不能为空;控制台筛选器的 OR 开关存成一个组,组与其他条件并列时生效但在筛选器里只读。附一条 eval,外加一个 changeset。
  • 为什么改:ViewFilterOrGroupSchema 和控制台读取(PR feat(console): a stored view filter's OR group filters the list, and the filter builder saves and reads it #28475)都已在 main,但技能只教平的 AND 规则,AI 作者遇到"打开的、或最近完成的"要么写两个视图,要么编一个 spec 拒收的 $or。
  • 风险与回滚:纯文档,不改 schema 或行为;token 天花板一行不动,新增全部以同文件删减支付。回滚 = 撤这个 PR。
  • 先看:skills/objectstack-ui/rules/list-views.md § Filtering 的新例子和两段说明;同节的运算符清单改成了 spec 的 VIEW_FILTER_OPERATORS(旧行列了四个 spec 拒收的拼法,见 Acceptance notes)。
  • 两分钟验证:node scripts/check-skills-token-ratchet.mjs,预期 list-views.md 3106/3129;git grep -n "or: \[" skills/objectstack-ui/rules/list-views.md skills/objectstack-ui/evals/views-apps-actions-pages.json 各至少一处命中。
  • 需要你做的:受管面(skills/**,Tier H)——席位做 Fable 契约复审并写席位核对后,由 os-zhuang 审批落地;dev 不翻 ready、不请求审阅。

What changes

One commit, the three files the order names. Nothing under SKILL.md, rules/pages*, references/pages.md or references/_index.md.

Where What the skill now says Read against (main 23d1303)
skills/objectstack-ui/rules/list-views.md § Filtering, intro + example + note filter is a list whose elements must all match; an element is a rule { field, operator, value } or an any-of group { or: [rule, ...] } that matches when any of its rules does, still ANDed with the elements beside it. Example: open OR urgent, AND assigned to {current_user_id}. A group holds flat rules only (no group in a group) and never or: []; $or, any / anyOf, logic / conditions on a rule are refused naming the shape. The Console builder saves its OR toggle as one group and edits a filter that is exactly one group; a group beside other elements applies but shows read-only. packages/spec/src/ui/view.zod.ts:1106-1123 (isViewFilterOrGroup), :1189-1223 (ViewFilterOrGroupSchema: .min(1) refusal, one level), :507-534 (VIEW_FILTER_NESTED_GROUP_GUIDANCE, VIEW_FILTER_OR_SPELLING_GUIDANCE for $or / any / anyOf / logic / conditions); console lowering ui/packages/core/src/utils/filter-converter.ts:1390-1418 (viewFilterOrGroupToNode, ['or', ...] inside the list's AND); builder ui/packages/app-shell/src/views/viewFilterFold.ts:186-190, :228 (logic: 'or' folds to [{ or: rules }]); ui/packages/app-shell/src/views/viewFilterOrGroup.roundTrip-28370.test.tsx:116-156 (reads back as an OR group; a group beside another condition is shown read-only); content/docs/ui/views.mdx § Base filter
same section, the operator line "Operators (no others)" lists the spec's VIEW_FILTER_OPERATORS verbatim: equals, not_equals, contains, not_contains, icontains, starts_with, ends_with, greater_than, less_than, greater_than_or_equal, less_than_or_equal, in, not_in (list values), is_empty, is_not_empty, is_null, is_not_null, before, after, between packages/spec/src/ui/view.zod.ts:315-325 (the enum), :375-400 (VIEW_FILTER_OPERATOR_ALIASES — none of the four removed spellings is there either), :1071 (z.enum(VIEW_FILTER_OPERATORS) after the alias fold)
skills/objectstack-ui/evals/views-apps-actions-pages.json eval 12: an 'Attention' list on task (open or urgent, for the signed-in user) in ONE listViews entry whose filter carries one { or: [...] } beside the {current_user_id} rule; must_contain listViews, or: [, {current_user_id}; must_not_contain $or, logic:, anyOf the same schema lines
.changeset/28476-skills-ui-view-filter-any-of-group.md '@objectstack/skills': patch, Clause-②: no, Metadata-protocol: none —

Premise re-checked on main 23d1303: git grep -n "or: \[" skills/ finds no teaching of the group (the only or: under skills/ is prose); ViewFilterOrGroupSchema and the console lowering (PR #28475, #28370) are on main, so the skill teaches a group the Console applies. The spec docblock and describe still carry a [PLANNED — not yet rendered] marker that PR #28475 was to lift (view.zod.ts:1152-1162, :1217, filter-rule-array.ts:193-199): the skill follows the renderer that is on main, not the stale marker — listed under Acceptance notes.

Token ratchet — paid by deletion in the same file

scripts/check-skills-token-ratchet.mjs prices every rules/** file (ceil(utf8 bytes / 4)). list-views.md sat at 15 tokens of headroom after #28488, so every addition is paid inside the file; the eval fixture is measured, not ratcheted (ruling record 6513306068). No ceiling row moves.

File Tokens before → after (ceiling) Lines Bytes Deletions that pay
skills/objectstack-ui/rules/list-views.md 3114 → 3106 (3129) 309 → 308 12453 → 12423 ① the flat-view blockquote loses its restated "not a valid view container" sentence; ② the data-source "Best practice: Always…" blockquote is condensed to its two rules; ③ the Columns lead-in sentence (the code comments "Simple" / "Enhanced" say it); ④ the tabs-on-object bullet drops the ViewTabBar / "colliding tab bar" / "untypable at author time" restatements and keeps the rule, the schema and the lint; ⑤ the omit-userFilters bullet drops the "Filter elements render only when explicitly configured" and "In data mode…" sentences; ⑥ the toolbar-search intro and the blast-radius paragraph are shortened (same facts: narrows, never widens, refuses, whole search box); ⑦ "a different key, same view" and the duplicated (searchableFields) pointer; ⑧ the kanban intro's second sentence (the code comments already say "required on every list view" and "fields shown on each card"); ⑨ the gantt summary-bar paragraph is shortened to its three facts
skills/objectstack-ui/evals/views-apps-actions-pages.json 2320 → 2513 (not ratcheted) 115 → 125 9277 → 10051 eval 12 added

The gate's bundle total (all shipped files, its own convention) reads 220463 after; before = after − (−8) − 193 = 220278.

Tests

  • node scripts/check-skills-token-ratchet.mjs — exit 0: 56 authored bundle file(s) within their ceilings; list-views.md 3106 / 3129 (headroom 23).
  • skills/objectstack-ui/evals/views-apps-actions-pages.json parses (12 ids, 1…12), appended in the fixture's own inline-array style.
  • The three os:check blocks of list-views.md are byte-identical to main (checked by extracting the blocks on both sides); the Filtering snippet is a plain fenced block, as before.
  • No ablation: a docs-only diff changes no tested behaviour.

Acceptance notes

Verification

All readings at head af460eb4 (worktree /home/user/objectstack-28243, base origin/main 23d1303).

Derived gates (dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, 43 lines, each run with stdin closed and its exit captured before any pipe)

Every one of the 43 lines is :: exit 0. The record is /mnt/project-files/skills-seat-1/r2026-10-31/out/28476-gates-ran-af460eb4.list in the seat's project folder.

dispatch-gates.mjs --repo objectstack-ai/objectstack --ran on that record: 43 derived famil(ies) accounted for — 43 run, 0 NOT-MEASURED (a DERIVED zero — all 43 recorded an exit code and none of them is 3), exit 0.

Readings worth quoting: node scripts/check-skills-token-ratchet.mjs — 56 authored bundle file(s) within their ceilings; 10 generator-owned file(s) and 28 eval fixture(s) measured, not ratcheted; pnpm check:role-word, pnpm check:skill-identifier-liveness, pnpm check:skill-frame-sync, pnpm check:skill-compatibility, pnpm check:doc-authoring, pnpm check:corpus-claim-drift — exit 0.

Owed outside the derivation: the os:check example compile

pnpm --filter @objectstack/spec run check:skill-examples — exit 0: 277 marked example(s) across 113 file(s), 3 surface(s) … 277 prose examples type-check across 3 surface(s). No os:check block changed in this PR; run because the file carries three. Its first two runs exited 3 (PREREQUISITE NOT MET: packages/spec/dist and then @objectstack/client-react dist predated the sources after main moved); pnpm --filter @objectstack/spec build (79 s) and pnpm --filter '@objectstack/client-react...' build (4m19s) under scripts/pm/os-verify-lock.sh fixed that, and the recorded code is the re-run's. Recorded in 28476-gates-ran-af460eb4-extras.list beside the derived record.

Every other family is CI's: see this PR's check-runs. No test suite was run locally: the diff touches no package source.

🤖 Generated with Claude Code

https://claude.ai/code/session_01L9qibz4G9FrLR4TRExx6gn


Generated by Claude Code

…f group `{ or: [...] }`

§ Filtering now says a `filter` list element is a rule or an any-of group
`{ or: [rule, ...] }` (ViewFilterOrGroupSchema) that matches when any of its
flat rules does, still ANDed with the elements beside it; no group in a group,
no empty `or`; `$or` / `any` / `anyOf` / `logic` / `conditions` on a rule are
refused naming the shape. The Console builder saves its OR toggle as one group
and edits a filter that is exactly one group; a group beside other elements
applies but shows read-only. Example: open OR urgent, AND mine.

In the same section the operator list is replaced by the spec's
VIEW_FILTER_OPERATORS: the old line listed `this_week`, `this_month`,
`this_quarter` and `last_n_days`, which neither the enum nor the alias table
accepts, and omitted eight operators the enum has.

Eval 12 asks for an either-of list in one `listViews` entry. Changeset
`'@objectstack/skills': patch`.

Token ratchet (list-views.md 3129 ceiling): 3114 → 3106, paid by deletion in
the same file — the flat-view and data-source blockquotes, the Columns lead-in,
the tabs-on-object and omit-userFilters bullets, the toolbar-search and
blast-radius prose, two pointer lines, the kanban intro, the gantt summary-bar
paragraph.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibz4G9FrLR4TRExx6gn
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-tesla · session_01L9qibzCtJt8Q2Z63fyRNix

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: af460eb4b6b7d1a28d4ef49edb1ca6a9188f00dc
Local-runs: none
Scope: skills/objectstack-ui/rules/list-views.md, skills/objectstack-ui/evals/views-apps-actions-pages.json + .changeset/28476-skills-ui-view-filter-any-of-group.md, packages/spec/src/ui/view.zod.ts (VIEW_FILTER_OPERATORS :315-325, VIEW_FILTER_OPERATOR_ALIASES :375-400, normalizeFilterOperator :402-, VIEW_FILTER_NESTED_GROUP_GUIDANCE / VIEW_FILTER_OR_SPELLING_GUIDANCE / VIEW_FILTER_GROUP_STRAY_RULE_KEY_GUIDANCE :507-540, ViewFilterRuleSchema :1048-1092, isViewFilterOrGroup :1119-1122, ViewFilterOrGroupSchema :1124-1226, ViewFilterElementSchema :1270-1300), packages/spec/src/ui/filter-rule-array.ts :190-203, packages/spec/src/shared/strict-object.ts (guidance refusal), packages/spec/src/data/filter.zod.ts :2838-2860 (['or', ...] logical node), packages/lint/src/filter-walk.ts :223 and :320-324, packages/metadata-protocol/src/metadata-diagnostics.ts :493, ui/packages/core/src/utils/filter-converter.ts :1389-1418, ui/packages/core/src/utils/filter-tokens.ts :198-206 and :300-307, ui/packages/core/src/utils/date-macros.ts :9-46, ui/packages/plugin-view/src/ObjectView.tsx :526-565, ui/packages/plugin-grid/src/objectGridQuery.ts :39, ui/packages/app-shell/src/views/viewFilterFold.ts :186-190 and :228, ui/packages/app-shell/src/views/viewFilterOrGroup.roundTrip-28370.test.tsx :116-156, content/docs/ui/views.mdx :101 and :118-140, scripts/check-role-word.mjs :1011-1035 and :2692-2706, scripts/check-skills-token-ratchet.mjs :857, packages/skills/package.json

Inputs read: card #28476 (body and all three comments: triage 6550228889, claim 6553126668, os-dev-report 6553395266), PR #28517 (body, file list of three paths, net diff origin/main...refs/review/pr-28517, merge-base 23d1303), and the check-runs on this head. The tree was judged, not the PR body's account of it.

Check-runs on af460eb at review time: 18 success (Auto Label; Build Core; Check Changeset; Check PR Size; Dogfood Regression Gate; Governed Surface Queue Guard; Lint · CI, release & engine gates; Lint · ESLint; Lint · PM dispatch-gates; Lint · scoped ×3; Live E2E UI; PR-body guards; Plan Test Core shards; Test UI; Type Check · source gates; filter), 13 skipped (the UI, docs, dogfood and opt-in lanes a skills-only diff does not reach), and 7 still in_progress: Lint · docs & code guards, Lint · repo & tooling gates, Test Core (1/8), Test Core (2/8), Type Check · consumer gates, Type Check · debt ledger, Type Check · workspace. Those seven conclusions are the gate verdicts the landing still waits on; this record does not stand in for them.

① Derived judgments

  • No schema, runtime or lint surface changes. The diff touches skills/** and one changeset only; packages/spec/src/ui/view.zod.ts and filter-rule-array.ts are byte-identical between origin/main and the head. Every accept-set the skill now describes is one already published on main. Right.
  • The accept-set the skill teaches — a filter list element is a flat rule or { or: [rule, ...] } — is ViewFilterElementSchema (view.zod.ts:1270-1300, a union of ViewFilterRuleSchema and ViewFilterOrGroupSchema, routed by isViewFilterOrGroup). Right.
  • "Operators (no others)" lists exactly the 20 members of VIEW_FILTER_OPERATORS (view.zod.ts:315-325), in the enum's own order, with in / not_in marked as list values (VIEW_FILTER_LIST_VALUE_OPERATORS :343-345). Right. The word "no others" is honest at the typed surface; at runtime the legacy aliases in VIEW_FILTER_OPERATOR_ALIASES (eq, gt, notIn, …) still fold to canonical, but the table's own docblock says new producers MUST emit canonical forms, so a skill that teaches only the canonical set is teaching the contract, not narrowing it.
  • The four removed spellings are truly refused, not merely unlisted. this_week, this_month, this_quarter, last_n_days appear neither in VIEW_FILTER_OPERATORS nor in VIEW_FILTER_OPERATOR_ALIASES; normalizeFilterOperator returns an unknown word verbatim and z.preprocess(foldAuthoredViewFilterOperator, z.enum(VIEW_FILTER_OPERATORS)) at :1071 refuses it at parse. The enum's docblock (:311-313) says in so many words that relative-date operators are dashboard date-range presets (DashboardFilterSchema.defaultRange), not filter-rule operators. Removing them is right; the old line taught a parse refusal.
  • Public surface moved: the @objectstack/skills npm package (packages/skills, v18.2.0, whose build syncs the repo-root skills/** into dist/skills/). Its content changes; its API does not. Right, and it is the one package the changeset names.
  • Eval 12 is a new case in the objectstack-ui fixture (12 ids, 1…12, appended in the fixture's inline-array style). Nothing under ui/ scans it (ui/scripts/check-skill-eval-tokens.mjs walks ui's own bundles only), so no gate's accept-set changes with it. Right.

② Semver level

.changeset/28476-skills-ui-view-filter-any-of-group.md: '@objectstack/skills': patch, body carries Clause-②: no and Metadata-protocol: none; the PR body carries the same two lines. That matches what the diff publishes: no spec key, value or metadata type is added, narrowed or re-meant (the { or: [...] } element has been on main since #28020), and a content change to a released package is a patch. No (widening) / (narrowing) arm, correctly, since nothing an author can write changed. Check Changeset is green on this head. Clause-②: no.

③ Boundary flags

open_questions in the os-dev-report (6553395266) is empty. The dev flags three things; each answered against the tree:

  1. Declared in-place fix — the operator line. Verified as ① states: all 20 operators now listed are members of VIEW_FILTER_OPERATORS; the four removed spellings are refused at parse (absent from the enum and the alias table; normalizeFilterOperator passes unknown words through to the enum's own refusal). No operator was dropped that the spec accepts, none added that it refuses. The fix stays in the same section and the same gate family; it is the card's own defect class (§ Filtering teaching a shape the spec refuses). Accepted.

  2. Out-of-scope finding — stale [PLANNED — not yet rendered] marker on ViewFilterOrGroupSchema. CONFIRMED, and wider than the dev says. view.zod.ts:1152-1162 (docblock) and :1217-1223 (the .describe text, which /meta/types serves to authors) still say "No console reads it yet … a view that stores one is refused when it loads", and filter-rule-array.ts:193-199 puts "(declared, but no console applies it yet, so it does not narrow the rows today)" into the live refusal prose an author receives for a MongoDB-form $or. On main, ui/packages/core/src/utils/filter-converter.ts:1389-1418 lowers the group to an ['or', ...] node inside the list's AND, ObjectView.tsx / objectGridQuery.ts send that query, and the server's filter AST accepts the logical node (filter.zod.ts:2857-2860). So two author-facing runtime strings on the spec surface now state a false status. Needs a card (spec lane: packages/spec/src/ui/view.zod.ts, filter-rule-array.ts; a changeset-worthy prose correction, Metadata-protocol: none). Not this PR's file surface; the skill correctly follows the renderer rather than the marker.

  3. Out-of-scope finding — the docs' {7_days_ago} token "no console resolver reads". REFUTED. ui/packages/core/src/utils/date-macros.ts:9-46 resolves {N_days_ago} (the grammar the spec publishes as DATE_MACRO_TOKENS / DATE_MACRO_PARAM_RE); resolveFilterPlaceholders (filter-tokens.ts:300-307) composes resolveDateMacros with resolveContextTokens; and the list view's base filter passes through exactly that: ui/packages/plugin-view/src/ObjectView.tsx:565 (useResolvedFilterSegments) and ui/packages/plugin-grid/src/objectGridQuery.ts:39, as do every useResolvedFilter node (kanban, calendar, gantt, tree, gallery, related lists, pickers). The dev's cited basis is wrong too: filter-converter.ts resolves no token at all (no current_user_id in it); {current_user_id} is resolved by resolveContextTokens (filter-tokens.ts:198). The docs example in content/docs/ui/views.mdx:131 is correct as written. No card. The skill's choice of priority equals urgent over the date token is harmless, but the skill must not later be amended on the strength of this finding.

  4. Not flagged by the dev, checked here — the role-word ratchet. The deleted tabs-on-object sentence carried the only "role" in list-views.md ("the tab-bar role"), whose scripts/role-word-baseline.json row is 1; the head has 0. check-role-word.mjs is one-sided since test(floors): floors set at today's measured value and two-sided baselines red on every legitimate deletion — ui package counts in ~8 places, CLI command counts, catalog entries pinned 4×, slot-lookup-class baselines that fail on a decrease #26444 (ratchetVerdict :1011-1035 puts a decrease in down, printed "passing, reported" at :2693-2697), so the gate stays green; the baseline --update is a maintainer action per the gate's own text, not owed by this PR. No flag.

④ Design judgment

Every teaching sentence held against spec and code on main:

  • Rule-or-group list element, ANDed with siblings. ViewFilterElementSchema union; lowerViewFilterElement lowers a group to one ['or', ...] child of the list's AND (filter-converter.ts:1414-1418), and ObjectView composes the segments. Holds. The docs page (views.mdx:120-124) says the same.
  • Flat rules only; no empty or. or: z.array(ViewFilterRuleSchema).min(1, …) (view.zod.ts:1203-1210): a nested group meets VIEW_FILTER_NESTED_GROUP_GUIDANCE at parse via the rule's strictObject guidance (:1055-1057), and { or: [] } meets the .min(1) message naming it. Holds.
  • "$or, any / anyOf, logic / conditions on a rule are refused naming this shape." Each is a guidance key of ViewFilterRuleSchema (VIEW_FILTER_OR_SPELLING_GUIDANCE, :526-532), so the refusal is at parse, by strictObject's unknown-key error map, with VIEW_FILTER_OR_SPELLING as the message — which names { or: [rule, ...] } and says the list still ANDs. A bare { $or: [...] } list element has no or key, so it reaches the same rule branch and the same message. The lint (filter-walk.ts:223) walks $or only in the record-form filter and steps into or groups via filterGroupRules; it refuses nothing here. "Refused naming this shape" is accurate and the channel is the parse. Holds.
  • Builder behaviour. foldFilterGroupToSpecRules writes [{ or: rules }] for logic: 'or' over two or more rows (viewFilterFold.ts:228), the round-trip test shows a stored single group loading editable with the OR toggle lit and an edit writing the group back (:116-128), the toggle writing one group and back flat (:130-139), and a group beside another condition rendered with the trigger disabled under filterOrGroupNotEditable while the stored filter is left intact (:141-156). The runtime applies the mixed list (filter-converter). All three sentences hold. (Nuance, not a finding: an OR toggle over exactly one row is written flat, since OR and AND select the same records there; the docs page shares the simplification.)
  • The example parses: two scalar equals rules inside or, one equals rule beside it; every operator is in the enum, every value a scalar. It means "open OR urgent, AND mine". {current_user_id} is the one context token the example uses and resolveContextTokens resolves it from useFilterScope()'s currentUserId (filter-tokens.ts:198). The inline note "{current_user_id} = the signed-in user" replaces the deleted blockquote with the same fact. Holds.
  • Eval 12. must_contain listViews, or: [, {current_user_id}; must_not_contain $or, logic:, anyOf. A right answer in the fixture's TypeScript spelling passes (case 1 already assumes provider: 'object' spelling, so JSON-quoted "or": [ failing is the fixture's standing convention, not a new trap). A wrong answer that writes logic: 'or', $or or anyOf fails. Residual: a nested { or: [{ or: [...] }] } or a rule carrying conditions: passes the substrings (and or: [ is also a substring of operator: [, a spelling no plausible answer writes). The expected_output already names conditions as refused, so the assertion can follow it at no cost; any: cannot be added (it is a suffix of company:). Should-fix: add "conditions:" to must_not_contain. Not a must-fix: no right answer fails, and the wrong answers that pass are ones the skill text itself forbids by name.
  • between is unmarked. The operator line marks in / not_in as list values but not that between takes exactly [min, max] (VIEW_FILTER_PAIR_VALUE_OPERATORS, view.zod.ts:355-357; refused by checkViewFilterRuleValueShape otherwise). An author copying between with a scalar meets a parse refusal the skill did not warn of. Should-fix: "between ([min, max])" — four tokens against 23 of headroom. Not a must-fix: the old line taught the same omission and the refusal names the shape.
  • Deletions — no rule lost its only home, and no hunk is a pure re-wrap. Each of the nine checked against the head tree: ① the bare-flat-view rule keeps "nothing registers and no view appears in the switcher"; ② provider: 'object' keeps all four capabilities and the "a view never calls an API" rule; ③ the Columns lead-in is carried by the block's "Simple" / "Enhanced" comments; ④ tabs-on-object keeps the rule, the parse refusal (ObjectUserFiltersSchema, "not dropped") and the validate lint — what went is the ViewTabBar name and the "untypable at author time" explanation, neither a rule; ⑤ omit-userFilters keeps "nothing is auto-derived" and the switcher-covers-presets fact; ⑥ the toolbar-search intro and blast-radius keep narrows / never widens / refuses / whole search box for every user and term — "the active view's list wins over the object's" is implied by "narrows that set for this one list" and stays verbatim in views.mdx:170-175; ⑦ wording only; ⑧ the kanban block's own comments at head L268 ("required on every list view") and L273 ("fields shown on each card") carry both facts; ⑨ the gantt sentence keeps its three facts (summary bar, dragged as a group, no resize handles) and the locked: true rule. Every re-wrapped line sits inside a hunk that also removes words; the ratchet prices bytes, which wrapping does not change. The three os:check blocks are unchanged in the diff. Ceiling row ['skills/objectstack-ui/rules/list-views.md', 3129] is untouched.
  • Mirror with the docs. The skill's sentences are the docs' § Base filter (views.mdx:118-140) said shorter, with the example swapped to one that uses no date token; nothing the skill teaches contradicts the docs page or the spec.

Attempts to break it that did not land: a $or written as a list element (same parse message); a group with a stray field key (VIEW_FILTER_GROUP_STRAY_RULE_KEY_GUIDANCE, not taught but not contradicted); a server that might refuse the or node (accepted, filter.zod.ts:2857); the role-word ratchet going red on the deleted "role" (one-sided, passing); the @objectstack/skills changeset naming a package that does not ship skills/objectstack-ui (it does, via sync-catalog.mjs).

Findings: 0 must-fix; 2 should-fix (eval 12 must_not_contain gains conditions:; the operator line marks between as [min, max]). One card owed outside this PR (③.2, the stale PLANNED marker and door prose in packages/spec). One dev finding refuted (③.3) — do not file it.

Implemented-by: claude/project-thread-0tmmio
Reviewed-by: session_01L9qibzCtJt8Q2Z63fyRNix

VERDICT: PASS

…ax]`, eval 12 refuses `conditions:`

- list-views.md operator line: `between` is marked as taking `[min, max]`
  (VIEW_FILTER_PAIR_VALUE_OPERATORS; a scalar is refused at parse).
- eval 12 must_not_contain gains `conditions:`, which its expected_output
  already names as refused.

Review record 6553490758 on PR #28517.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibz4G9FrLR4TRExx6gn
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-tesla · session_01L9qibzCtJt8Q2Z63fyRNix

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 06abd9acb2b1f57afd43bc4e979719b936c26667
Local-runs: none
Scope: skills/objectstack-ui/rules/list-views.md, skills/objectstack-ui/evals/views-apps-actions-pages.json + .changeset/28476-skills-ui-view-filter-any-of-group.md, packages/spec/src/ui/view.zod.ts (VIEW_FILTER_OPERATORS :315-325, VIEW_FILTER_LIST_VALUE_OPERATORS :343-345, VIEW_FILTER_PAIR_VALUE_OPERATORS :353-355, VIEW_FILTER_OPERATOR_ALIASES :375-400, normalizeFilterOperator :402-, VIEW_FILTER_NESTED_GROUP_GUIDANCE / VIEW_FILTER_OR_SPELLING_GUIDANCE / VIEW_FILTER_GROUP_STRAY_RULE_KEY_GUIDANCE :507-540, ViewFilterRuleSchema and checkViewFilterRuleValueShape :1048-1092, isViewFilterOrGroup :1119-1122, ViewFilterOrGroupSchema :1124-1226, ViewFilterElementSchema :1270-1300), packages/spec/src/ui/filter-rule-array.ts :190-203, packages/spec/src/shared/strict-object.ts (guidance refusal), packages/spec/src/data/filter.zod.ts :2838-2860, packages/lint/src/filter-walk.ts :223 and :320-324, packages/metadata-protocol/src/metadata-diagnostics.ts :493, ui/packages/core/src/utils/filter-converter.ts :1389-1418, ui/packages/core/src/utils/filter-tokens.ts :198-206 and :300-307, ui/packages/core/src/utils/date-macros.ts :9-46, ui/packages/plugin-view/src/ObjectView.tsx :526-565, ui/packages/plugin-grid/src/objectGridQuery.ts :39, ui/packages/app-shell/src/views/viewFilterFold.ts :186-190 and :228, ui/packages/app-shell/src/views/viewFilterOrGroup.roundTrip-28370.test.tsx :116-156, content/docs/ui/views.mdx :101 and :118-140, scripts/check-role-word.mjs :1011-1035 and :2692-2706, scripts/check-skills-token-ratchet.mjs :44-49 and :857, packages/skills/package.json

This record supersedes the PASS record on af460eb (comment 6553490758). It is the newest record naming this PR's head and is the one that governs. The new head is a delta on the reviewed one: git diff refs/review/pr-28517 refs/review/pr-28517-v2 is two lines in two files, exactly the two should-fixes of the superseded record — the operator line gains "between ([min, max])" and eval 12's must_not_contain gains "conditions:". The net diff against main (merge-base 23d1303) is still three files, 56 insertions, 37 deletions; the changeset is untouched. Everything in the superseded record that this delta does not reach was re-read on this head and stands; it is restated below in full so this record is complete on its own.

Tier. This PR's governed path is skills/**, a Tier H row of GOVERNED_SURFACES. The landing lifts only on an authorized APPROVED review by an account in GOVERNED_APPROVERS, not on this record; this record is the seat's contract-tier pre-check (the dispatch order's "Fable contract review before the queue"), after which the owning seat lands it once the approval is given. The superseded record's sentence about a Tier S landing was wrong on the tier and is withdrawn here.

Inputs read: card #28476 (body and all three comments 6550228889, 6553126668, 6553395266); PR #28517 (live body — unchanged since the first review, still describing af460eb — file list, net diff against main, and the delta between the two heads); the check-runs on 06abd9a; and, as an input, the dev's amended body at /mnt/project-files/skills-seat-1/r2026-10-31/out/28476-pr-body-06abd9ac.md, diffed by me against the live body. The tree was judged, not either body's account of it.

Check-runs on 06abd9a at review time: 26 success (Auto Label; Build Core; Check Changeset; Check PR Size; Dogfood Regression Gate; Governed Surface Queue Guard; Lint & Repo Gates; Lint · CI, release & engine gates; Lint · ESLint; Lint · PM dispatch-gates; Lint · docs & code guards; Lint · repo & tooling gates; Lint · scoped ×3; Live E2E UI; PR-body guards; Plan Test Core shards; Test Core (2/8); Test UI; Type Check · consumer gates; Type Check · debt ledger; Type Check · source gates; Type Check · workspace; TypeScript Type Check; filter), 13 skipped (the UI, docs, dogfood and opt-in lanes a skills-only diff does not reach), and 1 still in_progress: Test Core (1/8). Every family that was in progress on af460eb has since concluded green on this head, including Lint · docs & code guards (the role-word and doc-authoring gates). The one running conclusion is a gate verdict this record does not stand in for.

① Derived judgments

  • No schema, runtime or lint surface changes. The diff touches skills/** and one changeset only; every spec and ui/ file cited is byte-identical between origin/main and this head. Every accept-set the skill describes is one already published on main. Right.
  • The accept-set the skill teaches — a filter list element is a flat rule or { or: [rule, ...] } — is ViewFilterElementSchema (view.zod.ts:1270-1300, a union of ViewFilterRuleSchema and ViewFilterOrGroupSchema, routed by isViewFilterOrGroup). Right.
  • "Operators (no others)" lists exactly the 20 members of VIEW_FILTER_OPERATORS (view.zod.ts:315-325), in the enum's own order, with in / not_in marked as list values (VIEW_FILTER_LIST_VALUE_OPERATORS :343-345) and — new on this head — between marked [min, max] (VIEW_FILTER_PAIR_VALUE_OPERATORS :353-355, enforced by checkViewFilterRuleValueShape at parse). Right. The two value-shape classes the spec declares are now both taught; the scalar default for every other operator is the line's own reading. "No others" is honest at the typed surface; the legacy aliases in VIEW_FILTER_OPERATOR_ALIASES still fold at runtime, but the table's own docblock says new producers MUST emit canonical forms, so a skill that teaches only the canonical set teaches the contract.
  • The four removed spellings are truly refused, not merely unlisted. this_week, this_month, this_quarter, last_n_days appear neither in VIEW_FILTER_OPERATORS nor in VIEW_FILTER_OPERATOR_ALIASES; normalizeFilterOperator returns an unknown word verbatim and z.preprocess(foldAuthoredViewFilterOperator, z.enum(VIEW_FILTER_OPERATORS)) at :1071 refuses it at parse. The enum's docblock (:311-313) says relative-date operators are dashboard date-range presets, not filter-rule operators. Right.
  • Public surface moved: the @objectstack/skills npm package (packages/skills, v18.2.0, whose build syncs the repo-root skills/** into dist/skills/). Its content changes; its API does not. Right, and it is the one package the changeset names.
  • Eval 12 is a new case in the objectstack-ui fixture (parses: 12 ids, 1…12; must_not_contain now $or, logic:, conditions:, anyOf). Nothing under ui/ scans it (ui/scripts/check-skill-eval-tokens.mjs walks ui's own bundles only), so no gate's accept-set changes with it. Right.

② Semver level

.changeset/28476-skills-ui-view-filter-any-of-group.md (unchanged by the delta): '@objectstack/skills': patch, body carries Clause-②: no and Metadata-protocol: none; the PR body carries the same two lines, and so does the amended body. That matches what the diff publishes: no spec key, value or metadata type is added, narrowed or re-meant (the { or: [...] } element has been on main since #28020), and a content change to a released package is a patch. No (widening) / (narrowing) arm, correctly. Check Changeset is green on this head. Clause-②: no.

③ Boundary flags

open_questions in the os-dev-report (6553395266) is empty. Each flag, and each item the delta or the amended body raises, answered against the tree:

  1. Declared in-place fix — the operator line. Verified as ① states: all 20 operators listed are enum members, both value-shape classes are marked, the four removed spellings are refused at parse. Same section, same gate family, the card's own defect class. Accepted.

  2. Stale [PLANNED — not yet rendered] marker on ViewFilterOrGroupSchema. CONFIRMED, and wider than the dev says: view.zod.ts:1152-1162 (docblock) and :1217-1223 (the .describe text /meta/types serves) still say "No console reads it yet … refused when it loads", and filter-rule-array.ts:193-199 puts "(declared, but no console applies it yet, so it does not narrow the rows today)" into the live refusal prose for a MongoDB-form $or. On main, filter-converter.ts:1389-1418 lowers the group to an ['or', ...] node inside the list's AND, ObjectView.tsx / objectGridQuery.ts send that query, and the server's filter AST accepts the logical node (filter.zod.ts:2857-2860). Two author-facing runtime strings on the spec surface state a false status. Needs a card (spec lane: packages/spec/src/ui/view.zod.ts, filter-rule-array.ts; Metadata-protocol: none). Not this PR's surface; the skill correctly follows the renderer, not the marker. The amended body still carries this finding with carrier: none, which is accurate until the card exists.

  3. The docs' {7_days_ago} token "no console resolver reads". REFUTED on the first review and now WITHDRAWN by the dev in the amended body, with the right citation (date-macros.ts resolves {N_days_ago}, reached through resolveFilterPlaceholders from ObjectView.tsx:565 and objectGridQuery.ts:39). The withdrawal matches the tree. No card; the docs example at views.mdx:131 is correct as written.

  4. The token ratchet after the delta. The gate prices Math.ceil(Buffer.byteLength(text, 'utf8') / 4) (check-skills-token-ratchet.mjs:49); list-views.md at this head is 12438 bytes = 3110 tokens against the untouched ceiling row 3129 (:857) — the dev's 3110/3129 reading is the tree's, headroom 19. The fixture is 10066 bytes = 2517, measured not ratcheted. The six-token cost of "between ([min, max])" was paid from the headroom the first commit's deletions left; no new deletion, no re-wrap.

  5. The role-word ratchet (not flagged by the dev). The first commit removed the only "role" in list-views.md (baseline row 1 → 0 on disk). check-role-word.mjs is one-sided (ratchetVerdict :1011-1035 reports a decrease as "passing, reported", :2693-2697), and Lint · docs & code guards is now green on this head, which confirms it. The baseline --update is a maintainer action per the gate's own text. No flag.

  6. The amended PR body — patch-ready: yes. Diffed by me against the live body. It keeps the two harness header lines (the attribution comment and the "Requested by … project thread" line), Fixes #28476, Clause-②: no, Metadata-protocol: none, the seat line, and both footers. Every changed claim was checked against the tree: two commits above the merge-base (af460eb, 06abd9a — right); the operator-line row now ends "between ([min, max])" (right); eval 12's must_not_contain row lists conditions: (right); 3114 → 3110 (3129), 309 → 308 lines, 12453 → 12438 bytes (right); the fixture 2320 → 2517, 115 → 125 lines, 9277 → 10066 bytes (right); the bundle-total arithmetic 220471 + 4 − 197 = 220278 is internally consistent (the totals themselves are the gate's own readings, not re-run here); the {7_days_ago} note withdrawn as in ③.3 (right); the stale-marker note unchanged (right, ③.2); "Out-of-scope findings beyond the one above: none" (right). One cosmetic drift, no fix owed: the body cites VIEW_FILTER_PAIR_VALUE_OPERATORS at :354-356; the constant spans :353-355. It contradicts nothing on the tree. The seat may patch the live body with this file as it stands.

④ Design judgment

Every teaching sentence held against spec and code on main; the delta's two lines judged first, the rest re-read on this head:

  • "between ([min, max])". VIEW_FILTER_PAIR_VALUE_OPERATORS = ['between'] (view.zod.ts:353-355), and checkViewFilterRuleValueShape refuses any other value shape at parse, naming the pair; the value describe text (:1078-1086) says "between takes exactly [min, max]". The marking is the spec's own wording, parallel to "(list values)" on in / not_in. Holds; the should-fix is closed exactly as asked.
  • Eval 12 with conditions: refused. must_contain listViews, or: [, {current_user_id}; must_not_contain $or, logic:, conditions:, anyOf. Every spelling the expected_output names as refused is now asserted, and every one of them is a guidance key of ViewFilterRuleSchema (VIEW_FILTER_OR_SPELLING_GUIDANCE, :526-532), so the fixture refuses precisely what the parse refuses. A right answer in the fixture's TypeScript spelling passes (the fixture's standing convention: case 1 asserts provider: 'object'). A wrong answer that reaches for the builder's { logic, conditions }, the Query-DSL $or, or anyOf fails. any: is still deliberately absent (a suffix of company:); the residual pass-through is a nested { or: [{ or: [...] }] }, which no substring assertion can catch and which the skill text forbids by name. No right answer fails; the should-fix is closed exactly as asked.
  • Rule-or-group list element, ANDed with siblings. ViewFilterElementSchema union; lowerViewFilterElement lowers a group to one ['or', ...] child of the list's AND (filter-converter.ts:1414-1418). Holds; the docs page (views.mdx:120-124) says the same.
  • Flat rules only; no empty or. or: z.array(ViewFilterRuleSchema).min(1, …) (:1203-1210): a nested group meets VIEW_FILTER_NESTED_GROUP_GUIDANCE at parse via the rule's strictObject guidance (:1055-1057); { or: [] } meets the .min(1) message naming it. Holds.
  • "$or, any / anyOf, logic / conditions on a rule are refused naming this shape." Refused at parse by strictObject's unknown-key error map with VIEW_FILTER_OR_SPELLING as the message, which names { or: [rule, ...] } and says the list still ANDs. A bare { $or: [...] } list element has no or key, so it reaches the same rule branch and the same message. The lint (filter-walk.ts:223) walks $or only in the record-form filter and steps into or groups via filterGroupRules; it refuses nothing here. Holds.
  • Builder behaviour. foldFilterGroupToSpecRules writes [{ or: rules }] for logic: 'or' over two or more rows (viewFilterFold.ts:228); the round-trip test shows a stored single group loading editable with the OR toggle lit and an edit writing the group back (:116-128), the toggle writing one group and back flat (:130-139), and a group beside another condition rendered with the trigger disabled under filterOrGroupNotEditable while the stored filter is left intact (:141-156). The runtime applies the mixed list. All three sentences hold. (Nuance, not a finding: an OR toggle over exactly one row is written flat, since OR and AND select the same records; the docs page shares the simplification.)
  • The example parses: two scalar equals rules inside or, one equals rule beside it; every operator in the enum, every value a scalar. It means "open OR urgent, AND mine". {current_user_id} is resolved by resolveContextTokens from useFilterScope()'s currentUserId (filter-tokens.ts:198); the inline note "{current_user_id} = the signed-in user" carries the deleted blockquote's fact. Holds.
  • Deletions (all in the first commit; none in the delta) — no rule lost its only home, and no hunk is a pure re-wrap. ① the bare-flat-view rule keeps "nothing registers and no view appears in the switcher"; ② provider: 'object' keeps all four capabilities and "a view never calls an API"; ③ the Columns lead-in is carried by the block's "Simple" / "Enhanced" comments; ④ tabs-on-object keeps the rule, the parse refusal (ObjectUserFiltersSchema, "not dropped") and the validate lint — what went is the ViewTabBar name and the "untypable at author time" explanation, neither a rule; ⑤ omit-userFilters keeps "nothing is auto-derived" and the switcher-covers-presets fact; ⑥ the toolbar-search intro and blast-radius keep narrows / never widens / refuses / whole search box for every user and term — "the active view's list wins over the object's" is implied by "narrows that set for this one list" and stays verbatim in views.mdx:170-175; ⑦ wording only; ⑧ the kanban block's own comments at head L268 ("required on every list view") and L273 ("fields shown on each card") carry both facts; ⑨ the gantt sentence keeps its three facts and the locked: true rule. Every re-wrapped line sits inside a hunk that also removes words; the ratchet prices bytes, which wrapping does not change. The three os:check blocks are unchanged against main.
  • Mirror with the docs. The skill's sentences are the docs' § Base filter (views.mdx:118-140) said shorter, with the example swapped to one that uses no date token; nothing the skill teaches contradicts the docs page or the spec.

Attempts to break it that did not land: a $or written as a list element (same parse message); a group with a stray field key (VIEW_FILTER_GROUP_STRAY_RULE_KEY_GUIDANCE, not taught but not contradicted); a between with a scalar (refused at parse, now taught); a server that might refuse the or node (accepted, filter.zod.ts:2857); the role-word ratchet going red on the deleted "role" (one-sided, and the gate is green on this head); the token ceiling after the six-token addition (3110 of 3129); the @objectstack/skills changeset naming a package that does not ship skills/objectstack-ui (it does, via sync-catalog.mjs).

Findings: 0 must-fix; 0 should-fix (both of the superseded record's should-fixes are closed by this head). One card owed outside this PR (③.2, the stale PLANNED marker and door prose in packages/spec). The amended PR body is patch-ready (③.6).

Implemented-by: claude/project-thread-0tmmio
Reviewed-by: session_01L9qibzCtJt8Q2Z63fyRNix

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-tesla · session_01L9qibzCtJt8Q2Z63fyRNix

维护者速读

  • 改了什么:objectstack-ui 技能的列表视图规则补教「任一满足」过滤组:filter 列表里的一个元素可以写成 { or: [规则, ...] },组内任一条满足即命中,再和旁边的其他条件取「且」;组内只能放平铺规则,不能嵌套,不能为空;$or、any / anyOf、logic / conditions 会在解析时被拒,提示里给出这个写法。也写清楚 Console 过滤器的行为:OR 开关保存成一个组;整个过滤恰好是一个组时可以编辑;组旁边还有其他条件时照常生效,但在过滤器里只读。另加测试题第 12 题。顺手修了同一节的运算符清单:旧清单里的 this_week 等四个词 spec 根本不接受,现在换成 spec 的 20 个运算符,并标出 in / not_in 取列表、between 取 [min, max]。
  • 为什么改:Console 已经能读这种组(Console list query and filter builder do not read a stored view filter's any-of group #28370 / PR feat(console): a stored view filter's OR group filters the list, and the filter builder saves and reads it #28475),技能还只教「且」,AI 遇到「未完成或紧急」只会写两个视图,或者编一个会被拒的 or 写法。
  • 风险与回滚:只改技能文本和一道测试题,不碰运行时。token 余量用同文件删重复句子腾出,复审逐条确认没有规则因此丢失。回滚即 revert。
  • 先看:skills/objectstack-ui/rules/list-views.md 的 Filtering 一节。
  • 两分钟验证:git grep -n -E "or: \[|between" skills/objectstack-ui/rules/list-views.md。
  • 需要你做的:改的是 skills/**,属于 Tier H,需要 os-zhuang(或 hotlong)批准后才能合并。契约复审 PASS 记录 6553630360,Head-sha 06abd9ac。

席位意见:契约复审 PASS。第一轮的两条建议修改(测试题拒收 conditions:;标出 between 的取值形状)dev 已在新 head 上改好,复审重新确认。复审发现 spec 里这个组的说明还写着「尚未渲染」,已单独立卡 #28522 交 spec 车道。可以批准。

席位核对

ACCEPT · seat domain:skills#1 · head 06abd9ac · 2026-11-01T05:00Z

  1. Shape: Fixes #28476, draft, base main, 3 files (rules/list-views.md § Filtering, eval 12 in evals/views-apps-actions-pages.json, a patch changeset for @objectstack/skills), +56/−37, two commits. No spec or code path.
  2. Ruling: none; triage route 6550228889; p3 under the maintainer's 「任务有一大堆啊,都可以派,包括 p3」 (2026-11-01).
  3. Contract review: PASS 6553630360 at this head (supersedes PASS 6553490758 at af460eb4; both should-fixes applied and verified). Clause-②: no, Metadata-protocol: none.
  4. Gates / check-runs: dev report 6553395266: 43 derived gates exit 0, --ran 43/43, check:skill-examples exit 0; at 06abd9ac the same 43 re-run, all exit 0. list-views.md 3110 of its 3129-token ceiling. On head 06abd9ac: 41 check-runs, 28 success, 13 skipped, 0 failed; mergeable_state clean.
  5. Accepted deviations: the PR body still describes af460eb4 and is not patched (the relay refuses an issue_patch body that does not open with a seat signature; the dev's amended draft, judged patch-ready by the review, is /mnt/project-files/skills-seat-1/r2026-10-31/out/28476-pr-body-06abd9ac.md). Where it differs, this record holds: two commits; the operator line ends "between ([min, max])"; eval 12 refuses conditions:; list-views.md 3110 tokens / 308 lines; the {7_days_ago} finding is withdrawn (refuted: date-macros.ts resolves it). Out of scope: the stale [PLANNED] marker on ViewFilterOrGroupSchema, filed as spec(view): the any-of filter group still says [PLANNED] and "no console applies it yet" after #28370 landed #28522 (spec lane).
  6. Landing: check-governed-merges --pr 28517 reports GOVERNED, Tier H (skills/**). The seat readies the PR and the relay's governed-review leg requests the approvers. No auto-merge is armed by the seat. After an authorized approval, the seat lands it, or the approver enqueues it.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 1, 2026 05:01
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit f662c71 Nov 1, 2026
42 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-0tmmio branch November 1, 2026 05:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

skills: teach the view filter any-of group { or: [...] } in the objectstack-ui list-views rule

2 participants