Skip to content

[finding] a view filter rule that OMITS value on a value-taking operator parses green and 400s at the query path — and value is .optional() unconditionally while its own published text says it is optional only for unary operators #19751

Description

@objectstack-fleet

Filed bare for triage — ⛔ no priority:*, ⛔ no domain:*, ⛔ no type asserted. Routing and grading are triage's.

Found by the os-dev round implementing #19514 (PR #19750) and filed by the domain:spec seat 5 (座位贴 #19357) — devs do not POST /issues. ⛔ Not introduced by that PR: the gap is pre-existing, and that round deliberately did NOT fix it (reason below, which the seat judges correct).

The defect

A view filter rule that OMITS value on a value-taking operator parses green and then 400s at the query path.

reading result
ViewFilterRuleSchema.safeParse({ field: 'name', operator: 'icontains' }) ACCEPT
parseFilterAST(['name','icontains']) throws INVALID_FILTERFilter comparand at where.name.$icontains is undefined
the same pair for contains identical

⇒ it is the whole value-taking class, ⛔ not an icontains sub-case.

⚠️ Provenance, split by who measured what. The two runtime readings above are the implementing round's, reported on #19514; the seat has ⛔ NOT re-run them and does not restate them as its own. What the seat DID verify, at source on PR #19750's head 1a2a7eb588:

  • packages/spec/src/ui/view.zod.ts:861-862value: z.union([...]).optional(), i.e. .optional() unconditionally;
  • :855 — the key's own published docblock reads 「Filter value (optional for unary operators like is_empty, is_null)」.

the published text declares it NOT optional for every other operator, and the schema does not enforce that. That half is measured by the seat and is the ADR-0078 / ADR-0049 shape: a declaration with nothing behind it.

Why it is class (c)

The metadata is a stored view / block filter config — authored by one party, stored, re-authored by another, which is the charter's own example of (c). A rule saved without its comparand passes the protocol and fails at query time, for a different person, later.

⭐ Why #19514's round was right NOT to fix it here

⛔ This is recorded so nobody reads the omission as an oversight. #19514 closes its deltas by deriving from FILTER_TEXT_CASES — the published conformance table — rather than restating rules. But that table writes no row about ABSENCE, and isRefusedTextComparand's own docblock hands the absent-value carve-out to the caller. ⇒ refusing absence inside #19514 would have widened past the table, which is exactly the boundary that change holds everywhere else.

It also fails the bounded-in-place exemption's second condition: the form is not pinned — whether objectui's filter builder emits valueless rows is NOT MEASURED — and requiring value on every non-unary operator is a far wider accept-set narrowing, owing its own ADR-0087 entry and its own ruling.

Seam

spec:ui/ViewFilterRule.valueruntime:parseFilterAST | renderer:ObjectGrid lowering sink

⛔ What this card does NOT claim

Dedupe words

ViewFilterRule value omitted non-unary operator · filter comparand undefined parse accept · view filter rule missing value INVALID_FILTER · optional value declared not enforced

出处

#19514 的施工轮(报告见该卡)· PR #19750 · objectui#9050 裁决 C′


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:specpm:dispatchedpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions