Skip to content

ObjectValidationEngine 产品拍板:接线 console 提交预检,还是明确弃用——#3103 修好了语义,但零消费者 #3110

Description

@os-zhuang

背景(#3103 / PR #3107 Step 0 triage 的遗留结论)

PR #3107 把 ObjectValidationEngine 五类规则语义与服务端 objectql/src/validation/rule-validator.ts 逐条收敛(谓词极性、envelope condition、when/then/otherwise、regex + 4 具名格式、initialStates、spec 默认值、priority 排序),闸门 + mutation-test 钉住。但同一轮 triage 也核实了三个事实:

  1. 引擎在仓内零生产消费者 —— 全仓 grep 只命中它自己的测试文件;console 的提交/校验路径不经过它。它同时是 @object-ui/core 已发布的公共 API(src/index.ts → validation/index.ts → validators/index.ts),即「对外承诺、对内无人接线」。
  2. 现状 = 修对了但没人用:用户提交后才吃服务端 rule_violation,没有客户端预检。*Validation 五件套:注释冒领 Spec v2.0.1、condition 形状已漂移——对照 spec 17 triage + 派生/闸门(objectstack#4115) #3103 定级时明写「风险全部落在未来接线时静默分歧」——分歧已消,但接线本身没发生。
  3. 这已是第二轮投入(*Validation 五件套:注释冒领 Spec v2.0.1、condition 形状已漂移——对照 spec 17 triage + 派生/闸门(objectstack#4115) #3103 修语义)。不拍板,下一个 agent 还会撞上它再来第三轮。

这不是代码任务,是产品决策,两条路互斥。先拍板,再开实现 issue。

选项 A:接线(把客户端预检做成真的)

用户价值:script / cross_field / state_machine / format / conditional 违规在提交前亮出来,不等 round trip。语义已对齐 + fail-open,接线本身不会拦服务端允许的写入。

已核实的前置(比预想的小):

  • CEL evaluator 已经在场:@object-ui/core 已依赖 @objectstack/formula,evaluator/fieldRules.ts 已在 import ExpressionEngine —— 与服务端同一引擎,已在客户端 bundle 里。接线只需一个 CEL-backed ValidationExpressionEvaluator 适配器(包一层 ExpressionEngine.evaluate,scope 绑 {record, previous}),不引新依赖。默认的 SimpleExpressionEvaluator 只认单比较,复杂谓词 fail-open(不报错也不预检),不能当生产预检用。
  • 元数据通路有缺口:客户端 ObjectSchema 类型未声明 validations 键(服务端读的是 objectSchema.validations)。需核实 /meta 是否已把 validations 带到客户端(大概率带,类型没声明而已),再补类型声明。
  • 接线点待定位:record create/update 提交路径(表单 submit / app-shell 保存 hook),定位后写进实现 issue。
  • json_schema 变体引擎不实现(现在响亮 warn):接线时决定补齐(ajv 进 bundle 的代价)还是保持 warn。

选项 B:明确弃用

如果客户端预检不在路线图:引擎(ObjectValidationEngine / defaultObjectValidationEngine / validateRecord)标 @deprecated,changeset 写清楚「预检不做,以服务端错误为准」。类型不动 —— 五个 spec 派生类型是 objectstack#4115 台账的锚,与引擎存废无关。价值:止住无消费者公共 API 的维护成本,防止第三轮投入。

验收标准

  • 拍板结论写进本 issue(A 或 B + 一句话理由)
  • 选 A → 开实现 issue:CEL adapter + /meta validations 通路核实 + 接线点 + e2e(违规提交在客户端被拦,且与服务端结论一致)
  • 选 B → @deprecated 标注 + changeset

关联:#3103(语义收敛,PR #3107 已合并)、objectstack#4115(台账)。

Activity

  1. self-assigned this
    on Jul 31, 2026
  2. os-zhuang commented on Jul 31, 2026

    @os-zhuang
    ContributorAuthor

    拍板:选 B —— 弃用引擎;预提交反馈改走服务端 dry-run

    一句话理由:enforcement 单实现在服务端;要预提交 UX 就加 validate-only,而不是养第二个求值器。

    下面是支撑这个结论的四条事实,前两条是开 issue 后补核实出来的。

    1. 这条架构线仓里已经画好了,选 A 等于抹掉它

    packages/core/src/evaluator/fieldRules.ts 的头注释明文写着:客户端 CEL 委托给 canonical 的 ExpressionEngine,"rather than re-implementing a parallel evaluator"(ADR-0036 + formula 的 "No private expression DSL")。

    现状因此是有边界的,不是随意的:

    • presentation 谓词(visibleWhen / readonlyWhen / requiredWhen)客户端评估 —— 它们只影响 UX,判错了最坏是多显示一个字段。
    • enforcement 谓词(validation rules)单实现在服务端写路径 —— 判错了就是数据或可用性事故。

    ObjectValidationEngine 恰好就是那句注释里说的 "parallel evaluator"。接线等于官方给双实现背书,并且是在 enforcement 这一侧——恰恰是边界划在的那一侧。

    2. 刚收敛完一轮,现在就还剩一处已知分歧

    服务端带 ADR-0113 的 legacy-violation 豁免:只有当合并后状态违规、且这次写入使其恶化时才拒(存量违规行原样放行);客户端引擎没有这个逻辑。所以 A 接线当天就会出现「客户端拦、服务端放」——用户在一条老数据上改个无关字段,预检报红,而服务端本来会接受。

    这一条是整个判断的实证核心:PR #3107 是一轮认真的、逐条对着服务端源码做的收敛,配了 8 个 mutation test,仍然留了尾巴。说明靠镜像维护跨仓行为 parity 是结构性不可靠,不是这一轮执行力的问题。

    3. dry-run 严格优于客户端预检

    在写路径加 validate-only 模式(跑真的 rule-validator、不落库、返回 violations):同样的 UX、零 parity 风险,还顺带覆盖客户端永远做不了的两类 —— unique(要查库)和 json_schema(ajv 在服务端)。也符合既有原则:ObjectQL 已提供的能力不在 objectui 重实现。

    UX 收益本身也比看起来薄:预检只在「提交了无效记录」那一次省一个 round trip,而服务端拒绝已经是结构化的(field/code/message,#3957 起还带 label),console 渲染得很好。Salesforce 的 validation rules 同样是 save 时服务端评估,没有客户端预检——没有行业压力。

    4. AI 协作角度:活着的引擎是误导机制,弃用注释是教学机制

    附:选 A 的前置其实比开 issue 时估的便宜(@objectstack/formula 已在 core 的依赖里,fieldRules.ts 已在用 ExpressionEngine,adapter 不引新依赖)。这一条对 B 不利,如实记在这里 —— 但便宜的是首次接线,贵的是之后每次 framework 改动都要跟的镜像税,而那正是第 2 条证明躲不掉的。

    执行

    Tripwire:什么条件下重启选项 A

    offline 写入进路线图之日(types 里已有 OfflineConfig)。离线时没有服务端可问,客户端求值器从「冗余」变成「必需」。

    那时的前置条件必须是:spec 包携带 conformance fixtures —— golden 的「规则 + 记录 + 判定」三元组,两端 CI 各跑同一套。那是唯一能把跨仓行为 parity 从约定变成机制的办法;没有它,A 就还是第 2 条描述的那个结构性不可靠。

  3. os-zhuang commented on Jul 31, 2026

    @os-zhuang
    ContributorAuthor

    执行完毕

    • 结论记录(决策评论:选 B)
    • objectui 弃用 —— chore(core): 弃用 ObjectValidationEngine —— 规则 enforcement 单实现在服务端 (#3110) #3111 已合并。ObjectValidationEngine / defaultObjectValidationEngine / validateRecord 标 @deprecated,模块头写清「server is the authority + 预检走 dry-run」+ 重启条件。零行为变更、零删除,barrel 照常再导出。
    • 决策做成机制 —— validation-engine-stays-unwired.test.ts。
    • framework dry-run issue —— objectstack#4372(明确建议暂不排期,先作决策指针存在,免得下一个 agent 重新发明客户端预检)。

    五个 spec 派生类型未动(objectstack#4115 台账的锚,与引擎存废无关);字段/记录 shape 校验未动(schema-validator.ts / validation-engine.ts 是客户端表单校验,不是服务端规则的镜像 —— 已在 validation/index.ts 头注释点明,免得下一个 agent 顺手误伤)。

    闸门的一处自我修正,值得记一笔

    tripwire 初版用裸正则扫文件内容,立刻在 types/src/data-protocol.ts 上报红 —— 那里只是在文档注释里正当地提了一句引擎名。这说明初版写的是「提及禁令」,而非声称的「可达性禁令」。

    改为先剥注释与字符串体、再匹配代码。顺带保住了扫代码(而非扫 import 列表)的好处:import * as core + core.ObjectValidationEngine 这类间接触达也抓得到,纯 import 正则会漏 —— T2 变异证明了。

    闸门另有自体检(断言扫到 >500 文件且命中引擎自身):扫描路径写坏会导致扫 0 个文件、永远报绿,那正是 objectstack#4115 的失败模式。

    mutation-test 3/3 红:T1 直接 import、T2 namespace 间接、T3 扫描路径写坏。

    重启条件(已写进代码,不只写在这里)

    offline 写入进路线图之日(OfflineConfig)。离线时没有服务端可问,客户端求值器从「冗余」变成「必需」。届时前置条件必须是 spec 携带 conformance fixtures —— golden 的「规则 + 记录 + 判定」三元组,两仓 CI 各跑同一套。要翻案先翻本 issue,再更新 tripwire 的 ALLOWED 并写明理由。

    关闭。

  4. os-zhuang commented on Aug 1, 2026

    @os-zhuang
    ContributorAuthor

    执行清单更新:最后一项已有结论 —— objectstack#4372 关闭为 not planned

    上面「执行完毕」那条里,framework dry-run issue 是唯一的开放项:当时的措辞是「明确建议暂不排期,先作决策指针存在」,把排期问题挂给产品确认。产品评估已做,落在否定分支:

    • framework dry-run issue —— objectstack#4372 已关闭为 not planned(结论评论)。结论:没有当前业务价值,不排期。

    评估要点(压缩版,完整版在结论评论里):

    • 「提交前反馈」的增量为零或存疑。 objectui 对预检错误和真实写入拒绝走的是同一条渲染链(form.tsx 服务端拒绝分支 → announceFieldErrors:字段级 setError、tab 切换、reveal + toast)。提交点先 dry-run 再真写,体验与直接提交完全相同,只是每次成功提交多一个 round trip。显式「校验」按钮无需求证据;实时校验成本高(update dry-run 每次要读 prior record)。
    • 非 UI 消费者今天一个都没有。 导入预检已有自己的 dryRun;CLI 校验、AI 生成记录自检是推测性需求。
    • 反面成本是实的。 没有消费者的写路径 data-safety flag 正是 objectstack#4052 的失败模式:BatchOptionsSchema.validateOnly 声明了 dry-run、没有任何批量面读它,调用方发 validateOnly: true 想预览,拿到的是真的执行了。要把新 flag 做到不说谎,代价是 spec schema、engine 选项键闸门、client SDK 双 surface、objectui 契约 + feature-detect 的永久税。

    本 issue 的结论不变。 选 B 依然成立,弃用标注与 validation-engine-stays-unwired.test.ts 继续有效。变的只是「预提交 UX 什么时候补」的排期答案:今天不补。客户端预检仍然不做;正确路径依旧是提交、渲染服务端结构化拒绝。

    重开条件写在 objectstack#4372 的结论评论里(具体的产品需求 / 出现第二个真实非 UI 消费者 / AI 自检成为真实工作流),与本 issue 的重启条件(offline + spec conformance fixtures)相互独立。

    一处措辞的观察(未改,供单独决定)

    object-validation-engine.ts 的模块头与 tripwire 的失败文案都写着「for pre-submit feedback, use a validate-only (dry-run) write instead」。作为架构判断它依然正确——真要做就该这么做,这正是 #4372 论证过的形态。但字面读起来像「那东西已经有了,去用」,而它不存在、且现在明确不会建。顺着文案走的下一个 agent 会绕一小圈才在 objectstack#4372 落到答案(#4372 关闭时带了完整理由,所以最终会落对,只是多一跳)。

    要不要把那两处收紧成条件句(「若将来要做,机制上的答案是 …,现状见 objectstack#4372」)可以单独决定;目前保持原样,没有为改措辞开 PR。


    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

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions