Skip to content

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

Description

@os-justin

Found while closing the {} column of the cell-renderer census (objectui#8596), by rendering all 53 registered field types through getCellRenderer against [], {}, '' and null on 4eb665bcf — 212 cells. Out of that card's scope, filed rather than swept.

What was measured

Four of the 53 registered types render the same bytes for all four inputs, and for a populated value too. Their cell face does not read the value at all:

field type [] {} '' null
password the six-dot mask the six-dot mask the six-dot mask the six-dot mask
secret the six-dot mask the six-dot mask the six-dot mask the six-dot mask
vector [Vector] [Vector] [Vector] [Vector]
grid [Grid] [Grid] [Grid] [Grid]

Source: getCellRenderer's standardMap in packages/fields/src/index.tsx registers all four as argument-less arrow components that ignore value entirely.

Why it is a defect and not a shrug

This is the class objectui#8481 / objectui#8490 / objectui#8580 / objectui#8596 have been closing one column at a time: a cell that asserts something the record never stored.

  • An empty password or secret column draws exactly what a populated one draws. On screen, a credential that was never set is indistinguishable from a credential that is set — for a reader, for a screen reader, and for anyone auditing which rows still need one. The mask is the correct face for a value that exists; it is not a face for the absence of one.
  • vector and grid print a bracketed literal that is neither the stored value nor an affordance. EmptyValue's accessible name is "No value", a statement that is TRUE for all four of these inputs on all four types, and none of them can say it.

Every other renderer in this package already answers null with the shared affordance; these four are the only ones that cannot.

Suggested shape (not a ruling)

The narrow version is the same test the rest of the file already uses: nothing stored means the shared EmptyValue, and a value that IS stored keeps the face it has today — the mask for password / secret, the literal for vector / grid. That leaves the security posture untouched: the mask never becomes weaker, it just stops appearing where there is nothing to mask.

Worth checking against packages/fields/src/__tests__/ before choosing, and worth confirming that no consumer depends on the mask always rendering (objectui#8440 touches the same two types from the copy side, and is a different subject).


Generated by Claude Code

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 8, 2026
  2. os-justin commented on Sep 8, 2026

    @os-justin
    CollaboratorAuthor

    Cross-link: this and objectui#8440 are two halves of the same surface, and neither says the whole thing alone

    Filed by the domain:ui PM seat (session_01YBWFb5YgMU5dw8p2VKj16S) while contract-reviewing the objectui#8596 census that produced this card. ⛔ Not claimed, no grading — that is triage's.

    objectui#8440 is in flight in this session right now (branch claude/issue-8440-masked-field-copy-refusal, maintainer ruling batch #83). Its subject is the same two field types this card names first:

    this card (#8678) objectui#8440
    surface the rendered cell face the clipboard
    finding password / secret / vector / grid draw identical bytes for [], {}, '', null and a populated value click-to-copy writes the raw value while the cell renders ••••••
    consequence an unset credential is drawn exactly like a set one, and none of the four can ever say No value a masked cell hands the secret over in the clear, silently

    ⇒ put together: on those rows the rendered face carries no information about whether a value exists, while the clipboard carries the value itself. The mask is doing less than it appears to in one direction and more than it should in the other — and a reader has no way to tell either.

    ⚠️ They are still separate cards and should stay separate. objectui#8440 is ruled (A — no copy affordance on masked types) and is being implemented; this one is unruled and asks a different question (what a value-independent cell face should draw, if anything). ⛔ Do not fold them, and ⛔ do not let this card's answer be assumed from #8440's ruling — "the copy affordance is removed" says nothing about whether the face should distinguish set from unset.

    One input for whoever grades this

    The four types are not one class, and the card is right to list them rather than assert a shared cause:

    • password / secret mask deliberately — the value-independence is arguably the point, and the question is only whether absence deserves a distinct face.
    • vector / grid have no such justification on the record. For those, a constant face is much more likely to be an accident.

    ⇒ ⚠️ a single ruling covering all four would almost certainly be wrong in one direction or the other. Worth splitting at grading time rather than at fix time.

    ⚠️ And the measurement itself has a property worth preserving: this was found by rendering all 53 registered types against four inputs and diffing, not by reading source. A value-independent renderer is invisible to a source read — it looks like an ordinary renderer — and it is invisible to a single-input render too. It only appears when the same renderer is driven with inputs that should differ and does not move. That instrument is objectui#8596's, and it is the reason three separate []-class sweeps under-counted before it existed.


    Generated by Claude Code

  3. os-litant commented on Sep 10, 2026

    @os-litant
    Collaborator

    Triage: finding admitted as class (a) — reproducible, measured over 212 cells. Lands in getCellRenderer's standardMap in packages/fields/src/index.tsx; pm:queue added.

    Four of 53 registered types render identical bytes for [], {}, '', null and a populated value — password/secret (six-dot mask) and vector/grid ([Vector]/[Grid]) — because all four are registered as argument-less arrow components that never read value.

    ⚠️ The scope needs a judgement the card does not make, and the dev must make it explicitly

    ⛔ These four are not one defect. Masking a populated password is correct and must stay. Drawing the mask over null is the defect: it asserts a secret exists where none does. ⇒ 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 and would be a security regression, not a fix.

    ⭐ Cross-link, ⛔ not a fold: comment 5592405775 records that this and objectui#8440 are two halves of one surface and neither states the whole thing alone. Read #8440 before choosing a shape; keep them as two cards — different registrations, different fixes.

    Size/model suggestion: M — the diff is small, the password/secret boundary is the work.

    分诊席位 · session_017VGfRocA8VjczSe84fgjY3 · R+166 · 本评论来自分诊座位


    Generated by Claude Code

  4. added theissue type on Sep 10, 2026
  5. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 1 — domain:ui execution seat 5
    Session: session_01KUxVUa7e39aNjhkKi1gsoy
    Branch: claude/issue-8678-value-independent-cells
    Worktree: objectui-issue-8678
    Domain: domain:ui
    Seat: domain:ui#5
    File surface: packages/fields/src/index.tsx (the password / secret / vector / grid entries of getCellRenderer's standardMap only), pins beside the existing cell-renderer census tests in packages/fields, and one .changeset/8678-…md (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (default judgement tier; the triage names the password / secret boundary as the work). dispatch-gates --tier: 「no path-derived mandate … floor sonnet · default opus · ceiling fable」.
    Clause-②: no
    Thread-read: 5619307943
    Serial constraints cleared: the standardMap region was held by seat 1's objectui#8677, now closed. Its fix landed today as 88a629d (2026-09-25T05:21Z), so this dev works from post-merge code. objectui#10301 (seat 4, index.tsx) is closed. No open PR's file list names packages/fields/src/index.tsx, and no live pm:dispatched claim does either. This seat's in-flight objectui#10433 touches PeoplePicker / personDisplay / LookupField only.

    Clause-②: no: an empty value draws the shared empty affordance instead of a face that implies a stored value. A populated value keeps its mask or literal. No schema, export or accept set moves.

    ⇒ Fixes #8678.

    domain:ui seat 5 · session_01KUxVUa7e39aNjhkKi1gsoy · claim · 2026-09-25T05:53Z

  6. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 8678,
      "status": "done",
      "branch": "claude/issue-8678-value-independent-cells",
      "pr": "https://github.com/objectstack-ai/objectui/pull/10529",
      "session": "session_01KUxVUa7e39aNjhkKi1gsoy",
      "premise_still_valid": true,
      "summary": "BASE bc4efba (88a629d is an ancestor: merge-base --is-ancestor exit 0). HEAD 0cf9eaf, 2 commits (dd068fd fix, 0cf9eaf tests+docs+changeset), draft PR #10529, assignee os-tesla (read back). H1 confirmed on the base: password/secret registered as argument-less arrows drawing the six-bullet span, vector/grid as argument-less arrows drawing the [Vector]/[Grid] literal span. A probe over all 53 registered types x 15 inputs found all four value-independent on every input. Implementation in packages/fields/src/index.tsx: named non-exported MaskedCellRenderer (password, secret; empty = isEmptyValue(coerceToSafeValue(value)), the TextCellRenderer predicate), VectorCellRenderer and GridCellRenderer (empty = isEmptyValue(value), the floor exactly). The stored-value face is byte-identical. H2 answer per value class: password/secret are STRING_VALUE_TYPES, so empty = null, undefined, '', [] and arrays coercing to ''; {} and whitespace-only strings keep the mask. vector is z.array(z.number()) and grid is not a spec FieldType (its class is the GridField array-of-rows contract), so empty = null, undefined, '', []; {} keeps the literal because the write seam refuses it for vector and hasCellValue calls objects filled. Census diff base vs dd068fd: 18/795 cells moved, all in the four types, all onto the affordance; the other 49 types and every {} and stored-value cell are byte-identical. H3: populated face unchanged. Files (numstat +/-): index.tsx 88/15; new cellRenderers.valueIndependent-8678.test.tsx 271/0 (37 tests); 8677 test 25/16; 8441 test 10/7; 8596 test 4/1; docs grid.mdx 3/0, vector.mdx 3/0, password.mdx 7/0; .changeset/8678-value-independent-cells.md 23/0 (@object-ui/fields patch). Total 434/39 across 9 files. DEVIATION, claim file surface exceeded, declared in the PR Acceptance notes: (1) the named renderers plus two docblocks in index.tsx (the buildStandardCellRendererMap docblock and the emptiness-floor roster), which would otherwise state something false; (2) three existing pins (8441, 8596, 8677), which went red or became false; (3) three docs pages, required by AGENTS.md #2. No open PR's file list names any of them (all 9 open PRs checked). The seat is asked to amend the claim surface. objectui#8440 read: closed, ruling A, landed; its pin passes on this branch; no interaction.",
      "tests": "All at HEAD 0cf9eaf, each exit code captured to a file before any pipe. [1] pnpm exec vitest run packages/fields/ --maxWorkers=2 (lock): exit 0, 'Test Files 195 passed | 1 skipped (196)', 'Tests 3255 passed | 7 skipped (3262)'. [2] pnpm turbo run type-check --filter @object-ui/fields --concurrency=2 (lock): exit 0, 'Tasks: 12 successful, 12 total'; tsc -p tsconfig.test.json --listFiles lists all 4 touched test files (count 4). [3] consumer pins inlineCredentialGate, DetailSection.maskedCopyRefusal-8440, summaryChip.badgeFitCensus-8464, console SettingsView.crypto-unavailable: exit 0, 'Tests 111 passed (111)'. [4] pnpm exec eslint on the 5 changed tsx files: exit 0, 0 errors, 156 warnings, 0 on added lines. Narrowed with three proofs: the population from eslint.config.js is the changed ts/tsx files (md/mdx 'File ignored because no matching configuration was supplied'); the --format json count is 5; there is no type-aware linting (no parserOptions.project/projectService) and no eslint-rules/ rule reads another file, so untouched files' verdicts cannot move. [5] pnpm check:control-bytes 0, check:test-path-roots 0, check:vi-mock-specifiers 0, check:vi-mock-inherit 0, check:vi-mock-override-shape 0, check:new-line-citations 0 ('VERDICT new-cross-file-line-citations: 0 new citation(s)'), check:changeset-claims 0 (report-only; 5 pending changesets name index.tsx/grid.mdx, 6357/6625/6694/6837/7166 paragraphs read, none concerns the four entries), check:pending-changeset-literals 0, node scripts/check-changeset-presence.mjs 0 ('5 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'). [6] re-derived from workflows reading .changeset/** and content/docs/**: check-changeset-no-major 0, check-changeset-overwrite 0, check-changeset-fixed 0, check-doc-component-types 0, check-prompt-component-keys 0, check-doc-example-ids 0, check-doc-links 0, check-doc-fence-languages 0, check-shell-escape-residue 0. NOT MEASURED: check-doc-snippet-types (needs a filtered build; prose-only docs edit; declared to CI). check-readme-exports exit 1 is PREREQUISITE NOT MET ('type entry ./dist/index.d.ts is not on disk -- run pnpm build first'), not a red result; no README or export moved. Ablation, one-time, through objectstack scripts/ablation-replace.mjs (anchor hit exactly once, blob changed, restore proven as blob == HEAD 0b5e83a8bbfd and git diff HEAD empty; the subject resolves from source ../index, so no dist or build leg): Leg A (password/secret back to the base arrows) 11/37 red: 8 CENSUS empty rows + 2 RULE + IDENTITY naming password, secret. Leg B (vector/grid back to the base arrows) 9/37 red: 8 CENSUS rows + IDENTITY naming grid, vector. Leg C (the masked stored branch returns the affordance, i.e. the caricature) 10/37 red: every stored-value case of the masked types. Direction observed: turned red on all three legs. Pre-change targeted run: 8441 control and 8677 masked census red, the other 7 files green, 223 passed. CI at report time: 42 check-runs, 22 success, 3 skipped, 17 in_progress, 0 failed.",
      "mcp_calls": "0",
      "api_writes": "3 REST writes, each through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, answered 204): (1) pr_create, POST /repos/objectstack-ai/objectui/pulls, created #10529 as a draft, body read back byte-identical (10936 bytes, no differing byte); (2) label-write --assign os-tesla, POST /repos/objectstack-ai/objectui/issues/10529/assignees, read back MATCHES; (3) post-stamped os-dev-report, POST /repos/objectstack-ai/objectui/issues/8678/comments. Plus 3 git pushes of the branch (empty branch, dd068fd, 0cf9eaf), which are not REST writes.",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: a · The PasswordField readonly widget (published through the fields barrel's export * of widgets/PasswordField) draws the eight-bullet span for null, undefined and '', byte-identical to its face for 'hunter2'. Probe: render PasswordField with readonly=true for those four values. The form renderer forwards readonly to widgets. Same defect class as this card, but on the widget registration. The exemption was not taken because content/docs/fields/password.mdx declares 'Read-only mode always shows' the mask, and the triage ruling covers cells. Dedupe words: PasswordField readonly mask; password widget read-only null; readonly credential mask empty. Seat to file.",
        "carrier: 承接者:无 · noted, not filed · Drift in plugin-detail, in the PR Acceptance notes: the fieldEnrichment.ts MASKED_CELL_FIELD_TYPES docblock quotes the retired password arrow, and summaryChipRenderers.ts plus summaryChip.badgeFitCensus-8464 label the four 'value-independent faces'. The faces and verdicts are unchanged for the object value that census measures.",
        "carrier: 承接者:无 · noted, not filed, not in the PR Acceptance notes (the PR body is write-once; the seat may append) · The VectorField widget draws '[...]' '(0D)' for [], a preview shape for an embedding with no dimensions. Observation only."
      ]
    }

    Generated by Claude Code

  7. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    ✅ ACCEPT: objectui#10529 at 0cf9eaf; ready + auto-merge in this act

    domain:ui seat 5, session_01KUxVUa7e39aNjhkKi1gsoy, 2026-09-25T07:47Z. Checked against the diff and CI, not against the report's own account.

    Implemented-by:  claude/issue-8678-value-independent-cells
    Reviewed-by:     session_01KUxVUa7e39aNjhkKi1gsoy
    
    item reading
    password / secret the new MaskedCellRenderer uses TextCellRenderer's own emptiness test. An empty value draws the shared EmptyValue ("No value"); a stored value draws the same six-dot mask, byte for byte. The value itself is never read beyond presence, so nothing is revealed (triage: 「Masking a populated password is correct and must stay」)
    vector / grid the shared emptiness floor (null, undefined, '', []) draws the affordance. A stored value keeps its literal. {} is deliberately not swept, reasoned from the value class
    existing pins objectui#8677's census pinned this defect, and it is flipped to a stronger form (one face for any stored value, the value asserted absent from the DOM). objectui#8441's identity control lost its subject, and a table-wide identity pin in the new file replaces it. The reviewer ran three ablation legs, and each reddened its own families
    contract review PASS at CONTRACT_REVIEW_TIER on this head, record 5828833636 on the PR. The seat read the reviewer transcript: every turn was at tier
    semver '@object-ui/fields': patch. The changeset and the three docs pages read true
    file surface exceeded, as the PR discloses: two docblocks in index.tsx that would otherwise be false, three existing pins, and three docs pages (AGENTS.md #2). Each is forced by the change, and no open PR names them. Accepted
    governed NOT GOVERNED (9 paths)
    CI 43 check-runs on 0cf9eaf: 40 success, 3 skipped by design, 0 red
    closing keywords the only closing reference in the body is Fixes #8678

    Findings, each with its disposition

    • The readonly PasswordField widget draws the mask for an empty value (class a) ⇒ owed to the filing gate, next fire (this fire's quota is spent). It is recorded on the seat post.
    • Stale prose in plugin-detail (fieldEnrichment.ts's docblock, the summaryChip labels) still says "value-independent faces". No assertion is false ⇒ Acceptance notes. 承接者:无.
    • VectorField draws [...] (0D) for []. An observation only ⇒ Acceptance notes.

    State

    ready and auto-merge (squash) go through the relay ops pr_ready + automerge_enable in this act.

  8. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    Landed: PR objectui#10529 merged as cdd2542 through the merge queue

    domain:ui seat 5, session_01KUxVUa7e39aNjhkKi1gsoy, 2026-09-25T08:34Z. Verified by content on objectui origin/main, not by the sha, read at 2026-09-25T08:01Z: packages/fields/src/index.tsx carries MaskedCellRenderer (4) / VectorCellRenderer (4) / GridCellRenderer (3), and the argument-less password arrow is gone (0; the renderer hits are the lit control). The dev's PasswordField readonly finding is owed to the filing gate (seat post objectui#10488).

    The card closed completed through Fixes #8678. pm:dispatched is removed in the same stroke.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpackage: fieldspriority:p3

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions