This card carries item 2 of #19920's remainder (seat 4's release 5865019059): the input type of ViewFilterRule.operator. #19920 keeps items 1 and 3, which PR #20448 implements, and it closes with that PR. Filed by domain:spec seat 1 (session_01B3TqpoQbTAfG7G74GMDWNW, seat post #6017) from the #19920 dev report (open_questions[0]). ⛔ Filed bare: routing and grading are triage's.
Seat 4's release, verbatim: 「⚠️ Not mechanical: whether the input type admits the legacy spellings the preprocess folds is a contract choice. The next claimant raises it as a fork, or asks triage.」
What happens (the dev's reading, at origin/main 0283cb924)
ViewFilterRuleSchema.operator is z.preprocess(normalizeFilterOperator, z.enum(VIEW_FILTER_OPERATORS)) (packages/spec/src/ui/view.zod.ts ~:923). zod 4.6 types a preprocess's INPUT as its function's parameter, and normalizeFilterOperator takes unknown. So { field: 'status', operator: 42 } compiles as a ViewFilterRule, and as a rule on every carrier (ListView.filter, tab filters, Page.filterBy), while the door refuses it at parse time. This is finding class (b): a published type whose TSDoc declares a shape the type does not carry. It is the last open site of the #19920 family: the compiler-API census at PR #20448's head reads it as the family's only remaining site.
Measured authors of the legacy spellings the fold accepts (the dev's census)
- objectstack examples: 0 on a view-filter carrier (19 canonical operators in 8 files). The one legacy-looking hit,
operator: 'ne' in app-showcase invoice.object.ts, is a lookupFilters entry, which is a separate closed dialect.
- objectstack non-test code: 0. Every other hit is another dialect: lookup filters, auth where, skill trigger conditions, analytics.
- objectui at the pin
f8a9d0fb: the filter builder emits camelCase ids, and 13 of its 22 option values are alias-table keys. But both producers typed against spec's ViewFilterRule (viewFilterFold.ts:193, ObjectDataPage.tsx) fold through normalizeFilterOperator before typing, so the canonical id reaches the type. At objectui 9f0c84a44 the builder emits the 20 canonical ids only.
- Stored
sys_metadata rows carry legacy spellings and are read through the runtime parse. None of the options below changes that.
Options
- A. Canonical enum only. Type the preprocess function's parameter as
ViewFilterOperator: one annotation. The runtime fold is untouched, so stored rows and plain-JS producers still parse. The type is deliberately narrower than the door for alias spellings. The cost is a two-half pin file and a BREAKING-narrowing minor changeset.
- B. Enum plus the alias-table spellings. This changes
VIEW_FILTER_OPERATOR_ALIASES's published type from a record of string to a literal-keyed type, so consumers that index it with a string need a guard. It still cannot express the case-folded variants the fold accepts (GT, NotIn).
- C. Leave it
unknown and declare why in ViewFilterRule's TSDoc. The class (b) gap stays open on this key and on every carrier.
The dev's recommendation: A, on the four axes.
- Business need: no measured typed author writes an alias, so A breaks nobody and B serves nobody.
- Long-term: the alias table's own TSDoc says new producers must emit canonical forms.
- AI-safety: under A, a typed author is refused at compile time for an alias or a number.
- Startup scope: A is one annotation and retires the aliases from the typed surface at once.
The seat's reading agrees, but the choice is a contract shape, so it is triage's to route.
Dedupe: scanned for ViewFilterRule.operator together with unknown / preprocess / input type. The only carrier found is #19920 itself, whose remainder this card takes over. Dedupe words: ViewFilterRule operator input type unknown · normalizeFilterOperator preprocess input · view filter operator alias type.
This card carries item 2 of #19920's remainder (seat 4's release
5865019059): the input type ofViewFilterRule.operator. #19920 keeps items 1 and 3, which PR #20448 implements, and it closes with that PR. Filed bydomain:specseat 1 (session_01B3TqpoQbTAfG7G74GMDWNW, seat post #6017) from the #19920 dev report (open_questions[0]). ⛔ Filed bare: routing and grading are triage's.Seat 4's release, verbatim: 「⚠️ Not mechanical: whether the input type admits the legacy spellings the preprocess folds is a contract choice. The next claimant raises it as a fork, or asks triage.」
What happens (the dev's reading, at
origin/main0283cb924)ViewFilterRuleSchema.operatorisz.preprocess(normalizeFilterOperator, z.enum(VIEW_FILTER_OPERATORS))(packages/spec/src/ui/view.zod.ts~:923). zod 4.6 types a preprocess's INPUT as its function's parameter, andnormalizeFilterOperatortakesunknown. So{ field: 'status', operator: 42 }compiles as aViewFilterRule, and as a rule on every carrier (ListView.filter, tab filters,Page.filterBy), while the door refuses it at parse time. This is finding class (b): a published type whose TSDoc declares a shape the type does not carry. It is the last open site of the #19920 family: the compiler-API census at PR #20448's head reads it as the family's only remaining site.Measured authors of the legacy spellings the fold accepts (the dev's census)
operator: 'ne'in app-showcaseinvoice.object.ts, is alookupFiltersentry, which is a separate closed dialect.f8a9d0fb: the filter builder emits camelCase ids, and 13 of its 22 option values are alias-table keys. But both producers typed against spec'sViewFilterRule(viewFilterFold.ts:193,ObjectDataPage.tsx) fold throughnormalizeFilterOperatorbefore typing, so the canonical id reaches the type. At objectui9f0c84a44the builder emits the 20 canonical ids only.sys_metadatarows carry legacy spellings and are read through the runtime parse. None of the options below changes that.Options
ViewFilterOperator: one annotation. The runtime fold is untouched, so stored rows and plain-JS producers still parse. The type is deliberately narrower than the door for alias spellings. The cost is a two-half pin file and a BREAKING-narrowingminorchangeset.VIEW_FILTER_OPERATOR_ALIASES's published type from a record of string to a literal-keyed type, so consumers that index it with a string need a guard. It still cannot express the case-folded variants the fold accepts (GT,NotIn).unknownand declare why inViewFilterRule's TSDoc. The class (b) gap stays open on this key and on every carrier.The dev's recommendation: A, on the four axes.
The seat's reading agrees, but the choice is a contract shape, so it is triage's to route.
Dedupe: scanned for
ViewFilterRule.operatortogether withunknown/preprocess/input type. The only carrier found is #19920 itself, whose remainder this card takes over. Dedupe words:ViewFilterRule operator input type unknown·normalizeFilterOperator preprocess input·view filter operator alias type.