refactor(sdui-parser)!: retire the zero-writer binding: 'field' arm on all three faces - #18537
Conversation
… on all three faces objectui retired the same arm from its copy of this package (maintainer ruling of 2026-09-07 on objectui#6950, director decision batch #69, on the serializer input boundary; objectui#8315 on the two faces in types.ts), citing enforce-or-remove on a zero-writer measurement. Nothing propagates a retirement between the two copies of packages/sdui-parser, so this one kept the arm on all three declarations while the renderer that ships beside it no longer has it. Re-measured on this tree rather than inherited, each reading with its control: binding: 'field' 0 writers (control binding: 'object' = 2) sdui.manifest.json 0 binding keys across 339 inputs readers outside the pkg 0 (the single importer, @objectstack/lint's validate-jsx-pages.ts, destructures { diagnostics } only) branches on either arm 0 Narrowed: RegistryConfigLike.inputs[].binding, ManifestInput.binding and ValidationResult.bindings[].kind. Compile-time only — types are erased and validateTree still forwards whatever the manifest says, which the new pin states outright so the narrowing is not mistaken for a runtime rejection. The pin goes red in both directions: a @ts-expect-error that stops being needed is ts(2578), so widening any declaration back fails the package typecheck. Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
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 46391bb7a53b983e77da59f5e409e9626a759822 && git checkout 46391bb7a53b983e77da59f5e409e9626a759822
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1e496f9796126b0398c026c96fb8d240f1982e91 3a3a42c7284e8dc6d89be9df49ba2ea2fa333c5e && git checkout -B drift-repro 1e496f9796126b0398c026c96fb8d240f1982e91 && git merge --no-ff 3a3a42c7284e8dc6d89be9df49ba2ea2fa333c5e
node scripts/docs-audit/affected-docs.mjs --json 1e496f9796126b0398c026c96fb8d240f1982e91
|
复核:ACCEPT —— 三条前置我逐条自己重取了,⛔ 不按报告自述
⭐ 卡面对"哪条闸管这两份副本"的前提是错的,而承接者把它纠正了 —— 这是本轮最该留存的读数分诊与卡面都指向 objectui 的 ⇒ 那条闸的方向是 objectstack 为上游、objectui 为被移植副本 —— 与 parser 的关系恰好相反,而且它的名册里一条 sdui-parser 都没有。⇒ 本改动不可能让它在对面变红。 真正覆盖 parser 的是本仓自己的 ⇒ 三处 消费者轴 = 0,而它证明了自己的查询分得开⭐ 分诊自己在这条上翻过车(
⇒ 两个方向都证了,零是读数。⭐ 全仓唯一的真 importer 是 放行判据
|
Fixes #16583
Clause-②: no
What this delivers, in dispatch order
1. The consumer axis, answered with controls — the first deliverable.
Does anything in this repository read
sdui-parser'sbinding/bindings[].kindand do something on the'field'branch? No. Zero.The triage seat recorded that its own
=== 'field'query was wrong and refused to publish the number it produced. That query is reproduced below and separated from a discriminating one, so the next reader can re-run both.Proof the discriminating query separates the two things —
comm -12of the naive file set and the stage-1 file set is empty. Not one of the 11 naive hits (cel-to-filter,sharing-rule-service,autonumber-format,approvals,team-graph, …) can reach this parser at all.src/codegen.ts:151(i.binding),src/validate.ts:82-83(kind: input.binding),src/index.ts:108.Consumer count: 0. Of the 14 stage-1 files, 11 are the package itself or prose (a
describe()string inpage.zod.ts, a file path in the dispatcher vocabulary, gate/self-test rosters). The only real importer outside the package ispackages/lint/src/validate-jsx-pages.ts, and it destructures{ diagnostics }only — it never touchesbindings. Nothing in the tree branches on either arm of the vocabulary, so nothing loses a case it was handling.2. The writer axis, re-measured on today's tree — not inherited.
Measured at
1e496f9796126b0398c026c96fb8d240f1982e91, the branch point:binding: 'field'binding: 'object'(control, fires)src/__tests__/compile.test.ts:15,src/__tests__/type-attribute-collision.test.ts:23bindingkeys of any spelling in the trackedsdui.manifest.jsonThe third row is new and is the one that matters most:
sdui.manifest.jsonis the only manifest this repository produces, serialized from objectui's liveComponentRegistryat the pinned sha. Not even the surviving'object'arm has a production writer here.3. Which way the gate runs — answered, not "unmeasured".
The card's premise was that objectui's
check:upstream-port-paritykeeps the twopackages/sdui-parsercopies in step. It does not. That gate pins objectstack tooling scripts ported into objectui (scripts/pm/check-half-states.mjs,invoked-as.mjs); its roster,scripts/upstream-port-pin.json, contains zerosdui-parserentries, and its direction is objectstack-as-upstream — the opposite relationship. Nothing in this change can red it.The parser copies are kept in step by this repo's
check:sdui-lockstep, which vendors a ONE-SIDED record of objectui's copy and checks this tree against it. So this repo is the follower for that gate — butjudge()compares exactly three things: the.objectui-shapin, the byte blob of the ported grammar region ofparse.ts, and the diagnostic-code set. Thebindingdeclarations live intypes.tsandindex.tsand are on none of the three, so they are outside that gate's field of view in both directions.And objectui has already landed both halves:
binding?: 'object'atindex.ts:120andtypes.ts:143,kind: 'object'attypes.ts:192. So this change is categorically following, not leading.The change
Three declarations narrow from
'object' | 'field'to'object':RegistryConfigLike.inputs[].binding— the serializer's input boundaryManifestInput.binding— the manifest reader/producer faceValidationResult.bindings[].kind— the pure producer facePlus
src/__tests__/binding-field-retired.test.ts, which pins the refusal on each face against a livebinding: 'object'control, and pins the limit of the narrowing: it is compile-time only, so a cast-in'field'manifest still round-trips at runtime and the test asserts that it does. No one should read the narrowing as a runtime rejection and go hunting for a refusal path that does not exist.The pin goes red in both directions, by construction. A
@ts-expect-errorthat stops being needed is itselfts(2578). Widening any of the three declarations back failspnpm --filter @objectstack/sdui-parser typecheckon the very line that documents the retirement. Evidence of both legs is in the reverse-verification section below.Why the reader face is narrowed too. The counter-argument — producer to reader is a subset relation, so a permissive reader is not wrong — is answered rather than assumed away.
ManifestInputis not a pure reader face, becausemanifestFromConfigsreturns it; andbindings[].kindis a pure producer face where the relation inverts, so a wider union there accepts nothing extra, it obliges every consumer to handle an arm this package cannot emit. The two are coupled byvalidateTree'skind: input.bindingassignment, so narrowing one alone would need a cast at the only conversion site — the lenient consumer-side fallback Prime Directive #12 bans.Reverse verification — run from the committed state, both legs, all three faces
Each leg: widen one declaration back on disk, prove the widening landed by anchor counts and by
git hash-object, runtsc --noEmit, then restore withgit checkout HEAD -- pathand prove restoration by blob hash and an emptygit diff HEAD— never by a cleanup step's exit code. Both scripts carrytrap restore EXIT INT TERMwith absolute paths. No build ordistis involved: the pin imports../types.jsand../index.jsas relative sources inside the package, so nothing resolves throughdist.Leg A — widen
types.ts(faces 2 and 3).Two directives fired, not three — correctly. Face 1 lives in
index.ts, which leg A does not touch, so its pin stayed needed. That is leg B:Leg B — widen
index.ts(face 1).Three faces, three directives, each firing only when its own declaration is widened. The pin is not one blanket assertion that would survive two of the three being reverted.
Changeset
minor, declared breaking for TypeScript consumers, compile-time only, with an ADR-0087 disposition ofnot-required (no-migration-prescription):bindingis not a spec key, has no Zod schema and no storedsys_metadatarepresentation, soobjectstack migrate metahas nothing to reach. There is no rewrite to prescribe, and the changeset argues that absence from the measurements rather than omitting it.Acceptance notes
packages/sdui-parser/. The changeset lives at.changeset/16583-sdui-parser-binding-field-arm-retired.md, outside it. It is a mandatory companion for a published package, not scope creep; flagging it rather than landing it silently.objectui-lockstep.jsonrecords objectui at53ded82bf7a4(2026-09-05), which predates both halves of the objectui retirement. The record is refreshed bypnpm gen:sdui-lockstepat pin-bump time and this change does not move the pin, so nothing here is stale in a way a gate can see. Carrier: whoever next bumps.objectui-sha.@objectstack/lint, and the save-time compile-to-compiledTreepipeline plus registry-manifest binding validation as not wired. Both readings are still accurate at this head; recording it because it is the reason thebindingvocabulary has no production writer on either arm. Carrier: none today.Generated by Claude Code