fix(types,core): *Validation 五件套派生自 spec 17,引擎不再与服务端逐条相反 (#3103) - #3107
Merged
Conversation
…engine stops disagreeing with the server (#3103) Five rule types in `data-protocol.ts` were hand-written interfaces labelled `(ObjectStack Spec v2.0.1)` while the installed spec was 17.0.0-rc.0. Nothing bound them to the spec, so fifteen majors of drift accumulated with `tsc` silent and the comment still vouching for it — the planted-premise failure class objectstack#4115 was filed for. They are now `z.input` derivations of the spec's schemas. `z.input`, not `z.infer`: objectui consumes AUTHORED metadata off `/meta`, before the spec's defaults and ExpressionInput canonicalization run, so `condition` really is `string | {dialect, source}` and `active`/`events`/`priority` really can be absent. What the copies had drifted into, and what each cost the user — the engine is a client PRE-CHECK of rules `objectql/src/validation/rule-validator.ts` enforces, so every one of these is a disagreement with the authority: - Predicate POLARITY was inverted. The server violates a rule when the predicate is TRUE; the engine violated it when FALSE. Every spec-authored `script`/`cross_field` rule got the opposite verdict. - `conditional` read `condition` + `rules[]` where the spec says `when` + `then`/`otherwise` → silent no-op, and `otherwise` never ran at all. - `format` read `pattern` where the spec says `regex` → `undefined.test()` threw into a catch that reported a VIOLATION, blocking writes the server accepts. - Envelope-valued conditions hit `expression.trim()`, threw, and read as "passes" → silent no-op. - An absent `active` disabled the rule; an absent `events` threw. Both arrive absent because the spec defaults them at parse time. - `initialStates` (objectstack#3165) and `priority` were missing entirely; `events: 'delete'` was still advertised after objectstack#3184 retired it. `unique`/`async`/`range` have no spec counterpart — the spec removed the first two deliberately (uniqueness → a unique index, since SELECT-then-INSERT is racy; async → the form layer) — and are now @deprecated, with a test proving the spec's union rejects them, so the comment is load-bearing rather than decorative. Gates, each mutation-tested: the compile-time pins in `validation-rule-spec-parity.test.ts` (live because tsconfig.test.json compiles it, #3009) go TS1360 when a derivation is reverted, and the engine suite goes red for polarity, envelope coercion, `otherwise`, `initialStates` and the spec defaults. `ConditionalValidation.then/otherwise` carry one pinned divergence — the spec's published types erase them to `unknown` (objectstack#4171) — with an inverted pin that fails the day upstream fixes it. Canonicity now rides on the import and the gate, not the comment: `grep "Spec v2.0.1" packages/types/src/data-protocol.ts` is empty, and objectstack#4115's ledger drops 120 -> 115. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
This was referenced Jul 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3103. Ledger: objectstack#4115 120 → 115.
Step 0 — 引擎接线 triage(定级依据)
ObjectValidationEngine在 objectui 内没有生产消费者。全仓 grep 只找到它自己的测试文件;console 的客户端校验路径不经过它。但它是@object-ui/core的已发布公共 API(src/index.ts→validation/index.ts→validators/index.ts),而且模块头明文宣称 "ObjectStack Spec v2.0.1 compliant"。所以业务价值定级不是「线上正在坏」,而是:这是一个对外承诺 spec 合规、却与服务端逐条相反的 API。任何按注释接线它的宿主应用(或 agent)会拿到与服务端相反的判定。这反而把 Step 2 的力度调高了——没有 in-repo 调用方要兼容,所以直接收敛到 spec 语义,不留 back-compat 分支;风险全部落在「未来接线时静默分歧」,而那正是派生 + 闸门要挡的东西。
Step 1 — 逐个对照 spec 17 源(读 schema 源码,非注释)
对照
@objectstack/spec/src/data/validation.zod.ts(17.0.0-rc.0)与服务端权威实现objectql/src/validation/rule-validator.ts。BaseValidation.events: ('insert'|'update'|'delete')[]('insert'|'update')[],默认['insert','update']delete已作为已证实的 no-op 被退役(objectstack#3184)BaseValidation无prioritypriority: int 0..9999 = 100(低者先跑)active: boolean/severity必填ScriptValidation.condition: stringExpressionInput=string | {dialect,source}CrossFieldValidation.condition: stringfields[0]被读(定位报错字段)StateMachineValidation无initialStatesinitialStates?: string[]ConditionalValidation:condition+rules[]when+then/otherwiseFormatValidation:pattern+flags,8 个具名格式regex,4 个具名格式(email/url/phone/json)unique/async/rangejson_schema,不含前三者rangespec 从来没有过issue 里没料到的第 4 处实质漂移(最严重的一处)——谓词极性反了。
spec 的
.describe()、服务端模块头、以及checkPredicate实现三处一致:谓词为 TRUE ⇒ 规则被违反(if (result.value === true) return violation)。旧引擎是if (!result) return invalid——完全相反。也就是说 spec 写法的script/cross_field规则,客户端给出的是与服务端逐条相反的结论。(附:spec 的 cross_field 文档示例 #1 自身与
.describe()矛盾,写成了 assert-true。以实现为准;已在 PR 里记一笔,不阻塞。)每条漂移的用户可见后果(全部按「客户端预检 vs 服务端权威」判定):
condition→expression.trim()抛错 → catch 吞掉 → 旧极性下读作「通过」= 静默 no-op(用户失去客户端预检)。conditional→rule.condition为undefined,同上 = 静默 no-op;otherwise分支从未被求值。format→rule.pattern为undefined,undefined.test()抛错 → catch 里return {valid:false}= 误报违规(客户端拦了服务端允许的写入)。active→ 规则被跳过 = 静默 no-op;缺席的events→.includes抛 TypeError。Step 2 — 按 #3090 判别法分流
判别问题是「同一概念的两个方言」还是「两层不同概念」。这里是前者,而且比 SelectOption 更彻底:这些规则就是对象
ObjectSchema.validations里的那一份,服务端在写路径上评估,objectui 只是同一份规则的客户端预检。不存在独立的「UI 层校验规则」词汇。→ 派生 by reference。z.input<typeof Spec…Schema>派生。z.input而非z.infer:objectui 吃的是/meta上的作者态 JSON,尚未经过 spec 的 default 填充与 ExpressionInput 规范化。z.input才是 JSON 里真实的形状——condition是string | {dialect,source}(正是 issue 要的 wire 形状),active/events/priority/severity可以缺席。BaseValidation也派生(Omit<ScriptValidation,'type'|'condition'>),spec 往 base 加键会自动流进来。ConditionalValidation.then/otherwise。spec 把ValidationRuleSchema标注成z.ZodType<BaseValidationRuleShape>(带[key:string]: unknown的口袋类型),导致其 dist 里then/otherwise擦除成unknown(objectstack#4171)。整体派生会把一个可判别联合换成unknown—— 正是 DEBT 头部警告的「披着 burn-down 外衣的类型安全回退」。故只有这两个键改用 objectui 的精确联合,并配反向 pin:spec 哪天把它们标注对了,测试立刻红,提示退役这处分歧。unique/async/range与 spec 无同名冲突(不在台账里),保留但标@deprecated,注明 spec 为何移除、真正的机制在哪(唯一索引 / 表单层 / 字段 min-max),并由测试证明ValidationRuleSchema确实拒收它们——所以注释是真的。引擎侧同步收敛到服务端语义:极性、
.sourcecoerce(fail-open)、when/then/otherwise、regex+ 4 格式、initialStates、spec 默认值、priority排序;不实现的类型(spec 的json_schema)响亮 warn 而不是报「通过」。默认求值器仍不是 CEL,已明写并同时绑定 spec 的record.x与 objectui 历史的裸x作用域。Step 3 — 注释治理
grep "Spec v2.0.1" packages/types/src/data-protocol.ts→ 空(16 处清零,含 6 处 window-function 的冒领)。core 校验簇(validation/index.ts、validators/index.ts、引擎、引擎测试)与types/src/index.ts的校验导出块一并清理。canonicity 现在由 import/派生 + 闸门承载(#3017)。留了两处未动,属于别的簇、动它们只会撑大 diff:
types/src/ui-action.ts(6)、types/src/index.ts:868的 UI-Actions 段头(1)、types/src/app.ts(2)、core/src/query/query-ast.ts(3)、spec-derived-unions.test.ts(1)。建议随各自簇处理(ActionSchema已在 ALLOW 里)。Step 4 — 台账与 mutation-test
台账 120 → 115(五条全部删除,无一转 ALLOW)。派生守卫在删除前会自己报「5 symbols in DEBT that no longer collide」,删除后 ✅。
闸门 mutation-test(逐个删机制,证明会红) —— 9 个变异,8 红:
ConditionalValidation派生改回手写tsc -p tsconfig.test.jsonFormatValidation派生改回pattern手抄BaseValidation改为手写列举priority消失)initialStates插入检查active读作 falsy)otherwise分支field in record守卫M9 是如实记录的负面结果:objectui 校验的是记录快照,服务端校验的是 patch 负载,所以「字段缺席」在这里已经塌缩进「值为 undefined」,空值检查先接住了。该守卫保留是为了与
rule-validator.ts结构一一对应(以及未来空值语义若改动时的防御),但不声称它是闸门——测试注释里写明了这一点,而不是留一条看起来受保护、实际没有的机制。验收标准
grep "Spec v2\.0\.1" packages/types/src/data-protocol.ts为空验证
破坏面
@object-ui/types/@object-ui/core双 minor(按 AGENTS.md §9:objectui 的破坏性变更也标 minor,major 跟随 @objectstack)。规则类型的键改了(condition+rules[]→when+then/otherwise;pattern→regex),引擎极性反转,validateRecord的event不再收'delete'。仓内零消费者(只有 barrel 再导出),对外的宿主应用需要按上表迁移——changeset 正文里有完整迁移表。关联:objectstack#4115(台账)、#3090(判别法)、objectstack#4171(spec 类型擦除,本 PR 的 pinned divergence 依赖它)、objectstack#3165 / #3184(spec 侧的加键与退役)。后继 #3104。