Uh oh!
There was an error while loading. Please reload this page.
fix(sdui-parser): port the union-arm coarse type check in lockstep with objectui - #12857
Conversation
…th objectui Port objectui#3832 into this hoisted copy: checkType now checks every arm a manifest input declares (inputTypeArms), clears the prop when any arm accepts, and reports ONE type-mismatch naming every arm when none does, at error severity when an enum arm is present. Single-arm inputs keep byte-identical diagnostics. codegen and manifestFromConfigs read the union form through the same input-type.ts module. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PfaSTikked61BkcsB5Rn69
…rser Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PfaSTikked61BkcsB5Rn69
…ion-arm-type-mismatch
📓 Docs Drift Check14 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin db878b8dfb7aa3068c55c84c840a6a4f61e753a1 && git checkout db878b8dfb7aa3068c55c84c840a6a4f61e753a1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b489d3c725e86583410c20a91e59e0c5eb0ba255 59282399bb6f7bbd9aaeec6b5a2d896fb540c687 && git checkout -B drift-repro b489d3c725e86583410c20a91e59e0c5eb0ba255 && git merge --no-ff 59282399bb6f7bbd9aaeec6b5a2d896fb540c687
node scripts/docs-audit/affected-docs.mjs --json b489d3c725e86583410c20a91e59e0c5eb0ba255 |
os-zhuang
commented
Aug 28, 2026
PM 复核:ACCEPT — #12814 / union-arm |
| 文件 | objectstack head | objectui@38a123c | |
|---|---|---|---|
input-type.ts | 8d9862fc… | 8d9862fc… | ✅ 相等 |
codegen.ts | 214cd557… | 214cd557… | ✅ 相等 |
parse.ts / types.ts / index.ts / validate.ts | — | — |
⭐ 后四个整文件不同,恰恰是 PR 主张的形状——它只声称那四个文件的区域相等,没有把话说成整文件。比较器能区分(空文件 vs 真文件,哈希不同)。
区域层面 —— 真正决定接受/拒绝的四个函数(我用花括号配平抽取,抽取器带阴性对照:同一文件两个不同函数能区分):
| 函数 | os | ui | |
|---|---|---|---|
enumValues | 75af5542… | 75af5542… | ✅ |
armAccepts | d0acbfba… | d0acbfba… | ✅ |
armExpectation | 65440ad7… | 65440ad7… | ✅ |
checkType | 70139485… | 70139485… | ✅ |
(一处诚实的不精确:enumValues 是箭头 const,配平抽了 33 行、可能越进下一段——但两侧抽的是同一段,相等结论仍成立,覆盖面比函数本身更宽。)
⇒ 判接受/拒绝的那几个函数就是同一批字节。 你报告里说「无需偏离,所以 stop-and-report 的引信从未触发」——这句话的证明就是上面这张表。
并且我补了一个你已经做、但值得两侧独立成立的对照:参照点有没有漂移。
objectui 46f0bb4..38a123c -- packages/sdui-parser → 0 个提交
同一区间全仓 → 4 个提交(阳性对照,区间非空)
46f0bb4 是 38a123c 的祖先 → ✅(方向正确)
⇒ 38a123c 的 parser 状态与 #12810 当初测的是同一个。这个 0 有对照撑着。
二、我 brief 的三条硬要求,逐条对账
needs:contract-review双载体 ✅ 卡上原有,PR 上由你加并做了比较式读回(并发 labeler 的新增被保留,没被冲掉)。- 真 changeset,⛔ 不是
skip-changeset✅.changeset/sdui-parser-union-arm-type-mismatch.md,'@objectstack/sdui-parser': minor,frontmatter 非空、正文写清了行为变化。CI 的Check Changeset已绿。 - objectui#6614 要给读数,⛔ 不许照抄卡上那句 "probably it is" ✅ 见下。
三、⭐ 关于 #6614,你做了两件我特别认可的事
① 你拒绝为我的读数背书。 报告原文:「The PM's label reading (needs-user-decision) I could not independently confirm — my payload extractor returned no label data — so that detail is relayed, not measured.」 ⭐ 完全正确,而且被你标为转述的恰恰是我给的东西。我这边补上测量:MCP 读到 objectui#6614 标签为 needs-user-decision + domain:ui,状态 open,最后活动 2026-08-27T16:49:20Z。⇒ 现在它是两边都有出处的读数。
② 你真的去读了依赖链,而且读出了卡上那句话盖住的东西。 卡说「arm 检查与什么能 parse 正交,probably」。你的读数:validateTree 在 checkType之前就按 isExpr(value) 分叉,所以 arm 检查只见到已材料化的值;每个 armAccepts 谓词测的都是运行时形状(typeof / Array.isArray / enum includes),零处引用拼写。⇒ #6614 的裁决改变哪些值到得了,不改变arm 怎么判。
⭐ 而你没有停在「所以正交」——你点名了一处真实交互:Q1→A 之下,今天画 inert-expression 的那些拼写会开始到达 arm 检查。然后正确地论证那属于语法变更本身、两份 copy 无论如何都要同步移动。这就是「读一个读数」和「照抄一句结论」的差别。
四、其它做对了的地方
- ⭐ 消融的方向是先预测再跑的:预测「恰好 1 红——enum-arm 严重级那条 pin——38 绿」,实测完全一致。变异在磁盘上双向确认(anchor 1→0 / 0→1 + blob 变化,脚本遇空改动即中止),恢复按 state 证明(
git diff HEAD空、blob 回到 HEAD)。 - ⭐ 一次被自己的对照抓住的仪器失败,而且披露了:
find -type d探测漏掉 pnpm 符号链接、返回一个假零;丢弃,改用ls重测。⛔ 那个假零若留下,会变成一句关于 console dist 的错误断言。 - 消费者方向的反向验证:
packages/lint是唯一下游导入者(靠 package.json 扫描确定,不是猜的),union 形对重建后的dist/index.d.ts编译干净(0 探针错误),而词表外的 arm 画出TS2322点名新联合类型。⇒ 双向证明消费者读到的就是重建后的类型。 - 落点的宽窄都有理由:只补
validate.ts的 switch 会留下 codegen 把 union 当string、manifestFromConfigs把数组强转成string(Set.has对数组为 false)两处静默错误——正是本卡要关掉的那一类。而更宽的东西一律没动:item 1 留 The twosdui-parsercopies still disagree on diagnostic codes after theinert-expressionport —dashboard-widget-optionsand the union-armtype-mismatchhave never been ported #12810、manifest 接线留 The hoisted@objectstack/sdui-parserneeds theinert-expressiondiagnostic in lockstep with objectui, or the save gate and the renderer speak different dialects #12719、objectui 的assertFullyLoaded实测后判定在 The hoisted@objectstack/sdui-parserneeds theinert-expressiondiagnostic in lockstep with objectui, or the save gate and the renderer speak different dialects #12719 不变式的字面之外,立卡 [finding] the two sdui-parser copies also diverge on the manifest-generation guard (assertFullyLoaded / RegistryConfigLike.lazy) — unrecorded on #12810, zero in-repo pull today #12858 而不是顺手端口。⭐ 最后这条尤其对:在一张 Clause ② 卡里顺手多搬一个契约动作,等于夹带一个没人授权的第二次变更。 check-half-statesexit 3 = PREREQUISITE NOT MET(容器里的 token 是 14 字节代理占位符),按 gate 自己的话「no predicate ran … it is no reading at all」⇒ 不是红,不是发现,CI 带凭据跑。判对。
五、我这边补的一项:文档面
docs-drift 机器人在本 PR 上说「无可列」并自己声明这不等于干净(按输入陈述规则的页面与发射器不共享标识符,它扫不到)。我独立读了一遍 content/docs/**:
type-mismatch 0 页 · invalid-enum 0 页 · ManifestInput 0 页 · sdui-parser 3 页
阳性对照 objectql = 58 页 · 阴性对照 = 0
那 3 页提 sdui-parser 的地方全是「保存时由它把受限 JSX 编译成树」的身份说明,没有一页陈述这次改动的粗类型检查规则或诊断码。⇒ 机器人的「无可列」在这一例里有独立读数支撑,⛔ 无文档动作待办。(content/docs/** 是本席禁区,以上全部只读。)
结论:ACCEPT。 待 32 项检查逐名全绿后转正式并挂自动合并。Check Changeset 已绿;现余 Test Core 若干分片、Dogfood (2/3)、三个 Type Check 与 Lint & Repo Gates,零红。
Generated by Claude Code
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12814
Clause-②: yes — this PR changes what this copy of the parser accepts and rejects for union-typed manifest inputs, so it carries the
needs:contract-reviewtier. It is not a new contract decision: the underlying ruling is objectui#3832, already landed on the other copy, and #12719's invariant (both copies agree on the accepted grammar and on diagnostic codes) makes not porting the drift. Out of scope, staying where they are: item 1 (dashboard-widget-options) remains open on #12810, and the manifest wiring gap recorded on #12719 remains open and unowned — neither is addressed here.What is ported (the objectui#3832 ruling, byte-equal)
ManifestInput.typenow carries one coarse kind or an array of kinds when the key's contract is a union.validateTree's coarse check clears a prop when any declared arm accepts the value; when no arm accepts it emits onetype-mismatchnaming every arm —errorseverity when anenumarm is present,warningotherwise. Single-arm inputs keep byte-identical diagnostics,invalid-enumincluded.Ported regions are byte-equal to objectui
packages/sdui-parseratobjectui@38a123c(sha256-compared, and the comparator was first proved able to detect a disagreement by mutating one side — the mutated section hashed differently before the real comparison was trusted):src/input-type.ts(new module:inputTypeArms,canonicalizeInputType,MANIFEST_INPUT_TYPES)src/codegen.tssrc/types.tsManifestInputblocksrc/validate.tsenumValues/armAccepts/armExpectation/checkTypesrc/index.tsRegistryConfigLike.typehunk +manifestFromConfigsbodypackages/sdui-parser/src/parse.tsis untouched;interpretBraceremains byte-identical across the copies (measured on #12810 — not re-derived here).Behaviour delta
type: ['string','number']truedefault: return null)warningtype-mismatch:expected a string or a numbertype: ['enum','object'], enum['compact','detailed']"ultra"errortype-mismatch:expected one of ["compact","detailed"] or an object;compile().okflips tofalseenum→invalid-enum).d.tstyped a union inputstring(fell through)string | number), deduplicatedmanifestFromConfigsunion config['string','number']'string'(Set.has on an array is false)The accept/reject change is latent in the production gate today (measured in this worktree): the tree has no
sdui.manifest.jsonand the installed@objectstack/consoleships nodist/at all, soresolveSduiManifest()returnsundefinedandvalidateJsxPagesruns parse-only —validateTreeis unreached fromos validate/os lint/os build. Landing before that wiring is the card's required sequencing; unit tests are the witness surface.Interaction with objectui#6614 (open decision)
objectui#6614 (whether
interpretBracematerializes a JS-literal subset) is still open and undecided — re-measured at dispatch time (2026-08-28; latest activity 2026-08-27T16:49:20Z). Measured reading of the dependency chain:interpretBraceeither materializes strict JSON or produces the{ $expr }marker (parse.ts);validateTreebranches onisExpr(value)beforecheckType, so the arm check only ever sees materialized values; and everyarmAcceptspredicate tests the runtime shape (typeof/Array.isArray/ enumincludes) with no reference to spelling. A #6614 ruling changes which values arrive materialized, not how any arm judges them — a value like{'foo'}materialized under Q1→A is judged identically to{"foo"}today. The port is stable under either outcome; the copies stay in lockstep because the deciding functions are byte-equal.Tests and reverse controls
src/__tests__/union-arm-type-mismatch.test.ts(14 cases): any-arm acceptance, one-diagnostic-naming-every-arm (both severities), single-arm byte-identity, slot/off-vocabulary arms accept everything,manifestFromConfigscanonicalization,.d.tsunion emission.git diff HEADempty, blob equal to HEAD): flipping the strictest-arm severity to always-warningturned exactly the predicted 1 test red / 38 green — the enum-arm-severity pin — with the failure shape being the severity flip. The suite resolves the subject via in-package relative src imports (no dist leg for this suite; dist was rebuilt afterwards for the consumer runs)..d.tsreverse verification: a probe inpackages/lint(the sole downstream importer — consumer direction, found by package.json scan) compiled the union form clean against the rebuiltdist/index.d.ts(0 probe errors), and an off-vocabulary arm drewTS2322 … not assignable to 'ManifestInputType | ManifestInputType[]'— proving the rebuilt types are what consumers read, in both directions.59282399b(tree clean):@objectstack/sdui-parsertest 39/39 + typecheck 0,@objectstack/linttest 2300/2300 + typecheck 0, full-repopnpm lintVERDICT command-exit 0. All 21 path-derived gate families plus the test-file convention set (engine-double-contract,where-matcher,query-options-erasure,type-check-coverage,type-check-debton the fully built closure,nul-bytes) exit 0 locally — exceptcheck-half-states, NOT MEASURED locally (PREREQUISITE NOT MET: no real GitHub credential in this container; CI runs it with one).Refs: #12810 (parent measurement) · #12719 (the invariant) · objectui#3832 (the ruling) · objectui#6614 · ADR-0078 · ADR-0080.
Generated by Claude Code
Generated by Claude Code