fix(spec): the preset-comparand entry puts each carrier under the door that refuses it - #19801
Conversation
…r 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3c00e296960792c140150f6619501525df73fbda && git checkout 3c00e296960792c140150f6619501525df73fbda
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0b83e01627dbf2dce05cb38148fecd3cff430696 a82423396b1c204e2d8f6a1ae76377d9d20ea736 && git checkout -B drift-repro 0b83e01627dbf2dce05cb38148fecd3cff430696 && git merge --no-ff a82423396b1c204e2d8f6a1ae76377d9d20ea736
node scripts/docs-audit/affected-docs.mjs --json 0b83e01627dbf2dce05cb38148fecd3cff430696 |
Contract reviewServed-tier: Card #19778, round 2. Base Tier, measured by the SEAT from the reviewer's transcript (⛔ not its self-report): ① Derived judgments
② Semver level
③ Boundary flagsNone tripped. Exactly three files. CI at this head, the reviewer's reading: 33 names: 27 success, 3 skipped, and Non-blocking findings, and what the seat does with them
Implemented-by: VERDICT: PASS Generated by Claude Code |
…rrier 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 <noreply@anthropic.com>
…r 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Card #19778, round 3. Base Tier, measured by the SEAT from the reviewer's transcript (⛔ not its self-report): ① Derived judgmentsThe round-2 blocking concern is resolved:
② Semver level
③ Boundary flagsNone tripped. Three files; CI at this head, read after the seat's 07:20Z body edit: 35 names — 30 success, 5 skipped, 0 in progress / queued / cancelled / failure. All seven required contexts are Non-blocking, recorded and ⛔ not a round:
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #19778
Clause-②: no
The shipped ADR-0087 semantic entry
filter-preset-ordering-comparand-refusedlisted "page filter, component filter" among theFilterConditionSchemacarriers. It also said "the schema door and the @objectstack/lint filter-preset-comparand rule refuse it at publish". Page and component filters areViewFilterRuleSchemarule 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,reasonandacceptanceCriteriaas 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.tsis a string literal inside this one step-18 entry, which the exportedMIGRATIONS_BY_MAJORcarries, so what moves indistis prose.packages/lintis 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
acceptanceCriteriais the catch-all, and it now also names agt/gte/lt/ltelookup filter value.Slots typed
FilterConditionSchemaare refused at parse, at the comparand's own path, and the lint rule reports them too. The slots are:optionsFrom.filter);runtimeFilter, on the report or on a joined-report block;summaryOperations.filter;relatedListFilter.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:
ViewFilterRuleSchemarule arrays: a view'sfilter, a page element'sdataSource.filterand a page component'sfilterprop;FilterConditionSchema: a flow CRUD node'sconfig.filter.The lint is also what refuses a preset in an ordering
[field, op, value]triple.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.filterByrule array, and a lookup field'slookupFilters, whose ordering operators are spelledgt/gte/lt/lte.acceptanceCriteriasays the sweep is mechanical for groups 1 and 2 (os validate/os lint, plus asafeParseof the declaring schema for group 1), and by hand for group 3.filterByrule whose operator is an ordering one (greater_than,greater_than_or_equal,less_than,less_than_or_equal,before,after,between, or an alias);lookupFiltersentry whose operator isgt,gte,ltorlte. These are the only ordering spellings that key accepts, and it has nobetween.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-griddefaultFilters.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'stimeDimensions[].dateRangealso accepts the preset names and resolves them.packages/core/src/utils/analytics-date-range.tslowers them to a window.AnalyticsQuerySchemawithdateRange: 'last_30_days'is GREEN, and the control"Last 7 days"is refused attimeDimensions.0.dateRange.replacementis 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 declareddateordatetime. 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.Probe matrix: the prose against every cell
Everything was parsed against the built
dist, resolved through package exports:@objectstack/spec/uiand/dataresolve topackages/spec/dist/{ui,data}/index.js, and@objectstack/linttopackages/lint/dist/index.cjs.{ field: 'close_date', operator: OP, value: V }on a declareddatefield.Vis'last_30_days', or['today', '2026-12-31']forbetween.'2026-01-01'(or an ISO pair). It is GREEN in every cell of every row, so that column is omitted.@objectstack/lint, presetDashboardWidgetSchema.filterfilter.close_date.$gtdashboards[0].widgets[0].filter.close_date.$gtGlobalFilterOptionsFromSchema.filterfilter.close_date.$gtdashboards[0].globalFilters[0].optionsFrom.filter.close_date.$gtDatasetSchema.filterfilter.close_date.$gtdatasets[0].filter.close_date.$gtDatasetMeasureSchema.filterfilter.close_date.$gtdatasets[0].measures[0].filter.close_date.$gtReportSchema.runtimeFilterruntimeFilter.close_date.$gtreports[0].runtimeFilter.close_date.$gtJoinedReportBlockSchema.runtimeFilterruntimeFilter.close_date.$gtreports[0].blocks[0].runtimeFilter.close_date.$gtFieldSchema.relatedListFilterrelatedListFilter.close_date.$gtobjects[1].fields.parent.relatedListFilter.close_date.$gtFieldSchema.summaryOperations.filtersummaryOperations.filter.close_date.$gtobjects[1].fields.total.summaryOperations.filter.close_date.$gtListViewSchema.filter,greater_than/betweenviews[0].filter[0].value/.value[0]PageSchemaelementdataSource.filter, both opspages[0].regions[0].components[0].dataSource.filter[0].value/.value[0]record:related_listfilterprop (its props schema, andPageSchema), both ops...components[0].properties.filter[0].value/.value[0]['close_date', '>=', V]PageSchema)...properties.filter[2]PageSchemainterfaceConfig.filterBy, both opsControls on group 3, with the same rule:
filterByvalue{ $x: 1 }is REFUSED atinterfaceConfig.filterBy.0.value.interfaceConfig.filteris refused by the lint atpages[0].interfaceConfig.filter[0].value, whilePageSchemarefuses the alias key.The
eqrows back the narrowedreasonsentence:FilterConditionSchema{ close_date: 'last_30_days' }and{ close_date: { $in: ['last_30_days'] } }are both GREEN.{ close_date: 'last_30_days' }is refused by the lint atdashboards[0].widgets[0].filter.close_date.equalspreset is refused by the lint atviews[0].filter[0].value, and a view ruleinpreset atviews[0].filter[0].value[0].selectfield,this_quarterstays GREEN for bothequalsandin.The
replacementsentence checks:DashboardSchemadateRange.defaultRange: 'last_30_days'is GREEN, and the control'last_60_days'is refused atdateRange.defaultRange.DATE_RANGE_PRESET_MACRO_WINDOWSis exported from@objectstack/spec/datawith 13 keys.Rows to append to the matrix table (group column first):
@objectstack/lint, presetget_record/update_record/delete_recordnodeconfig.filter(a loose record),$gtand$betweenFlowSchema;GetRecordConfigSchematoo)flows[0].nodes[1].config.filter.close_date.$gt/.$between[0]FieldSchema.lookupFilters, operatorgt/gte/lt/lteBoth new rows read GREEN / GREEN with the ISO dark control.
The
lookupFiltersoperator vocabulary, each with value'last_30_days':FieldSchema:gt,gte,ltandlte, plus the non-orderingeq,ne,in,notInandcontains.lookupFilters.0.operator:greater_than,greater_than_or_equal,less_than,after,before,between,>,>=,$gtandGT.{ field, operator: 'gt', value: 'last_30_days' }under a view'sfilteris refused by the lint atviews[0].filter[0].value.Binding rows, which back the tightened
reasonsentence:datasetbinds todealwith{ close_date: 'last_30_days' }is REFUSED atdashboards[0].widgets[0].filter.close_date.dataset: 'ghost', which resolves to nothing, is GREEN.{ ghost_date: 'last_30_days' }, is GREEN.selectfield withthis_quarteris GREEN.{ 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
eqandreplacementrows, and the rows above were run on two builds:distbuilt atbff19cc8b9(identical to heada82423396bunderpackages/);07d787ce96.The outputs are byte-identical, and the round-2 matrix output is unchanged.
FilterConditionSchemaon{ name: { $icontains: '' } }is GREEN here and REFUSED atname.$icontainson fix(spec)!: the filter doors refuse the three shapes they already declared refused (#19514) #19750, so each run read its owndist.origin/mainatde4ed33fd5.git diff --quiet 2cf9db7c43 de4ed33fd5 -- packages/spec/src/data packages/spec/src/ui packages/spec/src/automation packages/spec/src/migrations packages/lintexits 0, so none of the probed surfaces moved onmain.Publishing reach
src/migrations/entries/**is generator input.registry.tsis whatdistis built from, and it was regenerated withpnpm --filter @objectstack/spec gen:migration-registry, never hand-edited.Counts use
grep -o -F | wc -loverdist/index.js,dist/index.mjs,dist/browser/index.jsanddist/browser/index.mjs. "Before" is base2cf9db7c43. For the new sentences, it is #19750'sa17615e3adbuild, whose copy of this entry is byte-identical to base's (git diff --quietexit 0);registry.tsat base holds 0 of them.page filter, component filter, rollup filter(removed)the schema door and the @objectstack/lint filter-preset-comparand rule refuse it at publish(removed)report each one by path, so the sweep is mechanical(removed)which is the only place any layer ever resolved them(removed)Ordering positions only, deliberately: equality and membership are NOT judged(removed)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)is two doors with different reach, not one: the FilterConditionSchema parse refuses the shape on the slots typed that way(new)Group (3) is BY HAND, because nothing reports it(new)stay fully legal where a layer resolves them to a window(new)Ordering positions only at the schema door, deliberately: it judges no equality(new)compared false against every row: HTTP 200(dark control, unchanged sentence)is a rule array under a key the lint DOES walk(zero control)Round 3. Counted with the same four bundles. "Before" is the build at
775de57ad9, and "after" is the build atbff19cc8b9.and by its key, in three groups(removed)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)which makes it the only door for a ViewFilterRuleSchema rule array(removed)because nothing reports it: search every(removed)position on a declared date or datetime field, and on a temporal field(removed)a list of what was measured, not a closed partition(new)a Mongo-shape filter record typed as a loose record rather than FilterConditionSchema(new)whose ordering operators are spelled gt / gte / lt / lte(new)the only ordering spellings that key accepts(new)on a field it can resolve to a declared date or datetime(new)compared false against every row: HTTP 200(dark control)a list of what was measured, and a closed partition(zero control)Cumulative against base
2cf9db7c43:does NOT walksentence, was reworded this round and is counted as removed above.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 inregistry.tsis 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.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsfrom this worktree: 80 derived. On--ranreconciliation: 79 run, exit 0; 1 NOT MEASURED; 0 UNRUN.pnpm check:dual-build-cjs-loads, exit 3PREREQUISITE NOT MET. It needs a repo-wide build, which is CI's run. ⛔ It is not a pass.pnpm check:lean-entry-closureexited 0 after the@objectstack/objectqlclosure was built.origin/mainde4ed33fd5, and one gate file changed across that range (scripts/check-spec-docblock-symbol-anchors.mjs).--self-testwere re-run frommain's copy on the merged generation, and both exit 0. The merged generation is a throwaway worktree at an unpushed merge commit of treea409220cc1, built withgit merge-tree --write-tree HEAD origin/main.check-changeset-fixed.mjs,check:meta-url-spelling,check:authz-resolver,check:error-code-casing,check:filter-alias-parity..tsfiles reports 2 files, 0 errors, 0 warnings.--print-configshows noparserOptions.projectorprojectService, so linting is not type-aware.pnpm lintis CI's.a82423396b, on a single read: 35 check-runs, 32 success, 3 skipped, 0 failure.Acceptance notes
FieldSchema.lookupFilterswas 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 [finding] the lint's filter walk (FILTER_KEYS = filter · filters · runtimeFilter · relatedListFilter) never reaches a page's interfaceConfig.filterBy — a bare date-range preset in that rule array parses green AND lints green, so nothing refuses it at publish #19791.config.filternow sits in group 2. It is lint-refused, so it was always swept mechanically; the prose now says so.origin/mainmoved tode4ed33fd5after 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.Generated by Claude Code
Generated by Claude Code