Skip to content

fix(plugin-grid,plugin-detail,plugin-dashboard,plugin-list,fields): withhold an untyped column until the object field types are known (objectui#10657) - #10819

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-10657-masked-first-paint
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-10657-masked-first-paint

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #10657

Clause-②: yes

PR 2 of 2 for objectui#10657, the masked-column closure card. PR 1 (objectui#10725, landed as 9c78ebe) made the three producers set TableColumn.masked. This PR carries the two rows the release comment 5852410452 left: the first paint before the object schema settles (the folded objectui#10706 row), and the same draw window at RelatedList and ObjectDataTable.

Branch claude/issue-10657-masked-first-paint, base objectui origin/main c907a9c87, head 9f9b8b7. Implemented in session https://claude.ai/code/session_01MCg3z5cGrV51xEddpGcxEY for the PM claim 5856905742.

What changed

Withheld columns (the triage's "for every other case"). While an object-bound producer's field types are unknown (its definition read is in flight, or it failed), a column it cannot type is drawn as the mask (MaskedCellRenderer) and carries masked. It stays so when the read fails: fail closed, never back to text.

  • ObjectGrid (@object-ui/plugin-grid). objectTypesPending is true when there is an object name, the data source can answer getObjectSchema, and no field catalogue is in hand. An untyped column (no authored type) on any of the three column paths (authored ListColumns, bare field names, host row keys) is withheld. No heuristic type is inferred for it, and its cell is the mask. isMaskedGridColumn takes a third, required argument, so the masked flag, the grid's client export (CSV and JSON), the mobile card and the grouping refusal all treat a withheld column as masked. A column that authors its own type draws from it, as before.
    • The group label follows the cell: a grouping entry on a withheld key is refused, like a masked key, until the types are known. That refusal is not warned; the masked-field warning still names a known masked key.
    • A failed inline read is still non-fatal for the rows. Its comment no longer claims the columns fall back to heuristics.
    • The record panel a row opens under overlay navigation, with no declared fields in hand, drew every value of the record by inference. In the same window it now draws each value as the mask.
  • RelatedList (@object-ui/plugin-detail). This list draws cells from the object's types only (makeCell), never from an authored type. So in its window (objectTypesPending, from PR 1), every column without a cell of its own gets the mask as its cell, at the one seam into the table (tableColumns). An author-supplied cell draws what it returns.
  • ObjectDataTable (@object-ui/plugin-dashboard). In its window, a column with no authored type gets the mask as its cell, and a column with an authored type draws from it. Its record drill-down drawer (RecordDetailDrawer, internal) takes the table's objectTypesPending and draws every value as the mask in that window.

No window where the host already holds the types (the triage's first shape). ListView (@object-ui/plugin-list) reads the object definition before it fetches the rows (the data fetch waits for objectDefLoaded). It now hands the definition's fields to the grid as the objectFields React prop, beside data. The grid answers field-type questions from that catalogue until its own read lands. Only the grid view is handed it.

The mask's owner exports it. @object-ui/fields now exports MaskedCellRenderer, with field optional. The three producers draw a withheld cell with the one mask the fields package owns and do not restate the glyph. That is the reason PR 1's review gave for keeping the mask's drawing out of components.

Mechanism assumptions (Zone 2), measured

  • B1: confirmed at base c907a9c87.
    • On the inline path objectSchema starts null, and untyped columns take their type from it or from inferColumnType.
    • The inline fetch effect's catch swallows a failed getObjectSchema ("Schema fetch failure for inline data is non-fatal").
    • The grid-fetch path reads the schema before its rows and shows a failed read as the error panel, so that path has no draw window.
  • B2: confirmed. PR 1's objectTypesPending stamps every column in both producers, and neither producer attached a cell in that window. RelatedList's makeCell needs a definition, and ObjectDataTable's renderFieldValue got no type. So the table drew the value.
  • B3: this branch adds no textual overlap with PR objectui#10278.
    • Each of its nine ObjectGrid.tsx hunks was checked, context by context, against this branch's base and head. Eight match on both.
    • Its -2349 hunk already misses on origin/main c907a9c87, before this branch: main moved under that draft.
    • Its plugin-grid README hunk matches on both.
  • B4: no existing grid input carried the types. The narrowest input is a new optional React prop, objectFields on ObjectGridComponentProps.
    • It reuses the runtime-only channel that decision batch Add cloud storage functionality with Vercel KV integration in studio #70 ruled for the object's field catalogue. SchemaRenderer already strips an AUTHORED objectFields in all three spellings (objectui#8818), so no view author can hand the grid a catalogue that types a credential field as text.
    • A new name, such as passing the whole objectSchema, would have been spread from authored metadata straight into the grid.
    • So Clause-②: yes holds: this adds a new optional prop to a published component's props type, and a new export to @object-ui/fields.

Design choices, stated

  • "Draw it as unknown" draws the mask. A skeleton would claim the row is still loading after a failed read, and an em-dash would claim the value is empty. The mask is the face this product already uses for "a value is here and is not shown".
    • The cost: while a host that does not hand the types is waiting, untyped columns flash as masks, and after a failed read they stay masks.
    • RelatedList and ObjectDataTable already paid PR 1's stated cost in that window (no copy, tooltip, export or sort). Now the cells are masked too.
  • A withheld grouping key is refused, not grouped under masked labels. Masked labels would still bucket the records that share a value, which is objectui#10583's own reason for refusing a masked key.

Beyond the claimed file surface (stated, not silent)

  • The mask export. packages/fields/src/index.tsx, packages/fields/README.md and a minor changeset: the MaskedCellRenderer export, from the mask's owner.
  • The same window one click further. packages/plugin-dashboard/src/RecordDetailDrawer.tsx, and the grid's record panel inside ObjectGrid.tsx, outside the column-type region.
    • This is a bounded in-place fix: the same defect class, the same withheld shape and pins, files in the packages this claim already holds, and the same gate family.
    • The claim's file surface should be amended to carry them.
  • Forced prose. packages/types/src/data-display.ts (JSDoc only), content/docs/components/complex/data-table.mdx, and the grid, detail and dashboard READMEs. Their text said an untyped column is still drawn as text in this window, and this change makes that false.
  • Existing pins edited: 8 files (9 case bodies, one shared wait helper, one fixture). Each read an untyped cell's raw value INSIDE the window this card withholds, as a readiness signal or as a control. Each now waits for the mask or for the settled draw, and each keeps its own claim:
    • authoredColumnsFls-6799, inlineDataFls-6723: the in-flight case waits for the mask.
    • hostFetchedDefaultColumns-6677: the two in-flight cases wait for the mask.
    • RelatedList.maskedColumn-10657: the settled wait also requires the drawn name, and the two fail-closed cases wait for the mask.
    • ObjectDataTable.maskedColumn-10657: the fail-closed fixture's Name column authors type: 'text', so it still draws. That file pins the flag, which still stamps every column.
    • ObjectDataTable.fetchGate-10664: the thrown-read case waits for the mask.
    • RelatedList.multiValueParentScope-7299: the thrown-read case counts the one parent-scoped row by its mask.
    • RecordRelatedListRenderer.unresolvedIdentityFailClosed-8793: the census case waits for the settled draw.
  • Pending changesets this supersedes. The pending objectui#10583 changeset and four pending objectui#10657 changesets (grid, related list, object-data-table, types) describe the window as uncovered or as drawing text. The overwrite gate forbids editing them, so each new changeset says which of those sentences no longer holds.

Pins (new)

  • plugin-grid, maskedFirstPaint-10657 (8 cases). Held, rejected, settled, host catalogue, group label (held and rejected), and the record panel. Each case has Name beside it, a column that authors type: 'text' and so draws, tooltips and copies in every arm.
  • plugin-detail, RelatedList.maskedFirstPaint-10657 (6 cases). Held, rejected and settled, over bare-name and authored-object columns, beside an author-supplied cell column.
  • plugin-dashboard, ObjectDataTable.maskedFirstPaint-10657 (6 cases). Held, rejected (inline and fetched rows), auto-derived, settled, and the record drawer, beside a column that authors type: 'text'.
  • plugin-list, ListView.gridObjectFields-10657 (2 cases). Every props bag that hands the grid rows also carries the declared fields. The control: after a failed definition read, the rows arrive with no catalogue.

Ablation. One run, restored by a trap.

  • Mutation. The seven source files were checked out at c907a9c87. The fix markers (objectTypesPending, withheldCell, the objectFields handoff, the MaskedCellRenderer export) read 0 on disk, and every blob equalled its base blob.
  • Red. The four new pin files gave Test Files 4 failed (4) and Tests 20 failed | 1 passed (21). The one pass is the ListView control arm.
  • Restore. The files were restored from HEAD: git diff HEAD was empty, every blob equalled its HEAD blob, and the same run gave Tests 21 passed (21).
  • The pins reach the packages through the vitest source aliases, so no build was involved.

Verification

All runs went through the shared verify lock. Head-pinned; 9f9b8b7 differs from e8503d3 by one changeset file only.

  • Package suites at e8503d3.
    • plugin-grid: Test Files 163 passed (163), Tests 1525 passed (1525).
    • plugin-dashboard: Test Files 138 passed (138), Tests 1290 passed (1290).
    • plugin-detail: Test Files 215 passed | 1 skipped (216), Tests 2108 passed | 8 skipped (2116).
  • Package suites at e742e22. The sources of these two packages are unchanged since the first commit.
    • plugin-list: Test Files 103 passed (103), Tests 1171 passed (1171).
    • fields: Test Files 214 passed | 1 skipped (215), Tests 3453 passed | 7 skipped (3460).
  • Downstream consumers at e742e22. Every test file outside the four producer packages (and outside types, core, fields, components, scripts) that names these components and getObjectSchema: Test Files 96 passed (96), Tests 1680 passed (1680).
  • Type-check at e8503d3. turbo run type-check for fields, types, plugin-grid, plugin-list, plugin-detail and plugin-dashboard: Tasks: 21 successful, 21 total, with each package's type-check script echoed and no error TS. Each package's tsconfig.test.json lists its new pin (--listFiles, 1 hit x4).
  • Lint, narrowed, at e8503d3. eslint on the 20 changed TS files gave 0 errors.
    • Per-file warnings equal the base for every pre-existing file except ListView.tsx, which goes from 184 to 185 (react-hooks/refs). The new read of viewComponentSchema.type carries the same finding that the existing schema={viewComponentSchema} read already carries. Warnings do not gate here.
    • The narrowing is a measurement: eslint.config.js sets no parserOptions.project or projectService, so linting is not type-aware; the population is the changed files from the diff against c907a9c87; the count of 20 is read from the JSON output.
  • Gates, exit 0 at 9f9b8b7 or e8503d3.
    • check-changeset-presence: "20 source file(s) of 6 released package(s) changed, and this change declares 6 changeset(s)".
    • check-changeset-no-major, check-changeset-fixed.
    • check-changeset-overwrite: "No pre-existing changeset was modified or deleted".
    • check:pending-changeset-literals, check:control-bytes.
    • check:new-line-citations: "0 new citation(s)".
    • check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape.
    • check-doc-links: "Links are valid across 17 scan roots".
    • check:doc-fences, check:doc-types, check:unreferenced-sources, check:phantom-deps.
    • check:changeset-claims (report-only).
    • check-governed-queue-guard --test on the 31 paths: "NOT GOVERNED".
  • NOT MEASURED.
    • check:doc-snippets: it exited 2 with PREREQUISITE NOT MET, because its 34-package build did not run here; CI builds it. For that reason the grid README states the new prop in prose rather than in an uncompiled snippet.
    • check:readme-exports: it exited 1 with "self-import(s) could not be judged" for unbuilt packages. The new fields README import resolves against the built fields dist.
    • A browser check of the mask under the real console.

Acceptance notes (not in this PR)

  • RelatedList's own opt-in filter box and its list sort buttons read raw values (objectui#10728, ordered after this card).
  • The data-table header context menu ignores sortable: false.
  • RelatedList's other bodies are not the data-table seam and were not measured for this window: the mobile object-gallery card view and type: 'list' (data-list).
  • A prefix compound cell on the grid draws its prefix field as a badge or as text whatever that field's type. This is an authored presentation, the same boundary as an authored type: 'text' over a secret field, in the settled state too.
  • A column that authors type: 'text' over a secret field draws its text in the window, as it does once settled. Its flag in the grid's window follows the authored type until the object's type is known.

Generated by Claude Code

…column while the object's field types are unknown (objectui#10657)

An object-bound table whose rows paint before its object definition is read
(ObjectGrid on the host-fetched path, RelatedList, ObjectDataTable) drew an
untyped column over a password / secret field as text in that window, and
for good when the read failed. Such a column is now WITHHELD: it draws the
mask (MaskedCellRenderer, now exported from @object-ui/fields) and is
flagged masked, and it stays withheld when the read fails. ListView hands
the grid the field catalogue it already holds (objectFields, the ruled host
channel), so a list view has no window at all.

Claude-Session: https://claude.ai/code/session_01MCg3z5cGrV51xEddpGcxEY
Co-authored-by: Claude <noreply@anthropic.com>
… withheld first paint and the host catalogue (objectui#10657)

New pins: the grid's held / rejected / settled / host-catalogue arms with a
typed text control, the group label, RelatedList's and ObjectDataTable's
held / rejected / settled arms, and ListView's handoff of the catalogue with
the rows. Existing in-flight pins wait for the mask instead of the raw value
they used to print in that window.

Claude-Session: https://claude.ai/code/session_01MCg3z5cGrV51xEddpGcxEY
Co-authored-by: Claude <noreply@anthropic.com>
…be withheld columns; changesets (objectui#10657)

The TableColumn.masked JSDoc, the data-table docs and four READMEs said an
untyped column over a password / secret field still draws as text before the
object's types are known; they now describe the withheld draw and the grid's
objectFields prop. One changeset per touched published package. Three more
existing pins read an untyped cell's raw value inside that window and now
read the mask or wait for the settled draw.

Claude-Session: https://claude.ai/code/session_01MCg3z5cGrV51xEddpGcxEY
Co-authored-by: Claude <noreply@anthropic.com>
…compiled snippet (objectui#10657)

Claude-Session: https://claude.ai/code/session_01MCg3z5cGrV51xEddpGcxEY
Co-authored-by: Claude <noreply@anthropic.com>
…er values while types are unknown (objectui#10657)

The same window, one click further: ObjectGrid's record panel (overlay
navigation, no declared fields) and ObjectDataTable's record drill-down
drawer drew a clicked row's fields from the unknown types, so after a failed
read they printed a password / secret field as text. Both now draw every
value as the mask in that window. Pinned beside the column arms.

Claude-Session: https://claude.ai/code/session_01MCg3z5cGrV51xEddpGcxEY
Co-authored-by: Claude <noreply@anthropic.com>
…des (objectui#10657)

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

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 35 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/4730-retire-dead-locale-key-batch.md

  • names ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    • Superseded twin vocabularies. cellRender.* and rowAction.* duplicated a grid.* vocabulary that won. RowActionMenu.tsx is fully i18n-wired and reads grid.openMenu / grid.edit / grid.delete; ObjectGrid.tsx reads grid.empty / grid.yes / grid.no / grid.systemFields. The twins had no reader on either side. - Labels that outlived their control. calendar.agenda labelled a view mode b55a34647 retired from CalendarViewMode (now 'month' | 'week' | 'day'). home.quickActions.createApp*, layout.systemNav.createApp, actionDialog.defaultActionTitle / .ok and grid.bulk.selectPlaceholder sit in namespaces whose consumers are live and wired but demonstrably read other siblings. - Surfaces that left the product. map.* is the strongest form: @object-ui/plugin-map declares no @object-ui/i18n dependency and contains no t() call at all, so it cannot consume a locale string. home.stats.* and recordDetail.viewersTooltip name surfaces nothing renders.

.changeset/5876-one-lookup-predicate.md

  • names ObjectDataTable.tsx → packages/plugin-dashboard/src/ObjectDataTable.tsx — edited by this change

    computeLookupExpand in ObjectDataTable.tsx carried its own isLookup, byte-identical to the exported isLookupType in recordFields.tsx after objectui#5692 pointed both at @object-ui/core's EXPANDABLE_FIELD_TYPES. Nothing kept them aligned: a future edit to either — a member added, the retirement gate moved — would have re-forked the $expand decision from the predicate whose docblock claims to drive it. computeLookupExpand now calls isLookupType, which gains its first production consumer, and the module no longer imports the shared family or the retirement gate at all.

.changeset/6286-explain-batch-cap-from-spec.md

  • names rowRecordCrudVerdict.test.tsx → packages/plugin-grid/src/__tests__/rowRecordCrudVerdict.test.tsx — edited by this change

    Covered by a new reference-identity test rather than a value assertion. Every assertion on 200 passes on both sides of this change — a ghost — so useRecordCrudVerdicts.batchCap.test.tsx stands the spec module in at a cap no hand copy could produce and asserts the request chunking follows it, with a control case proving the stand-in installed and differs from the shipped value. The pre-existing cap assertion in rowRecordCrudVerdict.test.tsx now derives its fixture and its bound from the same export instead of re-typing 200, which removes the last hand copy on this surface without pretending to be a two-world test.

.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/6597-retire-fieldmeta-referenceto.md

  • names plugin-grid/src/ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    No authoring story survived the search either. ObjectGrid's own relational-meta pass-through (applyRelationalMeta, plugin-grid/src/ObjectGrid.tsx) copies reference_to / reference / display_field / etc. from the SCHEMA field def only, at all three of its call sites — never from an authored column override. No doc, example, or fixture in this repo shows a table column pinning a lookup's target away from what its schema field already says. Under the maintainer's standing startup-stage rule (2026-08-27: deprecated/alias spellings retire immediately, no transition windows), no measured demand selects withdraw.

.changeset/6625-retire-fieldmeta-decimals.md

  • names fields/src/index.tsx → packages/fields/src/index.tsx — edited by this change

    buildFieldMeta computed decimals: overrides.decimals ?? meta?.decimals ?? meta?.scale on every call and the value reached nothing. Re-measured on this branch's base (efdc6c62): zero .decimals member reads across @object-ui/fields, @object-ui/i18n, @object-ui/components, @object-ui/core and plugin-dashboard itself — the only non-comment occurrence was the write being removed here. The positive control in the same query shape fires: .scale member reads hit NumberField.tsx, GridField.tsx and fields/src/index.tsx. So the zero is a finding, not a broken query. The overrides.decimals ?? head of that chain had already lost its only feeder when objectui#6425's ruling removed the authored read from ObjectDataTable.enrich(); RecordDetailDrawer, the only other buildFieldMeta caller, passes no overrides at all. Both halves retire together, so the key leaves in one move.

.changeset/6650-listcolumn-wrap-implemented.md

  • names packages/plugin-grid/README.md → packages/plugin-grid/README.md — edited by this change

    @objectstack/spec declares ListColumn.wrap and describes it to authors as "Allow text wrapping", and packages/plugin-grid/README.md shows it in its authored-column example. No renderer anywhere implemented it. Long cell text stayed clipped to one line, with no error, no warning and no feedback of any kind — a promise made at authoring time and silently broken at render time.

.changeset/6694-dashboard-lookup-reference-meta.md

  • names packages/fields/src/index.tsx → packages/fields/src/index.tsx — edited by this change

    ⚠️ The copy set is three keys where ObjectGrid's RELATIONAL_META_KEYS is nine, and the difference is measured per key, not preferred. The grid's cells are EDITABLE, so its extra keys drive the inline picker's query (LookupField / UserField read id_field, description_field, lookup_filters, lookupFilters); these two widgets are read-only and their render path ends at a cell renderer. packages/fields/src/index.tsx reads exactly reference_to, reference and display_field off a cell's field prop; titleFormat is never read off a field meta at all (its readers take it off the object schema, which arrives here through useRefObjectSchema(reference_to)), and reference_to_field has zero member reads anywhere in the repo. Copying the other six would mint six members written on every call and read by nothing — precisely what objectui#6625 (decimals) and objectui#6597 (referenceTo) retired from this same file.

.changeset/6723-inline-data-fls.md

  • names packages/plugin-grid/src/__tests__/inlineDataFls-6723.test.tsx → packages/plugin-grid/src/__tests__/inlineDataFls-6723.test.tsx — edited by this change

    Pinned in packages/plugin-grid/src/__tests__/inlineDataFls-6723.test.tsx (a readable declared field renders; an unreadable declared field does not, even with host data for it; a derived key is unaffected; plus the perms-not-loaded, no-objectName and schema-in-flight boundaries and a case through the real PermissionProvider) and, as a measured no-op on the ListView path, in packages/plugin-list/src/__tests__/ListView.inlineFlsNoop-6723.test.tsx.

.changeset/6799-authored-columns-fls.md

  • names packages/plugin-grid/src/__tests__/authoredColumnsFls-6799.test.tsx → packages/plugin-grid/src/__tests__/authoredColumnsFls-6799.test.tsx — edited by this change

    Pinned in packages/plugin-grid/src/__tests__/authoredColumnsFls-6799.test.tsx.

.changeset/6837-reference-to-arm-deletion.md

  • names fields/src/index.tsx → packages/fields/src/index.tsx — edited by this change

    Three readers were deliberately left alone. LookupCellRenderer (fields/src/index.tsx), LookupField and UserField read FieldMetadata — ObjectUI's OWN contract, whose LookupFieldMetadata declares reference_to and never declares reference. They are fed by the emitters above and by published example schemas (examples/schema-catalog/src/schemas/fields-lookup/*.json), so narrowing them would break in-repo producers, and plugin-grid's relationalMetaCopySet.derivation.test.ts re-derives its read set from exactly those three sources — where reference_to is recorded with verdict adapter-stamped. DetailViewFieldSchema is likewise untouched.

.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/6882-datatable-declare-render-cell-editor-and-cell-classname.md

  • names content/docs/components/complex/data-table.mdx → content/docs/components/complex/data-table.mdx — edited by this change

    ⚠️ What the schema-level cellClassName actually styles. It is NOT the table-level twin of the per-column key: the two reach disjoint cells. Measured on the render, the schema-level key is folded into the utility cells only — the selection-checkbox cell, the row-number cell and the row-actions cell — while every data cell folds TableColumn.cellClassName and nothing else. Row density is therefore a pair of settings (ObjectGrid sets both), and the schema-level key alone leaves data cells at the primitive's default p-4. The docblock, the zod describe and content/docs/components/complex/data-table.mdx all say this now.

.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.

.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/7070-no-invented-gantt-date-fields.md

  • names plugin-list/src/ListView.tsx → packages/plugin-list/src/ListView.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/7125-dashboard-empty-state-keys-retired.md

  • names ObjectDataTable.tsx → packages/plugin-dashboard/src/ObjectDataTable.tsx — edited by this change

    Not touched: table.noRows ('No rows to display') and engine.form.noRows (packages/app-shell/src/views/metadata-admin/i18n.ts, read at widgets.tsx) — two different, same-named keys in different namespaces. Nor the comments in WidgetEmptyState.tsx, DatasetWidget.tsx, ObjectDataTable.tsx and PivotTable.tsx that record WHY three widgets with three strings became one shared empty state; the packs' own comment keeps that rationale and now names the retirement instead of a row that is gone.

.changeset/7166-retire-inert-fieldmeta-copies.md

  • names packages/fields/src/index.tsx → packages/fields/src/index.tsx — edited by this change

    applyRelationalMeta writes the copy set onto the fieldMeta that generateColumns hands to ANGLE-BRACKETS(CellRenderer) as the field prop — six JSX passes across the three column-building paths, and nowhere else. For a relational column that resolves to LookupCellRenderer, which reads exactly reference_to, reference, display_field, displayField, reference_field and options; a user column resolves to UserCellRenderer, which destructures { value } and reads no field meta at all. Measured by receiver rather than by count: packages/fields/src/index.tsx, the file holding every cell renderer, contains zero occurrences of the three retired keys, against a control of 22 occurrences of the display_field / displayField / reference_to spellings the cell does read.

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

  • names plugin-list/src/ListView.tsx → packages/plugin-list/src/ListView.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/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 (objectui#6938), FileUploadSchema and FilterBuilderSchema (objectui#6150); these five were left out only because their doc pages never listed it.

.changeset/7762-object-grid-export-options-bare-array-refusal.md

  • names ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    What was measured, on this branch's base. The mirror declared NO exportOptions member at all, and BaseSchema is .passthrough(), so ObjectGridSchema.safeParse({ type: 'object-grid', objectName: 'accounts', exportOptions: ['csv', 'xlsx'] }) returned success: true with the array back VERBATIM — as did { formats: ['csv', 'pdf'], compression: 'gzip' }. Nothing on the render path parses, and ObjectGrid.tsx reads schema.exportOptions?.formats and only that, so the authored array then lost SILENTLY to the ['csv', 'json'] default: the useEffect that warns about dropped formats reads .formats too and returns early when it is absent, while !!schema.exportOptions kept the export button on screen. An author declared ['csv', 'xlsx'] and got csv/json with no error, no warning and no console line. The two authoring faces disagreed in the direction opposite to objectui#6956's: the TypeScript interface already declared the object form only, so TS refused what zod admitted.

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

  • names ListView.tsx → packages/plugin-list/src/ListView.tsx — 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/7780-object-kanban-record-source.md

.changeset/7997-detail-view-related-retired.md

  • names packages/plugin-detail/README.md → packages/plugin-detail/README.md — edited by this change

    Documentation. packages/plugin-detail/README.md and content/docs/api/schema-reference.md stop teaching the retired array and gain a migration block each.

.changeset/8114-detail-tab-activity-timeline.md

  • names plugin-dashboard/README.md → packages/plugin-dashboard/README.md — edited by this change

    README.md ships in this package's files, so the example went out in every tarball. DetailTabs renders a tab's content through ANGLE-BRACKETS(SchemaRenderer schema={toRenderableSchema(tab.content)} /), which makes content.type an SDUI node position judged by the component registry — so a reader copying the tab got the registry's Unknown component type panel (OBJUI-001) where the timeline should be. Same shape as the line-chart widget in plugin-dashboard/README.md (objectui#7896's census; fixed by objectui#7951) and the fourth known instance.

.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.

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

  • names packages/plugin-list/src/ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    • packages/plugin-list/src/ListView.tsx generates the node as groupBy: laneField. objectDef loads asynchronously, so laneField is undefined on every load until it lands, and stays undefined whenever the object offers no stageField hint and none of status / stage / state / phase. The renderer serves that node; both published faces refused it. - content/docs/utilities/data-objectstack.mdx documents an object-kanban node that is exactly { type, dataSource }, with no groupBy. ⚠️ This one is weaker and is cited for what it is: that fragment is still refused after this change, at RECORD_SOURCE_REQUIRED, because dataSource is not a rung of the record-source ladder. It shows a lane-less board is a documented authoring; it is not a document this change admits.

.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/9722-bulk-executor-datasource-face.md

  • names ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    • ObjectGrid.tsx → RecordDetailPanel: removing it type-checks GREEN. It was paying for nothing — both sides already declare the same DataSource | undefined from @object-ui/types. Removed outright. - ObjectGrid.tsx → BulkActionDialog: removing it reddens, and it was paying for TWO things at once — the optional-vs-required arm (the grid declares dataSource?: DataSource, the dialog demands one) AND the bulk-door contravariance. Only the first still needs erasing, so it is now a non-null assertion: today's runtime behaviour is preserved exactly, and any future drift of the face reddens at that site instead of passing silently.

.changeset/9853-grid-non-positive-page-size.md

  • names ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    Refuse a non-positive pageSize at all three of ObjectGrid.tsx's read points, instead of giving two different answers for one authored value (objectui#9853).

.changeset/dashboard-expandable-family-convergence-5692.md

  • names ObjectDataTable.tsx → packages/plugin-dashboard/src/ObjectDataTable.tsx — edited by this change

    plugin-dashboard's two private copies of the reference-bearing field family now read @object-ui/core's published EXPANDABLE_FIELD_TYPES instead of restating it — LOOKUP_TYPES in recordFields.tsx and the inline disjunction inside computeLookupExpand in ObjectDataTable.tsx. Both carry the identity pin the already-converged consumers carry, so a member-identical private copy fails rather than quietly re-forking the table.

.changeset/list-user-actions-collision-5398.md

  • names ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    The harvest now reads the object block only. Both userActions read sites in ListView.tsx carry a comment naming the collision, and __tests__/ListView.userActionsCollision.test.tsx pins each clause of it: the two shapes, a producer that manufactures the view one, the harvest's blindness to it, and the projection that must keep the object's operand with a toolbar block — or an empty block — present on the view.

.changeset/listview-comment-pair-4559.md

  • names ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    Two comment corrections in ListView.tsx (objectui#4559, objectui#4966). No runtime behaviour changes and the emitted bundle is byte-identical; the published .d.ts does change, which is why this is a patch rather than an empty frontmatter.

.changeset/plugin-detail-8937-parent-scope-residue.md

  • names packages/plugin-detail/README.md → packages/plugin-detail/README.md — edited by this change

    • packages/plugin-detail/README.md (it is in files[], so it ships). It said the node's filter is AND-combined with { [relationshipField]: parentId }, full stop. Since objectui#7299 the parent condition is compiled to match the relationship field's arity, so a multi-valued relationship gets { [relationshipField]: { $contains: parentId } } instead. The paragraph now states both spellings and names the arbiter (@objectstack/spec/data's isMultiValueField). - The claim that the SQL driver decides arity on that same predicate. It does not: driver-sql gates the equality family on its own storage question, which reads multiple as truthy on ANY type. The two rules therefore disagree for a type outside MULTI_CAPABLE_TYPES carrying multiple: true. objectui#9184 moved the arity compiler into @object-ui/core's parent-scope seam and carried the claim with it, so the correction is recorded there — the seam now states the driver's measured rule, records the divergence as a divergence, and points at the upstream card that owns which of the two rules is right (objectstack#17469). RelatedList.tsx's pointer comment and the objectui#7299 test header carried the same sentence and are corrected to match.
  • names RelatedList.tsx → packages/plugin-detail/src/RelatedList.tsx — edited by this change

    • packages/plugin-detail/README.md (it is in files[], so it ships). It said the node's filter is AND-combined with { [relationshipField]: parentId }, full stop. Since objectui#7299 the parent condition is compiled to match the relationship field's arity, so a multi-valued relationship gets { [relationshipField]: { $contains: parentId } } instead. The paragraph now states both spellings and names the arbiter (@objectstack/spec/data's isMultiValueField). - The claim that the SQL driver decides arity on that same predicate. It does not: driver-sql gates the equality family on its own storage question, which reads multiple as truthy on ANY type. The two rules therefore disagree for a type outside MULTI_CAPABLE_TYPES carrying multiple: true. objectui#9184 moved the arity compiler into @object-ui/core's parent-scope seam and carried the claim with it, so the correction is recorded there — the seam now states the driver's measured rule, records the divergence as a divergence, and points at the upstream card that owns which of the two rules is right (objectstack#17469). RelatedList.tsx's pointer comment and the objectui#7299 test header carried the same sentence and are corrected to match.

.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.

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 9a5f99880 (merge-base with origin/main): 28 file(s) changed outside .changeset/, read against 1606 pending declaration(s) that publish a body (2195 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3078.8 KB 3104.5 KB
Main entry chunk (gzip) 148.3 KB 350 KB
Entry file index-xOu9IZmI.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.15KB
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) 556.38KB 133.09KB
core (index.js) 9.57KB 3.81KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 224.97KB 62.47KB
fields (index.js) 260.64KB 66.17KB
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.67KB 14.70KB
plugin-charts (index.js) 83.58KB 22.75KB
plugin-chatbot (index.js) 198.40KB 47.22KB
plugin-dashboard (index.js) 134.42KB 35.70KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 233.40KB 61.78KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 158.35KB 40.47KB
plugin-gantt (index.js) 169.75KB 41.96KB
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) 43.55KB 11.99KB
plugin-timeline (index.js) 30.91KB 9.05KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 87.83KB 22.01KB
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.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 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.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: 246/246 CONTRACT_REVIEW_TIER
Head-sha: 9f9b8b79df7e075f7d809221fb8d5e0cb9c640e9

Read: card objectui#10657 (body; comments 5835192310 triage, 5839121438 scope extension, 5840909993 PR 1 ACCEPT, 5841025030 landing, 5852410452 release, 5856905742 seat claim; the dev's os-dev-report 5858002351 was not used as evidence); PR 1's at-tier record 5840905114 on PR objectui#10725; the objectui#8818 thread (ruling 5653291246, ratification 5810248981, claim 5830962988, ACCEPT 5831620555) for the batch #70 ruling; PR objectui#10819 body, file list (31 files, +1309 −118), both bot comments (changeset-claim-re-read, bundle report) and the check-runs on the head, polled twice, plus the merge-base's check-runs as a control; the diff c907a9c87..9f9b8b7 (merge-base c907a9c8708f300763509d9a6eda412064a8c87e, 6 commits); the head sources of ObjectGrid.tsx (the three column paths, the fourth objectSchema.fields literal, the columns stamp pass, the inline and grid-fetch schema effects, the record panel, the mobile card), maskedColumn.ts, plugin-grid/src/index.tsx, ListView.tsx (the definition fetch and the SchemaRenderer render site), SchemaRenderer.tsx (the metadata destructure and withoutAuthoredObjectFields), RelatedList.tsx (objectTypesPending, sortableColumns, tableColumns, makeCell, viewSchema's gallery/table/list arms), ObjectDataTable.tsx (objectTypesPending, enrich, the derive path), RecordDetailDrawer.tsx, fields/src/index.tsx (MaskedCellRenderer, isMaskedFieldType, the standard map), data-table.tsx's flag reads, data-objectstack's getObjectSchema, types/src/data.ts, zod/data-display.zod.ts; the eight edited pins and the four new pins in full; the six new changesets, .changeset/10583-masked-cell-copy.md and the five pending PR 1 10657-* changesets, .changeset/config.json, the plugin-grid and fields package.json contracts; the JSDoc, data-table.mdx and five README diffs; rowRecordCrudVerdict.test.tsx at head and its diff against base; vitest.config.mts (testTimeout, heavyDomTests); objectui AGENTS.md §5 and §9 (版本号策略, the changeset-presence rule, 测试纪律).

① Derived judgments

A. Fail closed: is an object-bound untyped column's raw value absent from the DOM while types are pending AND after a failed read?

  1. ObjectGrid, the state. objectFields = objectSchema?.fields ?? hostObjectFields; objectTypesPending = !!objectName && typeof dataSource?.getObjectSchema === 'function' && !objectFields. The inline effect's catch only warns, so a rejected read leaves objectSchema null and objectTypesPending true for the life of the grid; nothing ever flips it back. The grid-fetch effect reads the schema inside the same try as the rows, so a thrown read skips the rows and lands in the error branch: no draw window on that path. RIGHT.
  2. Path A (authored ListColumns): withheld = isWithheldGridColumn(col.type, objectTypesPending); baseInferredType is null when withheld (no inferColumnType), CellRenderer = withheld ? MaskedCellRenderer : …. Every branch that draws a value (link+action, isLinked, type-only, default) reads CellRenderer first, and the action branch draws a button, not the value. RIGHT.
  3. Path B (bare field names) and path C (host row keys): isWithheldGridColumn(undefined, objectTypesPending), type: withheld ? null : …, cell from MaskedCellRenderer. The fourth literal iterates objectSchema.fields and so exists only after the grid's own read. RIGHT.
  4. Every table path: the columns pass stamps masked: true from isMaskedGridColumn(producerType, objectFields?.[key]?.type, objectTypesPending), and data-table keys title={!isFit && !col.masked && …}, exportColumns = columns.filter(!col.masked), isEditable = … && !col.masked, sampleRows = col.masked ? [] : …, and the copy/search/sort reads of PR 1 on the same flag. The grid's own client export asks isMaskedKey with the third argument (CSV column and JSON key). The mobile card's isMaskedCardColumn takes the third argument, so a withheld key classifies other, and both the title row and the otherCols branch draw col.cell (the mask). The pin reads document.body.innerHTML for the raw value, which covers text and attributes. RIGHT.
  5. The record panel: the shared RecordDetailPanel branch is gated on hasDeclaredFields (the grid's OWN objectSchema.fields), so in the window the inference panel runs; it now returns ‹MaskedCellRenderer> for every regular value and every meta value when objectTypesPending. Pinned in the REJECTED record-panel case. RIGHT.
  6. RelatedList: tableColumns is the one seam into the table (columns: tableColumns on the data-table arm); withheld = objectTypesPending && typeof col.cell !== 'function' → { …col, masked: true, cell: withheldCell }. makeCell needs a definition, so the only cell present in the window is an author's. useSettledSchema settles def: null on a throw (PR 1's reading, unchanged here), so !objectSchema?.fields holds after a failure. RIGHT for the table body. The gallery (isMobile) and type: 'list' bodies take paginatedData and effectiveColumns, not tableColumns, and are outside this seam: see ③.
  7. ObjectDataTable: enrich returns cell: withheldCell when objectTypesPending && !authored.type, after the authored-cell early return and before renderFieldValue; both the declared branch (withHeaders.map(enrich)) and the derived branch (orderedKeys.map((k) => enrich(…))) pass through it. RecordDetailDrawer takes objectTypesPending and draws ‹MaskedCellRenderer> per field (and drops numeric). Rows FETCHED after a failed read are pinned withheld too. RIGHT.
  8. Any path back to text? None found: no site reads inferColumnType for a withheld column, and the tree-wide grep at head for the old sentences (still drawn as text, draws and hands out the raw value, keeps its heuristic, do not set the flag yet) finds only the rewritten comment in ObjectGrid.tsx. RIGHT.
  9. Nothing withheld that should not be: a column with a non-empty string type is never withheld (isWithheldGridColumn tests typeof columnType === 'string' && columnType.length > 0); inline value rows with no object name give objectTypesPending === false; a data source with no getObjectSchema gives false (the grid-fetch path even stubs fields: {}, which is truthy). RIGHT. Two shapes noted for ③: a definition that lands with no fields member keeps objectTypesPending true for good (fail-closed direction), and an ARRAY-shaped fields is truthy and so reads as "known".

B. The grouping refusal for a withheld key. maskedGroupingSignature now serialises { refused, warned }: refused from isMaskedGridColumn(…, objectTypesPending), warned from the same call with false. unmaskedGroupingFields drops refused; groupingRefusesNothing compares the string to the NO_GROUPING_REFUSAL constant (same shape, so the compare cannot silently miss); useGroupedData gets fields: unmaskedGroupingFields; the warning effect reads warned only. Keyed on the authored array and the signature string, not a memo identity (AGENTS.md #10 kept). What the user sees: the grid ungrouped by that entry while the types are unknown (other levels still apply), no warning, and the entry groups again once the types land unless the field is masked, which is then warned. Pinned: .group-label count 0 in the HELD and REJECTED arms beside a control mount grouped by the typed name column that labels Ada. RIGHT, fail closed.

C. isMaskedGridColumn's third required argument. Five call sites at head, all in ObjectGrid.tsx (grouping refused, grouping warned with a deliberate literal false, the export isMaskedKey, the columns stamp pass, isMaskedCardColumn), each passing objectTypesPending. The function is not in plugin-grid/src/index.tsx's export list, and no other file in the tree imports it, so the signature change is internal; TypeScript makes the parameter required, so there is no silent default. RIGHT.

D. The objectFields prop. (a) Runtime-only: SchemaRenderer's metadata destructure strips objectFields (top-level and hoisted properties), and withoutAuthoredObjectFields strips the props alias bag, both before the …props host spread that lands last; ListView renders ‹SchemaRenderer schema={viewComponentSchema} {...props} … {...{ objectFields: objectDef.fields }}>, and ObjectGridRenderer forwards …rest to ‹ObjectGrid>. The ListView.gridObjectFields-10657 pin drives the real SchemaRenderer and a registry probe, so the handoff is measured through the strip, not around it. (b) The ruling: objectui#8818's class-one ruling 5653291246 (ratified 5810248981) states the authority as batch #70 (objectui#7742 / PR #8799): 「objectFields is a React prop, ⛔ not a schema key」, and its executed form is exactly what the dev cites: an authored value never reaches a component prop; the host's …props still arrives. The dev's claim is RIGHT. (c) Published: ObjectGridComponentProps (and its ObjectGridProps alias) is exported from plugin-grid/src/index.tsx, so objectFields?: Readonly‹Record‹string, unknown>> is a new optional member of a published props type in the emitted .d.ts. (d) Clause-②: yes is RIGHT: a new published input on plugin-grid and a new export from fields.

E. The MaskedCellRenderer export. export function MaskedCellRenderer({ value }: Omit‹CellRendererProps,'field'> & { field?: … }). Justified as one owner of the glyph: the three producers import it and none restates ••••••; it is the same identity the standard map spreads for every MASKED_FIELD_TYPES member, and isMaskedFieldType answers live === MaskedCellRenderer, so the changeset's registerFieldRenderer('api_token', MaskedCellRenderer) sentence is true. Declared in .changeset/10657-fields-masked-cell-renderer-export.md as minor, and documented in packages/fields/README.md. RIGHT.

F. The eight edited pins — signal only, or weakened?

  • authoredColumnsFls-6799, inlineDataFls-6723, hostFetchedDefaultColumns-6677 (two cases): each swaps getByText('Acme expansion') for a wait on the mask and ADDS queryByText('Acme expansion') null; the header assertions (dataHeaders) and the FLS bodies are untouched. Stronger, not weaker.
  • RelatedList.maskedColumn-10657: settledWithMask now also requires Ada (the settled signal, since a mask alone no longer tells the windows apart); the two fail-closed cases wait through a new withheldWithMask that also asserts Ada absent; the copy assertions stand. Stronger.
  • ObjectDataTable.maskedColumn-10657: fixture only, Name authors type: 'text'; the file pins the FLAG, which still stamps every column in the window, and its settled arm still needs Name to copy. Same claim.
  • ObjectDataTable.fetchGate-10664: mask wait plus queryByText('Acme') null; the expands(ds) claim untouched.
  • RelatedList.multiValueParentScope-7299: counts rows against the existing SINGLE_MATCH_COUNT (defined from the fixture) and still asserts Bob absent and the scoped $filter; the claim (one parent-scoped row) stands.
  • RecordRelatedListRenderer.unresolvedIdentityFailClosed-8793: waits for the settled cells through the existing cellTexts() and keeps not.toContain('90000'). Same claim.
    None deletes or weakens an assertion. RIGHT.

G. The four new pins. plugin-grid/maskedFirstPaint-10657: 7 cases (the PR body says 8; 7+6+6+2 = 21 matches the ablation's 21 passed, so the prose is off by one), each mount carrying Name with type: 'text' that must draw AND copy (expectControlLive) before any absence is read; expectWithheld reads the mask, an empty clipboard and innerHTML free of the raw value; HELD, REJECTED (after the grid's own warning), SETTLED (Note lifts, API Key stays masked), HOST CATALOGUE (declared types at first paint while the read is held), group label held and rejected, and the record panel. plugin-detail/RelatedList.maskedFirstPaint-10657: 6 cases over bare-string and authored-object columns, held/rejected/settled, with an author-supplied cell as the lit control — the right control here, since this producer never draws from an authored type. plugin-dashboard/ObjectDataTable.maskedFirstPaint-10657: 6 cases with the typed Name control, inline and FETCHED rows after a rejected read, auto-derived, settled, and the drawer through record-detail-body. plugin-list/ListView.gridObjectFields-10657: 2 cases through the real SchemaRenderer, the control being the failed read that hands rows with no objectFields. Each can fail on base (the ablation's red leg is the claim; the assertions are the kind base would fail, since base drew the raw text). RIGHT.

H. Prose, sentence by sentence, at the head. The six changesets: TRUE throughout, including the fields one's isMaskedFieldType('api_token') sentence (verified in code), the list one's 「Only the grid view is handed it」 (viewComponentSchema.type === 'object-grid'), and the grid one's mobile-card and export sentences (items A4). Each names the pending sentences it supersedes: 10583-masked-cell-copy.md's host-fetched 「Not covered」 bullet and its 「do not set the flag yet」 line, 10657-plugin-grid-masked-client-paths.md's 「What stays not covered … the host-fetched path's window」, 10657-plugin-detail-masked-related-list.md's and 10657-plugin-dashboard-masked-data-table.md's 「Not covered: … drawn as text」, and 10657-types-masked-doc.md's 「what that window does not cover」; none of the five is edited (Changeset Overwrite Report green) and each is addressed. 10657-components-masked-client-paths.md is not made false. The TableColumn.masked JSDoc, data-table.mdx (「a masked entry is ignored with a console warning; a withheld one groups again once the types are known」 matches B), and the grid, detail, dashboard and fields READMEs are TRUE; the zod describe() from PR 1 does not mention the window and stays true. Nothing pending is left false. RIGHT.

② Semver level

@object-ui/fields minor (new export) RIGHT; @object-ui/plugin-grid minor (new optional member on the published ObjectGridComponentProps) RIGHT; plugin-list, plugin-detail, plugin-dashboard, types patch RIGHT (behaviour and prose, no shape change); no major (AGENTS.md 版本号策略; Changeset Bump Policy and Changeset Fixed Group Check green). Presence: the six released fixed-group packages with src/ edits (fields, plugin-grid, plugin-list, plugin-detail, plugin-dashboard, types) each carry one changeset; components is not edited; Changeset Declaration green.

③ Boundary flags

  • DEFECT — CI is red on the head, and the change is what moved the pin. Test (shard 3/8) and the aggregate Test (「1 test shard(s) did not report success」) failed on 9f9b8b7, while every Test shard on the merge-base c907a9c is green. The failing case is packages/plugin-grid/src/__tests__/rowRecordCrudVerdict.test.tsx, describe 「[Row kebab offers Edit/Delete on records the server refuses — the list ANDs only the OBJECT-level grant, the detail header also folds the RECORD-level one #4296] one batched call per (object, operation) per page」, case 「paginates under the server's cap instead of sending a request it would refuse」: Unable to find an element with the text: Row 249. The file is byte-identical between base and head. Its fixture hands ObjectGrid 250 inline rows (data: { provider: 'value' }), one untyped column { field: 'name', label: 'Name' }, and an async getObjectSchema that resolves with fields. On base Row 249 was in the FIRST paint; on head the first paint is 250 masks, and Row 249 appears only after the schema read lands and the table re-paints 250 rows through new column objects — all inside RTL's default 1000 ms waitFor budget (no asyncUtilTimeout is set anywhere in the tree; testTimeout 15 s does not apply to waitFor). The 50-row sibling in the same describe passed in the same shard. This is the AGENTS.md §9 测试纪律 class — a step whose cost the change made unbounded, counted inside a bounded window — and it is exactly the class the branch repaired on eight other pins while leaving this ninth untouched, so the PR body's 「plugin-grid: Test Files 163 passed」 is a local, unloaded reading that the head's own CI contradicts. What would be right: give this pin the same treatment as the eight — key its readiness on the settled draw, or remove the window in the fixture (the test renders ‹ObjectGrid> directly, so it can pass objectFields with the same fields, or author type: 'text' on name); ⛔ not a timeout bump and ⛔ not heavyDomTests (both forbidden by §9). The head cannot be accepted while its required Test context is red.
  • The dev's Acceptance-notes items, checked at source: (a) RelatedList's gallery (mobile) and type: 'list' (data-list) bodies hand paginatedData to another renderer and never pass through tableColumns, so the window is unmeasured there; they are outside this card's named site (normalizeColumn → the data-table seam) and belong to those renderers' own cards; stated, not silent. (b) The prefix compound cell prints row[prefixConfig.field] as a badge or String(prefixValue) whatever that field's type or window: pre-existing since objectui#10583, an authored presentation; for the filing gate. (c) An authored type: 'text' over a secret field draws its text in the window as after it: the objectui#10583 contract, consistent.
  • ARRAY-shaped fields: data-objectstack's applyFieldWidgetOverrides and ListView's own reader tolerate an array catalogue, but the grid's objectFields?.[name] lookups and !objectFields do not — an array reads as "known" with every field missing, i.e. heuristic inference. The same lookups already behave so on the grid's own objectSchema.fields, so this is not introduced here, but the new prop's Readonly‹Record‹string, unknown>> type is not enforced at ListView's any handoff; a one-line Array.isArray guard would close it. Filing-gate material.
  • A definition that lands with no fields member keeps the grid withheld for good (fail-closed direction, a cost); a host other than ListView that hands rows without objectFields flashes masks until the grid's own read lands (the console's ObjectView renders through ListView, so it is covered).
  • The PR body counts the grid pin as 「8 cases」; the file has 7. Prose only.
  • Draft: yes. Closing keyword: Fixes #10657 (PR 2). Changeset Claim Re-read names 34 pending changesets by file; the five that this change makes false are the 10583 and PR 1 10657 ones, each addressed (①H). mergeable: true, mergeable_state: behind; overlap with PR objectui#10278's page-size hunks not re-measured here beyond noting this change's ObjectGrid.tsx hunks are the column-type, grouping, export, stamp, record-panel and card regions.

Implemented-by: claude/issue-10657-masked-first-paint
Reviewed-by: session_01MCg3z5cGrV51xEddpGcxEY

VERDICT: FAIL

…he schema window (objectui#10657)

`rowRecordCrudVerdict`'s 250-row case (CI `Test (shard 3/8)`) waited for the
raw text of an untyped column. Since this card that column is withheld
(drawn as the mask) until the grid's own schema read lands, so the wait
covered a re-render of every row and outran waitFor's budget. Measured by
a runtime sweep over the 264 test files that render these producers with a
schema read: five cases wait across a withheld window on 50 or more rows.
Their readiness now counts the rows on screen (rowRecordCrudVerdict x2,
serverPagination x2), or, for the ListView paging shape, hands the grid the
field catalogue that host now passes (groupedPartialDisclosure-7189). No
assertion of the tests' subjects changes; no timeout is raised.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3078.7 KB 3104.5 KB
Main entry chunk (gzip) 148.3 KB 350 KB
Entry file index-BmjgoW_0.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.15KB
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) 556.67KB 133.27KB
core (index.js) 9.92KB 3.93KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 224.97KB 62.47KB
fields (index.js) 260.06KB 65.99KB
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.67KB 14.70KB
plugin-charts (index.js) 83.58KB 22.75KB
plugin-chatbot (index.js) 198.40KB 47.22KB
plugin-dashboard (index.js) 134.64KB 35.75KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 233.12KB 61.68KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 158.35KB 40.47KB
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) 43.55KB 11.99KB
plugin-timeline (index.js) 30.91KB 9.05KB
plugin-tree (index.js) 10.64KB 3.75KB
plugin-view (index.js) 87.83KB 22.01KB
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.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 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.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: 108/108 CONTRACT_REVIEW_TIER
Head-sha: da23a3342c3f58117ef86e686c2622e1e5c180b2

Read: the previous at-tier record on 9f9b8b79df7e075f7d809221fb8d5e0cb9c640e9 (PR comment 5858139752, VERDICT FAIL on one defect); the card thread objectui#10657 (10 comments; of the two os-dev-reports 5858002351 and 5858310306 only their open_questions and deviations lists, both open_questions empty); PR objectui#10819 body, file list (34 files, +1335 −124) and the changeset-claim-re-read bot comment 5857981794 as re-edited at 17:52:26Z (35 pending changesets, per the coordinator's added input); the head fetched into refs/review/pr-10819-re2, the delta git diff 9f9b8b7..da23a33 (3 files, +26 −6), git log from the merge-base, git merge-base --is-ancestor; the three edited test files in full at the head and their diff against the old head and the base; ObjectGrid.tsx at the head (the objectFields / objectTypesPending derivation at 1651–1665, the inline data effect at 1978–1990, the inline schema effect at 1996–2023, the grid-fetch effect at 2041–2515, the loading early return at 3869, the showAddRow handoff at 5063); maskedColumn.ts (isWithheldGridColumn); data-table.tsx at packages/components/src/renderers/complex/ (paginatedData at 1246, the TableBody at 2230–2832, showAddRow default at 755); ListView.tsx's base→head diff and its SchemaRenderer render site at 5029–5083; the grid maskedFirstPaint-10657 pin (renderHostFed, the HELD/REJECTED group-label cases, the SETTLED and HOST CATALOGUE cases); vitest.config.mts (testTimeout, heavyDomTests); objectui AGENTS.md §9 (测试纪律); .changeset/6286-explain-batch-cap-from-spec.md in full; the head's check-runs polled three times (17:57:53Z, 18:03:35Z, 18:04:38Z). No worktree was cut and nothing was run: the head's own Test (shard 3/8) is the runtime evidence.

① Derived judgments

The FAIL defect — FIXED at its root. The old head's one defect was Test (shard 3/8) red on rowRecordCrudVerdict.test.tsx 「paginates under the server's cap」: its readiness waited on Row 249's raw text, which the change withholds (mask) until the grid's own schema read lands and 250 rows re-paint, inside RTL's default 1000 ms waitFor budget. At da23a33 that case waits on container.querySelectorAll('tbody tr') having rows.length entries (line 472), a signal true on the FIRST table paint, before the schema lands. Test (shard 3/8) is green on the head (completed 18:00:41Z), and so are the other seven shards and the aggregate Test (18:03:47Z). The fix moves the unbounded step (schema read + 250-row re-render) out of the bounded window instead of widening the window: exactly the §9 repair (「先找竞态,别调超时」), and ⛔ neither forbidden route was taken (no timeout bump, no heavyDomTests entry).

(a) The five cases, readiness before → after, first-paint truth, and what each still gates.

  1. rowRecordCrudVerdict 「a 50-row page issues 2 explain calls, not 100」 (line 429). Before: getByText('Row 49'). After: tbody tr count === rows.length (50). Truth on the first paint: the fixture hands data: { provider: 'value', items } with pageSize: 500, so dataConfig.provider === 'value'; data starts [] and loading true, the loading early return draws the spinner (no table), then the inline effect sets the 50 rows and loading false in one commit; the table's paginatedData is sortedData.slice(0, 500) = 50 rows, one TableRow per row, the empty row only when paginatedData.length === 0, and showAddRow is !!operations?.create && … with no operations authored, so tbody holds exactly 50 tr on that paint, each name cell the mask (objectTypesPending true: object name, getObjectSchema a function, no objectFields). Still gates the subject: settle(2) waits for server.calls.length === 2 and awaits the responses; the explain batch is issued from the rows on screen, not from the schema, and the assertions (calls.length === 2, operations ['delete','update'], recordId undefined, recordIds === rows.map(id)) are byte-identical to the base. RIGHT.
  2. rowRecordCrudVerdict 「paginates under the server's cap」 (line 452). Before: getByText('Row 249'). After: tbody tr count === rows.length (cap + min(50, cap) = 250). Same first-paint argument as case 1; pageSize: 500 covers 250. Still gates: settle(4), then calls.length === 4, each chunk at most cap and non-empty (toBeLessThanOrEqual(cap), toBeGreaterThan(0)), and the asked ids per operation equal the fixture's — all unchanged. The cap fixture (const cap = EXPLAIN_BATCH_MAX_RECORD_IDS, line 464) and bound (line 477) are untouched by the delta. RIGHT.
  3. serverPagination 「shows the page count from the external rowCount」 (line 196). Before: '63' in textContent, then getByText('Row 0'). After: '63', then tbody tr count === PAGE_SIZE (50). The host-driven fixture passes data={window} (50 rows) with manualPagination, rowCount, page, pageSize, onPageChange, so paginatedData = sortedData (no client slice) = 50 tr, on the same commit that draws the footer's 63. Here the readiness is also the case's second assertion; it went from 「one row's raw text is drawn」 to 「the host's whole window is drawn」, which is the Form-view FormField.visibleOn (CEL) is never evaluated — conditional fields always render #2212 claim (the window is rendered as the page) and no longer depends on the schema. RIGHT.
  4. serverPagination 「calls the external onPageChange when the footer turns the page」 (line 207). Before: getByText('Row 0'). After: tbody tr count === PAGE_SIZE. First-paint truth as in case 3. Still gates: the enabled nav buttons exist on that paint (the footer is drawn with the rows), the last-page click, onPageChange called with a page above 1 (toBeGreaterThan(1)) — unchanged. RIGHT.
  5. groupedPartialDisclosure-7189 「reads the total a HOST supplies (the ListView paging shape)」 (line 336). Before: the fixture handed data, manualPagination, rowCount: 186, page, pageSize: 100, onPageChange and a dataSource whose getObjectSchema resolves async; readiness notice() (grouping-partial-notice). After: the same, plus objectFields: OBJECT_FIELDS; readiness unchanged. Why the readiness is the subject here: the grouping key business_unit is authored with no column type, so without a catalogue it is withheld and the grouping entry REFUSED (maskedGroupingSignature.refused, the previous record's B); with no grouping there is no notice until the schema lands and 100 rows re-group. With objectFields handed, objectTypesPending is false from the first render (objectFields = objectSchema?.fields ?? hostObjectFields), the key groups on the first table paint, and the notice is on that paint. Still gates: textContent contains 186 (the host's rowCount) and partialMarkers().length === groupRows().length — unchanged. When the grid's own read lands with the same fields, the signature does not change. RIGHT.

Across the five: no assertion of any subject changed (call counts, cap chunking, asked ids, notice text 186, partial markers, onPageChange argument, the 63 page count); the three files contain no timeout, asyncUtilTimeout, .skip, .only, .todo, retry or configure( token; vitest.config.mts is byte-identical between the heads (testTimeout: 15000, and heavyDomTests names none of the three); git grep asyncUtilTimeout on the head tree finds nothing. Nothing skipped, retried or quarantined.

(b) The fixture edit in groupedPartialDisclosure-7189 — is 「the ListView paging shape」 true at the head? Yes. ListView.tsx at the head renders ‹SchemaRenderer schema={viewComponentSchema} {...props} {...{ data }} {...(viewComponentSchema.type === 'object-grid' && objectDef?.fields ? { objectFields: objectDef.fields } : {})} loading … {...(paginate && serverTotal != null ? { manualPagination: true, rowCount: serverTotal, page: serverPage, pageSize: effectivePageSize, onPageChange, onPageSizeChange, sort, onSortChange, findParams } : {})}› (lines 5029–5083): the catalogue is spread beside data in the same props bag as the paging controls, on the grid path only, and the data fetch waits for objectDefLoaded (line 2190), so on a successful definition read the rows never arrive without the catalogue. The fixture's ds.getObjectSchema resolves with OBJECT_FIELDS, i.e. the successful-read shape, and now carries what ListView carries. The window the edit removes from this fixture (host rows, no catalogue, grouping key withheld → refused) is not this test's subject (#7189 is the partial-disclosure notice reading the host's total) and stays pinned where it belongs: the grid maskedFirstPaint-10657 pin's renderHostFed (data={ROWS}, no objectFields) HELD 「no group is labelled by an untyped column」 (line 174, .group-label count 0 beside the typed name control that labels) and REJECTED 「labels no group after the failed read」 (line 206); that file is byte-identical between the heads. RIGHT. One coverage note for ③: the refused→grouped transition once the schema lands on the host-rows shape, which the old fixture exercised by accident, is not pinned by name (the pin's SETTLED case reads cells, not group labels).

(c) The class boundary. The dev fixed every case whose withheld window covers 50 or more rows and left the ≤9-row cases (155, per the deviation list). Tested against the evidence rather than trusted: within the three files, the cases still waiting on a withheld cell's raw text inside the window are the 13 two-row cases of rowRecordCrudVerdict (getByText(OTHER_ROW.name) etc. at lines 275, 285, 295, 308, 329, 373, 381, 389, 397, 410, 491, 508, 517), every one of which then reads the row by that label through rowKebab and so needs the settled paint anyway; the three Row 0 waits in serverPagination's server-side describe (lines 95, 109, 132) and the Northgate Operations #0 wait in 7189 (line 316) are on the grid-fetch path, where loadSchemaAndData awaits getObjectSchema before find and setObjectSchema lands before setData (2041–2499), so there is no window and they are not in the class. The empirical ratio: at the old head Test (shard 3/8) ran the whole file and only the 250-row case failed; the 50-row sibling and the 13 two-row cases passed, and every other shard was green, so the ≤9-row class fit CI's budget with the margin between a 2-row and a 50-row re-render. At the new head all eight shards are green. §9's rule is how to fix a race, not a mandate to rewrite green tests; leaving 155 passing cases whose window is a resolved mock promise plus a single-digit-row re-render is proportionate, and I found no concrete, evidenced flake. It is a latent class all the same (a fixture that grows, or a costlier settled re-render, surfaces it), so it is a boundary flag, not a defect. The dev's stated probe range (83–1631 ms) is not evidence I could verify; the shards are.

(d) Every file not in the delta. git diff --stat 9f9b8b7 da23a33 lists exactly three paths (groupedPartialDisclosure-7189.test.tsx +6, rowRecordCrudVerdict.test.tsx +16 −4, serverPagination.test.tsx +10 −2; 26 insertions, 6 deletions), git diff --stat with those three excluded is empty, and git diff-tree -r --name-only between the heads counts 3. The head is the old head plus exactly one commit, da23a3342c 「test(plugin-grid): readiness no longer reads a withheld cell inside the schema window」, single parent 9f9b8b79df, git merge-base --is-ancestor 9f9b8b7 da23a33 true (no force-push), no merge commit, merge-base with origin/main unchanged at c907a9c8708f300763509d9a6eda412064a8c87e. All three delta files were byte-identical to the base at the old head. So every judgment the previous record made about the other 31 files carries forward unchanged, and I say so: ①A (fail closed on every path), ①B (the grouping refusal), ①C (the third argument), ①D (objectFields runtime-only, the batch #70 ruling, the published props member), ①E (the MaskedCellRenderer export), ①F (the eight edited pins), ①G (the four new pins, 7+6+6+2), ①H (the changesets and prose), and ② — all PASS-level, all on byte-identical sources. Pending changesets naming a delta file (the coordinator's added input): of the 35 bodies the re-edited claim re-read lists, exactly one names any of the three delta files, .changeset/6286-explain-batch-cap-from-spec.md → rowRecordCrudVerdict.test.tsx; a grep of all 2185 .changeset/*.md at the head finds no other. Its one sentence about that file — 「The pre-existing cap assertion in rowRecordCrudVerdict.test.tsx now derives its fixture and its bound from the same export instead of re-typing 200」 — is TRUE at the head: line 40 imports EXPLAIN_BATCH_MAX_RECORD_IDS from @objectstack/spec/security, line 464 const cap = EXPLAIN_BATCH_MAX_RECORD_IDS, line 465 sizes the fixture bigPage(cap + Math.min(50, cap)), line 477 bounds each chunk toBeLessThanOrEqual(cap); the delta touched lines 467–472 (the readiness) only. The rest of that body is about useRecordCrudVerdicts and batchCap.test.tsx, neither touched. The file itself is byte-identical between the heads. Nothing pending is made false by the delta; the five pending bodies the change supersedes (①H) are unchanged and remain addressed.

② Semver level

Unchanged from the previous record, on byte-identical package sources: @object-ui/fields minor (new export), @object-ui/plugin-grid minor (new optional member on the published ObjectGridComponentProps), plugin-list / plugin-detail / plugin-dashboard / types patch, no major. The delta is test-only in plugin-grid, which already carries .changeset/10657-plugin-grid-withheld-first-paint.md; Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check and Changeset Overwrite Report are green on the head.

③ Boundary flags

  • CI state read. Poll 1 at 17:57:53Z: 42 check-runs; every reported check green (Test (dist pins), Build & E2E, Build Docs, Bundle Analysis, the four changeset gates, Doc Snippet Type Check, README Export Check, Governed Surface Queue Guard, Line Citation Gate, Control Byte Scan, and the rest), with Lint, Type Check, Spec Main Shape Gate and Test (shard 1/8) … Test (shard 8/8) still in_progress, and the coverage shards and dependabot skipped. Poll 2 at 18:03:35Z: 43 check-runs; all eight Test shards success (3/8 at 18:00:41Z), Lint, Type Check and Spec Main Shape Gate success, the aggregate Test queued. Poll 3 at 18:04:38Z: 40 success, 3 skipped, nothing running; the aggregate Test success at 18:03:47Z. Nothing red at any poll. The PR read mergeable_state: behind at 17:57Z and clean at 18:04Z (mergeable: true both times); draft: yes.
  • The ≤9-row class (①c). 155 cases, 13 of them in rowRecordCrudVerdict, still take a withheld cell's raw text as readiness inside the window the change introduced; green on CI at both heads, proportionate to leave, latent all the same. If the maintainer reads §9's class as every raw-value wait inside any window, they are a follow-up, ⛔ never a timeout bump.
  • An unpinned transition. The old 7189 fixture incidentally exercised 「refused while withheld, then grouped once the object's types land」 on the host-rows shape; the maskedFirstPaint-10657 grid pin pins HELD and REJECTED group labels and a SETTLED cell draw, not a SETTLED group label. Not this delta's subject; filing-gate material.
  • PR body staleness (f). The delta makes these sentences stale: 「head 9f9b8b7」 (now da23a33); 「Existing pins edited: 8 files (9 case bodies, one shared wait helper, one fixture)」 (now 11 files, 14 case bodies, two fixtures; the three delta files are absent from that list and from the file surface); 「check-governed-queue-guard --test on the 31 paths」 (34); 「plugin-grid: Test Files 163 passed (163)」 at e8503d3 (a local reading the old head's CI contradicted; the head's shards are the current reading); 「maskedFirstPaint-10657 (8 cases)」 (7, carried from the previous record). None misstates behaviour: the delta is test-only. Prose only.
  • Carried from the previous record, unchanged on byte-identical sources: RelatedList's gallery and type: 'list' bodies outside the tableColumns seam; the prefix compound cell; an authored type: 'text' over a secret field; an ARRAY-shaped fields reading as 「known」 at ListView's any handoff (a one-line Array.isArray guard would close it); a definition with no fields member keeping the grid withheld for good; a host other than ListView flashing masks until its own read lands.

Implemented-by: claude/issue-10657-masked-first-paint
Reviewed-by: session_01MCg3z5cGrV51xEddpGcxEY

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 18:11
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit 12809a5 Sep 27, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10657-masked-first-paint branch September 27, 2026 18:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants