Skip to content

fix(core,plugin-view,types): the density fold tests an own key; the flat-property warning and NamedListView's docblock stop naming retired channels (objectui#10868) - #10883

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-10868-list-view-leftovers
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-10868-list-view-leftovers

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #10868
Clause-②: no

Three hygiene leftovers from the objectui#7924 and objectui#7928 retirements, as the card's Fix section lists them. Claim 5861960491, seat domain:ui#1. The implementing dev ran under session session_01DuWo5bdP9SdVebamn99GGk (the dispatching seat's id).

What changed

  1. @object-ui/core: the density fold tests an own key. normalizeListViewSchema set foldRowHeight with legacyDensity in DENSITY_MODE_TO_ROW_HEIGHT. It now uses Object.prototype.hasOwnProperty.call(DENSITY_MODE_TO_ROW_HEIGHT, legacyDensity). That is the idiom rowHeightToDensityMode and LIST_VIEW_KINDS already use in the same file. It was the only in operator in that file's code.
  2. @object-ui/plugin-view: the dev-mode flat-property warning in generateViewSchema. It said "Move them under options.KIND". It now says "Move them under its top-level KIND block ({ KIND: { ... } })", which is the block viewOptions[viewType] reads. A comment above it says why.
    • The edit sits above the VIEW-TYPE BRANCHES #region.
    • Nothing inside the HOST-COMPOSITION #region changed. objectui#10758 works there.
  3. @object-ui/types: NamedListView's docblock. It said "Used in ObjectViewSchema.listViews for named views".
    • It now says that no member of the contract has been typed by it since objectui#7928.
    • It names the two type pins that hold this in object-view-unmirrored-keys-7779.test.ts: _ListViewsIsNoLongerTheLocalRecord and _ListViewsIsTheSpecRecord.
    • It says the type stays exported, and that whether it is retired or narrowed follows objectui#7924.
    • The interface is byte-identical.
  4. Changeset .changeset/10868-list-view-leftovers.md: patch for @object-ui/core and @object-ui/types. @object-ui/plugin-view is not listed, because the warning does not ship (measured below).

Measurements

The fold, on the built package. These readings come from @object-ui/core's dist, as resolved from plugin-view, with normalizeListViewSchema({ densityMode: X }):

X base de1b879a6 head
'toString', 'constructor', 'hasOwnProperty', 'valueOf' rowHeight is a function, key dropped rowHeight undefined, key kept
'__proto__' rowHeight is Object.prototype, key dropped rowHeight undefined, key kept
'cozy' (control) undefined, key kept undefined, key kept
'compact' / 'spacious' 'compact' / 'tall', key dropped unchanged

The pin is red on base and green on head. The new cases sit in normalize-list-view.test.ts under densityMode → rowHeight.

  • Each of the five inherited keys stays unfolded, and with viewType: 'grid' the input comes back by reference.
  • A control case checks that 'cozy' stays unfolded and 'compact' still folds.
  • I wrote the test before the fix: Tests 5 failed | 87 passed (92). The 5 failures are the inherited keys (expected [Function toString] to be undefined), and the control stayed green.
  • With the fix: Tests 92 passed (92).

Where the fold is still reachable. This answers the card's reachability question.

  • The fold runs at three call sites:
    • plugin-list ListView, on its own schema;
    • plugin-view ObjectView, on the host views entry (the rowHeight relay, inside the HOST-COMPOSITION fence);
    • app-shell ObjectView, on the stored view definition.
  • A named view refuses densityMode by name: unrecognized_keys at listViews.v1, for 'compact' too. The control rowHeight: 'compact' is accepted.
  • @objectstack/spec 17.4.0's dist has 0 densityMode hits. The control rowHeight has 4 hits in dist/index.js.
  • The list-view node's zod schema still declares densityMode as a three-value legacy enum. It accepts 'compact' and refuses 'toString' with invalid_value.
  • So a real density still reaches the fold from a list-view node, a host views entry or a stored view body. 'toString' reaches it only on a path that does not validate.
  • No public door was driven.

The warning's remedy, run once. I used a temporary probe (not committed). It renders the real ObjectView with a host views kanban entry and NODE_ENV stubbed to development.

  • A flat groupField: 'stage' gives lane status. The warning now reads "The view uses flat properties ["groupField"]. Move them under its top-level kanban block ({ kanban: { ... } }) …".
  • Moved as the new text says, kanban: { groupField: 'stage' } gives lane stage and no warning.
  • Moved as the retired text said, options: { kanban: { groupField: 'stage' } } gives lane status and no warning. The old text sent the author to a place nothing reads.

The warning does not ship. plugin-view's vite.config.ts defines process.env.NODE_ENV as "production", and the build removes the development branch.

  • After turbo run build, Spec Compliance and Move them under have 0 hits in dist/index.js and dist/index.umd.cjs. The control ObjectView data fetch error hits both files.
  • files is dist, README, CHANGELOG and LICENSE.
  • So plugin-view gets no changeset entry: its CHANGELOG would describe text that no consumer receives. check-changeset-presence is satisfied by the one changeset: "4 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)".

The docblock ships. In types/dist/objectql.d.ts, the old sentence has 0 hits and the new pin name has 1.

Verification, all at head 96c9f9953

Heavy runs went through os-verify-lock.sh, and each exit was captured with the output redirected first.

  • Build: turbo run build --filter=@object-ui/plugin-view... gave 16 of 16 tasks successful, VERDICT command-exit 0.
  • type-check (tsc --noEmit plus tsconfig.test.json) exited 0 for each of @object-ui/types, @object-ui/core and @object-ui/plugin-view. I captured each exit on its own, and the script name echoed. core's test program lists the pin file (--listFiles).
  • vitest run packages/core/ packages/types/ packages/plugin-view/: 491 files, 9613 tests passed, VERDICT command-exit 0.
  • Downstream consumers of the fold: packages/plugin-list/ plus the 13 app-shell and react test files that name densityMode, normalizeListViewSchema or rowHeight. 117 files, 1310 tests passed.
  • vitest run scripts/__tests__/ (the whole directory): 177 files passed and 2 skipped (179), 5317 tests passed.
  • Gates that exited 0:
    • changeset gates: check-changeset-presence, -no-major, -fixed, -overwrite ("1 changeset(s) added, 0 modified"), -claims, and check:pending-changeset-literals;
    • check:control-bytes, check:new-line-citations ("0 new citation(s)") and check:spec-symbols;
    • test-harness gates: check:test-path-roots and the three check:vi-mock-*;
    • reader gates: check:handler-key-reads, check:i18n-keys, check:icon-record-names, check:unreferenced-sources, check:element-data-source-declaration, check:doc-types, check:prompt-keys, check:action-forward-parity, check:metadata-write-doors and check:registry-bare-names;
    • coverage and import gates: check-lint-coverage, check-type-check-coverage, check:self-import and check:phantom-deps;
    • check-governed-queue-guard --test on the 5 paths: NOT GOVERNED.
  • NOT MEASURED:
    • check:readme-exports. Prerequisite not met: 20 packages are unbuilt, and its population floor refused the run. No README is touched.
    • check:sdui-registration-pins. It needs a console build.
    • Spec Main Shape Gate.
    • CI runs all three.
  • ESLint --no-inline-config --format json on the 4 touched TS/TSX files: 0 errors and 94 warnings, the same per-file counts as base (60 and 34).
    • Population: all 4 files are in lint scope.
    • Invariance: eslint.config.js has 0 projectService / parserOptions (the control languageOptions has 1), so linting is not type-aware.
    • No eslint-rules/ rule reads the disk, so no untouched file's verdict can move.

Acceptance notes

  • The warning's per-key remedy. This predates the PR and is not changed here. Moving a flat key into the named block word for word lands on a spelling the branch strips, for groupBy on kanban and dateField on calendar.
    • Measured with the same probe: kanban: { groupBy: 'stage' } gives lane status and no warning, while the control groupByField gives stage.
    • calendar: { dateField: 'due' } gives a node without startDateField and no warning, while the control startDateField gives due.
    • The retired text had the same word-for-word shape. The text is development-only and does not ship, so no card is filed. .changeset/10002-map-style-remedy-text.md describes how the map warning's remedy was made executable. Carrier: none.
  • Stale prose that names options.kanban.conditionalFormatting as the nested home. It appears in the generateViewSchema kanban-branch comment ("nested options.kanban.conditionalFormatting wins") and in the header of ObjectView.kanbanConditionalFormatting.test.tsx. Since objectui#7928 the nested home is the top-level kanban block. These are comments only, so I left them untouched. The file is shared with objectui#10758's claim, so the in-place exemption does not apply. Carrier: none.
  • The comment above legacyViewOptions. It describes options.KIND as retired and folded at ViewPreview, which is correct, so it is unchanged.

Generated by Claude Code

`normalizeListViewSchema` tested `legacyDensity in DENSITY_MODE_TO_ROW_HEIGHT`,
and `in` walks the prototype chain: `densityMode: 'toString'` folded into
`rowHeight` as `Object.prototype.toString` and the key was dropped. The fold
now uses the own-key test `rowHeightToDensityMode` already uses in the read
direction, so an inherited key is an unrecognized density, like `'cozy'`.

Pinned beside the existing density cases: five inherited keys stay unfolded
(and a canonical view comes back by reference), the `'cozy'` control is
unchanged, and `'compact'` still folds.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
… KIND block; NamedListView's docblock names no channel it does not type

plugin-view: `generateViewSchema`'s dev-mode warning told authors to "Move
them under options.KIND". Since objectui#7928 this component reads no
`options` bag and a named view refuses `options` by name, so the text sent an
author to a place that is ignored or refused. It now names the view's own
top-level KIND block, the one `viewOptions[viewType]` reads. The text sits
behind the development-only branch, which the package build defines away.

types: `NamedListView`'s docblock said "Used in ObjectViewSchema.listViews".
That member is the protocol's `ObjectListViewSchema` by reference since
objectui#7928. The docblock now says the type is exported and types no member
of the contract, and points at the pins that hold that. The type is unchanged.

Changeset: patch for @object-ui/core (the fold) and @object-ui/types (the
docblock).

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 27 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/5903-objectgantt-declared-keys.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    Both halves move together. The TS declaration (packages/types/src/objectql.ts) and its zod mirror (src/zod/objectql.zod.ts) gain the same ten keys at the same requiredness — all optional — and no KnownDrift entry is added. navigation is taken from @objectstack/spec's NavigationConfigSchema by reference rather than restated, matching ObjectGridSchema.navigation.

.changeset/6726-find-envelope-records-arms.md

  • names plugin-view/src/ObjectView.tsx → packages/plugin-view/src/ObjectView.tsx — edited by this change

    | module | what it does | | --- | --- | | components/src/hooks/related-count-store.ts | related-list tab badge count | | components/src/renderers/basic/data-list.tsx | element:repeater rows | | components/src/renderers/basic/elements.tsx | element:number client-side aggregate | | components/src/renderers/basic/record-picker.tsx | element:record_picker options | | plugin-detail/src/renderers/record-activity.tsx | record:activity self-fetch | | plugin-detail/src/renderers/record-history.tsx | record:history self-fetch | | plugin-view/src/ObjectView.tsx | non-grid (kanban / calendar / gallery / timeline) fetch |

.changeset/7070-no-invented-gantt-date-fields.md

  • names plugin-view/src/ObjectView.tsx → packages/plugin-view/src/ObjectView.tsx — edited by this change

    • app-shell/src/views/ObjectView.tsx — the console object page. The inline branch becomes ganttViewOptions, the sibling of calendarViewOptions and timelineViewOptions: the declared block spread whole, title floored at 'name', no date field invented. - plugin-list/src/ListView.tsx — the render branch AND the capability gate. - plugin-view/src/ObjectView.tsx — generateViewSchema, the authored object-view element route, which bypasses ListView entirely.

