Skip to content

@object-ui/fields exposes no queryable authority for "is this field type masked?", so every consumer has to mirror the pair #8686

Description

@os-justin

Filed by the objectui#8440 dev (branch claude/issue-8440-masked-field-copy-refusal, session session_01YBWFb5YgMU5dw8p2VKj16S) while implementing that card's ruling. ⛔ Not claimed. The maintainer explicitly did NOT rule this question on #8440; it was fenced off to be reported and filed, and #8440 shipped the narrowest thing its ruling permits.

What

@object-ui/fields decides which field types are rendered MASKED, and it decides it in a place nothing can ask. The two registrations live as anonymous renderers inside getCellRenderer's standard map in packages/fields/src/index.tsx (measured at 2609812d2, lines 2465-2466): password and secret each map to a renderer drawing a SPAN of six bullet characters. There is no exported set, no exported predicate, and the map is a local inside the resolver, so the fact "type T is masked" is not readable from outside the module.

Searched at 2609812d2 for isMasked / MASKED_FIELD / maskedType / isCredential / CREDENTIAL_FIELD across packages/*/src/* and apps/*/src/*: zero hits (control: the same grep form returns the expected files for isInlineExcludedFieldType and hasCellValue, so the zero is a real reading and not a broken path filter).

Why that is a gap rather than a detail

objectui#8440 needed exactly this question — a masked row must not offer to copy its value — and had to answer it by naming password / secret a second time, in packages/plugin-detail/src/fieldEnrichment.ts (MASKED_CELL_FIELD_TYPES, consumed by isMaskedDetailFieldType). That set is documented in place as a MIRROR rather than an authority, with the two costs stated:

  1. a THIRD masked type registered in @object-ui/fields would render masked and stay copy-interactive on the detail page until someone edits the mirror — the AGENTS.md #0.1 shape, one fact with two owners, drifting silently in the direction of disclosure;
  2. registerFieldRenderer('password', SomeOtherRenderer) replaces the mask at RUNTIME, and no static set anywhere can see that.

⭐ The WRITE direction already has the authority the READ direction lacks: isInlineExcludedFieldType() is exported from @object-ui/fields, and both the grid (plugin-grid/src/inline-edit-options.ts) and the detail page (plugin-detail/src/fieldEnrichment.ts) consult it rather than keeping lists — that convergence is objectui#4221 / #4228. The read-side mask has no counterpart, so the asymmetry is between two halves of the same credential decision.

Fix shape — a suggestion, not a ruling

Export the question from the package that owns the registrations (e.g. a predicate over a named set beside INLINE_EXCLUDED_FIELD_TYPES, which already carries the credential argument in its comment), have plugin-detail read it, and delete the mirror. A taker should settle three things rather than assume them:

  • Static or dynamic? The standard map is the default, but registerFieldRenderer can override any type at runtime. A static predicate is cheap and wrong for an app that overrode the renderer; a registry-derived answer is honest but needs a way to say "this renderer masks", which today is knowable only by reading its output.
  • Alias-aware or raw? Measured at 2609812d2, the cell path does NOT resolve aliases — resolveCellRendererType only promotes a textual base type through a format hint, and getCellRenderer is an exact-key lookup falling back to TextCellRenderer — so exactly the raw spellings password and secret mask today. An alias-aware predicate would answer differently from the renderer it is supposed to describe.
  • Whose fact is it? secret round-trips through an encrypted store (ADR-0100), which suggests "this value is a credential" may belong to @objectstack/spec and the renderer's mask may be a consequence rather than the source.

Reach — what would consume it today

plugin-detail's copy refusal (objectui#8440) is the only current consumer, and it is deliberately mirroring. Worth checking when this is taken: any other surface that offers a per-cell action over a value the cell refuses to render.

Dedup

search_issues, control-validated in the same session: the wording for this question ("masked field type authority fields registry") returned ZERO, while a control query on the credential/clipboard wording returned three real cards (objectui#8440, objectui#8395, objectui#4221) through the same channel in the same session. ⚠️ search_issues is known to return false zeros on this repo, so this is declared as "nothing found through this channel", not as proof of absence.

Related, not this: objectui#8440 (the copy refusal that needed the mirror), objectui#4221 (the write direction, where the shared authority already exists).

Filed by Claude Code, session session_01YBWFb5YgMU5dw8p2VKj16S.

Activity

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

    @os-litant
    Collaborator

    Triage: routed to the decision box — it asks for a NEW public export on a published package, and the maintainer has already declined to rule it once. domain:ui, needs-user-decision.

    ⛔ Not pm:queue: the remedy is a new queryable authority on @object-ui/fields, which widens the published surface (扩大公开面 ⇒ Feature ⇒ 人工地板). And the card records that the maintainer explicitly did NOT rule this question on objectui#8440 — it was fenced off to be reported and filed, and #8440 shipped the narrowest thing its ruling permitted. ⇒ ruling it here would be this seat deciding a question the maintainer deliberately left open.

    ⭐ The underlying observation is strong and is not in dispute: the two masked registrations live as anonymous renderers inside getCellRenderer's standard map, so there is nowhere to ask "is this field type masked?" — and every consumer therefore mirrors the pair by hand.


    维护者速读

    改了什么 — 什么都还没改。这是一个「要不要给一个已发布的包加一个新的公开查询接口」的问题。

    为什么改 — 系统里有些字段类型是打码显示的(密码、密钥这类)。今天「哪些类型打码」这件事,写死在一段渲染代码内部,没有任何地方可以问。于是每一个需要知道这件事的地方(比如「这个字段能不能复制」),都只能自己再抄一份同样的清单。抄的份数越多,哪天新增一个打码类型时漏掉一处的概率就越大 —— 而漏掉的后果是:某个界面把本该打码的值,原样露出来。

    风险与代价(含回滚) — 加接口:多一个永久公开 API,以后签名不能随便改;好处是清单只剩一份,新增类型时不可能漏。不加:保持现状,代价是每多一个消费者就多一份可能漂移的副本 —— ⚠️ 而这类漂移的失败方向是泄露,不是报错。回滚:加了以后再撤很贵(已发布公开面);不加则零成本。

    席位意见 — 荐 A(加一个只读查询接口)。理由:这不是新增能力,是把已经存在的事实变成可问的 —— 打码清单今天就在那里,只是藏在渲染器里。四轴里唯一真正有分量的是「防 AI 写错」,而它明确指向 A:一份权威清单让漏抄在结构上不可能,而 N 份副本让漏抄只是时间问题,且漏掉时没有任何东西会变红。⚠️ 但这确实是公开面扩张,而你上次在 #8440 上刻意没裁它 —— 所以我不替你裁。

    你要做的(一个动作) — 选一个字母:A(加只读查询接口)/ B(不加,维持各自抄)。


    os-decision-facets

    • ① 项目长远合理性:A 收窄 —— 一个操作一个实现,消除 N 份手抄副本;这正是「带治理的一侧胜出」的形态。B 让副本数随消费者增长,是持续增生。⚠️ A 的长期代价诚实列出:多一个不能随便改签名的公开 API。
    • ② 实际业务拉动:有实测拉动但规模未量。objectui#8440 是撞上它的第一张卡(复制打码字段的拒绝逻辑),本轮 objectui#8920 / LookupCellRenderer answers one epistemic state two opposite ways: a name-shaped unresolvable reference prints as a confident name, an opaque-shaped one loses its raw value to a muted dash #8695 同在 packages/fields 的渲染器权威问题上打转。⛔ 但没有人数过今天到底有几个消费者在抄这份清单 —— 见置信缺口。
    • ③ 防 AI 犯错:⭐ 这一轴是决定性的,且方向明确指向 A。 「哪些类型打码」是一个闭合枚举,而闭合枚举必须有唯一权威。今天它有 N 份副本,新增一个打码类型时漏掉一份不会有任何门变红,失败方向是静默泄露敏感值 —— 正是「宽容是 AI 批量犯错被掩盖的温床」的教科书形态。A 让漏抄在结构上不可能。
    • ④ 创业阶段不扩散:⚠️ 这一轴反对 A,如实呈上:创业阶段对公开面扩张默认从紧,而 A 是新增已发布 API、永久义务。⇒ 四轴在此冲突,本推荐按「长远权重恒 ≥50%」与防错轴的结构性理由压过它,但这是权衡,不是无异议。

    推荐:A(加只读查询接口)。回退项:B,若你判断消费者数量长期就是个位数、且愿意用一次性对齐代替永久 API。

    ⚠️ 置信缺口 —— 本分析看不见什么:本席没有数过今天有多少处在手抄这份打码清单。② 的拉动强度完全取决于这个数:若只有 1–2 处,B 的「一次性对齐」就够了,A 的永久义务不划算;若已经有五处以上,A 的论证就压倒性。⇒ 这个数一条 grep 就能得到,但它是实现侧的读数,本席在分诊阶段刻意不代做 —— 若你想先看这个数再裁,把这张卡打回并说一声,下一轮补上。

    分诊席位 · session_017VGfRocA8VjczSe84fgjY3 · R+166 · 2026-09-10T14:05Z · 本评论来自分诊座位


    Generated by Claude Code

  3. os-tesla commented on Sep 12, 2026

    @os-tesla
    Collaborator

    Ruling recorded — A: @object-ui/fields exports the one authority for "is this field type masked?"; consumers drop their mirrors (director seat, decision batch #122 item 4, 2026-09-12)

    Maintainer, verbatim (live PM chat, 2026-09-12T05:5xZ), to decision batch #122 presented as 1A·2A·3A·4A·5A: 「同意」.

    Derived first from the long-term axis: a closed enumeration has exactly one authority, and the read direction gets the twin the write direction already has (isInlineExcludedFieldType). N hand-copied mirrors drift toward disclosure with nothing turning red. The one permanent public API this adds is the price of making a leak structurally impossible.

    What is ruled

    1. @object-ui/fields exports a masked-type predicate and the set behind it; the authority reads the live renderer registration where it can (so registerFieldRenderer('password', X) at runtime is seen), falling back to the declared set.
    2. packages/plugin-detail/src/fieldEnrichment.ts's MASKED_CELL_FIELD_TYPES becomes an import of that authority; pin: registering a new masked type in fields makes the detail page refuse copy without any edit there.
    3. Protocol-first check, first step of the dispatch: read packages/spec's field-type declarations for an existing "sensitive / masked" fact. If the spec declares it, the authority DERIVES from the spec; if not, fields owns the set for now and a domain:spec card is filed to lift the fact into the protocol — ⛔ not decided here which it is.
    4. Clause-②: yes (a new exported symbol on a published package), contract-review carrier.

    State

    needs-user-decision → pm:queue; domain:ui / plugin / priority:p3 kept.


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 1 — domain:ui execution seat
    Session: session_01BA3nKVUwKQJf8DBxrSVtNC
    Branch: claude/issue-8686-masked-field-type-authority
    Worktree: objectui-issue-8686
    Domain: domain:ui
    Seat: domain:ui#1
    File surface: packages/fields/src/index.tsx (the password / secret entries of getCellRenderer's standard map and the new exported masked-type predicate and set beside them only), the @object-ui/fields barrel only if the export needs it, packages/plugin-detail/src/fieldEnrichment.ts (MASKED_CELL_FIELD_TYPES / isMaskedDetailFieldType only), pins beside the existing masked-copy tests in both packages, and one .changeset/8686-…md (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (default judgement tier) — priority:p3: one fact ("is this field type masked?") with two owners, drifting toward disclosure
    Clause-②: yes
    Thread-read: 5644350822
    Serial constraints cleared: open-PR file lists read 2026-09-25T08:58Z ⇒ none names packages/fields/src/index.tsx or fieldEnrichment.ts. PR objectui#10552 edits plugin-detail/src/index.tsx, a different file. objectui#8678 (seat 5) is closed and its PR objectui#10529 merged, which was this card's prior hold. ⚠️ Live claim objectui#10493 (another seat) may touch ImageCellRenderer in packages/fields/src/index.tsx. ⛔ This card stays in the password / secret entries and its new export, which are hunk-disjoint.

    Scope

    The ruling 5644350822 (decision batch #122 item 4, letter A, maintainer 「同意」):

    1. Protocol-first check, first step: read packages/spec's field-type declarations for an existing "sensitive / masked" fact. If the spec declares it, the authority DERIVES from the spec. If not, fields owns the set for now, and the dev reports it so the seat files a domain:spec card to lift the fact into the protocol. ⛔ Not decided in advance which it is.
    2. @object-ui/fields exports a masked-type predicate and the set behind it. The authority reads the live renderer registration where it can (so registerFieldRenderer('password', X) at runtime is seen), falling back to the declared set.
    3. fieldEnrichment.ts's MASKED_CELL_FIELD_TYPES becomes an import of that authority. Pin: registering a new masked type in fields makes the detail page refuse copy with no edit there.
    4. Clause-②: yes (a new exported symbol on a published package), carried by a contract review.

    ⇒ Fixes #8686.

    domain:ui seat #1 · session_01BA3nKVUwKQJf8DBxrSVtNC · claim · 2026-09-25T08:59Z

  5. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 8686,
    "status": "done",
    "branch": "claude/issue-8686-masked-field-type-authority",
    "pr": "#10568",
    "session": "session_01BA3nKVUwKQJf8DBxrSVtNC — harness-stamped Claude-Session of this run (mode:subagent, so it is the parent seat's id)",
    "premise_still_valid": true,
    "summary": "Protocol-first check came first. Neither the installed @objectstack/spec 17.4.0 nor objectstack origin/main 95ab93f5 declares which field types are masked. The negative git grep exited 1; the positive control (the same form finding BOUNDED_STRING_FIELD_TYPES) exited 0. The spec carries only FieldType prose and SECRET_MASK (the mask string), and objectql's collectMaskedReadFields hard-codes the types. So fields owns the set for now, and the domain:spec lift is below as class b. @object-ui/fields now exports MASKED_FIELD_TYPES (ReadonlySet, password and secret) and isMaskedFieldType(). The standard cell table's masked entries are spread from the set, so the set and the drawn mask are one fact. The predicate reads the live registry: THE mask under any type = true; an undeclared type otherwise = false; a declared type whose mask was replaced by one of the package's own renderers = false (the override is seen, the cell shows the value); replaced by an opaque host component = true (falls back to the declared set, on the side that withholds). plugin-detail's fieldEnrichment.ts drops its local Set; isMaskedDetailFieldType asks isMaskedFieldType per type, keeping the narrow-only union. DetailSection.tsx is untouched. Premise check: since objectui#10529 the mask is a named MaskedCellRenderer rather than two anonymous arrows, but there was still no export and no predicate, so the premise held. Docs are a named gap: the AGENTS.md #2 README paragraph is outside the claim's file surface; see open_questions. The worktree has been removed and the branch head a47de0e is on the remote.",
    "tests": "All runs at head a47de0e, every heavy run via os-verify-lock (VERDICT command-exit 0 each). (1) pnpm exec vitest run packages/fields/ packages/plugin-detail/ from the repo root: Test Files 408 passed, 2 skipped (410); Tests 5351 passed, 15 skipped (5366). (2) turbo run build --filter='@object-ui/plugin-detail^...' --concurrency=2: 11/11 tasks. Then fields and plugin-detail type-check: both Done. tsc -p tsconfig.test.json --listFiles counts 1 hit for each new pin file. The fresh worktree had no dist, and dist/index.d.ts declares MASKED_FIELD_TYPES and isMaskedFieldType, so plugin-detail type-checked against the rebuilt declarations. (3) New pins: packages/fields/src/tests/isMaskedFieldType-8686.test.tsx (DECLARED with a lit text control; CENSUS over listCellRendererTypes, where the predicate equals 'the resolved renderer is the mask' on every type; RUNTIME: four override rows with control legs) and packages/plugin-detail/src/tests/DetailSection.maskedTypeAuthority-8686.test.tsx (a new masked type registered via registerFieldRenderer with getCellRenderer('password') refuses the copy on every desktop path with no plugin-detail edit; the control leg before the registration copies the raw value; the same-tree ordinary row copies; a TextCellRenderer override of password copies again). (4) Ablation, ruling pin (b): objectstack scripts/ablation-replace.mjs in wrap mode under the lock re-inlined the local two-member Set in fieldEnrichment.ts in place of isMaskedFieldType(fieldType). Tool-verified landing: anchor x1 to x0, replacement x0 to x1, blob 7fd637ea4e1a to ae9a46f0874a; in-process disk count anchor=0 inlined=1. Result: Tests 2 failed, 22 passed (24). Both 8686 detail pins went red, pin (b) on its payload assertion ('expected [ sk_live_objectui_8686, …(3) ] to deeply equal []'); the 8440 file stayed green. Restored blob 7fd637ea4e1a == HEAD blob, git diff HEAD empty. The expected direction was red, and red was observed. (5) Gates: check:control-bytes OK; check:new-line-citations 0 new; check-changeset-presence OK (1 changeset); check-changeset-no-major OK; check:changeset-claims exit 0; check:spec-symbols OK; check:vi-mock-inherit OK (828 inherit); check:test-path-roots, check:unreferenced-sources, check:self-import and check:comment-mask-corpus OK. NOT MEASURED: check:readme-exports as a whole, reason: exit 1 on a collapsed population, 25 packages unbuilt in the worktree. Its --list judged the 5 fields README self-imports real, and the diff touches no README. fields package.json defines no export-census script. (6) ESLint --no-inline-config --format json over the 4 touched source and test files (4 results), per rule vs base: index.tsx react-refresh/only-export-components warn 93 to 95 (the two new non-component exports), no-explicit-any 44 to 44, no-unused-vars 1 to 1; fieldEnrichment.ts no-explicit-any 5 to 5; new tests 0. Type-aware linting is not enabled in eslint.config.js, so untouched files cannot move. Downstream consumer suites (app-shell, plugin-view, console) were not run locally: the public face change is additive, and CI runs them. Side note: one read-only git fetch of origin main in the shared objectstack checkout (refs only, no file edit) for the protocol-first read.",
    "mcp_calls": "0 — no MCP GitHub tool used, read or write",
    "api_writes": "2 relay dispatches, each one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) pr_create, which became POST /repos/objectstack-ai/objectui/pulls (draft) and opened PR 10568; the stored body was read back byte-identical, 10639 bytes, no differing index; (2) this os-dev-report comment, POST /repos//issues/8686/comments via post-stamped. Plus 2 git pushes of the branch (the empty-branch probe and a47de0e). Zero label writes.",
    "open_questions": [
    {
    "question": "AGENTS.md #2 (docs-driven) asks for a packages/fields/README.md paragraph beside the listCellRendererTypes section documenting MASKED_FIELD_TYPES and isMaskedFieldType, including the host idiom registerFieldRenderer(type, getCellRenderer('password')). That file is outside the claim's declared file surface, so it was not written (the claim says stop on breach).",
    "options": ["A: the seat widens the surface to packages/fields/README.md and sends a patch round on PR 10568 (one paragraph, no code)", "B: a docs-only follow-up PR after this lands"],
    "recommendation": "A, because the repo rule treats the feature as not done until the README reflects it, and one paragraph in the same PR keeps the contract review reading code and docs together"
    },
    {
    "question": "For the Clause-② contract review: how should isMaskedFieldType answer a declared masked type whose mask a host replaced with its OWN component? It is implemented as the ruling's declared-set fallback (true).",
    "options": ["A (implemented): an opaque override keeps the declared answer (true); only an override with one of the package's own renderers unmasks (false)", "B: pure identity, where any non-mask renderer answers false"],
    "recommendation": "A, because B answers false for a host's custom mask. The detail page would then offer one-click copy of a credential the screen hides, a regression against the old mirror in the disclosure direction objectui#8440 shut. A only ever answers false for a declared type when it can see that the cell shows the value."
    }
    ],
    "out_of_scope_findings": [
    "class: b · contract text: @objectstack/spec data/field.zod.ts FieldType comments say a generic 'password' is 'masked to SECRET_MASK on read' and 'secret' 'masks it on read', yet the protocol exports no set or flag naming the masked field types (installed 17.4.0 and objectstack origin/main 95ab93f5: negative git grep exit 1, BOUNDED_STRING_FIELD_TYPES control exit 0). The runtime keeps its own list in objectql collectMaskedReadFields (type === 'secret', and 'password' unless managedBy better-auth), and objectui now keeps MASKED_FIELD_TYPES; two owners of one protocol fact. Seam: spec:FieldType (prose only, no exported masked-type set) → runtime:objectql collectMaskedReadFields | renderer:@object-ui/fields MASKED_FIELD_TYPES / isMaskedFieldType · this is the domain:spec lift the objectui#8686 ruling item 3 requires; file in objectstack-ai/objectstack with a back-link to objectui#8686 · dedupe words: masked field types spec; credential field type set; collectMaskedReadFields; SECRET_MASK field type; sensitive field type protocol",
    "class: a · ObjectGrid desktop: a password column draws the mask, yet Ctrl+C (or Cmd+C) on the focused cell writes the RAW value to the clipboard. Reproduced with a throwaway probe (ObjectGrid plus SchemaRendererProvider, schema api_key type password, value RAW-PROBE-CREDENTIAL, window width 1280, keyDown key c with ctrlKey on the masked td, then on the name td as control): cellFound true, rawInDom false, clipboard payloads [RAW-PROBE-CREDENTIAL, Row one]. Probe deleted, not committed. Cause: packages/components/src/renderers/complex/data-table.tsx handleCellKeyDown copies row[columnKey] for every cell (each td has tabIndex 0). This is the objectui#8440 defect class on the grid surface; the fix is a natural second consumer of isMaskedFieldType, and components cannot import fields, so plugin-grid would pass a per-column flag · recommend an independent objectui card with a Blocked-by: objectui#8686 line · dedupe words: grid ctrl+c password raw; DataTable handleCellKeyDown clipboard; masked cell copy grid; credential clipboard grid; 8440 grid",
    "carrier: whoever takes the grid Ctrl+C card · plugin-grid exports useCellClipboard, which also copies raw values (String of row[field]) with no masked-type check, but it has zero in-repo callers (zero pull) · noted in PR 10568 Acceptance notes, not filed"
    ]
    }


    Generated by Claude Code

  6. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 8686,
    "addendum": "round 2: the seat's answers on PR 10568 (Q1 = A: README paragraph; Q2 = A: no change)",
    "status": "done",
    "pr": "#10568",
    "head": "de6db2c31",
    "change": "packages/fields/README.md only: one prose paragraph under a new heading, 'Asking whether a cell is masked', beside the listCellRendererTypes section. It covers what isMaskedFieldType answers, MASKED_FIELD_TYPES, the live-registry reading (a package-renderer override unmasks; an opaque host override keeps the declared true; a host component under an undeclared type is false), the host idiom registerFieldRenderer(type, getCellRenderer('password')), and raw spellings. It states only what the 8686 pins measure. No code changed since a47de0e, and no line addresses. The PR body was not edited.",
    "gates": [
    "check:readme-exports EXIT 0: 'OK (43 tracked README(s) ... 543 of them self-imports judged (543 real, 0 wrong-path, 0 fabricated); 3369 export symbol(s) read from 36 of 40 tracked package(s) (1 unbuilt, 3 declare no types)'. The gate has no per-package flag, so this is the whole-tree run, made measurable by the scoped build below. The new paragraph is prose with no fenced import, so the gate judges none of its names. Checked by hand instead: fields dist/index.d.ts exports isMaskedFieldType, MASKED_FIELD_TYPES, registerFieldRenderer, getCellRenderer, TextCellRenderer and listCellRendererTypes, and isInlineExcludedFieldType comes through its export-star of FieldEditWidget.",
    "check:doc-snippets EXIT 0: 'Semantic phase: 674 of 674 block(s) judged, 0 failed.' The first run was exit 2, PRECONDITION NOT MET (unbuilt packages). After the gate's own --build-filter turbo build (35/35 tasks, under the verify lock), the rerun passed.",
    "check:doc-fences EXIT 0. docs:check-links EXIT 0: 'Links are valid across 17 scan roots.'",
    "check:control-bytes EXIT 0: 'OK (scanned 8647 tracked text file(s); skipped 85 binary)'",
    "check:new-line-citations EXIT 0: 'VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0'",
    "check-changeset-presence EXIT 0: '4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s): .changeset/8686-masked-field-type-authority.md'",
    "check:changeset-claims EXIT 0. The self-contradiction reading is clean. It lists 4 pending changesets that name packages/fields/src/index.tsx (6625, 6694, 6837, 7166). I read those paragraphs: they describe decimals and relational-meta reads and LookupCellRenderer, none of which this change touches. All four are still true, so no correction."
    ],
    "api_writes": "1 this round: this comment (relay POST /repos/objectstack-ai/objectstack/dispatches, which became POST /repos//issues/8686/comments). Plus 1 git push (a47de0e to de6db2c). Cumulative 3 relay writes. No PR body edit and no label writes.",
    "mcp_calls": "0"
    }


    Generated by Claude Code

  7. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    ✅ ACCEPT — PR objectui#10568 at 23030c656 · contract review PASS at de6db2c31, and its two prose notes cleared by a prose-only patch round · entering the merge queue once CI is green

    domain:ui seat #1, session_01BA3nKVUwKQJf8DBxrSVtNC. The claim 5829739042 says Clause-②: yes: MASKED_FIELD_TYPES and isMaskedFieldType are new exports of @object-ui/fields. A review-tier contract review ran at de6db2c31. Its record is below, verbatim.

    Implemented-by:  claude/issue-8686-masked-field-type-authority
    Reviewed-by:     session_01BA3nKVUwKQJf8DBxrSVtNC
    

    The review's verdict. PASS, with no blocking item.

    • ① Every code judgment passes. The spec declares no masked-type fact: a negative grep with a positive control, on the installed 17.4.0 and on objectstack origin/main.
    • Both symbols are in the built dist/index.d.ts. The table's masked entries are spread from the set, and the predicate's five rows match the ruling's words and are pinned.
    • fieldEnrichment.ts keeps no copy. The reviewer's own ablation (the Set re-inlined in plugin-detail) turned both objectui#8686 detail pins red, and the tree was restored by blob.
    • The full fields + plugin-detail suite is green at head (408 files, 5351 tests). Both type-checks are clean, and the merge with main is conflict-free with no file overlap.
    • ② minor on @object-ui/fields and patch on @object-ui/plugin-detail are correct. No pending changeset is falsified (8440, 8678, 8395, and the four check:changeset-claims lists, re-measured).

    How the prose notes were cleared. ③ 5 named two clauses that ship and over-state. The seat ordered a prose-only round. At 23030c656, the seat read the delta in full: 3 files, +19 / −15, and no code, test or level change.

    • "none of them masks, so the cell now shows the value" is rewritten in the isMaskedFieldType docblock, the README and the changeset as "none of them is the mask … the cell draws what that renderer draws (for TextCellRenderer, the value)". The review's census found six package renderers that draw a literal or a dash.
    • The README's "agrees with what getCellRenderer resolves for every type" gains "with nothing overridden at runtime" (the review's probe P3).
    • The PR body's Design-table row carried the same clause. The seat corrected it in the same words with this ACCEPT.
    • Gates at 23030c656: doc fences, doc links, changeset claims, control bytes and new line citations exit 0, and the four masked-cell pin files pass 71 of 71.

    The seat's answers during the round (recorded in the dev's addendum 5830583062):

    • Q1 = A: the twin is documented as a README paragraph. That is a file-surface extension beyond the claim, taken under objectui AGENTS.md Add automated testing infrastructure and CI/CD workflows #2 (docs-driven). content/docs/guide/*.md documents neither twin, so the README is the level at which both are documented (review ③ 9).
    • Q2 = A: an opaque host override that prints its value still answers true. The declared type stays refused. No change.

    Out-of-scope items — each one routed

    • The domain:spec lift of the masked-type fact ⇒ filed finding(spec): the protocol declares no masked field-type set — objectql's collectMaskedReadFields and objectui's MASKED_FIELD_TYPES each own a copy of one fact objectstack#20141. Its body already carries the review's sharpening (③ 1): objectql exempts a managedBy: 'better-auth' password, so the declaration must carry an object-level exemption as well as a type set.
    • The mask glyph: the spec's SECRET_MASK is eight bullets, while MaskedCellRenderer draws six and PasswordField eight (③ 1). This is a display glyph, not a served value ⇒ Acceptance notes. 承接者:无.
    • ObjectGrid desktop Ctrl+C on a masked cell writes the raw value (data-table.tsx handleCellKeyDown) ⇒ filed objectui#10583.
    • useCellClipboard (published from plugin-grid, 0 in-repo callers, no masked check) ⇒ carrier: objectui#10583 (named there).
    • The DetailSection.tsx comments that name password / secret as the masked types (③ 6; true of the shipped defaults) ⇒ Acceptance notes. 承接者:无.
    • MaskedCellRenderer is not exported, so the host idiom depends on password not being overridden first (③ 7; the README states the precondition) ⇒ Acceptance notes. 承接者:无.
    • isMaskedFieldType rebuilds the standard table per call, as getCellRenderer does (③ 8) ⇒ Acceptance notes. 承接者:无.
    Contract review record (verbatim, head de6db2c31)

    Contract review

    Served-tier: CONTRACT_REVIEW_TIER
    Head-sha: de6db2c31a2dfb39d307f85f9a878206b29ea42b

    Reviewed from 2026-09-25T10:06Z to 2026-09-25T10:43Z against origin/main a50600166466958f066ff123d7c2ceefbdf22455 (merge-base 9cbe4dbca1f7dbbe2dee45fd3d7d0f9c54fa53ff; main is 13 commits ahead of the merge-base, touching 97 files, none of them a file of this PR; the PR is a draft, mergeable: true, state behind, 2 commits — a47de0e0c the code and pins, de6db2c31 the README paragraph — and the REST changed_files: 6 is the same list as git diff --name-only <merge-base> <head>). Head at dispatch de6db2c31 is still the head. Inputs read in full: the PR body; the card objectui#8686 body and all 5 comments, the triage 5619981116, the ruling 5644350822, the claim 5829739042, the report 5830184679 and the README addendum 5830583062; objectui#8440's body, its ruling 5578821128, dispatch 5592269991, report 5592631894 and review 5592660545; the PR objectui#10529 body; the complete diff of the 6 files and both new pin files; objectui AGENTS.md #2 (docs-driven), #11 (citations) and the changeset rules; objectstack AGENTS.md Post-Task Checklist step 3; the installed @objectstack/spec 17.4.0 package (src, dist, api-surface, json-schema, llms.txt) and objectstack origin/main a08e059c61f6ea4784d400c40ddedd3c81495bef (packages/spec/src/data/field.zod.ts, secret-mask.ts, packages/objectql/src/secret-fields.ts; the dev read 95ab93f5, main has moved, so every reading was repeated at the current tip); the new changeset and the pending changesets that name a touched file or the masked pair (8440, 8678, 8395, 6625, 6694, 6837-reference-to-arm-deletion, 7166); the two finding cards the seat filed from the dev's report, objectui#10583 and objectstack#20141; the prior record scratchpad/pr-10534/cr.md.

    Method. One detached scratch worktree scratchpad/pr-10568/wt at head, installed with pnpm install --offline --frozen-lockfile (5.2 s; head pnpm-lock.yaml is identical to origin/main's; @objectstack/spec resolves to 17.4.0 from packages/fields). Every vitest, build and tsc run went through os-verify-lock.sh in three lock turns under the slot pr-10568-review (turn 1 waited 487 s behind a shard run and held 24 s; turn 2 waited 0 s and held 77 s; turn 3 waited 338 s and held 537 s, of which the full suite was 527 s — shared-box seconds, as the wrapper's verdict line says). Readings come from the PR's two pins, the untouched objectui#8440 pin, one throwaway probe file in packages/fields/src/__tests__/ (run once with console.log, which this repository's vitest reporter does not print, then again writing its rows to a scratch .jsonl; deleted before the full suite and at the end), the repository's own gates, ESLint via --stdin --stdin-filename for the merge-base counts, and git grep over both repositories. The vitest.config.mts alias map resolves @object-ui/fields and @object-ui/plugin-detail to src, so the test legs measure the sources; the type legs measure the dist. No GitHub write of any kind (the search endpoint is repo-bound here, so dedupe used repo-scoped listings); the shared checkouts were only fetched and read; the worktree was removed at the end with git status --porcelain empty.

    ① Derived judgments

    1. Ruling item 3, protocol-first. Installed spec 17.4.0, the whole package directory: grep -rIEl 'MASKED_FIELD_TYPES|MASKED_TYPES|SENSITIVE_FIELD|CREDENTIAL_FIELD_TYPES|SECRET_FIELD_TYPES|isMaskedFieldType|isSensitiveFieldType|MASK_ON_READ|maskedFieldTypes|MASKED_READ' → exit 1, 0 files; the positive control BOUNDED_STRING_FIELD_TYPES in the same form → exit 0 (src/data/field.zod.ts, src/ui/view.zod.ts, dist/identity/index.js, …). objectstack origin/main a08e059c6: git grep -n -E <same tokens> origin/main -- packages/spec → exit 1, 0 lines; control BOUNDED_STRING_FIELD_TYPES: ReadonlySet → exit 0, packages/spec/src/data/field.zod.ts:136. What the spec does carry, read: the FieldType enum comments ("password … plaintext at rest but masked to SECRET_MASK on read", "secret … masks it on read"), export const SECRET_MASK = '••••••••' in secret-mask.ts (the mask STRING, eight bullets), and the internal docblock's own sentence "collectMaskedReadFields collects by TYPE". The runtime's list, packages/objectql/src/secret-fields.ts collectMaskedReadFields: def.type === 'secret' always, def.type === 'password' only when the object is not managedBy: 'better-auth'. So no set, flag or predicate is declared; the dev's negative reading holds with its control, and under the ruling fields owns the set for now. The MASKED_FIELD_TYPES docblock records the derivation rule ("if @objectstack/spec comes to declare which field types are credentials, this set derives from that declaration"). The lift is filed as objectstack#20141 (created 2026-09-25T10:08:34Z, label finding, back-links objectui#8686, names collectMaskedReadFields and MASKED_FIELD_TYPES). PASS

    2. Ruling item 1: the exports, one fact, and the live-registry semantics. The diff adds export const MASKED_FIELD_TYPES: ReadonlySet<string> = new Set(['password', 'secret']) and export function isMaskedFieldType(fieldType: string | undefined): boolean to packages/fields/src/index.tsx, which is the package barrel (package.json exports: {'.', './style.css'}, types: ./dist/index.d.ts); the built dist/index.d.ts (turn 2) declares export declare const MASKED_FIELD_TYPES: ReadonlySet<string>; and export declare function isMaskedFieldType(fieldType: string | undefined): boolean;. The table's two literal entries are replaced by ...Object.fromEntries([...MASKED_FIELD_TYPES].map((type) => [type, MaskedCellRenderer])), so the drawn mask and the set are one fact; measured by the CENSUS pin and probe P4: over listCellRendererTypes() the types resolving to the mask are exactly the declared set (53 types listed, ["password","secret"] on both sides; every type resolves to one function object across two resolutions, P4_IDENTITY_STABLE true, so the identity comparison the predicate makes is sound). The predicate reads fieldRegistry first and the standard table second, the order getCellRenderer uses (read: getCellRenderer differs only by reportRetiredFieldType and the TextCellRenderer total fallback, neither of which can produce the mask). Rows, measured by isMaskedFieldType-8686.test.tsx (10 tests, green in turn 1) and the probe: a declared type (password, secret) → true, the cell draws •••••• with the raw value absent; an undeclared type (text, '', undefined, an unregistered spelling, field:password) → false, the text control draws the value; the mask registered under a new type (registerFieldRenderer('objectui_8686_api_token', getCellRenderer('password'))) → true with the set unedited and the cell masked; a declared type overridden by TextCellRenderer → the cell shows the value and the predicate answers false; a declared type overridden by an opaque host component → true (the declared-set fallback, the seat's decision A); a host component under an undeclared type → false. This matches the ruling's words "reads the live renderer registration where it can …, falling back to the declared set". The two rows where the predicate and the cell can part, named: (a) true while the cell shows the raw value — a declared type overridden by a host component that PRINTS its value: probe P2, registerFieldRenderer('secret', ({ value }) => <span>{String(value)}</span>) → { pred: true, rawInDom: true }; this is decision A's accepted cost, in the withholding direction (the detail page refuses to copy a value the host chose to show), and the changeset and README state it. (b) false while the cell still hides the value — a declared type overridden by one of THIS package's renderers that draws a literal instead of the value: probe P1 registered every distinct standard renderer (27 over the 53 listed types) under password and rendered each with a probe string: 20 of 27 show the raw string in the DOM with the predicate false (address, auto_number, color, composite, currency, email, formula, geolocation, html, lookup, markdown, number, percent, phone, url, user print it; audio, avatar, signature carry it in an attribute); the mask itself is the one true; and 6 answer false while the raw string is absent from the DOM — vector draws [Vector], grid draws [Grid], and boolean, date, datetime and repeater draw the — dash for a string they cannot read (checkboxes, the 27th, draws the string humanised, "REVIEW 8686 RAW VALUE Xyz", which is shown, not hidden). No package renderer answers true while showing the value (P1_TRUE_WHILE_SHOWN []). For those six renderers the docblock's and README's sentence "none of them masks, so the cell now shows the value" over-states; the consequence (the detail row would copy a credential whose cell shows [Vector]) needs a host to register a non-mask package renderer over a credential type on purpose, which is not the drift this card closes. A one-clause tightening ("the cell no longer draws the mask") is enough; no code change is needed. PASS, with the two rows named

    3. Ruling item 2: plugin-detail delegates, and the ruling's pin. grep -n -E "MASKED_CELL_FIELD_TYPES|new Set|'password'|'secret'" packages/plugin-detail/src/fieldEnrichment.ts at head: the only new Set literals are TEXTUAL_REF_FALLBACK_TYPES and DETAIL_ROUTED_INLINE_TYPES; 'password' occurs once, in the docblock (registerFieldRenderer('password', …)); MASKED_CELL_FIELD_TYPES 0 hits, and git grep MASKED_CELL_FIELD_TYPES -- . ':!.changeset' over the tracked tree is empty. isMaskedDetailFieldType(view, object) is isMaskedForDetail(view) || isMaskedForDetail(object) and isMaskedForDetail is typeof fieldType === 'string' && isMaskedFieldType(fieldType), so the narrow-only union (objectui#3355) is kept. The ruling's pin, DetailSection.maskedTypeAuthority-8686.test.tsx: the new type's spelling objectui_8686_api_token has 0 hits in packages/plugin-detail/src outside that test (grep); a control leg first renders the unregistered type, sees the raw value and copies it; after registerFieldRenderer(NEW, getCellRenderer('password')) the row draws the mask, the raw value is absent from document.body, the same-tree ordinary row still copies, and click, Enter, Space and the hover button write nothing (payloads() → [], no button, no role). Ablation, run by me in turn 1: the import narrowed to isInlineExcludedFieldType and isMaskedForDetail's body replaced by MASKED_CELL_FIELD_TYPES.has(fieldType) over a re-inlined two-member Set (anchor_count_after=0 inlined_count=1; blob 7fd637ea4e1a… → 32c64046cabe…); pnpm exec vitest run over the 8686 and 8440 detail files → Test Files 1 failed | 1 passed (2), Tests 2 failed | 22 passed (24): red "a masked type registered in fields refuses the copy here, with no edit to plugin-detail" (expected [ 'sk_live_objectui_8686', …(3) ] to deeply equal []) and red "a host that replaces a shipped mask with one of the package's text renderers gets its copy back" (expected [] to deeply equal [ 'sk_live_objectui_8686' ]), the 8440 file green (22). Restored by git checkout: blob 7fd637ea4e1a4ce944c1307cf29f84d85f8c0a5d equals HEAD:packages/plugin-detail/src/fieldEnrichment.ts, git status --porcelain empty apart from my probe, deleted afterwards. The 8686 pin exercises the desktop row; the mobile grouped-inset row shares the one gate (DetailSection.tsx: copyOffered = canCopy && !isMaskedField, read), which the 8440 pin still covers for the shipped types. PASS

    4. Reachability. grep -rn -E "isMasked|MASKED_|MaskedCell|'password'|\"password\"|'secret'|\"secret\"" over packages/*/src and apps/*/src, tests excluded: 43 files, every line read. The read-side fact "is this cell drawn as a mask" is decided by type in exactly two places: packages/fields/src/index.tsx (the owner) and packages/plugin-detail/src/fieldEnrichment.ts (now delegating); DetailSection.tsx names the pair only in two comments. Adjacent credential-type lists that decide OTHER facts, untouched and correctly so: fields/src/FieldEditWidget.tsx INLINE_EXCLUDED_FIELD_TYPES (the write-side twin, 'password', 'secret' with the credential comment); fields/src/widgets/deriveLookupColumns.ts NON_TABULAR_TYPES ('secret', 'password', 'encrypted', picker-column derivation); components/src/renderers/form/form.tsx SECRET_FIELD_TYPES = new Set(['password', 'secret']) (the field: prefix refusal) and its native-input map (password/secret → type="password"); plugin-form/src/ObjectForm.tsx field.type === 'password' → inputType; fields/src/widgets/TextField.tsx the same for the input type; app-shell SchemaForm.tsx (format: 'password', SECRET_FIELD_NAME_RE, JSON-schema settings forms) and ActionResultDialog.tsx (format === 'secret', action-result formats). @object-ui/components does not depend on @object-ui/fields (its @object-ui/* deps are core, i18n, react, react-runtime, sdui-parser, types, test-support), so the form-side lists could not read the authority even if they were the same fact. Mask glyphs drawn elsewhere: PasswordField.tsx (the form widget's read-only face, eight bullets), app-shell DatasourcePreview.tsx (redacted datasource config), i18n strings — none a cell renderer. No second owner of the cell-mask fact remains in objectui. PASS

    5. The README paragraph (packages/fields/README.md, "Asking whether a cell is masked", between the listCellRendererTypes() section and "File uploads in line-item grids"; the README ships, files: ["dist","README.md","CHANGELOG.md","LICENSE"]). Sentence by sentence against the code and the pins: the opening definition, the twin, the detail-page consumer, the declared set password / secret, "the standard cell of each member draws the mask, and the predicate answers true for it" — true (DECLARED pin). "Like listCellRendererTypes(), the answer is a live reading of the cell registry taken when you call it, and it agrees with what getCellRenderer resolves for every type that function lists" — true with nothing overridden (CENSUS pin; probe P3 pristine: 0 disagreeing types of 53) and false the moment a declared type carries an opaque host override (probe P3 after registerFieldRenderer('secret', Host): disagreeing types ["secret"], the predicate true while getCellRenderer resolves to the host component), which the paragraph itself states three sentences later; the unconditional "for every type" wants the qualifier "with nothing overridden at runtime". The host idiom registerFieldRenderer('api_token', getCellRenderer('password')) "while password still resolves to the shipped mask" — true, and the precondition is real (probe P5, password overridden by a host component first, then the idiom for a new type: { tokenIsMask: false, pred: false } — the idiom registers the override, not the mask, and the new type is not masked). "The predicate then answers true with no change to the declared set, and the detail page refuses to copy that row" — true (both pins). "Overriding a declared type with one of this package's own renderers unmasks it: after registerFieldRenderer('password', TextCellRenderer) the cell shows the value, the predicate answers false, and the detail row copies again" — true for the example given and pinned; the general clause is the item 2(b) over-statement for the literal-drawing renderers. The opaque-override sentence, the undeclared-host sentence and the raw-spelling sentence — true (RUNTIME pins 3 and 4, the field:password pin). Gates the dev ran, re-run here: check:doc-fences exit 0; docs:check-links exit 0 ("Links are valid across 17 scan roots"); check:readme-exports exit 0 after turn 2's build ("543 self-imports judged (543 real, 0 wrong-path, 0 fabricated); 3369 export symbol(s) read from 36 of 40 tracked package(s)"); check:doc-snippets exit 0 ("Semantic phase: 674 of 674 block(s) judged, 0 failed"). The paragraph carries no fenced import (no fence between README lines 100 and 160), so readme-exports judges none of its names; I read the five names it uses against the dist by hand (isMaskedFieldType, MASKED_FIELD_TYPES, isInlineExcludedFieldType, listCellRendererTypes, registerFieldRenderer, getCellRenderer, TextCellRenderer all in dist/index.d.ts). AGENTS.md Add automated testing infrastructure and CI/CD workflows #2's second half: content/docs/guide/*.md names registerFieldRenderer (guide/fields.md, guide/architecture.md) and neither isInlineExcludedFieldType nor listCellRendererTypes (grep), so the README is the level the twin is documented at; content/docs/fields/password.mdx's "Cell Renderer" section (objectui#10529) stays true. PASS with two prose notes (③ 5)

    6. Tests, types, merge. Turn 3, pnpm exec vitest run packages/fields/ packages/plugin-detail/ from the root at head with the probe already deleted: exit 0, Test Files 408 passed | 2 skipped (410), Tests 5351 passed | 15 skipped (5366), duration 527.09 s — the dev's counts exactly. Turn 1, the four files that matter (isMaskedFieldType-8686, DetailSection.maskedTypeAuthority-8686, DetailSection.maskedCopyRefusal-8440, the probe): Test Files 4 passed (4), Tests 39 passed (39). Both new files are .tsx under packages/**, so they land in the dom project (include: 'packages/**/*.test.tsx', neither is in heavyDomTests), which keeps isolate: true. Types, turn 2: pnpm exec turbo run build $(check-doc-snippet-types --build-filter) --concurrency=2 → Tasks: 35 successful, 35 total, Cached: 35 cached, 35 total, FULL TURBO — every task replayed from the container's shared worktree cache (keyed on this head's inputs; the restored packages/fields/dist/index.d.ts carries the new docblocks and both declarations), so the dist is cache-restored, not rebuilt here; pnpm --filter @object-ui/fields run type-check (tsc --noEmit && tsc -p tsconfig.test.json) exit 0, 0 error TS lines, compiled fresh from source; pnpm --filter @object-ui/plugin-detail run type-check exit 0, 0 error TS, against that dist; tsc -p tsconfig.test.json --listFiles names each new pin once. Merge: git merge-tree --write-tree origin/main de6db2c31 exit 0, tree 9e870dfe7892a306bb4910b9794905eca229db52, no conflict lines. Main's 97 files since the merge-base (13 commits, the last PR objectui#10571 in plugin-charts; 7 under packages/fields/src/widgets/ — DateTimeField.tsx, LookupField.tsx, GridField.tsx, nativeDateValue.ts, useFieldTranslation.ts and two tests — and 1 under packages/plugin-detail/src/renderers/__tests__/) have an empty intersection with the PR's 6; packages/fields/src/index.tsx and fieldEnrichment.ts are untouched on main since the merge-base. PASS

    ② Semver

    One changeset, .changeset/8686-masked-field-type-authority.md: minor on @object-ui/fields, patch on @object-ui/plugin-detail, both in the fixed group (check-changeset-fixed ✅). check-changeset-presence: "4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)"; check-changeset-no-major ✅; check-changeset-overwrite: 1 added, 0 modified, 0 deleted; check:changeset-claims exit 0, self-contradiction clean. objectstack AGENTS.md step 3: Clause-②: yes (PR body line 2, no arm) takes at least minor — met by fields; plugin-detail adds no export and changes behaviour only on the two runtime rows, so patch is right and the fixed group bumps it with the rest. Every sentence of the body against the diff and the readings: the two symbols and their types (dist lines 340 and 369), "additive; nothing existing changes shape" (the diff removes nothing and re-types nothing); the history paragraph ("two entries in getCellRenderer's standard table … the detail page's copy refusal kept its own two-member list") — the 8440 mirror, and the ablation shows exactly the drift it describes; "built from MASKED_FIELD_TYPES" (the spread); the three registry rows (pinned; the TextCellRenderer example is exact, and "since the cell now shows the value" is the item 2(b) over-statement only when generalised past the example); "raw spellings … field:password renders in the clear" (pinned); the plugin-detail paragraph, "asks isMaskedFieldType() instead of keeping its own list … narrow-only union … behaviour changes only where the two used to disagree: a masked type registered at runtime now refuses the copy affordance, and a shipped mask that a host replaced with a package text renderer offers it again" — both changes pinned, and nothing else in the detail gate moved (copyOffered / copyInteractive unchanged in DetailSection.tsx, which is not in the diff). Level correct.

    Pending changesets in the same release. check:changeset-claims lists four that name packages/fields/src/index.tsx — 6625-retire-fieldmeta-decimals, 6694-dashboard-lookup-reference-meta, 6837-reference-to-arm-deletion, 7166-retire-inert-fieldmeta-copies — read in full at the named paragraphs: they state .decimals / .scale member reads, which field-meta keys LookupCellRenderer reads, FieldMetadata readers, and 7166's counts ("zero occurrences of the three retired keys, against a control of 22 occurrences of the display_field / displayField / reference_to spellings"); re-measured on index.tsx at the merge-base and at head, display_field|displayField|reference_to is 26 and 26, the three retired keys 0 and 0, .decimals 0 and 0 — nothing this PR touches. Beyond the gate's list, I read the pending changesets that name the masked pair or the detail gate: 8440-masked-field-copy-refusal.md (plugin-detail: patch) describes the refusal, its five paths, the separate gate outside canCopy and the narrow-only union, and never names the mirror — every sentence stays true at head; 8678-value-independent-cells.md (fields: patch) — "the value of a password or secret is still never printed", true; 8395-detail-copy-object-values.md line 33 is a historical statement about that PR. No pending changeset is falsified. No blocking item.

    ③ Boundary flags

    1. (a) The domain:spec lift, confirmed and sharpened. Three copies of one protocol fact, and they do not agree: the spec's prose (both types "masked on read", with password qualified "on a generic (non-better-auth) object"); objectql's collectMaskedReadFields (secret always, password unless managedBy: 'better-auth'); objectui's MASKED_FIELD_TYPES (both, unconditionally, with no object-level input at all — the predicate takes a type string). So on a managedBy: 'better-auth' object the runtime serves a password column unmasked while the cell draws the mask and the detail page refuses copy: the safe direction, but a divergence a spec declaration must encode as more than a set of types (a type set plus an object-level exemption). Two mask strings as well: the spec's SECRET_MASK is eight bullets, MaskedCellRenderer draws six and PasswordField eight. The card is filed, objectstack#20141 (2026-09-25T10:08:34Z, finding, not graded or routed), and should carry both observations.
    2. (b) ObjectGrid desktop Ctrl+C on a masked cell writes the raw value. Source read at head, not re-probed: packages/components/src/renderers/complex/data-table.tsx handleCellKeyDown — if ((e.ctrlKey || e.metaKey) && e.key === 'c' && !editingCell) { … const value = row[columnKey]; const text = value != null ? String(value) : ''; navigator.clipboard.writeText(text) … } — wired on every body td (onKeyDown={(e) => handleCellKeyDown(e, rowIndex, col.accessorKey)}, tabIndex={0}), with no type or column gate; the cell's face comes from plugin-grid's resolveGridCellRendering → getCellRenderer, the same registry the predicate reads, so the grid draws the mask and the keyboard copies the value. components cannot import fields (deps above), so the consumer is plugin-grid passing a per-column flag, as the dev says. Filed as objectui#10583 (2026-09-25T10:09:07Z, finding, names handleCellKeyDown and useCellClipboard, back-links 8686).
    3. useCellClipboard (packages/plugin-grid/src/useCellClipboard.ts): published from plugin-grid/src/index.tsx (the hook and its three types), builds String(row[field] ?? '') per selected cell with no masked check; in-repo call sites: 0 (grep -rn "useCellClipboard(" outside its own file → exit 1). A published surface with zero pull that would inherit the same defect the moment a host calls it; carried on objectui#10583.
    4. The two predicate/cell rows named in ① 2: true while shown (an opaque host override that prints; decision A) and false while hidden (a package literal renderer over a declared type; probe P1). Neither is reachable without a deliberate host registration; both are prose corrections at most.
    5. Prose in the surface, optional, body-only: the README's "agrees with what getCellRenderer resolves for every type that function lists" and the docblock's / README's / changeset's "none of them masks, so the cell now shows the value" (① 2(b), ① 5). Two clauses.
    6. packages/plugin-detail/src/DetailSection.tsx comments (the isInlineExcluded and isMaskedField blocks) still say "(password / secret)" as the masked types — true of the shipped defaults, prose only, outside the claim's surface and left untouched per the dispatch.
    7. MaskedCellRenderer is not exported; the documented host idiom reaches it through getCellRenderer('password'), which returns whatever password currently resolves to — after a host override of password the idiom registers the override, not the mask (probe P5: tokenIsMask false, pred false, so the new type's cell draws the host's face and the detail page would copy it). The README states the precondition. A named export of the mask renderer would remove the ordering dependency; the seat's call, not this card's.
    8. isMaskedFieldType rebuilds the standard table on every call, as getCellRenderer does (the table is a function by design since objectui#8678); DetailSection asks it twice per row per render. Not a regression against the resolver it mirrors; noted.
    9. AGENTS.md Add automated testing infrastructure and CI/CD workflows #2's content/docs/guide/*.md half is not written; the guide does not document the twin either, so this is parity, and the seat chose the README (Q1 = A).
    10. ESLint, reproduced: packages/fields/src/index.tsx base → head react-refresh/only-export-components 93 → 95, @typescript-eslint/no-explicit-any 44 → 44, no-unused-vars 1 → 1; fieldEnrichment.ts no-explicit-any 5 → 5; the two new tests 0 — the dev's numbers exactly. check:new-line-citations: 0 new (AGENTS.md [WIP] Update documentation for project #11 holds; the diff cites by symbol and card).
    11. Light gates run here, all exit 0: check-changeset-presence, -no-major, -overwrite, -fixed, check:changeset-claims, check:new-line-citations, check:control-bytes, check:doc-fences, docs:check-links, check:readme-exports, check:doc-snippets, check:spec-symbols, check:test-path-roots, check:self-import, check:unreferenced-sources, check:comment-mask-corpus (1 file, inside the residue objectui#7882 holds open), check:vi-mock-inherit, check:pending-changeset-literals.
    12. Not measured here: the downstream suites the dev also left to CI (app-shell, plugin-view, console); the build's 35 tasks were all cache replays (① 6), so the compile of the dist itself was not observed in this worktree, only its content and the fresh tsc legs.
    13. The PR is a draft with mergeable_state: behind; the 13 main commits since the merge-base do not overlap its files (① 6).

    Verdict

    PASS. No blocking item. Every code judgment in ① passes: the spec declares no masked-type fact (negative grep with a positive control, on the installed 17.4.0 and on objectstack origin/main), the two symbols are published from the barrel and the dist, the table's masked entries are spread from the set, the predicate's five rows match the ruling's words and are pinned, fieldEnrichment.ts keeps no copy and the ruling's pin goes red under my own ablation and is restored by blob, no second owner of the cell-mask fact remains, the README's names resolve and its gates pass, the full fields + plugin-detail suite is green at head (5351 tests), both type-checks are clean, and the merge with origin/main is conflict-free with no file overlap. ② — the changeset's levels and sentences hold, and no pending changeset is falsified. What remains is prose (③ 5: two clauses in the README / docblock / changeset that over-state — "agrees with what getCellRenderer resolves for every type" without the no-override qualifier, and "none of them masks, so the cell now shows the value" for the six literal-drawing renderers) and the two filed cards (objectstack#20141, objectui#10583), which the seat should carry with the sharpenings in ③ 1 and ③ 3.

    domain:ui seat #1 · review · 2026-09-25T11:01Z

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

Metadata

Metadata

Assignees

No one assigned

    Labels

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions