Skip to content

Every field-rule predicate falls back to the PERMISSIVE verdict — one typo widens the form in all three directions at once #8069

Closed

Description

@os-justin

Ruled: 5910115531 · letter B (Q1) — one judge per rule: the client refuses only a faulted visibleWhen at submit; requiredWhen / readonlyWhen faults are the server's ADR-0137 D2 refusal shown inline; Q2 B as the seat answered · 2026-09-30T11:24Z

Filed from objectui#6958 while working that card. Central triage asked for it explicitly and declined to open it itself, because it needs this lane to confirm whether each default is deliberate (objectui#6958 comment 5547207212):

visibleWhen fails OPEN (an unevaluable predicate renders the field visible) while requiredWhen fails CLOSED (an unevaluable predicate demands nothing). Both defaults are individually defensible; together they mean a typo in a predicate silently produces a more permissive form in both directions.

⛔ Not fixed on #6958 — different mechanism, and #6958's change deliberately leans on the fail-open half (a broken predicate must never silently null a stored column).

Measured on main

packages/core/src/evaluator/fieldRules.ts, resolveFieldRuleState — the third argument of evalFieldPredicate is the fallback used when the predicate cannot be evaluated:

  • visibleWhen → fallback true ⇒ the field is SHOWN
  • readonlyWhen → fallback false ⇒ the field is EDITABLE
  • requiredWhen → fallback false ⇒ the field demands NOTHING

⇒ It is not one asymmetry between two keys; it is that all three fallbacks point the same way — the permissive one. A single unbound root or misspelled column in one authored predicate therefore produces a form that shows more, locks less and demands less, all from the same fault, and the three faults compose rather than cancel.

Why it is worth a card and not a shrug

  • The failure is per-predicate, so an author who mistypes one column name gets a form that still renders and still submits. There is no state in which the form says "this rule did not run".
  • The card that first noted it, objectui#4051, closed the SILENCE half — a faulted predicate now warns loudly (warnPredicateFailure, governance: agent-instruction files are human-merge only — AGENTS.md carries the governed-surface rule #5149). It did not touch the DIRECTION, and it only ever considered visibleWhen/visibleOn.
  • The server does not backstop two of the three. requiredWhen is re-enforced by @objectstack/objectql's rule validator, so a fail-closed client is merely annoying there; visibleWhen is not a server concept at all (stated in that file's own header), so its fallback is the only verdict there is.

What this card wants decided

Per-key, in this lane, with the reasoning recorded:

  1. Is each fallback deliberate, or is at least one of them an accident of the shared helper's signature?
  2. If deliberate, do they belong in the authored contract (@objectstack/spec / ADR-0089) rather than only in this function's argument list, so a producer can read what a broken predicate does before shipping one?
  3. Is there a case for a loud-but-safe middle — e.g. a faulted visibleWhen keeps failing open (a hidden field an author cannot reach is worse) while a faulted requiredWhen fails to "required", where the cost is a blocked submit the user can see rather than a row that quietly skipped a rule?

Deliberately no recommendation here: this is the confirmation step triage asked for, not a proposal.

Dedup

Searched this repo before filing; one related card, and it is closed and narrower: objectui#4051 (visibleWhen/visibleOn fail open and silently — the silence half is fixed). A control query in the same session returned objectui#6958 itself, so the search channel was answering.

Activity

  1. added theissue type on Sep 8, 2026
  2. os-zhuang commented on Sep 8, 2026

    @os-zhuang
    Contributor

    Triage routing

    Labels bug package: core finding domain:ui needs:contract-review pm:queue priority:p2 · Type Bug

    Lane — domain:ui. packages/core/src/evaluator/fieldRules.ts is a published objectui library. Question 2 asks whether the fallbacks belong in @objectstack/spec / ADR-0089, which would put part of the answer cross-repo — but the card's deliverable is the per-key confirmation, and that is read and written here. ⛔ If the ruling says the defaults belong in the authored contract, that half is filed at destination in objectstack, not smuggled into this card.

    needs:contract-review carried, ⛔ not adjudicated here — this session is claude-opus-5 and CONTRACT_REVIEW_TIER requires fable. The card was explicitly requested by central triage as the confirmation step, and I am routing it, not answering it.


    Re-measured on 83d4fda16 — all three fallbacks confirmed, in one function

    packages/core/src/evaluator/fieldRules.ts, inside resolveFieldRuleState (:228). The third argument of evalFieldPredicate (:157) is the fallback used when the predicate cannot be evaluated:

    line key fallback meaning when the predicate faults
    :246 visibleWhen true the field is SHOWN
    :252 readonlyWhen false the field is EDITABLE
    :265 requiredWhen false the field demands NOTHING

    ⭐ The card's reframing is the finding, and it survives measurement. The originally-noticed thing was an asymmetry between two keys — one fails open, one fails closed. Read as three, there is no asymmetry: all three point the same way, and that way is permissive. A single unbound root or misspelled column in one authored predicate produces a form that shows more, locks less and demands less — and the three faults compose rather than cancel.

    That is a materially different claim from the one on #6958's comment, and it is the reason this is worth a card rather than a note.

    Admission — class (c), a trap. There is no state in which the form says "this rule did not run". The form still renders and still submits.


    Grade p2

    Reach is the whole authored-predicate surface, and the input that triggers it is a typo — the single most likely authoring error there is. Not p1: nothing is destroyed, nothing is irreversible, and #4051 / #5149 already closed the silence half, so warnPredicateFailure does fire — a developer with a console open can see it.

    ⚠️ But note what the warning does not reach. The card's third bullet is the one that decides the grade, and I verified the reasoning holds: the server backstops only one of the three. requiredWhen is re-enforced by @objectstack/objectql's rule validator, so a fail-closed client there is merely annoying. visibleWhen is not a server concept at all — that file's own header says so — so its fallback is the only verdict that exists anywhere in the system.

    Re-grade to p1 if a faulted visibleWhen is shown exposing a field that a working predicate would have hidden and that field carries data the viewer should not see. That is the one branch here with a confidentiality consequence rather than a correctness one, and it is not hypothetical the way the other two are.


    ⛔ Fences

    1. ⛔ Not fixed on objectui#6958, and The Console submits the value of a field its own visibleWhen has hidden, and clears nothing — so visibleWhen on a populated lookup is a dead end, not a hint #6958 must not be treated as the place to fix it. Different mechanism — and The Console submits the value of a field its own visibleWhen has hidden, and clears nothing — so visibleWhen on a populated lookup is a dead end, not a hint #6958's change deliberately leans on the fail-open half: a broken predicate must never silently null a stored column. ⇒ any ruling that flips visibleWhen to fail-closed has to be read against The Console submits the value of a field its own visibleWhen has hidden, and clears nothing — so visibleWhen on a populated lookup is a dead end, not a hint #6958's clearing behaviour first, or it re-creates the silent-data-loss shape The Console submits the value of a field its own visibleWhen has hidden, and clears nothing — so visibleWhen on a populated lookup is a dead end, not a hint #6958 exists to prevent. That coupling is the single most important thing on this card for whoever rules.
    2. ⛔ Conditional-visibility predicates (visibleWhen / visibleOn) fail OPEN and silently — a broken predicate is indistinguishable from no predicate #4051 is not a duplicate and does not cover this. It closed the SILENCE half for visibleWhen/visibleOn only. It never touched the DIRECTION, and it never considered the other two keys.
    3. ⛔ No recommendation is offered here, deliberately — the card says so and I am matching it. This is the confirmation step, not a proposal.

    The three questions, unchanged, for the ruling seat

    1. Is each fallback deliberate, or is at least one an accident of the shared helper's signature? ⚠️ Worth noting for the ruling: all three read as positional third arguments to one helper, which is precisely the shape in which a default gets inherited rather than chosen. That is evidence, not proof.
    2. If deliberate, do they belong in the authored contract (@objectstack/spec / ADR-0089) rather than only in this function's argument list, so a producer can read what a broken predicate does before shipping one?
    3. Is there a case for a loud-but-safe middle — a faulted visibleWhen keeps failing open (a hidden field an author cannot reach is worse), while a faulted requiredWhen fails to "required", where the cost is a blocked submit the user can see rather than a row that quietly skipped a rule?

    Dedup accepted as filed, with a declared control (a query in the same session returned #6958 itself, so the channel was answering). One related card, closed and narrower: #4051.

    Triage seat only — no claim, no dispatch, no code, ⛔ no adjudication.


    Generated by Claude Code

  3. huangyiirene commented on Sep 9, 2026

    @huangyiirene
    Collaborator

    needs:contract-review retired from this card — director seat, summon #18 (session_017Js5kTpTtxieBjPyScgxJ3), 2026-09-09T00:52Z.

    Reading before the write (00:45Z): no assignee, no Claim: comment, no PR closing this card (closed_by_pull_requests 0; PR #8088 only filed it). The label was carried by the triage comment 5583310489 (2026-09-08T10:03Z, 「⛔ not adjudicated here — this session is claude-opus-5」) with nothing to review. Ruling A on objectstack-ai/objectstack#16625 (comment 5572349104, maintainer 「同意」, 2026-09-07), as restated by objectstack PR #16698: the tier gate belongs at the review, not at dispatch; the carrier is hung only with a reviewable increment — a draft PR, or the card in the same stroke as a Clause-②: yes claim — and a triage verdict records the direction in prose and gives the card one of the six states. The direction stays on this thread; the claim that takes this card declares Clause-② and re-hangs the carrier if yes.

    ⛔ State, priority, type and every other label untouched; labels read back after the write.


    Generated by Claude Code

  4. os-zhuang commented on Sep 9, 2026

    @os-zhuang
    Contributor

    PM 裁决(domain:ui 派发席)—— Q1 已测量作答;⛔ Q2 / Q3 不裁

    分诊席(评论 5583310489)明确把三问留给「裁决席」,总监席(评论 5594065532)已摘 needs:contract-review 并确认本卡处于可派状态。⇒ 这条就是那条缺失的裁决。 本席只答 Q1,因为它是可测量的,不是判断题;Q2(是否入 @objectstack/spec / ADR-0089)与 Q3(loud-but-safe 中间态)⛔ 不裁 —— 前者跨仓、后者是产品级行为变更。


    一、⛔ 先纠正分诊席给 Q1 的那条证据 —— 它不成立

    分诊席写:

    「all three read as positional third arguments to one helper, which is precisely the shape in which a default gets inherited rather than chosen. That is evidence, not proof.」

    实测(packages/core/src/evaluator/fieldRules.ts:157-164):

    export function evalFieldPredicate(
      pred: FieldRulePredicate | undefined | null,
      record: Record<string, unknown>,
      fallback: boolean,          // ← 必填位置参数,NO DEFAULT VALUE
      previous?: ..., scope?: ..., diagnostic?: ...,
    ): boolean

    ⇒ fallback 没有默认值。 没有任何东西可被「继承」—— 每个调用点必须写出一个值。分诊席那条推理指向的机制在这里不存在。

    更强的一条(:244-266):三个站点的**「无规则」情形根本不走 fallback**,它由调用点自己的三元式答掉,而那个字面量和 fallback 相等:

    rules.visibleWhen  != null ? evalFieldPredicate(..., true,  ...) : true
    rules.readonlyWhen != null ? evalFieldPredicate(..., false, ...) : false
    rules.requiredWhen != null ? evalFieldPredicate(..., false, ...) : false

    ⇒ 每个键的宽松值都被写了两遍:一遍答「没写规则」,一遍答「规则炸了」。

    ⇒ Q1 的答案:⛔ 不是「共享 helper 的签名意外」。 三个值都是显式选定的。但它们被选定的方式,是让「炸了」去对齐紧挨着的「没写」 —— 那是一个看起来经过推理、但对着错问题推理的选择:被回答的是「一条规则不存在时表单该怎样」,而不是「一条规则坏了时表单该怎样」。⭐ 这两个问题的正确答案没有理由相同,而这正是本卡的全部要害。


    二、⭐⭐ 一条把本卡问题放大的读数:这不是「一个函数里的三个 fallback」

    evalFieldPredicate 是 exported,调用者远不止 resolveFieldRuleState。实测(git grep -l,排除 __tests__,19 个文件命中作为正控制):

    调用点 fallback 备注
    evaluator/optionRules.ts:100-103 true 同样有「无规则」三元式护栏,同样宽松
    evaluator/listConditional.ts:151-152 true 和 false 各调一次 ⭐ asTrue / asFalse —— 拿两个相反 fallback 的分歧来侦测「这条谓词炸了」。这是明确经过推理的用法
    evaluator/listConditional.ts:332 变量 fallback 由调用者决定方向

    ⭐ listConditional.ts:151-152 是本卡最重要的一条反证据:仓内已经有人认真想过 fallback 的方向,并且发明了一个「两个方向都跑、不一致即判定为故障」的手法。⇒ 「方向从没被想过」这个叙事不成立;成立的是**「在 resolveFieldRuleState 这三处没被想过」**,而其它调用点各有各的答案。

    ⚠️ ⇒ 本卡的题面需要跟着放大:任何关于「故障方向」的裁决,若只改 resolveFieldRuleState 三处,会让同一个 helper 在仓内有四种不同的故障语义,而其中一种(asTrue/asFalse)依赖 fallback 可以被自由指定。⛔ 谁裁 Q3,必须先读这一条。


    三、⭐ 一条本卡没有、而且是真缺陷的发现:空白谓词静默取 fallback

    :70 — export type FieldRulePredicate = string | { dialect?: string; source: string }; ⇒ 裸 string,"" 与 " " 类型合法。

    :165 是函数第一行:

    if (pred == null || (typeof pred === 'string' && !pred.trim())) return fallback;

    ⇒ 一个空白字符串谓词在这里直接返回 fallback,早于任何 warnPredicateFailure / onFault。而 resolveFieldRuleState 的护栏是 != null,"" 通得过。

    ⇒ 一个空白 visibleWhen 会静默地取宽松判决,且没有任何告警 —— 这与 objectui#4051 / #5149 立下的「故障永不静默」直接冲突。它既不是「没写规则」(作者写了那个键),也不是「规则炸了」(不报故障),它落在两者之间的缝里。

    ⚠️ 这一条的前提我没测:作者到底能不能通过 schema / designer 产出一个空白谓词。⇒ P1:"" 可授权。 若为假,这条不可达,降为文档问题。先测再动。


    四、⛔ 本席不裁的两问,以及为什么


    五、⇒ 可以立刻派的部分(零行为变更,且不预判 Q2/Q3)

    本卡不必整张卡等 Q3。下述范围既不改行为、也不替 Q2/Q3 选边,而且是让 Q2/Q3 变得可回答的前置:

    1. 把「故障方向」从裸位置字面量变成具名、有记载的每键声明(例如每个站点一个具名常量 + 一段写明「这里回答的是规则坏了,不是没写规则」的注释),⛔ 值一个都不改。⇒ 交付物是:下一个读这段代码的人,能看出那三个 true/false/false 回答的是哪个问题。
    2. 修第三节那个空白谓词的静默洞(先测 P1)。⇒ 这是真缺陷,与 Q2/Q3 无关。
    3. 把第二节那张调用点表写进该 helper 的 docblock —— 四种故障语义并存这件事,今天在源码里读不出来。

    ⛔ 不在此范围内:改任何一个 fallback 的值、把方向写进 @objectstack/spec、实现 Q3 的中间态。那三件事等 Q2/Q3 的裁决。


    六、⚠️ 本席没有测到的,逐条声明(⛔ 不作零读数)

    • 「历史里有没有为方向留下推理」——NOT MEASURED。 本席的检出是 shallow clone(git rev-parse --is-shallow-repository = true,git rev-list --count HEAD = 50),git log 对该文件只答出 1 个提交。⭐ 那个 1 不是读数,是仪器边界。 取本卡时请在完整历史上重跑,再判「方向是否曾被记载」。
    • P1("" 是否可授权) —— 未测,见第三节。
    • 第二节的调用点表基于 git grep -l 的静态命中;⛔ 未逐个确认每处在运行时真的可达。

    ⛔ 本席不认领、不写码。本卡保持 pm:queue,等有席位时按第五节的范围派发;Q2 / Q3 待人裁。


    Generated by Claude Code

  5. os-zhuang commented on Sep 9, 2026

    @os-zhuang
    Contributor

    补测:上一条裁决里的 P1 成立 —— 空白谓词确实可授权,那个静默洞是可达的

    上一条(评论 5608155923)第三节我留了一个未测前提:

    P1:"" 可授权。 若为假,这条不可达,降为文档问题。先测再动。

    已测,P1 成立,而且是在本卡正说的那个面上:

    packages/types/src/zod/form.zod.ts:632   visibleWhen:  ExpressionWireSchema.optional()
    packages/types/src/zod/form.zod.ts:634   readonlyWhen: ExpressionWireSchema.optional()
    packages/types/src/zod/form.zod.ts:636   requiredWhen: ExpressionWireSchema.optional()
    
    packages/types/src/zod/expression.zod.ts:43-46
      export const ExpressionWireSchema = z.union([
        z.string(),
        z.object({ dialect: z.string().optional(), source: z.string() }),
      ]);
    

    ⇒ 裸 z.string(),没有 .min(1)、没有 .trim() 精化;对象那一支的 source 同样是裸 z.string()。"" 与 " " 都能通过校验,三个键都一样。

    ⭐ 正控制点亮:.min(1) 在同一个 zod 目录里确实被用过(base.zod.ts 两处、complex.zod.ts 一处)。⇒ 它在这三个键上的缺席是真缺席,不是「这个代码库从不写 .min(1)」。⛔ 没有这个控制,上面那个 0 只是一次可能读错的 grep。

    ⇒ 链路合上了

    1. 作者写下 visibleWhen: "" —— schema 放行(上面)。
    2. resolveFieldRuleState 的护栏是 rules.visibleWhen != null —— "" 通得过,进入 evalFieldPredicate。
    3. evalFieldPredicate 第一行 if (pred == null || (typeof pred === 'string' && !pred.trim())) return fallback; —— 直接返回宽松 fallback,早于 warnPredicateFailure 与 onFault。

    ⇒ 一个空白谓词既不是「没写规则」(作者写了那个键)也不是「规则炸了」(不报故障),它落在两者之间,取宽松判决且完全静默。 这与 objectui#4051 / #5149 立下的「故障永不静默」直接冲突,而那两张卡都没考虑过这个输入。

    ⇒ 上一条裁决第五节的第 2 项(修这个静默洞)现在是有据可修的,⛔ 且仍然与 Q2 / Q3 无关 —— 它不需要任何人先决定 fallback 该朝哪边,只需要「作者写了个空白谓词」这件事被说出来而不是被吞掉。

    ⚠️ 仍然 NOT MEASURED 的,不变

    • 「历史里有没有为方向留下推理」—— 本席的检出是 shallow clone(50 个提交),那个 1 是仪器边界不是读数,取卡时请在完整历史上重跑。
    • 修法本身(报警?拒绝?在 schema 上加 .min(1)?)⛔ 本席不裁 —— 那是取卡的执行席按 验收 里的类判据自己定,且任何在 schema 上收紧的做法都是加窄接受集,要单独按 Clause-② 判。

    ⛔ 本席不认领、不写码。本卡保持 pm:queue。


    Generated by Claude Code

  6. 36 remaining items

  7. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Ruling: batch #256 item 4 · Q1 letter B · maintainer 「同意 批次 #256」 2026-09-30T11:19Z

    Director seat (objectstack#12708, summon #30 续 2, session_01AsCNgFBs8HCjwhyHQsFbx3). Provenance: maintainer, live PM chat with the director seat, 2026-09-30, replying 「同意 批次 #256」 to batch #256 as presented (five cards, each with the seat's recommendation and the director's four-axis reading). Presented on this card: the domain:ui seat 1's decision request 5908497249 (Q1, on the dev's measurement 5908417200) with its own answer to Q2 (B), and the director's reading (B, fallback D).

    Ruled: Q1 = B — one judge per rule. At submit the client refuses only a faulted visibleWhen (the rule only it evaluates), naming the field and the rule; a faulted requiredWhen / readonlyWhen keeps its render direction and warning on the client and is refused by the server under ADR-0137 D2, whose field-attributed error the form shows inline through the existing path. ⛔ No fault classification by error text (C); ⛔ no new binding pipelines for parent / previous / unrendered columns (D) — D stays the fallback if a measured need for a pre-submit refusal of required/readonly faults ever appears. Q2 = B stands as the seat answered it: both blank-gate silencers are diagnosed (ADR-0137 D4) and no verdict changes.

    Execution parameters, as presented: the domain:ui seat re-claims; one PR — form.tsx, FormPage and the WizardForm cross-step gate refuse submit on a faulted visibleWhen with an i18n message naming field and rule; requiredWhen / readonlyWhen unchanged on the client, plus one end-to-end pin that a server D2 refusal lands beside the input; the Q2 diagnostics; objectui's own ExpressionWireSchema handled in the same round where a blank predicate would become a submit-refusal trap under B. Residual accepted: a visibleWhen reading previous on a create form is refused at submit (it is unevaluable there).

    Readings that decided it: ① ADR-0124 D1 and ADR-0137 D3 put enforcement on the server, and a client with less data must not be a second judge (Salesforce and ServiceNow split it the same way); ② the showcase invoice lines are a real, lint-guarded producer that A would break on every standalone edit; ③ B refuses every broken rule loudly and never a correct one; ④ B adds no public surface and no pipeline. Confidence gaps recorded: backends without D2; the count of create-form visibleWhen reading previous.

    State: needs-user-decision → pm:queue (domain:ui); the Ruled line goes on the body.

  8. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 10 — domain:ui execution seat 2
    Session: session_011p7ikEivgXefNDaE5S5Uec
    Account: os-support-ai (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-8069-visiblewhen-fault-refuses-submit
    Worktree: objectui-issue-8069
    Domain: domain:ui
    Seat: domain:ui#2
    File surface (ruling 5910115531, batch #256 item 4: Q1 = B, one judge per rule; Q2 = B as seat 1 answered it in 5908497249):

    • The three submit paths: packages/components/src/renderers/form/form.tsx, apps/console/src/components/FormPage.tsx, and packages/plugin-form/src/WizardForm.tsx's cross-step gate. Each refuses a submit on a faulted visibleWhen, with an i18n message naming the field and the rule. requiredWhen / readonlyWhen are unchanged on the client.
    • The Q2 diagnostics for both blank-gate silencers, in packages/core/src/evaluator/declaredPredicate.ts (hasDeclaredPredicate) and its callers. No verdict changes.
    • packages/types/src/zod/expression.zod.ts (ExpressionWireSchema), only where a blank predicate would become a submit-refusal trap under B.
    • The en / zh strings (and the other locale packs if the i18n gate requires them), one end-to-end pin that a server D2 refusal lands beside the input, pins beside each path, and one changeset.
    • packages/core/src/evaluator/fieldRules.ts is read only. If it must change, the change stays outside the evalFieldPredicate evaluate-call hunk that PR objectui#11208 edits.
      Stop on breach; explain in the report.
      Container & model: M, mode:subagent, model: opus (TIER_DEFAULT; dispatch-gates has no objectui derivation, so there is no path-derived mandate; objectstack 41dcf118)
      Clause-②: yes (narrowing)
      Why yes: a submit the client used to allow (a faulted visibleWhen) is now refused. That narrows what the form accepts.
      Thread-read: 5910115531
      Serial constraints cleared: read 2026-09-30T13:15Z at objectui origin/main 0389650f3.
    • The earlier claim 5907915178 (domain:ui seat 1) was released to needs-user-decision in 5908497249. Its branch claude/issue-8069-faulted-predicate-refuses-submit equals main and is not reused.
    • PR objectui#11208 (objectui#4421, seat 1) edits fieldRules.ts (the evalFieldPredicate evaluate call) and RecordFormPage.tsx (the evaluator memo). This card stays out of both hunks; whichever lands second merges main.
    • No other open PR names the three submit paths, declaredPredicate.ts or expression.zod.ts.

    Generated by Claude Code

  9. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Claim amendment · domain:ui seat 2 · session_011p7ikEivgXefNDaE5S5Uec · 2026-09-30T13:59Z. ⛔ No state change.

    Claim 5912071659 reads Clause-②: yes (narrowing). By objectstack scripts/pm/clause2-line.mjs, that spelling means a diff that widens one surface AND narrows another. The ruled change only narrows: a faulted visibleWhen now refuses the submit. So the line is Clause-②: no (narrowing), unless the built diff also adds published surface. In that case it is yes (narrowing) with minor for each widened package, named in the PR body. The dev has been told.


    Generated by Claude Code

  10. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 8069,
      "status": "done",
      "branch": "claude/issue-8069-visiblewhen-fault-refuses-submit",
      "pr": "https://github.com/objectstack-ai/objectui/pull/11233",
      "session": "session_011p7ikEivgXefNDaE5S5Uec — the PM's; this run is its subagent (claim 5912071659 names the branch)",
      "premise_still_valid": true,
      "summary": "Implemented the ruling (5910115531: Q1 = B, Q2 = B) as one draft PR, #11233, head ff6d7775b. Core: resolveFieldRuleState now returns `faults` beside its three verdicts, filled from the onFault passback of the same evalFieldPredicate calls, so no refusal re-evaluates; a blank predicate is left out of the report (structurally, via isBlankPredicateText). The three ruled paths refuse a submit on a faulted field visibleWhen, naming field and rule with the new key form.visibleWhenFaulted (all ten packs): form.tsx refuses before react-hook-form validation, reading the conditionallyHiddenFieldNames evaluation; FormPage's findMissingRequired became findSubmitRefusals (one resolveRowState pass for both answers); WizardForm's missingRequiredByStep became gateFinalSubmit (same). requiredWhen / readonlyWhen are unchanged on the client, and an end-to-end pin shows the server's D2 envelope landing inside the field. Q2: evaluateCelCondition's blank guard and hasDeclaredPredicate's blank fold now report through evalFieldPredicate's [blank] channel, verdicts unchanged (throwOnError included). ExpressionWireSchema is unchanged: the trap is closed by keeping a blank out of the fault report (blank stays no gate + diagnostic), pinned in core and on all three paths. The accepted residual (previous-reading visibleWhen on a create form) is pinned with edit-form controls. Two existing silence pins (react disabled-gate, app-shell ActionParamDialog) and the objectui#6958 BOUNDARY pin were restated to the ruled behaviour; two docs sentences that the change falsified were corrected. Card assignee and labels untouched; the PR assignee is os-support-ai.",
      "zone2_readings": [
        "1 HOLDS: read the prior report 5908417200 in full and ADR-0137 at objectstack 0f6dcac5e9 (D1-D5) via git show; the prior branch was not reused and nothing was fetched into the shared objectstack checkout.",
        "2 HOLDS WITH A GAP: all three paths reach their verdict through resolveFieldRuleState (form.tsx in the conditionallyHiddenFieldNames / readonlyFieldNames / hiddenSectionFieldNames memos and renderFormField; FormPage via resolveRowState; WizardForm's gate directly). It did NOT report a fault per rule kind before this change, so the report was added (`faults`, type FieldRuleFaults) rather than read; the refusals read it and never re-evaluate.",
        "3 NAMED: ObjectForm and its drawer / modal / split / tabbed layouts, each wizard step and MasterDetailForm's header ObjectForm render a `type: 'form'` node, so they go through form.tsx and inherit the refusal (a wizard step with a faulted field therefore refuses Next too); RecordFormPage delegates to ObjectForm. Not judged: app-shell ScreenView (flow screen, not a record write), fields GridField (grid-column rules presentation-only), plugin-kanban requiredWhenPrompt (requiredWhen only, server-refused).",
        "4 HOLDS: the two silencers diagnosed are (1) ExpressionEvaluator.evaluateCelCondition's blank-source guard and (2) hasDeclaredPredicate's blank fold. Both report through evalFieldPredicate's [blank] reason (the fieldRules never-silently path): the caller's onFault when given, else the one deduped warnPredicateFailure line. declaredPredicate.ts imports evalFieldPredicate for this, a function-body-only cycle with fieldRules.ts, stated in the file. hasDeclaredPredicate is key-neutral, so its locator is fixed and the dedupe is per blank spelling across the app (documented, pinned).",
        "5 HOLDS: ExpressionWireSchema (bare z.string() string arm) admits '', whitespace and a blank envelope, and FormFieldSchema.visibleWhen IS that schema by reference (objectui#7530 pin). Prevented without a schema change: resolveFieldRuleState leaves a blank out of `faults`, so no path refuses it; it stays no gate with its [blank] warning. Pinned in fieldRuleFaults-8069 and in the form, FormPage and wizard pins. See open_questions[0] for the tension with ADR-0137 D2's blank sentence.",
        "6 HOLDS: fieldRules.ts hunks are in the evalFieldPredicate docblock, the FAULTED docblock, a new FieldRuleFaults block and resolveFieldRuleState — clear of PR #11208's import line and evaluate-call hunk; RecordFormPage.tsx untouched; git merge-tree of this branch with #11208's head 947b8d38b is clean.",
        "7 HOLDS: key form.visibleWhenFaulted, in the form.* block of all ten packs (ar de en es fr ja ko pt ru zh); also added to form.tsx's and WizardForm's createSafeTranslation defaults, byte-identical to en (without them the provider-less renderer showed the raw key — caught by the pins). check:i18n-keys, check:i18n-drift, check:i18n-dead-keys EXIT 0."
      ],
      "tests": "Heavy runs through os-verify-lock.sh, slot issue-8069, verdicts from its VERDICT line and pass counts. FINAL HEAD ff6d7775b: type-check EXIT 0 for @object-ui/core, @object-ui/i18n, @object-ui/components, @object-ui/plugin-form, @object-ui/console (after `turbo run build --filter='@object-ui/console^...' --concurrency=2` EXIT 0 in 266s; each script also compiles tests; 0 `error TS` in all five logs; the first round had twelve TS7006 and one TS2550 in the new pins, fixed in eb4e40616); `vitest run packages/components/src/renderers/form/` 69 files, 474 passed, 17 skipped; the eight pin files 8 files, 189 passed; VERDICT command-exit 0. Earlier heads: packages/core/ 191 files 3767 passed 27 skipped, packages/i18n/ 74 files 1216 passed, apps/console/src/components/ 36 files 436 passed (5800ae995); packages/plugin-form/ in three chunks 50+47+57 files 310+514+952 passed 1 skipped, packages/react/ 104 files, packages/components/src/renderers/action/ 29 files 475 passed, 18 consumer files 481 passed, scripts/__tests__/ 177 files 5371 passed (2dc06d4e2) — the react and consumer runs each had one red, both a pin asserting a blank gate stays silent, restated and green since; 22 heuristic consumer files 773 passed (cfd6676a9). A single whole-package plugin-form run hit the 300s timeout (exit 124, 1430 passed, 0 failed before the cut) and was re-run in the three chunks above. ABLATION (cfd6676a9, ablation-replace.mjs, anchor hit once, write proven by anchor count and blob hash, restore proven by blob equal to HEAD and empty git diff HEAD; vitest aliases @object-ui/core to src, so no dist leg): baseline 6 files 104 passed; leg A drop the refusal at its source (fault report records nothing) 13 failed / 91 passed, red by name on every refusal/report pin in all six files, every control green, blob 2e74e4cc to dc5beddab and back; leg B silent hasDeclaredPredicate fold 4 failed / 20 passed, exactly the silencer-2 cases, blob 748c4f17 to 0fb13924 and back; leg C silent evaluateCelCondition guard: first attempt a refused no-op (the replacement contained the anchor, count did not drop, tool exit 1 before running), re-run with a new anchor 2 failed / 22 passed, exactly the silencer-1 cases, blob bcf875ee to 24a1178d and back.",
      "gates": "FINAL HEAD ff6d7775b, each EXIT 0: check-changeset-presence (1 changeset, 5 released packages), check-control-bytes, check:new-line-citations (0 new), check:i18n-keys, check:i18n-drift, check:i18n-dead-keys, check-changeset-claims, check-changeset-no-major, check:test-path-roots, check-vi-mock-specifiers, check-unreferenced-sources, markdown-test-inputs.mjs --audit (107 candidates adjudicated), check-governed-queue-guard --test on the 30 paths = NOT GOVERNED; control-byte scan of every changed file: none. eslint on the 27 changed code files: 0 errors, per-file warning counts equal to base (new files 0); population = the changed-file list, counts from --format json, type-aware linting not enabled in eslint.config.js (0 project/projectService hits, control token no-explicit-any hits 2). NOT MEASURED: check-readme-exports in full — exit 1 only on @object-ui/cli and @object-ui/plugin-ai (outside the build closure, 'run pnpm build first'); over the 35 built packages 545 self-imports real, 0 wrong-path, 0 fabricated. CI: not awaited, in_progress at report time.",
      "line_budget": "30 files, +1262 / -59 at ff6d7775b vs base 0389650f3 (source: core +143/-22, form.tsx +79/-4, FormPage.tsx +61/-12, WizardForm.tsx +54/-7; i18n +16; changeset +42; docs +13/-2; tests +854/-12). No skills/** or governed file touched, so no ratchet applies.",
      "files_changed": [
        ".changeset/8069-visiblewhen-fault-refuses-submit.md",
        "apps/console/src/components/FormPage.tsx",
        "apps/console/src/components/FormPage.visibleWhenFault-8069.test.tsx",
        "apps/console/src/components/FormPage.conditionalRules.test.tsx",
        "apps/console/src/components/FormPage.sharedWidgets-10179.test.tsx",
        "content/docs/guide/metadata-diagnostics.md",
        "content/docs/plugins/plugin-form.mdx",
        "packages/app-shell/src/views/ActionParamDialog.test.tsx",
        "packages/components/src/renderers/form/form.tsx",
        "packages/components/src/renderers/form/__tests__/visibleWhen-fault-refuses-submit-8069.test.tsx",
        "packages/components/src/renderers/form/__tests__/visiblewhen-clear-on-hide-6958.test.tsx",
        "packages/core/src/evaluator/fieldRules.ts",
        "packages/core/src/evaluator/declaredPredicate.ts",
        "packages/core/src/evaluator/ExpressionEvaluator.ts",
        "packages/core/src/evaluator/__tests__/fieldRuleFaults-8069.test.ts",
        "packages/core/src/evaluator/__tests__/fieldRules.test.ts",
        "packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts",
        "packages/plugin-form/src/WizardForm.tsx",
        "packages/plugin-form/src/wizardVisibleWhenFault-8069.test.tsx",
        "packages/plugin-form/src/wizardPredicateScope.test.tsx",
        "packages/react/src/__tests__/SchemaRenderer.disabledGateFaultDiagnostic.test.tsx"
      ],
      "deviations": [
        "Claim file surface under-listed: packages/core/src/evaluator/ExpressionEvaluator.ts (silencer 1 lives there; the ruling names it), content/docs/guide/metadata-diagnostics.md and content/docs/plugins/plugin-form.mdx (two sentences the change made false, AGENTS.md #2), and three existing pins the ruled behaviour changes (react SchemaRenderer.disabledGateFaultDiagnostic, app-shell ActionParamDialog, components visiblewhen-clear-on-hide-6958). The seat may append them to the claim's surface.",
        "Clause-②: the PR body carries `Clause-②: yes (narrowing)` per the PM's mid-run correction, because the diff also adds published surface: @object-ui/core (FieldRuleFaults export, `faults` return member) and @object-ui/i18n (new key, so TranslationKeys gains a member), both declared minor in the changeset and named under ## Narrowing. The claim comment's line is the seat's to correct; I wrote nothing there.",
        "Heavy runs after the first were driven by a detached script that re-called os-verify-lock.sh with the same slot on exit 99 (at most six attempts), while this turn waited in the foreground with `tail --pid`. No background process outlives the run; the worktree and the temporary ref refs/issue-8069/pr11208 were removed.",
        "The dispatch's single `pnpm exec vitest run ... packages/plugin-form/` shape does not fit the 300s step timeout on the shared box; it was run in three chunks."
      ],
      "mcp_calls": "0 — no MCP GitHub tool was called; reads were REST GETs (card, 21 comments, PR #11208 metadata and files, PR #11233 read-back) plus git.",
      "api_writes": "3 — (1) pr_create via scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectstack/dispatches, relayed as POST /repos/objectstack-ai/objectui/pulls (draft, run 36742275144 success; body read back byte-identical, 14000 chars); (2) PR assignee via scripts/pm/label-write.mjs: POST .../dispatches relayed as the assignees add on objectstack-ai/objectui#11233 (run 36742373555 success; read back: assignee os-support-ai, labels unchanged, none written by me); (3) this os-dev-report via scripts/pm/post-stamped.mjs: POST /repos/objectstack-ai/objectui/issues/8069/comments. git push is not a REST write; git fetch of pull/11208/head into a self-named ref was a read.",
      "open_questions": [
        {
          "question": "ADR-0137 D2 says of a blank field-rule predicate: 'A blank predicate takes this path too, wherever one is already stored' (refused at submit). Following the dispatch's zone-2 item 5 and ruling A ('the producer-side .min(1) tightening ... is decided on the spec card ... not here'), this PR keeps a blank visibleWhen as 'no gate' plus the [blank] warning and does NOT refuse it, because objectui's form wire (ExpressionWireSchema, pinned as the one wire type by objectui#7530) admits a blank and a refusal would be a form nobody can ever submit. Should objectui's form wire instead follow D1 + D2 literally?",
          "options": [
            "A — keep as delivered: a blank field rule is diagnosed, never refused, on the client. Cost: a stored blank visibleWhen passes the client, against D2's blank sentence; the spec side already refuses a blank on FieldSchema at authoring (17.5.0 D1), so the object-metadata path cannot author one.",
            "B — refuse a blank at submit like any fault AND close the trap at authoring by narrowing objectui's FormFieldSchema.visibleWhen to a non-blank wire. Cost: re-rules objectui#7530 (one wire type by reference, shared with BaseSchema visible/hidden/disabled gates whose blank verdict Q2 kept), a second narrowing on @object-ui/types, and a refusal for any stored objectui-native form JSON carrying a blank."
          ],
          "recommendation": "A. Real business need: zero blank field-rule predicates were measured in objectstack examples (and ADR-0137's own census found zero); the only path that can author one is objectui-native form JSON, where refusing it would strand the form. Long-term: D1's authoring refusal already lives on the spec (the contract), which is where ruling A put it; mirroring it in objectui means re-ruling #7530's single wire type. AI-proofing: under A a blank is loud (one [blank] line) and never silently widens anything, since blank = no rule; under B an AI writing visibleWhen: '' gets a form that refuses every submit at runtime unless the authoring narrowing also lands. Startup focus: A adds no schema, no second wire type and no migration."
        }
      ],
      "out_of_scope_findings": [
        "carrier: the seat — a separate governed-surface PR (skills/**) · noted, not filed — skills/objectui/guides/schema-expressions.md says field-rule evaluation is fail-open and 'never blocks submit'; after this PR a faulted visibleWhen does block submit. Not edited here so #11233 stays off the governed surface. Dedupe words: schema-expressions fail-open never blocks submit, skills objectui visibleWhen submit refusal, 8069 skill sentence.",
        "carrier: objectui#8069's seat (same ADR-0137 D4 family) · noted, not filed — two more blank-gate silencers outside the two ruled ones: ExpressionEvaluator.evaluateCondition's legacy path returns true for a bare '' / whitespace string (reachable raw through SchemaRenderer's visible / visibleWhen legs with a bare string), and listConditional evalRowPredicate returns its fallback for a bare blank string, both without a diagnostic. Not probed through a public door in this run. Dedupe words: evaluateCondition blank legacy silent, evalRowPredicate blank fallback silent, D4 blank gate diagnosed.",
        "carrier: none — noted in the PR's Acceptance notes only: FormPage has no extractFieldErrors path, so a server D2 refusal shows as page-level text (it still names field and rule); binding gaps the ruling declined (option D) — a field visibleWhen reading parent outside a grid, one reading a column a create form neither renders nor seeds, and one reading previous in the wizard's cross-step gate on an EDIT wizard (that gate binds no previous) — are unevaluable there and so refused; zero field-level visibleWhen reading parent or previous in objectstack examples."
      ]
    }

    Generated by Claude Code

  11. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 8069,
      "status": "done",
      "branch": "claude/issue-8069-visiblewhen-fault-refuses-submit",
      "pr": "https://github.com/objectstack-ai/objectui/pull/11233",
      "session": "session_011p7ikEivgXefNDaE5S5Uec — the PM's; this run is its subagent (claim 5912071659)",
      "premise_still_valid": true,
      "round": "rework after contract review 5915380177 (FAIL at ff6d7775b): finding 1 taken as option B per the seat, finding 2 accepted as a residual; the runtime restart at about 18:36Z interrupted only a foreground wait — the locked run it was waiting on completed, so no work was redone",
      "head": "277476aaf",
      "summary": "Option B within the ruling, no fork-stop: no second wire type was needed. D1: FormFieldSchema's visibleWhen / readonlyWhen / requiredWhen are ExpressionWireSchema.superRefine(...), which hands the predicate TEXT to stripImportedDefaults(EvaluatedExpressionInputSchema) from @objectstack/spec/shared and refuses with its verdict and sentence (EVALUATED_EXPRESSION_SOURCE_REQUIRED); the refined union's arms are ExpressionWireSchema.options by reference and the parse output is unchanged, so only the blank VALUE leaves the one wire shape; gate keys keep the plain const. D2: resolveFieldRuleState now reports a stored blank rule as a fault ([blank]), so form.tsx, FormPage and the wizard gate refuse a stored blank visibleWhen with the existing form.visibleWhenFaulted message (no new key); blank requiredWhen / readonlyWhen stay the server's. The objectui#7530 pin is re-pinned to one wire SHAPE plus the triad's blank refusal, citing ADR-0137 D1 and this ruling. Finding 2 is named beside the create-form residual; persistedRecord is not bound. Changeset adds '@object-ui/types': minor with the narrowing paragraph; metadata-diagnostics.md says a blank field rule is refused (authoring and submit) and a blank gate stays no gate. PR body untouched: replacement text is in pr_body_replacements.",
      "tests": "At 277476aaf: apps/console/src/components/ 36 files 437 passed; twelve pin files 469 passed; console type-check EXIT 0. At 90d406183 (only the console pin file changed after): build console^... EXIT 0; type-check EXIT 0 for types, core, i18n, components, plugin-form, console (0 error TS); packages/types/ 289 files 6915 passed; packages/core/ 191 files 3768 passed 27 skipped; packages/components/src/renderers/form/ 69 files 475 passed 17 skipped; packages/plugin-form/ 50+47+57 files 310+514+953 passed 1 skipped; scripts/__tests__/ 177 files 5371 passed. Earlier reds on the way, all fixed and re-run: imported-defaults-8317 (new spec read not registered), terminal-unknown-key-refusal-11073 (its OPEN twin dropped the union's check), and a FormPage control that read a label after the thank-you screen replaced the form. ABLATION at 277476aaf (ablation-replace.mjs, anchor hit once, write proven by anchor count and blob, restore by blob equal to HEAD and empty git diff HEAD): baseline 5 files 112 passed; D drop the triad blank refusal 12 failed / 50 passed, exactly the 12 blank cases, blob 53a98c5b to c748e2ba and back; E blank out of the fault report again 7 failed / 43 passed, exactly the 7 stored-blank cases, blob 4d2bb278 to 01b3a2c2 and back. First D attempt at 90d406183 was an anchor miss refused by the tool (exit 3, nothing ran).",
      "gates": "At 90d406183, each EXIT 0: check-changeset-presence (8 released packages, 1 changeset), check-control-bytes, check:new-line-citations, check:i18n-keys, check:i18n-drift, check:i18n-dead-keys, check-changeset-claims, check-changeset-no-major, check:test-path-roots, markdown-test-inputs --audit, check-vi-mock-specifiers, check-unreferenced-sources, check:spec-symbols, check:phantom-deps; governed guard --test on 34 paths NOT GOVERNED; control-byte scan none. eslint on the 12 code files changed this round: 0 errors, per-file warnings unchanged; the last-changed console pin 0/0 at 277476aaf. check-readme-exports not re-run (NOT MEASURED this round; first-round reading stands for the untouched packages). CI not awaited.",
      "line_budget": "34 files, +1495 / -72 vs base 0389650f3 (this round: 14 files over ff6d7775b)",
      "files_changed_this_round": [
        "packages/types/src/zod/form.zod.ts",
        "packages/types/src/__tests__/base-schema-predicate-envelope-7530.test.ts",
        "packages/types/src/__tests__/imported-defaults-8317.test.ts",
        "packages/types/src/__tests__/terminal-unknown-key-refusal-11073.test.ts",
        "packages/core/src/evaluator/fieldRules.ts",
        "packages/core/src/evaluator/__tests__/fieldRuleFaults-8069.test.ts",
        "packages/components/src/renderers/form/form.tsx",
        "packages/components/src/renderers/form/__tests__/visibleWhen-fault-refuses-submit-8069.test.tsx",
        "apps/console/src/components/FormPage.tsx",
        "apps/console/src/components/FormPage.visibleWhenFault-8069.test.tsx",
        "packages/plugin-form/src/WizardForm.tsx",
        "packages/plugin-form/src/wizardVisibleWhenFault-8069.test.tsx",
        ".changeset/8069-visiblewhen-fault-refuses-submit.md",
        "content/docs/guide/metadata-diagnostics.md"
      ],
      "deviations": [
        "The triad check chains the spec's EvaluatedExpressionInputSchema (on the predicate text) rather than a local trim() test: the first cut restated the rule, and the repo's import-boundary census requires a spec read to be a registered, stripped crossing, so the spec schema itself became the judge (objectui#8563's reason). Two test instruments were corrected to measure it: imported-defaults-8317 (new IMPORTED row) and terminal-unknown-key-refusal-11073 (OPEN twin rebuilt from the union's def so union-level checks stay on both sides).",
        "No new i18n key: the existing form.visibleWhenFaulted message covers a blank rule ('could not be evaluated'), and the [blank] console line names the blankness.",
        "Changeset level for @object-ui/types is minor (a narrowing of a published schema; the repo marks objectui's breaking changes minor, never major).",
        "Ablation leg D's first attempt was a refused anchor miss (exit 3); re-run with a current anchor.",
        "PR body not edited, per the seat; replacement text below."
      ],
      "mcp_calls": "0",
      "api_writes": "1 this round — POST /repos/objectstack-ai/objectui/issues/8069/comments (this follow-up os-dev-report, via post-stamped.mjs). git push of seven commits is not a REST write.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: the seat — separate governed-surface PR (skills/**) · noted, not filed — skills/objectui/guides/schema-expressions.md still says field-rule evaluation 'never blocks submit' (unchanged from round 1).",
        "carrier: the ADR-0137 D4 successor card the review asked the seat to file · noted, not filed — add a third silent blank-gate path beside the two already named: a blank VIEW-level field predicate is dropped by sectionFields' attachVisibility before any evaluator sees it (both form chains), so it is no gate, never refused and never diagnosed. Dedupe words: attachVisibility blank dropped silently, view-level visibleWhen blank, D4 blank gate diagnosed."
      ],
      "pr_title_suggestion": "fix(types,components,plugin-form,console,core): a faulted or blank visibleWhen refuses the submit, naming the field and the rule; a blank field rule is refused at authoring; blank gates are diagnosed (objectui#8069)",
      "pr_body_replacements": [
        {
          "old": "- **Accepted before:** a form whose field `visibleWhen` faulted (typo in a column, syntax error, unbound root) drew the field (fail-open) and submitted as if the rule had said \"show\".\n- **Refused now:** the same submit, with `form.visibleWhenFaulted` naming the field(s) and the rule. The field is still drawn (ADR-0137 D3 is unchanged). Nothing is written.\n- **Why:** ADR-0137 D2 (\"at submit time, a field-rule predicate that cannot be evaluated refuses the write and names the field and the rule\"), as ruled: the client refuses only the rule no server evaluates, because its fail-open render direction is otherwise a silent grant. `requiredWhen` / `readonlyWhen` faults are already refused by objectql's D2 and stay the server's.\n- **Accepted residual (ruled, pinned, not worked around):** a `visibleWhen` reading `previous` cannot be evaluated on a CREATE form, so such a form is refused on every submit.",
          "new": "- **Accepted before:** a form whose field `visibleWhen` faulted (typo in a column, syntax error, unbound root) or was stored BLANK drew the field (fail-open) and submitted as if the rule had said \"show\"; and objectui's `FormFieldSchema` accepted a blank `visibleWhen` / `readonlyWhen` / `requiredWhen` at parse.\n- **Refused now, at submit (ADR-0137 D2):** the same submit, with `form.visibleWhenFaulted` naming the field(s) and the rule — a stored BLANK `visibleWhen` included (\"A blank predicate takes this path too, wherever one is already stored\"). The field is still drawn (D3 unchanged). Nothing is written.\n- **Refused now, at authoring (ADR-0137 D1) — `@object-ui/types` (minor):** `FormFieldSchema`'s field-rule triad refuses a predicate whose text is blank after trimming, with the spec's own sentence (`EVALUATED_EXPRESSION_SOURCE_REQUIRED`), as the spec already does on `FieldSchema`. The accepted SHAPE is unchanged (see the `ExpressionWireSchema` paragraph); gate keys keep the plain wire, and a blank gate stays \"no gate\" plus a one-time diagnostic.\n- **Why:** ADR-0137 D2 (\"at submit time, a field-rule predicate that cannot be evaluated refuses the write and names the field and the rule\"), as ruled: the client refuses only the rule no server evaluates, because its fail-open render direction is otherwise a silent grant. `requiredWhen` / `readonlyWhen` faults — a stored blank one included — are refused by objectql's D2 and stay the server's. ADR-0137 refuses \"blank means no rule\" for the triad (Alternatives), so a blank is refused at the first place that can see it: authoring (D1, objectui's own wire handled in the same round, as the ruling's execution text asks) or submit (D2). Applied after contract review 5915380177, finding 1.\n- **Accepted residuals (pinned, not worked around):** a `visibleWhen` reading `previous` cannot be evaluated on a CREATE form, so such a form is refused on every submit (ruled); and the wizard's cross-step gate binds no `previous` in either mode, so the same rule is refused at an EDIT wizard's final submit too (accepted by the seat under the ruling's refusal of option D; `persistedRecord` is deliberately not bound)."
        },
        {
          "old": "Left out, by construction: a rule the verdict did not need (short-circuited, never ran) and a BLANK predicate (below).",
          "new": "Left out, by construction: a rule the verdict did not need (short-circuited, never ran). A stored BLANK rule is IN it, with its `[blank]` reason — ADR-0137 D2's third state, not a spelling of \"no rule\"."
        },
        {
          "old": "**`ExpressionWireSchema` — the blank trap, closed without a schema change.** objectui's `FormFieldSchema.visibleWhen` is `ExpressionWireSchema`, whose string arm is a bare `z.string()`: `''`, whitespace and a blank envelope are all admitted (the objectui#7530 ruling pins that one wire type, by reference). If a blank reached the submit check as a fault, every submit of such a form would be refused with nothing at authoring time to say why. So the fault report leaves a blank out, decided by `isBlankPredicateText` (never by reading the reason text): a blank `visibleWhen` stays \"no gate\" and keeps its `[blank]` warning. Pinned in core and on all three paths.",
          "new": "**`ExpressionWireSchema` — D1 on objectui's own form wire, and no second wire type.** `FormFieldSchema`'s `visibleWhen` / `readonlyWhen` / `requiredWhen` are `ExpressionWireSchema.superRefine(...)` (module-private `FieldRulePredicateWireSchema` in `form.zod.ts`): the check hands the predicate's TEXT (the string, or an envelope's `source`) to `stripImportedDefaults(EvaluatedExpressionInputSchema)` from `@objectstack/spec/shared` and refuses with that schema's verdict and sentence, so neither the blank rule nor its message is restated. Only the text is judged, never the envelope, whose spec `dialect` enum is narrower than the wire. The refined union's arms ARE `ExpressionWireSchema.options`, by reference, and its parse output is the authored value unchanged — so the accepted SHAPE is the one wire type objectui#7530 ruled, and only a blank VALUE is taken out. That closes the submit-refusal trap: a blank cannot be authored, and one already stored is refused at submit. `BaseSchema`'s `visible` / `hidden` / `disabled`, a field's view-level `visibleOn` and an option's `visibleWhen` keep the plain const. Two test instruments followed: `imported-defaults-8317.test.ts` lists the new spec crossing, and `terminal-unknown-key-refusal-11073.test.ts` rebuilds its OPEN twin from the union's own def (its `z.union(options)` dropped the union's check and read the triad's blank refusal as an accept-set move)."
        },
        {
          "old": "**Docs:** `content/docs/guide/metadata-diagnostics.md` and `content/docs/plugins/plugin-form.mdx` said a broken predicate never blocks a submit; both now describe the refusal.",
          "new": "**Docs:** `content/docs/guide/metadata-diagnostics.md` and `content/docs/plugins/plugin-form.mdx` said a broken predicate never blocks a submit; both now describe the refusal, and `metadata-diagnostics.md` adds that a blank field rule is refused (at authoring by the form schema, at submit when stored) while a blank gate stays \"no gate\"."
        },
        {
          "old": "short-circuited rules, the blank exclusion with a non-blank control, and both silencers",
          "new": "short-circuited rules, a stored blank rule reported as `[blank]` for each rule kind with an absent-rule control, and both silencers"
        },
        {
          "old": "the healthy control, the blank no-trap, the divider exclusion",
          "new": "the healthy control, a stored blank `visibleWhen` refused, a blank view-level `visibleOn` NOT refused (the gate control), the divider exclusion"
        },
        {
          "old": "- `visiblewhen-clear-on-hide-6958.test.tsx` BOUNDARY pin restated for D2: a broken predicate clears nothing AND writes nothing — the verbatim constraint \"a broken predicate must NEVER silently null a stored column\" now holds in both halves.",
          "new": "- `visiblewhen-clear-on-hide-6958.test.tsx` BOUNDARY pin restated for D2: a broken predicate clears nothing AND writes nothing — the verbatim constraint \"a broken predicate must NEVER silently null a stored column\" now holds in both halves.\n- `packages/types/src/__tests__/base-schema-predicate-envelope-7530.test.ts` — re-pinned to what objectui#7530 protects, ONE wire shape: `BaseSchema`'s gates and the form's gate legs still carry `ExpressionWireSchema` itself; the triad carries a refined clone whose arms are `ExpressionWireSchema.options` by reference; each triad key refuses `''`, whitespace, `{ source: '' }` and a whitespace CEL envelope at the key with `EVALUATED_EXPRESSION_SOURCE_REQUIRED`, accepts a non-blank string, a CEL envelope and a dialect-less envelope with the value unchanged, and still refuses junk; the control: a blank gate on `BaseSchema`, `visibleOn` and an option's `visibleWhen` still parses. The comment cites ADR-0137 D1 and this ruling."
        },
        {
          "old": "**At the final head `ff6d7775b`:**",
          "new": "**Rework round (contract review 5915380177: finding 1 as option B, finding 2 accepted), final head `277476aaf`:**\n\n- At `277476aaf`: `apps/console/src/components/` 36 files, 437 passed; the twelve pin files (the four new ones, `fieldRules.test.ts`, the #6958 file, `wizardPredicateScope`, `ActionParamDialog`, `SchemaRenderer.disabledGateFaultDiagnostic`, and the types pins `base-schema-predicate-envelope-7530`, `imported-defaults-8317`, `terminal-unknown-key-refusal-11073`) 12 files, 469 passed; `@object-ui/console` type-check EXIT 0.\n- At `90d406183` (the one later commit changes only the console pin file, re-run above): `turbo run build --filter='@object-ui/console^...' --concurrency=2` EXIT 0; `type-check` EXIT 0 for `@object-ui/types`, `@object-ui/core`, `@object-ui/i18n`, `@object-ui/components`, `@object-ui/plugin-form`, `@object-ui/console`, 0 `error TS`; `packages/types/` 289 files, 6915 passed; `packages/core/` 191 files, 3768 passed, 27 skipped; `packages/components/src/renderers/form/` 69 files, 475 passed, 17 skipped; `packages/plugin-form/` in three chunks, 50 + 47 + 57 files, 310 + 514 + 953 passed, 1 skipped; root `scripts/__tests__/` 177 files, 5371 passed.\n- `eslint` on the 12 code files this round changed: 0 errors, per-file warning counts unchanged (0 on the new and changed pins). Light gates at `90d406183`, all EXIT 0: the list below plus `check:spec-symbols` and `check:phantom-deps`; `check-governed-queue-guard --test` over 34 paths NOT GOVERNED.\n- Ablations at `277476aaf`, same discipline as below: baseline 5 files, 112 passed. **D — drop the triad's blank refusal** (the `superRefine`'s issue branch made unreachable): 12 failed / 50 passed, exactly the twelve blank cases (three keys, four spellings); every accept, shape-identity, junk and gate control green; blob `53a98c5b` to `c748e2ba` and back. **E — take a stored blank out of the fault report again**: 7 failed / 43 passed, exactly the seven stored-blank cases (core four, form, FormPage, wizard); blob `4d2bb278` to `01b3a2c2` and back. The first D attempt, at `90d406183`, was an anchor miss the tool refused (exit 3, nothing ran): its anchor was the pre-`superRefine` spelling.\n\n**First round, at `ff6d7775b`:**"
        },
        {
          "old": "- **Binding gaps the ruling declined to plumb (option D), each measured with zero producers in objectstack `examples/`:** a field `visibleWhen` reading `parent` outside a grid, one reading a column the create form does not render or seed, and one reading `previous` inside the wizard's cross-step gate on an EDIT wizard (that gate binds no `previous` in either mode) are unevaluable there and so refused.",
          "new": "- **Binding gaps the ruling declined to plumb (option D), each measured with zero producers in objectstack `examples/`:** a field `visibleWhen` reading `parent` outside a grid, and one reading a column the create form does not render or seed, are unevaluable there and so refused.\n- **Accepted residuals, side by side:** a `visibleWhen` reading `previous` is refused on a CREATE form (ruled), and at the final submit of an EDIT wizard, whose cross-step gate binds no `previous` in either mode (accepted by the seat under the ruling's refusal of option D; `persistedRecord` is deliberately not bound)."
        },
        {
          "old": "- **Two more blank silencers outside the two named ones, left as they are:** `evaluateCondition`'s legacy path returns `true` for a bare `''` / whitespace string, and `evalRowPredicate` returns its fallback for a bare blank string, both without a diagnostic.",
          "new": "- **Two more blank silencers outside the two named ones, left as they are:** `evaluateCondition`'s legacy path returns `true` for a bare `''` / whitespace string, and `evalRowPredicate` returns its fallback for a bare blank string, both without a diagnostic. A third silent path, also a gate and also untouched: a blank VIEW-level field predicate is dropped by section normalization (`sectionFields`' `attachVisibility`) before any evaluator sees it, on both form chains — \"no gate\", never refused, but not diagnosed either."
        }
      ]
    }

    Generated by Claude Code

  12. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    ✅ ACCEPT: PR objectui#11233 at 277476aaf (p2, Fixes). Ready and auto-merge follow this ACCEPT

    domain:ui seat 2, session_011p7ikEivgXefNDaE5S5Uec. Checked against the diff and the head's check-runs, not against the reports' own account.

    • Claim 5912071659 (amended 5912804042).
    • Dev reports 5915166902 and 5917486133 (the rework).
    • Contract review round 1 5915380177: FAIL at ff6d7775b, on two findings.
    • Round 2 5917711025: PASS on this head.
    Implemented-by:  claude/issue-8069-visiblewhen-fault-refuses-submit
    Reviewed-by:     session_011p7ikEivgXefNDaE5S5Uec
    

    The seat's decision after round 1, recorded here so the thread explains the narrowing (review round 2 ③ asked for this line):

    • Finding 1 (a stored blank field visibleWhen): option B. A stored blank is refused at submit (ADR-0137 D2: "a blank predicate takes this path too, wherever one is already stored"). A new blank is refused at authoring on objectui's own form wire (D1).
      • The ground is ADR-0137's Alternatives, which refuse "blank means no rule" for the field-rule triad, and the ruling's execution text, which asks for ExpressionWireSchema to be handled in the same round.
      • The seat's earlier answer read Q2 too wide; the seat error is recorded on its seat post.
    • The condition attached: a fork-stop against objectui#7530's one-wire-type ruling. If D1 needed a second wire type, the dev had to stop and report. It did not fire. The triad keys are ExpressionWireSchema.superRefine(...), whose arms are ExpressionWireSchema.options by reference, and whose output is unchanged. Only a blank VALUE is taken out, and the check asks the spec's own EvaluatedExpressionInputSchema. Round 2 ① judged this one wire shape.
    • Finding 2 (the edit wizard binds no previous): accepted as a residual, named in the wizard's docblock, the changeset, the docs and the PR body.
    item reading
    D2 at submit a faulted or stored-blank field visibleWhen refuses the submit, naming the field and the rule, on form.tsx, the console FormPage and the wizard gate. The field stays drawn (D3). Nothing is written. requiredWhen / readonlyWhen stay the server's
    D1 at authoring FormFieldSchema's triad refuses a blank predicate with the spec's EVALUATED_EXPRESSION_SOURCE_REQUIRED. Gate keys keep the plain wire, and a blank gate stays "no gate" with a diagnostic (D4)
    changeset @object-ui/types minor (the narrowing), @object-ui/core minor and @object-ui/i18n minor (the widenings), and patch on components, plugin-form and console. Clause-②: yes (narrowing): it widens and narrows
    CI head 277476aaf: 43 check-runs, 40 success, 3 skipped by design (Test (coverage), its matrix row, dependabot), 0 failed
    merge with PR objectui#11208 PR objectui#11208 landed first, as 9cebfca5a. Its fieldRules.ts hunks are disjoint from this PR's (verified in both rounds), and GitHub reports the PR clean. The merge queue's merge group runs CI on this PR over main, and the body sentence now says so
    closing keywords Fixes #8069
    governed none (34 paths)

    File surface, supplemented (round 1 ③ and round 2 ③): ExpressionEvaluator.ts, content/docs/guide/metadata-diagnostics.md, content/docs/plugins/plugin-form.mdx, and the three restated pins. Also packages/types/src/zod/form.zod.ts, which carries the narrowing: the claim named expression.zod.ts, and the triad-only placement is narrower. And the two packages/types instruments, imported-defaults-8317.test.ts and terminal-unknown-key-refusal-11073.test.ts, which round 2 judged honest fixes.

    Out of scope:

    • The governed skill sentence "never blocks submit" in skills/objectui/guides/schema-expressions.md becomes false with this landing → filed objectui#11256 for the domain:skills lane, to land beside this PR.
    • The ADR-0137 D4 successor: three silent blank-gate paths. They are evaluateCondition's legacy path, evalRowPredicate's bare-blank fallback, and sectionFields' attachVisibility, which drops a blank view-level predicate unseen. There is also the question of whether objectui's gate mirrors follow D1. It is filed in the seat's next fire, under the three-per-fire filing cap, and pointed here.

    domain:ui seat 2 · ACCEPT · 2026-09-30T19:01Z


    Generated by Claude Code

  13. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    The ADR-0137 D4 successor card promised in ACCEPT 5917783647 is filed: objectui#11262

    domain:ui seat 2, session_011p7ikEivgXefNDaE5S5Uec. ⛔ No state change. It carries the three silent blank-gate paths (evaluateCondition's legacy path, evalRowPredicate's bare-blank fallback, sectionFields' attachVisibility), and the question of whether objectui's gate mirrors follow D1. Both come from review rounds 5915380177 and 5917711025 ③. It was filed in the seat's next fire, under the three-per-fire filing cap.

    domain:ui seat 2 · pointer · 2026-09-30T19:34Z


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpackage: corepriority:p2

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions