Skip to content

feat(lint): catch a stale searchableFields entry at authoring time (#4254 follow-up) - #4328

Merged
os-zhuang merged 2 commits into
mainfrom
claude/quirky-benz-9d17b5
Jul 31, 2026
Merged

feat(lint): catch a stale searchableFields entry at authoring time (#4254 follow-up)#4328
os-zhuang merged 2 commits into
mainfrom
claude/quirky-benz-9d17b5

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

The gap

searchableFields is z.array(z.string()) in both object.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 — resolveSearchFields filters the declaration down to fields that exist (searchableFields?.filter((f) => all[f])) and says nothing. The tolerance fails in the direction nobody expects:

And it stops being quiet downstream. Clients echo the declaration verbatim as the $searchFields override (objectui's list search sends schema.searchableFields), so once the REST read path validates that override against the object (#4254), a stale entry the engine had been silently skipping becomes a 400 INVALID_FIELD on 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 to REFERENCE_INTEGRITY_RULES — so it reaches os validate, os lint and os compile with 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, a defineView default list, named listViews), 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-unknown are 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 call validate-flow-template-paths makes 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 json column) is not flagged: an explicit searchableFields is 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 + SystemFieldName rather 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.name because the query engine resolves the traversal; search does not — resolveSearchFields matches the field map by exact string, so a dotted entry is dropped exactly like a typo, and it is the spelling most likely borrowed from select/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 to warning would make os validate exit 0.
  • packages/cli — 64 files / 628 tests green (it consumes the suite).
  • Against real shipped metadata: os validate passes on showcase (23 objects / 202 fields), crm and todo — no false positive. Injecting email into the showcase account object's declaration fails it with the precise path and exit 1:
✗ Reference integrity check failed (1 issue)
  • object "showcase_account": searchableFields entry "email" is not a field on object "showcase_account". …
      rule: searchable-field-unknown  at objects[0].searchableFields[3]
  • main merged in and the full suite re-run afterwards (Multi-agent discipline chore: version packages #10) — the merge moved packages/lint/package.json and spec/src/data, so this was re-verified on the rebuilt tree, not the pre-merge one.

🤖 Generated with Claude Code

os-zhuang and others added 2 commits July 31, 2026 14:13
…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>
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 31, 2026 6:17am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint.

2 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang

Copy link
Copy Markdown
Contributor Author

Two out-of-scope gaps noticed while writing this rule, filed rather than folded in (Prime Directive #10):

@os-zhuang
os-zhuang merged commit 7967133 into main Jul 31, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/quirky-benz-9d17b5 branch July 31, 2026 06:21
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>
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 size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant