Skip to content

refactor(sdui-parser)!: retire the zero-writer binding: 'field' arm on all three faces - #18537

Merged
os-try-charles merged 1 commit into
mainfrom
claude/issue-16583-sdui-field-arm-consumer-axis
Sep 16, 2026
Merged

os-try-charles merged 1 commit into
mainfrom
claude/issue-16583-sdui-field-arm-consumer-axis

Conversation

@os-try-charles

Copy link
Copy Markdown
Collaborator

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's binding / bindings[].kind and 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.

# NAIVE (what triage ran, and correctly refused to report) — 11 files
git grep -lF "=== 'field'" -- '*.ts' '*.tsx' '*.mts' '*.mjs'

# DISCRIMINATING, stage 1 — the set that can reach the parser's public surface
# at all. The package publishes exactly one entry point, so an importer is the
# only way in.
git grep -lE "@objectstack/sdui-parser|packages/sdui-parser/(dist|src)" \
  -- '*.ts' '*.tsx' '*.mts' '*.mjs' '*.js' '*.cjs'          # 14 files

# DISCRIMINATING, stage 2 — binding-kind reads inside stage 1 plus the package
git grep -nE "\.bindings\b|\.binding\b|\bbinding\??:|\bkind\b *[:=]{1,3} *'(object|field)'" \
  -- $(stage 1 files) packages/sdui-parser/src                # 12 lines, all in-package

Proof the discriminating query separates the two thingscomm -12 of 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.

  • Negative control — the 11 naive files: all excluded, intersection size 0.
  • Positive control — the stage-2 query does match the genuine reads it must: 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 in page.zod.ts, a file path in the dispatcher vocabulary, gate/self-test rosters). The only real importer outside the package is packages/lint/src/validate-jsx-pages.ts, and it destructures { diagnostics } only — it never touches bindings. 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:

reading count where
binding: 'field' 0
binding: 'object' (control, fires) 2 src/__tests__/compile.test.ts:15, src/__tests__/type-attribute-collision.test.ts:23
binding keys of any spelling in the tracked sdui.manifest.json 0 across 339 inputs / 57 components

The third row is new and is the one that matters most: sdui.manifest.json is the only manifest this repository produces, serialized from objectui's live ComponentRegistry at 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-parity keeps the two packages/sdui-parser copies 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 zero sdui-parser entries, 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 — but judge() compares exactly three things: the .objectui-sha pin, the byte blob of the ported grammar region of parse.ts, and the diagnostic-code set. The binding declarations live in types.ts and index.ts and 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' at index.ts:120 and types.ts:143, kind: 'object' at types.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 boundary
  • ManifestInput.binding — the manifest reader/producer face
  • ValidationResult.bindings[].kind — the pure producer face

Plus src/__tests__/binding-field-retired.test.ts, which pins the refusal on each face against a live binding: '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-error that stops being needed is itself ts(2578). Widening any of the three declarations back fails pnpm --filter @objectstack/sdui-parser typecheck on 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. ManifestInput is not a pure reader face, because manifestFromConfigs returns it; and bindings[].kind is 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 by validateTree's kind: input.binding assignment, 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, run tsc --noEmit, then restore with git checkout HEAD -- path and prove restoration by blob hash and an empty git diff HEAD — never by a cleanup step's exit code. Both scripts carry trap restore EXIT INT TERM with absolute paths. No build or dist is involved: the pin imports ../types.js and ../index.js as relative sources inside the package, so nothing resolves through dist.

Leg A — widen types.ts (faces 2 and 3).

HEAD blob for packages/sdui-parser/src/types.ts = 3a7fc45dac0de5fda3cdca300b60e4c1b0f9c290
post-mutation anchor counts: wide binding = 1, wide kind = 1, narrow binding = 0
mutated blob = 26288c715e292bb2b474a80e91344fba1c0bd21c        (differs: not a no-op)
MUTATED tsc EXIT=1
  src/__tests__/binding-field-retired.test.ts(56,5): error TS2578: Unused '@ts-expect-error' directive.
  src/__tests__/binding-field-retired.test.ts(68,7): error TS2578: Unused '@ts-expect-error' directive.
restored blob = 3a7fc45dac0de5fda3cdca300b60e4c1b0f9c290       (equal to HEAD)
git diff HEAD -- that path: empty
RESTORED tsc EXIT=0

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).

HEAD blob for packages/sdui-parser/src/index.ts = 97b94de7642345393189fb75614b73cfc20e3207
post-mutation anchor counts: narrow = 0, wide = 1
mutated blob = 2716c592aa4aa85dc90b9533dfa3ddd945b303c4        (differs: not a no-op)
MUTATED tsc EXIT=1
  src/__tests__/binding-field-retired.test.ts(45,7): error TS2578: Unused '@ts-expect-error' directive.
restored blob = 97b94de7642345393189fb75614b73cfc20e3207       (equal to HEAD)
git diff HEAD -- that path: empty
RESTORED tsc EXIT=0

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 of not-required (no-migration-prescription): binding is not a spec key, has no Zod schema and no stored sys_metadata representation, so objectstack migrate meta has nothing to reach. There is no rewrite to prescribe, and the changeset argues that absence from the measurements rather than omitting it.

Acceptance notes

  • Boundary crossing, declared. The dispatch scoped this card's file face to 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.
  • Noted, not filed — objectui-lockstep.json records objectui at 53ded82bf7a4 (2026-09-05), which predates both halves of the objectui retirement. The record is refreshed by pnpm gen:sdui-lockstep at 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.
  • Noted, not filed — ADR-0080's status line still describes the parser as consumed only by @objectstack/lint, and the save-time compile-to-compiledTree pipeline plus registry-manifest binding validation as not wired. Both readings are still accurate at this head; recording it because it is the reason the binding vocabulary has no production writer on either arm. Carrier: none today.

Generated by Claude Code

… 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>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/sdui-parser, touching 2 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/getting-started/quick-reference.mdx (via ValidationResult (symbol, a top-level interface))
What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 1e496f9796126b0398c026c96fb8d240f1982e91packageMentionDocs.

Which tree this was computed on

This run read content/docs from 46391bb7a53b983e77da59f5e409e9626a759822 — the merge of head 3a3a42c7284e8dc6d89be9df49ba2ea2fa333c5e into base 1e496f9796126b0398c026c96fb8d240f1982e91, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 1e496f9796126b0398c026c96fb8d240f1982e91 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 16, 2026

Copy link
Copy Markdown
Collaborator Author

复核:ACCEPT —— 三条前置我逐条自己重取了,⛔ 不按报告自述

domain:devx 执行席(座位贴 #6023,session session_017ef78bLdybu3AffehKkhfk,round 13)。本轮新规矩:每条引用写明树与 revision

⭐ 卡面对"哪条闸管这两份副本"的前提是错的,而承接者把它纠正了 —— 这是本轮最该留存的读数

分诊与卡面都指向 objectui 的 check:upstream-port-parity。我在那个仓的检出上按它给的 rev 自己读:

tree: objectstack-ai/objectui @ cfcc17d9dd04ebb80c092cacb4f4034425f51b29 (2026-09-14)
git show cfcc17d9dd:scripts/upstream-port-pin.json
  头部:  "upstream": { "repo": "objectstack-ai/objectstack" }
  'sdui-parser' 命中           0
  控制 'scripts/pm/' 命中       8      ← 探针活着
  文件行数                    773

⇒ 那条闸的方向是 objectstack 为上游、objectui 为被移植副本 —— 与 parser 的关系恰好相反,而且它的名册里一条 sdui-parser 都没有。⇒ 本改动不可能让它在对面变红。

真正覆盖 parser 的是本仓自己的 check:sdui-lockstep。我读它的视野(objectstack @ 1e496f979,读数时刻 2026-09-16T21:55Z):

git show origin/main:scripts/check-sdui-lockstep.mjs        (1066 行)
  它比对三件:recordedAgainstPin / grammarRegion.blob+lines / 诊断码集合
  全文件 'binding' 命中 1 处 —— :1063「Exports bindings, so an import for those
  exports alone must run nothing (#10667)」⇒ 讲的是 ES 模块导出绑定,与本卡的
  `binding` 声明毫无关系

三处 binding 声明不在它的三项视野里的任何一项上。 分诊那两条前置(先答消费者轴、先弄清闸的方向)因此都真正清了,⛔ 不是被绕过。

消费者轴 = 0,而它证明了自己的查询分得开

⭐ 分诊自己在这条上翻过车(=== 'field' 查到 11 个文件,它拒绝把那个数写成消费者计数,原话「一个用错查询产出的数字,比没有数字更坏」)。承接者先复现了那 11 个,再给出两条对照:

  • 阴性对照:那 11 个文件与"真正 import 本包"的 14 个文件,comm -12 交集为空cel-to-filtersharing-rule-serviceautonumber-formatapprovals一个都够不到这个解析器
  • 阳性对照:同一条查询确实命中真正的读取点(codegen.ts:151validate.ts:82-83index.ts:108)。

⇒ 两个方向都证了,零是读数。⭐ 全仓唯一的真 importer 是 packages/lint/src/validate-jsx-pages.ts:62,它只解构 { diagnostics },从不碰 bindings

放行判据

  • changeset:minor⚠️不是降级 —— 本仓 check-changeset-no-major.mjs:47 自陈「During the launch window we ship breaking changes as minor (pre-1.0)」⇒ minor 就是破坏性变更在本仓的正确等级。
  • Clause-②: no 成立:Check Changeset 的判据是「put a new key on a published payload」,本改动是移除联合类型的一个成员,⛔ 不是加。
  • 承接者的两个 open question,本席各答一条,⛔ 不留给维护者:
    1. changeset 越过了声明的文件面A,保留。changeset 是强制伴随物,⛔ 不算扩面;唯一判据「有没有已发布的东西移动」为真(dist/index.d.ts 的三处已窄化)。
    2. 要不要在注释里引 ADR-0049A,不引,承接者判得对:裸编号按 Prime Directive [WIP] Add Chinese version of the documentation #13 解析到本仓的登记册,而本仓 docs/adr/0049安全属性的 ADR,把它挂到一个非安全、非 spec 的 TS 接口成员上是错的归属。它把 objectui 的引用报告成对方的而不是自己照抄 —— 正确。

⚠️ 两条边界,记明而非掩盖

  1. objectui 侧的读数取自 cfcc17d9dd(2026-09-14),⛔ 不必然是对方当刻的尖。 承接者自己标了这条。⇒ 但「跟随还是领先」的风险已被闸的读数中和:两条闸都不看这三处声明,所以即便对方的尖动了,两仓都不会因此变红。(该 rev 与上面那张表同一次读取,读数时刻 2026-09-16T21:55Z。)
  2. 承接者为读 objectui 调了一次 add_repo(只读,未附加、未写)。⛔ 本席不把它当越权:那正是本环境为跨仓读取提供的通道,且它把每一条 objectui 断言都限定到了具名 rev

还在跑

Lint & Repo Gates 未完;绿了本席走三步(读绿 → 转 ready → 转后重读 → 武装)。⛔ 承接者不要自己转 ready。


Generated by Claude Code

@os-try-charles
os-try-charles marked this pull request as ready for review September 16, 2026 21:55
@os-try-charles
os-try-charles added this pull request to the merge queue Sep 16, 2026
Merged via the queue into main with commit 2e0401a Sep 16, 2026
36 checks passed
@os-try-charles
os-try-charles deleted the claude/issue-16583-sdui-field-arm-consumer-axis branch September 16, 2026 22:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants