fix(app-shell): 唯一候选规则 — union 只剩一个成员读过值时,给出它的具名诊断 - #3693
Merged
Conversation
… the value PR #3677 narrowed its content rule so it could not speak for `config.sort`, which left every union where some member rejected the value's type outright collapsed to `Invalid input`. When exactly one member accepted the type, naming it is a fact rather than a preference: it is the only member that read the value, so it is the only one whose complaint can be about what the author wrote. `sort: [{field: 'n', order: 'bogus'}]` now reports `config.sort.0.order` with the spec's own `expected one of "asc"|"desc"` instead of `config.sort` / `Invalid input`. The same holds for a `columns[].summary` written as a bad enum string, a form `sections[].fields[]` entry missing its `field`, and an array `filter[].value` whose offending element is now addressed directly. The two nested rules are disjoint rather than layered: censusing the members by #3677's categorical test, k=1 is this rule, k=every-member is the content rule, and everything else is nobody's. Order is unobservable while both guards hold, and is written content-first so #3677's narrowing line stays the thing that fails when removed. Verdicts are untouched — this runs inside the issue mapping, downstream of `ok`. Two of #3677's narrowing pins are superseded and re-pinned: they were asserting that no rule spoke there, which is exactly what this changes. Fixes #3678 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 7, 2026 21:45
This was referenced Aug 7, 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.
Fixes #3678
问题
PR #3677 把嵌套 union 的失败诊断按值的内容展开(
config.columns),并刻意收窄:在 union 节点上直接拒绝了值的类型的成员,压根没读过值,内容就不能作为它的证据 —— 于是它不是候选。这条收窄是必要的,config.sort(string | ColumnSort[])就是它挡住的那个邻居。收窄之后留下一个空档,就是本单:当这条范畴判定过后恰好只剩一个成员时,选它不是启发式。它是唯一读过这个值的成员,因此是唯一一个抱怨可能关于「作者写了什么」的成员;其余成员在类型上就停住了,展示它们等于描述一个作者从未选择的形状。
实测(
@objectstack/spec,修前):spec 自己写的那句引导消息(#4001)就在下面一层,被丢掉了。
两条嵌套规则的关系(本单要求先测量再实现)
不是优先级关系,是同一个划分的两个不相交格子。 设
k= 接受了值的类型的成员数(即范畴判定不成立的成员数):kk === 1k === 成员总数k === 0,或 k 在 2 与成员总数之间且两端不取)k === 1与k === 成员总数只在单成员 union 上重合,而该 schema 不产生单成员 union;何况内容判别本就要求恰好两个成员。所以不存在两规则都想选的形状 —— 这正是派发单要求「先测量有无冲突形状」的那一步,测量结果见下面的普查表:12 个形状,每个只落进一个格子。代码里两条规则并列而不是嵌套,顺序写成内容判别在前是刻意的:这样 #3677 的收窄条款仍然是「删掉它就红」的那一行(删掉后
sort: ['name']会被内容判别选中string成员,复现 #3677 测到的误选)。如果把范畴判定上提到划分函数里统一做,#3677 那行就成了死代码。测量:每个嵌套 union 落在哪个格子
unionAt= 被测 union 节点,reportedAt= 诊断最终落在哪里。sort: [{field:'n',order:'bogus'}]config.sort.0.ordersort: ['name']config.sort.0sort: [42]config.sort.0sort: 42config.sort(不变)columns: [{field:123}]config.columns.0.field(#3677 已有)columns: ['a',42]config.columns.1(#3677 已有)columns: [42]config.columns(不变)columns: 'nope'config.columns(不变)columns[0].summary: 'bogus'…0.summary(路径不变,消息变具名)columns[0].summary: {type:'bogus'}…0.summary(不变)filter[0].value: [{}]config.filter.0.value.0filter[0].value: {}config.filter.0.value(不变)一个被测量纠正的直觉:「规则说话了」不等于「消息变好了」,两个可观测量可以各自单独移动 ——
filter[].value: [{}]下降到出错的那个元素,但元素自己的 union 被全体成员拒绝(k === 0),于是止步,Invalid input就是仅剩能显示的东西。summary: 'bogus'选中的是 enum 成员,它的 issue 就在 union 节点上,于是地址不变,全部收益是「告诉作者有哪些选项」。这条最初把普查断言写错了(只断言路径下降),跑出来才发现。现在断言写成析取,并单独钉了两个方向。
修前 / 修后(两条门逐一实测,逐字节相同)
sort: [{field:'n',order:'bogus'}]config.sort/Invalid inputconfig.sort.0.order/Invalid option: expected one of "asc"|"desc"sort: ['name']config.sort/Invalid inputconfig.sort.0/expected object, received stringsort: [42]config.sort/Invalid inputconfig.sort.0/expected object, received numbersort: [{}]config.sort/Invalid inputconfig.sort.0.field+config.sort.0.order(选中成员的全部 issue)list.sort: [{order:'bogus'}]list.sort/Invalid inputlist.sort.0.order/Invalid option …columns[0].summary: 'bogus'config.columns.0.summary/Invalid inputInvalid option: expected one of "none"|"count"|…sections[0].fields: [{}]config.sections.0.fields.0/Invalid inputconfig.sections.0.fields.0.field/expected string, received undefinedfilter[0].value: [{}]config.filter.0.value/Invalid inputconfig.filter.0.value.0/Invalid input(路径更近)sort: 42、columns: [42]、columns: 'nope'、summary: {type:'bogus'}派发单要求「既有 38 条一行不改只加」。测量证明这条约束与本单授权的规则不相容,如实报告而不是绕开:
does NOT answer for config.sort(sort: ['name'])—— issue metadata-admin:嵌套 union「只剩一个成员接受了值的类型」时仍塌成 Invalid input(config.sort 等) #3678 正文的对照表逐字要求这一行变成config.sort.0/expected object, received string。does NOT answer for a filter value union(value: [{}])—— issue 的「范围」段落估计错了:它把filter[].value读成「值类型不匹配时全体拒绝」。对标量值确实如此(value: {}→ k=0,不变),但对数组值,5 个成员里恰好数组成员接受,k === 1,唯一候选成立。两条都按「fixture 三分诊」里的整体替换处理,并保留各自原本守的那个 claim:
sort那条守的是「绝不能选中string成员」。新钉子仍然证明这一点(误选会读作expected string, received array),并且直接加了这句负断言,所以它不会空过。fix(app-shell): expand the nestedconfig.columnsunion into per-column diagnostics #3677 的收窄条款依然是删掉就红的那一行 —— 见下面的扰动 (c)。filter那条守的是「内容判别对五成员 union 不说话」。它依然不说话(内容判别要求恰好两个成员);说话的是另一条规则,注释里写清了这层区别。可逆的杠杆:若维护者认为
filter[].value不该被本规则触及,唯一的收窄方式是给唯一候选规则加groups.length === 2的元数限制。不建议:「唯一候选」与成员个数无关,为了保住一条旧钉子而加一条无原则的特例,正是本仓一贯拒绝的形状。但它是一行,随时可加。逆向验证(先写预测,后运行;三个扰动)
预测全文落盘在实现之前。
扰动 (a) —— 还原实现(
nestedUnionMemberIndex退回只调内容判别)。预测:所有唯一候选锚点 + 两条重钉的 NARROWING 红;k === 0/k >= 2两条「维持」锚点与全部 parity 钉子两向皆绿(它们钉的是「不变」)。实际:k === 0、k >= 2两条如预测未红 —— 如实注明:它们是防回归的那一半,不是「修前红修后绿」的那一半。扰动 (b) —— 「验证验证器」,只扰动唯一性条款(把「恰好一个」改成「第一个接受的」)。预测方向不是「新锚点红」—— 内容判别先跑,
columns里它说得上话的形状不受影响,受影响的恰是它弃权的那些,于是红的应该是 #3677 自己的两条钉子。实际:方向与预测一致。这证明唯一性条款守的是 #3677 的「不发明偏好」边界,而不只是它自己的锚点 —— 少了它,
columns: [42]会报config.columns.0/expected string, received number,summary: {type:'bogus'}会报 enum 的Invalid option。扰动 (c) —— #3677 的收窄条款是否变成死代码:删掉
if (groups.some(memberRejectedNodeType)) return null;。预测:sort: ['name']被内容判别选中string成员,重新报expected string, received array,重新钉的 sort 测试红。实际:该条款仍然是活的,没有因为本单变成死代码 —— 这也是把两条规则并列、而不是把范畴判定上提的原因。
判定奇偶
展开仍在 issue →
SchemaFormIssue的映射内部、ok判定之后、所有既有过滤器之后。判定不是「测出来没变」,而是结构上不可能变。照 #3624/#3677 的手法加了 8 条 parity pin,它们修前修后都绿 —— 这正是它们的意义,不是它们的弱点。测试
clientValidation.viewDiagnostics.test.ts:新增 10 条(6 CANARY + 2 维持 + 1 普查 + 1 双向可观测量)+ 8 条 parity pin;改动 2 条(上述被取代的 NARROWING 钉子),其余 36 条一行未动。已知残留(不在本单范围)
invalid_type:enum 成员被喂非枚举值时答的是invalid_value,于是被算作「读过值」的候选。summary: {type:'bogus'}因此维持 k=2 而塌陷,若判定放宽到「任何在 union 节点自身整体拒绝该值的 issue」则 k=1 可下降。属观测类,单独记录,不在本单改。config.columns)的诊断仍塌成「Invalid input」——创建路/编辑路都中 #3626 的方向 1)未做。config.columns)的诊断仍塌成「Invalid input」——创建路/编辑路都中 #3626 一致。Generated by Claude Code