feat(lint): catch a stale searchableFields entry at authoring time (#4254 follow-up) - #4328
Merged
Conversation
…4254 follow-up) `searchableFields` is `z.array(z.string())` in both `object.zod.ts` and the list-view schema, so nothing ever checked that an entry resolves to anything. Rename a field and the old name stays behind — Zod-valid, shipped, pointing at a column that no longer exists. The engine tolerates it, which is what kept the drift invisible: `resolveSearchFields` filters the declaration down to fields that exist and says nothing. The tolerance fails in the direction nobody expects — some entries stale searches a NARROWER set than the object declares, and every entry stale empties the filtered set, falling through to the auto-default. A declaration whose whole purpose is to CHOOSE the searchable set ends up selecting one the author never wrote. It also stops being quiet downstream: clients echo the declaration verbatim as `$searchFields`, so once the REST read path validates that override (#4254) a stale entry becomes a 400 INVALID_FIELD on every list search for that object. New rule `searchable-field-unknown`, appended to REFERENCE_INTEGRITY_RULES so it runs on `os validate` / `os lint` / `os compile` with no CLI edit. Covers the object's own ADR-0061 declaration and the list views that narrow it. Gating, unlike the advisory field-existence rules: those describe a consumer that skips an unknown name and renders the rest; this one either selects the wrong set or refuses the request. Existence only — a field that exists but is an odd search target is a choice, not drift. Skips an object this stack does not define, one with no authored field map, and registry-injected system columns (derived from the spec's FIELD_GROUP_SYSTEM_FIELDS + SystemFieldName rather than a sixth hand copy). Dotted paths ARE flagged, unlike in sibling rules: search matches the field map by exact string, so `owner_id.name` is dropped exactly like a typo. Verified against the shipped examples: showcase / crm / todo all validate clean, and injecting `email` into the showcase account object fails `os validate` with `objects[0].searchableFields[3]` and exit 1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
Contributor
Author
|
Two out-of-scope gaps noticed while writing this rule, filed rather than folded in (Prime Directive #10):
|
os-zhuang
added a commit
that referenced
this pull request
Jul 31, 2026
…s against the bound object's fields (#4329) (#4338) #4328's searchable-field-unknown gates the metadata surfaces but not the react page surface: ListView declares searchableFields as a dataProp, so a kind:'react' page could ship a stale name nothing resolves — the engine silently narrows the search (or widens it to the auto-default once every entry is stale), and #4254 turns the echoed $searchFields override into a 400 INVALID_FIELD. validate-searchable-fields now exports its core (indexObjectSearchTargets + checkSearchableFieldList) and validate-react-page-props runs it on <ListView> usages with static objectName + searchableFields, under the same rule id and severity. Same three skips, same dotted-path strictness; non-static values and spreads skip silently (ADR-0072 D1). Follow-up: #4340 tracks the remaining unchecked field-bearing React block props centrally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
searchableFieldsisz.array(z.string())in bothobject.zod.ts(ADR-0061) and the list-view schema, so nothing checked that an entry names a field that exists. Rename a field and the old name stays behind: Zod-valid, shipped, pointing at a column that is gone.The engine tolerates it, which is exactly what kept the drift invisible —
resolveSearchFieldsfilters the declaration down to fields that exist (searchableFields?.filter((f) => all[f])) and says nothing. The tolerance fails in the direction nobody expects:$searchscans a narrower set than the object declares. Records that should match do not, and the response is indistinguishable from "no such record".sort/select/expand指向不存在的字段时被静默丢弃(filter 轴已收口,这三条轴还没有) #4226 closed on the projection axis.And it stops being quiet downstream. Clients echo the declaration verbatim as the
$searchFieldsoverride (objectui's list search sendsschema.searchableFields), so once the REST read path validates that override against the object (#4254), a stale entry the engine had been silently skipping becomes a400 INVALID_FIELDon every list search for that object — a request-time break whose cause is an authoring typo made long before.The rule
searchable-field-unknown, appended toREFERENCE_INTEGRITY_RULES— so it reachesos validate,os lintandos compilewith no CLI edit, the wiring contract that suite exists to provide. It covers the object's own declaration and the list views that narrow it (objects[].listViews, adefineViewdefaultlist, namedlistViews), resolving each entry against the bound object's declared fields.Gating (
error), unlike the advisory field-existence rules.page-field-unknown/form-field-unknown/semantic-role-field-unknownare warnings because they describe a consumer that skips an unknown name and renders the rest. This one describes a declaration that either selects the wrong set or refuses the request outright — the same callvalidate-flow-template-pathsmakes for a filter-position token, where the miss widens the query instead of shrinking the page.Existence only. A field that exists but is an odd search target (a
jsoncolumn) is not flagged: an explicitsearchableFieldsis authoritative — the engine scans exactly what it names — so declaring one is a choice, not drift.Three skips keep false positives near zero (ADR-0072 D1 — one dead finding and authors stop trusting the linter): an object this stack does not define (may come from another package), an object with no authored field map (external / datasource-introspected), and registry-injected system columns. That last set is derived from the spec's own
FIELD_GROUP_SYSTEM_FIELDS+SystemFieldNamerather than hand-copied — this package already carries five slightly-different copies of that list, which is #3786's lesson going unlearned. (Converging the existing five is a separate cleanup, not folded in here.)Dotted paths are flagged, unlike in every sibling rule. They skip
owner_id.namebecause the query engine resolves the traversal; search does not —resolveSearchFieldsmatches the field map by exact string, so a dotted entry is dropped exactly like a typo, and it is the spelling most likely borrowed fromselect/sort. It gets its own fix hint.Verification
packages/lint— 38 files / 564 tests green, including 22 new cases: the stale entry, a clean control, the legacy array field map, each skip, the dotted path, and one per list-view surface. Severity is pinned, since a silent downgrade towarningwould makeos validateexit 0.packages/cli— 64 files / 628 tests green (it consumes the suite).os validatepasses on showcase (23 objects / 202 fields), crm and todo — no false positive. Injectingemailinto the showcase account object's declaration fails it with the precise path and exit 1:mainmerged in and the full suite re-run afterwards (Multi-agent discipline chore: version packages #10) — the merge movedpackages/lint/package.jsonandspec/src/data, so this was re-verified on the rebuilt tree, not the pre-merge one.🤖 Generated with Claude Code