Out-of-scope finding from #6675 (skill-side searchableFields coverage). Recorded, not claimed — filed unassigned.
Fact (origin/main @ e5bd76859)
content/docs/ui/views.mdx:106 (the searchableFields row, landed by PR #6670) says:
Entries must be the object's own columns: a lookup (project_id) or a dotted path (project_id.name) is refused, and every toolbar search on the list then returns 400 INVALID_FIELD (#4254)
The dotted-path half is correct. The lookup half is not: the boundary is membership in the object's server-resolved allowed set, and field type is consulted only on the auto-default branch — i.e. only when the object declares no searchableFields at all.
Measured, both layers
Object support_case declaring searchableFields: ['subject', 'account_id'], where account_id is { type: 'lookup', reference: 'crm_account' }:
| view narrows to |
validateSearchableFields (packages/lint) |
assertSearchFieldsAreSearchable (packages/metadata-protocol) |
['account_id'] — a lookup, declared |
[] (clean) |
ACCEPTED, engine searchFields = ["account_id"] |
['account_name'] — a text column, NOT declared |
searchable-field-unsearchable |
400 INVALID_FIELD |
So a text column is refused and a lookup is accepted, on the same object — the exact inverse of a type-based reading. The runtime rule is resolveSearchFieldResolution (packages/spec/src/data/search-fields.ts): the declared branch is searchableFields?.filter((f) =-> all[f]), filtered by existence, never by type; the type list (SEARCHABLE_TEXTUAL_TYPES / SEARCHABLE_ENUM_TYPES) is reached only by autoDefaultFields, i.e. the no-declaration branch.
packages/lint/src/validate-searchable-fields.ts already says this in its own module note, and deliberately does not flag a lookup declared on the object:
The OBJECT's own searchableFields stays existence-only: the runtime's declared branch filters by existence, never by type, so a json or lookup column declared THERE is a choice the engine executes (a $contains over the raw column), not a 400. Flagging it would reject metadata the runtime accepts — the false finding that makes authors stop trusting the linter (ADR-0072 D1).
Why it matters
The row reads as a prohibition on a configuration the platform supports. An author who has legitimately declared a lookup searchable (a $contains over the stored id — narrow, but the engine executes it) would read this row and delete a working narrowing; an AI author following the docs would refuse to emit one. It also teaches the wrong mental model for the boundary, which is what makes the real refusal (a column outside the declared set — including a text one) surprising when it lands.
Scope if promoted
XS: correct the views.mdx:106 row to state the rule as set-membership — "must be in the set the object allows: its declared searchableFields if it has one, otherwise the auto-default text-like columns" — keeping the dotted-path clause as-is. Check the sibling prose landed by the same PR (content/docs/data-modeling/schema-design.mdx, content/docs/api/data-api.mdx, content/docs/protocol/objectql/query-syntax.mdx, content/docs/data-modeling/queries.mdx) for the same type-based phrasing before editing.
Not fixed in PR for #6675 — that PR is scoped to skills/objectstack-ui/SKILL.md, whose new section states the set-membership rule and pins both directions in packages/lint/src/validate-searchable-fields.test.ts.
Refs: #6675, PR #6670, ADR-0061, #4254, #4830.
Out-of-scope finding from #6675 (skill-side
searchableFieldscoverage). Recorded, not claimed — filed unassigned.Fact (origin/main @
e5bd76859)content/docs/ui/views.mdx:106(thesearchableFieldsrow, landed by PR #6670) says:The dotted-path half is correct. The lookup half is not: the boundary is membership in the object's server-resolved allowed set, and field type is consulted only on the auto-default branch — i.e. only when the object declares no
searchableFieldsat all.Measured, both layers
Object
support_casedeclaringsearchableFields: ['subject', 'account_id'], whereaccount_idis{ type: 'lookup', reference: 'crm_account' }:validateSearchableFields(packages/lint)assertSearchFieldsAreSearchable(packages/metadata-protocol)['account_id']— a lookup, declared[](clean)searchFields = ["account_id"]['account_name']— atextcolumn, NOT declaredsearchable-field-unsearchable400 INVALID_FIELDSo a
textcolumn is refused and alookupis accepted, on the same object — the exact inverse of a type-based reading. The runtime rule isresolveSearchFieldResolution(packages/spec/src/data/search-fields.ts): the declared branch issearchableFields?.filter((f) =-> all[f]), filtered by existence, never by type; the type list (SEARCHABLE_TEXTUAL_TYPES/SEARCHABLE_ENUM_TYPES) is reached only byautoDefaultFields, i.e. the no-declaration branch.packages/lint/src/validate-searchable-fields.tsalready says this in its own module note, and deliberately does not flag a lookup declared on the object:Why it matters
The row reads as a prohibition on a configuration the platform supports. An author who has legitimately declared a lookup searchable (a
$containsover the stored id — narrow, but the engine executes it) would read this row and delete a working narrowing; an AI author following the docs would refuse to emit one. It also teaches the wrong mental model for the boundary, which is what makes the real refusal (a column outside the declared set — including atextone) surprising when it lands.Scope if promoted
XS: correct the
views.mdx:106row to state the rule as set-membership — "must be in the set the object allows: its declaredsearchableFieldsif it has one, otherwise the auto-default text-like columns" — keeping the dotted-path clause as-is. Check the sibling prose landed by the same PR (content/docs/data-modeling/schema-design.mdx,content/docs/api/data-api.mdx,content/docs/protocol/objectql/query-syntax.mdx,content/docs/data-modeling/queries.mdx) for the same type-based phrasing before editing.Not fixed in PR for #6675 — that PR is scoped to
skills/objectstack-ui/SKILL.md, whose new section states the set-membership rule and pins both directions inpackages/lint/src/validate-searchable-fields.test.ts.Refs: #6675, PR #6670, ADR-0061, #4254, #4830.