convertFiltersToAST has no branch for $and / $or / $not, so spec-legal FilterCondition combinators never reach the wire #6948

Description

@claude

Split out of #4664 (PR #6946). Pre-existing on every consumer of the shared
filter sink; that card newly makes it reachable by an author following the
spec's own type, which is why it is being written down now.

What happens

convertFiltersToAST (packages/core/src/utils/filter-converter.ts) — the
repo's ONE lowering from the MongoDB-style filter object to the ObjectQL AST,
reached through toFilterNode / mergeFilterNodes — has no branch for the
logical combinators. Measured against packages/core/dist at
40c479af2, composing each condition with a parent scope:

$or -> ["and",["task_version","=","tv-1"],["$or","=",[{"status":"open"},{"status":"blocked"}]]]
$and -> ["and",["task_version","=","tv-1"],["$and","=",[{"status":"open"},{"is_active":true}]]]
$not -> THROWS FilterOperatorError: Unknown filter operator 'status' for field '$not'.
plain -> ["and",["task_version","=","tv-1"],["status","!=","archived"]] (correct)

Two distinct failures, neither of them the right answer:

  • $and / $or fall through to the simple-equality branch (their value is
    an array, so the operator loop is skipped) and produce a leaf naming a field
    literally called $and / $or. That is a well-formed AST node carrying a
    nonsense field, so the server refuses it (400 INVALID_FILTER) — loud, but
    the diagnostic points at the author's field list rather than at the missing
    lowering.
  • $not enters the operator loop with its OWN nested object's keys read as
    operators, so the thrown message names a nonsense operator
    (Unknown filter operator 'status' for field '$not') and lists the supported
    operators — none of which is what the author needs to hear.

Why it matters now

