Skip to content

feat(lint)!: report a searchableFields entry that names a secret or password field - #27811

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-27705-searchable-credential-fields
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-27705-searchable-credential-fields

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by 田騏 · project thread

Fixes #27705

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: follow-up #27660

维护者速读

  • 改了什么: os validate、os build 和元数据发布门现在把 searchableFields 里指向 secret / password 字段的条目报为错误(沿用规则 searchable-field-unsearchable),对象自身集合、列表视图、React 页面 ListView 组件的 searchableFields 属性 全覆盖。
  • 为什么改: 引擎搜索时会静默跳过凭据字段,作者声明的搜索范围悄悄变窄;全是凭据字段时还会退回自动默认集。现在写的时候就告诉作者,严重级别与 formula 条目一致。
  • 风险与回滚: 这是收窄:以前能通过的元数据现在会报错。仓内没有这样的元数据(dogfood 全绿)。引擎行为不变。回滚就是 revert 本 PR。
  • 先看: packages/lint/src/validate-searchable-fields.ts(新增的凭据检查段,位于 formula 检查之后、可接受性检查之前)、packages/spec/src/data/masked-field-types.ts(新谓词 isCredentialFieldType)、packages/objectql/src/secret-fields.ts(一行替换,行为不变)。
  • 两分钟验证: pnpm --filter @objectstack/lint exec vitest run src/validate-searchable-fields.test.ts,预期全部通过,其中包括 "a secret or password entry" 这一组。
  • 需要你做的: 无,席位落地。

Independent review: none run (quota); the PR waits for the seat's contract-review hand-off.

What changes

  • packages/lint/src/validate-searchable-fields.ts. A credential check sits beside the formula check from A virtual formula field declared in searchableFields clears lint and the #4254 gate, then silently matches nothing — the same fail-open shape #4254 closed one axis over #6674. It reports a searchableFields entry whose field type is secret or password under searchable-field-unsearchable at error. It runs on every surface the virtual check covers: the object's own set, an object's built-in list views, defineView lists and named list views, a flattened list overlay, a ViewItem record, and a react page's ListView searchableFields prop (through the shared checkSearchableFieldList core). The remedy line is Drop "NAME": credential fields are never searched. The module note (check 4) gives the reason for the severity. The check runs before the compiler/validate: view searchableFields not validated against searchable field types — a declaration typo 400s the whole list at runtime #4830 admissibility pass, so a view over an auto-default object gets this verdict instead of the generic "of type 'password'… target a text-like field".
  • packages/spec/src/data/masked-field-types.ts. Adds isCredentialFieldType(fieldType). It answers whether a type is masked on read on at least one object, which today means secret and password. It reads that from the keys of MASKED_ON_READ_FIELD_TYPES, so it adds no second type literal, and it ignores the better-auth exemption. isMaskedOnReadFieldType now uses it for its own-key test, and its answers are unchanged.
  • packages/objectql/src/secret-fields.ts. collectCredentialFields now reads isCredentialFieldType instead of its own def.type === 'secret' || def.type === 'password' literal. This is the one-line swap the order allows. The linter and the engine's search withhold (expandSearchOnAst, unchanged) now judge credential types from one predicate. packages/objectql/src/engine.ts is not touched.
  • packages/lint/src/rule-explanations.ts. os explain searchable-field-unsearchable gains a paragraph on what the engine does with a credential entry on each surface, why there is no exemption, and why the finding is an error.
  • packages/spec/api-surface/data.json, packages/spec/export-origins/data.json. Regenerated by check:generated --fix (one line each, the new export).
  • .changeset/27705-searchable-credential-fields.md. @objectstack/lint minor, @objectstack/spec and @objectstack/objectql patch. It carries both declaration lines and an ADR-0087 not-required (no-migration-prescription) disposition, the precedent of 27225-mail-formatter-shape-lint.md.

Tests

  • validate-searchable-fields.test.ts: secret and password on the object's own set, and on a list view narrowing (it.each). Also a view entry the object itself declares (both findings), a better-auth password (exemption-free), a control where text and email entries in the same declarations stay clean, and the os explain text. The fix:-line coverage control adds the credential arm.
  • validate-react-page-props.test.ts: a ListView searchableFields={['name', 'api_key', 'portal_password']} prop reports both credential entries at error.
  • runtime-gate.object-writes.test.ts: the object publish door refuses searchableFields: ['owner', 'api_key'] (api_key is secret).
  • masked-field-types.test.ts: isCredentialFieldType over every FieldType, its agreement with "masked on some object", the one cell where it and isMaskedOnReadFieldType differ (password × better-auth), and unvalidated input.

Acceptance notes

Verification

Ablation: new pins fail without the fix

node scripts/ablation-replace.mjs --file packages/lint/src/validate-searchable-fields.ts --anchor " if (isCredentialFieldType(ctype)) {" --replacement " if (false && isCredentialFieldType(ctype)) {" -- …vitest run src/validate-searchable-fields.test.ts src/validate-react-page-props.test.ts

  • mutation landed: anchor 1 to 0, blob 1343dd89e129 to 892e53b49026
  • Tests 8 failed | 241 passed (249): the 7 credential pins plus the fix:-line coverage control. The text-field control and the os explain pin stayed green, as expected.
  • restored: blob == HEAD (1343dd89e129), git diff HEAD empty
Package tests and typecheck
  • @objectstack/lint test: Test Files 178 passed (178), Tests 7633 passed (7633). typecheck: tsc --noEmit clean, check:test-typecheck: OK.
  • @objectstack/spec test: Test Files 755 passed (755), Tests 22452 passed | 1 todo. typecheck exit 0.
  • @objectstack/objectql test (--project local): Test Files 490 passed (490), Tests 9244 passed (9244). typecheck: check:test-typecheck: OK.
  • @objectstack/spec check:generated: 2 artifacts stale before regeneration (api-surface, export-origins) and regenerated with --fix.
Gates (dispatch-gates --commands, 85 families, at 257e83d)

All 85 exit 0 at head 257e83df2f, each exit code captured before any pipe. --ran reconciliation: 85 derived, 85 run, 0 NOT-MEASURED, 0 UNRUN. A first run at 340955697a had pnpm check:dual-build-cjs-loads at exit 3 (PREREQUISITE NOT MET, no full build yet). It was re-run in the final union after the dogfood closure build: 112 require entry points across 69 packages load. check-adr-0087-registration found one declared-breaking changeset, and it carries its disposition. The level axis of check-changeset-no-major is scoped to the PR and reads this body on CI.

Dogfood

At 257e83df2f, after the @objectstack/dogfood^... closure build, under the lock: pnpm --filter @objectstack/dogfood test, VERDICT command-exit 0, Test Files 355 passed | 3 skipped (358), Tests 2867 passed | 20 skipped (2887). No shipped metadata names a credential field in searchableFields.

🤖 Generated with Claude Code

…ssword field

The engine withholds every credential column from the set a search scans
(ADR-0100), an author-declared one included, without a word. The
searchable-field-unsearchable rule now reports such an entry at error on
every surface it covers, judged by a new spec predicate
isCredentialFieldType that objectql's collectCredentialFields reads too.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:data tests tooling labels Oct 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

4 anchor(s) derived from 3 changed package(s); no hand-written page names any of them. ⚠️ 2 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/export-origins/data.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/export-origins/data.json) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 169 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json ca167f6d23417aaa85b1b3b5fa2e30286b3c4350 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9e2b153acba06213778a47ec816ec50b937cb117 — the merge of head 257e83df2f0883966cf37254eae5eab87d36bee4 into base ca167f6d23417aaa85b1b3b5fa2e30286b3c4350, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9e2b153acba06213778a47ec816ec50b937cb117 && git checkout 9e2b153acba06213778a47ec816ec50b937cb117
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ca167f6d23417aaa85b1b3b5fa2e30286b3c4350 257e83df2f0883966cf37254eae5eab87d36bee4 && git checkout -B drift-repro ca167f6d23417aaa85b1b3b5fa2e30286b3c4350 && git merge --no-ff 257e83df2f0883966cf37254eae5eab87d36bee4

node scripts/docs-audit/affected-docs.mjs --json ca167f6d23417aaa85b1b3b5fa2e30286b3c4350

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

席位核对 · ACCEPT (lane part) · card #27705 · 2026-10-30T00:27Z

  • Shape: 12 files, +314 / −15 at head 257e83df. searchable-field-unsearchable reports at error a searchableFields entry whose type is secret or password, on every surface the formula-entry check covers (object set, list views, react <ListView searchableFields>), fix line "Drop "NAME": credential fields are never searched", os explain paragraph, severity reasoning in the doc comment. One predicate, isCredentialFieldType, added to packages/spec/src/data/masked-field-types.ts (reads the keys of MASKED_ON_READ_FIELD_TYPES, no second literal); isMaskedOnReadFieldType and objectql's collectCredentialFields now call it, answers unchanged. Changeset: lint minor, spec + objectql patch, ADR-0087 not-required (no-migration-prescription) after the [finding] a mail template hole whose formatter text is not one word ({{ x | date-long }}, {{ x | currency :EUR }}) passes every mail door and is printed verbatim, while the in-app dialect refuses it #27225 lint-narrowing precedent. Matches triage 6506902320 (same severity as the formula finding ⇒ a narrowing).
  • Dev: opus under the quota-exhaustion exemption (claim 6509648663). Ablation 8/249 red without the check, control green; lint 7633 / spec 22452 / objectql 9244 tests and typecheck green; dogfood green (no shipped metadata trips the check); gates 85/85 exit 0. os-dev-report 6511812214.
  • For the contract reviewer — the Clause-② value: the diff adds a public export (isCredentialFieldType on @objectstack/spec/data, api-surface/data.json +1). By the reading on PR fix(spec)!: analytics query doors refuse an empty cube name with 400 VALIDATION_FAILED, not 500 #27691 (record 6508824023, ②) that makes the carriers yes (narrowing), while this PR body and changeset say no (narrowing). The relay cannot rewrite this PR body (line 1 is the attribution marker), so the seat records the value here: this PR is reviewed as Clause-②: yes (narrowing), at CONTRACT_REVIEW_TIER.
  • Acceptance notes:
  • Owed before any landing stroke: the CONTRACT_REVIEW_TIER review; this seat cannot serve that tier now and hands it over below.

Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (HTTP 429 "You've reached your Fable limit", 2026-10-29T17:05Z) · head 257e83d

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract-review-claim: session_01L6bhgV4fMGJHSdSdx3u9mK · head 257e83df2f0883966cf37254eae5eab87d36bee4 · 2026-10-30T00:34Z


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 257e83df2f0883966cf37254eae5eab87d36bee4
Local-runs: none
Scope: packages/spec/src/data/masked-field-types.ts + packages/objectql/src/secret-fields.ts (collectCredentialFields), packages/objectql/src/engine.ts (expandSearchOnAst), packages/objectql/src/engine-count-aggregate.ts, packages/objectql/src/credential-evaluation-guard.ts, packages/lint/src/validate-searchable-fields.ts (checkSearchableFieldList), packages/spec/src/data/search-fields.ts, packages/metadata-protocol/src/protocol-query-validation.ts (assertSearchFieldsAreSearchable), packages/spec/src/data/masked-field-types.test.ts, .changeset/27705-searchable-credential-fields.md

Inputs read: card #27705 (body, triage 6506902320, claim 6509648663, os-dev-report 6511812214), PR #27811 (body, 11-file list, net diff from the files endpoint, comments 6511803328 / 6511839038 / 6511932528) and the check-runs on this head. The cited code was read in the committed tree at this head; nothing was built or run. The remainder of the diff (the lint rule, its tests, os explain, the publish-door and react-page pins, the two regenerated spec ledgers) is the lane seat's ACCEPT at TIER_DEFAULT (comment 6511839038) and is read here only to judge the schema hit.

① Derived judgments

  1. Accept-set narrowing (right). searchable-field-unsearchable now reports, at error, a searchableFields entry whose field type is secret or password, on every surface the formula-entry check already covers: the object's own set, built-in list views, defineView lists and named list views, the flattened list overlay, the ViewItem record, and a react page's ListView searchableFields prop (through the shared checkSearchableFieldList core). os validate, os build, os lint and the object publish door (runtime-gate.object-writes.test.ts pin) refuse a declaration they accepted before. This is the narrowing the card asks for, at the severity triage 6506902320 fixed (the same as the formula entry; validate-searchable-fields.ts:443 is error on main), so the (narrowing) arm, the Metadata-protocol: narrowed line and the BREAKING banner are all right.
  2. Public surface widened (right as a change, wrongly declared, see ②). @objectstack/spec/data gains the export isCredentialFieldType(fieldType: unknown): boolean; api-surface/data.json and export-origins/data.json each gain its one line, and src/data/index.ts:194 already re-exports the module, so the export is live at the subpath the consumers import.
  3. isMaskedOnReadFieldType refactor (right, answers unchanged). Its own-key test now goes through the new predicate; the type guard and the exemptManagedBy read that follow are byte-equivalent to the old body, and masked-field-types.test.ts pins the full FieldType × managedBy table plus the one cell where the two predicates differ (password × better-auth).
  4. collectCredentialFields one-line swap (right, answers unchanged today). The objectql literal def.type === 'secret' || def.type === 'password' becomes isCredentialFieldType(def.type). Same answer over every current FieldType. The consequence the doc comment advertises is real and was read at its three call sites: a type added to MASKED_ON_READ_FIELD_TYPES becomes a credential type for the aggregate refusal (engine-count-aggregate.ts:172), the evaluation guard (credential-evaluation-guard.ts:189), the search withhold (engine.ts:13424) and the linter in one edit. That coupling is deliberate and is the right direction for a credential vocabulary (fail closed), and the module header already says an edit to the table is a security decision.
  5. Engine untouched (right). packages/objectql/src/engine.ts is not in the file list; the card's out-of-scope line holds. The runtime behaviour this PR changes is exactly nil; everything it changes is authoring-time.
  6. The narrowing itself against the spec on main. It takes away no declared capability at the consumer: the engine on main already withholds a credential column from every search (an author-declared one included), so a declaration naming one never searched it. It makes the spec's own refusal real where the entry is authored instead of silent at run time. Right.

② Semver level

  • Changeset 27705-searchable-credential-fields.md: @objectstack/lint minor, @objectstack/spec patch, @objectstack/objectql patch. Under the launch-window rule the level follows the metadata-protocol kind alone: narrowed takes at least minor on a package the diff moves (lint, right); a new export on spec is a non-protocol widening and ships patch (right); the objectql swap publishes no change in answer (patch, right). Check Changeset and Lint & Repo Gates are green on this head, so the level axis and the ADR-0087 disposition are machine-confirmed.
  • ADR-0087 disposition not-required (no-migration-prescription): right. No schema key is added, removed, renamed or re-shaped; the one remedy is to delete the entry, which no conversion can decide for the author. Same shape as the lint-narrowing precedent the changeset names.
  • Clause-②: line — wrong value, right arm (should-fix). The PR body and the committed changeset both read Clause-②: no (narrowing). The value answers whether the card widens an accept set or expands the public face; this diff expands the public face (a new @objectstack/spec/data export, api-surface/data.json +1), and the changeset body says so in its last paragraph. The truthful line is Clause-②: yes (narrowing) — a diff that widens one surface and narrows another, both facts read. The lane seat records the same value in 6511839038, and this record reviews the PR as yes (narrowing). Why should-fix and not must-fix: the arm is the fact every reader acts on — the face (reviewFace maps yes (narrowing) and no (narrowing) to the same narrowing face at this tier), the level axis (narrowed plus lint minor satisfies it either way) and the ADR-0087 disposition (keyed on the arm) all come out identical — so the wrong word moves no gate and no tier; it is still a false declaration on a carrier that ships verbatim into CHANGELOG.md. The dev corrects one word in the changeset and the PR body on the next push to this branch, if there is one.
  • Clause-②: claim comment 6509648663 reads no with no arm; the order and the carriers read no (narrowing). The carriers decide; the claim's missing arm is a dispatch spelling, not a tree defect.

③ Boundary flags

Dev deviations (os-dev-report 6511812214), each answered:

  1. Claim says no, carriers say no (narrowing). Answered in ②: the arm is right, the value should be yes; should-fix.
  2. Seat signature on line 1 of the report comment. A comment-shape matter under the dispatch's own rule; not a tree question, nothing owed here.
  3. PR body footer is the harness footer, not the session-URL form. Attribution lives in the commit trailers; not a tree question, nothing owed here.
  4. Independent review not run (quota). This record is the review of record at the reviewer tier; the lane's ACCEPT (6511839038) covers the remainder at TIER_DEFAULT.
  5. No PR assignees (os-dev.md vs AGENTS.md). A process conflict between two instruction files, outside the tree and outside this record's remit; escalated to the owning seat to settle with the maintainer, no landing consequence.
  6. Scope widened to the objectql one-line swap and the isMaskedOnReadFieldType refactor. Judged in ① items 3 and 4: both inside the order's "shared predicate where it already lives" line, answers unchanged and pinned. Accepted.

open_questions[0] — the REST ingress admits a $searchFields entry naming a credential field the object itself declares, and the engine then withholds it. Answered: option A, the premise holds in the code read. assertSearchFieldsAreSearchable (protocol-query-validation.ts:930) resolves the allowed set with resolveSearchFieldResolution over the registry field map without removing credential fields, so a declared credential entry is in allowed and passes; expandSearchOnAst (engine.ts:13424) removes it from the scan map first, so the declared branch loses it (or, when it was the only entry, falls to the auto-default), and resolveSearchFields (search-fields.ts:263) answers a request whose every name was removed with the whole allowed set. That is the "asked narrower, answered wider" shape, in code. It is a runtime change, out of this card's scope by the card's own line; the owning seat said in 6511839038 it will measure it through the data door before filing, which is the right order. Not filed from this record because a card names a measured reproduction, and this is a reading.

Note on provenance, not a finding: the hunks cite "ADR-0100 search withhold"; ADR-0100 decides the read mask and the better-auth exemption and names the aggregate gap as a follow-up, and does not itself decide the search withhold, which the engine comment at engine.ts:13418 cites to ADR-0100 as well. The lint and changeset prose follow the engine's existing citation; outside the schema hit and outside this record.

④ Design judgment

packages/spec/src/data/masked-field-types.ts — judged against the ruling it implements (triage 6506902320: a credential-type finding at the formula entry's severity, reusing a shared predicate rather than a second type literal) and its two first consumers (collectCredentialFields, checkSearchableFieldList), in the committed tree at this head.

Holds:

  • The right home and the right table. The card asked for one shared predicate for "is this a credential type". SEARCH_AUTO_EXCLUDED_TYPES could not serve (it also names json, lookup, boolean, which an author may declare and the engine executes), and isMaskedOnReadFieldType could not serve (it applies the better-auth exemption that the search withhold does not). The exemption-free question is a distinct one and the predicate states it as such, next to the table it reads. Prime Directive ✨ Set up Copilot instructions #2 is kept: a predicate over a declared table, the same shape as the predicate beside it, no business logic.
  • No second literal. secret and password appear once as data, as the keys of MASKED_ON_READ_FIELD_TYPES. The objectql literal is removed. The predicate is total over unvalidated input (non-string, prototype keys, case variants return false), pinned.
  • The refactor cannot change an answer. isMaskedOnReadFieldType returns false on exactly the inputs it did before (non-string or not an own key) and reads the same exemptManagedBy list after; a function declaration, so the forward reference is fine.
  • Consumers agree. Linter and engine now judge credential types from one predicate; the lint check (validate-searchable-fields.ts:468) runs after the existence and virtual checks and before the admissibility pass, so the remedy an author sees is "drop it", not "target a text-like field". target.fields[name]?.type is undefined for a registry-injected system column the linter cannot see, and the predicate answers false, so no false positive on stub metadata. The ordering also means that for a view narrowing over an object whose own set is all-credential, the object's entries are each reported first, so the author fixes the cause before any admissibility finding.
  • Severity and the remedy match the card: error for the formula entry's reason (the entry can never match wherever it is declared; a warning would leave os validate passing a declaration the runtime does not honour); the hint is one line, "Drop NAME: credential fields are never searched", with the per-surface account in os explain.
  • Tests per the card: secret and password on the object's own set and on a list view (it.each), the control (text and email entries in the same declaration stay clean), a view entry the object itself declares, a better-auth password (exemption-free), the publish door, the react prop, and the predicate over every FieldType. The dev's ablation (anchor replaced, 8 of 249 red, control green) is read as reported, not re-run.

Tried to break it:

  • Could the coupling "credential type = masked-on-read type" refuse something it should not? Only if a future type is added to the mask table that is masked for a reason other than being a credential. The table header already declares an edit there a security decision pinned by the full-table test, and fail-closed (refused in aggregate, search and lint on the same edit) is the direction the regime wants for that table. Not a finding.
  • Does the search-fields vocabulary drift from the mask table? Should-fix. SEARCH_AUTO_EXCLUDED_TYPES (search-fields.ts:67) still names secret and password by hand and search-fields.test.ts:189 pins only that secret is in it. Nothing pins that every key of MASKED_ON_READ_FIELD_TYPES is in SEARCH_AUTO_EXCLUDED_TYPES. If a textual type were added to the mask table and not to that set, the engine would withhold it (it filters the scan map before resolving) and the linter would refuse it, but the REST ingress, which resolves the allowed set over the unfiltered registry map, would admit an auto-default $searchFields naming it — the exact "asked narrower, answered wider" shape ③ names, opened by drift. One assertion in search-fields.test.ts or masked-field-types.test.ts (for every own key of MASKED_ON_READ_FIELD_TYPES: SEARCH_AUTO_EXCLUDED_TYPES.has(key)) closes it. Pre-existing structure, but this PR makes the mask table the one credential vocabulary, so this is the PR that owes the pin. Not a must-fix: no current type drifts, and the engine side fails closed today.
  • Is the type signature too loose? (fieldType: unknown): boolean rather than a type guard. SecretFieldError.fieldType: 'secret' | 'password' (secret-fields.ts:140) remains a type-level restatement of the vocabulary. Type-level only, no runtime literal; a guard would not reach it without a derived union type. Not a finding.
  • The ui copy. ui/packages/fields/src/index.tsx:4007 keeps its interim MASKED_FIELD_TYPES set, which the module header already names as owed to its own re-bind card. Out of this card's scope; not a finding.

Verdict on the schema hit: the predicate holds against the ruling it implements and against both first consumers; the one should-fix above is a missing pin, not a wrong answer.

Gates on this head: read from the check-runs on 257e83df2f0883966cf37254eae5eab87d36bee4 at 2026-10-30T00:46Z, after the last run completed: 54 check-runs, all completed, every conclusion success or skipped. The ten required contexts: Lint & Repo Gates, Test Core (all eight shards), Build Core, Governed Surface Queue Guard, Type Check UI, Test UI, Build & E2E UI, Live E2E UI and Type Check · workspace are success; Lint UI is skipped (no ui/ path in the diff). Check Changeset, PR-body guards, Dogfood Regression Gate, Type Check · source gates, Type Check · consumer gates and Lint · PM dispatch-gates are success. Those conclusions are the gate verdicts; nothing was re-run here.

Findings:

  • must-fix: none.
  • should-fix: (a) ② the Clause-②: value on the changeset and PR body reads no; the diff adds a public export, so it is yes (narrowing) — one word on two carriers; (b) ④ pin SEARCH_AUTO_EXCLUDED_TYPES ⊇ own keys of MASKED_ON_READ_FIELD_TYPES beside the predicate's tests.

Implemented-by: claude/issue-27705-searchable-credential-fields
Reviewed-by: session_01L6bhgV4fMGJHSdSdx3u9mK

VERDICT: PASS


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation protocol:data size/m tests tooling

Projects

None yet

1 participant