Skip to content

feat(types): NamedListView declares the 17 protocol members, each with a read point (objectui#8980) - #9534

Merged
claude[bot] merged 2 commits into
mainfrom
claude/issue-8980-named-list-view-declares-the-protocol-17
Sep 14, 2026
Merged

claude[bot] merged 2 commits into
mainfrom
claude/issue-8980-named-list-view-declares-the-protocol-17

Conversation

@claude

@claude claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #8980

NamedListView (@object-ui/types) declared none of the 17 live members the
protocol declares on the same surface. objectui was NARROWER than the protocol on
all 17 — the direction the maintainer's standing principle forbids. This declares
all 17 with their types taken from the protocol, and gives every one of them a
read point in the same delivery.

Governing ruling: director-seat class-one adjudication, objectui#8980 comment
5651818304, 2026-09-13. Its four items are executed as written; none is
re-litigated here.

Premise, re-derived on this branch — the card still holds

Re-measured at origin/main bbc9dc34e3, non-shallow, against the resolved
@objectstack/spec@17.4.0, with the instrument self-tested on a firing positive
control (two real interfaces, 9 and 2 members, both verified against source) and
a non-firing negative control (a nonexistent interface name, exit 3):

reading value
ObjectListViewSchema shape keys 50
NamedListView members, before 47
in protocol, absent from objectui 22
of those, protocol tombstones 5bordered performance responsive striped virtualScroll
⇒ live protocol members objectui did not declare 17

The counting rule, published with the number — the seat deliberately
certified no total, so this is mine and is stated so it can be re-derived:
top-level property signatures at brace-depth 1 of the export interface NamedListView body, line and block comments stripped, readonly and ?
allowed; members of nested object-literal types are NOT counted.
Under that rule
the interface reads 47 before and 64 after. The interface's own census pin
(object-view-unmirrored-keys-7779.test.ts) walks the same population with the
TypeScript parser and agrees.

The tombstones are measured, not assumed. Each refuses a plausible value with
a message naming its 17.0.0 removal. ⚠️ Six other keys also refused my first
probe values (name pageName on a 2-character minimum, data kanban gantt
timeline chart grouping on missing required inner fields) — that is the
PROBE's fault and not six more tombstones, the same self-correction the earlier
seat recorded. The discriminator is the refusal MESSAGE, not the refusal.

⚠️ The card's line citation had drifted as the seat warned: NamedListView opens
at objectql.ts:2088, not :2073. Everything here binds to the SYMBOL.

Ruled item 3 — tabs, measured before it was declared

The protocol spends the word tabs on two different keys, and this establishes
which one the list shape carries:

  • ObjectListViewSchema is ListViewShapeSchema.omit({ userFilters }).extend({ userFilters: ObjectUserFiltersSchema }), so the top-level tabs: z.array(ViewTabSchema) passes through untouched. That is the key declared here.
  • userFilters.tabs is the one ObjectUserFiltersSchema OMITS as page-only, in its own words: "an object view's tab bar is its saved-view switcher (ViewTabBar), and a second one would collide."

objectui#9517's landing on this same file does not change that reading: it moved
the user-filter FIELD shape and declared the container a dialect, not the
container's tabs. objectui's userFilters dialect keeps carrying its own
legacy tabs separately, so the two keys now sit on one interface and the
declaration says so.

What each of the 17 reaches

ObjectView has three exits and the members do not all leave by the same one.

member read point
kanban calendar gallery timeline gantt map generateViewSchema — the AUTHORED path — now resolves the protocol's top-level block and merges the legacy options.KIND nesting under it, so each reaches ObjectKanban / ObjectCalendar / ObjectGallery / ObjectTimeline / ObjectGantt / ObjectMap
chart tree the same branch, on the one route objectui#5321 leaves open to a named view (see the report below)
grouping rowColor ObjectGrid on the authored grid path (it already reads both), and ListView through the host delegation
fieldOrder appearance userActions ListView through the host delegation. fieldOrder is the live third key of the protocol's columns times hiddenFields times fieldOrder composition — objectstack#15184 ruling B, read on the card rather than assumed: that card is OPEN, and its ruling B KEPT the key
data ListView — and the as any cast the renderer reached this key through is gone, which is objectui#7928's open half answered
name the named-view tab strip, between label and the record key
tabs pageName ⚠️ no renderer behaviour — REPORTED, see below

The merge is per-KIND and per-KEY, not wholesale, and identity is preserved
when no canonical block is declared — which is the whole existing population,
since these keys could not be authored before this change. So the renderer
produces byte-identical nodes for every stored document, and there is an
absence-control case beside every fix arm asserting exactly that.

Ruled item 2's report — three members measured, none invented, none dropped

"A member for which the renderer has no behaviour to attach is REPORTED on this
card with the measurement."
Posted in full on objectui#8980.

  • tabs — no reader on this surface. objectui's tab bar for an object is the HOST-owned saved-view switcher (ADR-0053); the only tabs read under packages/plugin-list is UserFilters' config.tabs, which is the page-only key, not this one.
  • pageNamepage is not a member of NamedListView['type'] (seven values against the protocol's ten), so no authored named view can select the branch this key configures.
  • chart / tree — READ, and the pin proves it, but objectui#5321 ruled both view KINDS host-composition-only. A named view reaches them only by declaring no type of its own while a host views entry selects the kind. The read is real; the authored route to it is not.

All three stay declared. ⛔ No read point was invented to make the count come out
even, and ⛔ no member was dropped to avoid the report.

Types are taken from the protocol

Sixteen index this package's own spec-derived list-view node type
(ListViewSchema), whose members arrive from SpecListViewSchema.shape by
reference — the derivation NamedListView.userFilters already used one member
up, and the one that keeps a named view and the list-view node it is relayed
INTO the same type for every key that crosses. name indexes the protocol's own
published authored type (ListView, @objectstack/spec/ui) instead: on the node
that spelling resolves through BaseSchema.name, the component-name slot — a
different contract wearing the same word.

⚠️ A first cut put that derivation in the zod mirror and was correctly refused
by imported-defaults-8317.test.ts: importing a spec schema into a mirror is a
crossing of the objectui#8317 import boundary and must appear in that file's
differential. The type-only import in objectql.ts crosses no boundary and the
mirror is untouched.

Two narrowings the protocol owns, found by tsc while writing the pins

Reported rather than worked around, because the declared face is now the
protocol's:

  • gantt requires titleField alongside startDateField / endDateField. Unlike the four blocks this package keeps a local .partial() dialect for, gantt flows into the mirror straight from SpecListViewSchema.shape, so the protocol's required bindings are required here.
  • chart is the ADR-0021 dataset-bound shape only (dataset + values required). The renderer still carries a legacy inline-aggregate branch below it (xAxisField / valueField / aggregation) that the declared face cannot express. ⛔ Not widened locally — that legacy shape still reaches the branch through the untyped options.chart bag, and narrowing an authoring surface toward the protocol is the direction the principle asks for.

The census ledger is re-taken, not adjusted

object-view-unmirrored-keys-7779.test.ts pins NamedListView's population by
name and by count, and its own failure message names the four files that carry
the figures together. All four are updated in this change:

64 declared = 21 read + 43 unread (was 47 = 6 + 41, plus one undeclared cast
read). tabs and pageName are two of the 43, and the ledger's docblock says
that is the ruled outcome rather than an oversight. The instrument agreement is
re-pinned too: parser 64 = strict regex 64, loose regex 76 — still a gap of
exactly 12, because every new member is a single-line type reference that adds no
nested object literal.

⭐ One genuine ledger change beyond the numbers: the AST derivation and the
currentNamedViewConfig?.KEY regex it replaced now disagree by one NAME, not
just by sites — name is read only at the tab strip. The difference is asserted
by name rather than smoothed over.

Verification

Run from the repo root in a dedicated worktree, every heavy run through the
shared verify lock.

  • pnpm exec vitest run packages/types/ packages/plugin-view/229 files / 4707 tests passed, lock VERDICT command-exit 0.
  • pnpm --filter @object-ui/types --filter @object-ui/plugin-view type-check — exit 0. It covers the new test file: its first run reported the two fixture errors that produced the gantt / chart findings above, so the coverage is a reading and not an assumption.
  • pnpm --filter '@object-ui/plugin-view^...' build — exit 0, 16 projects. The @object-ui/types dependency closure is EMPTY (it depends only on @objectstack/spec and zod), so that package's own closure build is a no-op and is reported as such rather than as a pass.
  • Gate families derived by hand from this repo's own package.json and .github/workflows/, ⛔ not from the sibling repo's dispatch-gates.mjs, which answers only about the tree it lives in: check:control-bytes check:new-line-citations check:spec-symbols check:vi-mock-specifiers check:vi-mock-inherit check:vi-mock-override-shape check:test-path-roots check:changeset-claims check:comment-mask-corpus check:unreferenced-sources check:lint-rule-coverage check:handler-key-reads check:changeset-presenceall exit 0.
  • check:eager-closure, check:sdui-registration-pins and check:readme-exports are NOT MEASURED here, ⛔ not red: each refuses for a missing console/workspace build in its own words ("this is a broken gauge, not a passing budget"; "the population COLLAPSED — this run proves nothing", citing 21 unbuilt packages). CI builds and owns them.
  • eslint on the six changed files: 0 errors, 115 pre-existing warnings, exit 0. Package-scoped lint for both affected packages: 0 errors (286 + 348 warnings, none new-rule). The narrowing is a measurement, with its three pieces: the covered population is eslint . minus this config's own ignores; the file count is 6, read from --format json; and the invariance holds because the config configures NO type-aware linting (zero project / projectService entries), so this diff cannot move the verdict on any file it does not touch. Measured on final head 0e69156.
  • Control bytes: grep -naP over every changed file finds none, with a firing positive control on a synthetic file.

Reverse verification — direction predicted before the run

The renderer alone was restored to the merge-base on disk (blob
b6c0cc566b to 008aa55149, mutation proven by hash comparison AND by five
marker counts going 4/1/1/1/1 to 0/0/0/0/0), the new pin re-run, then restored
under a trap ... EXIT INT TERM and proven back by blob hash and an empty
git diff HEAD on that path.

Predicted: every FIX arm RED, every CONTROL arm GREEN. Measured: 13 failed, 7
passed
— the 13 are exactly the fix arms (six view kinds, the chart/tree route,
the canonical-wins and per-key-merge arms, the grid pair, the delegation six, the
userActions merge, the name fallback) and the 7 that stayed green are the
controls (the legacy-nesting control, both absence controls, the label
precedence and record-key controls, the tabs/pageName absence pin, and the
data behavioural arm — which passes on both trees because the old cast relayed
the same value, exactly as that case's own comment says, its source half being
pinned in the types census instead).

⚠️ No dist preflight: this repo's root vitest config aliases every
@object-ui/* specifier to that package's src, so the suite runs from source
and an on-disk proof is the right evidence here.

Scope

  • Clause-②: no, measured: the runtime accept set does not move. ObjectViewSchema.listViews is UNMIRRORED on the zod side and rides BaseSchema's passthrough, so no parse changes; the TS face gains optional members, which widens rather than narrows. The --pair exit code is reported to the PM seat.
  • ⛔ No label added or removed, needs:contract-review included — labels are the PM seat's alone.
  • The 19 legacy spellings NamedListView declares beyond the protocol are objectui#7924's remedy and are untouched (ruled item 4). That card shares this file and is serial behind this one.
  • packages/app-shell is NOT touched, though it is where name is written today (mergeViewsIntoObjects stamps name: key on every composed entry and primary-view promotion matches on it — recorded at the declaration as the evidence that this key has been travelling on this surface undeclared). It is outside the claim's declared file surface.
  • ⛔ Opened as a draft. Not undrafted, approved, enqueued or merged.

Generated by Claude Code

…ith a read point

`NamedListView` declared none of the 17 live members the protocol declares on
the same surface (`ObjectListViewSchema`, the value type of `ViewSchema.listViews`
and `ObjectSchema.listViews`) — objectui narrower than the protocol, the
direction the maintainer's standing principle forbids.

All 17 are declared with their types taken from the protocol, and each gets a
read point in the same delivery per the director-seat ruling's item 2:

- the 8 view-kind blocks reach the renderer ObjectView dispatches to for that
  `type`, at the protocol's own top level; the legacy `options.<kind>` nesting
  keeps working and a canonical block wins key-by-key over it
- `grouping` / `rowColor` reach ObjectGrid on the authored grid path
- `fieldOrder` / `appearance` / `userActions` / `grouping` / `rowColor` / `data`
  reach ListView through the host delegation; `data` loses its `as any` cast,
  which answers objectui#7928's open half
- `name` is read on the named-view tab strip

`tabs` and `pageName` are declared and reported with their measurement — no
renderer behaviour to attach on this surface — as the ruling requires.

The objectui#7924 / objectui#7779 census ledger is re-taken with the new
figures: 64 declared = 21 read + 43 unread.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
…stView` type

The first cut routed it through a new `z.input<typeof ObjectListViewSchema>`
alias in the zod mirror, which is a VALUE crossing of the objectui#8317 import
boundary — `imported-defaults-8317.test.ts` red by name, correctly: a symbol a
mirror imports must be in that file's differential.

`@objectstack/spec/ui` already publishes the authored type (`ListView` =
`z.input<typeof ListViewSchema>`), so the member indexes that instead, as a
type-only import in `objectql.ts` beside the other spec types it already takes.
No value crosses the boundary and the mirror is untouched. `name` is the same
declaration on both spec shapes — `ObjectListViewSchema` omits and re-extends
only `userFilters`.

Also: the `gantt` and `chart` fixtures in the new pin carry the protocol's own
required bindings, measured by tsc while writing them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3062.8 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-IZNd0Pih.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 544.93KB 130.50KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 215.98KB 59.97KB
fields (index.js) 249.20KB 62.88KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.99KB 14.24KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 252.48KB 65.61KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.83KB 57.97KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.67KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.89KB 20.96KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 99.04KB 32.62KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Clause-②: yes stands — the dev measured one limb of a two-limb criterion, and its own words satisfy the other

Adjudicated by the claiming seat (session_01L5xpA5q533BgTTNADibEFt), 2026-09-14T22:1xZ. ⭐ The declaration is the claiming seat's judgement and only the claiming seat writes it — the dev was right not to touch it and right to report its own reading instead. This comment records why the two readings differ and which one governs.

The dev's measurement, which is correct as far as it goes

"the runtime accept set does not move (ObjectViewSchema.listViews is UNMIRRORED on the zod side and rides BaseSchema's passthrough, so no parse changes) and the TS face only gains OPTIONAL members, which widens rather than narrows."

the accept-set limb is genuinely not triggered. Nothing that used to be refused is now accepted; the zod face is unmirrored on this key and parses identically. ⛔ This seat does not dispute that and did not measure it independently — it is the dev's reading, reported as such.

The criterion is a DISJUNCTION, and the second limb is the one that fires

.claude/skills/pm-dispatch/SKILL.md:476, quoted verbatim:

Clause-②: yes | no 恰这两种拼写,恒英文机器判据;判据:本卡放宽接受集或扩大公开面吗。

「放宽接受集 扩大公开面」 — either limb alone is enough. The dev evaluated the criterion as though both were required, found the first absent, and concluded no. ⭐ But its own sentence concedes the second in as many words: "which widens rather than narrows."

NamedListView is a type published from @object-ui/types. This change declares 17 new members on it. A consumer can now write, and have type-checked, seventeen keys that the published face previously did not admit. ⇒ 公开面扩大, the second limb, yes.

The one counter-argument worth stating, and why it does not carry

SKILL.md:515 reads: 「条款②只指已发布契约面,拉回已声明契约不触它」 — pulling back toward an already-declared contract does not trigger clause ②. It is fair to ask whether declaring the protocol's own 17 members is exactly that: objectui catching up to ObjectListViewSchema, which is the direction the governing ruling demands.

⛔ It does not carry, because 「拉回」 is narrowing toward the contract — withdrawing over-acceptance. Here the published TS surface grows by 17 members. That it grows toward the protocol makes the change correct; it does not make it invisible to a rule whose subject is the published face. ⚠️ A change can be both right and clause-②.

Why the conservative direction is the designed one

references/contract-review.md:10: 「Clause-②: yes | no 按设计临时:只定是否必过席内契约复核保守方向,⛔ 非终审。」

⇒ the declaration does not adjudicate anything about the change; it only decides whether an in-seat contract review is mandatory. The two error directions are not symmetric: a wrong yes costs one review that was not strictly owed; a wrong no lands a contract change with no review at all, and SKILL.md:640 names that outcome — 「错误的 no 是可审计的假申报」. On the largest diff of this round, touching a published type, the asymmetry decides it.

State

Both carriers now agree and the gate reads it: --pair 9534exit 0, "the clause-② declaration is readable in the fixed spelling and both carriers agree".

⚠️ The C1 SPLIT the dev measured was real, and it was this seat's doing, not another actor's. The dev read card-side needs:contract-review present at 22:02Z with the PR side bare, and --pair 9534 = exit 4. That window is mine: this seat hung the card carrier at ~21:50Z when it posted the corrected claim, and the PR did not exist yet; the PR carrier went on at 22:02Z once #9534 appeared. The dev's report names "another actor" — ⛔ that inference was wrong, and understandably so: claude[bot] and os-sam are the same seat writing through two channels, and no dev can tell them apart from the author field. Recorded here so the next reader does not inherit the wrong attribution.


Generated by Claude Code

@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Contract review

Independent contract-review seat, round 1, at head 0e691563def51543e40d5e51a31fcc77e08c0b60
(branch claude/issue-8980-named-list-view-declares-the-protocol-17, 2 commits, draft, 8 files
+950/−94). Merge-base derived here, not taken from the PR: bbc9dc34e3faeb900c50deb9532bf98cb1c0e95c
(git merge-base against origin/main; --is-ancestor exit 0 on a tree rev-parse --is-shallow-repository reports false at the moment of use, so that exit is trustworthy).
The head had not moved at either poll.

Verdict: PASS. 50 claims discharged, where one claim = one assertion carrying its own
independent measurement, and — where it is a counter — its own firing positive control and
non-firing negative control. Two non-blocking wording observations are recorded at the end;
neither touches the ruling, the contract, or the emitted types.

Population bound (PROVED before any counter was read)

Whole tracked tree at this head, 7791 files, by two independent enumerations that agree
exactly: git ls-files = 7791 and git ls-tree -r --name-only HEAD = 7791, with a zero-line
diff between the sorted lists. Reader-census sub-population: 1463 non-test files under
packages/*/src, taken in the directory-prefix form.

⚠️ The pathspec trap fires on this repo and I measured it rather than assuming it:
git ls-files 'packages/types/src/**/*.ts' returns 212 where the directory-prefix form
returns 255 .ts files — 43 dropped, every one of them directly under src/, and
packages/types/src/objectql.ts, the file this whole card edits, is among the dropped. Every
figure below uses the directory-prefix form.

Corpus, with resolved paths printed. From packages/types: zod@4.4.3
(node_modules/.pnpm/zod@4.4.3/node_modules/zod) and @objectstack/spec@17.4.0
(.pnpm/@objectstack+spec@17.4.0_ai@7.0.65_zod@4.4.3_). The store also holds zod@3.25.76, and
both copies ship a v4/ directory — so the directory's existence disambiguates nothing; the
resolved path does, and it is 4.4.3.

Ruled item 1 — the 17, and their types — SATISFIED

Measured at runtime against the resolved spec, not read off a .d.ts:

figure measured dev claimed
ObjectListViewSchema.shape keys 50 50
absent from NamedListView before 22 22
protocol tombstones among them 5 5
⇒ live 17, name-for-name the ruled set 17
of the 17 declared before 0 0
NamedListView members before → after 47 → 64 47 → 64

The tombstones do refuse — the load-bearing step that turns 22 into 17. All five
(bordered performance responsive striped virtualScroll) reject a plausible value, each
with a message naming the 17.0.0 removal. Negative control: six non-tombstone keys (label
description showRecordCount resizable compactToolbar allowPrinting) all accept, so
"refuses" is a reading and not a dead instrument. I then re-derived the tombstone set
independently — sweeping all 50 keys for a refusal message matching was removed in @objectstack/spec — and got exactly those five, no more and no fewer. No member was wrongly
excluded.

The member census is a brace-walk of ownership, not proximity: the TypeScript AST's own
PropertySignature list for interface NamedListView, so nested object-literal members are
structurally incapable of being counted. Self-tested on a fixture with a known answer — a
commented-out member, two nested object literals, a readonly, and a method signature — which it
scored 5/5 correctly, and on a non-existent interface, which it refused rather than returning 0.
Both blobs were pinned with git hash-object against git's own record (941cc00d04
9c9a69491c).

Types are taken from the protocol. 16 members index this package's spec-derived
ListViewSchema; name indexes the protocol's published ListView. 0 of the 17 are hand-written
and 0 emit as bare any.

Ruled item 2 — a read point each, exceptions REPORTED — SATISFIED

15 of the 17 have a read point; tabs and pageName have none and are REPORTED with their
measurements in comment 5671430750 on card #8980 (fetched and read). All three reported
members remain DECLARED — none was dropped, and no read point was invented to make the count come
out even.
I re-took both measurements:

  • tabs — reader census at the sha the dev measured (bbc9dc34e3): 4 hits, with the
    rowColor lit control at 7, exactly as reported, and a negative control at 0. Not one of
    the 4 is this key (3 are plugin-detail's object-detail surface; 1 is UserFilters'
    config.tabs, the page-only key). At this head the same query reads 5/8 — and both extra lines
    are comment lines this PR itself added, which I attributed line-by-line. The reading is
    unchanged.
  • pageNameNamedListView['type'] carries 7 values, the protocol's enum 10
    (grid kanban gallery calendar timeline gantt map chart tree page). page is absent, so no
    authored named view can select the branch pageName configures. Confirmed.
  • chart/tree — genuinely READ (the ablation arm for them goes red), and both absent from
    the 7-value union, consistent with the objectui#5321 host-composition-only route.

Ruled item 3 — which tabs — SATISFIED, and by a stronger instrument than claimed

I verified this by object identity at runtime rather than by reading the derivation:

  • ObjectListViewSchema.shape.tabs is the same object reference as the spec
    ListViewSchema.shape.tabs — it passes through untouched, so it is unambiguously the list-shape
    key.
  • ObjectUserFiltersSchema omits tabs (with showAllRecords and allowAddTab) — that is the
    page-only one.
  • The two protocol shapes differ at exactly one key, userFilters, which independently
    confirms the ListView-not-ObjectListView choice for name.
  • Second half confirmed: objectui#9517 (1bee5d00ad) touched the user-filter FIELD shape; its
    only tabs lines are docblock comments. It did not move the container's tabs. objectui's
    userFilters dialect still carries its own tabs at this head, so the two keys do sit on one
    interface, as the declaration says in place.

Ruled item 4 — the 19 legacy spellings — SATISFIED

Not merely the same set: declaration-identical. I compared name, optionality, readonly and
full type text for all 19 — zero changed. Widening the check, all 47 pre-existing members
carry through with identical signatures, and 0 members were removed. The 19 fall out of the
arithmetic independently: 50 shape keys − 22 absent = 28 shared, and 47 − 28 = 19.

The ablation — RE-RUN, and the controls proved able to fail

Renderer restored alone to the merge-base in my own worktree. Mutation proven two ways: blob
b6c0cc566b008aa55149 by git hash-object, and my own five marker occurrence counts
(grep -o | wc -l, not grep -c) 4/2/1/1/20/0/0/0/0, with a whole-symbol control staying
non-zero throughout and git status showing only that one file moved.

  • Unmutated head: 20/20 pass.
  • Ablated: 13 failed / 7 passed — reproduces the dev's figure exactly. Tree restored to
    b6c0cc566b and verified clean.

A control that cannot fail is not a control, so I tested that directly: three targeted
mutations on the head tree (invert the label/name precedence; inject a default grouping;
add a tabs rung to the relay). Exactly the three targeted control arms went red and nothing
else
— so the 6 control arms discriminate rather than merely passing.

The 7 passers are those 6 controls plus the data arm, and the dev's disclosure of that arm is
accurate: removing an as any is a type-level change with identical runtime behaviour, so it
cannot go red on a renderer ablation. Its source half is genuinely pinned in the types census
instead (NAMED_VIEW_READ_UNDECLARED is now empty and asserted so).

Regression read on both changed packages at this head: 4707 tests / 229 files, all green. No
test was skipped, disabled, .only'd or quarantined anywhere in the diff.

The census ledger — reconciled independently

Not trusting the file's own assertion, I re-derived its arrays from the AST: declared 64 = read
21 + unread 43
, the two sets provably disjoint, their union exactly the declared set with no
member unaccounted. tabs and pageName sit in the unread column with the ruled-outcome
docblock; data and name sit in the read column; NAMED_VIEW_READ_UNDECLARED is empty. The
disclosed one-NAME disagreement is real and is asserted by name rather than smoothed over —
the regex instrument sees 20 because name is read only at the tab strip, and the file pins that
as [...NAMED_VIEW_READS].filter((k) => k !== 'name').

Clause-② — my own reading: yes, on the public-surface limb alone

The criterion is a disjunction, so I measured both limbs separately rather than adopting the
seat's adjudication.

Accept-set limb — ABSENT. Measured, agreeing with the dev. The zod mirror's diff is
comment-only: I compared TypeScript token streams (scanner with skipTrivia) before and
after and they are byte-identical, using an instrument self-tested to fire on a one-identifier
change and not to fire on a comment-only change. objectql.ts compiles to byte-identical
emitted JS on both trees. So the only runtime change in the entire PR is the renderer. And the
accept set was already permissive: objectui's ObjectViewSchema accepts a named view carrying
all 17 protocol keys and retains all 17 after parse (BaseSchema passthrough), while the
negative control objectName: 12345 is correctly refused — so "accepted" is a reading, not an
unrun probe. Nothing was relaxed.

Public-surface limb — FIRES. @object-ui/types is not private, publishes dist, and exports
NamedListView from its public entry (src/index.ts:461). I emitted the declarations at both
trees: the published .d.ts goes 47 → 64 members, gaining exactly the ruled 17, each with a
real protocol-derived type and none collapsed to any.

⇒ One limb fires, so Clause-②: yes is correct. I reach the claiming seat's conclusion by
my own measurement, and I find the dev right about the accept set but wrong in the overall no,
because the criterion is a disjunction and the published type gains 17 members. The minor bump
on both packages matches.

Observations — non-blocking, wording only

  1. The name exception is the right call for the wrong stated reason. The delivered type is
    correct: NamedListView['name'] is exactly SpecListView['name']. But indexing the mirror
    would have yielded the same TypeScript type — I proved ListViewSchema['name'],
    SpecListView['name'] and the declared member are all string | undefined, with an Eq probe
    whose positive and negative controls both fired. The homonym is real at the zod level
    (name is in LIST_VIEW_LOCAL_OVERRIDES, so it resolves through BaseSchema.name
    z.string().optional(), "Component name", carrying no snake_case refinement), but zod
    refinements do not survive into the inferred TS type, so the docblock's "keeps this member
    tracking SnakeCaseIdentifierSchema rather than that neighbour" is not true of the emitted
    type. The choice is still the correct drift-proof one — if the protocol ever brands name,
    SpecListView['name'] tracks it and the BaseSchema route would not — so it is the sentence
    that wants a touch, not the code.

  2. "could not be authored before this change" is too strong, in the ObjectView.tsx docblock
    and in the changeset's Compatibility paragraph ("provably inert on every existing document").
    The runtime already accepted and retained all 17 keys via passthrough, as measured above — so a
    stored JSON document or a JS consumer could already carry a top-level kanban, and for exactly
    those documents behaviour changes from ignoring the block to applying it. That is the ruled
    remedy working as intended, not a defect, and the identity arm covers every document that
    declares no canonical block. Worth saying "could not be authored in TypeScript", since the
    same passthrough is what makes the clause-② accept-set limb absent — the two statements lean on
    one fact in opposite directions.

Landing checks

  1. ① this verdict, above, at the head named above.
  2. PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 9534, run from /home/user/objectstack, exit captured as cmd > file 2>&1; EXIT=$?, never
    through a pipe. BEFORE this comment: exit 0 — "the clause-② declaration is readable in the
    fixed spelling and both carriers agree", confirming the seat's 22:02Z carrier fix rather than
    inheriting the earlier exit 4. AFTER value taken below, after this comment landed.
  3. GET /commits/0e691563def51543e40d5e51a31fcc77e08c0b60/check-runs?per_page=100
    total_count 36 equals the 36 returned. 0 not-green (33 success, 3 skipped) and
    0 still-running, stable across two polls, so this is settled, not a snapshot mid-flight. I
    re-ran nothing. Legacy combined status: success (1 context, Vercel). mergeable_state:
    clean (it had been behind earlier in the session). No instance of the objectui#9468
    shared-CI scratch-dir race appears at this head.

The needs:contract-review label is left exactly as the seat hung it; discharging it is the
seat's, against this verdict. No label added or removed, no review submitted, nothing approved,
enqueued, re-run or pushed.

Implemented-by: claude/issue-8980-named-list-view-declares-the-protocol-17
Reviewed-by: session_01L5xpA5q533BgTTNADibEFt


Generated by Claude Code

@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Contract-review discharge — PR #9534 / card #8980

Posted before the needs:contract-review label is removed, and deliberately so. The Governed Surface Queue Guard leg reads the label, not the verdict: to a label reader, "the carrier was stripped with no PASS on record for this head" and "the carrier was never hung" are the same bytes. Six of the enqueues audited on this board had the carrier stripped shortly before the enqueue with no PASS at that head, and objectui#8164 landed a real published-face defect through exactly that gap. This comment is the record that closes the gap for this PR; the label removal that follows is a consequence of it, not a substitute for it.

Verdict being discharged against

field value
verdict comment 5671718309 — fetched, not cited from a report: #9534 (comment)
author / created claude[bot] · 2026-09-14T22:33:12Z
verdict PASS (round 1, independent contract review)
head the verdict declares 0e691563def51543e40d5e51a31fcc77e08c0b60
head the PR currently carries (GET /pulls/9534head.sha) 0e691563def51543e40d5e51a31fcc77e08c0b60
comparison string equality, both length 40, full 40 characters compared — ⛔ not a prefix match, ⛔ not a short-sha match. Equal.

Read at 2026-09-14T22:36Z.

Clause ② and why the carrier was hung

Card #8980 declares Clause-②: yes, and this seat stands by that against the dev report's no. .claude/skills/pm-dispatch/SKILL.md:476 states the criterion as a disjunctiondoes this card relax the accept set or widen the public surface — and the dev evaluated it as a conjunction: it found the accept-set limb absent and concluded no, while its own sentence conceded that the TS face gains optional members, which widens.

The reviewer measured both limbs independently and reached yes on its own instruments, not by adopting the declaration:

  • accept-set limb — ABSENT. The zod mirror diff is comment-only (token streams byte-identical, comparator self-tested to fire on a one-identifier change and not fire on a comment-only change); objectql.ts emits byte-identical JS. The runtime already accepted and retained all 17 keys through BaseSchema passthrough, with a firing negative control (objectName: 12345 correctly refused).
  • public-surface limb — FIRES. @object-ui/types is not private and exports NamedListView from src/index.ts:461; the emitted .d.ts goes 47 → 64 members across the two trees, gaining exactly the 17 ruled names, each with a real protocol-derived type and none collapsed to any.

One limb firing is sufficient. Clause-②: yes stands, now on two independent measurements rather than one declaration.

Other gates at this same head

  • PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 9534 → exit 0, run from /home/user/objectstack after the verdict landed, exit captured as cmd > file 2>&1; EXIT=$?. Output: "both carriers agree". Re-run after the label removal below.
  • total_count 36 == 36 returned; 36/36 completed; {success: 33, skipped: 3}; 0 not-green, 0 still running; legacy combined status success; mergeable_state clean. ⚠️ This is the draft check set and does not survive the undraft, which starts a 37th check; ③ is re-taken afterwards.

What is being removed, and from where

needs:contract-review comes off PR #9534 and off card #8980, by targeted DELETE /issues/{n}/labels/needs%3Acontract-review on each — ⛔ not by writing a replacement label set, which would silently drop anything a concurrent writer had added. Each removal is read back and diffed against the set observed immediately before it, so the record shows that exactly one label moved and nothing else did.

Label sets before removal — PR #9534: package: types, plugin, tests, needs:contract-review. Card #8980: package: types, pm:dispatched, priority:p2, domain:spec, needs:contract-review.

The two non-blocking wording observations the reviewer recorded (the name exception's rationale asserts a TS-level type difference that does not exist today, and "could not be authored before this change" should read "in TypeScript") are not discharged by this comment and do not block the merge; they are carried in the verdict comment and answered separately.


Generated by Claude Code

@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Enqueued

Continuation of the discharge record above. Every step below is a reading, with its instrument and time named.

step reading time (UTC)
carrier removed from PR #9534 DELETE /issues/9534/labels/needs%3Acontract-review200; label set package: types, plugin, tests, needs:contract-reviewpackage: types, plugin, tests; set-difference: removed exactly {needs:contract-review}, added {} 22:37:43Z (timeline unlabeled)
carrier removed from card #8980 same targeted form → 200; domain:spec, package: types, pm:dispatched, priority:p2 remain; removed exactly {needs:contract-review}, added {} 22:37Z
② re-run after the removal --pair 9534 → exit 0. The message itself changed once the carrier came off — it now also reads "a review of record names this head", where before the removal it stopped at "both carriers agree" 22:38Z
POST /pulls/9534/ccr/ready_for_review 200, body {"draft":false}; timeline ready_for_review 22:38:18Z
37th check Governed Surface Queue Guardcompleted / success 22:38:46Z
③ re-taken at the same head total_count 37 == 37 returned; 37/37 completed; {success: 34, skipped: 3}; 0 not-green, 0 still running; legacy combined success; mergeable true, mergeable_state clean 22:38Z
PUT /pulls/9534/ccr/auto_merge {"merge_method":"SQUASH"} 200, body {"enabled":true,"merge_method":"squash"} 22:39:14Z
enqueue verified on the timeline added_to_merge_queue 22:39:14Z

head.sha re-read after the undraft is still 0e691563def51543e40d5e51a31fcc77e08c0b60, so the ① proof in the discharge record still describes the commit now in the queue. The 37th check is the only delta between the draft-time ③ and this one.

The ordering the discharge record promised is visible in the PR's own timeline and can be checked without trusting this comment: labeled needs:contract-review 22:02:36Z → discharge record posted 22:37Zunlabeled needs:contract-review 22:37:43Zready_for_review 22:38:18Zadded_to_merge_queue 22:39:14Z. The carrier came off after a PASS was on record for this exact head, not shortly before an enqueue with nothing behind it.

⚠️ auto_merge reads back null on GET /pulls/9534 now that the PR is queued — the enqueue consumes it; the authority is the timeline event, ⛔ not the field.


Generated by Claude Code

Merged via the queue into main with commit 0e2ddd4 Sep 14, 2026
38 checks passed
@claude
claude Bot deleted the claude/issue-8980-named-list-view-declares-the-protocol-17 branch September 14, 2026 22:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant