Skip to content

ADR-0136 D4 is declared ahead of enforcement: 21 action/visibility gate slots still accept an ast-only or blank-source predicate #19000

Description

@os-elon-musk

What this is

ADR-0136 (docs/adr/0136-predicate-fault-semantics-are-contract.md, landing in PR #18985) declares D4 — "A blank or faulting GATE predicate is diagnosed, never a silent true" — and its own Scope boundary section states that D4's spec-side authoring refusal is deliberately not applied in that PR:

That conversion is filed as a follow-up with the slot inventory, and it is the one place where D4 is currently declared ahead of its authoring-side enforcement.

This card is that follow-up, and it carries the inventory.

Class

Class (c) — a metadata trap. ExpressionInputSchema accepts an envelope carrying ast with a blank or absent source. On the gate slots below that value passes the schema door, and then passes packages/lint too, because validate-visibility-predicates.ts only judges a non-blank source:

origin/main:packages/lint/src/validate-visibility-predicates.ts:988

const refusal = source ? celRefusal(source) : null;

and the same function's docblock (:668) records the deliberate opposite position:

A blank predicate is not a fault at all. The front end answers empty for a whitespace-only source, and without this the rule would report visibleWhen: ' ' — which is "no predicate", exactly what the author meant

So an authored gate predicate can be accepted, linted clean, and carry no evaluable source. What the runtime then does on that specific input is not measured here — the adjacent documented failure mode is the over-budget case in the same file, where a gate the front end refuses makes the element "render unconditionally and look exactly like one with no predicate at all (#5149)". Treat the runtime half as the reading this card still owes, not as an established consequence.

Inventory

Instrument: git grep for slot definitions composing ExpressionInputSchema (directly, or via the derived SettingsVisibilityInputSchema / ActionConditionInputSchema) in packages/spec/src, excluding *.test.ts, then a 15-line window per slot for a declared fault direction. Read on origin/main at f347c793e16322a4befc77651d1ab8760bf36874 and on origin/claude/issue-17778-field-rule-predicate-fault-semantics at e1978a0f3b02608d6137ac4daab928540bb52b6b, taken 2026-09-18T10:36Z.

21 slots sit on the wide schema on a gate or inline-column rule path after #18985 narrows the FieldSchema triad. Of those, 19 are named by ADR-0136's Scope boundary and 2 are not:

slot name declared fault direction
ui/view.zod.ts:2891 visibleWhen none
ui/view.zod.ts:2893 visibleOn none
ui/view.zod.ts:3098 visibleWhen none
ui/view.zod.ts:3100 visibleOn none
ui/page.zod.ts:390 visibleWhen none
ui/page.zod.ts:392 visibility none
ui/action.zod.ts:336 visibleWhen none
ui/action.zod.ts:430 visible none
ui/action.zod.ts:1378 visible none
ui/action.zod.ts:1393 disabled none
ui/bulk-action.zod.ts:209 visible fail-closed
ui/component.zod.ts:741 visibleWhen none
ui/app.zod.ts:319 visible none
data/object.zod.ts:1194 visibleWhen fail-closed
data/object.zod.ts:1387 visibleWhen fail-closed
data/object.zod.ts:1390 disabledWhen fail-soft
data/field.zod.ts:387 visibleWhen none
data/field.zod.ts:884 readonlyWhen none
data/field.zod.ts:885 requiredWhen none
system/settings-manifest.zod.ts:424 visible none — NOT named by ADR-0136
system/settings-manifest.zod.ts:686 visible none — NOT named by ADR-0136

Paths are relative to packages/spec/src. The two unnamed slots compose SettingsVisibilityInputSchema, which is ExpressionInputSchema.superRefine(...): the refinement adds a check but does not narrow the ast-only or blank-source arm, so the exposure is the same as the named 19. Control for that claim: ADR-0136 mentions settings-manifest or SettingsVisibility 0 times against 10 hits for visibleWhen in the same file, so the zero is read on a lit instrument.

4 of 21 declare a fault direction, and they disagree with each other — 3 fail-closed against 1 fail-soft. 17 declare none at all. This is the load-bearing number for the options below, and it is sharper than the "several" the implementing report carried: one rule cannot serve a 3-to-1 split, and 17 slots have no declared direction to inherit.

Options

  • A — convert in a follow-up, re-measuring each slot's declared direction first, and rebind packages/lint's validate-visibility-predicates.ts celRefusal, which records the opposite position today. Cost: the 17 direction-less slots each need a direction chosen, which is per-slot product judgment, not a mechanical sweep.
  • B — leave the gate slots on ExpressionInputSchema permanently and narrow ADR-0136 D4 to the consumer-side diagnostic only (the objectui half), so nothing is declared beyond what is enforced.
  • C — convert them inside PR spec: hold a predicate to what the engine can run; declare its fault semantics (ADR-0136) #18985. Refused on that card's own verbatim constraint: binding these slots to one rule without re-measuring each declared direction would bake a direction the ruling did not give.

Recommendation: A, which is also the implementing report's. ADR-0136's Status line and its Scope boundary both state this half is pending, so the declaration is declared-as-pending rather than claimed-as-done. Note for whoever takes it: A is not one change but two packages — a schema narrowing in packages/spec and a behaviour reversal in packages/lint — and the 17 direction-less slots make it a decision card's worth of product judgment, not a refactor.

Dedup words

gate predicate, visibleWhen, ExpressionInputSchema, evaluated slot, blank predicate

I did not run dedup — filing seat attaches words, triage searches. Limits of those words: they are the vocabulary of the fix, not of a symptom an author would report. A card filed from the authoring side ("my visibility rule is ignored", "action button always shows") would share none of these terms, so a zero on all five is not evidence of no duplicate. #15807 and #7327 are the nearest prior art on the same narrowing family and are worth reading before this is priced.

Filed by domain:spec seat 3 as an execution-seat finding: finding only, no domain:* and no priority — those are triage's to set.


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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions