From e99f214743578ecb6006593bf61fc5cc83e43c23 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 07:36:55 +0000 Subject: [PATCH 01/11] fix(lint): walk page filterBy and lookup-field lookupFilters as authored filters FILTER_KEYS gains the two consumed rule-array carriers the walk never entered, so filter-preset-comparand (and filter-token-unknown) judge them. The preset rule binds filterBy through interfaceConfig.source and lookupFilters through the field's reference, never the owning object. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude --- packages/lint/src/filter-walk.ts | 21 ++++++- .../lint/src/validate-preset-comparands.ts | 58 ++++++++++++++++++- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/packages/lint/src/filter-walk.ts b/packages/lint/src/filter-walk.ts index 6848404f34a..a8a6575bfbd 100644 --- a/packages/lint/src/filter-walk.ts +++ b/packages/lint/src/filter-walk.ts @@ -64,8 +64,27 @@ type AnyRec = Record; * door already judges it at parse), and listing it here is what extends the * three walking rules — tokens, empty combinators, preset comparands — to the * new position instead of leaving a per-rule hole. + * + * `filterBy` and `lookupFilters` (#19791) are the two consumed RULE-ARRAY + * carriers that do not spell the key `filter` either: a list page's always-on + * base filter (`interfaceConfig.filterBy`, `z.array(ViewFilterRuleSchema)`, + * which the console spreads into the list query beside the view's own + * `filter`) and a lookup field's picker filter (`lookupFilters`, lowered by the + * console to a Mongo `$filter` on the REFERENCED object). Both values reach + * the engine's `where` verbatim. Their schemas carry no preset check, so + * before this entry an ordering preset in either parsed green, linted green, + * and was refused only at query time. What the entry changes per walking rule, + * measured: preset comparands and filter tokens now judge both carriers (the + * engine resolves the same two placeholder vocabularies there and refuses the + * same residue); empty combinators is unchanged, because both carriers are + * arrays and that rule judges Mongo-shape nodes only; the flow token rule is + * unchanged, because it walks `flows` alone and no flow schema declares + * either key. The binding half — which object a condition on each carrier + * addresses — is `validate-preset-comparands.ts`'s, not this walk's. */ -export const FILTER_KEYS: ReadonlySet = new Set(['filter', 'filters', 'runtimeFilter', 'relatedListFilter']); +export const FILTER_KEYS: ReadonlySet = new Set([ + 'filter', 'filters', 'runtimeFilter', 'relatedListFilter', 'filterBy', 'lookupFilters', +]); /** One stack collection a caller wants walked. */ export interface FilterSurface { diff --git a/packages/lint/src/validate-preset-comparands.ts b/packages/lint/src/validate-preset-comparands.ts index ed5acbe83af..614554f5776 100644 --- a/packages/lint/src/validate-preset-comparands.ts +++ b/packages/lint/src/validate-preset-comparands.ts @@ -115,6 +115,16 @@ import { indexObjectGraph, recordsOf, resolveFieldPath, type ObjectGraph } from * (`validate-flow-node-writes`), a templated `{…}` value skipped; * - `dataSource.object`, then `properties.object` / `properties.objectName` * — page components (`validate-page-field-bindings`); + * - `interfaceConfig.source` — a list page's config, which names its object + * `source` (the console queries exactly that object); read only at that + * position, the page's own `interfaceConfig`, and falling through to the + * page's `object` when absent — the binding `validate-page-field-bindings` + * already makes there (#19791); + * - a lookup field's `lookupFilters` → that field's `reference`, else NOTHING: + * the picker queries the REFERENCED object, so like the public-lookup picker + * below it must never fall through to the object that owns the field (a + * `relatedListFilter` on the same field keeps binding to the owner, whose + * rows it filters) (#19791); * - `publicPicker.object`, else the enclosing form field's `reference` * resolved on the view's object, else NOTHING — a form field's public-lookup * picker (`FormFieldPublicPickerSchema`) queries the REFERENCED object, so @@ -354,7 +364,10 @@ interface Ancestor { * the SAME `recordsOf` coercion `walkAuthoredFilters` applied to the * collection, so a map-form collection's injected `name` is visible here too. */ -function ancestorsOf(stack: AnyRec, path: string): { collection: string; chain: Ancestor[] } | null { +function ancestorsOf( + stack: AnyRec, + path: string, +): { collection: string; chain: Ancestor[]; filterKey: string | number } | null { const segments = pathSegments(path); if (!segments || segments.length < 3) return null; const [collection, index, ...rest] = segments; @@ -373,7 +386,7 @@ function ancestorsOf(stack: AnyRec, path: string): { collection: string; chain: if (isPlainObject(node)) chain.push({ key, node }); else if (!Array.isArray(node)) break; } - return { collection, chain }; + return { collection, chain, filterKey: rest[rest.length - 1] }; } /** @@ -385,6 +398,22 @@ function ancestorsOf(stack: AnyRec, path: string): { collection: string; chain: */ const PUBLIC_PICKER_KEY = 'publicPicker'; +/** + * [#19791] The filter key of a lookup field's picker filter + * (`FieldSchema.lookupFilters`). The console lowers each `{ field, operator, + * value }` entry to a Mongo `$filter` on the REFERENCED object — the field's + * `reference` — and never on the object that owns the field. Every spelling of + * this key in the platform means that same picker filter, so the reader claims + * it by the filter key itself. + */ +const LOOKUP_FILTERS_KEY = 'lookupFilters'; + +/** + * [#19791] The page key holding a list page's interface config + * (`InterfacePageConfigSchema`), which names its object `source`. + */ +const INTERFACE_CONFIG_KEY = 'interfaceConfig'; + /** * Bind one authored filter to the object its conditions address — the NEAREST * ancestor that declares one, in the carriers' own spellings (module note, @@ -401,6 +430,21 @@ function boundObjectOf( ): string | undefined { const located = ancestorsOf(stack, path); if (!located) return undefined; + + // [#19791] A lookup field's picker filter is a CLAIMING reader, for the + // #16106 B1 reason the public-lookup picker below is one: its conditions + // address the REFERENCED object, so the position binds to the enclosing + // field's literal `reference` and to NOTHING otherwise. Falling through to + // the owning object would be a false refusal wherever the two objects share + // a field name with differing types. Unbound leaves arm 2 silent on this + // subtree only; arm 1 still judges it. + if (located.filterKey === LOOKUP_FILTERS_KEY) { + const field = located.chain[located.chain.length - 1].node; + return Object.prototype.hasOwnProperty.call(field, LOOKUP_FILTERS_KEY) + ? literalObjectName(field.reference) + : undefined; + } + return bindAncestors(located.collection, located.chain, located.chain.length - 1, datasets, graph); } @@ -450,6 +494,16 @@ function bindAncestors( return verdict?.kind === 'ok' ? strName(verdict.meta?.reference) : undefined; } + // [#19791] A list page's `interfaceConfig` names its object `source`. Read + // at that one position — the page's own config, directly under the + // `pages` item — and NOT claiming: without a `source` the search goes on + // outward to the page's `object`, the fallback + // `validate-page-field-bindings` applies to the same config. + if (collection === 'pages' && i === 1 && key === INTERFACE_CONFIG_KEY) { + const viaSource = literalObjectName(r.source); + if (viaSource) return viaSource; + } + const direct = literalObjectName(r.object) ?? literalObjectName(r.objectName); if (direct) return direct; From 74612771f33de436a438db6be40c455b60b313aa Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 07:38:07 +0000 Subject: [PATCH 02/11] test(lint): pin filterBy and lookupFilters as walked filter carriers Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude --- .../lint/src/validate-filter-tokens.test.ts | 38 ++++++ .../src/validate-preset-comparands.test.ts | 113 ++++++++++++++++++ 2 files changed, 151 insertions(+) diff --git a/packages/lint/src/validate-filter-tokens.test.ts b/packages/lint/src/validate-filter-tokens.test.ts index 068004f41dc..01d23e4610a 100644 --- a/packages/lint/src/validate-filter-tokens.test.ts +++ b/packages/lint/src/validate-filter-tokens.test.ts @@ -180,6 +180,44 @@ describe('validateFilterTokens', () => { ).toEqual([]); }); + // [#19791] A list page's `interfaceConfig.filterBy` and a lookup field's + // `lookupFilters` reach the engine's `where` verbatim, where the same two + // placeholder vocabularies resolve — so an unknown token there is the same + // silent literal it is in a view's `filter`, and the known ones are fine. + it('reaches a page filterBy and a lookup field lookupFilters', () => { + const findings = validateFilterTokens({ + objects: [{ + name: 'invoice', + fields: { + account: { + type: 'lookup', + reference: 'account', + lookupFilters: [ + { field: 'owner', operator: 'eq', value: '{current_user}' }, + { field: 'owner', operator: 'ne', value: '{current_user_id}' }, + ], + }, + }, + }], + pages: [{ + name: 'deals', + type: 'list', + interfaceConfig: { + source: 'deal', + filterBy: [ + { field: 'owner', operator: 'equals', value: '{user_id}' }, + { field: 'created_at', operator: 'greater_than', value: '{30_days_ago}' }, + ], + }, + }], + }); + expect(findings.map((f) => f.path).sort()).toEqual([ + 'objects[0].fields.account.lookupFilters[0].value', + 'pages[0].interfaceConfig.filterBy[0].value', + ]); + for (const f of findings) expect(f.rule).toBe(FILTER_TOKEN_UNKNOWN); + }); + it('survives a cyclic metadata graph', () => { const dash: Record = { name: 'd', widgets: [] }; dash.self = dash; diff --git a/packages/lint/src/validate-preset-comparands.test.ts b/packages/lint/src/validate-preset-comparands.test.ts index 0c78a34a91a..31355ebc6aa 100644 --- a/packages/lint/src/validate-preset-comparands.test.ts +++ b/packages/lint/src/validate-preset-comparands.test.ts @@ -672,3 +672,116 @@ describe('validatePresetComparands — arm 2, the FIELD-TYPED equality / members ]); }); }); + +// ── [#19791] The two consumed rule-array carriers the walk never entered ──── +// +// A list page's `interfaceConfig.filterBy` and a lookup field's +// `lookupFilters` both reach the engine's `where` verbatim, and neither schema +// carries a preset check. Before `FILTER_KEYS` named them, the card's rule +// `{ field: 'close_date', operator: 'gt', value: 'last_30_days' }` parsed +// green AND linted green on both, while the identical rule on a component +// `dataSource.filter` was refused — the lit control every block below keeps. +describe('validatePresetComparands — page filterBy and lookup-field lookupFilters (#19791)', () => { + const card = { field: 'close_date', operator: 'gt', value: 'last_30_days' }; + // `close_date` is a DATE on `crm_deal` and a SELECT on `crm_region`, whose + // option value collides with a preset name — the pair that tells a binding + // to the right object from a binding to the wrong one. + const carrierObjects = [ + { name: 'crm_deal', fields: { close_date: { type: 'date' }, name: { type: 'text' } } }, + { name: 'crm_region', fields: { close_date: { type: 'select', options: [{ label: 'This Quarter', value: 'this_quarter' }] } } }, + ]; + const listPage = (interfaceConfig: Record, over: Record = {}) => ({ + objects: carrierObjects, + pages: [{ name: 'deals', type: 'list', interfaceConfig, ...over }], + }); + const lookupHolder = (account: Record, owner: Record = {}) => ({ + objects: [ + ...carrierObjects, + { name: 'crm_invoice', fields: { ...owner, account: { type: 'lookup', ...account } } }, + ], + }); + + it("refuses the card's rule in a page's filterBy, beside the lit control on the same page", () => { + const findings = validatePresetComparands({ + objects: carrierObjects, + pages: [{ + name: 'deals', type: 'list', + interfaceConfig: { source: 'crm_deal', filterBy: [card, { field: 'close_date', operator: 'between', value: ['this_week', '2026-01-01'] }] }, + components: [{ type: 'list', dataSource: { object: 'crm_deal', filter: [card] } }], + }], + }); + expect(findings.map((f) => f.path).sort()).toEqual([ + 'pages[0].components[0].dataSource.filter[0].value', + 'pages[0].interfaceConfig.filterBy[0].value', + 'pages[0].interfaceConfig.filterBy[1].value[0]', + ]); + const onFilterBy = findings.find((f) => f.path === 'pages[0].interfaceConfig.filterBy[0].value')!; + expect(onFilterBy.severity).toBe('error'); + expect(onFilterBy.rule).toBe(FILTER_PRESET_COMPARAND); + expect(onFilterBy.where).toBe('page "deals"'); + expect(onFilterBy.message).toContain('"last_30_days" is a dashboard date-range PRESET name'); + }); + + it("refuses the card's rule in a lookup field's lookupFilters, map-form and array-form fields alike", () => { + expect(validatePresetComparands(lookupHolder({ reference: 'crm_deal', lookupFilters: [card] })) + .map((f) => f.path)).toEqual(['objects[2].fields.account.lookupFilters[0].value']); + expect(validatePresetComparands({ + objects: [{ name: 'crm_invoice', fields: [{ name: 'account', type: 'lookup', reference: 'crm_deal', lookupFilters: [card] }] }], + }).map((f) => f.path)).toEqual(['objects[0].fields[0].lookupFilters[0].value']); + }); + + it('judges equality on filterBy against the object interfaceConfig.source names, falling back to the page object', () => { + const eq = { field: 'close_date', operator: 'equals', value: 'this_quarter' }; + // `source` is the date object: refused. + expect(validatePresetComparands(listPage({ source: 'crm_deal', filterBy: [eq] })).map((f) => f.path)) + .toEqual(['pages[0].interfaceConfig.filterBy[0].value']); + // `source` is the select object while the page's own `object` is the date + // one: the list queries `source`, so the equality is the picklist case. + expect(validatePresetComparands(listPage({ source: 'crm_region', filterBy: [eq] }, { object: 'crm_deal' }))).toEqual([]); + // No `source`: the page's `object`, as validate-page-field-bindings reads it. + expect(validatePresetComparands(listPage({ filterBy: [eq] }, { object: 'crm_deal' })).map((f) => f.path)) + .toEqual(['pages[0].interfaceConfig.filterBy[0].value']); + }); + + it("judges equality on lookupFilters against the field's reference, never the owning object", () => { + const eq = { field: 'close_date', operator: 'eq', value: 'this_quarter' }; + // The referenced object declares `close_date` as a date: refused. + expect(validatePresetComparands(lookupHolder({ reference: 'crm_deal', lookupFilters: [eq] })).map((f) => f.path)) + .toEqual(['objects[2].fields.account.lookupFilters[0].value']); + // The OWNING object declares a date `close_date`, the referenced one a + // select: binding to the owner would be the #16106 B1 false refusal. + const owner = { close_date: { type: 'date' } }; + expect(validatePresetComparands(lookupHolder({ reference: 'crm_region', lookupFilters: [eq] }, owner))).toEqual([]); + // No `reference` to follow: unjudged by arm 2, never the owner. + expect(validatePresetComparands(lookupHolder({ lookupFilters: [eq] }, owner))).toEqual([]); + // A `relatedListFilter` on the SAME field keeps binding to the owner, + // whose rows it filters — one field, two filters, two objects. + const both = validatePresetComparands(lookupHolder( + { reference: 'crm_region', lookupFilters: [eq], relatedListFilter: { close_date: 'this_quarter' } }, + owner, + )); + expect(both.map((f) => f.path)).toEqual(['objects[2].fields.account.relatedListFilter.close_date']); + }); + + it('stays quiet on every legal comparand in both carriers', () => { + const legal = [ + { field: 'close_date', operator: 'gt', value: '{30_days_ago}' }, + { field: 'close_date', operator: 'gte', value: '2026-01-15' }, + { field: 'close_date', operator: 'between', value: ['{week_start}', '{week_end}'] }, + { field: 'name', operator: 'equals', value: 'last_30_days' }, + ]; + // The lookup picker's own flat operator vocabulary (`FieldSchema.lookupFilters`). + const legalLookup = [ + { field: 'close_date', operator: 'gt', value: '{30_days_ago}' }, + { field: 'close_date', operator: 'gte', value: '2026-01-15' }, + { field: 'name', operator: 'eq', value: 'last_30_days' }, + ]; + expect(validatePresetComparands({ + objects: [ + ...carrierObjects, + { name: 'crm_invoice', fields: { account: { type: 'lookup', reference: 'crm_deal', lookupFilters: legalLookup } } }, + ], + pages: [{ name: 'deals', type: 'list', interfaceConfig: { source: 'crm_deal', filterBy: legal } }], + })).toEqual([]); + }); +}); From 63597c22c571ac8f07f368b370701a10f8563e7d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 07:41:36 +0000 Subject: [PATCH 03/11] chore(changeset): lint walks page filterBy and lookup-field lookupFilters Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude --- .changeset/19791-filter-walk-rule-array-carriers.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/19791-filter-walk-rule-array-carriers.md diff --git a/.changeset/19791-filter-walk-rule-array-carriers.md b/.changeset/19791-filter-walk-rule-array-carriers.md new file mode 100644 index 00000000000..0299fab7ae5 --- /dev/null +++ b/.changeset/19791-filter-walk-rule-array-carriers.md @@ -0,0 +1,12 @@ +--- +"@objectstack/lint": patch +--- + +`filter-preset-comparand` now judges a list page's `interfaceConfig.filterBy` and a lookup field's `lookupFilters`, the two consumed filter carriers the shared filter walk never entered (#19791). + +Both carriers are rule arrays (`{ field, operator, value }`) whose values reach the engine's `where` verbatim, and neither schema carries a preset check. So `{ field: 'close_date', operator: 'gt', value: 'last_30_days' }` in either one parsed green and linted green, then the engine refused it at query time (`INVALID_FILTER` / 400). The same rule on a component `dataSource.filter` or a view `filter` was already refused. `filterBy` and `lookupFilters` join `FILTER_KEYS`, so `os lint`, `os validate` and the runtime publish gate (for `page` and `object` writes) now refuse it where it is written. Each finding carries its path (`pages[0].interfaceConfig.filterBy[0].value`, `objects[2].fields.account.lookupFilters[0].value`). + +- **Which object a condition addresses.** The field-typed arm, which refuses a preset under equality or membership on a `date` / `datetime` field, binds `filterBy` to `interfaceConfig.source`. Without a `source` it falls back to the page's `object`. It binds `lookupFilters` to the field's `reference` and never to the object that owns the field, because the picker queries the referenced object. A `relatedListFilter` on the same field still binds to the owner. +- **`filter-token-unknown` reaches the same two carriers.** An unresolvable placeholder such as `{current_user}` in `filterBy` or `lookupFilters` is now reported, as it already is in a view's `filter`. `{current_user_id}` and the date macros stay clean. +- **Unchanged:** `filter-empty-combinator` / `filter-empty-node` judge Mongo-shape nodes only, and both carriers are arrays. `flow-filter-token-unknown` walks `flows`, and no flow schema declares either key. +- **What you do:** in a `filterBy` or `lookupFilters` rule, replace a preset name with the `{date-macro}` window the message names (`{ operator: 'gte', value: '{30_days_ago}' }`) or with an ISO date. From c6b83d66af6d19b7b1ff53345d6455bf4262a664 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 11:20:06 +0000 Subject: [PATCH 04/11] fix(spec): drop the preset entry's unwalked-carrier group and by-hand clause The lint's filter walk now descends a page's interfaceConfig.filterBy and a lookup field's lookupFilters, so the entry's surface group that put both keys beyond every publish door, and the by-hand search its acceptanceCriteria prescribed for them, no longer describe the platform. Both are deleted; the surface's group count goes with them. registry.ts is regenerated with gen:migration-registry. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude --- ....filter-preset-ordering-comparand-refused.ts | 17 +++-------------- packages/spec/src/migrations/registry.ts | 17 +++-------------- 2 files changed, 6 insertions(+), 28 deletions(-) diff --git a/packages/spec/src/migrations/entries/semantic/18.filter-preset-ordering-comparand-refused.ts b/packages/spec/src/migrations/entries/semantic/18.filter-preset-ordering-comparand-refused.ts index 114ae5029b4..514d9a20967 100644 --- a/packages/spec/src/migrations/entries/semantic/18.filter-preset-ordering-comparand-refused.ts +++ b/packages/spec/src/migrations/entries/semantic/18.filter-preset-ordering-comparand-refused.ts @@ -13,7 +13,7 @@ export const entry: SemanticMigration = { + '$gt / $gte / $lt / $lte value or a $between endpoint, a greater_than / less_than / ' + 'before / after / between view filter rule value, or an ordering [field, op, value] ' + 'filter triple. WHICH DOOR refuses it at publish is decided by the carrier\'s declared ' - + 'type and by its key. The carriers measured fall in three groups, and the groups are a ' + + 'type and by its key. The carriers measured fall in groups, and the groups are a ' + 'list of what was measured, not a closed partition: the grep in the acceptance criteria ' + 'is the catch-all. (1) A slot typed FilterConditionSchema — a ' + 'dashboard widget filter, a dashboard global-filter options-source filter ' @@ -26,11 +26,7 @@ export const entry: SemanticMigration = { + 'filter, a page element\'s dataSource.filter, a page component\'s filter prop), and a ' + 'Mongo-shape filter record typed as a loose record rather than FilterConditionSchema (a ' + 'flow CRUD node\'s config.filter). The lint is likewise what refuses a preset in an ' - + 'ordering filter triple wherever its walk meets one. (3) A filter under a key the lint ' - + 'does NOT walk parses GREEN and lints GREEN, so neither door refuses it at publish and ' - + 'only a search of the authored and stored metadata finds it: a page\'s ' - + 'interfaceConfig.filterBy rule array, and a lookup field\'s lookupFilters, whose ordering ' - + 'operators are spelled gt / gte / lt / lte', + + 'ordering filter triple wherever its walk meets one', replacement: 'the date-macro window the preset already means — { $gte: "{30_days_ago}" } for ' + 'last_30_days, { $between: ["{week_start}", "{week_end}"] } for this_week, and so on ' @@ -81,14 +77,7 @@ export const entry: SemanticMigration = { + 'grep is the catch-all; the surface\'s groups are the carriers measured. The sweep is ' + 'mechanical for groups (1) and (2): `os validate` / `os lint` report each one by path, and ' + 'a group (1) slot is also refused by a `safeParse` of the schema that declares it, at the ' - + 'comparand\'s own path. Group (3) is BY HAND, because nothing reports it. Search every ' - + 'page for an `interfaceConfig.filterBy` rule whose operator is an ordering one ' - + '(greater_than, greater_than_or_equal, less_than, less_than_or_equal, before, after, ' - + 'between, or an alias of one) and whose value — or either `between` endpoint — is one of ' - + 'the thirteen names. Search every lookup field for a `lookupFilters` entry whose operator ' - + 'is `gt`, `gte`, `lt` or `lte` — the only ordering spellings that key accepts; it has no ' - + '`between` — and whose value is one of the thirteen names. Take each window from ' - + '`DATE_RANGE_PRESET_MACRO_WINDOWS`, since no rejection names it. Leave presets in dashboard ' + + 'comparand\'s own path. Leave presets in dashboard ' + 'date-filter positions (dateRange.defaultRange, date global filter defaultValue) ' + 'untouched — they remain the declared vocabulary there. A filter that carried one of ' + 'these shapes was never returning the window it named (silent zero before the engine ' diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 8430b24a9f3..7a05b2beeea 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -8703,7 +8703,7 @@ const step18: MigrationStep = { + '$gt / $gte / $lt / $lte value or a $between endpoint, a greater_than / less_than / ' + 'before / after / between view filter rule value, or an ordering [field, op, value] ' + 'filter triple. WHICH DOOR refuses it at publish is decided by the carrier\'s declared ' - + 'type and by its key. The carriers measured fall in three groups, and the groups are a ' + + 'type and by its key. The carriers measured fall in groups, and the groups are a ' + 'list of what was measured, not a closed partition: the grep in the acceptance criteria ' + 'is the catch-all. (1) A slot typed FilterConditionSchema — a ' + 'dashboard widget filter, a dashboard global-filter options-source filter ' @@ -8716,11 +8716,7 @@ const step18: MigrationStep = { + 'filter, a page element\'s dataSource.filter, a page component\'s filter prop), and a ' + 'Mongo-shape filter record typed as a loose record rather than FilterConditionSchema (a ' + 'flow CRUD node\'s config.filter). The lint is likewise what refuses a preset in an ' - + 'ordering filter triple wherever its walk meets one. (3) A filter under a key the lint ' - + 'does NOT walk parses GREEN and lints GREEN, so neither door refuses it at publish and ' - + 'only a search of the authored and stored metadata finds it: a page\'s ' - + 'interfaceConfig.filterBy rule array, and a lookup field\'s lookupFilters, whose ordering ' - + 'operators are spelled gt / gte / lt / lte', + + 'ordering filter triple wherever its walk meets one', replacement: 'the date-macro window the preset already means — { $gte: "{30_days_ago}" } for ' + 'last_30_days, { $between: ["{week_start}", "{week_end}"] } for this_week, and so on ' @@ -8771,14 +8767,7 @@ const step18: MigrationStep = { + 'grep is the catch-all; the surface\'s groups are the carriers measured. The sweep is ' + 'mechanical for groups (1) and (2): `os validate` / `os lint` report each one by path, and ' + 'a group (1) slot is also refused by a `safeParse` of the schema that declares it, at the ' - + 'comparand\'s own path. Group (3) is BY HAND, because nothing reports it. Search every ' - + 'page for an `interfaceConfig.filterBy` rule whose operator is an ordering one ' - + '(greater_than, greater_than_or_equal, less_than, less_than_or_equal, before, after, ' - + 'between, or an alias of one) and whose value — or either `between` endpoint — is one of ' - + 'the thirteen names. Search every lookup field for a `lookupFilters` entry whose operator ' - + 'is `gt`, `gte`, `lt` or `lte` — the only ordering spellings that key accepts; it has no ' - + '`between` — and whose value is one of the thirteen names. Take each window from ' - + '`DATE_RANGE_PRESET_MACRO_WINDOWS`, since no rejection names it. Leave presets in dashboard ' + + 'comparand\'s own path. Leave presets in dashboard ' + 'date-filter positions (dateRange.defaultRange, date global filter defaultValue) ' + 'untouched — they remain the declared vocabulary there. A filter that carried one of ' + 'these shapes was never returning the window it named (silent zero before the engine ' From 258a8bc1fa089fc45b30779167f663b32b85a0c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 11:20:06 +0000 Subject: [PATCH 05/11] chore(changeset): spec patch for the preset entry correction Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude --- .changeset/19791-preset-entry-filterby-lookupfilters.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/19791-preset-entry-filterby-lookupfilters.md diff --git a/.changeset/19791-preset-entry-filterby-lookupfilters.md b/.changeset/19791-preset-entry-filterby-lookupfilters.md new file mode 100644 index 00000000000..71bf89e26eb --- /dev/null +++ b/.changeset/19791-preset-entry-filterby-lookupfilters.md @@ -0,0 +1,7 @@ +--- +'@objectstack/spec': patch +--- + +The shipped ADR-0087 semantic entry `filter-preset-ordering-comparand-refused` drops, from its `surface`, the group that said a page's `interfaceConfig.filterBy` and a lookup field's `lookupFilters` are refused by neither door at publish, and drops, from its `acceptanceCriteria`, the by-hand search it prescribed for those two keys. The `@objectstack/lint` `filter-preset-comparand` rule now walks both keys, so `os lint`, `os validate` and the runtime publish gate (on `page` and `object` writes) refuse `{ field: 'close_date', operator: 'gt', value: 'last_30_days' }` in either one, while the schema parse still accepts it. + +Clause-②: no From 559a023798f67ec9fd17a1e6b8b8100fd65ae1c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 12:19:26 +0000 Subject: [PATCH 06/11] fix(spec): drop the preset entry's reached-by-neither clause from reason The lint now walks a page's interfaceConfig.filterBy and a lookup field's lookupFilters, so the reason clause saying both are reached by neither door is false. The clause is deleted; the sentence ends on the surface's groups. registry.ts is regenerated with gen:migration-registry. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude --- .../semantic/18.filter-preset-ordering-comparand-refused.ts | 3 +-- packages/spec/src/migrations/registry.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/spec/src/migrations/entries/semantic/18.filter-preset-ordering-comparand-refused.ts b/packages/spec/src/migrations/entries/semantic/18.filter-preset-ordering-comparand-refused.ts index 514d9a20967..5df45c9f062 100644 --- a/packages/spec/src/migrations/entries/semantic/18.filter-preset-ordering-comparand-refused.ts +++ b/packages/spec/src/migrations/entries/semantic/18.filter-preset-ordering-comparand-refused.ts @@ -51,8 +51,7 @@ export const entry: SemanticMigration = { + 'shape on the slots typed that way, and the @objectstack/lint filter-preset-comparand ' + 'rule refuses it on every filter its walk reaches, which makes it the only door for a ' + 'walked filter whose declared type carries no preset check. Both answer at publish, where ' - + 'the author — an AI author in particular — can still act on the message; a page\'s ' - + 'interfaceConfig.filterBy and a lookup field\'s lookupFilters are reached by neither, and ' + + 'the author — an AI author in particular — can still act on the message; ' + 'the surface\'s groups say which measured carrier sits under which door. Ordering ' + 'positions only at the schema door, deliberately: it judges no equality or membership, ' + 'because a select/picklist column legitimately stores values that collide with preset ' diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 7a05b2beeea..1d58dae3981 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -8741,8 +8741,7 @@ const step18: MigrationStep = { + 'shape on the slots typed that way, and the @objectstack/lint filter-preset-comparand ' + 'rule refuses it on every filter its walk reaches, which makes it the only door for a ' + 'walked filter whose declared type carries no preset check. Both answer at publish, where ' - + 'the author — an AI author in particular — can still act on the message; a page\'s ' - + 'interfaceConfig.filterBy and a lookup field\'s lookupFilters are reached by neither, and ' + + 'the author — an AI author in particular — can still act on the message; ' + 'the surface\'s groups say which measured carrier sits under which door. Ordering ' + 'positions only at the schema door, deliberately: it judges no equality or membership, ' + 'because a select/picklist column legitimately stores values that collide with preset ' From 48dfbe5c214ca172e2b72ae881816f2589c2698c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 12:19:26 +0000 Subject: [PATCH 07/11] chore(changeset): drop the pending unwalked-carrier item and name reason The pending preset-entry changeset's third group said filterBy and lookupFilters lint green and are swept by hand; this branch makes both false, so the item and the group count go. The spec patch changeset now names reason beside surface. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude --- .changeset/19778-preset-entry-carriers.md | 3 +-- .changeset/19791-preset-entry-filterby-lookupfilters.md | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.changeset/19778-preset-entry-carriers.md b/.changeset/19778-preset-entry-carriers.md index 7e6b178d473..89af9888f58 100644 --- a/.changeset/19778-preset-entry-carriers.md +++ b/.changeset/19778-preset-entry-carriers.md @@ -8,10 +8,9 @@ Clause-②: no No behaviour moves. No schema, accept set or lint rule is touched, and no export is added, removed or retyped. Every line this change edits in `registry.ts` is a string literal inside that one step-18 entry, which the exported `MIGRATIONS_BY_MAJOR` carries, so what moves in `dist` is prose. -- **The three groups the entry now draws.** They list the carriers measured, not a closed partition; the entry's grep sentence is the catch-all. Each was measured against the built `dist` with a preset comparand (`last_30_days`, and `today` as a `between` endpoint), and an ISO-date dark control reads green in every cell. +- **The groups the entry now draws.** They list the carriers measured, not a closed partition; the entry's grep sentence is the catch-all. Each was measured against the built `dist` with a preset comparand (`last_30_days`, and `today` as a `between` endpoint), and an ISO-date dark control reads green in every cell. 1. Slots typed `FilterConditionSchema`: `DashboardWidgetSchema.filter`, `GlobalFilterOptionsFromSchema.filter`, `DatasetSchema.filter`, `DatasetMeasureSchema.filter`, `ReportSchema.runtimeFilter`, `JoinedReportBlockSchema.runtimeFilter`, `FieldSchema.relatedListFilter` and `FieldSchema.summaryOperations.filter`. A parse of the declaring schema refuses each one at the comparand's own path, and the lint rule reports each one as well. 2. Filters under a key the lint walks whose declared type carries no preset check. These are `ViewFilterRuleSchema` rule arrays (a view's `filter`, a page element's `dataSource.filter`, a page component's `filter` prop) and a Mongo-shape record typed as a loose record rather than `FilterConditionSchema` (a flow `get_record` / `update_record` / `delete_record` node's `config.filter`). These parse green, and the lint rule alone refuses them. - 3. A page's `interfaceConfig.filterBy` and a lookup field's `lookupFilters` parse green and also lint green, because the lint's filter walk descends neither key. Neither door refuses them at publish, so the entry now tells the upgrader to sweep both by hand. Two controls back the `filterBy` reading. A malformed `filterBy` value is refused at `interfaceConfig.filterBy.0.value`, so the slot is parsed. The same rule under `interfaceConfig.filter` is refused by the lint, so the key name is what decides. `lookupFilters` takes `gt`, `gte`, `lt` and `lte` as its only ordering spellings (`greater_than`, `after`, `between`, `>` and `$gt` are refused at `lookupFilters.0.operator`), and the by-hand clause names those four. - **Two more false sentences are narrowed.** - The `replacement` called the dashboard date-filter positions "the only place any layer ever resolved" a preset name. An analytics query's `timeDimensions[].dateRange` accepts and resolves the names too. - The `reason` said equality and membership "are NOT judged". That holds for the schema door only. The lint rule refuses a preset in an equality or membership position on a field it can resolve to a declared `date` or `datetime`, while `this_quarter` on a `select` field stays green. Where the filter binds to no object, such as a widget whose `dataset` names no dataset, that arm does not fire. diff --git a/.changeset/19791-preset-entry-filterby-lookupfilters.md b/.changeset/19791-preset-entry-filterby-lookupfilters.md index 71bf89e26eb..00e24952074 100644 --- a/.changeset/19791-preset-entry-filterby-lookupfilters.md +++ b/.changeset/19791-preset-entry-filterby-lookupfilters.md @@ -2,6 +2,6 @@ '@objectstack/spec': patch --- -The shipped ADR-0087 semantic entry `filter-preset-ordering-comparand-refused` drops, from its `surface`, the group that said a page's `interfaceConfig.filterBy` and a lookup field's `lookupFilters` are refused by neither door at publish, and drops, from its `acceptanceCriteria`, the by-hand search it prescribed for those two keys. The `@objectstack/lint` `filter-preset-comparand` rule now walks both keys, so `os lint`, `os validate` and the runtime publish gate (on `page` and `object` writes) refuse `{ field: 'close_date', operator: 'gt', value: 'last_30_days' }` in either one, while the schema parse still accepts it. +The shipped ADR-0087 semantic entry `filter-preset-ordering-comparand-refused` drops, from its `surface` and its `reason`, the text that said a page's `interfaceConfig.filterBy` and a lookup field's `lookupFilters` are refused by neither door at publish, and drops, from its `acceptanceCriteria`, the by-hand search it prescribed for those two keys. The `@objectstack/lint` `filter-preset-comparand` rule now walks both keys, so `os lint`, `os validate` and the runtime publish gate (on `page` and `object` writes) refuse `{ field: 'close_date', operator: 'gt', value: 'last_30_days' }` in either one, while the schema parse still accepts it. Clause-②: no From 1346a996f69d63f572007f7ab100f46c31026171 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 13:32:41 +0000 Subject: [PATCH 08/11] docs(lint): cut the exclusive and unchanged claims about the walked rule-array carriers filterBy and lookupFilters are not the only consumed rule-array carriers outside the walk (object-grid defaultFilters is another), so the article that said so goes from the lint changeset, the FILTER_KEYS docblock and the test header; relatedListFilter is no longer the one member off the filter spelling either. The empty-combinator and flow-token rules did change for values the lint receives (filterBy: {} and a flow node config.lookupFilters), so the clauses and the changeset bullet calling them unchanged are cut. Comments and changeset prose only; no code moves. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude --- .changeset/19791-filter-walk-rule-array-carriers.md | 3 +-- packages/lint/src/filter-walk.ts | 12 ++++-------- packages/lint/src/validate-preset-comparands.test.ts | 2 +- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/.changeset/19791-filter-walk-rule-array-carriers.md b/.changeset/19791-filter-walk-rule-array-carriers.md index 0299fab7ae5..7a410167723 100644 --- a/.changeset/19791-filter-walk-rule-array-carriers.md +++ b/.changeset/19791-filter-walk-rule-array-carriers.md @@ -2,11 +2,10 @@ "@objectstack/lint": patch --- -`filter-preset-comparand` now judges a list page's `interfaceConfig.filterBy` and a lookup field's `lookupFilters`, the two consumed filter carriers the shared filter walk never entered (#19791). +`filter-preset-comparand` now judges a list page's `interfaceConfig.filterBy` and a lookup field's `lookupFilters`, consumed filter carriers the shared filter walk never entered (#19791). Both carriers are rule arrays (`{ field, operator, value }`) whose values reach the engine's `where` verbatim, and neither schema carries a preset check. So `{ field: 'close_date', operator: 'gt', value: 'last_30_days' }` in either one parsed green and linted green, then the engine refused it at query time (`INVALID_FILTER` / 400). The same rule on a component `dataSource.filter` or a view `filter` was already refused. `filterBy` and `lookupFilters` join `FILTER_KEYS`, so `os lint`, `os validate` and the runtime publish gate (for `page` and `object` writes) now refuse it where it is written. Each finding carries its path (`pages[0].interfaceConfig.filterBy[0].value`, `objects[2].fields.account.lookupFilters[0].value`). - **Which object a condition addresses.** The field-typed arm, which refuses a preset under equality or membership on a `date` / `datetime` field, binds `filterBy` to `interfaceConfig.source`. Without a `source` it falls back to the page's `object`. It binds `lookupFilters` to the field's `reference` and never to the object that owns the field, because the picker queries the referenced object. A `relatedListFilter` on the same field still binds to the owner. - **`filter-token-unknown` reaches the same two carriers.** An unresolvable placeholder such as `{current_user}` in `filterBy` or `lookupFilters` is now reported, as it already is in a view's `filter`. `{current_user_id}` and the date macros stay clean. -- **Unchanged:** `filter-empty-combinator` / `filter-empty-node` judge Mongo-shape nodes only, and both carriers are arrays. `flow-filter-token-unknown` walks `flows`, and no flow schema declares either key. - **What you do:** in a `filterBy` or `lookupFilters` rule, replace a preset name with the `{date-macro}` window the message names (`{ operator: 'gte', value: '{30_days_ago}' }`) or with an ISO date. diff --git a/packages/lint/src/filter-walk.ts b/packages/lint/src/filter-walk.ts index a8a6575bfbd..19d8d48bd15 100644 --- a/packages/lint/src/filter-walk.ts +++ b/packages/lint/src/filter-walk.ts @@ -57,16 +57,15 @@ type AnyRec = Record; /** * Keys whose subtree is a filter. The one place a filter is authored. * - * `relatedListFilter` (#8704) is the one member that does not spell the key - * `filter`: it sits flat on a FIELD beside its `relatedList`/`relatedListTitle`/ + * `relatedListFilter` (#8704) sits flat on a FIELD beside its `relatedList`/`relatedListTitle`/ * `relatedListColumns` family, so the family naming wins over the filter-key * convention. It carries a canonical Query-DSL `FilterCondition` (the schema * door already judges it at parse), and listing it here is what extends the * three walking rules — tokens, empty combinators, preset comparands — to the * new position instead of leaving a per-rule hole. * - * `filterBy` and `lookupFilters` (#19791) are the two consumed RULE-ARRAY - * carriers that do not spell the key `filter` either: a list page's always-on + * `filterBy` and `lookupFilters` (#19791) are consumed RULE-ARRAY + * carriers that do not spell the key `filter`: a list page's always-on * base filter (`interfaceConfig.filterBy`, `z.array(ViewFilterRuleSchema)`, * which the console spreads into the list query beside the view's own * `filter`) and a lookup field's picker filter (`lookupFilters`, lowered by the @@ -76,10 +75,7 @@ type AnyRec = Record; * and was refused only at query time. What the entry changes per walking rule, * measured: preset comparands and filter tokens now judge both carriers (the * engine resolves the same two placeholder vocabularies there and refuses the - * same residue); empty combinators is unchanged, because both carriers are - * arrays and that rule judges Mongo-shape nodes only; the flow token rule is - * unchanged, because it walks `flows` alone and no flow schema declares - * either key. The binding half — which object a condition on each carrier + * same residue). The binding half — which object a condition on each carrier * addresses — is `validate-preset-comparands.ts`'s, not this walk's. */ export const FILTER_KEYS: ReadonlySet = new Set([ diff --git a/packages/lint/src/validate-preset-comparands.test.ts b/packages/lint/src/validate-preset-comparands.test.ts index 31355ebc6aa..b65fd90133a 100644 --- a/packages/lint/src/validate-preset-comparands.test.ts +++ b/packages/lint/src/validate-preset-comparands.test.ts @@ -673,7 +673,7 @@ describe('validatePresetComparands — arm 2, the FIELD-TYPED equality / members }); }); -// ── [#19791] The two consumed rule-array carriers the walk never entered ──── +// ── [#19791] consumed rule-array carriers the walk never entered ──── // // A list page's `interfaceConfig.filterBy` and a lookup field's // `lookupFilters` both reach the engine's `where` verbatim, and neither schema From 12f2190e4d51f563d79da36dee6cd330a5df07ee Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 13:32:41 +0000 Subject: [PATCH 09/11] chore(changeset): cut the nine-sentences reach count from the pending preset note This branch deletes sentences that count was taken over, so the count no longer holds for the release that ships the note. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude --- .changeset/19778-preset-entry-carriers.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/19778-preset-entry-carriers.md b/.changeset/19778-preset-entry-carriers.md index 89af9888f58..8f4a207dddb 100644 --- a/.changeset/19778-preset-entry-carriers.md +++ b/.changeset/19778-preset-entry-carriers.md @@ -16,5 +16,4 @@ No behaviour moves. No schema, accept set or lint rule is touched, and no export - The `reason` said equality and membership "are NOT judged". That holds for the schema door only. The lint rule refuses a preset in an equality or membership position on a field it can resolve to a declared `date` or `datetime`, while `this_quarter` on a `select` field stays green. Where the filter binds to no object, such as a widget whose `dataset` names no dataset, that arm does not fire. - **Reach.** Counted over `dist/index.js`, `dist/index.mjs`, `dist/browser/index.js` and `dist/browser/index.mjs`: - The removed carrier list `page filter, component filter, rollup filter` and each of the four other removed claims read 4 before and 0 after. - - Each of nine sentences unique to the corrected text reads 0 before and 4 after. - The unchanged dark control `compared false against every row: HTTP 200` reads 4 on both sides. From bf2d6dcd5ef9eacb3c99ecbc77c85f51b148e04f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 13:33:36 +0000 Subject: [PATCH 10/11] docs(lint): cut the per-walking-rule sentence whose list is now partial With the two unchanged clauses gone, the sentence framed as what the entry changes per walking rule named two of the four rules and implied the rest held still, which the empty-node and flow-token measurements refute. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude --- packages/lint/src/filter-walk.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/lint/src/filter-walk.ts b/packages/lint/src/filter-walk.ts index 19d8d48bd15..d169e126fa9 100644 --- a/packages/lint/src/filter-walk.ts +++ b/packages/lint/src/filter-walk.ts @@ -72,10 +72,7 @@ type AnyRec = Record; * console to a Mongo `$filter` on the REFERENCED object). Both values reach * the engine's `where` verbatim. Their schemas carry no preset check, so * before this entry an ordering preset in either parsed green, linted green, - * and was refused only at query time. What the entry changes per walking rule, - * measured: preset comparands and filter tokens now judge both carriers (the - * engine resolves the same two placeholder vocabularies there and refuses the - * same residue). The binding half — which object a condition on each carrier + * and was refused only at query time. The binding half — which object a condition on each carrier * addresses — is `validate-preset-comparands.ts`'s, not this walk's. */ export const FILTER_KEYS: ReadonlySet = new Set([ From 4dda1fd0fd8bda0888711cabaa737711350cce08 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 14:34:17 +0000 Subject: [PATCH 11/11] docs(lint): cut three false runtime and control claims from the walk comments The preset test header claimed its dataSource.filter lit control in every block below; only the first block carries one. The token test comment called an unknown token a silent literal; the engine's token resolver throws FILTER_TOKEN_UNKNOWN on it. The FILTER_KEYS docblock said an ordering preset in either carrier was refused at query time; the engine's temporal door refuses only on a date, datetime or time field. Each clause is cut; comments only, no code moves. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude --- packages/lint/src/filter-walk.ts | 4 ++-- packages/lint/src/validate-filter-tokens.test.ts | 3 +-- packages/lint/src/validate-preset-comparands.test.ts | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/lint/src/filter-walk.ts b/packages/lint/src/filter-walk.ts index d169e126fa9..9bdbdcbfa61 100644 --- a/packages/lint/src/filter-walk.ts +++ b/packages/lint/src/filter-walk.ts @@ -71,8 +71,8 @@ type AnyRec = Record; * `filter`) and a lookup field's picker filter (`lookupFilters`, lowered by the * console to a Mongo `$filter` on the REFERENCED object). Both values reach * the engine's `where` verbatim. Their schemas carry no preset check, so - * before this entry an ordering preset in either parsed green, linted green, - * and was refused only at query time. The binding half — which object a condition on each carrier + * before this entry an ordering preset in either parsed green, linted green. + * The binding half — which object a condition on each carrier * addresses — is `validate-preset-comparands.ts`'s, not this walk's. */ export const FILTER_KEYS: ReadonlySet = new Set([ diff --git a/packages/lint/src/validate-filter-tokens.test.ts b/packages/lint/src/validate-filter-tokens.test.ts index 01d23e4610a..b5ebe29b8da 100644 --- a/packages/lint/src/validate-filter-tokens.test.ts +++ b/packages/lint/src/validate-filter-tokens.test.ts @@ -182,8 +182,7 @@ describe('validateFilterTokens', () => { // [#19791] A list page's `interfaceConfig.filterBy` and a lookup field's // `lookupFilters` reach the engine's `where` verbatim, where the same two - // placeholder vocabularies resolve — so an unknown token there is the same - // silent literal it is in a view's `filter`, and the known ones are fine. + // placeholder vocabularies resolve. it('reaches a page filterBy and a lookup field lookupFilters', () => { const findings = validateFilterTokens({ objects: [{ diff --git a/packages/lint/src/validate-preset-comparands.test.ts b/packages/lint/src/validate-preset-comparands.test.ts index b65fd90133a..2eb709a7cd5 100644 --- a/packages/lint/src/validate-preset-comparands.test.ts +++ b/packages/lint/src/validate-preset-comparands.test.ts @@ -680,7 +680,7 @@ describe('validatePresetComparands — arm 2, the FIELD-TYPED equality / members // carries a preset check. Before `FILTER_KEYS` named them, the card's rule // `{ field: 'close_date', operator: 'gt', value: 'last_30_days' }` parsed // green AND linted green on both, while the identical rule on a component -// `dataSource.filter` was refused — the lit control every block below keeps. +// `dataSource.filter` was refused. describe('validatePresetComparands — page filterBy and lookup-field lookupFilters (#19791)', () => { const card = { field: 'close_date', operator: 'gt', value: 'last_30_days' }; // `close_date` is a DATE on `crm_deal` and a SELECT on `crm_region`, whose