[finding] the published explain schema names only one zero-rows payload sentinel, and explain can now publish the other #13961

Description

@claude

Recording only, no severity asserted — routing and ranking are triage's. Found while
implementing slice 2 of #13639 (PR #13960); deliberately NOT repaired there, because the
repair lands in packages/spec and that card's scope is explain's computation, not its
schema.

Measured

packages/spec/src/security/explain.zod.ts documents the zero-rows payload vocabulary as
a two-item enumeration, in two places:

  • ExplainDecisionSchema.readFilter"the composed row filter the caller would be
    served with … (null = unrestricted, { id: '__deny_all__' } = zero rows)"
  • ExplainRecordAttributionSchema.rowFilter — same shape, both in the JSDoc and in the
    .describe() string, and the .describe() is mirrored verbatim into published docs at
    content/docs/references/security/explain.mdx (two rows).

That enumeration is now incomplete. As of PR #13960, explain reports verdict denies
and decision.allowed: false for a fail-closed RLS denial, whose readFilter /
rowFilter is plugin-security's RLS_DENY_FILTER — spelled __rls_deny__ followed by a
colon and a UUID-shaped string. That payload is published as composed (deliberately —
not rewriting it is the boundary between options A and B on #13639), so a consumer reading
the schema prose sees a zero-rows shape the prose does not name.

Why it is small

⛔ No consumer is misled about the decision: the sibling verdict field says denies
and decision.allowed is false, so a reader who consults them gets the right answer.
The gap is that the payload prose under-describes the vocabulary — incomplete, not wrong.
A consumer pattern-matching on the payload alone to detect "zero rows" would miss this
shape, and that is the only measured exposure.

Why it is worth recording anyway

The schema prose is the published contract for a diagnostic surface whose entire purpose
is telling an operator the truth about a request. "The one enumeration a reader would
trust is missing a member" is exactly the shape that costs someone an hour later, and it
is cheap to close.

Scope NOT established

Related

#13639 (the card this fell out of) · PR #13960 (slice 2, which made the shape reachable
with verdict denies) · PR #13942 (slice 1) · ADR-0123 D2

