Repository navigation
ADR-0136 D4 is declared ahead of enforcement: 21 action/visibility gate slots still accept an ast-only or blank-source predicate #19000
Description
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
emptyfor a whitespace-only source, and without this the rule would reportvisibleWhen: ' '— 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'svalidate-visibility-predicates.tscelRefusal, 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
ExpressionInputSchemapermanently 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
os-elon-musk commentedon Sep 18, 2026
Closing as a duplicate of #15811, which is older, already ruled, already dispatched and already carried by a PR in flight.
What I got wrong. I filed this card as an open follow-up asking whether the gate-slot conversion "should be converted, and on whose authority". That question was already answered six days before I asked it: card #15811 carries the maintainer's ruling in comment 5644350409 (2026-09-12T07:02:53Z, decision batch #122 item 2, 「同意」 to
1A·2A·3A·4A·5A), whose item 1 rules thatEvaluatedExpressionSchemacomposes into every evaluated slot the census found — naming the field, option and grid-columnvisibleWhen/readonlyWhen/requiredWhen, objectvisibleWhen/disabledWhen, actionvisibleWhen/visible/ActionConditionInputSchema, appvisible, settings visibility andui/bulk-actionvisibility, 「⛔ By identity on all 36 positions」. #15811 ispm:dispatchedto os-litant, and PR #18638 (claude/issue-15811-evaluated-slot-narrowing, head70407326463d08f8d0c2e6419037328ea1bf93fd) implements it.So this card did not merely duplicate an open question — it re-opened a settled ruling. That is the more serious half of the error and it is the reason this closes rather than being narrowed and kept.
Scope comparison, since that decides which one closes. #15811 covers all 36 evaluated declaring positions under a standing ruling; this card covered 21 gate and inline-column slots and proposed three options for a direction already given. This card's scope is a strict subset. #15811 stays open; this one closes.
What was carried over, so closing loses nothing. The one measurement here that I did not find stated on #15811 — the declared fault directions, 4 of 21 slots declaring one and disagreeing 3-to-1 (fail-closed at
data/object.zod.ts:1194,data/object.zod.ts:1387,ui/bulk-action.zod.ts:209; fail-soft atdata/object.zod.ts:1390), plus thesystem/settings-manifest.zod.ts:424and:686spelling note — is now on #15811 as comment 5728924245, posted as evidence only with no change to that card's labels, assignee or state.How the duplicate happened, recorded so the mechanism is visible rather than just the outcome. The filing protocol has the filer attach dedup words and the triage seat run them. I attached five —
gate predicate,visibleWhen,ExpressionInputSchema,evaluated slot,blank predicate— and did not run them myself. #15811's own title containsExpressionInputSchema,visibleWhenand the phrase "ast-only or blank-sourceenvelope", so three of the five would have hit it on the first query. I also wrote in this card's own dedup section that a zero on those words would not be evidence of no duplicate, which was the right caution aimed at the wrong risk: the words were fine, the omission was running them at all.Not reopening this under a narrowed scope. If the
settings-manifestspelling turns out to fall outside #18638's census after all, that belongs on #15811 as a scope note, not as a second card.Closed by the
domain:specseat 3 (Seat: domain:spec#3) that filed it, 2026-09-18T10:48Z.Generated by Claude Code