From f393db9beca44212220235fc95ab9d06257b4e58 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 22:26:34 +0000 Subject: [PATCH 1/4] feat(lint): report a searchableFields entry that names a secret or password 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 Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- packages/lint/src/rule-explanations.ts | 23 +++- .../src/validate-react-page-props.test.ts | 18 +++ .../src/validate-searchable-fields.test.ts | 126 +++++++++++++++++- .../lint/src/validate-searchable-fields.ts | 56 +++++++- packages/objectql/src/secret-fields.ts | 4 +- .../spec/src/data/masked-field-types.test.ts | 31 ++++- packages/spec/src/data/masked-field-types.ts | 31 ++++- 7 files changed, 274 insertions(+), 15 deletions(-) diff --git a/packages/lint/src/rule-explanations.ts b/packages/lint/src/rule-explanations.ts index e2b08282d16..c40ffe92bab 100644 --- a/packages/lint/src/rule-explanations.ts +++ b/packages/lint/src/rule-explanations.ts @@ -2465,11 +2465,24 @@ const SEARCHABLE_FIELD_UNSEARCHABLE_EXPLANATION: RuleExplanation = { VIRTUAL_FIELD_STORAGE, 'A formula entry is refused on EVERY surface, the object\'s own set included — the REST ingress ' + 'answers `400 INVALID_FIELD`, the same answer a stale entry gets: measured, it ' + - 'matched 0 rows on driver-memory, and 0 rows with no error on driver-sql. Otherwise the ' + - 'object\'s own `searchableFields` is existence-only: its declared branch scans any stored ' + - 'column, so a json or lookup column declared there is a narrow choice the engine executes, not ' + - 'a finding. A system column outside the allowed set is not judged either: its runtime ' + - 'metadata is registry-owned and invisible here.', + 'matched 0 rows on driver-memory, and 0 rows with no error on driver-sql. Otherwise, apart ' + + 'from a credential entry, the object\'s own `searchableFields` is existence-only: its declared ' + + 'branch scans any stored column, so a json or lookup column declared there is a narrow choice ' + + 'the engine executes, not a finding. A system column outside the allowed set is not judged ' + + 'either: its runtime metadata is registry-owned and invisible here.', + 'A `secret` or `password` entry is refused on EVERY surface as well, the object\'s own set ' + + 'included. The engine leaves every credential column out of the set a search scans (ADR-0100), ' + + 'an author-declared one included, and says nothing: a declaration naming one scans a narrower ' + + 'set than written, and one naming only credential fields falls through to the AUTO-DEFAULT ' + + 'set. A list view naming one is refused with `400 INVALID_FIELD` when the object\'s set is the ' + + 'auto-default, which never admits a credential type; when the object declares the entry, the ' + + 'REST ingress admits it and the engine withholds it, so a view naming only credential fields ' + + 'searches the object\'s whole set. Credential types are judged by the spec\'s ' + + '`isCredentialFieldType`, the predicate the engine\'s credential collector reads, with no ' + + 'better-auth exemption: the read mask spares a better-auth `password`, the search withhold ' + + 'does not. An error, as a formula entry is: the entry can never match, and the narrowing is ' + + 'otherwise silent. The one fix is to drop it — mirroring the value onto a searchable column ' + + 'would publish what the withhold protects.', ], }; diff --git a/packages/lint/src/validate-react-page-props.test.ts b/packages/lint/src/validate-react-page-props.test.ts index b6f07fc4258..282793b0c3b 100644 --- a/packages/lint/src/validate-react-page-props.test.ts +++ b/packages/lint/src/validate-react-page-props.test.ts @@ -520,6 +520,24 @@ describe('validateReactPageProps — searchableFields (#4329)', () => expect(f[0].message).toContain('400 INVALID_FIELD'); }); + it('flags a secret or password entry under the metadata verdict — the shared core\'s credential check', () => { + const vault = { + name: 'crm_account', + fields: { name: { type: 'text' }, api_key: { type: 'secret' }, portal_password: { type: 'password' } }, + }; + const f = pastDeprecation(validateReactPageProps( + listPage( + list(`data={{ provider: 'object', object: 'crm_account' }} searchableFields={['name', 'api_key', 'portal_password']}`), + [vault], + ), + )); + expect(f.map((x) => [x.rule, x.severity, x.path])).toEqual([ + [SEARCHABLE_FIELD_UNSEARCHABLE, 'error', 'pages[0].source › searchableFields[1]'], + [SEARCHABLE_FIELD_UNSEARCHABLE, 'error', 'pages[0].source › searchableFields[2]'], + ]); + expect(f[0].hint).toBe('Drop "api_key": credential fields are never searched'); + }); + it('[#8404] warns on an unprovisioned anchor, and PINS that this surface threads the index', () => { // Load-bearing beyond the warning itself. `validateReactPageProps` builds // `unprovisionedAnchors` for its other field-prop checks, so if this diff --git a/packages/lint/src/validate-searchable-fields.test.ts b/packages/lint/src/validate-searchable-fields.test.ts index 36dc1b00489..65e284963f7 100644 --- a/packages/lint/src/validate-searchable-fields.test.ts +++ b/packages/lint/src/validate-searchable-fields.test.ts @@ -939,6 +939,127 @@ describe('[#6674] validateSearchableFields — a virtual formula entry', () => { }); }); +/** + * A credential entry. Since the engine's ADR-0100 search withhold, the search + * expansion leaves every `secret` / `password` column out of the field map it + * resolves over, an author-declared one included, and says nothing — so the + * declared set is silently narrower than written. The finding is that + * withhold's build-time voice, at the virtual entry's severity. + */ +describe('validateSearchableFields — a secret or password entry', () => { + const accountFields = { + name: { type: 'text' }, + billing_email: { type: 'email' }, + api_key: { type: 'secret' }, + portal_password: { type: 'password' }, + }; + + it.each([ + ['secret', 'api_key'], + ['password', 'portal_password'], + ])("flags a '%s' entry on the OBJECT's own searchableFields — previously clean", (ctype, field) => { + const findings = validateSearchableFields({ + objects: [ + { name: 'crm_account', fields: accountFields, searchableFields: ['name', field, 'billing_email'] }, + ], + }); + + // The text and email entries beside it stay clean: the check reads the TYPE. + expect(findings).toHaveLength(1); + expect(findings[0].rule).toBe(SEARCHABLE_FIELD_UNSEARCHABLE); + expect(findings[0].severity).toBe('error'); + expect(findings[0].path).toBe('objects[0].searchableFields[1]'); + expect(findings[0].where).toBe('object "crm_account"'); + expect(findings[0].message).toContain(`"${field}"`); + expect(findings[0].message).toContain(`is a '${ctype}' field, a credential`); + expect(findings[0].hint).toBe(`Drop "${field}": credential fields are never searched`); + }); + + it.each([ + ['secret', 'api_key'], + ['password', 'portal_password'], + ])("flags a '%s' entry on a list view narrowing, under the same verdict", (ctype, field) => { + const findings = validateSearchableFields({ + objects: [ + { + name: 'crm_account', + fields: accountFields, + listViews: { all: { type: 'grid', searchableFields: ['name', field] } }, + }, + ], + }); + + // Auto-default object: without the credential check the admissibility pass + // would explain this entry by its type alone and offer a text-like field. + expect(findings).toHaveLength(1); + expect(findings[0].rule).toBe(SEARCHABLE_FIELD_UNSEARCHABLE); + expect(findings[0].severity).toBe('error'); + expect(findings[0].path).toBe('objects[0].listViews.all.searchableFields[1]'); + expect(findings[0].message).toContain(`is a '${ctype}' field, a credential`); + expect(findings[0].hint).toBe(`Drop "${field}": credential fields are never searched`); + }); + + it('flags a view entry the object ITSELF declares — the declared set does not make it searchable', () => { + // The object's declaration admits the entry at the REST ingress, so the + // #4830 admissibility pass passes it; the engine still withholds it. + const findings = validateSearchableFields({ + objects: [ + { + name: 'crm_account', + fields: accountFields, + searchableFields: ['name', 'api_key'], + listViews: { all: { type: 'grid', searchableFields: ['api_key'] } }, + }, + ], + }); + + expect(findings.map((f) => [f.rule, f.path])).toEqual([ + [SEARCHABLE_FIELD_UNSEARCHABLE, 'objects[0].searchableFields[1]'], + [SEARCHABLE_FIELD_UNSEARCHABLE, 'objects[0].listViews.all.searchableFields[0]'], + ]); + }); + + it('is exemption-free: a `password` on a better-auth object is reported too', () => { + // The read mask spares a better-auth `password`; the search withhold does not. + const findings = validateSearchableFields({ + objects: [ + { + name: 'crm_account', + managedBy: 'better-auth', + fields: accountFields, + searchableFields: ['name', 'portal_password'], + }, + ], + }); + + expect(findings).toHaveLength(1); + expect(findings[0].path).toBe('objects[0].searchableFields[1]'); + expect(findings[0].message).toContain("is a 'password' field, a credential"); + }); + + it('CONTROL — the same declaration with text and email entries only stays clean', () => { + expect( + validateSearchableFields({ + objects: [ + { + name: 'crm_account', + fields: accountFields, + searchableFields: ['name', 'billing_email'], + listViews: { all: { type: 'grid', searchableFields: ['name', 'billing_email'] } }, + }, + ], + }), + ).toEqual([]); + }); + + it('`os explain` names the silent withhold and the exemption-free predicate', () => { + const text = explanationOf(SEARCHABLE_FIELD_UNSEARCHABLE); + expect(text).toContain('A `secret` or `password` entry is refused on EVERY surface'); + expect(text).toContain('`isCredentialFieldType`'); + expect(text).toContain('no better-auth exemption'); + }); +}); + /** * [#8404] The FIFTH blanket-`SYSTEM_FIELDS` read site. Existence and provenance * are different questions: on an ADR-0015 `external` object the platform @@ -1132,13 +1253,14 @@ describe('[#22161] one-line verdicts — the three searchable ids', () => { describe('[#22161] one-line fixes — the three searchable ids', () => { it('every `fix:` line the cases above fired is one line of at most 200 characters', () => { // Coverage control: every arm fired — both unknown arms, the anchor, and - // the four unsearchable arms (virtual, outside a declared set, a reference - // column and any other type outside the auto-default set). + // the five unsearchable arms (virtual, credential, outside a declared set, + // a reference column and any other type outside the auto-default set). const hints = fired.map((f) => f.hint); expect(hints.some((h) => h.startsWith('Fix the name, or add "')), 'unknown name').toBe(true); expect(hints.some((h) => h.startsWith("'search' scans this object's own columns only")), 'dotted').toBe(true); expect(hints.some((h) => h.startsWith("If the remote table carries '")), 'anchor').toBe(true); expect(hints.some((h) => h.startsWith('Mirror the computed value')), 'virtual').toBe(true); + expect(hints.some((h) => h.endsWith(': credential fields are never searched')), 'credential').toBe(true); expect(hints.some((h) => h.startsWith('Add "') && h.includes('.searchableFields')), 'declared set').toBe(true); expect(hints.some((h) => h.includes("to search by the related record's title")), 'reference').toBe(true); expect(hints.some((h) => h.endsWith('or target a text-like field instead')), 'other type').toBe(true); diff --git a/packages/lint/src/validate-searchable-fields.ts b/packages/lint/src/validate-searchable-fields.ts index f6c690acdb1..1343dd89e12 100644 --- a/packages/lint/src/validate-searchable-fields.ts +++ b/packages/lint/src/validate-searchable-fields.ts @@ -68,6 +68,30 @@ * WITH NO ERROR on driver-sql. Since #6674 the spec resolution drops such * an entry and both enforcement faces refuse it by name. * + * 4. CREDENTIALS, on EVERY surface — the object's own set included + * (`searchable-field-unsearchable`): a `secret` or `password` entry names a + * real, stored field, so checks 1 and 3 pass it — but since the engine's + * ADR-0100 search withhold the expansion leaves every credential column out + * of the field map it resolves over (`expandSearchOnAst`, + * `@objectstack/objectql`, by `collectCredentialFields`), an author-declared + * one included, and says nothing. The declared set is silently narrower + * than written, and a declaration naming ONLY credential fields falls + * through to the auto-default set — #4254's "asked narrower, answered + * wider" again. Judged by the spec's own predicate + * ({@link isCredentialFieldType}, `@objectstack/spec/data`), the one that + * engine collector reads, so linter and engine cannot disagree about which + * types are credentials. The withhold is exemption-free (a better-auth + * `password` is withheld too), and so is this check. + * + * `error`, the virtual entry's severity, deliberately and for the virtual + * entry's reason: the entry can never match wherever it is declared, and a + * narrowing the runtime makes in silence gets its word at build time. A + * warning would leave `os validate` passing a declaration the runtime does + * not honour, which is the gap this check exists to close. The remedy has + * one form — drop the entry — since no mirror of a credential onto a + * searchable column is a fix: that would publish the very value the + * withhold protects. + * * The OBJECT's own `searchableFields` stays existence-only OTHERWISE, and the * dividing line is STORAGE, not search quality: the runtime's declared branch * filters by existence and scannability, never by type taste, so a json or @@ -121,6 +145,7 @@ import { resolveSearchFieldResolution, + isCredentialFieldType, isVirtualSearchField, SEARCH_AUTO_EXCLUDED_FIELDS, type SearchFieldMeta, @@ -159,8 +184,9 @@ export interface SearchableFieldFinding { * * - `'canonical'` — the object's own `searchableFields`. The runtime honors * any entry that exists and has a stored column to scan (existence- and - * scannability-filtered, never type-filtered), so existence and virtuality - * are checked and nothing else (#6674). + * scannability-filtered, never type-filtered), apart from the credential + * columns the engine withholds, so existence, virtuality and credentials + * are checked and nothing else (#6674, module note check 4). * - `'narrowing'` — a list view's `searchableFields` (metadata or react * surface). Clients echo it as the `$searchFields` override, which the * #4254 ingress gate intersects with the object's allowed set — entries the @@ -430,6 +456,32 @@ export function checkSearchableFieldList( continue; } + // ── Credential entries — EVERY surface, canonical included ── + // + // The engine's search expansion withholds every `secret` / `password` + // column from the set it scans (ADR-0100), a declared one included, without + // a word; this is that withhold's build-time voice (module note, check 4). + // Judged by the spec predicate the engine's collector reads. Ahead of the + // admissibility pass on purpose: on a view over an auto-default object + // that pass would otherwise explain the entry by its type alone, and the + // remedy it offers ("target a text-like field") is not this one. + const ctype = target.fields[name]?.type; + if (isCredentialFieldType(ctype)) { + findings.push({ + severity: 'error', + rule: SEARCHABLE_FIELD_UNSEARCHABLE, + where, + path: `${path}[${i}]`, + message: + `${subject} entry "${name}" on object "${objectName}" is a '${ctype}' field, a ` + + `credential: 'search' never scans it`, + // One remedy line; what the engine does with the entry on each surface + // is the id's `os explain` entry. + hint: `Drop "${name}": credential fields are never searched`, + }); + continue; + } + // ── Runtime admissibility (#4830) — view-level narrowings only ── if (!resolution || resolution.allowed.has(name)) continue; // ③ System column outside the allowed set: its runtime metadata is diff --git a/packages/objectql/src/secret-fields.ts b/packages/objectql/src/secret-fields.ts index 4da61ac1c88..ef928fc2616 100644 --- a/packages/objectql/src/secret-fields.ts +++ b/packages/objectql/src/secret-fields.ts @@ -37,7 +37,7 @@ * ruling of 2026-08-12 on #7728. */ -import { isMaskedOnReadFieldType, type ServiceObject } from '@objectstack/spec/data'; +import { isCredentialFieldType, isMaskedOnReadFieldType, type ServiceObject } from '@objectstack/spec/data'; /** * Prefix marking a persisted field value as a `sys_secret` handle ref rather @@ -291,7 +291,7 @@ export function collectCredentialFields(schema: ServiceObject | undefined | null if (!fields) return []; const out: string[] = []; for (const [name, def] of Object.entries(fields)) { - if (def && (def.type === 'secret' || def.type === 'password')) out.push(name); + if (def && isCredentialFieldType(def.type)) out.push(name); } return out; } diff --git a/packages/spec/src/data/masked-field-types.test.ts b/packages/spec/src/data/masked-field-types.test.ts index 88c2a1f08ac..2ac89acfb29 100644 --- a/packages/spec/src/data/masked-field-types.test.ts +++ b/packages/spec/src/data/masked-field-types.test.ts @@ -14,7 +14,11 @@ import { describe, it, expect } from 'vitest'; import { FieldType } from './field.zod'; import { ObjectSchema } from './object.zod'; -import { MASKED_ON_READ_FIELD_TYPES, isMaskedOnReadFieldType } from './masked-field-types'; +import { + MASKED_ON_READ_FIELD_TYPES, + isCredentialFieldType, + isMaskedOnReadFieldType, +} from './masked-field-types'; const FIELD_TYPES: readonly string[] = FieldType.options; const MANAGED_BY_BUCKETS: readonly string[] = ( @@ -97,3 +101,28 @@ describe('isMaskedOnReadFieldType — the one reading of the declaration', () => } }); }); + +describe('isCredentialFieldType — the exemption-free reading (the search withhold and the lint check)', () => { + it('answers every FieldType: secret and password on any object, nothing else', () => { + for (const type of FIELD_TYPES) { + expect(isCredentialFieldType(type), type).toBe(type === 'secret' || type === 'password'); + } + }); + + it('ignores the read-mask exemption: a type masked on ANY object is a credential type', () => { + for (const type of FIELD_TYPES) { + const maskedSomewhere = [...MANAGED_BY_BUCKETS, undefined].some((m) => isMaskedOnReadFieldType(type, m)); + expect(isCredentialFieldType(type), type).toBe(maskedSomewhere); + } + // The one cell the two predicates disagree on, spelled out. + expect(isMaskedOnReadFieldType('password', 'better-auth')).toBe(false); + expect(isCredentialFieldType('password')).toBe(true); + }); + + it('is total over unvalidated input: non-strings, prototype keys and case variants are not credential types', () => { + for (const type of [undefined, null, 42, ['secret'], { toString: () => 'secret' }, '', 'constructor', + 'toString', '__proto__', 'hasOwnProperty', 'SECRET', 'Password']) { + expect(isCredentialFieldType(type), String(type)).toBe(false); + } + }); +}); diff --git a/packages/spec/src/data/masked-field-types.ts b/packages/spec/src/data/masked-field-types.ts index 44fc16f29c4..9fbe1d61530 100644 --- a/packages/spec/src/data/masked-field-types.ts +++ b/packages/spec/src/data/masked-field-types.ts @@ -114,10 +114,35 @@ export const MASKED_ON_READ_FIELD_TYPES: Readonly Date: Thu, 29 Oct 2026 22:33:52 +0000 Subject: [PATCH 2/4] chore(spec): regenerate api-surface and export-origins for isCredentialFieldType Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- packages/spec/api-surface/data.json | 1 + packages/spec/export-origins/data.json | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/spec/api-surface/data.json b/packages/spec/api-surface/data.json index b5d09c0afcb..3c9d682c02e 100644 --- a/packages/spec/api-surface/data.json +++ b/packages/spec/api-surface/data.json @@ -909,6 +909,7 @@ "isComposedReadableIds (function)", "isContextToken (function)", "isControlledByParentMasterField (function)", + "isCredentialFieldType (function)", "isCurrentUserDefaultToken (function)", "isDateMacroToken (function)", "isDateRangePresetName (function)", diff --git a/packages/spec/export-origins/data.json b/packages/spec/export-origins/data.json index 8c4479e2c51..d3d9d77d002 100644 --- a/packages/spec/export-origins/data.json +++ b/packages/spec/export-origins/data.json @@ -895,6 +895,7 @@ "isComposedReadableIds": "src/data/readable-ids.ts#isComposedReadableIds (function)", "isContextToken": "src/data/context-tokens.zod.ts#isContextToken (function)", "isControlledByParentMasterField": "src/data/controlled-by-parent-master.ts#isControlledByParentMasterField (function)", + "isCredentialFieldType": "src/data/masked-field-types.ts#isCredentialFieldType (function)", "isCurrentUserDefaultToken": "src/data/default-value-tokens.ts#isCurrentUserDefaultToken (function)", "isDateMacroToken": "src/data/date-macros.zod.ts#isDateMacroToken (function)", "isDateRangePresetName": "src/data/date-range-presets.ts#isDateRangePresetName (function)", From 30530357f51db35f4a3b4b4ee4904521c9436e41 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 22:36:06 +0000 Subject: [PATCH 3/4] test(lint): pin the credential entry at the object publish door; add the changeset Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- .../27705-searchable-credential-fields.md | 22 +++++++++++++++++++ .../src/runtime-gate.object-writes.test.ts | 16 ++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 .changeset/27705-searchable-credential-fields.md diff --git a/.changeset/27705-searchable-credential-fields.md b/.changeset/27705-searchable-credential-fields.md new file mode 100644 index 00000000000..f157c908832 --- /dev/null +++ b/.changeset/27705-searchable-credential-fields.md @@ -0,0 +1,22 @@ +--- +'@objectstack/lint': minor +'@objectstack/spec': patch +'@objectstack/objectql': patch +--- + +feat(lint)!: `searchable-field-unsearchable` reports a `searchableFields` entry that names a `secret` or `password` field, which the engine leaves out of every search without a word + +Clause-②: no (narrowing) — `os validate`, `os build`, `os lint` and the metadata publish door refuse a `searchableFields` entry whose field type is `secret` or `password`, on the object's own set, on every list view surface and on a react page's ``, which they accepted before. The finding takes the severity of the `formula`-entry finding (`error`). No rule id is added or removed. +Metadata-protocol: narrowed + + + +**BREAKING** accept-set narrowing at every authoring door for `searchableFields`. + +- **Before.** The engine's search expansion leaves every credential column (`secret`, `password`) out of the set it scans (ADR-0100), an author-declared one included, and says nothing. A declaration naming one passed every gate and searched a narrower set than written; a declaration naming only credential fields fell through to the auto-default set. +- **Now.** The rule refuses the entry at `error`, naming the entry, its type and the object. It is judged on every object, a better-auth `password` included: the read mask spares that one, the search withhold does not. +- **Who sees it.** An object or list view whose `searchableFields` names a `secret` or `password` field. No metadata shipped in this repository does. + +**The remedy.** Drop the entry: credential fields are never searched. `os explain searchable-field-unsearchable` states what the engine does with it on each surface. + +`@objectstack/spec` exports `isCredentialFieldType(fieldType)` from `@objectstack/spec/data`: whether a field type is masked on read on at least one object (`secret`, `password`), the exemption-free question. `@objectstack/objectql`'s `collectCredentialFields` now reads it instead of its own type literal; its answer is unchanged. diff --git a/packages/lint/src/runtime-gate.object-writes.test.ts b/packages/lint/src/runtime-gate.object-writes.test.ts index ed8341d20cd..f7ba800e770 100644 --- a/packages/lint/src/runtime-gate.object-writes.test.ts +++ b/packages/lint/src/runtime-gate.object-writes.test.ts @@ -301,6 +301,22 @@ describe('the object write door dispatches at the adjudicated scope (#4716)', () expect(f.message).toContain('gone_field'); }); + it('REFUSES an object whose searchableFields names a credential field the engine never searches', () => { + // The same rule's credential arm, at the publish door: the engine withholds + // a `secret` / `password` column from every search (ADR-0100), so the + // declaration is refused where a Studio tenant writes it. + const result = expectSingleRefusal( + cleanObject({ + fields: { owner: { type: 'text' }, api_key: { type: 'secret' } }, + searchableFields: ['owner', 'api_key'], + }), + 'searchable-field-unsearchable', + ); + const f = result.errors.find((e) => e.rule === 'searchable-field-unsearchable')!; + expect(f.path).toBe('objects.leave_request.searchableFields[1]'); + expect(f.message).toContain("is a 'secret' field, a credential"); + }); + it('REFUSES an object whose built-in list view names a column it does not have', () => { const result = expectSingleRefusal( cleanObject({ listViews: { all: { label: 'All', columns: ['owner', 'gone_column'] } } }), From 257e83df2f0883966cf37254eae5eab87d36bee4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 23:27:47 +0000 Subject: [PATCH 4/4] docs(changeset): spell the react ListView prop without a tag-shaped fragment Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- .changeset/27705-searchable-credential-fields.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/27705-searchable-credential-fields.md b/.changeset/27705-searchable-credential-fields.md index f157c908832..df269d26fba 100644 --- a/.changeset/27705-searchable-credential-fields.md +++ b/.changeset/27705-searchable-credential-fields.md @@ -6,7 +6,7 @@ feat(lint)!: `searchable-field-unsearchable` reports a `searchableFields` entry that names a `secret` or `password` field, which the engine leaves out of every search without a word -Clause-②: no (narrowing) — `os validate`, `os build`, `os lint` and the metadata publish door refuse a `searchableFields` entry whose field type is `secret` or `password`, on the object's own set, on every list view surface and on a react page's ``, which they accepted before. The finding takes the severity of the `formula`-entry finding (`error`). No rule id is added or removed. +Clause-②: no (narrowing) — `os validate`, `os build`, `os lint` and the metadata publish door refuse a `searchableFields` entry whose field type is `secret` or `password`, on the object's own set, on every list view surface and on a react page's ListView `searchableFields` prop, which they accepted before. The finding takes the severity of the `formula`-entry finding (`error`). No rule id is added or removed. Metadata-protocol: narrowed