Skip to content

Commit 9df3934

Browse files
fix(spec): the preset-comparand entry puts each carrier under the door that refuses it (#19801)
Fixes #19778 Clause-②: no The shipped ADR-0087 semantic entry `filter-preset-ordering-comparand-refused` listed "page filter, component filter" among the `FilterConditionSchema` carriers. It also said "the schema door and the @objectstack/lint filter-preset-comparand rule refuse it at publish". Page and component filters are `ViewFilterRuleSchema` rule arrays, and no schema door judges a preset there. So an upgrader who swept stored pages with a schema parse found nothing and read the sweep as clean. This PR rewrites `surface`, `reason` and `acceptanceCriteria` as a three-way split, with each carrier named by its declared type and its key. It also narrows two more sentences that are false today (see below). No behaviour moves. No schema, accept set or lint rule is touched, and no export is added, removed or retyped. Every changed line in `registry.ts` is a string literal inside this one step-18 entry, which the exported `MIGRATIONS_BY_MAJOR` carries, so what moves in `dist` is prose. `packages/lint` is not touched; the lint's reach gap is **#19791**. ## What the entry now says The groups list the carriers measured, not a closed partition. The grep sentence at the head of `acceptanceCriteria` is the catch-all, and it now also names a `gt`/`gte`/`lt`/`lte` lookup filter value. 1. **Slots typed `FilterConditionSchema`** are refused at parse, at the comparand's own path, and the lint rule reports them too. The slots are: - a dashboard widget filter; - a dashboard global-filter options-source filter (`optionsFrom.filter`); - a dataset filter and a dataset measure filter; - a report `runtimeFilter`, on the report or on a joined-report block; - a rollup `summaryOperations.filter`; - a `relatedListFilter`. 2. **Filters under a key the lint walks, whose declared type carries no preset check,** parse GREEN, and the lint rule is the only door. These are: - `ViewFilterRuleSchema` rule arrays: a view's `filter`, a page element's `dataSource.filter` and a page component's `filter` prop; - a Mongo-shape record typed as a loose record rather than `FilterConditionSchema`: a flow CRUD node's `config.filter`. The lint is also what refuses a preset in an ordering `[field, op, value]` triple. 3. **Filters under a key the lint does not walk** parse GREEN and lint GREEN, so neither door refuses them at publish. These are a page's `interfaceConfig.filterBy` rule array, and a lookup field's `lookupFilters`, whose ordering operators are spelled `gt` / `gte` / `lt` / `lte`. - `acceptanceCriteria` says the sweep is mechanical for groups 1 and 2 (`os validate` / `os lint`, plus a `safeParse` of the declaring schema for group 1), and **by hand** for group 3. - The hand sweep is one search per carrier: - a `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); - a `lookupFilters` entry whose operator is `gt`, `gte`, `lt` or `lte`. These are the only ordering spellings that key accepts, and it has no `between`. - The windows come from `DATE_RANGE_PRESET_MACRO_WINDOWS`. The prose names no refinement function, no issue or PR number beyond the entry's existing ruling citations, and says nothing about `object-grid` `defaultFilters`. ## Two more sentences that were false, each changed alone - **`replacement`: "... dashboard date-filter positions ..., which is the only place any layer ever resolved them".** An analytics query's `timeDimensions[].dateRange` also accepts the preset names and resolves them. `packages/core/src/utils/analytics-date-range.ts` lowers them to a window. - Measured: `AnalyticsQuerySchema` with `dateRange: 'last_30_days'` is GREEN, and the control `"Last 7 days"` is refused at `timeDimensions.0.dateRange`. - The sentence now names both positions. The rest of `replacement` is unchanged. - **`reason`: "Ordering positions only, deliberately: equality and membership are NOT judged".** This is true of the schema door only. The lint rule's field-typed arm 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 (see the binding rows below). The sentence now says which door is ordering-only, and what the arm needs. - The ruling citation, the "no D2 conversion" disposition and every other sentence are unchanged. ## Probe matrix: the prose against every cell Everything was parsed against the built `dist`, resolved through package exports: `@objectstack/spec/ui` and `/data` resolve to `packages/spec/dist/{ui,data}/index.js`, and `@objectstack/lint` to `packages/lint/dist/index.cjs`. - The rule is `{ field: 'close_date', operator: OP, value: V }` on a declared `date` field. - The preset `V` is `'last_30_days'`, or `['today', '2026-12-31']` for `between`. - The dark control is `'2026-01-01'` (or an ISO pair). It is GREEN in every cell of every row, so that column is omitted. | group | carrier: declared type and key | schema parse, preset | `@objectstack/lint`, preset | |:--|:--|:--|:--| | 1 | `DashboardWidgetSchema.filter` | REFUSED `filter.close_date.$gt` | REFUSED `dashboards[0].widgets[0].filter.close_date.$gt` | | 1 | `GlobalFilterOptionsFromSchema.filter` | REFUSED `filter.close_date.$gt` | REFUSED `dashboards[0].globalFilters[0].optionsFrom.filter.close_date.$gt` | | 1 | `DatasetSchema.filter` | REFUSED `filter.close_date.$gt` | REFUSED `datasets[0].filter.close_date.$gt` | | 1 | `DatasetMeasureSchema.filter` | REFUSED `filter.close_date.$gt` | REFUSED `datasets[0].measures[0].filter.close_date.$gt` | | 1 | `ReportSchema.runtimeFilter` | REFUSED `runtimeFilter.close_date.$gt` | REFUSED `reports[0].runtimeFilter.close_date.$gt` | | 1 | `JoinedReportBlockSchema.runtimeFilter` | REFUSED `runtimeFilter.close_date.$gt` | REFUSED `reports[0].blocks[0].runtimeFilter.close_date.$gt` | | 1 | `FieldSchema.relatedListFilter` | REFUSED `relatedListFilter.close_date.$gt` | REFUSED `objects[1].fields.parent.relatedListFilter.close_date.$gt` | | 1 | `FieldSchema.summaryOperations.filter` | REFUSED `summaryOperations.filter.close_date.$gt` | REFUSED `objects[1].fields.total.summaryOperations.filter.close_date.$gt` | | 2 | `ListViewSchema.filter`, `greater_than` / `between` | GREEN / GREEN | REFUSED `views[0].filter[0].value` / `.value[0]` | | 2 | `PageSchema` element `dataSource.filter`, both ops | GREEN / GREEN | REFUSED `pages[0].regions[0].components[0].dataSource.filter[0].value` / `.value[0]` | | 2 | `record:related_list` `filter` prop (its props schema, and `PageSchema`), both ops | GREEN, GREEN / GREEN, GREEN | REFUSED `...components[0].properties.filter[0].value` / `.value[0]` | | 2 | the same prop authored as a triple `['close_date', '>=', V]` | GREEN (`PageSchema`) | REFUSED `...properties.filter[2]` | | 3 | `PageSchema` `interfaceConfig.filterBy`, both ops | **GREEN / GREEN** | **GREEN / GREEN (0 findings)** | Controls on group 3, with the same rule: - **The slot is parsed.** A `filterBy` value `{ $x: 1 }` is REFUSED at `interfaceConfig.filterBy.0.value`. - **The key name decides.** The same rule under `interfaceConfig.filter` is refused by the lint at `pages[0].interfaceConfig.filter[0].value`, while `PageSchema` refuses the alias key. The `eq` rows back the narrowed `reason` sentence: - `FilterConditionSchema` `{ close_date: 'last_30_days' }` and `{ close_date: { $in: ['last_30_days'] } }` are both GREEN. - A widget filter `{ close_date: 'last_30_days' }` is refused by the lint at `dashboards[0].widgets[0].filter.close_date`. - A view rule `equals` preset is refused by the lint at `views[0].filter[0].value`, and a view rule `in` preset at `views[0].filter[0].value[0]`. - On a `select` field, `this_quarter` stays GREEN for both `equals` and `in`. The `replacement` sentence checks: - `DashboardSchema` `dateRange.defaultRange: 'last_30_days'` is GREEN, and the control `'last_60_days'` is refused at `dateRange.defaultRange`. - `DATE_RANGE_PRESET_MACRO_WINDOWS` is exported from `@objectstack/spec/data` with 13 keys. Rows to append to the matrix table (group column first): | group | carrier: declared type and key | schema parse, preset | `@objectstack/lint`, preset | |:--|:--|:--|:--| | 2 | flow `get_record` / `update_record` / `delete_record` node `config.filter` (a loose record), `$gt` and `$between` | GREEN (`FlowSchema`; `GetRecordConfigSchema` too) | REFUSED `flows[0].nodes[1].config.filter.close_date.$gt` / `.$between[0]` | | 3 | `FieldSchema.lookupFilters`, operator `gt` / `gte` / `lt` / `lte` | **GREEN** | **GREEN (0 findings)** | Both new rows read GREEN / GREEN with the ISO dark control. **The `lookupFilters` operator vocabulary**, each with value `'last_30_days'`: - GREEN at `FieldSchema`: `gt`, `gte`, `lt` and `lte`, plus the non-ordering `eq`, `ne`, `in`, `notIn` and `contains`. - REFUSED at `lookupFilters.0.operator`: `greater_than`, `greater_than_or_equal`, `less_than`, `after`, `before`, `between`, `>`, `>=`, `$gt` and `GT`. - The lint is GREEN on every spelling. - Key-name control: the same `{ field, operator: 'gt', value: 'last_30_days' }` under a view's `filter` is refused by the lint at `views[0].filter[0].value`. **Binding rows**, which back the tightened `reason` sentence: - A widget whose `dataset` binds to `deal` with `{ close_date: 'last_30_days' }` is REFUSED at `dashboards[0].widgets[0].filter.close_date`. - The same widget with `dataset: 'ghost'`, which resolves to nothing, is GREEN. - A bound widget with an undeclared field, `{ ghost_date: 'last_30_days' }`, is GREEN. - A bound `select` field with `this_quarter` is GREEN. - An unbound widget with an ordering `{ close_date: { $gt: 'last_30_days' } }` is REFUSED, because arm 1 is field-agnostic. **True on both trees** (replaces the round-2 paragraph). The whole matrix, the `eq` and `replacement` rows, and the rows above were run on two builds: - this branch, with `dist` built at `bff19cc8b9` (identical to head `a82423396b` under `packages/`); - #19750's head at `07d787ce96`. The outputs are byte-identical, and the round-2 matrix output is unchanged. - **Tree control.** `FilterConditionSchema` on `{ name: { $icontains: '' } }` is GREEN here and REFUSED at `name.$icontains` on #19750, so each run read its own `dist`. - **`origin/main` at `de4ed33fd5`.** `git diff --quiet 2cf9db7 de4ed33 -- packages/spec/src/data packages/spec/src/ui packages/spec/src/automation packages/spec/src/migrations packages/lint` exits 0, so none of the probed surfaces moved on `main`. ## Publishing reach `src/migrations/entries/**` is generator input. `registry.ts` is what `dist` is built from, and it was regenerated with `pnpm --filter @objectstack/spec gen:migration-registry`, never hand-edited. Counts use `grep -o -F | wc -l` over `dist/index.js`, `dist/index.mjs`, `dist/browser/index.js` and `dist/browser/index.mjs`. "Before" is base `2cf9db7c43`. For the new sentences, it is #19750's `a17615e3ad` build, whose copy of this entry is byte-identical to base's (`git diff --quiet` exit 0); `registry.ts` at base holds 0 of them. | string | before | after | |:--|--:|--:| | `page filter, component filter, rollup filter` (removed) | 4 | **0** | | `the schema door and the @objectstack/lint filter-preset-comparand rule refuse it at publish` (removed) | 4 | **0** | | `report each one by path, so the sweep is mechanical` (removed) | 4 | **0** | | `which is the only place any layer ever resolved them` (removed) | 4 | **0** | | `Ordering positions only, deliberately: equality and membership are NOT judged` (removed) | 4 | **0** | | `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` (new) | 0 | **4** | | `is two doors with different reach, not one: the FilterConditionSchema parse refuses the shape on the slots typed that way` (new) | 0 | **4** | | `Group (3) is BY HAND, because nothing reports it` (new) | 0 | **4** | | `stay fully legal where a layer resolves them to a window` (new) | 0 | **4** | | `Ordering positions only at the schema door, deliberately: it judges no equality` (new) | 0 | **4** | | `compared false against every row: HTTP 200` (dark control, unchanged sentence) | 4 | 4 | | `is a rule array under a key the lint DOES walk` (zero control) | 0 | 0 | **Round 3.** Counted with the same four bundles. "Before" is the build at `775de57ad9`, and "after" is the build at `bff19cc8b9`. | string | before | after | |:--|--:|--:| | `and by its key, in three groups` (removed) | 4 | **0** | | `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` (removed) | 4 | **0** | | `which makes it the only door for a ViewFilterRuleSchema rule array` (removed) | 4 | **0** | | `because nothing reports it: search every` (removed) | 4 | **0** | | `position on a declared date or datetime field, and on a temporal field` (removed) | 4 | **0** | | `a list of what was measured, not a closed partition` (new) | 0 | **4** | | `a Mongo-shape filter record typed as a loose record rather than FilterConditionSchema` (new) | 0 | **4** | | `whose ordering operators are spelled gt / gte / lt / lte` (new) | 0 | **4** | | `the only ordering spellings that key accepts` (new) | 0 | **4** | | `on a field it can resolve to a declared date or datetime` (new) | 0 | **4** | | `compared false against every row: HTTP 200` (dark control) | 4 | 4 | | `a list of what was measured, and a closed partition` (zero control) | 0 | 0 | **Cumulative against base `2cf9db7c43`:** - The five round-2 removed strings still read 0. - Four of round 2's five new sentences still read 4. The fifth, the `does NOT walk` sentence, was reworded this round and is counted as removed above. - The changeset's reach bullet now says nine sentences. ## Verification Head is `a82423396b`. Every exit code was captured before any pipe. - `pnpm --filter @objectstack/spec check:migration-registry` → exit 0, "current (232 semantic, 207 retired-key, 183 retired-def)". Every changed line in `registry.ts` is a string literal inside this entry. - `pnpm --filter @objectstack/spec check:generated` → exit 0, "All 15 generated artifacts are up to date", with a declaration stamp match. - `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2` → exit 0: 517 test files passed (517); 15091 tests passed, 1 todo. - `pnpm --filter @objectstack/spec typecheck` → exit 0, `check:test-typecheck: OK`. - Gates derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` from this worktree: **80 derived**. On `--ran` reconciliation: **79 run, exit 0; 1 NOT MEASURED; 0 UNRUN**. - The NOT MEASURED one is `pnpm check:dual-build-cjs-loads`, exit 3 `PREREQUISITE NOT MET`. It needs a repo-wide build, which is CI's run. ⛔ It is not a pass. - `pnpm check:lean-entry-closure` exited 0 after the `@objectstack/objectql` closure was built. - The derivation warned **STALE TREE**: the branch is behind `origin/main` `de4ed33fd5`, and one gate file changed across that range (`scripts/check-spec-docblock-symbol-anchors.mjs`). - That gate and its `--self-test` were re-run from `main`'s copy on the merged generation, and both exit 0. The merged generation is a throwaway worktree at an unpushed merge commit of tree `a409220cc1`, built with `git merge-tree --write-tree HEAD origin/main`. - The same merged tree derives the identical 80 families, with no stale warning. - The branch is not merged: no conflict forces it. - The five artifact-roster gates whose roster sits under a touched directory all exit 0: `check-changeset-fixed.mjs`, `check:meta-url-spelling`, `check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`. - A control-byte self-scan of the three changed files finds 0 (grep exit 1). - Narrowed eslint on the two changed `.ts` files reports 2 files, 0 errors, 0 warnings. - The population is the flat config's ts/js object. - `--print-config` shows no `parserOptions.project` or `projectService`, so linting is not type-aware. - The repo-wide `pnpm lint` is CI's. - CI at `a82423396b`, on a single read: 35 check-runs, 32 success, 3 skipped, 0 failure. ## Acceptance notes - `FieldSchema.lookupFilters` was reported last round as outside both doors. It is now named in the entry's group 3, and the by-hand clause covers the operator spellings it actually takes. The lint-side fix for both group-3 carriers is **#19791**. - A flow CRUD node's `config.filter` now sits in group 2. It is lint-refused, so it was always swept mechanically; the prose now says so. - `origin/main` moved to `de4ed33fd5` after this branch's base. None of the probed surfaces moved, and the one gate file that changed was re-run on the merged generation. Not merged. - When #19791 lands, group 3's sentences and the by-hand clause go stale in the safe direction, for both carriers. That round owes one more edit and regeneration of this entry. --- _Generated by [Claude Code](https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr)_ --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f151ef2 commit 9df3934

3 files changed

Lines changed: 129 additions & 34 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
`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.
6+
7+
Clause-②: no
8+
9+
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.
10+
11+
- **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.
12+
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.
13+
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.
14+
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.
15+
- **Two more false sentences are narrowed.**
16+
- 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.
17+
- 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.
18+
- **Reach.** Counted over `dist/index.js`, `dist/index.mjs`, `dist/browser/index.js` and `dist/browser/index.mjs`:
19+
- The removed carrier list `page filter, component filter, rollup filter` and each of the four other removed claims read 4 before and 0 after.
20+
- Each of nine sentences unique to the corrected text reads 0 before and 4 after.
21+
- The unchanged dark control `compared false against every row: HTTP 200` reads 4 on both sides.

‎packages/spec/src/migrations/entries/semantic/18.filter-preset-ordering-comparand-refused.ts‎

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,36 @@ export const entry: SemanticMigration = {
1010
'a dashboard date-range preset name (last_7_days / last_30_days / last_90_days, today, '
1111
+ 'yesterday, this_week, last_week, this_month, last_month, this_quarter, last_quarter, '
1212
+ 'this_year, last_year) authored as a bare ORDERING comparand in a filter — a '
13-
+ '$gt / $gte / $lt / $lte value or a $between endpoint on any carrier of '
14-
+ 'FilterConditionSchema (dashboard widget filter, dataset filter, report runtimeFilter, '
15-
+ 'page filter, component filter, rollup filter), a greater_than / less_than / before / '
16-
+ 'after / between view filter rule value, or an ordering [field, op, value] filter triple',
13+
+ '$gt / $gte / $lt / $lte value or a $between endpoint, a greater_than / less_than / '
14+
+ 'before / after / between view filter rule value, or an ordering [field, op, value] '
15+
+ 'filter triple. WHICH DOOR refuses it at publish is decided by the carrier\'s declared '
16+
+ 'type and by its key. The carriers measured fall in three groups, and the groups are a '
17+
+ 'list of what was measured, not a closed partition: the grep in the acceptance criteria '
18+
+ 'is the catch-all. (1) A slot typed FilterConditionSchema — a '
19+
+ 'dashboard widget filter, a dashboard global-filter options-source filter '
20+
+ '(optionsFrom.filter), a dataset filter, a dataset measure filter, a report runtimeFilter '
21+
+ '(on the report or on a joined-report block), a rollup summaryOperations.filter and a '
22+
+ 'relatedListFilter — is refused at PARSE, at the comparand\'s own path, and the '
23+
+ '@objectstack/lint filter-preset-comparand rule reports it as well. (2) A filter under a '
24+
+ 'key the lint walks, whose declared type carries no preset check, parses GREEN, and the '
25+
+ 'lint rule is the only door that refuses it: a ViewFilterRuleSchema rule array (a view\'s '
26+
+ 'filter, a page element\'s dataSource.filter, a page component\'s filter prop), and a '
27+
+ 'Mongo-shape filter record typed as a loose record rather than FilterConditionSchema (a '
28+
+ 'flow CRUD node\'s config.filter). The lint is likewise what refuses a preset in an '
29+
+ 'ordering filter triple wherever its walk meets one. (3) A filter under a key the lint '
30+
+ 'does NOT walk parses GREEN and lints GREEN, so neither door refuses it at publish and '
31+
+ 'only a search of the authored and stored metadata finds it: a page\'s '
32+
+ 'interfaceConfig.filterBy rule array, and a lookup field\'s lookupFilters, whose ordering '
33+
+ 'operators are spelled gt / gte / lt / lte',
1734
replacement:
1835
'the date-macro window the preset already means — { $gte: "{30_days_ago}" } for '
1936
+ 'last_30_days, { $between: ["{week_start}", "{week_end}"] } for this_week, and so on '
2037
+ '(the rejection names the exact window per preset; DATE_RANGE_PRESET_MACRO_WINDOWS in '
2138
+ '@objectstack/spec/data is the table) — or an ISO date such as 2026-01-15. The preset '
22-
+ 'names themselves stay fully legal in the dashboard date-filter positions '
23-
+ '(dateRange.defaultRange, a date global filter defaultValue), which is the only place '
24-
+ 'any layer ever resolved them',
39+
+ 'names themselves stay fully legal where a layer resolves them to a window: the dashboard '
40+
+ 'date-filter positions (dateRange.defaultRange, a date global filter defaultValue) and an '
41+
+ 'analytics query\'s timeDimensions[].dateRange. A filter comparand is not one of those '
42+
+ 'positions',
2543
reason:
2644
'The C half of #8690, maintainer-ruled 2026-08-15 alongside the engine door (PR #8808). '
2745
+ 'The preset vocabulary is declared in the dashboard schema and lowered to {date-macro} '
@@ -32,13 +50,22 @@ export const entry: SemanticMigration = {
3250
+ 'count 0, indistinguishable from "there is no data" (measured on #8690: $gte '
3351
+ '"last_30_days" returned 0 of 51 seeded rows where the macro spelling returned the 38 '
3452
+ 'in-window). The engine now refuses the bare name on a declared temporal field at query '
35-
+ 'time (INVALID_FILTER / 400); this entry records the AUTHORING-time half: the schema '
36-
+ 'door and the @objectstack/lint filter-preset-comparand rule refuse it at publish, '
37-
+ 'where the author — an AI author in particular — can still act on the message. '
38-
+ 'Ordering positions only, deliberately: equality and membership are NOT judged, because '
39-
+ 'a select/picklist column legitimately stores values that collide with preset names, '
40-
+ 'and on a temporal field the engine door already refuses those with the field type in '
41-
+ 'hand. ⚠️ Metadata AT REST is deliberately not rewritten and there is no D2 conversion: '
53+
+ 'time (INVALID_FILTER / 400); this entry records the AUTHORING-time half, and that half '
54+
+ 'is two doors with different reach, not one: the FilterConditionSchema parse refuses the '
55+
+ 'shape on the slots typed that way, and the @objectstack/lint filter-preset-comparand '
56+
+ 'rule refuses it on every filter its walk reaches, which makes it the only door for a '
57+
+ 'walked filter whose declared type carries no preset check. Both answer at publish, where '
58+
+ 'the author — an AI author in particular — can still act on the message; a page\'s '
59+
+ 'interfaceConfig.filterBy and a lookup field\'s lookupFilters are reached by neither, and '
60+
+ 'the surface\'s groups say which measured carrier sits under which door. Ordering '
61+
+ 'positions only at the schema door, deliberately: it judges no equality or membership, '
62+
+ 'because a select/picklist column legitimately stores values that collide with preset '
63+
+ 'names and a schema has no field type in hand. The lint rule, which reads the stack\'s '
64+
+ 'object metadata, additionally refuses a preset in an equality or membership position, '
65+
+ 'in a filter its walk reaches, on a field it can resolve to a declared date or datetime '
66+
+ '(where the filter binds to no object, or the field resolves to nothing, that arm cannot '
67+
+ 'fire), and on a temporal field the engine door already refuses those with the field '
68+
+ 'type in hand. ⚠️ Metadata AT REST is deliberately not rewritten and there is no D2 conversion: '
4269
+ 'this shape was never written by any first-party producer (every preset in this repo '
4370
+ 'and the example apps sits in a dashboard date-filter position — measured) and never '
4471
+ 'executed usefully (it returned a silent zero before #8808 and a 400 after). Coercing '
@@ -49,9 +76,19 @@ export const entry: SemanticMigration = {
4976
acceptanceCriteria:
5077
'Grep your authored filters for the thirteen preset names in ordering positions — a '
5178
+ '$gt/$gte/$lt/$lte value, a $between endpoint, a greater_than/less_than/before/after/'
52-
+ 'between view rule value, an ordering filter triple — and rewrite each to the '
53-
+ '{date-macro} window the rejection names (or an ISO date). `os validate` / `os lint` '
54-
+ 'report each one by path, so the sweep is mechanical. Leave presets in dashboard '
79+
+ 'between view rule value, an ordering filter triple, a gt/gte/lt/lte lookup filter value '
80+
+ '— and rewrite each to the {date-macro} window the rejection names (or an ISO date). That '
81+
+ 'grep is the catch-all; the surface\'s groups are the carriers measured. The sweep is '
82+
+ 'mechanical for groups (1) and (2): `os validate` / `os lint` report each one by path, and '
83+
+ 'a group (1) slot is also refused by a `safeParse` of the schema that declares it, at the '
84+
+ 'comparand\'s own path. Group (3) is BY HAND, because nothing reports it. Search every '
85+
+ 'page for an `interfaceConfig.filterBy` rule whose operator is an ordering one '
86+
+ '(greater_than, greater_than_or_equal, less_than, less_than_or_equal, before, after, '
87+
+ 'between, or an alias of one) and whose value — or either `between` endpoint — is one of '
88+
+ 'the thirteen names. Search every lookup field for a `lookupFilters` entry whose operator '
89+
+ 'is `gt`, `gte`, `lt` or `lte` — the only ordering spellings that key accepts; it has no '
90+
+ '`between` — and whose value is one of the thirteen names. Take each window from '
91+
+ '`DATE_RANGE_PRESET_MACRO_WINDOWS`, since no rejection names it. Leave presets in dashboard '
5592
+ 'date-filter positions (dateRange.defaultRange, date global filter defaultValue) '
5693
+ 'untouched — they remain the declared vocabulary there. A filter that carried one of '
5794
+ 'these shapes was never returning the window it named (silent zero before the engine '

0 commit comments

Comments
 (0)