Repository navigation
fix(components,plugin-grid,types): withhold a masked grid field's raw value from copy, tooltip, inline edit, the client export, the mobile card and group headers - #10643
Conversation
…alue to nobody Ctrl+C / Cmd+C on a focused password or secret cell wrote String(row[accessorKey]) to the clipboard while the cell drew the mask, and the cell wrapper carried the raw value as its title tooltip. - types: TableColumn declares `masked?: boolean`, mirrored as z.boolean() on TableColumnSchema and tombstoned on the static table column (lockstep rule). - plugin-grid: the emit seam stamps `masked: true` from isMaskedFieldType() (via isMaskedGridColumn), as the narrow-only union of the column's type and the object-declared type, before the type fold erases it. - components: data-table obeys the flag - the keyboard copy writes nothing, no title tooltip is drawn, and the CSV export omits the column. Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude <noreply@anthropic.com>
…for the masked-cell refusal Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude <noreply@anthropic.com>
|
changeset-claim-re-read
|
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: ① Derived judgmentsSecurity completeness on this surface (data-table.tsx and ObjectGrid.tsx at head, line numbers from the head blobs):
Stamp site: Declared key: Ctrl+C with Pins: re-run in a scratch worktree at head (offline install from the pnpm store), removed afterwards. Head: 13 passed (13). Base leg (4 implementation files at Dev findings: (1) real — neither ② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL What stops landing: inline edit on a masked column still shows and (via the native input copy) hands out the raw value on a path Generated by Claude Code |
…e edit, the grid's client export and the mobile card title Round 2 of the masked-cell refusal, from the contract review: - components: data-table's startEdit refuses a masked column (Enter, click, double-click), and a masked cell no longer reads as editable (no edit cursor, the row click is not swallowed). - plugin-grid: the client export fallback (CSV and JSON) leaves every masked field out, asked per key through isMaskedGridColumn; the mobile card draws a masked title through its cell and never classifies a masked field by name into the raw amount / stage / date / percent rows. - docs, JSDoc, zod describe, README, changeset: list exactly the paths withheld, and state what is not covered (client search and sort, the other column producers). Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
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
Size Limits
|
…e it edits Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: ① Derived judgmentsRound-1 requirements at head (line numbers from the head blobs):
Measured (scratch worktree at head, offline install, one lock hold: waited 401s, held 131s; worktree removed;
Other paths, from source, unmeasured: the server-streamed export (ObjectGrid 3480-3505) sends the masked columns in Open question: A (withhold-only) is right — a fields-free table must not become a second mask authority; the "draws a column with no ② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL What stops landing is wording plus one unnamed path, not the round-1 code: the four required paths are closed, pinned and ablation-proven. (1) The title and changeset lead claim "export" while the server-streamed export is unchanged and the client JSON still writes an expanded related record's credential (measured): narrow both to "the client export" and complete "Not covered" with the three sentences above. (2) A grid grouped by a masked field draws the raw value as its group label (measured, Generated by Claude Code |
…uncovered paths exactly Round 3 of the masked-cell refusal, from the security re-review: - plugin-grid: a grouping entry on a masked field (isMaskedGridColumn over the view column type and the object-declared type) is dropped, and a console warning names it; the other grouping levels still apply. A masked group label alone would still bucket the records that share a credential, in its raw order. - The expanded-lookup JSON export stays open: the grid holds only its own object's schema, and the related object's field types are not in hand without a new fetch. It is stated as not covered. - docs, README, JSDoc, zod describe, changeset: add the server-streamed export, the expanded lookup record and the auto-width length to "not covered"; cite objectui#10657 and objectui#10658. Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude <noreply@anthropic.com>
ℹ️ Console Performance Budget — not measuredThis run did not produce a console bundle to measure, so there is no pass/fail verdict for the performance budget. This is not a budget violation. Nothing was measured — the numbers a real violation would carry are simply absent.
See the workflow run for details. No package size report: it is only generated from a complete package build, so a partial one is never shown. |
…t the type `unmaskedGroupingFields` is now always the filtered usable list, and the unmasked or ungrouped path hands `useGroupedData` the authored config itself, so the config passed never carries `fields: undefined`. Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
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
Size Limits
|
Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
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
Size Limits
|
…ead of an any cast Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: ① Derived judgmentsGrouping refusal, every route (source at head): Measured (scratch worktree at head, offline install, one lock hold: waited 230s, held 55s; worktree removed; FAIL — an unnamed, measured window on the host-fetched path. Every guard this PR added reads objectui#10625 date editors (merged in Other surfaces: data-table has no filter menu of distinct values, no body-cell context menu, no row copy, no selection export; Expanded lookup, "stated, not closed": the reason is true. ② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL What stops landing: one measured, unnamed path in this PR's surface plus wording. (1) On the host-fetched path (every Generated by Claude Code |
…dow; scope the schema-dependent claims Round 4 (wording only, no behaviour change): - Add the host-fetched-path sentence (objectui#NEWCARD placeholder) to the changeset, the docs "Masked columns" section, the plugin-grid README, the TableColumn.masked JSDoc, the isMaskedGridColumn header and the grouping comment. - Scope the schema-dependent sentences with "once the object schema has loaded". - Cite objectui#10658 as folded into objectui#10657. Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude <noreply@anthropic.com>
…fetched schema window Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: ① Derived judgmentsRound 4 is wording only (measured). The window sentence: true on the pending arm, false on the failed arm. Source at head: the inline effect ( Unconditional survivor, same class as round 3. Title ( The three wording fixes. objectui#10658: closed Body. The only ② Semver levelUnchanged and right: ③ Boundary flags
Implemented-by: VERDICT: FAIL Wording only; the code, pins, gates, merges and CI all hold. Two things to change before landing: (1) "until it settles" implies the exposure ends, but a failed schema read is swallowed and leaves the column raw for the life of the grid; use the replacement sentences above in the changeset, docs, README, JSDoc, Generated by Claude Code |
…t; scope the text-over-secret union Round 5 (wording only, no behaviour change): - "until it settles" becomes "until it arrives, and for good if that read fails": the grid swallows a failed schema read and keeps its heuristic column types (objectui#10706). - The object-declared half of the narrow-only union applies once the object schema has loaded; say so wherever the text-over-secret case is stated. Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
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
Size Limits
|
… objectui#10706 Round 6: citation only, no behaviour change. Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgmentsRound 5 is wording only (measured). Round 4's replacements: all applied, verbatim. Long form at changeset 61-64, README 398-402 and body 26; inline form at docs 171-173 and JSDoc 738-740; The new sentences are true at head. (1) "swallows the failure and keeps its heuristic column types": the catch at FAIL, wording, same class as round 3's item (2): every window citation points at a closed duplicate. objectui#10706 was closed Body. The only closing keyword is line 1; the only ② Semver levelUnchanged and right: ③ Boundary flags
Implemented-by: VERDICT: FAIL Wording only; both round-4 replacements are applied verbatim, the new sentences are true at head, the code is untouched, the pins and gates pass, the merge is clean and CI is green so far. One thing to change before landing: objectui#10706 was folded into objectui#10657 after the round-5 commit, so replace Generated by Claude Code |
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: ① Derived judgmentsRound 6 is exactly the six one-line edits (measured). Both new citations are true. objectui#10657: open, No bare citation survives. Frontmatter unchanged. Changeset lines 1-5 md5 Cosmetic, not blocking: the spliced lines now run 117 (changeset 64), 137 (docs 173) and 143 (JSDoc 740) columns past their files' wrap width, as round 5 noted for their predecessors. ② Semver levelUnchanged and right: Title ( The three changed body lines. Line 26: the prescribed sentence verbatim, identical to changeset 64 and README 402. Line 103: "Rounds 4 to 6 ( FAIL, wording, one unchanged body line is false: line 136. "The only open PR on these files is the draft objectui#10278." Of the 14 open PRs, two touch the 12 files: the draft objectui#10278 ( ③ Boundary flags
Implemented-by: VERDICT: FAIL Wording only, and in the body alone: the round-6 delta is exactly the six one-line citation edits, both new citations are true, no bare citation survives anywhere in the PR, both frontmatters are unchanged, semver is unchanged, the title is still true of the head, the merge is clean against live main and against objectui#10710, and CI is green so far. One thing to change before landing: body line 136 says the draft objectui#10278 is the only open PR on these files, but the draft objectui#10710 has edited Generated by Claude Code |
Contract reviewServed-tier: ① Derived judgmentsHead unchanged. REST The landing text is exactly the two prescribed edits (measured). Line 136, re-measured now, sentence by sentence.
Line 137. Nothing else contradicts. Line 135 re-measured: objectui#10278 is still a draft at ② Semver levelUnchanged: the head has not moved since round 6, so the changeset ( ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…nderer's fetch keys on the query it issues, and the map reads once, expanded (objectui#10664) (objectstack-ai#10688) Fixes objectstack-ai#10664 Clause-②: no Each data renderer's fetch effect now keys on the inputs of the query it issues. The map's query waits for a settled definition, so a mount reads once and that read is expanded. The repeater and four other census rows re-read when a query input they send changes. None of them re-reads when an equal value arrives in a new object. Folds objectui#10665 (the repeater row), per the triage. Scope, per the rulings: fetch gating and dependency lists only. No view's `error` lifecycle is touched; that is objectui#10663, dispatched in parallel. Every new key is a string compared by value (AGENTS.md objectstack-ai#10). ## What changed, per renderer | Renderer (package) | Mismatch on the base | Change | |:--|:--|:--| | `object-map` / `ObjectMap` (plugin-map) | **Over-key.** The definition sat in a local `useState` fed by its own effect and was listed in the fetch effect's dependencies. Measured: 2 `find` per mount, first without `$expand`; switching the bound object sent the new object's query with the previous object's expansion (`[['owner'], ['manager']]` for `depot`). | The definition comes from `useSettledSchema`, keyed on `recordSourceObjectName`. The object branch waits on `ready` (the objectui#7895 / objectui#7903 gate) and holds the loading placeholder while it waits. Host rows and inline `value` sets are not held. | | `element:repeater` / `RepeaterRenderer` (components) | **Under-key.** `properties.sort` goes onto `$orderby`; the dependency list had no sort. | `sortKey`, by content, the way `filterKey` keys the filter. | | `element:record_picker` (components) | **Under-key**, same shape: `sort` (flat, or the `dataSource` binding's) goes onto `$orderby`, not keyed. | `sortKey`, by content. | | `object-data-table` / `ObjectDataTable` (plugin-dashboard) | **Over-key**, the map's shape. Measured: expand sets `[null, ['account', 'owner_dept']]` per mount. **Under-key**: the expansion reads `schema.columns`, which was not keyed. | `useSettledSchema` gate on the fetching branch. `lookupExpandKey` (the expansion the columns produce, by content), so a relabelled column does not re-read. | | Dashboard filter bar options (plugin-dashboard) | **Under-key.** `optionsFrom.filter` goes onto both option reads (`runtimeFilter`, `$filter`), not keyed. | `optionsFilterKey`, by content. | | `object-view` / `ObjectView` non-grid read (plugin-view) | **Under-key.** The read falls back to `schema.table.sort` for `$orderby`; the named-view and active-view sort sources are keyed, this one was not. | `tableSortKey`, by content. | Fixture re-judged: the `expandFor` helper in `ObjectDataTable.expandFls-7230.test.tsx` waited for a second `find`, so it pinned the double read. It now reads the single post-definition `find` and asserts there is exactly one, so an ungated regression reddens instead of reading an unexpanded first call. The header of `ObjectMap.invalidationRefetch-10623.test.tsx` said the map issues two `find` calls on mount. It now points at the new pin. ## Census: every registered data renderer's fetch effect Population: renderers registered through `ComponentRegistry` in `packages/components` and `packages/plugin-*` whose `useEffect` issues a record read (`find`, `aggregate`, `queryDataset`). Enumerated by a scratch script over `origin/main` `4758b33`, comparing what each query reads with the effect's dependency list. The script is not committed; this table is a reading taken once, not a live count (AGENTS.md objectstack-ai#9). Field widgets (`LookupField` and siblings), app-shell pages and the `useViewData` hook (no in-tree renderer consumes it) are outside the population. | Renderer | Reading | Action | |:--|:--|:--| | the six rows above | mismatch | changed here | | `object-grid` / `ObjectGrid` (plugin-grid) | **Under-key.** The `$select` harvest reads `conditionalFormatting`, `rowActionDefs` and `bulkActionDefs` (predicate operands), and `$searchFields` reads `searchableFields`. None is keyed. Measured by an uncommitted probe: adding a `conditionalFormatting` rule referencing `industry` to a mounted grid issued no read (`$select` stayed `['id','name']`); a fresh mount with the rule selected `['id','name','industry']`. | **Not edited.** The file is on seat 2's live claims: objectui#9853 (PR objectstack-ai#10278) and objectui#10583 (PR objectstack-ai#10643). Handed to the seat. | | `list-view` / `ListView` (plugin-list) | **Under-key**, same harvest (`conditionalFormatting`, `rowActionDefs`, `bulkActionDefs`). Same probe, same reading. | **Not edited.** The file is on objectui#9853's file surface (PR objectstack-ai#10278, seat 2). Handed to the seat. | | `RelatedList` (plugin-detail) | The same over-key shape: `expandKey`, `selectKey` and the arity flag move when the child definition lands, so a child with expandable columns or a multi-valued relationship reads twice. | **Not edited.** Ungated by a recorded decision, pinned as `DECLARED COST` in `RelatedList.multiValueParentScope-7299.test.tsx`. Raised as an open question in the report, not overridden here. | | `ObjectTimeline`, `ObjectGallery`, `ObjectKanban`, `ObjectCalendar`, `ObjectGantt` (main read and quick-filter options), `ObjectTree` | settled-schema gate; content keys; match | none | | `ObjectChart`, `ObjectMetricWidget` | match by content keys; the effect keys on a `useCallback` identity (`fetchData`, `fetchMetric`) | none (Acceptance notes) | | `ObjectPivotTable`, `element:number`, tab-count probe (`containers.tsx`), `record-activity`, `record-history`, reference rail, `DatasetWidget`, `DatasetReportRenderer`, `LineItemsPanel` | match | none | ## Evidence Every measured leg ran through `SchemaRenderer` and each package's own registration, or rendered the component directly where a sibling test already does. Vitest resolves every `@object-ui/*` specifier to its package's `src` (root `vitest.config.mts` alias), so no leg's resolution path goes through `dist`, and no rebuild leg applies. **On the base, before any source change** (pins written first): - `ObjectMap.fetchGate-10664`: exit 1, 4 failed / 3 passed. The 4 are the SUBJECT cases; the green 3 are the two SETTLES cases and the CONTROL. - `data-list.sortKey-10664` + `record-picker.sortKey-10664`: exit 1, 3 failed / 2 passed (3 SUBJECT red, 2 CONTROL green). - `ObjectDataTable.fetchGate-10664`: exit 1, 3 failed / 3 passed. - `DashboardFilterBar.optionsFilterKey-10664`: exit 1, 2 failed / 1 passed. - `ObjectView.tableSortKey-10664`: exit 1, 1 failed / 1 passed. **Reverse verification**, fix committed first (`46c5432`). Each of the six source files was set to its `4758b33` blob; the mutation was proven on disk (blob equals base, not head; each new anchor counted 0). The seven pin files then ran red, exit 1, 20 failed / 10 passed. The 20 are every SUBJECT case plus the seven FLS cases, which now assert one read. The 10 are every CONTROL and SETTLES case. Restore was `git checkout HEAD -- PATH` under a `trap … EXIT INT TERM`, proven by each blob equalling HEAD and an empty `git diff HEAD`. The restored run exited 0, 30 / 30. **Targeted ablations**, both through `ablation-replace.mjs`, which confirms the anchor hit and verifies the restore: - `lookupExpandKey` removed from the data table's dependency list, gate kept: exit 1, only the column-change SUBJECT red (1 failed / 5 passed). This isolates the columns under-key from the double read. - `objectSchema` removed from the map's dependency list, gate kept: exit 1, only the CONTROL red. `$expand` stayed `['owner']` where `['owner','region']` was due. The control can fail for the reason it exists. ## Gates The four package suites, the type-check and eslint ran on `2d48eb5`, after merging `origin/main` `526fc11`. The branch then merged `origin/main` `4df0f3d` to give `6451600`. That merge touches only `app-shell` and `plugin-charts`, no file in the four packages here. On `6451600` the seven pin files and the ratchet gates were re-run. The components suite's full run is from the `f133b5c` merge head. On `2d48eb5`, where the merge brought only the `RefreshIndicator` change into `components`, the component files this PR edits and the `refresh-indicator` tests were re-run. That is a declared narrowing; CI runs the full farm. - `6451600`, the seven pin files: exit 0, 7 / 7 files, 30 / 30 tests. - `6451600`, exit 0: `check-changeset-presence` (4 changesets for 4 released packages), `check-changeset-overwrite`, `check:control-bytes`, `check:test-path-roots`, `check:vi-mock-specifiers`, `check:vi-mock-inherit`, `check:vi-mock-override-shape`, `check:new-line-citations` (0 new), `check:changeset-claims`, `check:pending-changeset-literals`, `check:phantom-deps`. - `2d48eb5`, `pnpm exec vitest run --maxWorkers=2 packages/plugin-dashboard/`: exit 0, 135 / 135 files, 1268 / 1268 tests. - `2d48eb5`, `… packages/plugin-map/ packages/plugin-view/` plus the 12 `data-list` / `record-picker` / `refresh-indicator` test files in `components`: exit 0, 94 / 94 files, 743 / 743 tests. - `f133b5c`, `… packages/components/`: exit 0, 309 files passed and 1 skipped; 3032 tests passed and 17 skipped. - `f133b5c`, the consumer sample: exit 0, 10 / 10 files, 900 / 900 tests. The sample is the schema-catalog render tests, `InterfaceListPage.mapConfig`, `widget-dom-leak-sweep`, `public-block-binding-reach`, three `ListView.map*` and two app-shell drill tests. - `2d48eb5`, `pnpm turbo run type-check --filter @object-ui/plugin-map --filter @object-ui/components --filter @object-ui/plugin-dashboard --filter @object-ui/plugin-view --concurrency=2`: exit 0, 22 / 22 tasks. - `2d48eb5`, `pnpm exec eslint` on the 14 changed `.ts`/`.tsx` files, plain form (what each package's `lint` runs): exit 0, 0 errors. The warnings are the files' existing `no-explicit-any` and React Compiler advisories. Each new `useMemo` key draws the same `preserve-manual-memoization` advisory the `filterKey` beside it already draws. - `2d48eb5`, also exit 0: `check-changeset-fixed`, `check-changeset-no-major`, `check-type-check-coverage`, `check:element-data-source-declaration`, `check:handler-key-reads`, `check:self-import`, `check:unreferenced-sources`, and `check-governed-queue-guard --test` (NOT GOVERNED, 18 paths). - `check:changeset-claims` (report-only): it names ten pending changesets that cite files this change edits. Each paragraph was re-read, and none describes the fetch gating or dependency lists changed here. Changesets: one `patch` per touched package (`plugin-map`, `components`, `plugin-dashboard`, `plugin-view`), each naming its renderers and citing the card. ## Acceptance notes - Latent AGENTS.md objectstack-ai#10 identity keys, recorded and not changed here. A discard does not happen in this tree on its own, so none is reachable today: - `ObjectChart` keys its fetch effect on `fetchData`, and `ObjectMetricWidget` keys on `fetchMetric`; both are `useCallback` results. - `LineItemsPanel` keys on `load`, a `useCallback` over the memoised `listFilterNode` / `orderBy`. - `ObjectGrid` lists the memoised `dataConfig` and `schemaFilter`. - Each query's content is keyed correctly. - `useViewData` (`@object-ui/react`, exported, no in-tree renderer consumer) rebuilds its adapter on an inline `value` set's length alone, not its content. - The map and the data table now hold their loading placeholder while a changed object's definition settles, instead of drawing the previous object's rows. The gate closes only when the key moves, and on those branches the key is the queried object, so a closed gate always means a changed query. --- _Generated by [Claude Code](https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #10583
Clause-②: yes —
TableColumn(published@object-ui/types) gains a declared key, and the data table's Ctrl+C refuses a masked cell.What changed
The house shape is the detail page's (objectui#8440, option A: no copy at all, never the bullets), with
isMaskedFieldType()(objectui#8686) as the one authority.componentscannot import@object-ui/fields, so the rule is not restated in the table: the producer asks, and the table obeys a flag.A masked column's raw value is withheld on these paths:
preventDefault()kept)data-tabletitletooltipdata-tabledata-tabledata-tableexportDownload: every masked field of the grid's object is left out of the CSV columns and out of every JSON recordObjectGridcell; a masked field is never classified by name into the raw amount / stage / date / percent rowsObjectGridObjectGridNot covered, and stated in the docs, the JSDoc, the zod
describe(), the README and the changeset where each applies:cellrenderer, and the table draws a column with nocellas its value.exportDownload) sends the masked columns as before and relies on the server's masking.data-tablecolumns (RelatedList,ObjectDataTable) do not set the flag yet (objectui#10657).data, asListViewandObjectViewdo), the grid's guards and the cell's own mask depend on the object schema, which the grid fetches after first paint. Until it arrives, and for good if that read fails (the grid swallows the failure and keeps its heuristic column types), an untyped view column over apassword/secretfield draws and hands out the raw value (objectui#10657, which folded objectui#10706).By package
@object-ui/types:TableColumn.masked?: boolean.z.boolean()onTableColumnSchema.StaticTableColumn/StaticTableColumnSchemaunder the lockstep rule.describe()list the withheld paths and the uncovered ones.@object-ui/plugin-grid:isMaskedGridColumn(columnType, objectFieldType)insrc/maskedColumn.ts. It is the narrow-only UNION, in the same shape asisMaskedDetailFieldType.masked: truefrom it, BEFORE thenormalizeTableColumnTypefold that dropspassword/secret.handleExport's client fallback asks it per key: per column for the CSV, and per record key for the JSON, which includes fields that are not columns.grouping.fieldsentry. It uses the view column'stypeand the object-declared type, the same pair the group-label formatter reads. A masked entry is dropped beforeuseGroupedData, and one[ObjectUI] ObjectGrid grouping:warning names it. On the unmasked path the authored config reachesuseGroupedDataunchanged.@object-ui/componentsdata-table.tsx:handleCellKeyDownwrites nothing for a masked cell.titlegate reads!col.masked.handleExportfilters masked columns.startEditrefuses a masked column. This is the one door that Enter, click and double-click all pass through, so it covers JSON-authored tables too.isEditablereads!col.masked, so a masked cell shows no edit cursor and no longer swallows the row's click.content/docs/components/complex/data-table.mdxaddsmaskedto theTableColumnblock and a "Masked columns" section.packages/plugin-grid/README.mdgains one paragraph under "Cell appearance". The docs frontmatter is byte-identical: the md5 of lines 1-4 isd069943e…before and after..changeset/10583-masked-cell-copy.md:'@object-ui/types': minor,'@object-ui/components': patch,'@object-ui/plugin-grid': patch.Decisions, and what they rest on
preventDefault()stays. I measured this in Chromium (the preinstalled/opt/pw-browsers/chromium). The page was a focusedtdholding••••••, and the clipboard was seeded withSENTINEL. With no selection, both options leaveSENTINEL. With the mask selected,preventDefaultleavesSENTINEL, but the browser default copies the bullets. objectui#8440's ruling refused that payload.typereaches the emit-seam.map. The client export and the card view readgenerateColumns()drafts, which run before the seam, so they askisMaskedGridColumndirectly. The rule is the same and is not restated.isFieldInlineEditable. That gate reads only the object-declared type. The flag's union also masks a view-authoredtype: 'password'over an objecttextfield, and a JSON-authoredmasked: true. On base, only the view-typed case leaked (measured: red atc2d86599bbelow). The key did not exist there, so no JSON-authored case could leak.fc874c1e2, grouping byapi_keybuilt 2 groups, one per distinct raw value. Masking the header label would still leave those buckets: they show which records share a credential, and the group order follows the raw value. Only refusing the key closes both. The refusal followsusableGroupingFields' existing contract: one bad entry is dropped, and the rest still group. It uses the grid's existing diagnostic channel, a console warning.ObjectGridcallsgetObjectSchemaonly for its ownobjectName, once in the inline-data branch and once in the fetch path.DataSourceexposes no synchronous schema cache, only the asyncgetObjectSchema. The main schema's lookup field carriesreference_to, but not the related object's field types. So pruning would need a new fetch, which the order rules out. It is stated under "Not covered".Premise (checked against
origin/mainc2d86599b)ObjectGridwith apasswordfield. Ctrl+C on the maskedtdwroteRAW-PASSWORD-10583. Thetextcontrol copiedRow one.rawInDom: falsewas a reading of the text only. The data-table cell wrapper carriedtitle={String(cellValue)}, so the raw credential was in the DOM and showed on hover. Leg A3 shows it.Evidence: red, then green
Pins:
packages/plugin-grid/src/__tests__/maskedCellCopyRefusal-10583.test.tsx(6): the keyboard copy and the tooltip, over three column-emit shapes.packages/plugin-grid/src/__tests__/maskedColumnSurfaces-10583.test.tsx(9): inline edit, the client CSV and JSON exports with a no-masked-field byte-identity control, the mobile card at 375px, and grouping. The grouping pins are atext-grouped control, a refusedpasswordgrouping, and a mixed grouping where the masked level is dropped while the other level still groups.packages/components/src/renderers/complex/__tests__/data-table-masked-column-10583.test.tsx(10): copy, tooltip, CSV, and inline edit in single-click and double-click modes, with flag-absent controls and a zod survival pin.Every absence carries a control in the same mounted tree.
c2d86599bdata-table.tsx+ObjectGrid.tsxat round-1 head6bad2d4ff, all 22 pins'Name,Notes,API Key,Token,Vault Key,PIN'), JSON (records carried the masked keys), the mobile card (RAW-PASSWORD-10583in the card), and both data-table inline-edit casesObjectGrid.tsxat round-2 headfc874c1e2, all 25 pinsno group was built on the masked field: … got 2, andthe category level still groups: expected 5 to be 2. Thetext-grouped control passesdata-table.tsx+ObjectGrid.tsxatc2d86599b,maskedColumnSurfaces(9)965716a72(after mergingorigin/main0c3f70aae)ef874bd8b(after mergingorigin/mained8251189)4d341308agit diff HEADempty afterPer-hunk ablations were done with
ablation-replace.mjs(anchor hit 1 → 0, blob changed). The round-1 legs ran against the 13 round-1 pins, the round-2 legs against 22, and the round-3 legs against all 25. After each one,git diff HEADwas empty.draft!col.maskedremoved fromtitleVault Key (text over secret))startEditguard removed!col.maskedremoved from render-timeisEditableno edit cursor on a masked cell() => trueRAW-SECRET-VALUE-10583 is nowhere in the cardfalseRAW-PASSWORD-10583 is nowhere in the cardfalse && …useGroupedDatagetsschema.groupingGates (objectui families derived by hand from
package.jsonand.github/workflows/)Verdict lines are quoted. The last code head is
4d341308a. It changes onlyObjectGrid.tsxfromef874bd8b(the typed column lookup for the grouping refusal). Soplugin-gridand every check that reads that file were re-run on4d341308a. Thetypesandcomponentssuites were run onef874bd8b: this PR's files in those packages are byte-identical at4d341308a. Rounds 4 to 6 (244498f70,fe124b769,2dcc28aae,d45109cb0) change wording only: the docs, the README, the changeset, and comments indata-display.ts,maskedColumn.tsandObjectGrid.tsx. Ond45109cb0I re-ranturbo run type-checkfor types and plugin-grid (Tasks: 16 successful, 16 total, 0error TS), the 25 pins (Tests 25 passed (25)),check:control-bytes(✅ OK),check:new-line-citations(VERDICT … 0 new citation(s)),check-changeset-presence(✅ 9 source file(s) of 3 released package(s) changed …),check-doc-links(Links are valid across 17 scan roots.) and, onfe124b769,check:doc-fences(✅).turbo run type-check --filter=@object-ui/types --filter=@object-ui/components --filter=@object-ui/plugin-grid(dependency closure built via^build)4d341308aTasks: 17 successful, 17 total· exit 0 · 0error TSturbo run build --filter=@object-ui/plugin-grid(thedtsemit, which fails on type errors)4d341308aTasks: 14 successful, 14 total· exit 0 · 0error TS4d341308aTests 25 passed (25)pnpm exec vitest run packages/plugin-grid/(under the verify lock,VERDICT command-exit 0)4d341308aTest Files 160 passed (160)·Tests 1514 passed (1514)pnpm exec vitest run packages/types/ef874bd8bTest Files 239 passed (239)·Tests 5270 passed (5270)pnpm exec vitest run packages/components/ef874bd8bTest Files 309 passed | 1 skipped (310)·Tests 3041 passed | 17 skipped (3058)TableColumnliteral withmasked: 'yes', read through the BUILT@object-ui/types.d.tsTS2322: Type 'string' is not assignable to type 'boolean | undefined', the only errorcheck-changeset-presence4d341308a✅ 9 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)check-changeset-no-major·check-changeset-overwriteef874bd8b✅ No changeset declares a major bump·✅ No pre-existing changeset was modified or deletedcheck-changeset-claims(report-only)ef874bd8b✅. 29 pending bodies name a touched file, none of them new ined8251189; the five bodies of theed8251189range's commits that touched this PR's source files (objectui#10580, objectui#10625 ×3, objectui#9002) were read separately; none is falsifiedcheck:new-line-citations4d341308aVERDICT new-cross-file-line-citations: 0 new citation(s)check:control-bytes4d341308a✅ check-control-bytes: OKcheck:doc-types·check:prompt-keys·check:doc-fences·check-doc-linksef874bd8b✅ Every documented component type is registered.·Links are valid across 17 scan roots.)check:unreferenced-sources4d341308aOK Every shipped source file in every covered package is reachable.check:vi-mock-specifiers·check:vi-mock-inherit·check:test-path-rootsef874bd8b.ts/.tsxfiles (package mode,--format json)4d341308ano-explicit-any; the typed lookup in4d341308aremoved it again. The 3 new test files carry 8no-explicit-anywarnings (schema casts, as sibling tests use)check-governed-queue-guard --test(12 PR paths)ef874bd8b✅ NOT GOVERNEDcheck:readme-exportsimportline to any README4d341308aThe lint row is a narrowing, not the repo-wide
pnpm lint, which CI owns. It covers exactly the changed files.eslint.config.jssets noparserOptions.project/projectService, so linting is not type-aware and the diff cannot move a verdict in an untouched file.Acceptance notes
RelatedList.normalizeColumn(@object-ui/plugin-detail) andObjectDataTable.normalizeColumns(@object-ui/plugin-dashboard) draw apasswordcell as the mask throughgetCellRenderer, but never setmasked. I measured this with a throwaway probe onbcec471b8: both drew••••••, the raw value was ininnerHTMLbut not in the text, and Ctrl+C on the maskedtdwroteRAW-PROBE-10583.type: 'text'over asecretfield keeps the refusal, but its cell draws the value as the text the author asked for. On the mobile card that cell is the one the card draws.useCellClipboard(plugin-grid, zero in-repo callers) copies raw values. It is already recorded in objectui#10568's notes.ObjectGrid.tsxand alsopackages/plugin-grid/README.md. At its headeab4c8e52,git merge-treewith4d341308a(the last code head) conflicts inObjectGrid.tsx. It also conflicts withorigin/mainon its own, so the rebase it needs is not created by this PR. This PR touches the emit-seam.map, the grouping config handed touseGroupedData,handleExport's client fallback, the card view's classifier and title row, and one import line.9fbbb17a2,0c3f70aae), and both are already inside this head through the two main merges below. Two open PRs touch these files, both drafts: objectui#10278 above, and objectui#10710 (objectui#10685), which editspackages/types/src/data-display.tsin theDrillDownConfig/ObjectMetricDrillDownConfigregion (from base line 2249), while this PR's hunks there areTableColumn(705-751) andStaticTableColumn(811-818);git merge-treeofd45109cb0with its heada1217b515is clean.git merge-treeofd45109cb0against a freshorigin/main93a558555was clean, and main has not moved in this PR's files sinceed8251189.origin/mainmoved in this PR's files twice during round 3, so it was merged with a merge commit each time. The first move wasObjectGrid.tsxat0c3f70aae, merged as965716a72. The second wasdata-table.tsx,ObjectGrid.tsxanddata-display.tsated8251189, which includes objectui#10625's inline date editors, merged asef874bd8b. Both merges were clean. Type-check, the plugin-grid build and the pins were re-run on each merge head before its push, and the package suites after it. objectui#10670 (plugin-formonly) is ined8251189and touches no file of this PR.git grep -l -i "clipboard\|ctrl+c" origin/main -- .changesetfinds8440,8686,8395,8596and6278. None of their sentences is falsified.check-changeset-claimsflagged the same 29 pending bodies. I read them again for export, card and edit claims, and none is moved.Implemented by the
domain:uiseat 2 dispatch, sessionhttps://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN.Generated by Claude Code