Repository navigation
Commit 2e0401a
refactor(sdui-parser)!: retire the zero-writer
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 things** — `comm -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 = 3a7fc45
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 = 3a7fc45 (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 = 97b94de
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 = 97b94de (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](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_
Co-authored-by: Claude <noreply@anthropic.com>binding: 'field' arm on all three faces (#18537)1 parent 777d0c2 commit 2e0401a
4 files changed
Lines changed: 240 additions & 5 deletions
File tree
- .changeset
- packages/sdui-parser/src
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
78 | 78 | | |
79 | 79 | | |
80 | 80 | | |
81 | | - | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
82 | 99 | | |
83 | 100 | | |
84 | 101 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
80 | 80 | | |
81 | 81 | | |
82 | 82 | | |
83 | | - | |
84 | | - | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
85 | 134 | | |
86 | 135 | | |
87 | 136 | | |
| |||
103 | 152 | | |
104 | 153 | | |
105 | 154 | | |
106 | | - | |
107 | | - | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
108 | 167 | | |
0 commit comments