Field.relatedListFilter (@objectstack/spec 17.1.0, objectstack#8704 /
PR #8955) is typed FilterConditionSchema, and FilterCondition explicitly
declares all three:

$and?: FilterCondition[];
$or?: FilterCondition[];
$not?: FilterCondition;// NULL-safe per #5146

So the spec tells a metadata author these are legal in the very key #4664 wired
up, while this repo's lowering cannot carry any of them to the wire. The same
gap applies to every other producer that reaches this sink — the component-level
record:related_list.filter (objectstack#7118), ListView.filter /
ViewTab.filter via buildEffectiveFilter, and plugin-view's ObjectView —
so this is a sink-level bug, not a related-list one.

Also worth deciding as part of the fix: $not is NULL-safe by ruling
(objectstack#5146, maintainer 2026-08-04 — NOT (…) OR col IS NULL, with the
guard on each LEAF inside the negation, not hoisted). Whatever lowering is
chosen has to preserve that or hand the negation to the server intact.

Why it was not fixed in #4664

Changing the shared sink changes what every one of those callers puts on the
wire, so it needs its own card, its own fixture set and its own reverse
verification. Working around it renderer-side — accepting the combinators in
one consumer and lowering them there — is precisely the second de-facto
contract AGENTS.md #0.1 forbids, so #4664 carried the value verbatim to the
existing sink and named this boundary in its PR body instead.

Where

  • packages/core/src/utils/filter-converter.tsconvertFiltersToAST
    (the missing combinator branch), toFilterNode, mergeFilterNodes

Filed unassigned by the #4664 execution seat. No dedupe hit: a targeted issue
search for the combinator lowering returned only objectui#4744 (a different
function, ListView.convertFilterGroupToAST, closed), with a control query in
the same session returning #4664 itself.


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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      convertFiltersToAST has no branch for $and / $or / $not, so spec-legal FilterCondition combinators never reach the wire #6948

      Description

      @claude

      Split out of #4664 (PR #6946). Pre-existing on every consumer of the shared
      filter sink; that card newly makes it reachable by an author following the
      spec's own type, which is why it is being written down now.

      What happens

      convertFiltersToAST (packages/core/src/utils/filter-converter.ts) — the
      repo's ONE lowering from the MongoDB-style filter object to the ObjectQL AST,
      reached through toFilterNode / mergeFilterNodes — has no branch for the
      logical combinators. Measured against packages/core/dist at
      40c479af2, composing each condition with a parent scope:

      $or -> ["and",["task_version","=","tv-1"],["$or","=",[{"status":"open"},{"status":"blocked"}]]]
      $and -> ["and",["task_version","=","tv-1"],["$and","=",[{"status":"open"},{"is_active":true}]]]
      $not -> THROWS FilterOperatorError: Unknown filter operator 'status' for field '$not'.
      plain -> ["and",["task_version","=","tv-1"],["status","!=","archived"]] (correct)
      

      Two distinct failures, neither of them the right answer:

      • $and / $or fall through to the simple-equality branch (their value is
        an array, so the operator loop is skipped) and produce a leaf naming a field
        literally called $and / $or. That is a well-formed AST node carrying a
        nonsense field, so the server refuses it (400 INVALID_FILTER) — loud, but
        the diagnostic points at the author's field list rather than at the missing
        lowering.
      • $not enters the operator loop with its OWN nested object's keys read as
        operators, so the thrown message names a nonsense operator
        (Unknown filter operator 'status' for field '$not') and lists the supported
        operators — none of which is what the author needs to hear.

      Why it matters now

      Field.relatedListFilter (@objectstack/spec 17.1.0, objectstack#8704 /
      PR #8955) is typed FilterConditionSchema, and FilterCondition explicitly
      declares all three:

      $and?: FilterCondition[];
      $or?: FilterCondition[];
      $not?: FilterCondition;// NULL-safe per #5146

      So the spec tells a metadata author these are legal in the very key #4664 wired
      up, while this repo's lowering cannot carry any of them to the wire. The same
      gap applies to every other producer that reaches this sink — the component-level
      record:related_list.filter (objectstack#7118), ListView.filter /
      ViewTab.filter via buildEffectiveFilter, and plugin-view's ObjectView —
      so this is a sink-level bug, not a related-list one.

      Also worth deciding as part of the fix: $not is NULL-safe by ruling
      (objectstack#5146, maintainer 2026-08-04 — NOT (…) OR col IS NULL, with the
      guard on each LEAF inside the negation, not hoisted). Whatever lowering is
      chosen has to preserve that or hand the negation to the server intact.

      Why it was not fixed in #4664

      Changing the shared sink changes what every one of those callers puts on the
      wire, so it needs its own card, its own fixture set and its own reverse
      verification. Working around it renderer-side — accepting the combinators in
      one consumer and lowering them there — is precisely the second de-facto
      contract AGENTS.md #0.1 forbids, so #4664 carried the value verbatim to the
      existing sink and named this boundary in its PR body instead.

      Where

      • packages/core/src/utils/filter-converter.tsconvertFiltersToAST
        (the missing combinator branch), toFilterNode, mergeFilterNodes

      Filed unassigned by the #4664 execution seat. No dedupe hit: a targeted issue
      search for the combinator lowering returned only objectui#4744 (a different
      function, ListView.convertFilterGroupToAST, closed), with a control query in
      the same session returning #4664 itself.


      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

      No one assigned

        Labels

        No labels
        No labels

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          convertFiltersToAST has no branch for $and / $or / $not, so spec-legal FilterCondition combinators never reach the wire #6948

          Description

          @claude

          Split out of #4664 (PR #6946). Pre-existing on every consumer of the shared
          filter sink; that card newly makes it reachable by an author following the
          spec's own type, which is why it is being written down now.

          What happens

          convertFiltersToAST (packages/core/src/utils/filter-converter.ts) — the
          repo's ONE lowering from the MongoDB-style filter object to the ObjectQL AST,
          reached through toFilterNode / mergeFilterNodes — has no branch for the
          logical combinators. Measured against packages/core/dist at
          40c479af2, composing each condition with a parent scope:

          $or -> ["and",["task_version","=","tv-1"],["$or","=",[{"status":"open"},{"status":"blocked"}]]]
          $and -> ["and",["task_version","=","tv-1"],["$and","=",[{"status":"open"},{"is_active":true}]]]
          $not -> THROWS FilterOperatorError: Unknown filter operator 'status' for field '$not'.
          plain -> ["and",["task_version","=","tv-1"],["status","!=","archived"]] (correct)
          

          Two distinct failures, neither of them the right answer:

          • $and / $or fall through to the simple-equality branch (their value is
            an array, so the operator loop is skipped) and produce a leaf naming a field
            literally called $and / $or. That is a well-formed AST node carrying a
            nonsense field, so the server refuses it (400 INVALID_FILTER) — loud, but
            the diagnostic points at the author's field list rather than at the missing
            lowering.
          • $not enters the operator loop with its OWN nested object's keys read as
            operators, so the thrown message names a nonsense operator
            (Unknown filter operator 'status' for field '$not') and lists the supported
            operators — none of which is what the author needs to hear.

          Why it matters now

          Field.relatedListFilter (@objectstack/spec 17.1.0, objectstack#8704 /
          PR #8955) is typed FilterConditionSchema, and FilterCondition explicitly
          declares all three:

          $and?: FilterCondition[];
          $or?: FilterCondition[];
          $not?: FilterCondition;// NULL-safe per #5146

          So the spec tells a metadata author these are legal in the very key #4664 wired
          up, while this repo's lowering cannot carry any of them to the wire. The same
          gap applies to every other producer that reaches this sink — the component-level
          record:related_list.filter (objectstack#7118), ListView.filter /
          ViewTab.filter via buildEffectiveFilter, and plugin-view's ObjectView —
          so this is a sink-level bug, not a related-list one.

          Also worth deciding as part of the fix: $not is NULL-safe by ruling
          (objectstack#5146, maintainer 2026-08-04 — NOT (…) OR col IS NULL, with the
          guard on each LEAF inside the negation, not hoisted). Whatever lowering is
          chosen has to preserve that or hand the negation to the server intact.

          Why it was not fixed in #4664

          Changing the shared sink changes what every one of those callers puts on the
          wire, so it needs its own card, its own fixture set and its own reverse
          verification. Working around it renderer-side — accepting the combinators in
          one consumer and lowering them there — is precisely the second de-facto
          contract AGENTS.md #0.1 forbids, so #4664 carried the value verbatim to the
          existing sink and named this boundary in its PR body instead.

          Where

          • packages/core/src/utils/filter-converter.tsconvertFiltersToAST
            (the missing combinator branch), toFilterNode, mergeFilterNodes

          Filed unassigned by the #4664 execution seat. No dedupe hit: a targeted issue
          search for the combinator lowering returned only objectui#4744 (a different
          function, ListView.convertFilterGroupToAST, closed), with a control query in
          the same session returning #4664 itself.


          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

          No one assigned

            Labels

            No labels
            No labels

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              convertFiltersToAST has no branch for $and / $or / $not, so spec-legal FilterCondition combinators never reach the wire #6948

              Description

              @claude

              Split out of #4664 (PR #6946). Pre-existing on every consumer of the shared
              filter sink; that card newly makes it reachable by an author following the
              spec's own type, which is why it is being written down now.

              What happens

              convertFiltersToAST (packages/core/src/utils/filter-converter.ts) — the
              repo's ONE lowering from the MongoDB-style filter object to the ObjectQL AST,
              reached through toFilterNode / mergeFilterNodes — has no branch for the
              logical combinators. Measured against packages/core/dist at
              40c479af2, composing each condition with a parent scope:

              $or -> ["and",["task_version","=","tv-1"],["$or","=",[{"status":"open"},{"status":"blocked"}]]]
              $and -> ["and",["task_version","=","tv-1"],["$and","=",[{"status":"open"},{"is_active":true}]]]
              $not -> THROWS FilterOperatorError: Unknown filter operator 'status' for field '$not'.
              plain -> ["and",["task_version","=","tv-1"],["status","!=","archived"]] (correct)
              

              Two distinct failures, neither of them the right answer:

              • $and / $or fall through to the simple-equality branch (their value is
                an array, so the operator loop is skipped) and produce a leaf naming a field
                literally called $and / $or. That is a well-formed AST node carrying a
                nonsense field, so the server refuses it (400 INVALID_FILTER) — loud, but
                the diagnostic points at the author's field list rather than at the missing
                lowering.
              • $not enters the operator loop with its OWN nested object's keys read as
                operators, so the thrown message names a nonsense operator
                (Unknown filter operator 'status' for field '$not') and lists the supported
                operators — none of which is what the author needs to hear.

              Why it matters now

              Field.relatedListFilter (@objectstack/spec 17.1.0, objectstack#8704 /
              PR #8955) is typed FilterConditionSchema, and FilterCondition explicitly
              declares all three:

              $and?: FilterCondition[];
              $or?: FilterCondition[];
              $not?: FilterCondition;// NULL-safe per #5146

              So the spec tells a metadata author these are legal in the very key #4664 wired
              up, while this repo's lowering cannot carry any of them to the wire. The same
              gap applies to every other producer that reaches this sink — the component-level
              record:related_list.filter (objectstack#7118), ListView.filter /
              ViewTab.filter via buildEffectiveFilter, and plugin-view's ObjectView —
              so this is a sink-level bug, not a related-list one.

              Also worth deciding as part of the fix: $not is NULL-safe by ruling
              (objectstack#5146, maintainer 2026-08-04 — NOT (…) OR col IS NULL, with the
              guard on each LEAF inside the negation, not hoisted). Whatever lowering is
              chosen has to preserve that or hand the negation to the server intact.

              Why it was not fixed in #4664

              Changing the shared sink changes what every one of those callers puts on the
              wire, so it needs its own card, its own fixture set and its own reverse
              verification. Working around it renderer-side — accepting the combinators in
              one consumer and lowering them there — is precisely the second de-facto
              contract AGENTS.md #0.1 forbids, so #4664 carried the value verbatim to the
              existing sink and named this boundary in its PR body instead.

              Where

              • packages/core/src/utils/filter-converter.tsconvertFiltersToAST
                (the missing combinator branch), toFilterNode, mergeFilterNodes

              Filed unassigned by the #4664 execution seat. No dedupe hit: a targeted issue
              search for the combinator lowering returned only objectui#4744 (a different
              function, ListView.convertFilterGroupToAST, closed), with a control query in
              the same session returning #4664 itself.


              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

              No one assigned

                Labels

                No labels
                No labels

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  convertFiltersToAST has no branch for $and / $or / $not, so spec-legal FilterCondition combinators never reach the wire #6948

                  Description

                  @claude

                  Split out of #4664 (PR #6946). Pre-existing on every consumer of the shared
                  filter sink; that card newly makes it reachable by an author following the
                  spec's own type, which is why it is being written down now.

                  What happens

                  convertFiltersToAST (packages/core/src/utils/filter-converter.ts) — the
                  repo's ONE lowering from the MongoDB-style filter object to the ObjectQL AST,
                  reached through toFilterNode / mergeFilterNodes — has no branch for the
                  logical combinators. Measured against packages/core/dist at
                  40c479af2, composing each condition with a parent scope:

                  $or -> ["and",["task_version","=","tv-1"],["$or","=",[{"status":"open"},{"status":"blocked"}]]]
                  $and -> ["and",["task_version","=","tv-1"],["$and","=",[{"status":"open"},{"is_active":true}]]]
                  $not -> THROWS FilterOperatorError: Unknown filter operator 'status' for field '$not'.
                  plain -> ["and",["task_version","=","tv-1"],["status","!=","archived"]] (correct)
                  

                  Two distinct failures, neither of them the right answer:

                  • $and / $or fall through to the simple-equality branch (their value is
                    an array, so the operator loop is skipped) and produce a leaf naming a field
                    literally called $and / $or. That is a well-formed AST node carrying a
                    nonsense field, so the server refuses it (400 INVALID_FILTER) — loud, but
                    the diagnostic points at the author's field list rather than at the missing
                    lowering.
                  • $not enters the operator loop with its OWN nested object's keys read as
                    operators, so the thrown message names a nonsense operator
                    (Unknown filter operator 'status' for field '$not') and lists the supported
                    operators — none of which is what the author needs to hear.

                  Why it matters now

                  Field.relatedListFilter (@objectstack/spec 17.1.0, objectstack#8704 /
                  PR #8955) is typed FilterConditionSchema, and FilterCondition explicitly
                  declares all three:

                  $and?: FilterCondition[];
                  $or?: FilterCondition[];
                  $not?: FilterCondition;// NULL-safe per #5146

                  So the spec tells a metadata author these are legal in the very key #4664 wired
                  up, while this repo's lowering cannot carry any of them to the wire. The same
                  gap applies to every other producer that reaches this sink — the component-level
                  record:related_list.filter (objectstack#7118), ListView.filter /
                  ViewTab.filter via buildEffectiveFilter, and plugin-view's ObjectView —
                  so this is a sink-level bug, not a related-list one.

                  Also worth deciding as part of the fix: $not is NULL-safe by ruling
                  (objectstack#5146, maintainer 2026-08-04 — NOT (…) OR col IS NULL, with the
                  guard on each LEAF inside the negation, not hoisted). Whatever lowering is
                  chosen has to preserve that or hand the negation to the server intact.

                  Why it was not fixed in #4664

                  Changing the shared sink changes what every one of those callers puts on the
                  wire, so it needs its own card, its own fixture set and its own reverse
                  verification. Working around it renderer-side — accepting the combinators in
                  one consumer and lowering them there — is precisely the second de-facto
                  contract AGENTS.md #0.1 forbids, so #4664 carried the value verbatim to the
                  existing sink and named this boundary in its PR body instead.

                  Where

                  • packages/core/src/utils/filter-converter.tsconvertFiltersToAST
                    (the missing combinator branch), toFilterNode, mergeFilterNodes

                  Filed unassigned by the #4664 execution seat. No dedupe hit: a targeted issue
                  search for the combinator lowering returned only objectui#4744 (a different
                  function, ListView.convertFilterGroupToAST, closed), with a control query in
                  the same session returning #4664 itself.


                  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

                  No one assigned

                    Labels

                    No labels
                    No labels

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      convertFiltersToAST has no branch for $and / $or / $not, so spec-legal FilterCondition combinators never reach the wire #6948

                      Description

                      @claude

                      Split out of #4664 (PR #6946). Pre-existing on every consumer of the shared
                      filter sink; that card newly makes it reachable by an author following the
                      spec's own type, which is why it is being written down now.

                      What happens

                      convertFiltersToAST (packages/core/src/utils/filter-converter.ts) — the
                      repo's ONE lowering from the MongoDB-style filter object to the ObjectQL AST,
                      reached through toFilterNode / mergeFilterNodes — has no branch for the
                      logical combinators. Measured against packages/core/dist at
                      40c479af2, composing each condition with a parent scope:

                      $or -> ["and",["task_version","=","tv-1"],["$or","=",[{"status":"open"},{"status":"blocked"}]]]
                      $and -> ["and",["task_version","=","tv-1"],["$and","=",[{"status":"open"},{"is_active":true}]]]
                      $not -> THROWS FilterOperatorError: Unknown filter operator 'status' for field '$not'.
                      plain -> ["and",["task_version","=","tv-1"],["status","!=","archived"]] (correct)
                      

                      Two distinct failures, neither of them the right answer:

                      • $and / $or fall through to the simple-equality branch (their value is
                        an array, so the operator loop is skipped) and produce a leaf naming a field
                        literally called $and / $or. That is a well-formed AST node carrying a
                        nonsense field, so the server refuses it (400 INVALID_FILTER) — loud, but
                        the diagnostic points at the author's field list rather than at the missing
                        lowering.
                      • $not enters the operator loop with its OWN nested object's keys read as
                        operators, so the thrown message names a nonsense operator
                        (Unknown filter operator 'status' for field '$not') and lists the supported
                        operators — none of which is what the author needs to hear.

                      Why it matters now

                      Field.relatedListFilter (@objectstack/spec 17.1.0, objectstack#8704 /
                      PR #8955) is typed FilterConditionSchema, and FilterCondition explicitly
                      declares all three:

                      $and?: FilterCondition[];
                      $or?: FilterCondition[];
                      $not?: FilterCondition;// NULL-safe per #5146

                      So the spec tells a metadata author these are legal in the very key #4664 wired
                      up, while this repo's lowering cannot carry any of them to the wire. The same
                      gap applies to every other producer that reaches this sink — the component-level
                      record:related_list.filter (objectstack#7118), ListView.filter /
                      ViewTab.filter via buildEffectiveFilter, and plugin-view's ObjectView —
                      so this is a sink-level bug, not a related-list one.

                      Also worth deciding as part of the fix: $not is NULL-safe by ruling
                      (objectstack#5146, maintainer 2026-08-04 — NOT (…) OR col IS NULL, with the
                      guard on each LEAF inside the negation, not hoisted). Whatever lowering is
                      chosen has to preserve that or hand the negation to the server intact.

                      Why it was not fixed in #4664

                      Changing the shared sink changes what every one of those callers puts on the
                      wire, so it needs its own card, its own fixture set and its own reverse
                      verification. Working around it renderer-side — accepting the combinators in
                      one consumer and lowering them there — is precisely the second de-facto
                      contract AGENTS.md #0.1 forbids, so #4664 carried the value verbatim to the
                      existing sink and named this boundary in its PR body instead.

                      Where

                      • packages/core/src/utils/filter-converter.tsconvertFiltersToAST
                        (the missing combinator branch), toFilterNode, mergeFilterNodes

                      Filed unassigned by the #4664 execution seat. No dedupe hit: a targeted issue
                      search for the combinator lowering returned only objectui#4744 (a different
                      function, ListView.convertFilterGroupToAST, closed), with a control query in
                      the same session returning #4664 itself.


                      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

                      No one assigned

                        Labels

                        No labels
                        No labels

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          convertFiltersToAST has no branch for $and / $or / $not, so spec-legal FilterCondition combinators never reach the wire #6948

                          Description

                          @claude

                          Split out of #4664 (PR #6946). Pre-existing on every consumer of the shared
                          filter sink; that card newly makes it reachable by an author following the
                          spec's own type, which is why it is being written down now.

                          What happens

                          convertFiltersToAST (packages/core/src/utils/filter-converter.ts) — the
                          repo's ONE lowering from the MongoDB-style filter object to the ObjectQL AST,
                          reached through toFilterNode / mergeFilterNodes — has no branch for the
                          logical combinators. Measured against packages/core/dist at
                          40c479af2, composing each condition with a parent scope:

                          $or -> ["and",["task_version","=","tv-1"],["$or","=",[{"status":"open"},{"status":"blocked"}]]]
                          $and -> ["and",["task_version","=","tv-1"],["$and","=",[{"status":"open"},{"is_active":true}]]]
                          $not -> THROWS FilterOperatorError: Unknown filter operator 'status' for field '$not'.
                          plain -> ["and",["task_version","=","tv-1"],["status","!=","archived"]] (correct)
                          

                          Two distinct failures, neither of them the right answer:

                          • $and / $or fall through to the simple-equality branch (their value is
                            an array, so the operator loop is skipped) and produce a leaf naming a field
                            literally called $and / $or. That is a well-formed AST node carrying a
                            nonsense field, so the server refuses it (400 INVALID_FILTER) — loud, but
                            the diagnostic points at the author's field list rather than at the missing
                            lowering.
                          • $not enters the operator loop with its OWN nested object's keys read as
                            operators, so the thrown message names a nonsense operator
                            (Unknown filter operator 'status' for field '$not') and lists the supported
                            operators — none of which is what the author needs to hear.

                          Why it matters now

                          Field.relatedListFilter (@objectstack/spec 17.1.0, objectstack#8704 /
                          PR #8955) is typed FilterConditionSchema, and FilterCondition explicitly
                          declares all three:

                          $and?: FilterCondition[];
                          $or?: FilterCondition[];
                          $not?: FilterCondition;// NULL-safe per #5146

                          So the spec tells a metadata author these are legal in the very key #4664 wired
                          up, while this repo's lowering cannot carry any of them to the wire. The same
                          gap applies to every other producer that reaches this sink — the component-level
                          record:related_list.filter (objectstack#7118), ListView.filter /
                          ViewTab.filter via buildEffectiveFilter, and plugin-view's ObjectView —
                          so this is a sink-level bug, not a related-list one.

                          Also worth deciding as part of the fix: $not is NULL-safe by ruling
                          (objectstack#5146, maintainer 2026-08-04 — NOT (…) OR col IS NULL, with the
                          guard on each LEAF inside the negation, not hoisted). Whatever lowering is
                          chosen has to preserve that or hand the negation to the server intact.

                          Why it was not fixed in #4664

                          Changing the shared sink changes what every one of those callers puts on the
                          wire, so it needs its own card, its own fixture set and its own reverse
                          verification. Working around it renderer-side — accepting the combinators in
                          one consumer and lowering them there — is precisely the second de-facto
                          contract AGENTS.md #0.1 forbids, so #4664 carried the value verbatim to the
                          existing sink and named this boundary in its PR body instead.

                          Where

                          • packages/core/src/utils/filter-converter.tsconvertFiltersToAST
                            (the missing combinator branch), toFilterNode, mergeFilterNodes

                          Filed unassigned by the #4664 execution seat. No dedupe hit: a targeted issue
                          search for the combinator lowering returned only objectui#4744 (a different
                          function, ListView.convertFilterGroupToAST, closed), with a control query in
                          the same session returning #4664 itself.


                          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

                          No one assigned

                            Labels

                            No labels
                            No labels

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              convertFiltersToAST has no branch for $and / $or / $not, so spec-legal FilterCondition combinators never reach the wire #6948

                              Description

                              @claude

                              Split out of #4664 (PR #6946). Pre-existing on every consumer of the shared
                              filter sink; that card newly makes it reachable by an author following the
                              spec's own type, which is why it is being written down now.

                              What happens

                              convertFiltersToAST (packages/core/src/utils/filter-converter.ts) — the
                              repo's ONE lowering from the MongoDB-style filter object to the ObjectQL AST,
                              reached through toFilterNode / mergeFilterNodes — has no branch for the
                              logical combinators. Measured against packages/core/dist at
                              40c479af2, composing each condition with a parent scope:

                              $or -> ["and",["task_version","=","tv-1"],["$or","=",[{"status":"open"},{"status":"blocked"}]]]
                              $and -> ["and",["task_version","=","tv-1"],["$and","=",[{"status":"open"},{"is_active":true}]]]
                              $not -> THROWS FilterOperatorError: Unknown filter operator 'status' for field '$not'.
                              plain -> ["and",["task_version","=","tv-1"],["status","!=","archived"]] (correct)
                              

                              Two distinct failures, neither of them the right answer:

                              • $and / $or fall through to the simple-equality branch (their value is
                                an array, so the operator loop is skipped) and produce a leaf naming a field
                                literally called $and / $or. That is a well-formed AST node carrying a
                                nonsense field, so the server refuses it (400 INVALID_FILTER) — loud, but
                                the diagnostic points at the author's field list rather than at the missing
                                lowering.
                              • $not enters the operator loop with its OWN nested object's keys read as
                                operators, so the thrown message names a nonsense operator
                                (Unknown filter operator 'status' for field '$not') and lists the supported
                                operators — none of which is what the author needs to hear.

                              Why it matters now

                              Field.relatedListFilter (@objectstack/spec 17.1.0, objectstack#8704 /
                              PR #8955) is typed FilterConditionSchema, and FilterCondition explicitly
                              declares all three:

                              $and?: FilterCondition[];
                              $or?: FilterCondition[];
                              $not?: FilterCondition;// NULL-safe per #5146

                              So the spec tells a metadata author these are legal in the very key #4664 wired
                              up, while this repo's lowering cannot carry any of them to the wire. The same
                              gap applies to every other producer that reaches this sink — the component-level
                              record:related_list.filter (objectstack#7118), ListView.filter /
                              ViewTab.filter via buildEffectiveFilter, and plugin-view's ObjectView —
                              so this is a sink-level bug, not a related-list one.

                              Also worth deciding as part of the fix: $not is NULL-safe by ruling
                              (objectstack#5146, maintainer 2026-08-04 — NOT (…) OR col IS NULL, with the
                              guard on each LEAF inside the negation, not hoisted). Whatever lowering is
                              chosen has to preserve that or hand the negation to the server intact.

                              Why it was not fixed in #4664

                              Changing the shared sink changes what every one of those callers puts on the
                              wire, so it needs its own card, its own fixture set and its own reverse
                              verification. Working around it renderer-side — accepting the combinators in
                              one consumer and lowering them there — is precisely the second de-facto
                              contract AGENTS.md #0.1 forbids, so #4664 carried the value verbatim to the
                              existing sink and named this boundary in its PR body instead.

                              Where

                              • packages/core/src/utils/filter-converter.tsconvertFiltersToAST
                                (the missing combinator branch), toFilterNode, mergeFilterNodes

                              Filed unassigned by the #4664 execution seat. No dedupe hit: a targeted issue
                              search for the combinator lowering returned only objectui#4744 (a different
                              function, ListView.convertFilterGroupToAST, closed), with a control query in
                              the same session returning #4664 itself.


                              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

                              No one assigned

                                Labels

                                No labels
                                No labels

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions