Skip to content

fix(types): a bind-only list is accepted; ListSchema.items is optional with an at-least-one-of bind / items refinement (objectui#11405) - #11407

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11405-list-bind-only
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11405-list-bind-only

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11405

Clause-②: yes — the list arm of @object-ui/types only widens. A bind-only list goes from refused to accepted; a list with neither key was refused before (at items) and is refused now (at the node). Nothing went from accepted to refused.

Dispatched run for the domain:spec seat (objectui#10217), session https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT, claim comment 5942476028, triage ruling 5941529511. Draft, left for the seat; the at-tier contract-review record is owed on the landing head before enqueue.

What changes

  • zod face (packages/types/src/zod/data-display.zod.ts): ListSchema.items is .optional(), and the arm ends in .superRefine(listHasAnEntrySource, …) with a when that always answers true, so the rule is reported beside any other issue on the node. A node with neither bind nor items is refused once, at the node: code: custom, params.code: LIST_ENTRIES_REQUIRED, a message naming both keys. The rule is at least one, not exactly one, because items is the renderer's fallback beside bind. Presence is !== undefined, as in requireRecordSource. The strict face carries the check, because its clone keeps def.checks.
  • TS face (packages/types/src/data-display.ts): items?: ListItem[]. The docblock names the read site and the mirror refinement, in the shape ObjectKanbanSchema.objectName uses (objectui#7780). bind stays declared once, on BaseSchema.
  • Pin: packages/types/src/__tests__/list-bind-only-11405.test.ts.
  • Docs (AGENTS.md Add automated testing infrastructure and CI/CD workflows #2): content/docs/components/data-display/list.mdx documents bind / optional items and the at-least-one rule.
  • Changeset: .changeset/11405-list-bind-only.md, @object-ui/types minor.
  • Ledger row: scripts/markdown-test-inputs.mjs gains one ADJUDICATED row for the new pin. The pin reads the two guides, and the ledger audit (scripts/__tests__/markdown-test-inputs.test.ts) fails on an unregistered reader. This row is outside the claim's declared file surface; see Deviations.

⛔ Not edited: skills/** (the four fences become valid as they stand), list.tsx, and every other arm.

Zone 2 measurements

hypothesis reading
H1 the faces, and the refusal Confirmed on base 7728c67c8 (rebuilt dist). { type: list, bind: customerNames } is refused with invalid_type at items by the arm, safeValidateSchema and StrictAnyComponentSchema. A list with neither key was refused the same way, at items.
H2 the refinement in the union superRefine on the arm keeps it a valid DataDisplaySchema / AnyComponentSchema member: the module loads, DataDisplaySchema._zod.propValues.type still holds list, and a wrong literal is still invalid_union at type. The issue lands at the node: [] at the root, children.0 when nested. Precedent: requireRecordSource already sits on discriminated arms. TS union spelling, measured with a probe: it refuses a literal with neither key, but the TS2322 names only bind as missing. The reader's type for schema.items is identical to the optional member's (ListItem[] or undefined). An interface can no longer extend it (TS2312), and a type alias drops declaration merging. So it was not taken: no reader gains, and three precedent arms (object-kanban, object-map, object-tree) keep the optional member plus a mirror refinement.
H3 readers of ListSchema['items'] Type-checker census over one program from the repo-root tsconfig (5787 files): one read site, list.tsx's Array.isArray(schema.items) ? schema.items : [], guarded. @object-ui/components type-check is green against the rebuilt types. Nothing to retype.
H4 the registration list.tsx's items input carries no required. The console parity guard has no list row, and check:component-surface-parity compares key sets only. The registration is left alone.
H5 the fences Read off disk with scanSkillFences (the marker gate's own reader) and judged through safeValidateSchema and the strict face. Four marked fences author a bind-only list. The three taught as valid pass. The fence the guide marks as wrong is refused only at children, never at items and never on the new rule.
H6 parity Both faces move together, so no zod-mirror-parity row opens; that file is untouched. Ablation M3 below shows the TS face moving alone reddens the parity ratchet.

Verification (landing head 8b317ec61)

  • pnpm --filter @object-ui/types build: exit 0. pnpm --filter @object-ui/types type-check (three legs, including tsc -p tsconfig.test.json, whose --listFilesOnly includes the new pin): exit 0.
  • pnpm exec vitest run packages/types/: Test Files 321 passed (321), Tests 8560 passed (8560).
  • pnpm --filter '@object-ui/components^...' build, then pnpm --filter @object-ui/components type-check: exit 0.
  • Consumer and ledger suites, 15 files: the components list and bind tests (list-title-declared-11347, list-item-content-channel-9590, shadowed-renderer-behaviour, bind-base-prop-parser-tier-11008, skill-guide-data-table-binding), markdown-test-inputs, check-skill-examples, and the changeset-script suites that name the ledger. Result: Test Files 15 passed (15), Tests 494 passed (494).
  • Gates, each exit 0: check:control-bytes, check:new-line-citations, check:test-path-roots, check:vi-mock-specifiers, check:phantom-deps, check:self-import, check:unreferenced-sources, check:spec-symbols, check:changeset-claims, check:pending-changeset-literals, check-changeset-no-major, check-changeset-presence, check:doc-types, check:doc-fences, check:doc-example-ids, check-doc-links, check-doc-expression-carriage, check:doc-example-readers, check:component-surface-parity --type list (report-only), check:prompt-keys, check:pre-install-import-graph, check-lint-coverage, check-type-check-coverage, markdown-test-inputs --audit, and check-governed-queue-guard --test over the six changed paths (NOT GOVERNED).
  • Narrowed, declared: check:doc-snippets and check:doc-examples need the whole-workspace build (check:doc-examples exited 2, could-not-run). In their place, the list.mdx ts fence was compiled --strict against the rebuilt @object-ui/types dist (exit 0), with a failing control in the same harness (TS2322, exit 2). CI builds their closure.
  • Lint, narrowed and proven: the population is read from eslint's own config. Four changed files are linted: data-display.zod.ts, data-display.ts, the pin, and markdown-test-inputs.mjs. The .mdx and the changeset are reported as "no matching configuration". The count of 4 comes from --format json. Result: 0 errors. data-display.ts has 30 no-explicit-any warnings, the same 30 as its base blob. eslint.config.js enables no type-aware linting (no parserOptions.project / projectService), so this diff cannot move any untouched file's verdict.

Ablation (fix committed first, each leg through ablation-replace, restored from HEAD and proven blob-equal)

  • M1, zod items required again: 11 failed, 1 passed of 12. Only the counter-probe stays green, which is correct.
  • M2, refinement removed: 5 failed, 7 passed. The five are the "neither" rows and the nested and beside-another-fault rows; bind-only stays green. ⚠️ The first M2 attempt was a no-op: the replacement count did not rise, so the tool refused it before the command ran. It was re-run with a delete-mode anchor.
  • M3, TS items required again: tsc -p tsconfig.test.json exit 2. TS2741 at the pin's bind-only literal, plus the zod-mirror-parity ratchet (TS2322 on data-display.zod.ts#ListSchema and items).
  • Control, unmutated HEAD: 12 of 12 passed, tsc exit 0.

Acceptance notes

  • The TypeScript face cannot spell "at least one": a literal with neither key still compiles. The validator enforces the rule, as for object-kanban.
  • content/docs/guide/expressions.md ("Scoped Data", "Index in Loops") teaches a list with items as a string, an itemTemplate and per-item item / index expressions. The published skills/objectui expressions guide refutes that shape. safeValidateSchema refuses it at items, and the strict face adds unrecognized_keys for itemTemplate. check-doc-expression-carriage already lists both sites, and its header points to that family's open cards. Noted, not filed. Untouched here.
  • tree-view is the other useDataScope reader in @object-ui/components. Measured: it already accepts a bind-only node, so the same defect is not present there.

Deviations

  • scripts/markdown-test-inputs.mjs: one ledger row, outside the claim's file surface. It is required because the claimed pin reads the guides off disk, and markdown-test-inputs.test.ts fails on an unregistered reader. The script's own suite and its importers ran green (above).

Generated by Claude Code

claude added 3 commits October 1, 2026 23:24
…ional with an at-least-one-of `bind` / `items` rule

The `list` renderer reads `useDataScope(schema.bind)` first and falls back to
`schema.items`, so a bind-only list draws its entries from the host scope. Both
published faces required `items` and refused that shape with `invalid_type` at
`items`. `items` is now optional on the TypeScript and zod faces, and the zod arm
ends in `listHasAnEntrySource`, which refuses a node with neither key once, at
the node, keyed `LIST_ENTRIES_REQUIRED`. The strict face carries the check.

Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT
Co-authored-by: Claude <noreply@anthropic.com>
… `bind` stays declared once

`base-bind-declared.test.ts` scans for a second `bind?:` declaration outside
`BaseSchema` (objectui#6357); the refinement's destructuring annotation
matched it. The refinement now reads the raw node as a plain record.

Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT
Co-authored-by: Claude <noreply@anthropic.com>
…e two skill guides

`list-bind-only-11405.test.ts` reads its fences off
`skills/objectui/guides/data-integration.md` and `schema-expressions.md`, so the
markdown-test-inputs ledger records it: an edit to either guide is an edit to
that test's input, and CI's markdown fast path must not skip it.

Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation package: types tests labels Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 13 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/5926-empty-action-visible-when.md

  • names data-display.zod.ts → packages/types/src/zod/data-display.zod.ts — edited by this change

    emptyAction was the one authored-node exception in the tree. The empty-state CTA slot resolved the registry directly — ComponentRegistry.get(node.type) — and mounted the result itself, so the node never passed through SchemaRenderer and its visibleWhen was never evaluated. @objectstack/spec accepts the key (SchemaNodeSchema carries visibleWhen, and data-display.zod.ts types emptyAction as a SchemaNode), so an author wrote a gate, the platform took it, and nothing enforced it — declared-not-enforced, the same class c86185eb5 closed for record:alert, one level down.

.changeset/6349-types-internal-name-collisions-batch-1.md

  • names data-display.ts → packages/types/src/data-display.ts — edited by this change

    BreadcrumbItem / BreadcrumbSchema — re-pointed, because one copy was stale. Both were declared in data-display.ts and in navigation.ts. The data-display pair was not a second dialect but a strict SUBSET: no key declared differently on either side, and missing BreadcrumbItem.icon / onClick / siblings and BreadcrumbSchema.maxItems. Everything that reads a breadcrumb was already on the navigation declaration — registry.ts maps the 'breadcrumb' component type to it, src/index.ts re-exports it under the bare names, zod/navigation.zod.ts mirrors it (icon, onClick, siblings, maxItems included), the ui:breadcrumb renderer consumes it, and the component's own documentation page documents icon and maxItems. data-display.ts now re-exports the one authority.

.changeset/6881-retire-data-table-toolbar.md

  • names data-display.ts → packages/types/src/data-display.ts — edited by this change

    What was measured. The key was declared on both published faces — data-display.ts (toolbar?: SchemaNode[], "Table toolbar actions/content") and the Zod mirror (SchemaNode | SchemaNode[]) — documented, mirrored, and read by NOTHING: data-table.tsx, the registered renderer for type: 'data-table', contains the word only in two prose comments and never reads schema.toolbar. The sibling emptyAction slot on the same interface IS mounted through SchemaRenderer, so the census zero is a reading, not a blind query. An author who wrote a toolbar got a green document and a blank result, with no signal anywhere that said so — the declared-vs-enforced failure mode that is worst for AI-authored metadata, which has nothing but the declaration to go on.

.changeset/6940-rowactions-boolean-mirror.md

  • names zod/data-display.zod.ts → packages/types/src/zod/data-display.zod.ts — edited by this change

    The hand-written zod mirror in zod/data-display.zod.ts declared rowActions: z.array(z.any()).optional(). Every other face of the same key says boolean: the TS declaration it mirrors (rowActions?: boolean), the renderer's destructuring default (rowActions = false), its two truthiness gates and two colSpan arithmetic sites, the registered authoring input ({ type: 'boolean', label: 'Show Row Actions' }), defaultProps: { rowActions: true }, and the renderer's own docblock example, which authors "rowActions": true. The mirror was the single outlier — and the published one, so safeValidateSchema refused the exact spelling the component's documentation, defaults and authoring UI all teach. Two shipped examples/schema-catalog entries (user-table.json, full-featured-table.json) failed validation for this and no other reason; both now validate unchanged.

.changeset/6951-tree-view-data-retired.md

  • names data-display.ts → packages/types/src/data-display.ts — edited by this change

    Two published faces, one retirement — and why a tombstone, not a deletion. The TypeScript interface TreeViewSchema (@object-ui/types, data-display.ts) declares data?: never; the Zod mirror TreeViewSchema (@object-ui/types/zod, data-display.zod.ts) declares data as a retirementTombstone(). BaseSchema already declares data?: any (z.any().optional() on the mirror), so DELETING the member would not have refused the key — it would have ADMITTED it, unvalidated, through the base member, and the renderer would have drawn an empty tree. The tombstone on the extended schema shadows the base member on both faces; the pin measures the base accepting the very document the extended schema refuses.

  • names data-display.zod.ts → packages/types/src/zod/data-display.zod.ts — edited by this change

    Two published faces, one retirement — and why a tombstone, not a deletion. The TypeScript interface TreeViewSchema (@object-ui/types, data-display.ts) declares data?: never; the Zod mirror TreeViewSchema (@object-ui/types/zod, data-display.zod.ts) declares data as a retirementTombstone(). BaseSchema already declares data?: any (z.any().optional() on the mirror), so DELETING the member would not have refused the key — it would have ADMITTED it, unvalidated, through the base member, and the renderer would have drawn an empty tree. The tombstone on the extended schema shadows the base member on both faces; the pin measures the base accepting the very document the extended schema refuses.

.changeset/6972-markdown-inert-keys-retired.md

  • names data-display.ts → packages/types/src/data-display.ts — edited by this change

    What was measured, on this branch's base. sanitize was declared ?: boolean with @default true on both published faces — data-display.ts and the Zod mirror — documented, and read by NOTHING. Worse than an ordinary inert key, it implied a switch that does not exist: sanitization is unconditional. rehypePlugins in plugin-markdown/src/MarkdownImpl.tsx is a module-level const array whose last link is [rehypeSanitize, sanitizeSchema], handed to ReactMarkdown as-is — no ternary, no if, no runtime assembly. MarkdownRenderer forwards exactly content and className, and MarkdownImplProps accepts only those two. A repo-wide grep for schema.sanitize over packages/ and apps/ returns nothing, against a control of 20 .tsx files reading schema.content in the same query shape, so the zero is a reading, not a blind query. An author writing sanitize: false believed they turned XSS filtering off; one writing sanitize: true believed they turned it on. Neither was true.

.changeset/7352-drill-down-config-mirror.md

  • names zod/data-display.zod.ts → packages/types/src/zod/data-display.zod.ts — edited by this change

    DrillDownConfigSchema is the zod mirror of DrillDownConfig, and both declarations that carry drillDown reference it — ChartSchema (zod/data-display.zod.ts) and ObjectDataTableSchema (zod/objectql.zod.ts) — so the published validator under @object-ui/types/zod reads the key for the first time (objectui#7352).

.changeset/7694-chart-series-chart-type-alias-refusal.md

  • names zod/data-display.zod.ts → packages/types/src/zod/data-display.zod.ts — edited by this change

    • Before: series: [{ name: 'revenue', chartType: 'line' }] validated green through @object-ui/types/zod (safeValidateSchema, objectui check / objectui validate, any pipeline that keeps parse()'s output) — and the key was gone from the output, so a consumer of the parse result drew that series in the chart's own family, precisely what the author was overriding. On the TypeScript face the key was merely an excess property on a fresh literal; a widened object carrying it assigned structurally. - After: the same document REFUSES at series[i].chartType (issue code invalid_type) with one message on both channels — the parse-time issue and the .describe() metadata: Unrecognized key(s) on this chart series: \chartType`. Did you mean `chartType` → `type`? …followed by the reason and the remedy. Writetype: 'bar' | 'line' | 'area'. On the TypeScript face ChartDataSeries.chartTypeis a?: nevertombstone, so both the fresh literal and the widened assignment aretsc errors. - **Both written** ({ type: 'bar', chartType: 'line' }) is refused at chartTypealone — the key is not folded ontotypeand no precedence is minted between the two spellings. - **Which documents to scan.** The narrowing does not stop atChartDataSeriesSchema; it reaches every document through the parents that embed it — ChartSchema.series (zod/data-display.zod.ts, z.array(ChartDataSeriesSchema)) and, one level further out, ReportSectionSchema.chart (zod/reports.zod.ts, ChartSchema.optional()). Authors meet it through safeValidateSchema() (zod/index.zod.ts, which parses AnyComponentSchema) and through the CLI's objectui validate command (packages/cli/src/cli.ts). In practice: every chartnode'sseries[], and every report section whose chart` carries one.

.changeset/7722-wrapper-class-five-more.md

  • names data-display.ts → packages/types/src/data-display.ts — edited by this change

    Each of renderers/form/switch.tsx, textarea.tsx, date-picker.tsx, select.tsx and renderers/data-display/list.tsx reads schema.wrapperClass onto its wrapper element, and neither the TypeScript interface (form.ts, data-display.ts) nor the zod mirror (zod/form.zod.ts, zod/data-display.zod.ts) declared the key. The reads compiled through BaseSchema's index signature (objectui#5155) and the values parsed through .passthrough(), admitted unexamined. The same key, on the same class of read, is declared on CheckboxSchema (b74a8598d), FileUploadSchema and FilterBuilderSchema (objectui#6150); these five were left out only because their doc pages never listed it.

  • names zod/data-display.zod.ts → packages/types/src/zod/data-display.zod.ts — edited by this change

    Each of renderers/form/switch.tsx, textarea.tsx, date-picker.tsx, select.tsx and renderers/data-display/list.tsx reads schema.wrapperClass onto its wrapper element, and neither the TypeScript interface (form.ts, data-display.ts) nor the zod mirror (zod/form.zod.ts, zod/data-display.zod.ts) declared the key. The reads compiled through BaseSchema's index signature (objectui#5155) and the values parsed through .passthrough(), admitted unexamined. The same key, on the same class of read, is declared on CheckboxSchema (b74a8598d), FileUploadSchema and FilterBuilderSchema (objectui#6150); these five were left out only because their doc pages never listed it.

.changeset/8331-data-table-empty-action-primitive-node.md

  • names packages/types/src/data-display.ts → packages/types/src/data-display.ts — edited by this change

    Behaviour change on a published surface, deliberately. DataTableSchema.emptyAction is declared SchemaNode on both published faces — packages/types/src/data-display.ts and its Zod twin in packages/types/src/zod/data-display.zod.ts — and SchemaNode is BaseSchema | string | number | boolean | null | undefined. The empty-state render path additionally required typeof … === 'object', so an authored emptyAction: 'Create the first record' rendered nothing and reported nothing: declared wider than enforced, failing in the direction that loses the author's content without a diagnostic. A node that renders nothing today therefore starts rendering.

  • names packages/types/src/zod/data-display.zod.ts → packages/types/src/zod/data-display.zod.ts — edited by this change

    Behaviour change on a published surface, deliberately. DataTableSchema.emptyAction is declared SchemaNode on both published faces — packages/types/src/data-display.ts and its Zod twin in packages/types/src/zod/data-display.zod.ts — and SchemaNode is BaseSchema | string | number | boolean | null | undefined. The empty-state render path additionally required typeof … === 'object', so an authored emptyAction: 'Create the first record' rendered nothing and reported nothing: declared wider than enforced, failing in the direction that loses the author's content without a diagnostic. A node that renders nothing today therefore starts rendering.

.changeset/8478-describe-line-addresses.md

.changeset/9256-list-timeline-content-channels.md

  • names packages/types/src/data-display.ts → packages/types/src/data-display.ts — edited by this change

    These two were held out of the previous family-D slice for a SERIAL constraint on packages/types/src/data-display.ts and never for a verdict. Readership was re-derived for both rather than inherited: a TypeScript compiler-API sweep files every .body / .children read under the declared type of its receiver and answers zero for ListSchema and TimelineSchema while its live controls fire. timeline's bare-key owner is any-typed, so it was attributed directly as well — packages/plugin-timeline contains no channel read of any kind.

.changeset/table-renderer-declared-column-contract-5350.md

  • names packages/types/src/data-display.ts → packages/types/src/data-display.ts — edited by this change

    renderers/complex/table.tsx resolved a heading as col.header || col.label and a cell as row[col.accessorKey || col.name]. Neither label nor name is declared on TableColumn, which declares header and accessorKey — both required (packages/types/src/data-display.ts). This was the fourth site of the column-alias family, after data-table, ObjectDataTable and ObjectGrid (objectui#5350).

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.

Compared the checked-out tree with 7728c67c8 (merge-base with origin/main): 5 file(s) changed outside .changeset/, read against 1953 pending declaration(s) that publish a body (2574 pending in total). · run

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3552.9 KB 3574.6 KB
Main entry chunk (gzip) 150.4 KB 350 KB
Entry file index-pbixheNU.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.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
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.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
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.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
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) 570.62KB 136.73KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 230.46KB 63.94KB
fields (index.js) 260.89KB 66.19KB
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.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.49KB 9.23KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 40.40KB 11.41KB
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.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
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) 53.15KB 15.45KB
plugin-charts (index.js) 84.07KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 139.77KB 37.47KB
plugin-designer (index.js) 216.57KB 44.72KB
plugin-detail (index.js) 245.70KB 64.54KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 173.14KB 44.58KB
plugin-gantt (index.js) 178.11KB 44.87KB
plugin-grid (index.js) 231.72KB 63.65KB
plugin-kanban (index.js) 49.49KB 15.54KB
plugin-list (index.js) 116.63KB 28.97KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 34.29KB 10.13KB
plugin-tree (index.js) 14.58KB 5.17KB
plugin-view (index.js) 90.32KB 22.76KB
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) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
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) 21.42KB 7.05KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 5.07KB 2.39KB
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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 21.59KB 7.71KB
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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8b317ec61edbdd00f7b90d0b66c8c801bf7cf121
Local-runs: none

Isolated at-tier reviewer, written 2026-10-01T23:57Z. Inputs: card #11405 (body and comments 5941529511, 5942476028, 5942839913, 5942857572), PR #11407 (body, 6-file list, net diff against main 7728c67c8), and the check-runs on the head. Files at the head were read with git show; nothing was checked out, built or run. Scope is the contract faces (objectstack contract-review.md); the changeset and docs prose are the seat's and are named below only where they touch a face.

① Derived judgments

Judged against triage's ruling 5941529511 (at least one of bind / items, ⛔ not exactly one) and against list.tsx's read order (useDataScope(schema.bind) first, Array.isArray(schema.items) second, else []):

  1. zod items: z.array(ListItemSchema).optional() — a bind-only list moves from refused (invalid_type at items) to accepted through the arm, safeValidateSchema and the strict face. Right: the renderer draws it and the ruling orders it.
  2. .superRefine(listHasAnEntrySource, { when: () => true }) — a node with neither key is refused once, at the node (path: [], code: custom, params.code: LIST_ENTRIES_REQUIRED, message naming both keys), beside any other issue on the node. Right: it is at-least-one (a node carrying both stays accepted, which is the renderer's fallback shape), the refusal is at the node with params.code as the ruling's pin asks, and the when spelling is the one requireRecordSource and elementNumberObjectIsSupplied already carry.
  3. Presence is !== undefined, so bind: '' and items: [] count as present. Right: the ruling asked for presence, not non-emptiness; requireRecordSource uses the same test; the renderer draws an empty list for either rather than failing.
  4. Union and strict membership. The arm stays a z.discriminatedUnion option of DataDisplaySchema and so of AnyComponentSchema: zod 4's .superRefine appends to def.checks on the same ZodObject, which is how ObjectTreeSchema, ObjectCalendarSchema, ObjectKanbanSchema and ListViewSchema already sit superRefine'd inside ObjectQLComponentSchema. The strict face derives objects through cloneWithDef(schema, { shape, catchall: z.never() }), which spreads the def and therefore carries def.checks, so the refinement reaches StrictAnyComponentSchema. Right.
  5. TS items?: ListItem[] — the bind-only literal compiles; a neither-key literal also compiles (the TS face cannot spell at-least-one, as for object-kanban; the validator carries it); a reader of ListSchema['items'] now sees ListItem[] | undefined. Right: both faces move together so no zod-mirror-parity row opens, bind stays declared once on BaseSchema (the refinement reads the raw record), and the reader-side narrowing is a published change the changeset states. The one in-repo reader, list.tsx, is Array.isArray-guarded.
  6. Nothing moves from accepted to refused. The list registration's inputs carry no required, so the registry and component-surface-parity surfaces do not move; both list entries in packages/types/examples/data-display-examples.json carry items and stay valid. Right.
  7. Governed rule text. skills/** is untouched and the four os:check fences become valid as written. I searched the head's skills/**, the two packages/types READMEs and content/docs/** for text stating that items is required on list and found none, so no governed face is left misstating the contract. Right.
  8. The pin list-bind-only-11405.test.ts. Each of the three faces is driven over bind-only, items-only, both and neither, plus beside-another-fault (ordered: 'yes'), a mistyped bind (rule does not fire twice) and a nested node (children.0). The guide fences are read off disk with scanSkillFences, the marker gate's own reader (its main is behind isEntrypoint, so the import has no side effect), filtered to a root list with bind set and items unset, which reaches exactly the four fences the card counts: one in data-integration.md, three in schema-expressions.md, the ❌ one among them judged only as "refused, and not at items, not on the new rule". REPO_ROOT is rooted at the test file, not cwd. Right: it pins all four of the ruling's stated pins.

② Semver level

  • Changeset .changeset/11405-list-bind-only.md: @object-ui/types: minor. Matches what the diff publishes: a widened validator accept set and an optional member on a published type. This repo reserves major for following @objectstack across a major (AGENTS.md 版本号策略, enforced by check-changeset-no-major), so a published-face change, including the reader-side ListItem[] | undefined narrowing, ships as minor with the semantics in the body. The body states that narrowing; it does not carry the "breaking for TypeScript readers" label its siblings (e.g. 10946-expression-wire-slots-by-reference.md) use, which is prose, the seat's, and misstates nothing.
  • Clause-②: yes on the PR body: right and required. A required member of a published type becomes optional and the validator's accept set moves; the one-direction claim (nothing from accepted to refused) holds on the diff.

③ Boundary flags

open_questions: []. Every dev flag, deviation and out-of-scope finding, answered:

  • Ledger row in scripts/markdown-test-inputs.mjs outside the claimed surface — not a contract face; forced once a pin reads governed guides off disk. Answered, nothing owed.
  • Ablation section names no sha — db0a0537a and 8b317ec61 touch none of the three anchors; not a contract face. Answered.
  • Clause-② line spelled with an em-dash and no parenthetical — the value is yes, read as such. Answered.
  • ⚠️ M2's first attempt was a no-op and was re-run — the five rows that redden on M2 are the neither / nested / beside-a-fault rows, consistent with the rule living on the refinement. Answered.
  • check:doc-snippets / check:doc-examples narrowed locally — answered by the head's check-runs Doc Snippet Type Check and Doc Component Type Check, both success.
  • Out-of-scope class (c): content/docs/guide/expressions.md teaches items: '${users}' plus itemTemplate on list — docs prose, already censused by check-doc-expression-carriage, routed to the objectui#7440 / finding(examples,docs): the color key three catalog text nodes author — and the docs page teaches — is declared by nothing and read by nothing #7444 / finding(docs): docs/ARCHITECTURE.md teaches the ObjectRenderer phantom in 3 blocks — third file, and docs/** is in no gate's scan surface #7838 family. Not a face of this PR; routing stands, nothing escalated.
  • Out-of-scope: tree-view already accepts a bind-only node — noted, nothing owed.
  • Noted, not owed: list.mdx's illustrative items?: SchemaNode[] (the real member type is ListItem[]) predates this PR; prose face, the seat's.

Check-runs on the head (their conclusions are the gate verdicts; read last, polled until none was pending): 43 runs, 40 success, 3 skipped by design (dependabot and the two coverage matrix placeholders), 0 failure, 0 in progress. Named for the faces above: Type Check, Test and all eight Test (shard n/8), Test (dist pins), Spec Main Shape Gate, Changeset Bump Policy, Changeset Declaration, Changeset Fixed Group Check, Changeset Claim Re-read, Skill Example Check, Doc Snippet Type Check, Doc Component Type Check, Lint, Line Citation Gate, Governed Surface Queue Guard — each success.

Implemented-by: claude/issue-11405-list-bind-only
Reviewed-by: session_01VhxTqosz7wn54ahqyxgERT

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 1, 2026 23:58
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 1, 2026 23:58
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit 9547063 Oct 2, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11405-list-bind-only branch October 2, 2026 00:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation package: types tests

Projects

None yet

2 participants