Repository navigation
ObjectValidationEngine 产品拍板:接线 console 提交预检,还是明确弃用——#3103 修好了语义,但零消费者 #3110
Description
Activity
拍板:选 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 协作角度:活着的引擎是误导机制,弃用注释是教学机制
- 接线 = 造一个常驻的 planted-premise 发生器。 行为 parity 是跨仓的,现有闸门够不着:fix(types,core):
*Validation五件套派生自 spec 17,引擎不再与服务端逐条相反 (#3103) #3107 的 mutation test 钉的是「客户端 vs 我对服务端的理解」,不是「客户端 vs 服务端」。framework 每动一次 rule-validator(record:activity的 feed 结构上不可能有内容——11 个声明的输入全是空 feed 上的过滤器(objectstack#4413 的形状,第三次) #3165 / 列头排序在服务端分页下只排了当前页:data-table 的排序意图没有出口能到达发请求的那一层 #3106 / fix(deps): 模态表单里 lookup 搜索框聚焦即被夺回——补丁修复 focus-scope 栈驱逐竞态 (#3183) #3184 这个频率),客户端就漂一次,然后就会有 agent 从客户端行为反推出错误前提去报 bug —— objectstack#2901 的剧本重演。 - 弃用会被照做,活着会被扩展。
@deprecated+ 「server is the authority」会被下一个 agent 读到并遵守;活引擎则会被扩展,而最自然的扩展方向恰恰是 spec 有意删掉的东西(客户端 SELECT-then-check 的 uniqueness = TOCTOU 竞态)。 - 成本不对称。 B 是一小时 + changeset;A 是 adapter +
/meta通路 +ObjectSchema.validations类型 + 接线 + e2e + 永久镜像税。*Validation五件套:注释冒领 Spec v2.0.1、condition 形状已漂移——对照 spec 17 triage + 派生/闸门(objectstack#4115) #3103 已是第二轮投入,B 保证没有第三轮。
附:选 A 的前置其实比开 issue 时估的便宜(
@objectstack/formula已在 core 的依赖里,fieldRules.ts已在用ExpressionEngine,adapter 不引新依赖)。这一条对 B 不利,如实记在这里 —— 但便宜的是首次接线,贵的是之后每次 framework 改动都要跟的镜像税,而那正是第 2 条证明躲不掉的。执行
- 结论记录(本评论)
- objectui:
ObjectValidationEngine/defaultObjectValidationEngine/validateRecord标@deprecated,模块头写清「server is the authority + 预检走 dry-run」。五个 spec 派生类型不动 —— 它们是 objectstack#4115 台账的锚,与引擎存废无关。 - 把决策做成机制,而不是注释(排查「手抄 spec 清单 + "keep in sync" 注释」模式:objectui 侧六处,目前全部一致但零闸门(framework#3786) #3017:comments are not mechanisms):加一个 tripwire 测试,扫
packages/*/src,若有生产模块 import 了这个引擎就红,失败文案直指「决策在ObjectValidationEngine产品拍板:接线 console 提交预检,还是明确弃用——#3103 修好了语义,但零消费者 #3110,要翻案先翻 issue」。没有这个,弃用注释和它取代的那条冒领注释是同一种东西。 - framework 开 issue:写路径 validate-only / dry-run(产品确认要预提交 UX 后再排期)
Tripwire:什么条件下重启选项 A
offline 写入进路线图之日(
types里已有OfflineConfig)。离线时没有服务端可问,客户端求值器从「冗余」变成「必需」。那时的前置条件必须是:spec 包携带 conformance fixtures —— golden 的「规则 + 记录 + 判定」三元组,两端 CI 各跑同一套。那是唯一能把跨仓行为 parity 从约定变成机制的办法;没有它,A 就还是第 2 条描述的那个结构性不可靠。
- presentation 谓词(
- added a commit that references this issue
on Jul 31, 2026 执行完毕
- 结论记录(决策评论:选 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并写明理由。关闭。
执行清单更新:最后一项已有结论 —— 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
- framework dry-run issue —— objectstack#4372 已关闭为
- added a commit that references this issue
on Sep 25, 2026 - added a commit that references this issue
on Sep 28, 2026
背景(#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 也核实了三个事实:@object-ui/core已发布的公共 API(src/index.ts → validation/index.ts → validators/index.ts),即「对外承诺、对内无人接线」。rule_violation,没有客户端预检。*Validation五件套:注释冒领 Spec v2.0.1、condition 形状已漂移——对照 spec 17 triage + 派生/闸门(objectstack#4115) #3103 定级时明写「风险全部落在未来接线时静默分歧」——分歧已消,但接线本身没发生。*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,接线本身不会拦服务端允许的写入。已核实的前置(比预想的小):
@object-ui/core已依赖@objectstack/formula,evaluator/fieldRules.ts已在 importExpressionEngine—— 与服务端同一引擎,已在客户端 bundle 里。接线只需一个 CEL-backedValidationExpressionEvaluator适配器(包一层ExpressionEngine.evaluate,scope 绑{record, previous}),不引新依赖。默认的SimpleExpressionEvaluator只认单比较,复杂谓词 fail-open(不报错也不预检),不能当生产预检用。ObjectSchema类型未声明validations键(服务端读的是objectSchema.validations)。需核实/meta是否已把 validations 带到客户端(大概率带,类型没声明而已),再补类型声明。json_schema变体引擎不实现(现在响亮 warn):接线时决定补齐(ajv 进 bundle 的代价)还是保持 warn。选项 B:明确弃用
如果客户端预检不在路线图:引擎(
ObjectValidationEngine/defaultObjectValidationEngine/validateRecord)标@deprecated,changeset 写清楚「预检不做,以服务端错误为准」。类型不动 —— 五个 spec 派生类型是 objectstack#4115 台账的锚,与引擎存废无关。价值:止住无消费者公共 API 的维护成本,防止第三轮投入。验收标准
/metavalidations 通路核实 + 接线点 + e2e(违规提交在客户端被拦,且与服务端结论一致)@deprecated标注 + changeset关联:#3103(语义收敛,PR #3107 已合并)、objectstack#4115(台账)。