Skip to content

fix(fields): password, secret, vector and grid cells draw "No value" when nothing is stored (objectui#8678) - #10529

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-8678-value-independent-cells
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-8678-value-independent-cells

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #8678
Clause-②: no

What changed

getCellRenderer's standard table registered password, secret, vector and grid as argument-less arrows that never read value. Every input, null included, drew the six-dot mask or the [Vector] / [Grid] literal. Those four entries now point at three named module-level renderers in packages/fields/src/index.tsx. None of them is exported.

  • MaskedCellRenderer (password, secret): the shared EmptyValue affordance (accessible name "No value") when nothing is stored. Otherwise the same mask span as before. The value is never printed.
  • VectorCellRenderer / GridCellRenderer: the affordance when nothing is stored. Otherwise the same literal span as before.

They are named rather than inline because EmptyValue holds a hook. An inline arrow in the table is a new component type on every resolution, so it would tear that hook down on every render. That is the objectui#8441 reason, and the table's docblock now says it.

Which inputs count as empty, per value class (read from the spec, not the card)

type value class, and where it is read from empty, so it draws the affordance stored, so it keeps today's face
password, secret STRING_VALUE_TYPES in @objectstack/spec TextCellRenderer's own predicate, the floor on the coerced text: null, undefined, '', [], and any array whose coerced text is '' {} (per the objectui#8596 string-class ruling), a whitespace-only string (text keeps its spaces, and a blank credential is still a set one), any other string
vector valueSchemaFor: z.array(z.number()) the floor exactly: null, undefined, '', [] {}, and every other value
grid not a spec FieldType, so the class is GridField's own contract: an array of row objects the floor exactly {}, and every other value

Why {} is not added for vector / grid: neither class can produce it. The spec's write seam refuses a non-array vector, and that is pinned as a premise below. GridField writes arrays. Also, hasCellValue in @object-ui/plugin-detail calls every object filled, so a {} clause would draw "No value" inside a row that band has already called filled.

The platform contract this restores: the SECRET_MASK docblock in @objectstack/spec/data (installed 17.4.0), verbatim: "An UNSET credential reads back null instead, never this mask — the masking is presence-preserving on both surfaces, which is what lets a console render "configured" vs "not configured" at all."

objectui#8440 (the clipboard half), read before choosing: it is closed (ruling A, landed), and this change leaves it as it was. Its gate reads hasCellValue plus the masked-type set in plugin-detail. An empty credential row was never copy-interactive, and a populated one still draws the mask. Its pin DetailSection.maskedCopyRefusal-8440 passes on this branch.

Measurement: objectui#8596's census instrument, taken again

Every registered type (listCellRendererTypes(), 53 at this base) was rendered through getCellRenderer against 15 inputs: [], {}, '', null, undefined, a whitespace string, {a:1}, [null], [''], 0, false, a string, [1,2,3], [[1]] and SECRET_MASK. The innerHTML was diffed between bc4efba (base) and dd068fd (the fix commit).

  • 18 of 795 cells moved. All 18 are in the four types, and all 18 moved onto the affordance: [], '', null and undefined for each of the four, plus [''] for password and secret.
  • The other 49 types, every {} cell and every stored-value cell render byte-identical.

This is a single reading from a probe that was not committed. cellRenderers.valueIndependent-8678 re-derives the four types' rows on every run.

Existing pins this change made false, now updated

  • cellRenderers.signatureValueClass-8677: its census required password / secret to draw the same face for null as for a value. That is exactly the defect. The excluded set is now MASKED_FACES, measured as one face for every stored value, {} included, with the value absent from the DOM and the affordance for nothing stored.
  • cellRenderers.countLabelI18n-8441: a control proved that the resolver rebuilds its table by using vector as the last inline arrow. There is no inline entry left, so the control has nothing to test and is retired with a note. The property it protected is now pinned for every registered type by the new file's IDENTITY test.
  • cellRenderers.objectLiteral-8596: census rows unchanged, because the {} column did not move. Only the section label changed ("value-independent" is no longer true).

New pin: packages/fields/src/__tests__/cellRenderers.valueIndependent-8678.test.tsx (37 tests)

  • PREMISE: class membership comes from the installed spec. valueSchemaFor({type:'vector'}) accepts a real embedding (the lit half) and refuses {}. grid is absent from FieldType.options, with vector present as the control.
  • CENSUS: the four types × [], {}, '', null, undefined. Each case asserts the defect is absent before it asserts the replacement.
  • RULE: password / secret render byte-equal to text for every input text calls empty, with a control showing that text really does separate the inputs.
  • POPULATED: one real value per type, and the face must be byte-for-byte the pre-change span. This is the lit control, and it refuses "EmptyValue for everything". It also checks that the stored credential never reaches the DOM.
  • BOUNDARY: a whitespace-only credential keeps the mask, and {a:1} keeps the literal.
  • IDENTITY: every registered type resolves to one function object across two resolutions.

Verification, all run at HEAD 0cf9eaf (the final commit), exit code captured before any pipe

  • pnpm exec vitest run packages/fields/ --maxWorkers=2: exit 0, Test Files 195 passed | 1 skipped (196), Tests 3255 passed | 7 skipped (3262).
  • pnpm turbo run type-check --filter @object-ui/fields --concurrency=2: exit 0, Tasks: 12 successful, 12 total. tsc -p tsconfig.test.json --listFiles lists all four touched test files.
  • Consumers named by the census (inlineCredentialGate, DetailSection.maskedCopyRefusal-8440, summaryChip.badgeFitCensus-8464, console SettingsView.crypto-unavailable): exit 0, Tests 111 passed (111).
  • eslint on the changed files, plain form: exit 0, 0 errors, 156 warnings, and 0 of those on an added line (the rest are pre-existing react-refresh warnings in index.tsx). Narrowed, with the evidence for the narrowing:
    • The population comes from eslint.config.js: the five changed .tsx files. .md / .mdx get "File ignored because no matching configuration was supplied."
    • The --format json count is 5 files.
    • Invariance: no type-aware linting (no parserOptions.project / projectService), and no rule under eslint-rules/ reads another file. So this diff cannot move the verdict for any untouched file.
  • pnpm 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 citation(s)) · check:changeset-claims · check:pending-changeset-literals · node scripts/check-changeset-presence.mjs: all exit 0.
    • check:changeset-claims lists five pending changesets that name index.tsx / grid.mdx. Each paragraph was read (6357, 6625, 6694, 6837, 7166), and none concerns the four entries or the grid cell section.
  • Gates derived from the workflows that read .changeset/** and content/docs/**: check-changeset-no-major, check-changeset-overwrite, check-changeset-fixed, check-doc-component-types, check-prompt-component-keys, check-doc-example-ids, check-doc-links, check-doc-fence-languages, check-shell-escape-residue: all exit 0.
  • NOT MEASURED:
    • check-doc-snippet-types: needs a filtered build. The docs edits are prose only, with no snippet added or changed. Declared to CI.
    • check-readme-exports: exit 1 with "type entry ./dist/index.d.ts is not on disk -- run pnpm build first". The prerequisite is not met, so this is not a red result. No README or export moved.

Ablation (one-time; not a permanent test)

Each mutation went through ablation-replace.mjs from the objectstack checkout. The anchor had to hit exactly once, the blob changed, and the restore was proven: blob equals HEAD 0b5e83a8bbfd and git diff HEAD is empty. The subject resolves from source (../index), and no dist/ is on the path, so there is no build leg.

  • Leg A, password / secret entries back to the base arrows: 11 of 37 red. That is 8 CENSUS empty rows, 2 RULE and IDENTITY, which names password and secret.
  • Leg B, vector / grid back to the base arrows: 9 of 37 red. That is 8 CENSUS rows and IDENTITY, which names grid and vector.
  • Leg C, the caricature (the masked stored branch returns the affordance): 10 of 37 red, covering every stored-value case of the masked types.

All three turned red, which is the expected direction.

Acceptance notes

  • Surface. The claim declared the four standardMap entries, pins beside the census tests, and one changeset. This PR also touches the following, each forced by the change:
    • The three renderer functions above the table, plus two docblocks in the same file: the table's own docblock and the emptiness-floor roster. Both would otherwise state something false.
    • Three existing pins in packages/fields/src/__tests__/ (8441, 8596, 8677), which went red or became false.
    • Three docs pages: content/docs/fields/grid.mdx, vector.mdx and password.mdx (a new "Cell Renderer" section), under AGENTS.md Add automated testing infrastructure and CI/CD workflows #2.
    • No open PR lists any of these files; this was checked against every open PR's file list.
  • Drift left untouched (plugin-detail, outside this package):
    • The MASKED_CELL_FIELD_TYPES docblock in plugin-detail/src/fieldEnrichment.ts quotes the retired arrow. Its argument still holds: @object-ui/fields exports no "is this type masked" predicate.
    • summaryChipRenderers.ts and summaryChip.badgeFitCensus-8464 call the four "value-independent faces". For the object value that census measures, the faces and the fit verdicts are unchanged. Owner: none.
  • H3, vector / grid populated face: kept byte-identical, as instructed. For the record, the VectorField widget already draws a preview plus a dimension count for a stored embedding, so a cell could reuse that. That is a separate question.
  • Cross-band: a password holding [''] now draws "No value" in the cell while hasCellValue calls it filled. text has had exactly this behaviour all along, and this change inherits it deliberately so the string class answers one way.

Generated by Claude Code

…when nothing is stored (objectui#8678)

The four were argument-less arrows in getCellRenderer's standard table that
never read `value`, so an unset credential was drawn exactly like a set one
and `vector` / `grid` printed their literal over `null`.

- password / secret: MaskedCellRenderer, empty = TextCellRenderer's
  predicate (the floor on the coerced text). A stored value keeps the
  same mask; the value is never printed.
- vector / grid: VectorCellRenderer / GridCellRenderer, empty = the
  floor exactly. A stored value keeps the same literal.

Named module-level components: EmptyValue holds a hook, which an inline
arrow would tear down on every resolution.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
…ns the fix falsified (objectui#8678)

- cellRenderers.valueIndependent-8678: the census of the four types over
  the empty-shaped inputs, the string-class rule against `text`, the
  populated faces byte-for-byte, the boundary, and a table-wide identity
  pin (no entry of the standard table is a fresh function per resolution).
- 8677: the masked-type exclusion is now measured as "one face for every
  stored value, the affordance for none", no longer as value-independent.
- 8441: the rebuild control used `vector` as the last inline arrow; there
  is none left, and the identity property is pinned table-wide instead.
- 8596: the census label for the four `{}` rows (faces unchanged).
- docs: grid / vector / password cell-renderer sections name the empty face.
- changeset: @object-ui/fields patch.

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

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 5 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/6357-basechema-bind-declaration.md

  • names content/docs/fields/grid.mdx → content/docs/fields/grid.mdx — edited by this change

    bind was read by ten production sites and declared by no schema shape. It resolved as any through BaseSchema's index signature and rode .passthrough() on the validator, while three separate documents taught it as an authorable key of every node: this repo's own AGENTS.md §4 ("Every node in the UI tree follows this shape (@object-ui/types)"), the published agent-facing skills/objectui/rules/protocol.md ("Every UI component node MUST follow this shape"), and content/docs/fields/grid.mdx. So the agent-facing protocol told authors to write a key the published types did not know existed.

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

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 09ab32bff (merge-base with origin/main): 8 file(s) changed outside .changeset/, read against 1424 pending declaration(s) that publish a body (2006 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3044.7 KB 3104.5 KB
Main entry chunk (gzip) 147.8 KB 350 KB
Entry file index-CBur7OVB.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.68KB 6.20KB
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.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.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) 544.97KB 130.34KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.86KB 62.25KB
fields (index.js) 256.82KB 65.03KB
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 (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.38KB 14.60KB
plugin-charts (index.js) 74.95KB 20.88KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.59KB 35.39KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.25KB 61.40KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.50KB 37.51KB
plugin-gantt (index.js) 169.37KB 41.83KB
plugin-grid (index.js) 215.49KB 58.91KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.55KB 28.29KB
plugin-map (index.js) 23.82KB 7.42KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.64KB 8.94KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 88.00KB 21.97KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 114.58KB 37.60KB
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) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
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) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 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: CONTRACT_REVIEW_TIER
Head-sha: 0cf9eaf456a199945c6749912e5efdccd4b38663

① Derived judgments

Read against the card body, the cross-link comment 5592405775, the triage 5619307943 (the binding text: "Masking a populated password is correct and must stay. Drawing the mask over null is the defect. ⇒ the deliverable is that the empty cases become distinguishable from populated ones, ⛔ not that the four start printing their values. Any change that reveals secret content is out of scope"), the objectui#8440 ruling A, the objectui#8596 string-class ruling, and @objectstack/spec@17.4.0 (the lockfile's resolution; read from the published tarball into scratch because the shared checkout carries no node_modules). Every line cited is at head 0cf9eaf. Merge-base with main is bc4efba.

Behaviour changes, one by one:

  1. packages/fields/src/index.tsx:3377-3378 — password and secret now point at the named, unexported MaskedCellRenderer (index.tsx:3274-3277). Predicate isEmptyValue(coerceToSafeValue(value)), which is byte-for-byte TextCellRenderer's own test (index.tsx:728-736). Empty ⇒ the shared EmptyValue (a span with data-slot empty-value and aria-label "No value", packages/components/src/custom/empty.tsx:155-174); otherwise the same six-bullet span as at the base. RIGHT: matches the triage deliverable exactly; the value is never read beyond presence, so nothing is revealed. {} coerces to [Object] and keeps the mask (coerceToSafeValue.ts:79), which is the objectui#8596 string-class answer ("the record IS storing something") applied to two STRING_VALUE_TYPES members (spec src/data/field-value.zod.ts:56-60 lists password and secret). A whitespace-only string keeps the mask because text keeps its spaces. [''] joins to '' and draws the affordance, as text does. Consistent per class.
  2. index.tsx:3388-3389 — vector and grid now point at VectorCellRenderer / GridCellRenderer (index.tsx:3304-3312), predicate the floor exactly (isEmptyValue, packages/core/src/utils/emptiness.ts:97-104: null, undefined, '', []). RIGHT: the triage names a constant face over nothing as the defect; the cross-link comment says these two have no masking rationale. [] is a genuine empty member of vector's class (spec valueSchemaFor returns z.array(z.number()), field-value.zod.ts:538) and of GridField's array-of-rows contract (GridField.tsx:712 writes arrays). {} deliberately NOT swept: hasCellValue (plugin-detail/src/emptiness.ts:150-156) calls every populated object a value, so a {} clause would put "No value" inside a row that band calls filled; and the spec write seam refuses a non-array vector (pinned as PREMISE). Reasoned from the contract, not from the card; RIGHT.
  3. The populated face for all four is byte-identical to the base (the same two span spellings). RIGHT: "must stay", and pinned as bytes (POPULATED in the new file).
  4. index.tsx:365-373 roster docblock and index.tsx:3313-3328 table docblock rewritten. Both true at head: the three renderers ask the floor by name, and I read the whole table index.tsx:3329-3392 and found no arrow entry left. The table is still a function; the docblock now says the difference is no longer observable through identity, which is accurate.
  5. The three consumers that quote the mask were checked: ObjectGrid column cells pass the raw value straight to the renderer (plugin-grid/src/ObjectGrid.tsx:3409), so the change reaches a grid column as the changeset says; DetailSection and HeaderHighlight pre-guard with hasCellValue, so they never drew the mask over null and are unchanged; gallery omits floor-empty rows before resolving (ObjectGallery.tsx:635).

Asked and missing: nothing. The triage asked the dev to make the scope judgement explicitly (done in the PR body's per-class table and in the docblocks), to read objectui#8440 first (done; the copy gate reads hasCellValue plus the plugin-detail mirror set and is untouched), and the card asked for a check against the existing pins and for consumers depending on the mask always rendering (the four test files in packages/fields that name the faces were all adjusted or re-read; the two in plugin-detail measure a populated object value and still hold).

Done but not asked: the three docs pages (content/docs/fields/grid.mdx:181-182, vector.mdx:88-89, password.mdx:50-55) — required by AGENTS.md #2, and each pre-existing "renders: [Vector]" / "[Grid]" example line is still true for a stored value. The new IDENTITY pin covers every registered type rather than the four; extra but harmless. The claim's file surface was exceeded (declared under Acceptance notes) by docblocks in the same file, three existing pins, and the docs pages; each is forced by the change, none overlaps an open PR per the body, and the record here treats that as a disclosed deviation, not a defect.

Existing pins edited:

  • cellRenderers.signatureValueClass-8677.test.tsx:118-127, 202-217 — the old THE CENSUS required password / secret to draw the same bytes for null as for a value, i.e. it pinned this card's defect. Flip is load-bearing. The replacement is stronger, not weaker: one face for alpha, bravo and {} (so a member that starts printing its value still fails), the stored value asserted absent from the DOM, no affordance for a value, and the affordance for null. The VALUE_INDEPENDENT_FACES to MASKED_FACES rename keeps the "an exclusion must name a class member" control.
  • cellRenderers.countLabelI18n-8441.test.tsx:351-371 — the retired control asserted getCellRenderer('vector') differs across two calls. It could only be true while vector was an arrow entry, and there is no arrow entry left, so the control has no subject. The property the 8441 describe guards (a hook-holding entry is one component type across renders) is now pinned table-wide by IDENTITY in the new file, and my Leg A shows it goes red by name. Not a weakened guard; the repeater toBe pin itself is untouched.
  • cellRenderers.objectLiteral-8596.test.tsx:281-288 — comment label only; the four {} rows and their expected bytes are unchanged, and the file's THE BOUNDARY loops do not include the four types, so nothing in it was asserting the old null face.

New pin cellRenderers.valueIndependent-8678.test.tsx (37 tests). Lit controls: POPULATED (lines 216-237) pins the exact pre-change bytes and the DOM absence of sk_live_do_not_render; THE RULE control (207-213) proves text separates null from {} so the byte-equality is not against a constant; PREMISE (141-149) proves the vector schema accepts a real embedding before asserting the {} refusal; IDENTITY asserts a non-empty registry first. Negative legs, run by me in a scratch export of the head (never the shared checkout) with pnpm install --offline from the lockfile, four files, positive run 109 passed / 0 failed with the index blob equal to the head blob 0b5e83a8bbfd5f69e2914821fe0efc06511b927c; every mutation anchor hit exactly once and each restore was proven by blob equality:

  • Leg A (four entries back to the base arrows): 20 red — 16 CENSUS empty rows, 2 RULE, IDENTITY naming the four, and 8677's THE CENSUS. 8441 and 8596 stay green, as expected (they did not assert the empty face).
  • Leg C (caricature: masked renderer draws the affordance for everything): 13 red — 2 CENSUS {} rows, 2 RULE, 4 POPULATED, 2 BOUNDARY, plus 8596's census and POPULATED and 8677's THE CENSUS.
  • Leg B (vector draws the affordance for everything): 5 red — the vector {}, POPULATED and BOUNDARY rows plus 8596's two.
    All three legs redden in the expected direction and the failure sets are the families each leg mutates.

② Semver level

.changeset/8678-value-independent-cells.md declares '@object-ui/fields': patch. Published source under packages/fields/src/ moved, so a non-empty changeset is owed (AGENTS.md changeset-presence rule; the Changeset Declaration and Bump Policy checks are green). No major (AGENTS.md 版本号策略); a user-visible render fix with no export or key change is a patch in the fixed group, matching the objectui#8596 and objectui#8677 precedents. Level correct.

Changeset prose, sentence by sentence: the title (an empty cell now says "No value") — true, the aria-label. "These four cell renderers never read their value." — true at the base, argument-less arrows. "drew the six-dot mask ... printed [Vector] and [Grid] for every input, null included." — true. "looked exactly like one that is set wherever the cell is drawn for an empty value, such as an ObjectGrid column." — true, ObjectGrid.tsx:3409 hands the raw value to the renderer. "An empty value now renders the shared EmptyValue affordance, whose accessible name is No value" — true (empty.tsx:14-20 falls back to "No value"). "password / secret use the string class's rule, the same one text applies: null, undefined, '' and [] are empty." — true. "{} and a whitespace-only string are stored values and keep the mask." — true. "vector / grid use the shared emptiness floor: null, undefined, '' and []." — true. "A stored value renders exactly what it rendered before: the same mask, the same literal." — true, pinned as bytes. "The value of a password or secret is still never printed." — true.

Docs prose added: grid.mdx:181-182 and vector.mdx:88-89 (an empty value of the four floor members renders the shared "No value" affordance, distinguishable from a populated one) — true. password.mdx:52-55 ("a stored password or secret value renders as a fixed mask, never as the value"; empty renders the affordance) — true. No README changed and none states the four faces.

The PR body's one verbatim spec quote (the SECRET_MASK docblock: an UNSET credential "reads back null instead, never this mask — the masking is presence-preserving on both surfaces, which is what lets a console render "configured" vs "not configured" at all") matches dist/data/index.d.ts of the 17.4.0 tarball word for word.

③ Boundary flags

  • PR body flags answered. Surface exceeded: each extra file is forced (docblocks that would otherwise be false, pins that went red or false, docs under Add automated testing infrastructure and CI/CD workflows #2); acceptable, and no open PR names them per the body. Drift left in plugin-detail: confirmed present at head — fieldEnrichment.ts:154-156 still quotes the retired arrow spelling and calls the renderers "anonymous", and summaryChipRenderers.ts:51 plus summaryChip.badgeFitCensus-8464.test.tsx:148 still say "value-independent faces". Stale prose only; the mirror's argument (no exported masked-type predicate) still holds and the 8464 rows measure a populated object, so no assertion is false. Outside the package and the claim surface; noted, non-blocking. H3 (vector / grid populated face byte-identical): verified by POPULATED and by Leg B. Cross-band ['']: verified — hasCellValue(['']) is true through the object branch while the masked cell and text both draw the affordance; inherited from text on purpose, not new. No open questions listed.
  • CI at 0cf9eaf: 43 check-runs, all completed — 40 success, 3 skipped (Test (coverage), Test (coverage shard), dependabot), 0 failure, 0 in progress. All eight test shards, Type Check, Lint, README Export Check, Line Citation Gate, Doc Snippet Type Check and the changeset family are success. No re-poll needed.
  • Draft: yes (draft: true), as a dispatched PR must be.
  • Clause-②: no holds. MaskedCellRenderer, VectorCellRenderer and GridCellRenderer are module-private (no export), the table keys are unchanged so listCellRendererTypes() answers the same set, no schema, validation or accept set is touched, and README Export Check is green.
  • Main moved 11 commits since bc4efba; git log refs/review/main --not bc4efba -- (the nine PR paths) is empty, so main did not move under any file this PR touches.

Implemented-by: claude/issue-8678-value-independent-cells
Reviewed-by: session_01KUxVUa7e39aNjhkKi1gsoy

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 25, 2026 07:48
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit cdd2542 Sep 25, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-8678-value-independent-cells branch September 25, 2026 08:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(fields): four cell renderers draw a value-INDEPENDENT face — password/secret mask, vector/grid print a literal, for every input including null

2 participants