Found while implementing #7073 (PR #7209), which fixes the same defect shape at the producer (validateExpression). Out of scope there — #7073's ruling is explicitly "fix once at the producer, not per-consumer", and this gate deliberately does not go through validateExpression (its own docblock, validate-visibility-predicates.ts:396-399: "this function neither parses with an environment of its own nor reaches for celEngine.compile / validateExpression"). So the producer fix does not reach it. Filed unassigned, unlabeled — severity is triage's call.
The defect
packages/lint/src/validate-visibility-predicates.ts, the visibility-predicate-syntax gate (#6253):
// celSyntaxFault, :401
if (parseCelToAst(source) !== null) return null;
const parsed = collectCelRootIdentifiers(source);
const detail = parsed.ok ? 'the expression could not be parsed' : parsed.error.split('\n')[0].trim();
parseCelToAst returns null for a bounds overrun exactly as it does for a syntax fault (that collapse is by design — cel-engine.ts docblock, "Returns null … when the source is empty or does not parse"). So an over-budget visibleWhen — perfect CEL, merely past maxAstNodes 256 — reaches the syntax gate and is reported as:
- message:
visibility predicate is not valid CEL — Exceeded maxAstNodes (256) … — the headline is false. It IS valid CEL.
- hint (
:609, the fallback arm, since no NON_CEL_SPELLINGS token matches a source that has none): Visibility predicates are bare CEL, e.g. \record.status == 'open'`. Spellings from other languages do not parse: write `==` (not `===`), `!=` …`
That hint is the #7073 defect byte for byte in a different file: the author is told to change the dialect, the source is already bare CEL with the right operators, and any obedient rewrite — an LLM author above all — regresses.
Measured
origin/main @ edb4af099, via the same 80-clause conjunction #6833/#7073 use:
$ npx tsx probe.ts
parseCelToAst(over-budget) = null
collectCelRootIdentifiers.ok = false
detail = Exceeded maxAstNodes (256)
So celSyntaxFault returns { detail: 'Exceeded maxAstNodes (256)', token: null } — the error arm and the fallback hint arm, both.
Not the same as #6833's benign divergence
#6833 measured, and its closure ruled, that lint being stricter than the pushdown runtime during the rc grace window is benign and self-healing at GA. That ruling is about the verdict, and it stands. This is about the wording of a verdict everyone agrees should fire: the gate is right to refuse, and wrong about why, in both the sentence that names the fault and the sentence that prescribes the fix.
Shape of the fix (not prescribing)
The gate's docblock gives a reason for not reaching validateExpression — it would widen "does not parse" to "does not type-check" — which is a good reason and does not apply to parseCelToAstWithReason, the reason-carrying entrance that answers the same parse question and additionally names the bound. validate-rls-predicate-enforceability.ts:223 already reads it from this package. So the minimal move looks like: ask parseCelToAstWithReason, and on kind: 'bounds' emit a size message + size hint (name the bound and its value) instead of the syntax headline and the dialect hint; on kind: 'parse' change nothing. PR #7209 has a slot-generic bounds prescription that may be reusable verbatim.
Whether the rule id should also change (a size refusal reported under visibility-predicate-syntax is arguably mislabelled at the id level too, which is #6778's "wrong label" shape) is a separate question worth deciding rather than assuming.
Refs: #7073 / PR #7209 (producer-side fix), #6778 / PR #6831 (RLS-side precedent), #6253 (this gate's origin), #6132 (the bounds-reason entrance).
Generated by Claude Code
Found while implementing #7073 (PR #7209), which fixes the same defect shape at the producer (
validateExpression). Out of scope there — #7073's ruling is explicitly "fix once at the producer, not per-consumer", and this gate deliberately does not go throughvalidateExpression(its own docblock,validate-visibility-predicates.ts:396-399: "this function neither parses with an environment of its own nor reaches forcelEngine.compile/validateExpression"). So the producer fix does not reach it. Filed unassigned, unlabeled — severity is triage's call.The defect
packages/lint/src/validate-visibility-predicates.ts, thevisibility-predicate-syntaxgate (#6253):parseCelToAstreturnsnullfor a bounds overrun exactly as it does for a syntax fault (that collapse is by design —cel-engine.tsdocblock, "Returnsnull… when the source is empty or does not parse"). So an over-budgetvisibleWhen— perfect CEL, merely pastmaxAstNodes256 — reaches the syntax gate and is reported as:visibility predicate is not valid CEL — Exceeded maxAstNodes (256) …— the headline is false. It IS valid CEL.:609, the fallback arm, since noNON_CEL_SPELLINGStoken matches a source that has none):Visibility predicates are bare CEL, e.g. \record.status == 'open'`. Spellings from other languages do not parse: write `==` (not `===`), `!=` …`That hint is the #7073 defect byte for byte in a different file: the author is told to change the dialect, the source is already bare CEL with the right operators, and any obedient rewrite — an LLM author above all — regresses.
Measured
origin/main@edb4af099, via the same 80-clause conjunction #6833/#7073 use:So
celSyntaxFaultreturns{ detail: 'Exceeded maxAstNodes (256)', token: null }— the error arm and the fallback hint arm, both.Not the same as #6833's benign divergence
#6833 measured, and its closure ruled, that lint being stricter than the pushdown runtime during the rc grace window is benign and self-healing at GA. That ruling is about the verdict, and it stands. This is about the wording of a verdict everyone agrees should fire: the gate is right to refuse, and wrong about why, in both the sentence that names the fault and the sentence that prescribes the fix.
Shape of the fix (not prescribing)
The gate's docblock gives a reason for not reaching
validateExpression— it would widen "does not parse" to "does not type-check" — which is a good reason and does not apply toparseCelToAstWithReason, the reason-carrying entrance that answers the same parse question and additionally names the bound.validate-rls-predicate-enforceability.ts:223already reads it from this package. So the minimal move looks like: askparseCelToAstWithReason, and onkind: 'bounds'emit a size message + size hint (name the bound and its value) instead of the syntax headline and the dialect hint; onkind: 'parse'change nothing. PR #7209 has a slot-generic bounds prescription that may be reusable verbatim.Whether the rule id should also change (a size refusal reported under
visibility-predicate-syntaxis arguably mislabelled at the id level too, which is #6778's "wrong label" shape) is a separate question worth deciding rather than assuming.Refs: #7073 / PR #7209 (producer-side fix), #6778 / PR #6831 (RLS-side precedent), #6253 (this gate's origin), #6132 (the bounds-reason entrance).
Generated by Claude Code