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
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/specand that card's scope isexplain's computation, not itsschema.
Measured
packages/spec/src/security/explain.zod.tsdocuments the zero-rows payload vocabulary asa two-item enumeration, in two places:
ExplainDecisionSchema.readFilter— "the composed row filter the caller would beserved 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 atcontent/docs/references/security/explain.mdx(two rows).That enumeration is now incomplete. As of PR #13960,
explainreports verdictdeniesand
decision.allowed: falsefor a fail-closed RLS denial, whosereadFilter/rowFilteris plugin-security'sRLS_DENY_FILTER— spelled__rls_deny__followed by acolon 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
verdictfield saysdeniesand
decision.allowedisfalse, 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
.describe()strings plus their JSDoc and regenerating the docs, or (b) the larger vocabulary
unification, is not decided here. Option (b) is option C on [finding] an RLS denial caused by an unresolved variable logs NOTHING and
explainreports it asnarrowsrather thandenies— the operator debugging "this user sees no rows" gets no signal #13639 and is alreadyrecorded there as the maintainer's — it has a measured blast radius (
__deny_all__inthe published schema and docs;
__rls_deny__pinned as a bound SQL parameter by twoservice-analyticssuites and dispatched on by value inisTenantWallDenial,ADR-0123 D2). This card is only the narrow prose gap, which stands whether or not C
ever happens.
verdictis unmeasured.
packages/spec, so it carries that tier and its regeneratedbaselines and docs.
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 D2Generated by Claude Code
Generated by Claude Code