…t the protocol (objectui#7924) (objectstack-ai#9720)
Part of objectstack-ai#7924 — the card is NOT settled by this PR; see "What this does
not do".
Clause-②: no — no declaration face moves.
`packages/types/src/objectql.ts`, every `*.zod.ts` and every renderer
read set are untouched.
## Why
The liveness census (PR objectstack-ai#8933) answers one question per member: *is it
declared, and is it read off a named view?* The disposition this card is
waiting on turns on a **second** question the census never pinned:
**does `@objectstack/spec` declare the member?** That is what separates
"the renderer sources it from the wrong place" from "objectui invented a
spelling", and the 2026-09-16 director-seat ruling is written entirely
on that axis — as **prose, with no assertion that can fail**.
That is the exact failure mode this card has already produced twice:
`about 52 / seven / ~45` → `47 / six / 41` → **stale again** at `64 / 21
/ 43` since objectui#8980. The protocol-side partition is today in the
same state the member count was in then: measured once, on a **branch**
revision (`d4733f27e4`) in a shallow checkout with a stale
`origin/main`, and carried forward in prose. The sweep said *"Re-sample
before dispatching bucket ②"*; the ruling repeated it. This PR
re-samples it **and makes it an assertion**.
## Figures, each with its instrument — all re-derived on `78a9c6744`,
none inherited
| figure | value | instrument |
| --- | --- | --- |
| `NamedListView` declared members | **64** | TypeScript parser
(`ts.createSourceFile`, the interface's own `PropertySignature`
members). No heritage clause, no index signature, no computed member —
so the property signatures ARE the population. One required member:
`label`. |
| read off a named view | **21** | the pin's AST derivation, every route
from `schema.listViews` to a named view classified |
| declared and unread | **43** | set difference of the two above |
| protocol keys for a named list view | **50** |
`ObjectListViewSchema.shape` off `@objectstack/spec@17.4.0` as installed
— read off the schema object, not a hand list |
| protocol retirement tombstones | **5** | derived by the slot
description's own `[REMOVED]` marker, then each refused BY NAME through
a real `safeParse` |
| live protocol keys | **45** | 50 minus the 5 tombstones |
| live protocol keys objectui does NOT declare | **0** | set difference
over those two corpora |
| objectui-only members | **19** | the same set difference, other
direction — the control that the 0 above is a reading |
| unread partition | **23 + 8 + 10 + 2 = 43** | derived per member from
the protocol key set and the runtime fold |
### Disagreement with the figures the card body carries
The card's corrected block says **47 declared / six read / 41 unread**.
On today's head those read **64 / 21 / 43**. This is not a new
correction by me: objectui#8980 (PR objectstack-ai#9534) declared the seventeen
protocol members objectui was missing, and the census pin,
`objectql.zod.ts`, `zod-mirror-parity.test.ts` and
`.changeset/object-view-unmirrored-keys-7779.md` were all updated with
it. **The card body and its title were not.** Not repaired here — a card
body is the PM/filing seat's to edit.
## What lands
A new `describe` block in the existing census pin (extended, not
duplicated — same corpus, same helpers), with every figure **derived at
test time** and the literals as the pinned reading:
- **the protocol declares 50 keys, 5 of them tombstones** — each refused
by name with the protocol's own `[REMOVED]` prescription, with the
accept leg (`{ label, columns }` parses) as the control. So objectui not
declaring those five is **agreement, not narrowness**; a raw key-count
diff reads it the other way and is wrong to.
- **objectui declares all 45 live protocol keys** — the
narrower-than-protocol direction is **empty today**. It was
**seventeen** when objectui#8979 was filed. The control for that zero is
the same set difference over the same two key sets in the other
direction: **19** objectui-only members.
- **exactly one objectui-only member is read** (`options`) — the whole
cost of a by-reference mirror in one name, with the strict protocol
value's refusal of it measured.
- **`.omit().extend().superRefine()` still refuses unknown keys under
zod 4** — executed with `safeParse` plus an accept control. This was
carried as NOT MEASURED by the 2026-09-10 sweep and by the triage that
answered it; if it ever flips, bucket ③'s "the protocol refuses these"
weakens to "does not declare these".
- **the 43 unread members partition four ways, disjoint and
exhaustive**, each member pinned by name: ① 23 protocol-declared, ② 8
legacy `show*` spellings, ③ 10 objectui-only, plus the 2 that
objectui#8980 declared inert by ruling. A member changing bucket now
fails **by name** instead of staling a prose list.
- **the fold is read off the fold**: `SHOW_FLAG_TO_USER_ACTION` is
parsed out of `packages/core/src/utils/normalize-list-view.ts` and each
of the seven is asserted against its canonical protocol twin inside
`userActions`; the eighth (`showDescription` to
`appearance.showDescription`) by its own branch. Control: a flag the
fold does not know is absent from the map on the same query.
## Ablation — both legs, mutation proven on disk BEFORE any result was
read
The subject reaches the assertions by `readFileSync` of the source (plus
the installed schema object), so there is no `dist` hop to prove; the
proof is the on-disk text count plus the blob hash.
**Leg A — the fold derivation**
(`packages/core/src/utils/normalize-list-view.ts`, HEAD blob
`59dc77b6`): removed-text count 1 to 0, injected 0 to 1, blob `59dc77b6`
to `1567eae0`. Result: `Tests 1 failed | 213 passed (214)`, and the
**only** red row is *"the runtime fold maps seven of the eight legacy
spellings…"*. Restored with `git checkout HEAD -- ABSOLUTE_PATH` from a
`trap … EXIT INT TERM`; restoration proven by blob equality back to
`59dc77b6` **and** an empty `git diff HEAD` for the path.
**Leg B — the declaration and the protocol difference**
(`packages/types/src/objectql.ts`, HEAD blob `033c24f9`): one bucket ①
member renamed (`navigation` to `navigationZZZ`), counts 1 to 0 and 0 to
1, blob `033c24f9` to `147d6bbd`. Result: `Tests 5 failed | 209 passed
(214)`, red rows exactly: the declared-set pin, the 21+43=64 partition,
`UNREAD — navigation`, **`objectui declares every LIVE protocol key … is
EMPTY today`**, and **`BUCKET ① — navigation`**. So the zero in this
PR's headline row is a live query, not a dead one. Restored and proven
the same way; blob back to `033c24f9`, `git diff HEAD` empty.
Neither ablation file is edited by this PR.
## Gates — exit code captured to a file before any pipe
| gate | exit | printed verdict |
| --- | --- | --- |
| `pnpm exec vitest run
packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts` |
0 | `Test Files 1 passed (1)` · `Tests 214 passed (214)` (164 before) |
| `pnpm --filter @object-ui/types test` | 0 | `Test Files 202 passed
(202)` · `Tests 4748 passed (4748)` |
| `pnpm --filter @object-ui/types run type-check` | 0 | all three `tsc`
invocations echoed, `tsconfig.test.json` among them, so the test file IS
typechecked |
| `eslint --no-inline-config` on the changed test file | 0 | `--format
json`: 1 file linted, 0 errors, 0 warnings |
| `check:control-bytes` | 0 | |
| `check-changeset-presence` | 0 | *"Every one of them has an EMPTY
frontmatter — declared as releasing nothing, which is the explicit
exemption"* |
| `check-changeset-no-major` | 0 | `No changeset declares a major bump.`
|
| `check-changeset-fixed` | 0 | `All workspace packages are in the
changeset fixed group.` |
| `check-changeset-overwrite` | 0 | **report-only finding, named on
purpose — see below** |
| `check:changeset-claims` · `check:pending-changeset-literals` | 0 | |
| `check:new-line-citations` | 0 | this PR adds no cross-file line
address |
| `check:test-path-roots` · `check:installed-pin-claims` | 0 | |
| `check-governed-queue-guard --test` on the changed test path | 0 |
`NOT GOVERNED — 1 path(s) checked against 5 governed surface(s); none
matched.` |
Every heavy run went through the shared verify lock
(`OS_VERIFY_LOCK_SLOT=dev-7924`), each printing its own `VERDICT
command-exit` line.
**Declared narrowing of the lint run.** Repo-wide `pnpm lint` is `turbo
run lint` and belongs to CI; locally only the changed file was linted.
The narrowing is measurable: the file count is read off eslint's own
`--format json` output (1), and `eslint.config.js` configures **no**
type-aware linting (no `project` / `projectService`), so this diff
cannot move the verdict for any untouched file. Figures taken at
`4794f4786`, the final commit.
## The in-place repair, named with its evidence
`.changeset/7924-named-list-view-liveness-census.md` — the census's own,
still unreleased — carried the retired `47 declared / 6 read / 41
unread`. The other three sites that carry the pair were re-taken at
objectui#8980; this was the fourth and last, and it would have shipped
those figures into the release notes. Repaired in the same defect class
as this card. **Frontmatter untouched** (empty before, empty after), so
no package's release changes; `check-changeset-overwrite` reports the
edit (`declared at base: nothing` / `declares now: nothing`) and it is
that gate's own documented case 2, *"CORRECTING a declaration on purpose
— prose that no longer matches the change"*.
## What this does not do
- **No member is retired and no member is declared.** Buckets ② and ③ of
the 2026-09-16 ruling retire 18 members from a published TS face — that
narrows a published accept set (`Clause-②: yes`) and is outside this
dispatch's remit. This PR only makes the bucket each of those 18 sits in
a **derived, failing** assertion, which is the input that PR needs and
the thing that was still prose.
- **Bucket ① is not implemented.** Making the delegation read the 23
protocol-declared members off the named view is capability growth, on
`packages/plugin-view/src/ObjectView.tsx`.
- **The card is not closed** and its body's figures are not edited.
## 维护者速读(草稿)
- **改了什么**:只加测试。把「这 43 个成员分别属于哪一类处置」从评论里的文字,变成每次跑测试都会重新推导的断言 ——
推导来源是安装着的 `@objectstack/spec`、声明文件自己的 AST、以及运行时的折叠表。
- **为什么改**:这张卡的数字已经错过两轮(52 → 47 → 今天的
64)。裁决所依据的「协议有没有声明这个键」这一面,至今只在一条分支上量过一次,从没有任何断言守着。现在有了:某个成员换了桶,测试按名字红。
- **风险与代价(含回滚)**:不动任何已发布面,风险仅限于测试变严;回滚就是 revert 这个 PR。
- **席位意见**:(留空)
- **你要做的**:不需要做什么。若要推进退休(八个 `show*` 拼法 + 十个本地键),那是另一张 `Clause-②: yes` 的
PR。
---
_Generated by [Claude
Code](https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Refs #7924 — this PR lands the census only and decides nothing. The two dispositions the card names for the unread members (
?: nevertombstones per the objectui#7129 route, or making the delegation read them from the named view) are maintainer rulings and stay with objectui#7928. ⛔ No declaration face moved:packages/types/src/objectql.ts, every*.zod.tsand every renderer read set are byte-identical tomainin this diff, which touches one test file and one changeset.Why none of the card's figures are quoted
The card body says "about 52 members" / "exactly seven read" / "~45", measured on
6a9ee323. Every figure below is re-derived on this branch's head (3b5053d45). The reading is 47 declared, 6 declared-and-read, 41 unread, plus one read-but-undeclared key. "About 52" reproduces only as a hand figure sitting between two instruments and is neither of them — the instrument table below pins that gap as an assertion rather than a sentence.The census
NamedListView(packages/types/src/objectql.ts:2025–:2202today — re-located by AST, not by the card's stale line numbers) declares 47 top-level members. Theobject-viewnode renderer reads 7 names off a named view; 6 of them are declared members.activeView?.KEY— the host'sviewsprop — is not read off the named view. It is included because it is what the ruling needs: it separates "unread here" from "unread anywhere".activeViewlabelactiveView?.labeltypeactiveView?.typecolumnsactiveView?.columnsfilteractiveView?.filtersortactiveView?.sortoptionsshowSearchactiveView?.showSearchshowSortactiveView?.showSortshowFiltersactiveView?.showFiltersshowHideFieldsshowGroupshowColorshowDensitycompactToolbaractiveView?.compactToolbarallowExportactiveView?.allowExportcoloractiveView?.colorinlineEditactiveView?.inlineEditwrapHeadersactiveView?.wrapHeadersclickIntoRecordDetailsactiveView?.clickIntoRecordDetailsaddRecordViaFormactiveView?.addRecordViaFormaddDeleteRecordsInlineactiveView?.addDeleteRecordsInlinecollapseAllByDefaultactiveView?.collapseAllByDefaultfieldTextColoractiveView?.fieldTextColorprefixFieldactiveView?.prefixFielddescriptionshowDescriptionactiveView?.showDescriptionnavigationactiveView?.navigationselectionactiveView?.selectionpaginationactiveView?.paginationsearchableFieldsactiveView?.searchableFieldsfilterableFieldsactiveView?.filterableFieldsresizableactiveView?.resizabledensityModeactiveView?.densityModerowHeightactiveView?.rowHeighthiddenFieldsactiveView?.hiddenFieldsexportOptionsrowActionsactiveView?.rowActionsbulkActionsactiveView?.bulkActionsbulkActionDefssharingactiveView?.sharingaddRecordactiveView?.addRecordconditionalFormattingactiveView?.conditionalFormattinguserFiltersactiveView?.userFiltersshowRecordCountactiveView?.showRecordCountallowPrintingactiveView?.allowPrintingemptyStateactiveView?.emptyStateariaactiveView?.ariadataas anycastactiveView?.dataTotals: 47 declared = 6 read + 41 unread. Read set off the named view = 7 names (the 6 above plus the undeclared
data)."fold only" means the member has no⚠️ Worth stating because a named-property probe alone reports those four as having no reader at all, which is false — and that mistake would have gone straight into the ruling.
activeView?.KEYaccess, but IS consumed when it appears onactiveView, throughnormalizeListViewSchema(activeView)(packages/core/src/utils/normalize-list-view.ts, itsSHOW_FLAG_TO_USER_ACTIONmap).Three readings the ruling request should carry
datais still read-but-undeclared. It reaches the renderer asdata: (currentNamedViewConfig as any)?.data ?? …. objectui#7928 requires it to be declared or its cast removed and names this card as its home; it is still neither. It is why the arithmetic subtracts six, not seven, from 47.activeView; 4 more are folded fromactiveView; 3 have no reader on any path —description,exportOptions,bulkActionDefs. Of those,exportOptionsandbulkActionDefsdo not appear in the renderer at all (pinned), anddescriptionappears only offschema/schema.table/schema.form, never off a view.labelis the only required member, and it is read — so a tombstone route would leave the sole required member intact.Instruments, and the control that makes each zero a reading
namedListViewMembers()(new)ts.createSourceFile, the interface's ownPropertySignaturemembersnamedListViewMemberCount()(kept)namedListViewLooseMemberCount()(new)⛔ Not a brace-depth count: a brace parser on this repo has already failed to terminate at the end of an object literal (objectui#8071). The three are now pinned against each other, so the instrument swap is itself a measurement and "52 is between two instruments and is neither" stops being prose.
The read set is derived by walking every route from
schema.listViewsto a named view — a binding annotatedNamedListView; the value binding ofObject.entries(record).map(...)(through an intermediateconst entries = …too); and element access straight off the record — stripping( ),!andas anyat each access, which is the only reasondatais visible at all.⭐ That walk found a named-view read the old
currentNamedViewConfig?.KEYregex cannot see:{view.label || key}on the named-view tab strip.labelis read at two sites, and the count is asserted.Completeness, not just coverage: every occurrence of the record binding is classified into a role (
DECLARATION,ELEMENT_ACCESS,OBJECT_KEYS,OBJECT_ENTRIES,COMPARISON,CONDITION,HOOK_DEPENDENCY). A new syntactic route lands asUNCLASSIFIEDand fails before the census can silently under-count — the test says in its own message not to add the role to make it green.Firing controls (a silent probe is a parser that found nothing, not a reading of zero):
columns— asserted present in both the declared set and the read set;rowHeightis asserted declared and not read off the named view, withrowHeight: activeView?.rowHeight,pinned as the text that makes it so;stickyHeader, in neither set, is non-vacuity for both probes at once;grepthat finds nothing for them is asserted to findrowHeight.Ablation — the pin is non-vacuous in both directions
Each leg: mutation proven on disk before any result was read (anchor counts on both the removed and the injected text, plus a
git hash-objectdiffering from the HEAD blob), restore from atrap … EXIT INT TERMusinggit checkout HEAD -- ABSOLUTE_PATH, restoration proven by blob hash, never by an exit code. HEAD blob ofObjectView.tsx=c1a807af683a0147796780c801ac34ffa97e8c70.Leg A — unread becomes read.
navigation: activeView?.navigation ?? …rewritten to read the named view first. Removed-text count 1 to 0, injected 0 to 1, blobc1a807aftofdfe41ac. Result: 4 failed / 143 passed, and the only per-member row that moved is the expected one:UNREAD — navigation is declared and NOT read off a named view⇒ red6 read + 41 unread = 47partition ⇒ redLeg B — read becomes unread. The
currentNamedViewConfig?.typeread deleted. Blobc1a807aftoc9e79701. Result: 4 failed / 143 passed:READ — type is declared AND read off a named view⇒ redif (activeView?.type) return activeView.type;) already existed on the next line, so the on-disk proof read injected-count 2, expected 1, and the script aborted withexit 94before running anything and restored the file. The reading was void, not green. Re-anchored on a unique marker and re-run; both blob hashes above are from the run that actually happened.Restoration verified after every leg:
git hash-objectback toc1a807af683a0147796780c801ac34ffa97e8c70andgit diff HEADempty for that path.Readings — commands and exit codes
Exit codes captured into a file before any pipe (
cmd > out 2>&1; RC=$?), never off the end of a pipeline.pnpm exec vitest run packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.tsTest Files 1 passed (1)·Tests 147 passed (147)pnpm --filter @object-ui/types type-checktsc --noEmit,tsconfig.examples.json,tsconfig.test.jsonechoed and green — the test file is typecheckedpnpm exec eslint packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.tsnode scripts/check-changeset-presence.mjsnode scripts/check-changeset-no-major.mjsmajorpnpm check:control-bytespnpm check:phantom-depstypescriptis already a declared devDependency of@object-ui/typesnode scripts/check-governed-queue-guard.mjs --test(both changed paths)NOT GOVERNED— 2 paths, no governed surface matchedAll test runs went through the container's shared heavy-verify lock; each printed
VERDICT command-exit 0.Declared narrowing. Repo-wide
pnpm lintandpnpm testare CI's runs, not this seat's. The lint reading above is narrowed to the one changed file;eslint.config.jsconfigures no type-aware linting (noproject/projectService), so this diff cannot move the verdict on any file it does not touch. Test scope is the single pin file because the diff changes exactly that file plus a changeset, and no published byte moves.Scope
⛔ The three live branches' files were not touched:
packages/types/src/complex.ts/plugin-kanban/plugin-gantt(PR #8865),packages/types/src/layout.ts/page-breadcrumbs-refusal-8871.test.ts(PR #8930),registry-inputs-spec-parity.test.ts/plugin-calendar(PR #8872).The census extends the existing pin rather than adding a file. That file already owned both halves of this measurement (the 47 count and the seven-name read set) and is where the regex instrument being improved on lives; a second file would create a second instrument for the same number, which is the staleness shape objectui#7947 was filed about. The header's prose figures are now backed by the assertions underneath them.
⛔ This PR stays draft — the PM lands it. The card is not closed by it: the disposition is still unruled.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
Generated by Claude Code
Generated by Claude Code