From 76dd36f0423a45a7a763b593f19eb11c625c5f7d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 05:47:33 +0000 Subject: [PATCH 1/6] fix(spec): the preset-comparand entry puts each carrier under the door that refuses it The step-18 semantic entry filter-preset-ordering-comparand-refused listed page and component filters among the FilterConditionSchema carriers the schema door refuses. They are ViewFilterRuleSchema rule arrays: the lint is the only door there, and a page's interfaceConfig.filterBy is walked by neither. surface, reason and acceptanceCriteria now draw the three-way split; the replacement's "only place any layer ever resolved them" and the reason's "equality and membership are NOT judged" are narrowed to what is true today. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- ...ilter-preset-ordering-comparand-refused.ts | 58 ++++++++++++++----- 1 file changed, 42 insertions(+), 16 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 1c9cc4086fd..6ad74137ad0 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 @@ -10,18 +10,31 @@ export const entry: SemanticMigration = { 'a dashboard date-range preset name (last_7_days / last_30_days / last_90_days, today, ' + 'yesterday, this_week, last_week, this_month, last_month, this_quarter, last_quarter, ' + 'this_year, last_year) authored as a bare ORDERING comparand in a filter — a ' - + '$gt / $gte / $lt / $lte value or a $between endpoint on any carrier of ' - + 'FilterConditionSchema (dashboard widget filter, dataset filter, report runtimeFilter, ' - + 'page filter, component filter, rollup filter), a greater_than / less_than / before / ' - + 'after / between view filter rule value, or an ordering [field, op, value] filter triple', + + '$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, in three groups. (1) A slot typed FilterConditionSchema — a ' + + 'dashboard widget filter, a dashboard global-filter options-source filter ' + + '(optionsFrom.filter), a dataset filter, a dataset measure filter, a report runtimeFilter ' + + '(on the report or on a joined-report block), a rollup summaryOperations.filter and a ' + + 'relatedListFilter — is refused at PARSE, at the comparand\'s own path, and the ' + + '@objectstack/lint filter-preset-comparand rule reports it as well. (2) A ' + + 'ViewFilterRuleSchema rule array under a key the lint walks — a view\'s filter, a page ' + + 'element\'s dataSource.filter, a page component\'s filter prop — parses GREEN, because the ' + + 'rule schema carries no preset check: the lint rule is the only door that refuses it, and ' + + 'the lint is likewise what refuses a preset in an ordering filter triple wherever its walk ' + + 'meets one. (3) A page\'s interfaceConfig.filterBy is a rule array under a key the lint ' + + 'does NOT walk: it parses GREEN and lints GREEN, so neither door refuses it at publish and ' + + 'only a search of the authored and stored pages finds it', 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 ' + '(the rejection names the exact window per preset; DATE_RANGE_PRESET_MACRO_WINDOWS in ' + '@objectstack/spec/data is the table) — or an ISO date such as 2026-01-15. The preset ' - + 'names themselves stay fully legal in the dashboard date-filter positions ' - + '(dateRange.defaultRange, a date global filter defaultValue), which is the only place ' - + 'any layer ever resolved them', + + 'names themselves stay fully legal where a layer resolves them to a window: the dashboard ' + + 'date-filter positions (dateRange.defaultRange, a date global filter defaultValue) and an ' + + 'analytics query\'s timeDimensions[].dateRange. A filter comparand is not one of those ' + + 'positions', reason: 'The C half of #8690, maintainer-ruled 2026-08-15 alongside the engine door (PR #8808). ' + 'The preset vocabulary is declared in the dashboard schema and lowered to {date-macro} ' @@ -32,13 +45,19 @@ export const entry: SemanticMigration = { + 'count 0, indistinguishable from "there is no data" (measured on #8690: $gte ' + '"last_30_days" returned 0 of 51 seeded rows where the macro spelling returned the 38 ' + 'in-window). The engine now refuses the bare name on a declared temporal field at query ' - + 'time (INVALID_FILTER / 400); this entry records the AUTHORING-time half: the schema ' - + 'door and the @objectstack/lint filter-preset-comparand rule refuse it at publish, ' - + 'where the author — an AI author in particular — can still act on the message. ' - + 'Ordering positions only, deliberately: equality and membership are NOT judged, because ' - + 'a select/picklist column legitimately stores values that collide with preset names, ' - + 'and on a temporal field the engine door already refuses those with the field type in ' - + 'hand. ⚠️ Metadata AT REST is deliberately not rewritten and there is no D2 conversion: ' + + 'time (INVALID_FILTER / 400); this entry records the AUTHORING-time half, and that half ' + + 'is two doors with different reach, not one: the FilterConditionSchema parse refuses the ' + + '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 ' + + 'ViewFilterRuleSchema rule array. Both answer at publish, where the author — an AI author ' + + 'in particular — can still act on the message; a page\'s interfaceConfig.filterBy is ' + + 'reached by neither, and the surface\'s three groups say which 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 names and a schema has no field type in hand. The lint rule, which reads the ' + + 'stack\'s object metadata, additionally refuses a preset in an equality or membership ' + + 'position on a declared date or datetime field, and on a temporal field the engine door ' + + 'already refuses those with the field type in hand. ⚠️ Metadata AT REST is deliberately not rewritten and there is no D2 conversion: ' + 'this shape was never written by any first-party producer (every preset in this repo ' + 'and the example apps sits in a dashboard date-filter position — measured) and never ' + 'executed usefully (it returned a silent zero before #8808 and a 400 after). Coercing ' @@ -50,8 +69,15 @@ export const entry: SemanticMigration = { 'Grep your authored filters for the thirteen preset names in ordering positions — a ' + '$gt/$gte/$lt/$lte value, a $between endpoint, a greater_than/less_than/before/after/' + 'between view rule value, an ordering filter triple — and rewrite each to the ' - + '{date-macro} window the rejection names (or an ISO date). `os validate` / `os lint` ' - + 'report each one by path, so the sweep is mechanical. Leave presets in dashboard ' + + '{date-macro} window the rejection names (or an ISO date). The sweep is mechanical for ' + + 'the surface\'s 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, and take its window from `DATE_RANGE_PRESET_MACRO_WINDOWS`, since no ' + + 'rejection names it. 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 850a1c85c4cff6f63a8780a72ef4450b395b8157 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 05:47:47 +0000 Subject: [PATCH 2/6] chore(spec): regenerate the migration registry for the corrected entry Output of `pnpm --filter @objectstack/spec gen:migration-registry`, never hand-edited; every changed line is a string literal inside step 18's filter-preset-ordering-comparand-refused entry. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- packages/spec/src/migrations/registry.ts | 58 +++++++++++++++++------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 6e17292ff0e..983eb5114de 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -8620,18 +8620,31 @@ const step18: MigrationStep = { 'a dashboard date-range preset name (last_7_days / last_30_days / last_90_days, today, ' + 'yesterday, this_week, last_week, this_month, last_month, this_quarter, last_quarter, ' + 'this_year, last_year) authored as a bare ORDERING comparand in a filter — a ' - + '$gt / $gte / $lt / $lte value or a $between endpoint on any carrier of ' - + 'FilterConditionSchema (dashboard widget filter, dataset filter, report runtimeFilter, ' - + 'page filter, component filter, rollup filter), a greater_than / less_than / before / ' - + 'after / between view filter rule value, or an ordering [field, op, value] filter triple', + + '$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, in three groups. (1) A slot typed FilterConditionSchema — a ' + + 'dashboard widget filter, a dashboard global-filter options-source filter ' + + '(optionsFrom.filter), a dataset filter, a dataset measure filter, a report runtimeFilter ' + + '(on the report or on a joined-report block), a rollup summaryOperations.filter and a ' + + 'relatedListFilter — is refused at PARSE, at the comparand\'s own path, and the ' + + '@objectstack/lint filter-preset-comparand rule reports it as well. (2) A ' + + 'ViewFilterRuleSchema rule array under a key the lint walks — a view\'s filter, a page ' + + 'element\'s dataSource.filter, a page component\'s filter prop — parses GREEN, because the ' + + 'rule schema carries no preset check: the lint rule is the only door that refuses it, and ' + + 'the lint is likewise what refuses a preset in an ordering filter triple wherever its walk ' + + 'meets one. (3) A page\'s interfaceConfig.filterBy is a rule array under a key the lint ' + + 'does NOT walk: it parses GREEN and lints GREEN, so neither door refuses it at publish and ' + + 'only a search of the authored and stored pages finds it', 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 ' + '(the rejection names the exact window per preset; DATE_RANGE_PRESET_MACRO_WINDOWS in ' + '@objectstack/spec/data is the table) — or an ISO date such as 2026-01-15. The preset ' - + 'names themselves stay fully legal in the dashboard date-filter positions ' - + '(dateRange.defaultRange, a date global filter defaultValue), which is the only place ' - + 'any layer ever resolved them', + + 'names themselves stay fully legal where a layer resolves them to a window: the dashboard ' + + 'date-filter positions (dateRange.defaultRange, a date global filter defaultValue) and an ' + + 'analytics query\'s timeDimensions[].dateRange. A filter comparand is not one of those ' + + 'positions', reason: 'The C half of #8690, maintainer-ruled 2026-08-15 alongside the engine door (PR #8808). ' + 'The preset vocabulary is declared in the dashboard schema and lowered to {date-macro} ' @@ -8642,13 +8655,19 @@ const step18: MigrationStep = { + 'count 0, indistinguishable from "there is no data" (measured on #8690: $gte ' + '"last_30_days" returned 0 of 51 seeded rows where the macro spelling returned the 38 ' + 'in-window). The engine now refuses the bare name on a declared temporal field at query ' - + 'time (INVALID_FILTER / 400); this entry records the AUTHORING-time half: the schema ' - + 'door and the @objectstack/lint filter-preset-comparand rule refuse it at publish, ' - + 'where the author — an AI author in particular — can still act on the message. ' - + 'Ordering positions only, deliberately: equality and membership are NOT judged, because ' - + 'a select/picklist column legitimately stores values that collide with preset names, ' - + 'and on a temporal field the engine door already refuses those with the field type in ' - + 'hand. ⚠️ Metadata AT REST is deliberately not rewritten and there is no D2 conversion: ' + + 'time (INVALID_FILTER / 400); this entry records the AUTHORING-time half, and that half ' + + 'is two doors with different reach, not one: the FilterConditionSchema parse refuses the ' + + '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 ' + + 'ViewFilterRuleSchema rule array. Both answer at publish, where the author — an AI author ' + + 'in particular — can still act on the message; a page\'s interfaceConfig.filterBy is ' + + 'reached by neither, and the surface\'s three groups say which 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 names and a schema has no field type in hand. The lint rule, which reads the ' + + 'stack\'s object metadata, additionally refuses a preset in an equality or membership ' + + 'position on a declared date or datetime field, and on a temporal field the engine door ' + + 'already refuses those with the field type in hand. ⚠️ Metadata AT REST is deliberately not rewritten and there is no D2 conversion: ' + 'this shape was never written by any first-party producer (every preset in this repo ' + 'and the example apps sits in a dashboard date-filter position — measured) and never ' + 'executed usefully (it returned a silent zero before #8808 and a 400 after). Coercing ' @@ -8660,8 +8679,15 @@ const step18: MigrationStep = { 'Grep your authored filters for the thirteen preset names in ordering positions — a ' + '$gt/$gte/$lt/$lte value, a $between endpoint, a greater_than/less_than/before/after/' + 'between view rule value, an ordering filter triple — and rewrite each to the ' - + '{date-macro} window the rejection names (or an ISO date). `os validate` / `os lint` ' - + 'report each one by path, so the sweep is mechanical. Leave presets in dashboard ' + + '{date-macro} window the rejection names (or an ISO date). The sweep is mechanical for ' + + 'the surface\'s 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, and take its window from `DATE_RANGE_PRESET_MACRO_WINDOWS`, since no ' + + 'rejection names it. 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 775de57ad957923ced730dfefb764ef7fd7b1be6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 05:52:19 +0000 Subject: [PATCH 3/6] chore(changeset): patch for the corrected preset-comparand entry Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- .changeset/19778-preset-entry-carriers.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .changeset/19778-preset-entry-carriers.md diff --git a/.changeset/19778-preset-entry-carriers.md b/.changeset/19778-preset-entry-carriers.md new file mode 100644 index 00000000000..f51d93f19d7 --- /dev/null +++ b/.changeset/19778-preset-entry-carriers.md @@ -0,0 +1,21 @@ +--- +'@objectstack/spec': patch +--- + +`packages/spec/src/migrations/registry.ts`: the shipped ADR-0087 semantic entry `filter-preset-ordering-comparand-refused` listed a page filter and a component filter among the `FilterConditionSchema` carriers, and said the schema door and the `@objectstack/lint` `filter-preset-comparand` rule refuse a bare date-range preset there at publish. Both are `ViewFilterRuleSchema` rule arrays, and a rule array carries no preset check, so an upgrader who swept stored pages with a schema parse found nothing and concluded the sweep was clean. The entry's `surface`, `reason` and `acceptanceCriteria` now put each carrier under the door that actually refuses it. + +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.** 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. `ViewFilterRuleSchema` rule arrays under a key the lint walks: a view's `filter`, a page element's `dataSource.filter` and a page component's `filter` prop. These parse green, and the lint rule alone refuses them. + 3. A page's `interfaceConfig.filterBy` parses green and also lints green, because the lint's filter walk does not descend that key. Neither door refuses it at publish, so the entry now tells the upgrader to sweep it by hand. Two controls back this 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. +- **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 declared `date` or `datetime` field, while `this_quarter` on a `select` field stays green. +- **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 five 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 9414b8d2bf512b66dfb19b1c0f399ee9a065bd99 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 06:46:44 +0000 Subject: [PATCH 4/6] fix(spec): the preset-comparand entry's groups name every measured carrier A lookup field's lookupFilters is walked by neither door, like a page's interfaceConfig.filterBy, and a flow CRUD node's loose-record config.filter is refused by the lint alone. Both now sit in the surface's groups, the by-hand clause covers the gt / gte / lt / lte spelling lookupFilters takes, the groups are stated as the carriers measured rather than a closed partition, and the field-typed arm names the binding it needs. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- ...ilter-preset-ordering-comparand-refused.ts | 59 +++++++++++-------- 1 file changed, 35 insertions(+), 24 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 6ad74137ad0..114ae5029b4 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,19 +13,24 @@ 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, in three groups. (1) A slot typed FilterConditionSchema — a ' + + 'type and by its key. The carriers measured fall in three 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 ' + '(optionsFrom.filter), a dataset filter, a dataset measure filter, a report runtimeFilter ' + '(on the report or on a joined-report block), a rollup summaryOperations.filter and a ' + 'relatedListFilter — is refused at PARSE, at the comparand\'s own path, and the ' - + '@objectstack/lint filter-preset-comparand rule reports it as well. (2) A ' - + 'ViewFilterRuleSchema rule array under a key the lint walks — a view\'s filter, a page ' - + 'element\'s dataSource.filter, a page component\'s filter prop — parses GREEN, because the ' - + 'rule schema carries no preset check: the lint rule is the only door that refuses it, and ' - + 'the lint is likewise what refuses a preset in an ordering filter triple wherever its walk ' - + 'meets one. (3) A page\'s interfaceConfig.filterBy is a rule array under a key the lint ' - + 'does NOT walk: it parses GREEN and lints GREEN, so neither door refuses it at publish and ' - + 'only a search of the authored and stored pages finds it', + + '@objectstack/lint filter-preset-comparand rule reports it as well. (2) A filter under a ' + + 'key the lint walks, whose declared type carries no preset check, parses GREEN, and the ' + + 'lint rule is the only door that refuses it: a ViewFilterRuleSchema rule array (a view\'s ' + + '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', 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 ' @@ -49,15 +54,18 @@ export const entry: SemanticMigration = { + 'is two doors with different reach, not one: the FilterConditionSchema parse refuses the ' + '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 ' - + 'ViewFilterRuleSchema rule array. Both answer at publish, where the author — an AI author ' - + 'in particular — can still act on the message; a page\'s interfaceConfig.filterBy is ' - + 'reached by neither, and the surface\'s three groups say which 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 names and a schema has no field type in hand. The lint rule, which reads the ' - + 'stack\'s object metadata, additionally refuses a preset in an equality or membership ' - + 'position on a declared date or datetime field, and on a temporal field the engine door ' - + 'already refuses those with the field type in hand. ⚠️ Metadata AT REST is deliberately not rewritten and there is no D2 conversion: ' + + '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 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 ' + + 'names and a schema has no field type in hand. The lint rule, which reads the stack\'s ' + + 'object metadata, additionally refuses a preset in an equality or membership position, ' + + 'in a filter its walk reaches, on a field it can resolve to a declared date or datetime ' + + '(where the filter binds to no object, or the field resolves to nothing, that arm cannot ' + + 'fire), and on a temporal field the engine door already refuses those with the field ' + + 'type in hand. ⚠️ Metadata AT REST is deliberately not rewritten and there is no D2 conversion: ' + 'this shape was never written by any first-party producer (every preset in this repo ' + 'and the example apps sits in a dashboard date-filter position — measured) and never ' + 'executed usefully (it returned a silent zero before #8808 and a 400 after). Coercing ' @@ -68,16 +76,19 @@ export const entry: SemanticMigration = { acceptanceCriteria: 'Grep your authored filters for the thirteen preset names in ordering positions — a ' + '$gt/$gte/$lt/$lte value, a $between endpoint, a greater_than/less_than/before/after/' - + 'between view rule value, an ordering filter triple — and rewrite each to the ' - + '{date-macro} window the rejection names (or an ISO date). The sweep is mechanical for ' - + 'the surface\'s groups (1) and (2): `os validate` / `os lint` report each one by path, and ' + + 'between view rule value, an ordering filter triple, a gt/gte/lt/lte lookup filter value ' + + '— and rewrite each to the {date-macro} window the rejection names (or an ISO date). That ' + + '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 ' + + '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, and take its window from `DATE_RANGE_PRESET_MACRO_WINDOWS`, since no ' - + 'rejection names it. Leave presets in dashboard ' + + '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 ' + '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 bff19cc8b98b7358f477eae83fd9d56becbce3df Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 06:46:47 +0000 Subject: [PATCH 5/6] chore(spec): regenerate the migration registry for the widened carrier groups Output of `pnpm --filter @objectstack/spec gen:migration-registry`, never hand-edited; every changed line is a string literal inside step 18's filter-preset-ordering-comparand-refused entry. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- packages/spec/src/migrations/registry.ts | 59 ++++++++++++++---------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 983eb5114de..78625bde8aa 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -8623,19 +8623,24 @@ 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, in three groups. (1) A slot typed FilterConditionSchema — a ' + + 'type and by its key. The carriers measured fall in three 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 ' + '(optionsFrom.filter), a dataset filter, a dataset measure filter, a report runtimeFilter ' + '(on the report or on a joined-report block), a rollup summaryOperations.filter and a ' + 'relatedListFilter — is refused at PARSE, at the comparand\'s own path, and the ' - + '@objectstack/lint filter-preset-comparand rule reports it as well. (2) A ' - + 'ViewFilterRuleSchema rule array under a key the lint walks — a view\'s filter, a page ' - + 'element\'s dataSource.filter, a page component\'s filter prop — parses GREEN, because the ' - + 'rule schema carries no preset check: the lint rule is the only door that refuses it, and ' - + 'the lint is likewise what refuses a preset in an ordering filter triple wherever its walk ' - + 'meets one. (3) A page\'s interfaceConfig.filterBy is a rule array under a key the lint ' - + 'does NOT walk: it parses GREEN and lints GREEN, so neither door refuses it at publish and ' - + 'only a search of the authored and stored pages finds it', + + '@objectstack/lint filter-preset-comparand rule reports it as well. (2) A filter under a ' + + 'key the lint walks, whose declared type carries no preset check, parses GREEN, and the ' + + 'lint rule is the only door that refuses it: a ViewFilterRuleSchema rule array (a view\'s ' + + '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', 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 ' @@ -8659,15 +8664,18 @@ const step18: MigrationStep = { + 'is two doors with different reach, not one: the FilterConditionSchema parse refuses the ' + '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 ' - + 'ViewFilterRuleSchema rule array. Both answer at publish, where the author — an AI author ' - + 'in particular — can still act on the message; a page\'s interfaceConfig.filterBy is ' - + 'reached by neither, and the surface\'s three groups say which 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 names and a schema has no field type in hand. The lint rule, which reads the ' - + 'stack\'s object metadata, additionally refuses a preset in an equality or membership ' - + 'position on a declared date or datetime field, and on a temporal field the engine door ' - + 'already refuses those with the field type in hand. ⚠️ Metadata AT REST is deliberately not rewritten and there is no D2 conversion: ' + + '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 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 ' + + 'names and a schema has no field type in hand. The lint rule, which reads the stack\'s ' + + 'object metadata, additionally refuses a preset in an equality or membership position, ' + + 'in a filter its walk reaches, on a field it can resolve to a declared date or datetime ' + + '(where the filter binds to no object, or the field resolves to nothing, that arm cannot ' + + 'fire), and on a temporal field the engine door already refuses those with the field ' + + 'type in hand. ⚠️ Metadata AT REST is deliberately not rewritten and there is no D2 conversion: ' + 'this shape was never written by any first-party producer (every preset in this repo ' + 'and the example apps sits in a dashboard date-filter position — measured) and never ' + 'executed usefully (it returned a silent zero before #8808 and a 400 after). Coercing ' @@ -8678,16 +8686,19 @@ const step18: MigrationStep = { acceptanceCriteria: 'Grep your authored filters for the thirteen preset names in ordering positions — a ' + '$gt/$gte/$lt/$lte value, a $between endpoint, a greater_than/less_than/before/after/' - + 'between view rule value, an ordering filter triple — and rewrite each to the ' - + '{date-macro} window the rejection names (or an ISO date). The sweep is mechanical for ' - + 'the surface\'s groups (1) and (2): `os validate` / `os lint` report each one by path, and ' + + 'between view rule value, an ordering filter triple, a gt/gte/lt/lte lookup filter value ' + + '— and rewrite each to the {date-macro} window the rejection names (or an ISO date). That ' + + '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 ' + + '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, and take its window from `DATE_RANGE_PRESET_MACRO_WINDOWS`, since no ' - + 'rejection names it. Leave presets in dashboard ' + + '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 ' + '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 a82423396b1c204e2d8f6a1ae76377d9d20ea736 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 06:50:21 +0000 Subject: [PATCH 6/6] chore(changeset): name the carriers the widened groups added Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- .changeset/19778-preset-entry-carriers.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.changeset/19778-preset-entry-carriers.md b/.changeset/19778-preset-entry-carriers.md index f51d93f19d7..7e6b178d473 100644 --- a/.changeset/19778-preset-entry-carriers.md +++ b/.changeset/19778-preset-entry-carriers.md @@ -8,14 +8,14 @@ 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.** 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 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. 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. `ViewFilterRuleSchema` rule arrays under a key the lint walks: a view's `filter`, a page element's `dataSource.filter` and a page component's `filter` prop. These parse green, and the lint rule alone refuses them. - 3. A page's `interfaceConfig.filterBy` parses green and also lints green, because the lint's filter walk does not descend that key. Neither door refuses it at publish, so the entry now tells the upgrader to sweep it by hand. Two controls back this 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. + 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 declared `date` or `datetime` field, while `this_quarter` on a `select` field stays green. + - 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 five sentences unique to the corrected text reads 0 before and 4 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.