Generated by Claude Code


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    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

      [finding] the published explain schema names only one zero-rows payload sentinel, and explain can now publish the other #13961

      Description

      @claude

      Recording only, no severity asserted — routing and ranking are triage's. Found while
      implementing slice 2 of #13639 (PR #13960); deliberately NOT repaired there, because the
      repair lands in packages/spec and that card's scope is explain's computation, not its
      schema.

      Measured

      packages/spec/src/security/explain.zod.ts documents the zero-rows payload vocabulary as
      a two-item enumeration, in two places:

      • ExplainDecisionSchema.readFilter"the composed row filter the caller would be
        served with … (null = unrestricted, { id: '__deny_all__' } = zero rows)"
      • ExplainRecordAttributionSchema.rowFilter — same shape, both in the JSDoc and in the
        .describe() string, and the .describe() is mirrored verbatim into published docs at
        content/docs/references/security/explain.mdx (two rows).

      That enumeration is now incomplete. As of PR #13960, explain reports verdict denies
      and decision.allowed: false for a fail-closed RLS denial, whose readFilter /
      rowFilter is plugin-security's RLS_DENY_FILTER — spelled __rls_deny__ followed by a
      colon and a UUID-shaped string. That payload is published as composed (deliberately —
      not rewriting it is the boundary between options A and B on #13639), so a consumer reading
      the schema prose sees a zero-rows shape the prose does not name.

      Why it is small

      ⛔ No consumer is misled about the decision: the sibling verdict field says denies
      and decision.allowed is false, so a reader who consults them gets the right answer.
      The gap is that the payload prose under-describes the vocabulary — incomplete, not wrong.
      A consumer pattern-matching on the payload alone to detect "zero rows" would miss this
      shape, and that is the only measured exposure.

      Why it is worth recording anyway

      The schema prose is the published contract for a diagnostic surface whose entire purpose
      is telling an operator the truth about a request. "The one enumeration a reader would
      trust is missing a member" is exactly the shape that costs someone an hour later, and it
      is cheap to close.

      Scope NOT established

      Related

      #13639 (the card this fell out of) · PR #13960 (slice 2, which made the shape reachable
      with verdict denies) · PR #13942 (slice 1) · ADR-0123 D2

      Generated by Claude Code


      Generated by Claude Code

      Metadata

      Metadata

      Assignees

      No one assigned

        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

          [finding] the published explain schema names only one zero-rows payload sentinel, and explain can now publish the other #13961

          Description

          @claude

          Recording only, no severity asserted — routing and ranking are triage's. Found while
          implementing slice 2 of #13639 (PR #13960); deliberately NOT repaired there, because the
          repair lands in packages/spec and that card's scope is explain's computation, not its
          schema.

          Measured

          packages/spec/src/security/explain.zod.ts documents the zero-rows payload vocabulary as
          a two-item enumeration, in two places:

          • ExplainDecisionSchema.readFilter"the composed row filter the caller would be
            served with … (null = unrestricted, { id: '__deny_all__' } = zero rows)"
          • ExplainRecordAttributionSchema.rowFilter — same shape, both in the JSDoc and in the
            .describe() string, and the .describe() is mirrored verbatim into published docs at
            content/docs/references/security/explain.mdx (two rows).

          That enumeration is now incomplete. As of PR #13960, explain reports verdict denies
          and decision.allowed: false for a fail-closed RLS denial, whose readFilter /
          rowFilter is plugin-security's RLS_DENY_FILTER — spelled __rls_deny__ followed by a
          colon and a UUID-shaped string. That payload is published as composed (deliberately —
          not rewriting it is the boundary between options A and B on #13639), so a consumer reading
          the schema prose sees a zero-rows shape the prose does not name.

          Why it is small

          ⛔ No consumer is misled about the decision: the sibling verdict field says denies
          and decision.allowed is false, so a reader who consults them gets the right answer.
          The gap is that the payload prose under-describes the vocabulary — incomplete, not wrong.
          A consumer pattern-matching on the payload alone to detect "zero rows" would miss this
          shape, and that is the only measured exposure.

          Why it is worth recording anyway

          The schema prose is the published contract for a diagnostic surface whose entire purpose
          is telling an operator the truth about a request. "The one enumeration a reader would
          trust is missing a member" is exactly the shape that costs someone an hour later, and it
          is cheap to close.

          Scope NOT established

          Related

          #13639 (the card this fell out of) · PR #13960 (slice 2, which made the shape reachable
          with verdict denies) · PR #13942 (slice 1) · ADR-0123 D2

          Generated by Claude Code


          Generated by Claude Code

          Metadata

          Metadata

          Assignees

          No one assigned

            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

              [finding] the published explain schema names only one zero-rows payload sentinel, and explain can now publish the other #13961

              Description

              @claude

              Recording only, no severity asserted — routing and ranking are triage's. Found while
              implementing slice 2 of #13639 (PR #13960); deliberately NOT repaired there, because the
              repair lands in packages/spec and that card's scope is explain's computation, not its
              schema.

              Measured

              packages/spec/src/security/explain.zod.ts documents the zero-rows payload vocabulary as
              a two-item enumeration, in two places:

              • ExplainDecisionSchema.readFilter"the composed row filter the caller would be
                served with … (null = unrestricted, { id: '__deny_all__' } = zero rows)"
              • ExplainRecordAttributionSchema.rowFilter — same shape, both in the JSDoc and in the
                .describe() string, and the .describe() is mirrored verbatim into published docs at
                content/docs/references/security/explain.mdx (two rows).

              That enumeration is now incomplete. As of PR #13960, explain reports verdict denies
              and decision.allowed: false for a fail-closed RLS denial, whose readFilter /
              rowFilter is plugin-security's RLS_DENY_FILTER — spelled __rls_deny__ followed by a
              colon and a UUID-shaped string. That payload is published as composed (deliberately —
              not rewriting it is the boundary between options A and B on #13639), so a consumer reading
              the schema prose sees a zero-rows shape the prose does not name.

              Why it is small

              ⛔ No consumer is misled about the decision: the sibling verdict field says denies
              and decision.allowed is false, so a reader who consults them gets the right answer.
              The gap is that the payload prose under-describes the vocabulary — incomplete, not wrong.
              A consumer pattern-matching on the payload alone to detect "zero rows" would miss this
              shape, and that is the only measured exposure.

              Why it is worth recording anyway

              The schema prose is the published contract for a diagnostic surface whose entire purpose
              is telling an operator the truth about a request. "The one enumeration a reader would
              trust is missing a member" is exactly the shape that costs someone an hour later, and it
              is cheap to close.

              Scope NOT established

              Related

              #13639 (the card this fell out of) · PR #13960 (slice 2, which made the shape reachable
              with verdict denies) · PR #13942 (slice 1) · ADR-0123 D2

              Generated by Claude Code


              Generated by Claude Code

              Metadata

              Metadata

              Assignees

              No one assigned

                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

                  [finding] the published explain schema names only one zero-rows payload sentinel, and explain can now publish the other #13961

                  Description

                  @claude

                  Recording only, no severity asserted — routing and ranking are triage's. Found while
                  implementing slice 2 of #13639 (PR #13960); deliberately NOT repaired there, because the
                  repair lands in packages/spec and that card's scope is explain's computation, not its
                  schema.

                  Measured

                  packages/spec/src/security/explain.zod.ts documents the zero-rows payload vocabulary as
                  a two-item enumeration, in two places:

                  • ExplainDecisionSchema.readFilter"the composed row filter the caller would be
                    served with … (null = unrestricted, { id: '__deny_all__' } = zero rows)"
                  • ExplainRecordAttributionSchema.rowFilter — same shape, both in the JSDoc and in the
                    .describe() string, and the .describe() is mirrored verbatim into published docs at
                    content/docs/references/security/explain.mdx (two rows).

                  That enumeration is now incomplete. As of PR #13960, explain reports verdict denies
                  and decision.allowed: false for a fail-closed RLS denial, whose readFilter /
                  rowFilter is plugin-security's RLS_DENY_FILTER — spelled __rls_deny__ followed by a
                  colon and a UUID-shaped string. That payload is published as composed (deliberately —
                  not rewriting it is the boundary between options A and B on #13639), so a consumer reading
                  the schema prose sees a zero-rows shape the prose does not name.

                  Why it is small

                  ⛔ No consumer is misled about the decision: the sibling verdict field says denies
                  and decision.allowed is false, so a reader who consults them gets the right answer.
                  The gap is that the payload prose under-describes the vocabulary — incomplete, not wrong.
                  A consumer pattern-matching on the payload alone to detect "zero rows" would miss this
                  shape, and that is the only measured exposure.

                  Why it is worth recording anyway

                  The schema prose is the published contract for a diagnostic surface whose entire purpose
                  is telling an operator the truth about a request. "The one enumeration a reader would
                  trust is missing a member" is exactly the shape that costs someone an hour later, and it
                  is cheap to close.

                  Scope NOT established

                  Related

                  #13639 (the card this fell out of) · PR #13960 (slice 2, which made the shape reachable
                  with verdict denies) · PR #13942 (slice 1) · ADR-0123 D2

                  Generated by Claude Code


                  Generated by Claude Code

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    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

                      [finding] the published explain schema names only one zero-rows payload sentinel, and explain can now publish the other #13961

                      Description

                      @claude

                      Recording only, no severity asserted — routing and ranking are triage's. Found while
                      implementing slice 2 of #13639 (PR #13960); deliberately NOT repaired there, because the
                      repair lands in packages/spec and that card's scope is explain's computation, not its
                      schema.

                      Measured

                      packages/spec/src/security/explain.zod.ts documents the zero-rows payload vocabulary as
                      a two-item enumeration, in two places:

                      • ExplainDecisionSchema.readFilter"the composed row filter the caller would be
                        served with … (null = unrestricted, { id: '__deny_all__' } = zero rows)"
                      • ExplainRecordAttributionSchema.rowFilter — same shape, both in the JSDoc and in the
                        .describe() string, and the .describe() is mirrored verbatim into published docs at
                        content/docs/references/security/explain.mdx (two rows).

                      That enumeration is now incomplete. As of PR #13960, explain reports verdict denies
                      and decision.allowed: false for a fail-closed RLS denial, whose readFilter /
                      rowFilter is plugin-security's RLS_DENY_FILTER — spelled __rls_deny__ followed by a
                      colon and a UUID-shaped string. That payload is published as composed (deliberately —
                      not rewriting it is the boundary between options A and B on #13639), so a consumer reading
                      the schema prose sees a zero-rows shape the prose does not name.

                      Why it is small

                      ⛔ No consumer is misled about the decision: the sibling verdict field says denies
                      and decision.allowed is false, so a reader who consults them gets the right answer.
                      The gap is that the payload prose under-describes the vocabulary — incomplete, not wrong.
                      A consumer pattern-matching on the payload alone to detect "zero rows" would miss this
                      shape, and that is the only measured exposure.

                      Why it is worth recording anyway

                      The schema prose is the published contract for a diagnostic surface whose entire purpose
                      is telling an operator the truth about a request. "The one enumeration a reader would
                      trust is missing a member" is exactly the shape that costs someone an hour later, and it
                      is cheap to close.

                      Scope NOT established

                      Related

                      #13639 (the card this fell out of) · PR #13960 (slice 2, which made the shape reachable
                      with verdict denies) · PR #13942 (slice 1) · ADR-0123 D2

                      Generated by Claude Code


                      Generated by Claude Code

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        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

                          [finding] the published explain schema names only one zero-rows payload sentinel, and explain can now publish the other #13961

                          Description

                          @claude

                          Recording only, no severity asserted — routing and ranking are triage's. Found while
                          implementing slice 2 of #13639 (PR #13960); deliberately NOT repaired there, because the
                          repair lands in packages/spec and that card's scope is explain's computation, not its
                          schema.

                          Measured

                          packages/spec/src/security/explain.zod.ts documents the zero-rows payload vocabulary as
                          a two-item enumeration, in two places:

                          • ExplainDecisionSchema.readFilter"the composed row filter the caller would be
                            served with … (null = unrestricted, { id: '__deny_all__' } = zero rows)"
                          • ExplainRecordAttributionSchema.rowFilter — same shape, both in the JSDoc and in the
                            .describe() string, and the .describe() is mirrored verbatim into published docs at
                            content/docs/references/security/explain.mdx (two rows).

                          That enumeration is now incomplete. As of PR #13960, explain reports verdict denies
                          and decision.allowed: false for a fail-closed RLS denial, whose readFilter /
                          rowFilter is plugin-security's RLS_DENY_FILTER — spelled __rls_deny__ followed by a
                          colon and a UUID-shaped string. That payload is published as composed (deliberately —
                          not rewriting it is the boundary between options A and B on #13639), so a consumer reading
                          the schema prose sees a zero-rows shape the prose does not name.

                          Why it is small

                          ⛔ No consumer is misled about the decision: the sibling verdict field says denies
                          and decision.allowed is false, so a reader who consults them gets the right answer.
                          The gap is that the payload prose under-describes the vocabulary — incomplete, not wrong.
                          A consumer pattern-matching on the payload alone to detect "zero rows" would miss this
                          shape, and that is the only measured exposure.

                          Why it is worth recording anyway

                          The schema prose is the published contract for a diagnostic surface whose entire purpose
                          is telling an operator the truth about a request. "The one enumeration a reader would
                          trust is missing a member" is exactly the shape that costs someone an hour later, and it
                          is cheap to close.

                          Scope NOT established

                          Related

                          #13639 (the card this fell out of) · PR #13960 (slice 2, which made the shape reachable
                          with verdict denies) · PR #13942 (slice 1) · ADR-0123 D2

                          Generated by Claude Code


                          Generated by Claude Code

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            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

                              [finding] the published explain schema names only one zero-rows payload sentinel, and explain can now publish the other #13961

                              Description

                              @claude

                              Recording only, no severity asserted — routing and ranking are triage's. Found while
                              implementing slice 2 of #13639 (PR #13960); deliberately NOT repaired there, because the
                              repair lands in packages/spec and that card's scope is explain's computation, not its
                              schema.

                              Measured

                              packages/spec/src/security/explain.zod.ts documents the zero-rows payload vocabulary as
                              a two-item enumeration, in two places:

                              • ExplainDecisionSchema.readFilter"the composed row filter the caller would be
                                served with … (null = unrestricted, { id: '__deny_all__' } = zero rows)"
                              • ExplainRecordAttributionSchema.rowFilter — same shape, both in the JSDoc and in the
                                .describe() string, and the .describe() is mirrored verbatim into published docs at
                                content/docs/references/security/explain.mdx (two rows).

                              That enumeration is now incomplete. As of PR #13960, explain reports verdict denies
                              and decision.allowed: false for a fail-closed RLS denial, whose readFilter /
                              rowFilter is plugin-security's RLS_DENY_FILTER — spelled __rls_deny__ followed by a
                              colon and a UUID-shaped string. That payload is published as composed (deliberately —
                              not rewriting it is the boundary between options A and B on #13639), so a consumer reading
                              the schema prose sees a zero-rows shape the prose does not name.

                              Why it is small

                              ⛔ No consumer is misled about the decision: the sibling verdict field says denies
                              and decision.allowed is false, so a reader who consults them gets the right answer.
                              The gap is that the payload prose under-describes the vocabulary — incomplete, not wrong.
                              A consumer pattern-matching on the payload alone to detect "zero rows" would miss this
                              shape, and that is the only measured exposure.

                              Why it is worth recording anyway

                              The schema prose is the published contract for a diagnostic surface whose entire purpose
                              is telling an operator the truth about a request. "The one enumeration a reader would
                              trust is missing a member" is exactly the shape that costs someone an hour later, and it
                              is cheap to close.

                              Scope NOT established

                              Related

                              #13639 (the card this fell out of) · PR #13960 (slice 2, which made the shape reachable
                              with verdict denies) · PR #13942 (slice 1) · ADR-0123 D2

                              Generated by Claude Code


                              Generated by Claude Code

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions