Repository navigation
Commit f4a8eed
feat(spec): a stored view filter list can hold an any-of group (#28380)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
Fixes #28020
Clause-②: yes (widening)
Metadata-protocol: added
## 维护者速读
- 改了什么:已保存视图的筛选列表(`ListView.filter`、视图页签、页面
`filterBy`、数据源与相关列表等)原来只能写"全部满足"的平铺规则,现在可以多放一种元素 `{ any: [规则, ...]
}`,表示"满足其中任意一条";只允许一层,列表本身仍是"并且"。
- 为什么改:业务上最常见的"未完成,或最近 7 天内完成"这类视图以前无法保存,只能拆成两个视图;现在可以作为一个视图存下来。
- 风险与回滚:今天已保存的每个筛选解析结果逐字节不变(接受的输出相同,拒绝的报错码/路径/文案相同);控制台尚未应用该分组(已标
`[PLANNED — not yet rendered]`,由 #28370 跟进),存了分组的视图暂时不会按它过滤;回滚只需还原本 PR。
- 先看:`packages/spec/src/ui/view.zod.ts` 中的 `ViewFilterAnyGroupSchema` /
`ViewFilterElementSchema`,以及钉住全部要求的
`packages/spec/src/ui/view-filter-any-group.test.ts`。
- 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run
src/ui/view-filter-any-group.test.ts`,再跑 `pnpm --filter
@objectstack/lint exec vitest run src/filter-walk-any-group.test.ts`。
- 需要你做的:请裁定这一存储形状(列表中一层 any-of 分组),并批准键名
`any`;另请确认蓝图(solution-blueprint)的 `filters` 保持平铺规则、相关列表的 UI
镜像暂不放开分组这两处取舍。
Independent review: none run (quota); the PR waits for the seat's
contract-review hand-off.
## What changes
- **Spec (`packages/spec/src/ui/view.zod.ts`).** A view filter list now
holds `ViewFilterElementSchema` = `ViewFilterRuleSchema |
ViewFilterAnyGroupSchema`. The group is `{ any: [rule, ...] }`: a strict
object whose only key is `any`, a non-empty array of
`ViewFilterRuleSchema` itself, so the operator vocabulary, the
legacy-alias fold, the operator/value coupling and the `icontains`
comparand check all apply inside a group as at the top level. One level
only.
- **Key spelling `any`.** No at-rest any-of spelling exists to mirror.
The console builder's `{ id, logic: 'and'|'or', conditions }` is editing
state: its `and` arm repeats the list's own AND, so `logic` would have
exactly one legal value, and `id` is a UI row key. `$or` on views is out
of bounds (no `FilterConditionSchema` admitted). `{ any: [...] }` says
the semantics in the key and leaves nothing to agree with.
- **Byte-identical for stored filters.** The element union carries a
check (the same pattern as `focusClaimedBranch` on `ViewMetadataSchema`)
that reports only the branch the element claims, so a refused flat rule
shows exactly the issues it showed before, not an `invalid_union`.
- **Refusals.** Empty group: `too_small` at `filter.N.any`. Nested
group: `unrecognized_keys` naming `any` on the inner element, with
guidance. A non-rule member, a non-array `any`, or a rule key beside
`any` are refused. `$or`, `or`, `anyOf`, `logic` and `conditions`
written on a rule are answered with the group shape.
- **Carriers.** Every carrier that held `z.array(ViewFilterRuleSchema)`
moved: `ListView.filter`, `ViewTab.filter`, the three page carriers
(`ElementDataSource.filter`, record binding filter,
`InterfacePageConfig.filterBy`) and the eleven component doors. A
source-scan pin holds that set. The one exception is
`BlueprintViewSchema.filters`
(`packages/spec/src/ai/solution-blueprint.zod.ts`): the model writes it
through a strict mirror that cannot carry a union, and its applier lives
outside this repo. A comment there says so.
- **Readers.** Updated: `@objectstack/lint` `filter-walk.ts` (list-view,
dataset and widget field-reference rules),
`validate-preset-comparands.ts`, and `validate-page-field-bindings.ts`
(`filterBy`, the related-list `filter`).
`@objectstack/metadata-protocol` `computeViewReferenceDiagnostics` reads
preset-tab filters. Checked and unchanged: `graftNormalizedOperators`
(structural, now pinned through a group), `validate-field-consumers`
(generic walk, already credits fields inside a group) and
`validate-empty-combinators`. Marked `[PLANNED — not yet rendered]`:
every console reader that turns the list into a query (plugin-list
`buildEffectiveFilter`, core `convertFiltersToAST`, data-objectstack
`objectFilterEntriesToAST`, app-shell `viewFilterFold`, plugin-view
`parseSpecFilter`). No server-side reader in `packages/` does that, so
the group carries the PLANNED tag in its describe, TSDoc and
`liveness/view.json`.
- **Follow-up.** The console half is filed as **#28370** (domain:ui,
`finding`). It lands after this PR.
- Prose that said `$or` cannot be written in a rule array
(`filter-rule-array.ts`, `conversions/registry.ts`) now names the group.
- Changeset: `@objectstack/spec`, `@objectstack/lint`,
`@objectstack/metadata-protocol` minor.
## Tests
- `packages/spec/src/ui/view-filter-any-group.test.ts` (new) covers:
- a group is accepted on ListView, ViewTab, ElementDataSource and
InterfacePageConfig;
- stored flat rules, accepted and refused, parse identically as an
element and as a bare rule;
- refinements apply inside a group;
- refusals: empty, nested, a non-rule member, a non-array `any`, a stray
rule key, the OR spellings;
- the wire `id` strip inside a group, and the authoring refusal of `id`;
- JSON-schema `anyOf`, the `isViewFilterAnyGroup` predicate, and a
carrier source scan.
- `packages/lint/src/filter-walk-any-group.test.ts` (new), plus cases in
`validate-page-field-bindings.test.ts` and
`validate-preset-comparands.test.ts`: a field inside a group is checked
at `…[0].any[1]…`, and `any` is never reported as a field.
-
`packages/metadata-protocol/src/metadata-diagnostics.view-filter-any-group.test.ts`
(new): diagnostics through a group, and the operator graft through a
group.
- `ui/packages/types` type-parity pins move to `ViewFilterElement[]`.
The two related-list mirrors are pinned as "the contract minus the
group" plus "the contract also takes the group". `record-components.ts`
stays flat because widening it reaches plugin-detail prop types, which
belong to #28370.
## Acceptance notes
- Shape A: one group element, one level, inside the existing list. No
`$or`, no `FilterConditionSchema` on views.
- `solution-blueprint` view `filters` stay flat (strict mirror, applier
out of repo). That call is the seat's to confirm.
- The related-list UI mirror
(`ui/packages/types/src/record-components.ts`) deliberately lags the
contract by the group, with pins that hold the lag exactly. Widening it
is #28370's job.
- The conversion registry does not write groups: converting a stored
`$or` record into a group would widen that conversion, so it is left as
is.
- The group is `[PLANNED — not yet rendered]` until #28370: a view that
stores one saves and validates but does not narrow its rows yet.
## Verification
- Post-merge (tree at the head of this PR):
- `@objectstack/spec`: 773 files / 22857 tests passed;
- targeted lint tests: 4 files / 263 tests passed;
- metadata-protocol: 1 file / 3 tests passed.
- Pre-merge full suites:
- lint: 183 files / 7693 tests, typecheck passed;
- metadata-protocol: 285 files, typecheck passed;
- ui type-check over spec dependents: green after the pin moves
(`@object-ui/types`, plugin-detail);
- ui pin tests: 6 files / 184 tests passed.
- `dispatch-gates.mjs --commands`: 110 derived commands, each run with
its exit code recorded, then reconciled with `--ran`. The result: 110
run, 110 exit 0, 0 NOT-MEASURED. Two gates (`check:skill-examples`,
`check:dual-build-cjs-loads`) first exited 3 (prerequisite not met,
unbuilt dist). Both were rerun green after building
`@objectstack/client-react` and then the full workspace (`turbo run
build`, 116/116 tasks). `check:dev-prereqs` and `check:nul-bytes` also
pass.
- `gen:schema`, `gen:docs`, `check:generated -- --fix` (all artifacts
current), `check:liveness` and `check:authorable-surface` all pass.
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 5517005 commit f4a8eed
38 files changed
Lines changed: 1253 additions & 122 deletions
File tree
- .changeset
- content/docs/references
- api
- data
- ui
- docs/audits/2026-07-unknown-key-strictness-ledger.counts
- packages
- lint/src
- metadata-protocol/src
- spec
- api-surface
- authorable-surface
- declaration-map
- export-origins
- json-schema.manifest
- liveness
- src
- ai
- conversions
- ui
- ui/packages/types/src/__tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1856 | 1856 | | |
1857 | 1857 | | |
1858 | 1858 | | |
1859 | | - | |
| 1859 | + | |
1860 | 1860 | | |
1861 | 1861 | | |
1862 | 1862 | | |
| |||
1935 | 1935 | | |
1936 | 1936 | | |
1937 | 1937 | | |
1938 | | - | |
| 1938 | + | |
1939 | 1939 | | |
1940 | 1940 | | |
1941 | 1941 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
375 | 375 | | |
376 | 376 | | |
377 | 377 | | |
378 | | - | |
| 378 | + | |
379 | 379 | | |
380 | 380 | | |
381 | 381 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | | - | |
37 | | - | |
| 36 | + | |
| 37 | + | |
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| |||
371 | 371 | | |
372 | 372 | | |
373 | 373 | | |
374 | | - | |
| 374 | + | |
375 | 375 | | |
376 | 376 | | |
377 | 377 | | |
| |||
392 | 392 | | |
393 | 393 | | |
394 | 394 | | |
395 | | - | |
| 395 | + | |
396 | 396 | | |
397 | 397 | | |
398 | 398 | | |
| |||
0 commit comments