From 9ddbf3934aeff192df9237e07bb101fd08b61e92 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 16:40:39 +0000 Subject: [PATCH 1/4] docs(spec): describe the view filter any-of group as live, not planned The console half landed (toFilterNode lowers a group to one or node inside the list's AND, and the builder edits a one-group filter), so ViewFilterOrGroupSchema's PLANNED docblock and describe, the $or refusal prose in filter-rule-array.ts and the view liveness note now state the live behaviour. Tests that pinned the old wording are updated. Claude-Session: https://claude.ai/code/session_01JQwxbYCMdGp2Cju6A39cTB Co-authored-by: Claude --- .changeset/28522-view-or-group-live.md | 15 +++++++++++ packages/spec/liveness/view.json | 2 +- .../src/ui/filter-rule-array-guidance.test.ts | 4 +-- packages/spec/src/ui/filter-rule-array.ts | 8 +++--- .../spec/src/ui/view-filter-or-group.test.ts | 5 ++-- packages/spec/src/ui/view.zod.ts | 25 +++++++++---------- 6 files changed, 35 insertions(+), 24 deletions(-) create mode 100644 .changeset/28522-view-or-group-live.md diff --git a/.changeset/28522-view-or-group-live.md b/.changeset/28522-view-or-group-live.md new file mode 100644 index 00000000000..1fed473326a --- /dev/null +++ b/.changeset/28522-view-or-group-live.md @@ -0,0 +1,15 @@ +--- +'@objectstack/spec': patch +--- + +docs(spec): the view filter any-of group is described as live, not planned + +Clause-②: no +Metadata-protocol: none + +The console now applies a view filter's any-of group `{ or: [rule, ...] }`: the list query lowers it to one `or` node ANDed with the filter's other elements, and the filter builder edits a filter that is exactly one group. Two author-facing texts still said otherwise and are corrected: + +- `ViewFilterOrGroupSchema`'s description (what `/meta/types` and the generated reference show) no longer starts with `[PLANNED — not yet rendered]` and no longer says a view that stores a group is refused when it loads. +- The refusal for a MongoDB-style `$or` on a rule-array `filter` still prescribes the any-of group element, and no longer adds that no console applies it. + +Nothing an author can write changes: the schema accepts and refuses exactly what it did. diff --git a/packages/spec/liveness/view.json b/packages/spec/liveness/view.json index 824b9507c0d..6fd1917f8e6 100644 --- a/packages/spec/liveness/view.json +++ b/packages/spec/liveness/view.json @@ -49,7 +49,7 @@ }, "filter": { "status": "live", - "note": "objectui: ListView.tsx client-side filter shaping (audit L15); no list-render prop reaches the server query (audit data-flow note). The console's filter rows carry a React list key that the wire door removes before the parse (packages/spec/src/ui/view.zod.ts, the VIEW_CONSOLE_ROW_DECORATIONS header and the ViewFilterRule docblock; moved out of spec source, #23434): `id: crypto.randomUUID()` is stamped on each new row by objectui @b9814b334 packages/components/src/custom/filter-builder.tsx#FilterBuilder (its `addCondition`), and again on a stored filter read back into the builder by objectui @b9814b334 packages/plugin-view/src/config/view-config-utils.ts#parseTriplet and objectui @b9814b334 packages/plugin-view/src/config/view-config-utils.ts#parseSingleOrNested (both reached from that file's parseSpecFilter); re-read 2026-10-15, first cited by line, sha-less. One element kind rides this live verdict at PLANNED strength (#28020, 2026-10-31): the any-of group `{ or: [rule, ...] }` (ViewFilterOrGroupSchema, its describe marked `[PLANNED — not yet rendered]`) has no console reader yet — the list query build (ui/packages/plugin-list/src/ListView.tsx, buildEffectiveFilter; ui/packages/core/src/utils/filter-converter.ts, convertFiltersToAST) and the builder save fold (ui/packages/app-shell/src/views/viewFilterFold.ts, which refuses an OR group) read flat rules only, read on this tree at 22ce3d13fe. The console half is #28370; amend this note, citing the group read, when it lands." + "note": "objectui: ListView.tsx client-side filter shaping (audit L15); no list-render prop reaches the server query (audit data-flow note). The console's filter rows carry a React list key that the wire door removes before the parse (packages/spec/src/ui/view.zod.ts, the VIEW_CONSOLE_ROW_DECORATIONS header and the ViewFilterRule docblock; moved out of spec source, #23434): `id: crypto.randomUUID()` is stamped on each new row by objectui @b9814b334 packages/components/src/custom/filter-builder.tsx#FilterBuilder (its `addCondition`), and again on a stored filter read back into the builder by objectui @b9814b334 packages/plugin-view/src/config/view-config-utils.ts#parseTriplet and objectui @b9814b334 packages/plugin-view/src/config/view-config-utils.ts#parseSingleOrNested (both reached from that file's parseSpecFilter); re-read 2026-10-15, first cited by line, sha-less. The any-of group element `{ or: [rule, ...] }` (ViewFilterOrGroupSchema, #28020, 2026-10-31) rides this live verdict at full strength since its console half landed (#28370, PR #28475): the list query build lowers it to one `or` node inside the list's AND (ui/packages/core/src/utils/filter-converter.ts, toFilterNode via viewFilterOrGroupToNode, reached from ui/packages/plugin-list/src/ListView.tsx buildEffectiveFilter through mergeFilterNodes and ui/packages/plugin-view/src/ObjectView.tsx), the builder save fold writes an OR group over two or more rules as one group (ui/packages/app-shell/src/views/viewFilterFold.ts) and builderRulesOfSpecFilter reads exactly-one-group back as the builder's OR logic; read on this tree at 94d3e254, 2026-11-02. Its describe lost the `[PLANNED — not yet rendered]` marker in the same change (#28522)." }, "sort": { "status": "live", diff --git a/packages/spec/src/ui/filter-rule-array-guidance.test.ts b/packages/spec/src/ui/filter-rule-array-guidance.test.ts index 5abdbe2c47a..59d93e6b2f3 100644 --- a/packages/spec/src/ui/filter-rule-array-guidance.test.ts +++ b/packages/spec/src/ui/filter-rule-array-guidance.test.ts @@ -165,10 +165,8 @@ describe('§1 the record form is refused WITH the new spelling', () => { expect(issue.code).toBe('invalid_type'); expect(issue.message).not.toContain("field: '$or'"); expect(issue.message).toContain('`$or` is a combinator, not a field, and the rule array spells a combinator only as its own elements'); - // [#28020] The one combinator the rule array can spell is named, with its - // status: the any-of group is declared but no console applies it yet. + // [#28020] The one combinator the rule array can spell is named. expect(issue.message).toContain('`$or` over flat rules is one any-of group element'); - expect(issue.message).toContain('no console applies it yet'); expect(issue.message).toContain('`$not` cannot be written at all'); expect(issue.message).toContain('flattening one changes which rows the filter selects'); expect(issue.message).toContain('decide which rows this filter should select'); diff --git a/packages/spec/src/ui/filter-rule-array.ts b/packages/spec/src/ui/filter-rule-array.ts index d7efe87769c..2c01c792641 100644 --- a/packages/spec/src/ui/filter-rule-array.ts +++ b/packages/spec/src/ui/filter-rule-array.ts @@ -188,16 +188,14 @@ export function ruleArrayFilterError(options: RuleArrayFilterErrorOptions): z.co } const named = combinators.map((key) => `\`${key}\``).join(' and '); // [#28020] The rule array gained ONE combinator spelling, the any-of group - // element, and this sentence names it — with its status, because no console - // applies it yet: prescribing it bare would send an author to a filter that - // does not narrow the rows (`ViewFilterOrGroupSchema`'s PLANNED note). + // element, and this sentence names it as the shape a `$or` over flat rules + // is written in (`ViewFilterOrGroupSchema`). const combinatorSentence = combinators.length === 0 ? '' : ` ${named} ${combinators.length === 1 ? 'is a combinator, not a field' : 'are combinators, not fields'}, ` + `and the rule array spells a combinator only as its own elements: they AND, so \`$and\` is ` + `the separate rules it joins, \`$or\` over flat rules is one any-of group element ` - + `\`{ or: [{ field, operator, value }, ...] }\` (declared, but no console applies it yet, so it ` - + `does not narrow the rows today), and \`$not\` cannot be written at all. Nothing rewrites a ` + + `\`{ or: [{ field, operator, value }, ...] }\`, and \`$not\` cannot be written at all. Nothing rewrites a ` + `combinator for you — flattening one changes which rows the filter selects — so decide which ` + `rows this filter should select, and write the rules that select exactly those.`; diff --git a/packages/spec/src/ui/view-filter-or-group.test.ts b/packages/spec/src/ui/view-filter-or-group.test.ts index 61d1aef5b2c..65f2c9007a2 100644 --- a/packages/spec/src/ui/view-filter-or-group.test.ts +++ b/packages/spec/src/ui/view-filter-or-group.test.ts @@ -233,7 +233,7 @@ describe('the wire door and the published shape', () => { expect(issues[0]!.message).toContain('`id` is a console row key'); }); - it('the element publishes both kinds, and the group says it is not yet rendered', () => { + it('the element publishes both kinds, and the group carries no PLANNED marker', () => { const json = z.toJSONSchema(ViewFilterElementSchema, { io: 'input', unrepresentable: 'any' }) as { anyOf?: Array<{ properties?: Record }>; }; @@ -241,7 +241,8 @@ describe('the wire door and the published shape', () => { ['field', 'operator', 'value'], ['or'], ]); - expect(ViewFilterOrGroupSchema.description?.startsWith('[PLANNED — not yet rendered]')).toBe(true); + expect(ViewFilterOrGroupSchema.description?.startsWith('An any-of group:')).toBe(true); + expect(ViewFilterOrGroupSchema.description).not.toContain('[PLANNED'); }); it('`isViewFilterOrGroup` tells the element kinds apart by the own `or` key', () => { diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index e89e25aad27..30a11016972 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -1163,16 +1163,16 @@ export function isViewFilterOrGroup(value: unknown): value is { [VIEW_FILTER_OR_ * rule never carries `or`, so it is routed to {@link ViewFilterRuleSchema} * exactly as before. * - * ## [PLANNED — not yet rendered] + * ## Applied by the console * - * No reader applies the group yet. The query a list view runs is built by the - * console (`ui/packages/core/src/utils/filter-converter.ts#toFilterNode`), which - * lowers flat rules only and passes a group through untouched, so the server - * refuses the query as a malformed filter (400 `INVALID_FILTER`) and the list - * shows that error and no rows. It fails closed: a stored group never widens - * what a view, a related list or a picker shows. The describe below says so, - * so no author is told it works. - * The console half is #28370; the marker leaves when it lands. + * The query a list view runs is built by the console + * (`ui/packages/core/src/utils/filter-converter.ts#toFilterNode`), which lowers + * a group to one `or` node of its rules, a child of the list's AND like any + * rule, so a group is ANDed with its sibling elements. The console's filter + * builder edits a filter that is exactly one group as its OR logic, and saves + * its OR logic over two or more rules as one group; a filter holding a group + * beside other elements is applied, and shown read-only in the builder rather + * than flattened on save (`builderRulesOfSpecFilter`). * * @example * ```ts @@ -1214,13 +1214,12 @@ export const ViewFilterOrGroupSchema = lazySchema(() => strictObject({ + 'each judged exactly as a rule at the top level of the filter. A group cannot hold a group.', ), }).describe( - '[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its ' + 'An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its ' + 'rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, ' + 'so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", ' + 'operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the ' - + 'last 7 days. One level only: its members are flat rules. No console reads it yet: until the list ' - + 'query build reads the group, a view that stores one is refused when it loads (the list reports a ' - + 'malformed filter and shows no rows).', + + 'last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its ' + + 'sibling elements, and its filter builder edits a filter that is exactly one group.', )); export type ViewFilterOrGroup = z.input; From 70500e670d85421cafb3f746204f3d42a451dc87 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 16:43:52 +0000 Subject: [PATCH 2/4] docs(spec): regenerate the ui references for the any-of group describe Claude-Session: https://claude.ai/code/session_01JQwxbYCMdGp2Cju6A39cTB Co-authored-by: Claude --- content/docs/references/ui/component.mdx | 20 ++++++++++---------- content/docs/references/ui/page.mdx | 6 +++--- content/docs/references/ui/view.mdx | 10 +++++----- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index b48ef169ca1..d97480f888b 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -518,7 +518,7 @@ View filter rule ### Nested Shape: `ObjectCalendarProps.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -741,7 +741,7 @@ View filter rule ### Nested Shape: `ObjectGanttProps.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -899,7 +899,7 @@ View filter rule ### Nested Shape: `ObjectGridProps.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -1066,7 +1066,7 @@ View filter rule ### Nested Shape: `ObjectKanbanProps.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -1149,7 +1149,7 @@ View filter rule ### Nested Shape: `ObjectMapProps.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -1293,7 +1293,7 @@ View filter rule ### Nested Shape: `ObjectMetricProps.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -1372,7 +1372,7 @@ View filter rule ### Nested Shape: `ObjectTimelineProps.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -1471,7 +1471,7 @@ View filter rule ### Nested Shape: `ObjectTreeProps.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -1938,7 +1938,7 @@ View filter rule ### Nested Shape: `RecordLineItemsProps.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -2099,7 +2099,7 @@ View filter rule ### Nested Shape: `RecordRelatedListProps.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | diff --git a/content/docs/references/ui/page.mdx b/content/docs/references/ui/page.mdx index 985d5c0b0d6..9a513ffc7d9 100644 --- a/content/docs/references/ui/page.mdx +++ b/content/docs/references/ui/page.mdx @@ -68,7 +68,7 @@ View filter rule ### Nested Shape: `ElementDataSource.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -150,7 +150,7 @@ View filter rule ### Nested Shape: `InterfacePageConfig.filterBy[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -487,7 +487,7 @@ View filter rule ### Nested Shape: `PageRecordBinding.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index f5e5689d813..ded560c2b56 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -875,7 +875,7 @@ View filter rule ### Nested Shape: `ListView.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -1264,7 +1264,7 @@ View filter rule ### Nested Shape: `ObjectListView.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -2001,7 +2001,7 @@ View filter rule #### Option 2 -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. ### Properties @@ -2026,7 +2026,7 @@ View filter rule ## ViewFilterOrGroup -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. ### Properties @@ -2535,7 +2535,7 @@ View filter rule ### Nested Shape: `ViewTab.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | From b71e28d33c7a050a0d8b4f8847b81bbffa13a86d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 17:21:06 +0000 Subject: [PATCH 3/4] docs(spec): regenerate the view reference after merging main Claude-Session: https://claude.ai/code/session_01JQwxbYCMdGp2Cju6A39cTB Co-authored-by: Claude --- content/docs/references/ui/view.mdx | 39 ++++++++++++++--------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index ded560c2b56..5bf556f851c 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -800,7 +800,7 @@ Map view configuration | **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer }` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | | **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **description** | `string \| Record` | optional | View description for documentation/tooltips | -| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration | +| **sharing** | `{ lockedBy?: string }` | optional | View sharing configuration: who locked the view configuration | | **requiredPermissions** | `string[]` | optional | [ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through `systemPermissions`, the same list as `requiredPermissions` on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. The server enforces it: a user who does not hold them all is not listed the item and is refused it by name; a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object. | | **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting | | **grouping** | `{ fields: object[] }` | optional | Group records by one or more fields — server-side: the groups and their header numbers come from an aggregate query over the whole filtered set, rows within a group are paged (see GroupingConfigSchema) | @@ -1026,7 +1026,7 @@ An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules ma | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **type** | `Enum<'personal' \| 'collaborative'>` | optional (default: `"collaborative"`) | View ownership type | +| **type** | `never` | optional | [REMOVED] `view.list.sharing.type` was removed in @objectstack/spec 18.3.0 (ADR-0049 enforce-or-remove) — nothing enforced it: a `personal` view was listed for every user who can read the object, and `collaborative` described every view, so the key only ever drew a badge. Delete the key; `sharing.lockedBy` is unchanged. A view is visible to everyone who can read its object; personal named views are a user-owned record, not view metadata (ADR-0017). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **lockedBy** | `string` | optional | User who locked the view configuration | ### Nested Shape: `ListView.grouping` @@ -1188,7 +1188,7 @@ An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules ma | **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer }` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | | **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **description** | `string \| Record` | optional | View description for documentation/tooltips | -| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration | +| **sharing** | `{ lockedBy?: string }` | optional | View sharing configuration: who locked the view configuration | | **requiredPermissions** | `string[]` | optional | [ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through `systemPermissions`, the same list as `requiredPermissions` on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. The server enforces it: a user who does not hold them all is not listed the item and is refused it by name; a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object. | | **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting | | **grouping** | `{ fields: object[] }` | optional | Group records by one or more fields — server-side: the groups and their header numbers come from an aggregate query over the whole filtered set, rows within a group are paged (see GroupingConfigSchema) | @@ -1405,7 +1405,7 @@ An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules ma | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **type** | `Enum<'personal' \| 'collaborative'>` | optional (default: `"collaborative"`) | View ownership type | +| **type** | `never` | optional | [REMOVED] `view.list.sharing.type` was removed in @objectstack/spec 18.3.0 (ADR-0049 enforce-or-remove) — nothing enforced it: a `personal` view was listed for every user who can read the object, and `collaborative` described every view, so the key only ever drew a badge. Delete the key; `sharing.lockedBy` is unchanged. A view is visible to everyone who can read its object; personal named views are a user-owned record, not view metadata (ADR-0017). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **lockedBy** | `string` | optional | User who locked the view configuration | ### Nested Shape: `ObjectListView.grouping` @@ -1770,7 +1770,7 @@ Tab configuration for multi-tab view interface | **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer }` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | | **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **description** | `string \| Record` | optional | View description for documentation/tooltips | -| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration | +| **sharing** | `{ lockedBy?: string }` | optional | View sharing configuration: who locked the view configuration | | **requiredPermissions** | `string[]` | optional | [ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through `systemPermissions`, the same list as `requiredPermissions` on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. The server enforces it: a user who does not hold them all is not listed the item and is refused it by name; a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object. | | **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting | | **grouping** | `{ fields: object[] }` | optional | Group records by one or more fields — server-side: the groups and their header numbers come from an aggregate query over the whole filtered set, rows within a group are paged (see GroupingConfigSchema) | @@ -1849,7 +1849,7 @@ Tab configuration for multi-tab view interface | **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer }` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | | **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **description** | `string \| Record` | optional | View description for documentation/tooltips | -| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration | +| **sharing** | `{ lockedBy?: string }` | optional | View sharing configuration: who locked the view configuration | | **requiredPermissions** | `string[]` | optional | [ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through `systemPermissions`, the same list as `requiredPermissions` on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. The server enforces it: a user who does not hold them all is not listed the item and is refused it by name; a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object. | | **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting | | **grouping** | `{ fields: object[] }` | optional | Group records by one or more fields — server-side: the groups and their header numbers come from an aggregate query over the whole filtered set, rows within a group are paged (see GroupingConfigSchema) | @@ -2104,8 +2104,8 @@ This schema accepts one of the following structures: | **label** | `string \| Record` | optional | Display label (supports i18n). | | **isDefault** | `boolean` | optional | Whether this is the object's default view in the switcher. | | **order** | `integer` | optional | Sort order within the object's view switcher / left rail. | -| **scope** | `Enum<'package' \| 'shared' \| 'personal'>` | optional | Identity layer (defaults to `package` for source-loaded views). | -| **owner** | `never` | optional | [REMOVED] `view.owner` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the user a `personal` view item belonged to, and nothing ever read it: the view switcher (`GET /meta/view?object=`) serves every item bound to the object without looking at `owner`, so a view marked as one user's was listed for every user who can read the object. Delete the key. Nothing restricts a view item to one user today — per-user view scoping is a parked direction (ADR-0017), not a shipped mechanism — so a view item is visible to everyone who can read its object. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **scope** | `Enum<'package' \| 'shared'>` | optional | Identity layer (defaults to `package` for source-loaded views). | +| **owner** | `never` | optional | [REMOVED] `view.owner` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the user a `personal` view item belonged to, and nothing ever read it: the view switcher (`GET /meta/view?object=`) serves every item bound to the object without looking at `owner`, so a view marked as one user's was listed for every user who can read the object. Delete the key. Nothing restricts a view item to one user — personal named views are a user-owned record, not a view item (ADR-0017) — so a view item is visible to everyone who can read its object. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **hidden** | `never` | optional | [REMOVED] `view.hidden` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it promised to hide a view item from the switcher, and nothing ever read it: `GET /meta/view?object=` and the console's view switcher list every item bound to the object, `hidden: true` included. Delete the key; to take a view out of the switcher, delete the view item itself (or stop shipping it from source). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this view. | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | @@ -2146,7 +2146,7 @@ This schema accepts one of the following structures: | **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer }` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | | **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **description** | `string \| Record` | optional | View description for documentation/tooltips | -| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration | +| **sharing** | `{ lockedBy?: string }` | optional | View sharing configuration: who locked the view configuration | | **requiredPermissions** | `string[]` | optional | [ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through `systemPermissions`, the same list as `requiredPermissions` on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. The server enforces it: a user who does not hold them all is not listed the item and is refused it by name; a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object. | | **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting | | **grouping** | `{ fields: object[] }` | optional | Group records by one or more fields — server-side: the groups and their header numbers come from an aggregate query over the whole filtered set, rows within a group are paged (see GroupingConfigSchema) | @@ -2198,8 +2198,8 @@ This schema accepts one of the following structures: | **label** | `string \| Record` | optional | Display label (supports i18n). | | **isDefault** | `boolean` | optional | Whether this is the object's default view in the switcher. | | **order** | `integer` | optional | Sort order within the object's view switcher / left rail. | -| **scope** | `Enum<'package' \| 'shared' \| 'personal'>` | optional | Identity layer (defaults to `package` for source-loaded views). | -| **owner** | `never` | optional | [REMOVED] `view.owner` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the user a `personal` view item belonged to, and nothing ever read it: the view switcher (`GET /meta/view?object=`) serves every item bound to the object without looking at `owner`, so a view marked as one user's was listed for every user who can read the object. Delete the key. Nothing restricts a view item to one user today — per-user view scoping is a parked direction (ADR-0017), not a shipped mechanism — so a view item is visible to everyone who can read its object. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **scope** | `Enum<'package' \| 'shared'>` | optional | Identity layer (defaults to `package` for source-loaded views). | +| **owner** | `never` | optional | [REMOVED] `view.owner` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the user a `personal` view item belonged to, and nothing ever read it: the view switcher (`GET /meta/view?object=`) serves every item bound to the object without looking at `owner`, so a view marked as one user's was listed for every user who can read the object. Delete the key. Nothing restricts a view item to one user — personal named views are a user-owned record, not a view item (ADR-0017) — so a view item is visible to everyone who can read its object. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **hidden** | `never` | optional | [REMOVED] `view.hidden` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it promised to hide a view item from the switcher, and nothing ever read it: `GET /meta/view?object=` and the console's view switcher list every item bound to the object, `hidden: true` included. Delete the key; to take a view out of the switcher, delete the view item itself (or stop shipping it from source). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this view. | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | @@ -2287,8 +2287,8 @@ This schema accepts one of the following structures: | **label** | `string \| Record` | optional | Display label (supports i18n). | | **isDefault** | `boolean` | optional | Whether this is the object's default view in the switcher. | | **order** | `integer` | optional | Sort order within the object's view switcher / left rail. | -| **scope** | `Enum<'package' \| 'shared' \| 'personal'>` | optional | Identity layer (defaults to `package` for source-loaded views). | -| **owner** | `never` | optional | [REMOVED] `view.owner` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the user a `personal` view item belonged to, and nothing ever read it: the view switcher (`GET /meta/view?object=`) serves every item bound to the object without looking at `owner`, so a view marked as one user's was listed for every user who can read the object. Delete the key. Nothing restricts a view item to one user today — per-user view scoping is a parked direction (ADR-0017), not a shipped mechanism — so a view item is visible to everyone who can read its object. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **scope** | `Enum<'package' \| 'shared'>` | optional | Identity layer (defaults to `package` for source-loaded views). | +| **owner** | `never` | optional | [REMOVED] `view.owner` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the user a `personal` view item belonged to, and nothing ever read it: the view switcher (`GET /meta/view?object=`) serves every item bound to the object without looking at `owner`, so a view marked as one user's was listed for every user who can read the object. Delete the key. Nothing restricts a view item to one user — personal named views are a user-owned record, not a view item (ADR-0017) — so a view item is visible to everyone who can read its object. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **hidden** | `never` | optional | [REMOVED] `view.hidden` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it promised to hide a view item from the switcher, and nothing ever read it: `GET /meta/view?object=` and the console's view switcher list every item bound to the object, `hidden: true` included. Delete the key; to take a view out of the switcher, delete the view item itself (or stop shipping it from source). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this view. | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | @@ -2334,7 +2334,7 @@ This schema accepts one of the following structures: | **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer }` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | | **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **description** | `string \| Record` | optional | View description for documentation/tooltips | -| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration | +| **sharing** | `{ lockedBy?: string }` | optional | View sharing configuration: who locked the view configuration | | **requiredPermissions** | `string[]` | optional | [ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through `systemPermissions`, the same list as `requiredPermissions` on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. The server enforces it: a user who does not hold them all is not listed the item and is refused it by name; a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object. | | **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting | | **grouping** | `{ fields: object[] }` | optional | Group records by one or more fields — server-side: the groups and their header numbers come from an aggregate query over the whole filtered set, rows within a group are paged (see GroupingConfigSchema) | @@ -2393,8 +2393,8 @@ This schema accepts one of the following structures: | **label** | `string \| Record` | optional | Display label (supports i18n). | | **isDefault** | `boolean` | optional | Whether this is the object's default view in the switcher. | | **order** | `integer` | optional | Sort order within the object's view switcher / left rail. | -| **scope** | `Enum<'package' \| 'shared' \| 'personal'>` | optional | Identity layer (defaults to `package` for source-loaded views). | -| **owner** | `never` | optional | [REMOVED] `view.owner` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the user a `personal` view item belonged to, and nothing ever read it: the view switcher (`GET /meta/view?object=`) serves every item bound to the object without looking at `owner`, so a view marked as one user's was listed for every user who can read the object. Delete the key. Nothing restricts a view item to one user today — per-user view scoping is a parked direction (ADR-0017), not a shipped mechanism — so a view item is visible to everyone who can read its object. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | +| **scope** | `Enum<'package' \| 'shared'>` | optional | Identity layer (defaults to `package` for source-loaded views). | +| **owner** | `never` | optional | [REMOVED] `view.owner` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the user a `personal` view item belonged to, and nothing ever read it: the view switcher (`GET /meta/view?object=`) serves every item bound to the object without looking at `owner`, so a view marked as one user's was listed for every user who can read the object. Delete the key. Nothing restricts a view item to one user — personal named views are a user-owned record, not a view item (ADR-0017) — so a view item is visible to everyone who can read its object. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **hidden** | `never` | optional | [REMOVED] `view.hidden` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it promised to hide a view item from the switcher, and nothing ever read it: `GET /meta/view?object=` and the console's view switcher list every item bound to the object, `hidden: true` included. Delete the key; to take a view out of the switcher, delete the view item itself (or stop shipping it from source). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this view. | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | @@ -2480,26 +2480,25 @@ Whether `config` is a ListView (list family) or a FormView. ## ViewScope -View identity layer: package | shared | personal. +View identity layer: package | shared. ### Allowed Values * `package` * `shared` -* `personal` --- ## ViewSharing -View sharing and access configuration +View sharing configuration — who locked the view configuration ### Properties | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **type** | `Enum<'personal' \| 'collaborative'>` | optional (default: `"collaborative"`) | View ownership type | +| **type** | `never` | optional | [REMOVED] `view.list.sharing.type` was removed in @objectstack/spec 18.3.0 (ADR-0049 enforce-or-remove) — nothing enforced it: a `personal` view was listed for every user who can read the object, and `collaborative` described every view, so the key only ever drew a badge. Delete the key; `sharing.lockedBy` is unchanged. A view is visible to everyone who can read its object; personal named views are a user-owned record, not view metadata (ADR-0017). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **lockedBy** | `string` | optional | User who locked the view configuration | From f42d8a2eac84c80c119768e8097ee9339634ccc5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 18:36:37 +0000 Subject: [PATCH 4/4] docs(spec): the record-form filter conversion's $or TODO reason drops its stale status dollarKeysReason still told a $or author the any-of group element is "declared, not yet applied by any console"; the console applies it. Claude-Session: https://claude.ai/code/session_01JQwxbYCMdGp2Cju6A39cTB Co-authored-by: Claude --- packages/spec/src/conversions/registry.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/spec/src/conversions/registry.ts b/packages/spec/src/conversions/registry.ts index 8ba55456e27..a12912c7d80 100644 --- a/packages/spec/src/conversions/registry.ts +++ b/packages/spec/src/conversions/registry.ts @@ -13927,8 +13927,8 @@ function dollarKeysReason(keys: readonly string[]): string { : ` (its top-level ${quoteKeys(others)} ${others.length === 1 ? 'is' : 'are'} not a field either)`; return `carries the combinator${combinators.length === 1 ? '' : 's'} ${quoteKeys(combinators)}` + `${alsoNotFields}: a rule array's elements AND, so \`$and\` is only the separate rules it ` - + 'joins, `$or` over flat rules only an any-of group element `{ or: [rule, ...] }` (declared, ' - + 'not yet applied by any console) that this conversion does not write, and `$not` has no rule ' + + 'joins, `$or` over flat rules only an any-of group element `{ or: [rule, ...] }` that this ' + + 'conversion does not write, and `$not` has no rule ' + 'spelling; a combinator is never flattened — ' + 'that would change which rows the filter selects. Decide which rows it should select, and ' + 'write the rules that select exactly those';