.changeset/7322-object-kanban-group-by-limit.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    Breaking for authored metadata: ObjectKanbanSchema.groupField is RETIRED (objectui#7322, ADR-0049 enforce-or-remove), and the two keys the object-kanban renderer actually reads — groupBy and limit — are now DECLARED and validated on both published faces: the TypeScript interface in objectql.ts and the Zod mirror in zod/objectql.zod.ts.

  • names packages/core/src/utils/normalize-list-view.ts → packages/core/src/utils/normalize-list-view.ts — edited by this change

    Who is NOT affected. The VIEW-LEVEL kanban config is untouched: kanban.groupField there is a live legacy alias of the spec's groupByField (packages/core/src/utils/normalize-list-view.ts maps it; plugin-list's ListView and plugin-view's ObjectView still read it). groupField is dead only on the object-kanban NODE. The declarative kanban node (KanbanSchema) is untouched, BaseSchema's unknown-key policy is byte-identical (an undeclared key still passes through), and the renderer is unchanged — boards authored the documented way rendered before and render now. One in-repo fixture authored groupField on this node (packages/types/src/__tests__/kanban-conditional-formatting.test.ts); it now authors groupBy. No doc snippet, catalog entry, skill or app in this repository authored groupField on an object-kanban node.

.changeset/7363-objectql-union-arms.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    ObjectGallerySchema and ObjectDataTableSchema are members of ObjectQLComponentSchema on both faces — the TS union in objectql.ts and the zod union in zod/objectql.zod.ts — so AnyComponentSchema, and with it validateSchema / safeValidateSchema / objectui validate, has an arm for object-gallery and object-data-table nodes (objectui#7363).

.changeset/7499-gantt-non-axis-floors-omitted.md

  • names plugin-view/src/ObjectView.tsx → packages/plugin-view/src/ObjectView.tsx — edited by this change

    • plugin-list/src/ListView.tsx — the object-gantt render branch. - plugin-view/src/ObjectView.tsx — generateViewSchema, the authored object-view element route, which bypasses ListView entirely.

.changeset/7632-shared-record-source-config.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    That ladder is published contract on both faces — packages/types/src/objectql.ts and its zod mirror both ship .describe() strings naming getDataConfig's order (77cb489b4, maintainer ruling 2026-09-02), pinned by objectql-record-source-refinement-6939.test.ts — and it was hand-copied into five plugin components with no gate holding them together. A change to the ruled order had five edit sites and nothing that noticed a missed one; that is the AGENTS.md #0.1 drift class.

.changeset/7772-page-block-kanban-group-by-control.md

  • names core/src/utils/normalize-list-view.ts → packages/core/src/utils/normalize-list-view.ts — edited by this change

    The view-level kanban.groupField alias (core/src/utils/normalize-list-view.ts) is live and untouched — node-level retirement and the view-level alias are different things.

.changeset/7773-kanban-adapter-groupfield-write.md

  • names normalize-list-view.ts → packages/core/src/utils/normalize-list-view.ts — edited by this change

    Who is NOT affected — the boundary is node-local. Every VIEW-LEVEL groupField read is untouched and still live: it is a legacy alias of the spec's groupByField on the kanban view config, mapped by normalize-list-view.ts, and both adapters still resolve lanes through it (ObjectView.tsx's kanbanCfg.groupField ||, ListView.tsx's groupByField || groupField). Authoring options.kanban.groupField on a list-view or object-view keeps working exactly as documented in packages/plugin-list/README.md. groupField is dead only on the generated object-kanban NODE.

.changeset/7804-objectql-handler-key-arms.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    The four plain objectql.ts node faces declare the nine handler keys their registered renderers read (objectui#7804, the objectql.ts slice): ObjectFormSchema.onCancel / .onError / .onOpenChange / .onStepChange / .onSuccess, ObjectGallerySchema.onCardClick / .onRowClick, ObjectGridSchema.onNavigate and ObjectViewSchema.onNavigate.

.changeset/7804-tree-view-handler-slot.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    'runtime-slot' and not 'retired', measured at this key's own channel. 'retired' publishes "no renderer reads this key, so nothing could ever run it" — true of the two siblings already tombstoned on this arm (onSelectChange, onExpandChange) and flatly false here, since the read is live and INVOKED. ⚠️ No in-repo host builds a tree-view node carrying the key: the channel is wired end to end and only the supplier is absent, which is the same shape as ObjectFormSchema.onStepChange in this card's objectql.ts slice and is not evidence of a dead read. The TypeScript declaration is unchanged and still callable, so a programmatic host supplies it exactly as before.

.changeset/7963-alert-dialog-footer-keys-retired.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    Nothing else moves. These spellings are overloaded across the tree and every other owner is a live key on a different declaration — FormSchema.cancelLabel, objectql.ts's confirmLabel, plugin-designer's ConfirmDialog React props, plugin-grid's def.confirmLabel, and plugin-form's ModalForm / DrawerForm, which build a local cancelLabel from schema.cancelText. None is an AlertDialogSchema; none is touched, and a pin asserts it. No fixture, catalog schema, example app or doc fence authored any of the three on an alert-dialog node, so no shipped document is stranded.

.changeset/8653-listview-title-retired-rowactiondefs-pinned.md

  • names packages/plugin-view/src/ObjectView.tsx → packages/plugin-view/src/ObjectView.tsx — edited by this change

    title — retired. ListView resolved its export filename through schema.label || (schema as any).title. @objectstack/spec/ui's ListViewSchema refuses title by name (unrecognized_keys: ['title']) while ObjectGridPropsSchema accepts it; packages/types mirrors the platform contract rather than ruling over it, so declaring title on ListViewSchema would have made this repo accept what the platform save gate rejects. That asymmetry is also why objectui#6639 could take the declare branch for ObjectGridSchema.title one package over and this site could not. A parse-based census of apps/ examples/ content/ and packages/ found zero list-view nodes authoring title, so the retirement costs no author a filename. Over that same corpus the instrument reports three object-grid nodes carrying the key: two authored ones, both in content/docs/api/schema-reference.md, plus one that is not authored at all — packages/plugin-view/src/ObjectView.tsx composes title: schema.table?.title onto a grid node it builds, so it is a producer writing the key rather than an author declaring it. ObjectGrid's own title reads are untouched — they remain declared, ruled and read.

.changeset/8767-object-grid-refuses-string-sort.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    Migration. Write the array: sort: [{ field: 'name', order: 'desc' }]. Both keys are required. SortConfig.order carries no ? in @object-ui/types (packages/types/src/objectql.ts) and no .optional() in its zod mirror, and the protocol's own reusable SortItemSchema requires order as well — measured: that schema refuses [{ field: 'name' }] with invalid_value at 0.order. Do not omit it: this block's array arm interpolates whatever is present, so an omitted order lowers to $orderby: 'name undefined' today. That is pre-existing behaviour on the arm this change does not touch, and it is filed as a successor card rather than widened into here.

.changeset/8801-object-kanban-allow-collapse-retired.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    • the declarations retired here — packages/types/src/objectql.ts and its mirror packages/types/src/zod/objectql.zod.ts; - the pins that assert the retirement — object-kanban-allow-collapse-retired-8801.test.ts and bare-kanban-node-key-retired-8802.test.ts; - a comment in packages/types/src/zod/complex.zod.ts, recording that the deleted retiredZeroReadKanbanKey helper once carried this spelling on the SIBLING arm; - one row of content/docs/api/schema-reference.md; - the .changeset/ release notes that discuss it — this one, the two historical entries covering the sibling arm's own spelling, and objectui#9629's note recording the correction to this paragraph.

.changeset/8885-object-chart-drilldown-title-compareto.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    ObjectChart.tsx reads all three off schema, and until now neither published copy declared any of them: not the TS interface (packages/types/src/objectql.ts) and not the zod mirror (packages/types/src/zod/objectql.zod.ts). They rode BaseSchema's index signature / .passthrough() and arrived unvalidated. drillDown was the sharpest case — this component's registry inputs advertise it to the designer palette, and @objectstack/spec publishes ChartDrillDownSchema for exactly this carrier, so an author was offered a key that neither published shape mentioned.

.changeset/8913-object-kanban-columns-declared.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    What moved. ObjectKanbanSchema gains columns on both halves that move together — the TypeScript interface (objectql.ts) and its Zod mirror (zod/objectql.zod.ts). Retiring the bare kanban node type key (objectui#8802) removed the only face that judged a lane, and object-kanban had never declared the key, so it rode BaseSchema's [key: string]: any / .passthrough(): read by the renderer at three sites, named by no published face.

.changeset/8990-object-kanban-groupby-optional.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    @objectstack/spec declares the key optional — groupBy: z.string().optional() on ObjectKanbanPropsSchema — while this package required it on the TypeScript declaration (packages/types/src/objectql.ts) and on the Zod mirror (packages/types/src/zod/objectql.zod.ts). objectui was therefore narrower than the protocol on a published key: ObjectKanbanSchema.safeParse and safeValidateSchema refused an object-kanban node the protocol accepts, and such a node could not be annotated with its own type.

.changeset/9092-inline-locale-declared-face.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    • AppComponentSchema.label (app.ts) - ObjectGridSchema.label and .description (objectql.ts) - PageNodeSchema.aria.ariaLabel (layout.ts)

.changeset/9309-object-gallery-filter-destination-typed.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    ObjectGallerySchema.filter is typed as the destination its own docblock names — QueryParams['$filter'] — on both faces, the TS interface in objectql.ts and the zod mirror in zod/objectql.zod.ts (objectui#9309).

.changeset/9511-record-id-is-a-string.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    The three authorable keys, each on BOTH faces. ObjectFormSchema.recordId (objectql.ts + zod/objectql.zod.ts), DetailViewSchema.resourceId (views.ts + zod/views.zod.ts) and DetailSchema.resourceId (crud.ts + zod/crud.zod.ts). ⚠️ The crud pair is DetailSchema, not DetailViewSchema, and it reaches the same renderer — not by symbol but by data flow: plugin-detail registers the 'detail' node type onto DetailView. A read that follows TypeScript symbols alone finds two keys and is incomplete.

.changeset/9549-tree-filter-declared.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    ObjectTreeSchema.filter is declared on both faces, in the shape objectui#9309 settled for ObjectGallerySchema.filter: QueryParams['$filter'] by indexed access on the TS interface in objectql.ts, and the same two-arm union (array first) on the zod mirror in zod/objectql.zod.ts (objectui#9549).

.changeset/9550-object-tree-root-barrel.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    ObjectQLComponentSchema declares the node types an ObjectQL block may be. Every one of its arms was a named export of this package's root barrel except ObjectTreeSchema, which was declared in objectql.ts, applied by the union, and re-exported by the ./zod barrel (objectui#7917) — while no TypeScript consumer could name it. There is no ./objectql subpath to reach around the barrel: the package's exports map is pinned by packages/types/src/__tests__/package-exports-manifest.test.ts, and the root barrel was the only route to this type.

.changeset/9606-object-kanban-card-title.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    Both published faces of the object-kanban arm now name the key: the zod mirror ObjectKanbanSchema in zod/objectql.zod.ts and its TypeScript twin, the ObjectKanbanSchema interface in objectql.ts. Both declare it OPTIONAL, at the same requiredness the other face uses, so the two faces accept and refuse the same documents. (Located and cited by SYMBOL: line addresses in zod/objectql.zod.ts have drifted before, and this change is itself about a drifted mirror.)

.changeset/9628-kanban-column-collapsed-honoured.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    The key was declared on both published faces of the object-kanban arm — the lane element of ObjectKanbanSchema (objectql.ts and its Zod mirror) and the runtime lane KanbanColumn (complex.ts and its mirror) — and read by KanbanEnhanced alone, a module no production source imports. An authored { "id": "todo", "title": "To Do", "collapsed": true } therefore parsed green on both faces and reached a board that did nothing with it: KanbanImpl's only collapse is the SWIMLANE row's, held in viewer state under objectui:kanban-collapsed:ANGLE-BRACKETS(swimlaneField) and never keyed to a lane's declared value. That is the ADR-0049 declared-but-unhonoured shape.

.changeset/9880-undrawable-view-kinds-both-legs.md

  • names normalize-list-view.ts → packages/core/src/utils/normalize-list-view.ts — edited by this change

    Compiled both ways rather than argued. With the published pin @objectstack/spec@17.4.0 installed, turbo run type-check --filter=@object-ui/core exits 0 before and after this change. With a spec built from objectstack main injected in its place, the same command reported the TS2353 in normalize-list-view.ts before and does not report it after; the diagnostics that remain on that leg come from other files, are reproduced against the pristine source, and are not this change's.

.changeset/object-view-unmirrored-keys-7779.md

  • names packages/plugin-view/src/ObjectView.tsx → packages/plugin-view/src/ObjectView.tsx — edited by this change

    What was measured. Every reading was taken on the object-view node renderer (packages/plugin-view/src/ObjectView.tsx, registered by plugin-view/src/index.tsx) with schema.objectName / schema.layout as the positive controls of the same schema.KEY query, so each zero is a reading; the repo-wide census of viewTabBar finds the key in no source file outside @object-ui/types (two doc tables listed it as authorable and are corrected here). The spec side was read through the installed pin (@objectstack/spec@17.2.0, ui entry, 117 exported object schemas walked; control keys objectName / columns / navigation / listViews hit): the three spec-modelled keys are optional slots on ListViewSchema and ObjectListViewSchema; the six local keys have no spec slot anywhere.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Angle-bracketed names in the quoted prose above are rewritten as ANGLE-BRACKETS(name): GitHub deletes tag-shaped fragments from a stored body, and a quote that silently loses the identifier it is about is worse than a visible repair.

Compared the checked-out tree with c6678b1bd (merge-base with origin/main): 4 file(s) changed outside .changeset/, read against 1649 pending declaration(s) that publish a body (2240 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3087.5 KB 3104.5 KB
Main entry chunk (gzip) 148.4 KB 350 KB
Entry file index-CBqo08la.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.57KB 6.17KB
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) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.17KB 10.58KB
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.39KB 1.21KB
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.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
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) 27.13KB 7.95KB
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) 557.59KB 133.61KB
core (index.js) 9.93KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 225.01KB 62.50KB
fields (index.js) 261.26KB 66.37KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
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) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
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.33KB 3.07KB
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) 16.01KB 3.93KB
plugin-calendar (index.js) 51.73KB 14.72KB
plugin-charts (index.js) 83.99KB 22.86KB
plugin-chatbot (index.js) 197.67KB 46.90KB
plugin-dashboard (index.js) 136.82KB 36.44KB
plugin-designer (index.js) 216.42KB 44.47KB
plugin-detail (index.js) 233.48KB 61.79KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 161.21KB 41.41KB
plugin-gantt (index.js) 169.83KB 41.99KB
plugin-grid (index.js) 218.66KB 59.90KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 114.78KB 28.44KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.17KB 12.20KB
plugin-timeline (index.js) 31.00KB 9.09KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 87.90KB 22.06KB
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.81KB 3.58KB
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) 119.16KB 39.05KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
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.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.22KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.52KB 1.36KB
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.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
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) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.27KB 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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 96c9f9953ef9b23d732b71b35296a6c9c804e43a

