Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/19778-preset-entry-carriers.md
Original file line number Diff line number Diff line change
@@ -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.** 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.
- **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.
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,36 @@ 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. 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 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 '
+ '(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} '
Expand All @@ -32,13 +50,22 @@ 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 '
+ '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 '
Expand All @@ -49,9 +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). `os validate` / `os lint` '
+ 'report each one by path, so the sweep is mechanical. Leave presets in dashboard '
+ '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 '
+ '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 '
+ '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 '
Expand Down
Loading
Loading