Skip to content

Commit f662c71

Browse files
docs(skills): objectstack-ui list-views teaches the view filter any-of group { or: [...] } (#28517)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoX9GDX2cb55BEVTSE1eYheJ)_ 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 #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 - **In-place fix, same section (os-dev.md bounded exemption, all four conditions):** the old "Common operators" line taught `this_week`, `this_month`, `this_quarter` and `last_n_days`, which `VIEW_FILTER_OPERATORS` (`view.zod.ts:315-325`) and `VIEW_FILTER_OPERATOR_ALIASES` (`:375-400`) both refuse — an AI author copying them gets a parse refusal — and omitted eight operators the enum has. ① Same defect class as the card (§ Filtering teaching what the spec does not accept); ② mechanical: the line is replaced by the enum, verbatim; ③ no other claim holds the file (#28488 merged; ui seat 1's #27059 is on `rules/pages*` / `references/pages.md`); ④ the same gate family (`check-skills-token-ratchet`, `check:role-word`), no new verification surface. Evidence: the enum and alias table cited; `git grep -c "last_n_days\|this_week" packages/spec/src/ui/view.zod.ts` = 1 (a docblock, not a member). The claim's file surface already names this file. - **The example uses `priority equals urgent`, not the docs' `completed_at after {7_days_ago}`.** `{7_days_ago}` appears only in spec docblocks and `content/docs/ui/views.mdx`; no console resolver reads it (`ui/packages/core/src/utils/filter-converter.ts` resolves `{current_user_id}` only). The skill teaches only what runs. Carrier for the docs' token: `none` — noted for the seat, not filed (a docs card, not this one's). - **Stale `[PLANNED — not yet rendered]` marker** on `ViewFilterOrGroupSchema` (`view.zod.ts:1152-1162`, `:1217-1223`) and in `filter-rule-array.ts:193-199` ("declared, but no console applies it yet"): the console half #28370 landed with PR #28475, so the marker and the describe text are now false. `class: stale-docblock`, `reach: /meta/types describe text and the spec source`, `carrier: none` — the seat decides whether #28370 / PR #28475's own follow-up carries it; not this PR's file surface. - **No `_index.md` row:** the group is a key inside an existing `filter` element, not a new rule file or section; `check:skill-identifier-liveness` and `check:skill-frame-sync` exit 0. - Out-of-scope findings beyond the two above: none. ## Verification All readings at head `af460eb4` (worktree `/home/user/objectstack-28243`, base `origin/main` 23d1303). <details><summary>Derived gates (<code>dispatch-gates.mjs --commands --repo objectstack-ai/objectstack</code>, 43 lines, each run with stdin closed and its exit captured before any pipe)</summary> 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. </details> <details><summary>Owed outside the derivation: the <code>os:check</code> example compile</summary> `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. </details> 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.com/claude-code) https://claude.ai/code/session_01L9qibz4G9FrLR4TRExx6gn --- _Generated by [Claude Code](https://claude.ai/code/session_01L9qibz4G9FrLR4TRExx6gn)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f5e6709 commit f662c71

3 files changed

Lines changed: 56 additions & 37 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
Clause-②: no
6+
Metadata-protocol: none
7+
8+
objectstack-ui `rules/list-views.md` § Filtering teaches the view filter any-of group `{ or: [rule, ...] }` (`ViewFilterOrGroupSchema`): an element of the `filter` list that matches when any of its flat rules does, still ANDed with the elements beside it; no group in a group, no empty `or`; the Console builder saves its OR toggle as one group and shows a group beside other elements read-only. One eval case asks for an either-of list in one view.
9+
10+
Token ratchet: the growth is paid by deletion in the same file (the flat-view and data-source blockquotes, the `tabs`-on-object and omit-`userFilters` bullets, the search-blast-radius and toolbar-search prose, the kanban intro).

‎skills/objectstack-ui/evals/views-apps-actions-pages.json‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,16 @@
110110
"must_contain": ["params", "submitText: 'Log Call'"],
111111
"must_not_contain": ["submitLabel", "confirmLabel", "confirmText"]
112112
}
113+
},
114+
{
115+
"id": 12,
116+
"prompt": "On `task`, add an 'Attention' list for the signed-in user: their tasks that are open or urgent, in one list.",
117+
"expected_output": "One `listViews.attention` entry, not two views. Its `filter` is a list whose elements all match: one any-of group `{ or: [{ field: 'status', operator: 'equals', value: 'open' }, { field: 'priority', operator: 'equals', value: 'urgent' }] }` beside the rule `{ field: 'assigned_to', operator: 'equals', value: '{current_user_id}' }`. Flat rules only inside the group; no `$or`, `logic` / `conditions` or `anyOf` key.",
118+
"files": [],
119+
"assertions": {
120+
"must_contain": ["listViews", "or: [", "{current_user_id}"],
121+
"must_not_contain": ["$or", "logic:", "conditions:", "anyOf"]
122+
}
113123
}
114124
]
115125
}

‎skills/objectstack-ui/rules/list-views.md‎

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,8 @@ export const CaseViews = defineView({
3131
```
3232

3333
> **Never export a bare flat view object** (`{ name, label, type, columns }`
34-
> at top level). It is not a valid view container — nothing registers and no
35-
> view appears in the switcher. Every view lives under `list` / `form` /
36-
> `listViews` / `formViews`, exactly as in the `defineView` example above.
34+
> at top level): nothing registers and no view appears in the switcher. Every
35+
> view lives under `list` / `form` / `listViews` / `formViews`, as above.
3736
3837
### Data Source (`data`)
3938

@@ -47,15 +46,12 @@ data: { provider: 'object', object: 'support_case' }
4746
data: { provider: 'value', items: [...] }
4847
```
4948

50-
> **Best practice:** Always use `provider: 'object'` when the data source is
51-
> an ObjectStack-managed object. It enables automatic CRUD, real-time updates,
52-
> filtering, and pagination. A view never calls an API: an API read is a
49+
> Use `provider: 'object'` for an ObjectStack-managed object (CRUD, live
50+
> updates, filtering, pagination). A view never calls an API: an API read is a
5351
> page element's `dataSource` binding.
5452
5553
### Columns
5654

57-
Columns can be defined as a simple string array or detailed config:
58-
5955
```typescript
6056
// Simple — field names only
6157
columns: ['subject', 'status', 'priority', 'assigned_to', 'due_date']
@@ -96,18 +92,30 @@ by nature — a log, an audit trail, a history, a report roll-up — declares
9692

9793
### Filtering
9894

95+
`filter` is a list whose elements must **all** match. An element is a rule
96+
`{ field, operator, value }` or an any-of group `{ or: [rule, ...] }` that
97+
matches when **any** of its rules does — still ANDed with the elements beside it:
98+
9999
```typescript
100100
filter: [
101-
{ field: 'status', operator: 'not_equals', value: 'closed' },
102-
{ field: 'assigned_to', operator: 'equals', value: '{current_user_id}' },
101+
{ or: [ // open, OR urgent …
102+
{ field: 'status', operator: 'equals', value: 'open' },
103+
{ field: 'priority', operator: 'equals', value: 'urgent' },
104+
] },
105+
{ field: 'assigned_to', operator: 'equals', value: '{current_user_id}' }, // … AND mine ({current_user_id} = the signed-in user)
103106
]
104107
```
105108

106-
Common operators: `equals`, `not_equals`, `contains`, `starts_with`,
107-
`greater_than`, `less_than`, `is_empty`, `is_not_empty`, `in`, `not_in`,
108-
`this_week`, `this_month`, `this_quarter`, `last_n_days`.
109+
Operators (no others): `equals`, `not_equals`, `contains`,
110+
`not_contains`, `icontains`, `starts_with`, `ends_with`, `greater_than`, `less_than`,
111+
`greater_than_or_equal`, `less_than_or_equal`, `in`, `not_in` (list values),
112+
`is_empty`, `is_not_empty`, `is_null`, `is_not_null`, `before`, `after`, `between` (`[min, max]`).
109113

110-
> **`{current_user_id}`** resolves to the signed-in user's id.
114+
A group holds flat rules only (no group in a group) and never `or: []`; `$or`,
115+
`any` / `anyOf`, `logic` / `conditions` on a rule are refused naming this shape.
116+
The Console builder saves its OR toggle as one group and edits a filter that is
117+
exactly one group; a group beside other elements applies, but the builder shows
118+
it read-only.
111119

112120
### End-User Quick Filters (`userFilters`, ADR-0047)
113121

@@ -136,27 +144,22 @@ Rules:
136144
entry, rendered as a tab in the object's saved-view switcher.
137145
- **On an object list view (`*.view.ts` `list` / `listViews`), only
138146
`element: 'dropdown'` (value chips) is allowed — `tabs` is page-only**
139-
(ADR-0047 amendment). An object view's saved-view `ViewTabBar` already owns
140-
the tab-bar role, so a `tabs` user-filter would render a second, colliding
141-
tab bar. The spec narrows it (`ObjectUserFiltersSchema` — a `tabs` element is
142-
untypable at author time and **rejected** at parse, not dropped) and the
147+
(ADR-0047 amendment): the saved-view switcher already owns the tab bar, so
148+
`ObjectUserFiltersSchema` **rejects** `tabs` at parse (not dropped) and the
143149
`validate` list-view-mode lint reports it.
144150
Need named presets on an object? Add a `listViews` entry instead. The full
145151
`dropdown | tabs | toggle` range applies only to **page lists** /
146152
`interfaceConfig.userFilters` (the block above).
147-
- **Omit `userFilters` when unsure — omission means a clean toolbar.** Filter
148-
elements render only when explicitly configured; nothing is auto-derived.
149-
In data mode the saved-views switcher already covers the preset use case,
150-
so most views need no filter elements at all.
153+
- **Omit `userFilters` when unsure — omission means a clean toolbar.** Nothing
154+
is auto-derived, and the saved-views switcher already covers presets.
151155
- `userFilters: { element: 'dropdown' }` (no `fields`) is valid shorthand:
152156
the renderer fills the field list from the object's select/boolean fields.
153157

154158
### Toolbar Search (`searchableFields`, ADR-0061)
155159

156160
The toolbar's search box scans a set the **object** owns. A list view's
157-
`searchableFields` **narrows** that set for this one list — it can never widen
158-
it, and the runtime enforces that by **refusing the request**, not by quietly
159-
dropping the extra name.
161+
`searchableFields` **narrows** that set for this one list — never widens it;
162+
the runtime **refuses the request** rather than dropping the extra name.
160163

161164
<!-- os:check -->
162165
```typescript
@@ -190,11 +193,9 @@ Modelling side: **objectstack-data → Search Fields**. Query side
190193

191194
#### ⛔ One bad entry 400s EVERY search on that list
192195

193-
The client echoes this declaration verbatim as the `$searchFields` override —
194-
the active view's list wins over the object's — and the ingress gate refuses any
195-
entry outside the allowed set before the engine ever runs. The blast radius is
196-
the list's whole search box, for every user and every term: not a narrower
197-
result, no result at all.
196+
The client sends this list verbatim as the `$searchFields` override and the
197+
ingress gate refuses any entry outside the allowed set before the engine runs —
198+
the whole search box, for every user and every term: no result at all.
198199

199200
| What you write on the view | `os validate` | Toolbar search at runtime |
200201
|:---------------------------|:--------------|:--------------------------|
@@ -208,7 +209,7 @@ result, no result at all.
208209

209210
Both diagnostics are **errors**, not warnings — `os validate` fails the build.
210211

211-
To remove the search box, toggle the affordance — a different key, same view:
212+
To remove the search box, toggle the affordance instead:
212213

213214
<!-- os:check -->
214215
```typescript
@@ -226,7 +227,7 @@ export const AuditViews = defineView({
226227
```
227228

228229
**Searching by a related record's title:** mirror it into a **stored** field
229-
and narrow to that — **objectstack-data → Search Fields (`searchableFields`)**.
230+
and narrow to that (objectstack-data → Search Fields).
230231

231232
### Sorting
232233

@@ -258,8 +259,7 @@ grouping: { fields: [
258259
## Configuring Kanban Views
259260

260261
Board settings nest under `kanban:` (`KanbanConfigSchema`) — there is **no
261-
top-level `groupBy`**. The top-level `columns` is required on every list view
262-
(including kanban), while `kanban.columns` picks the fields shown on each card.
262+
top-level `groupBy`**; the top-level `columns` stays required.
263263

264264
```typescript
265265
{
@@ -304,6 +304,5 @@ Timeline settings nest under `gantt:` (`GanttConfigSchema`);
304304
}
305305
```
306306

307-
Rows with children (or `type: 'summary'`) render as **summary bars** — they
308-
move the whole group on drag and have **no resize handles**. Leaf tasks resize
309-
freely unless `locked: true`.
307+
Rows with children (or `type: 'summary'`) are **summary bars**: dragged as a
308+
group, **no resize handles**. Leaf tasks resize unless `locked: true`.

0 commit comments

Comments
 (0)