Diff read three-dot against objectui origin/main 1c5ee334a (merge-base de1b879a6): 5 files, +79 / −6. The PR implements card objectui#10868 (claim 5861960491, seat domain:ui#1); the dev report 5862278805 was read as a claim and each measurement below was re-taken or re-derived from source. Read-only reads of the branch through a private review ref; no gate family was re-run.

① Derived judgments

  1. The fold's own-key test is right for every input class. normalizeListViewSchema now gates foldRowHeight on typeof legacyDensity === 'string' && Object.prototype.hasOwnProperty.call(DENSITY_MODE_TO_ROW_HEIGHT, legacyDensity), the idiom the same file already uses in rowHeightToDensityMode, readListViewKind (on LIST_VIEW_KINDS) and the UNDRAWABLE_VIEW_KINDS read. I ran the base and head copies of the module under Node with types stripped: on base, 'toString', 'constructor', 'hasOwnProperty', 'valueOf' and 'isPrototypeOf' fold to a function and '__proto__' folds to Object.prototype, each dropping the key; on head all six leave rowHeight undefined, keep densityMode, and a { viewType: 'grid', densityMode } input comes back by reference. 'cozy', a number, null, an array and an object are unchanged on both; compact / comfortable / spacious fold to compact / medium / tall on both; canonical rowHeight still wins when both keys are present. At the merge-base the only in operator in the file's code was that one line (the other hit is inside a string literal), so nothing else in normalize-list-view.ts still tests a lookup table with in.
  2. The pin fails on base for the right reason and carries both controls. The it.each over five inherited keys asserts rowHeight undefined first, so on base each case fails at expected [Function toString] to be undefined (and Object.prototype for '__proto__'), not on a later assertion; the by-reference assertion would also fail on base because the fold allocated a copy. The sibling case holds the 'cozy' control (unfolded, key kept) and the 'compact' fold (rowHeight: 'compact', key dropped), and the pre-existing 'cozy' case is untouched. isPrototypeOf is not in the list; same class, same fix, not a gap.
  3. The rewritten warning names the block the component reads. generateViewSchema reads every kind's config off viewOptions[viewType] (viewOptions.kanban, viewOptions.calendar, …), and since objectui#7928 no options bag is read and a named view refuses options by name, so the old remedy pointed at a place that is ignored or refused for every key. The new text, "Move them under its top-level KIND block ({ KIND: { ... } })", is an executable remedy for eleven of the thirteen keys it can name: groupField and titleField on kanban; startDateField, endDateField, titleField on calendar; imageField and titleField on gallery; dateField, startDateField, titleField on timeline; startDateField / endDateField on gantt; locationField, latitudeField, longitudeField, titleField on map (FLAT_MAP_CONFIG_SPELLING). The two it is wrong for are groupBy on kanban (destructured away as _strayGroupBy) and dateField on calendar (destructured away as _retiredDateField): both flat spellings are already a stray or retired alias whose real remedy is a rename to groupByField / startDateField, which neither the old nor the new sentence gives. That is the pre-existing per-key remedy flaw the dev names, narrowed by this PR rather than introduced; the card's acceptance requires only that no dev-mode text name options.KIND, and the changeset carries no plugin-view sentence, so nothing in the acceptance or the changeset rests on the two-key false remedy. Carried to ③.
  4. The warning is compiled out of the published dist. packages/plugin-view/vite.config.ts defines process.env.NODE_ENV as JSON.stringify(process.env.NODE_ENV || 'production'); no workflow under .github/workflows sets NODE_ENV, so the branch becomes a constant-false if that the library build removes, and files publishes dist, README, CHANGELOG and LICENSE only. I did not rebuild (no node_modules in the shared checkout); the dev's zero-hit reading of Spec Compliance / Move them under in both dist files beside a firing control (ObjectView data fetch error) is consistent with that config and is accepted on it. No consumer of the package receives the text, so no plugin-view changeset is owed.
  5. The docblock's sentences are true. In packages/types/src outside tests the only non-comment mentions of NamedListView are its declaration and the index.ts re-export; ObjectViewSchema.listViews is a record of string to z.input of typeof SpecObjectListViewSchema, whose own docblock already says "NamedListView is no longer this member's type, and its retirement follows objectui#7924", so the new sentence and the member it cross-references agree. The two pins it names, _ListViewsIsNoLongerTheLocalRecord and _ListViewsIsTheSpecRecord, exist at the named path __tests__/object-view-unmirrored-keys-7779.test.ts. "Stays exported" holds (index.ts). The interface body is byte-identical (the hunk touches docblock lines only). Consumers outside the package still index the type (plugin-view's AuthoredFilterSegments.view: NamedListView['filter'], the finding(types): NamedListView declares none of 17 members the protocol declares live on the same surface — objectui is NARROWER than the spec, the direction the principle forbids #8980 fixtures), and the docblock scopes its claim to "this package's contract", which is exact.
  6. Clause-②: no is honest. The declaration is directional (widening the accept set or the public surface); this diff adds no zod key, enum member, export or registration, and the fold now folds strictly less than before. The fleet's own reader, check-widening-tells.mjs --declaration no on the three-dot diff, reports 0 tells with all 5 files NOT MEASURED (no declared surface covers them), which is consistent and not a proof. The behaviour change is on an input no validating door admits: the list-view node's zod densityMode is z.enum(['compact', 'comfortable', 'spacious']), and a named view is the spec's strict record, which refuses densityMode as an unrecognized key.

② Semver level

patch for @object-ui/core (a behaviour fix on an input no producer writes and no door validates) and @object-ui/types (a published docblock, the emitted .d.ts text changes, the type does not) — right for both. Leaving @object-ui/plugin-view out is right: the only change is a development-only string the build defines away, the presence gate asks for one declaration per change (an empty frontmatter being the "no release" form), and the claim comment pre-set the condition ("for plugin-view if the warning ships"). Every changeset sentence checks against ① items 1, 2, 5 and 6: in walks the prototype chain; 'toString' came back as Object.prototype.toString with the key dropped; the read direction's own-key test is now used; an inherited key is treated like 'cozy'; the three densities fold exactly as before; such a value reaches the fold only on a path that does not validate; since objectui#7928 the member is the protocol's ObjectListViewSchema by reference; the type itself is unchanged.

③ Boundary flags

  • Fence verified. The ObjectView.tsx hunks sit at the dev-mode warning above the VIEW-TYPE BRANCHES #region (head lines 1771 to 2114); the HOST-COMPOSITION #region (head lines 2403 to 2563) is untouched. PR fix(plugin-view): the host delegation reads a named view's remaining protocol members off the named view (objectui#10758) #10884 (objectui#10758) changes that file only inside HOST-COMPOSITION (its hunks start at 2426), and git merge-tree --write-tree of the two heads exits 0 with no conflict.
  • Open PRs. Of the 12 other open PRs, only fix(plugin-view): the host delegation reads a named view's remaining protocol members off the named view (objectui#10758) #10884 touches a file this PR touches, and the hunks are disjoint (above). git merge-tree --write-tree origin/main at 1c5ee334a exits 0; the two commits main gained since the merge-base touch none of the five files.
  • Out-of-scope note 1, carrier owed. The warning's word-for-word remedy is a silent failure for groupBy (kanban) and dateField (calendar) — ① item 3. It is development-only and does not ship, so it is hygiene, not a defect; the executable-remedy pattern of .changeset/10002-map-style-remedy-text.md (parse the emitted warning, write the block it prescribes, measure through the real renderer) is the shape a carrier would take. The dev filed none; the seat should.
  • Out-of-scope note 2, correctly left. The kanban-branch comment "nested options.kanban.conditionalFormatting wins" and the header of ObjectView.kanbanConditionalFormatting.test.tsx still name the retired nested home; both are prose only, and the file is held by objectui#10758's claim, so not touching them was right.
  • PR body. Every checkable statement held: the only in in the file's code; the edit above the VIEW-TYPE BRANCHES fence and nothing in HOST-COMPOSITION; the interface byte-identical; the two pins named; files limited to dist, README, CHANGELOG and LICENSE; the measurement table matches my base-versus-head probe cell for cell. The per-key remedy flaw is disclosed in the Acceptance notes rather than hidden.
  • Pending changesets. None quotes the old warning text, the old docblock sentence or the in membership test, so none is made false. .changeset/7924-named-list-view-retire.md still calls NamedListView "one entry of ObjectViewSchema.listViews"; that was superseded by the objectui#7928 entry and was flagged in record 5860951313 — unchanged by this PR.
  • The docblock's "follows objectui#7924". PR feat(types,plugin-view,app-shell)!: retire NamedListView.densityMode; both relays read the density through the fold (objectui#7924) #10793 ("Fixes [finding] NamedListView declares 64 members; the object-view renderer reads 21 names off a named view — all 21 declared, 43 declared-and-unread (figures re-derived with the TypeScript parser) #7924") has merged, so the card the sentence points at is closed; the wording mirrors the listViews member's docblock and the card's own Fix section, and where the type's disposition is decided next is the seat's ledger, not this PR's. Non-blocking.
  • CI on the head. Polled through REST until settled: 43 check runs on 96c9f9953, 40 success (all eight test shards, Type Check, Lint, Spec Main Shape Gate, the changeset and citation gates among them), 3 skipped (dependabot, the two coverage rows), 0 red. The eight shards carry the new pin on head; its red-on-base reading is item 2 above, re-derived from the base module rather than re-run.

Implemented-by: claude/issue-10868-list-view-leftovers
Reviewed-by: session_01DuWo5bdP9SdVebamn99GGk

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 02:51
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit b2683a2 Sep 28, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10868-list-view-leftovers branch September 28, 2026 03:06
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…bject views, detail-view, email, password) and repair the shorthand Omit erasure (objectui#9256) (objectstack-ai#10897)

Part of objectstack-ai#9256
Clause-②: yes

**Clause-② `yes`, as the claim declared:** twelve published arms stop
accepting an authored `children`, and two published TypeScript faces
stop erasing the members they inherit. Both narrow an accept set; a
contract review is owed before landing.

The E3 residual that the maintainer's ruling on objectui#8284 (comment
`5861449497`, Q2 A) moved onto this card: keys with a published arm
whose renderer reads **neither** content channel. It also carries the
ruling's execution parameter 3, the `InputShorthandSchema` /
`UiCalendarSchema` TypeScript `Omit` erasure. objectui#9256 stays open
for the rest of family D.

## What changed

- **Narrowed (12 keys):** `object-grid`, `object-form`, `object-kanban`,
`object-map`, `object-tree`, `object-view`, `object-gantt`,
`object-calendar`, `object-chart`, `detail-view`, `email`, `password`.
- TypeScript face: `body?: never` and `children?: never`, each with a
docblock naming what the node renders instead.
- zod mirror: `body` and `children` are two `retirementTombstone`
members fed ONE string from the new `neitherContentChannelGuidance`
builder in `zod/tombstone.zod.ts`. Both stay MEMBERS, so
`zod-mirror-parity`'s key sets stay equal.
- `body` was already refused on these arms (by `BaseSchema`,
objectui#6771), except on the `email` / `password` TypeScript face,
which the `Omit` erased. It is restated because `BaseSchema`'s refusal
names `children` as the remedy, and `children` is dead here too.
- **`Omit` erasure repaired:** `InputShorthandSchema` and
`UiCalendarSchema` now inherit through `OmitDeclared` (`form.ts`, not
exported). It is a key-remapping mapped type: it drops exactly the named
keys and keeps every other named member and the index signature.
- `ui:calendar` gains no refusal of its own. Its face now re-exposes
what `CalendarSchema` declares, so the published TypeScript face refuses
`body`, `children` and a `mode` outside the declared enum, which the
erased face accepted. Its mirror (a `.extend()` of that one) already
refused them.
- I measured the `ui:calendar` renderer anyway
(`renderers/form/calendar.tsx`, typed `CalendarSchema`). It reads
`defaultValue`, `mode` and `value`, and no channel.
- **Held (3 keys): `br`, `hr`, `img`.** See "Held, and the question it
leaves" below.

## Per-key measurement, taken on `origin/main` `0c50f1863` before any
edit

**Instrument.** The TypeScript compiler API, ⛔ not grep. It builds one
program per workspace package from that package's own `tsconfig.json`:
40 programs over 1573 source files, on a BUILT tree (turbo build 42/42
first), with `@object-ui/types` resolving to `packages/types/dist`. It
files every `.body` / `.children` read (property access, string element
access, object destructuring) under the declared type of its receiver.

- 547 channel reads were filed.
- **Lit controls fire:** `ButtonSchema` 1, `DivSchema` 1, `CardSchema`
1, `ContainerSchema` 1 (each `children`).
- **Receiver-reachability control:** the same program sees every
narrowed declaration as a receiver of the keys its renderer reads. The
zeros below are readings, not blindness.

| key | registration(s) claiming it | registration hop → reader, and the
type it reads the node as | `body` / `children` reads under that type |
receiver reads (reachability) | arm, TS face: before → after | arm, zod
face: before → after | disposition |
|---|---|---|---|---|---|---|---|
| `object-grid` | `plugin-grid:object-grid` (+ console `registerLazy` of
the same plugin) | `ObjectGridRenderer` (`any`) → `ObjectGrid` ·
`ObjectGridSchema` | 0 / 0 | 158 reads, 38 keys | body refused
(BaseSchema), children accepted → both refused | same → both refused by
name | narrowed |
| `object-form` | `plugin-form:object-form` | `ObjectFormRenderer`
(`any`) → `ObjectForm` · `ObjectFormSchema` | 0 / 0 | 200 / 44 | same |
same | narrowed |
| `object-kanban` | `plugin-kanban:object-kanban` (+ lazy) |
`ObjectKanbanRenderer` (`any`) → `ObjectKanban` · `ObjectKanbanSchema` |
0 / 0 | 67 / 13 | same | same | narrowed |
| `object-map` | `plugin-map:object-map` (+ lazy) | `ObjectMapRenderer`
(`any`) → `ObjectMap` · `ObjectMapSchema` | 0 / 0 | 30 / 16 | same |
same | narrowed |
| `object-tree` | `plugin-tree:object-tree` (+ lazy) |
`ObjectTreeRenderer` (`any`) → `ObjectTree` · `ObjectTreeSchema` | 0 / 0
| 13 / 8 | same | same | narrowed |
| `object-view` | `plugin-view:object-view` (and the `view` alias) |
`ObjectViewRenderer` (`any`) → `ObjectView` · `ObjectViewSchema` | 0 / 0
| 112 / 19 | same | same | narrowed |
| `object-gantt` | `plugin-gantt:object-gantt` (+ lazy) |
`ObjectGanttRenderer` (`any`) → `ObjectGantt` · `ObjectGanttSchema` | 0
/ 0 | 61 / 48 | same | same | narrowed |
| `object-calendar` | `plugin-calendar:object-calendar` (+ lazy) |
`ObjectCalendarRenderer` (`any`) → `ObjectCalendar` ·
`ObjectCalendarSchema` | 0 / 0 | 40 / 12 | same | same | narrowed |
| `object-chart` | `plugin-charts:object-chart` (+ lazy) |
`ObjectChartBlock` (`any`) → `ObjectChart` · `ObjectChartSchema` | 0 / 0
| 123 / 15 | same | same | narrowed |
| `detail-view` | `plugin-detail:detail-view` | `DetailViewRenderer`
(`any`) → `DetailView` · `DetailViewSchema` | 0 / 0 | 195 / 31 | same |
same | narrowed |
| `email` | `ui:email` (`field:email` stands down) | `any` wrapper
spreads the node into `InputRenderer` · `InputSchema` | 0 / 0 | 23 / 17
| BOTH accepted (erased `Omit`) → both refused | body refused, children
accepted → both refused by name | narrowed |
| `password` | `ui:password` (`field:password` stands down) | same as
`email` | 0 / 0 | 23 / 17 | same as `email` | same as `email` | narrowed
|
| `br` | `ui:br` | html-elements factory (`any`), void tag: passes
`undefined` as the child | reads neither (the one `schema?.children`
read is behind the void-tag guard) | n/a (`any`) | body refused,
children accepted (family arm) → unchanged | same → unchanged | **held**
|
| `hr` | `ui:hr` | same | same | n/a | same | same | **held** |
| `img` | `ui:img` | same | same | n/a | same | same | **held** |

The residual count stays 15: no key reads `children` today. 12 are
narrowed and 3 are held.

**`any`-typed receivers, attributed rather than assumed.**

- Every hop above is `any`-typed. Each hop body was read: none contains
a channel read.
- The any-typed reads in the eleven hosting files and packages were
attributed one by one:
- React `children` destructured off internal components' own props
(`SchemaRenderer` passes no node child as a React child);
  - `document.body` and `err.body`;
  - `TreeNode.children` on the record hierarchy `ObjectTree` builds;
  - `FeedItem.body`;
  - `record:alert`'s own `props.body`, which is another registration.
- The html factory's `schema?.children` read sits behind `isVoid ?
undefined : …`.

**Delegation hosts, checked for pass-through.** Every `SchemaRenderer`
call in the ten object/detail packages was traced. The node it renders
is built one of three ways:

- key by key — `ObjectForm` / `ModalForm` / `DrawerForm` / `TabbedForm`
/ `SplitForm` / `WizardForm` all build their `form` node that way, and
the `form` renderer DOES draw `children`, which is why this was checked;
- from a sub-key — `DetailView`'s `header` / `footer` / tab `content`,
`ObjectChart`'s drill-down table;
- or from a generated `object-*` node — `ObjectView` assembles
`object-kanban`, `object-calendar`, `object-gallery`, `object-timeline`,
`object-gantt`, `object-map`, `object-tree` and `object-chart` from
named keys, and every one of those reads neither channel (for gallery
and timeline: `packages/plugin-list` and `packages/plugin-timeline` hold
no node-channel read).

No host spreads its own node into a node that reads `children`.

**Generic traversers** (`validateChildren` in core, `sdui-parser`
`validateTree`, the metadata-admin preview canvases, the vscode
validator) walk children to validate or preview. They render nothing and
are node-agnostic, as for every row family D already holds.

**objectui#9910 cross-check.** None of the 12 narrowed registrations
declares a `children` slot input: a grep over their registration files
finds 0 hits, and the lit control is `html-elements.tsx` / `div.tsx`,
which do.

**Producers.** `pnpm census:body-dialect --keys` over the 15, plus
`ui:calendar` and `input`, with `div` / `card` / `page` / `button` / `p`
/ `h1` as lit controls in the same pass: 8981 files, 2744 candidate
nodes.

- **0** nodes author `children` on any of the 15.
- 1 node authors `body` on `ui:calendar`: it was the family-D TRIPWIRE
test line (test bucket), moved by this change.
- The controls read `div` 170 / `card` 183 / `page` 47 `children`.
- Producers derived: none. Nothing was migrated.

## The `Omit` repair — measured on the BUILT d.ts

A probe program under `packages/components` resolved `@object-ui/types`
to `packages/types/dist/index.d.ts` (`--traceResolution`). It carried 15
`@ts-expect-error` pins (the 10 object/detail `children`, `email`
`children`, `password` `body`, `email` `label: 42`, and the
`ui:calendar` `children` and `mode: 'default'`), a lit control (`const
n: number = 'x'`), and 12 channel-free controls.

- **On this branch:** exit 0 — every directive used, every control
compiles, the lit control fires.
- **On the base dist:** exit 2 with 15 × TS2578, and the lit control is
still used.
- The emitted `dist/form.d.ts` declares `OmitDeclared` locally and both
heritage clauses use it.

## Red on base, then green — one-shot, the restore proven by state

The predictions were written to a file BEFORE the mutation. The mutation
was `git checkout 0c50f18 -- THE-SEVEN-DECLARATION-FILES`, with every
test file at HEAD. It was proven by 7/7 blob hashes equal to BASE plus
anchor counts (`neitherContentChannel` 0, `OmitDeclared` 0). The run sat
under `trap … EXIT INT TERM` with absolute paths.

| reader | mutated (BASE arms) | restored (HEAD `14fd13c51`) |
|---|---|---|
| vitest, the new pin file | **RED** exit 1 — 73 failed / 90 passed;
exactly the predicted 12 × 5 `children` families + 12 `body`-message
rows + 1 nested row | **GREEN** 163 / 163 |
| `tsc -p packages/types/tsconfig.test.json` | **RED** exit 2 — 18 ×
TS2578 and 3 × TS2322 (the `Eq` pins) in the new file, plus 3 in
`zod-mirror-parity` naming the two re-collapsed pairs and the stale
`KnownDrift` row; 0 elsewhere | **GREEN** exit 0 |
| built-d.ts probe (types rebuilt; dist markers 0 → 29 / 6) | **RED**
exit 2, 15 × TS2578 | **GREEN** exit 0 |

The restore was proven by 7/7 blob hashes equal to HEAD, `git diff HEAD`
empty and `git status` clean, both after the restore and at the end.

⚠️ **The `body` rows on the ten object/detail arms are GREEN on base, by
design:** `BaseSchema` already refused `body` there. Only their MESSAGE
changes, and the message rows are the ones that turn red. `?: never` is
erased before vitest runs, so the TypeScript half is read by `tsc`
alone. The vitest run cannot see it.

## Serial constraints

- **Mapped at branch time (`0c50f1863`).** PRs objectui#10714, objectstack-ai#10883
and objectstack-ai#10884 were open then; all three have since merged, and PR
objectui#10875 was the base.
- `layout.ts` (TabItem) and `layout.zod.ts` (TabItemSchema): untouched,
because `br` / `hr` / `img` are held and neither file is edited.
- `content-channel-family-d-9256.test.ts`: the only edits are forced
ones, clear of PR objectui#10714's hunks, which sit in the ListItem and
Tabs sections:
- The file's own `ui:calendar` TRIPWIRE said "when the collapse is
repaired this line goes RED … turn it back into a `@ts-expect-error`
pair". The repair fired it.
- The pair moved to the new file. That file lost the tripwire line, its
`uiCalendarBody` array entry (32 → 31) and the now-unused
`UiCalendarSchema` type import.
    - No pin was added there.
- `objectql.ts`: the insertion after `ObjectViewSchema`'s last member is
clear of PR objectui#10883's `NamedListView` docblock edit.
- `object-view-unmirrored-keys-7779.test.ts` (PR objectui#10884):
untouched.
- **Open PRs the branch-time map missed** (re-mapped by contract review
`5863631182`):
- PR objectui#10890 rewrote the same two `KnownDrift` header figures in
`zod-mirror-parity.test.ts` and landed first (`c30c8dd4c`). This branch
merged `main` at that commit (merge commit `31bb63bcb`, no rebase, no
force-push) and recounted the figures from the merged table; see "Merge
round" under Gates.
- PRs objectui#10888, objectstack-ai#10889 and objectstack-ai#10892 are textually disjoint, and all
three have since merged. `git merge-tree` of the merge head against
`main` at `6a7f24e92`, which carries all of them, reports no conflict.
- ⛔ `zod/index.zod.ts` and the `packages/cli` ratchet (objectui#10872)
are untouched.

## Collateral that the repair and narrowing made true or false — each
measured, none widened

- **`zod-mirror-parity.test.ts`:**
- The two `Omit` COLLAPSE rows (32 + 26 keys) leave
`MirroredUndeclared`. Its figures move from 4 / 60 to 2 / 2, and the
class note records the emptying.
- Restoring the named members NARROWED those faces: a collapsed face
accepted every key at `any`. So this is not the "declare the keys"
remedy that ledger refuses.
- The repaired shorthand face now carries `InputSchema`'s `onChange`
runtime slot. It enters `KnownDrift` as inherited drift: one entry, one
key. After the merge with PR objectui#10890 (which took
`ai.zod.ts#AIFormAssistSchema` out, one entry and two keys), the table
holds 49 entries / 88 keys, and both header sites say so.
- The union-arm non-vacuity leg had no multi-arm entry left in this
ledger. It now takes the same reader's arm-walking on `KnownDrift` and
re-arms here when a multi-arm entry lands.
- **`object-view-spec-parity.test.ts`:** `body` / `children` are
redeclared envelope keys on `object-view` now. They are triaged into
`SANCTIONED_LOCAL` as refusal-only surface, like `viewTabBar`, and the
declared-surface figure moves 24 → 26.
- **`CalendarSchema`'s texts on both faces said the TypeScript `Omit`
"collapses … and carries no member".** That became false; they are
corrected.
- **Two comments** that spelled the old plain-`Omit` heritage
(`index.ts`, `form-barrel-mirror-9406.test.ts`) are corrected.
- **`scripts/check-handler-key-read-sites.mjs`** caught my first
spelling. It was a computed spread (`...helper(…)`) that the gate cannot
resolve, so it stopped judging all eleven arms and its `detail-view`
`onTabChange` waiver read stale. The fix, the second commit, declares
both members explicitly. Now 49 / 49 reads are judged and 0 are left
unjudged.

## Held, and the question it leaves

`br` / `hr` / `img` read neither channel, but their only published arm
is `HtmlElementSchema`: ONE object over 38 tags, whose renderer draws
`children` for the other 35 (`p`, `h1`, …). Refusing the key on three
tags means splitting that family arm, or refining it per tag. Both
change a published contract's shape.

- The option the ruling adopted (option A in measurement `5859897857`)
reads: "leaving the three void tags to the tier because their arm is the
shared HtmlElementSchema family whose per-tag split was ruled out
(objectui#9067)".
- The parser tier already names them: the registration declares no
`children` slot on a void tag, so `validateTree` reports
`not-a-container`.
- A HELD control in the new pin file records the shared-arm fact, so the
hold is visible.

That the brief listed them among the keys to narrow is recorded as a
conflict in the report, ⛔ not resolved here by guessing.

## Gates (at HEAD `14fd13c51`, exit codes captured by
redirect-then-capture; heavy runs through the shared verify lock)

| gate | result |
|---|---|
| `@object-ui/types` build (tsc + vite + dist completeness) | exit 0 —
132 emitted files verified |
| `@object-ui/types` `type-check` (`tsc --noEmit` + examples +
`tsconfig.test.json`) | exit 0 |
| `pnpm exec vitest run packages/types/` (parity, node-slot, arm,
mirror, family-D and the new pins) | 259 files / 5670 tests, exit 0 |
| `pnpm exec vitest run scripts/` | 177 passed + 2 skipped of 179 files
/ 5317 tests, exit 0 |
| consumer type-check — every package naming a narrowed type (28
packages: the ten object plugins, app-shell, components, core, fields,
cli, react, runner, console, schema-catalog, vscode-extension …) | exit
0, 28 × `type-check: Done` |
| `pnpm --filter @object-ui/types lint` | 0 errors; 0 findings on any
added line (eslint JSON over the 13 touched files) |
| `check:control-bytes` · `check:new-line-citations` (0 new) ·
`changeset:check` · `check-changeset-presence` ·
`check:changeset-claims` · `check:pending-changeset-literals` | exit 0
each |
| `check:doc-types` · `check:doc-snippets` · `check:doc-examples` ·
`check:skill-examples` · `check:doc-fences` · `check:doc-example-ids`
(against the built types) | exit 0 each |
| `check:handler-key-reads` · `check:component-surface-parity` ·
`check:prompt-keys` · `check:registry-bare-names` · `check:spec-symbols`
· `check:readme-exports` · `pnpm check` (CLI self-check) | exit 0 each |
| governed guard `--test` over the 14 paths | NOT GOVERNED |

NOT MEASURED:

- `check:sdui-registration-pins` exits 2 without a console build. The
diff touches no registration and no `sideEffects` array. Left to CI.
- The 4-way `pnpm test` shards beyond `packages/types` and `scripts/`,
plus `test:dist` and E2E: left to CI.

### Merge round — head `31bb63bcb` (merges `main` at `c30c8dd4c`)

The one conflict was the two `KnownDrift` header figures in
`zod-mirror-parity.test.ts`. It was resolved by RECOUNTING the merged
table with the test's own AST reader, not by picking a side:

| tree | `KnownDrift` |
|---|---|
| base `0c50f1863` | 49 entries / 89 keys |
| PR objectui#10890 removes `ai.zod.ts#AIFormAssistSchema` | −1 entry /
−2 keys (48 / 87 on `c30c8dd4c`) |
| this branch adds `form.zod.ts#InputShorthandSchema` | +1 entry / +1
key (50 / 90 on `14fd13c51`) |
| merged `31bb63bcb` | **49 entries / 88 keys** |

The population bullet and the KNOWN_DRIFT restatement both read 49 / 88,
and the bullet keeps both histories in landing order. The other ledgers
read the same as before: `MirroredUndeclared` 2 / 2,
`UnmirroredDeclared` 12 / 84, `RuntimeOnlyDeclared` 3 / 7,
`WiderThanDeclared` 6 / 7. The merge head differs from `main` by exactly
this PR's 14 files.

| gate at `31bb63bcb` | result |
|---|---|
| `zod-mirror-parity` + `content-channel-e3-residual-9256` | 2 files /
200 tests, exit 0 |
| `@object-ui/types` build · `type-check` (incl. `tsconfig.test.json`) |
exit 0 · exit 0 |
| `pnpm exec vitest run packages/types/` (incl. PR objectui#10890's
`ai-zod-arms-10859` / `ai-node-faces-agree-10874` and the family-D file
PR objectui#10714 edited) | 261 files / 5696 tests, exit 0 |
| `pnpm exec vitest run scripts/` | 177 passed + 2 skipped of 179 files
/ 5322 tests, exit 0 |
| `check:control-bytes` · `check:new-line-citations` · `changeset:check`
· `check-changeset-presence` · `check:changeset-claims` ·
`check:pending-changeset-literals` · `check:handler-key-reads` | exit 0
each |

## Acceptance notes — out of scope, not fixed here

- **`input` reads neither channel as well.** `InputSchema`: 0 channel
reads, 17 keys read; the registration declares no `children` slot.
- It is this card's remaining family-D name, and it was not in this
claim.
- Its old hold-out ground ("a TS `Omit` carries nothing") is gone with
this repair: a tombstone on `InputSchema` now propagates on both faces.
  - Carrier: the next objectui#9256 slice.
- **`PartialSchema`'s docblock in `index.ts`** says every generic
re-spelling of its `Omit` collapses for the same `keyof T` reason.
`OmitDeclared` is a generic spelling that does not: this PR measures it
on two faces.
- Carrier: the next objectui#9256 slice, which touches the same `Omit`
subject. objectui#6397, that alias's card, is closed.
  - Noted, not filed.
- **The family-D test header** still records `InputSchema` as "family E,
frozen" and `DetailViewSchema` as unattributable. Both readings are
superseded by this change.
  - Carrier: the next objectui#9256 slice.
- Noted, not edited: PR objectui#10714 held that file while this PR was
written. It has since merged, so the next slice edits the header freely.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment