Measured while landing objectui#8513. That card fixed the CONSUMER half (ValueDataSource now answers these three correctly); this is the producer half, on a different consumer, and it needs a decision rather than a mechanical repair — so it is filed, not folded in. ⛔ Not claimed.
Measured
convertFiltersToAST({ $and: [] }) => { $and: [] } isFilterAST: false
convertFiltersToAST({ $or: [{}] }) => { $or: [{}] } isFilterAST: false
convertFiltersToAST({ $and: [{}] }) => { $and: [{}] } isFilterAST: false
convertFiltersToAST({ $or: [] }) => ['$or', '=', []] isFilterAST: true <- correct
(Run against @objectstack/spec 17.3.0 and this repo's built @object-ui/core.)
Why
lowerLogicalGroup returns undefined for a group that reduces to the TRUE identity — deliberately, and its docblock explains it: TRUE is "the absence of a constraint", and emitting a childless ['and'] would be isFilterAST FALSE and parseFilterAST undefined, i.e. no filter at all, which widens.
But when that undefined is the ONLY thing the filter produced, convertFiltersToAST falls to its general tail:
// If no conditions, return original filter
if (conditions.length === 0) {
return filter;
}
— and hands back the caller's original OBJECT. So the group does not disappear at that level after all; it reappears one level up, in the $ dialect, in a slot the AST is expected to occupy. toFilterNode and mergeFilterNodes pass it straight through, and every consumer on that chain (ObjectGrid's schemaFilter, plugin-list's buildEffectiveFilter, plugin-view's ObjectView) drops it onto $filter.
Consequence, per consumer
provider: 'object' (the wire). @object-ui/data-objectstack routes an object $filter through this same function and then gates on isFilterAST. These three shapes do not pass it, so a filter whose ruled answer is EVERY row (objectstack#5322, merged objectstack#5365) is refused instead of being dropped as no-constraint.
provider: 'value'. Answered ZERO rows until objectui#8513; correct as of that card, which is why this one is about the producer and not about the matcher.
The decision this needs
Returning undefined from toFilterNode for a TRUE-identity filter is the obvious shape and is NOT obviously safe: convertFiltersToAST is exported and its declared return type includes Record<string, any>, the conditions.length === 0 tail also serves inputs that are not combinators at all (an all-null filter, {}), and undefined vs. the original object is exactly how some callers tell "no constraint" from "unlowerable". Whoever takes this should decide whether the identity fold belongs in lowerLogicalGroup's caller, in toFilterNode, or in a distinguishable return value — not pick one in passing.
Relationship to neighbours
Same function as objectui#8567 / #8568 / #8530 / #8555, and a residual of objectui#6948 (which added the $and / $or branch). Four different defect shapes with four different fixes — ⛔ not foldable; they serialise.
Dedup
Run, instrument validated with a control in the same session. search_issues (repo-scoped, semantic) over the convertFiltersToAST neighbourhood returns objectui#8567, #8568, #8530, #6948, #8555 and #7752 — 9 total, none of them this shape. Closed cards were in range.
Generated by Claude Code
Measured while landing objectui#8513. That card fixed the CONSUMER half (
ValueDataSourcenow answers these three correctly); this is the producer half, on a different consumer, and it needs a decision rather than a mechanical repair — so it is filed, not folded in. ⛔ Not claimed.Measured
(Run against
@objectstack/spec17.3.0 and this repo's built@object-ui/core.)Why
lowerLogicalGroupreturnsundefinedfor a group that reduces to the TRUE identity — deliberately, and its docblock explains it: TRUE is "the absence of a constraint", and emitting a childless['and']would beisFilterASTFALSE andparseFilterASTundefined, i.e. no filter at all, which widens.But when that
undefinedis the ONLY thing the filter produced,convertFiltersToASTfalls to its general tail:— and hands back the caller's original OBJECT. So the group does not disappear at that level after all; it reappears one level up, in the
$dialect, in a slot the AST is expected to occupy.toFilterNodeandmergeFilterNodespass it straight through, and every consumer on that chain (ObjectGrid'sschemaFilter,plugin-list'sbuildEffectiveFilter,plugin-view'sObjectView) drops it onto$filter.Consequence, per consumer
provider: 'object'(the wire).@object-ui/data-objectstackroutes an object$filterthrough this same function and then gates onisFilterAST. These three shapes do not pass it, so a filter whose ruled answer is EVERY row (objectstack#5322, merged objectstack#5365) is refused instead of being dropped as no-constraint.provider: 'value'. Answered ZERO rows until objectui#8513; correct as of that card, which is why this one is about the producer and not about the matcher.The decision this needs
Returning
undefinedfromtoFilterNodefor a TRUE-identity filter is the obvious shape and is NOT obviously safe:convertFiltersToASTis exported and its declared return type includesRecord<string, any>, theconditions.length === 0tail also serves inputs that are not combinators at all (an all-null filter,{}), andundefinedvs. the original object is exactly how some callers tell "no constraint" from "unlowerable". Whoever takes this should decide whether the identity fold belongs inlowerLogicalGroup's caller, intoFilterNode, or in a distinguishable return value — not pick one in passing.Relationship to neighbours
Same function as objectui#8567 / #8568 / #8530 / #8555, and a residual of objectui#6948 (which added the
$and/$orbranch). Four different defect shapes with four different fixes — ⛔ not foldable; they serialise.Dedup
Run, instrument validated with a control in the same session.
search_issues(repo-scoped, semantic) over theconvertFiltersToASTneighbourhood returns objectui#8567, #8568, #8530, #6948, #8555 and #7752 — 9 total, none of them this shape. Closed cards were in range.Generated by Claude Code