Skip to content

Commit 7fc4d23

Browse files
docs(lint): a declared never-string-capable search column is admitted but contributes no clause (#29612)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8GoPCMAEZf1wXgQu2mQ7K6f)_ Fixes #29585 Clause-②: no Metadata-protocol: none Dispatched by `domain:spec#5 · os-warren · session_01JQwxbWR3K3h7i1ytwycVg1`; dev = card thread `session_01JQwxbY5V8enQpRc6k9NscP`. ## 维护者速读 - 改了什么:只改注释。`packages/lint/src/validate-searchable-fields.ts` 模块说明里讲对象自身 `searchableFields` 的那一段,改为「json 等从不存字符串的列仍被接纳进搜索集,但搜索展开不给它出子句;只由这类列组成的集合返回零行,不报错;单值 lookup 走 `$icontains`」。 - 为什么改:#29502 之后 `$search` 展开跳过这类列,旧注释仍说 json/lookup 列经 `$contains`「a scan that CAN match」。spec 侧的同款修正是 #29584。 - 风险与回滚:零行为改动,规则、发现、断言都没动(测试文件只改了三处注释);回滚即 revert。 - 需要你做的:无,席位落地。 ## What changes - `packages/lint/src/validate-searchable-fields.ts`, the module docblock paragraph on the object's own `searchableFields` (the "dividing line is STORAGE" paragraph): a lookup or json column declared there is admitted to the resolved set, neither a 400 nor a finding. What it contributes is the expansion's call (`expandSearchToFilter`, `@objectstack/objectql`): a single-valued lookup becomes an `$icontains` over the stored foreign key (a `multiple: true` one, a `$contains` membership match), a scan that can match; a column whose declared type can never hold a string (the text-operator door's `door-refusal` verdict, `textOperatorDoorVerdict`, json among them) is admitted but contributes no clause, and a set made only of such columns answers no rows, never an error. - `packages/lint/src/validate-searchable-fields.test.ts`, three comments that still carried the old "`$contains` over the stored JSON text … CAN match" reasoning ("does not flag a field that exists but is an odd search target", "passes a view entry of odd type once the object declares it searchable", and the CONTROL "a json or lookup entry on the OBJECT's own set stays clean"): reworded to the docblock's reading, keeping the ADR-0072 D1 reason for staying clean. No assertion, fixture or test name moves. The test file does not ship, so the changeset is unchanged. - `.changeset/29585-searchable-fields-lint-no-clause-comment.md`: `@objectstack/lint` patch. The docblock ships in the published `.d.ts` (measured below). No rule behaviour, finding, message or assertion moves. ## Why Since #29502 (#29394) the `$search` expansion skips never-string-capable columns, so the docblock's "a scan that CAN match" via `$contains` was false for json, and named the wrong operator for a single lookup. Same correction as the spec twin #29584. ## Verification Premise measured on `origin/main` `81cd9eb0` with a throwaway vitest file in `packages/objectql` (deleted, not committed), calling `expandSearchToFilter('acme', { fields, searchableFields })` with `name: text`, `payload: json`, `account_id: lookup`, `members: lookup multiple: true`: ``` ["payload"] → {"$or":[]} ["account_id"] → {"$or":[{"account_id":{"$icontains":"acme"}}]} ["members"] → {"$or":[{"members":{"$contains":"acme"}}]} ["name","payload","account_id"] → {"$or":[{"name":{"$icontains":"acme"}},{"account_id":{"$icontains":"acme"}}]} ``` An `$or` with no arm is FALSE (the module header of `search-filter.ts`): no rows, no error. Changeset decision: after `turbo run build --filter=@objectstack/lint... --filter=@objectstack/objectql...` (exit 0), the new phrase "admitted but contributes NO" is in `packages/lint/dist/index.d.ts` and `dist/index.d.cts`; positive control "ADR-0061 — searchable set" hits the same two files. So the text publishes and takes a patch changeset, not skip-changeset. Gates run at head `a123ca3f`, and the lint suite and eslint again at head `f0848a59` after the test-comment commit: - `pnpm --filter @objectstack/lint exec vitest run`: Test Files 192 passed (192), Tests 7891 passed (7891), at both heads. - `pnpm exec eslint packages/lint/src/validate-searchable-fields.ts packages/lint/src/validate-searchable-fields.test.ts`: exit 0. - `pnpm check:empty-changeset`: exit 0 (1 declaring changeset added). - `pnpm check:changeset-fixed`: exit 0. - `pnpm check:changeset-gate-self-tests`: exit 0. - `pnpm check:adr-0087-registration`: exit 0 (1 non-breaking changeset seen). - `node scripts/check-changeset-no-major.mjs`: pass. No protocol.ts file is touched. ## Does "neither a 400 nor a finding" still hold for a json entry? Yes. The runtime accepts the entry (no 400; the resolved set is not narrowed), so flagging it would reject metadata the runtime accepts (ADR-0072 D1); a warning for a never-matching entry would be a new gate, and the seat ruled that no separate card is filed. ## Human Minimum Nothing; the seat lands it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JQwxbY5V8enQpRc6k9NscP <!-- signed: domain:spec#5 · os-warren · session_01JQwxbY5V8enQpRc6k9NscP --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5bd615e commit 7fc4d23

3 files changed

Lines changed: 46 additions & 16 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/lint': patch
3+
---
4+
5+
docs(lint): the `validateSearchableFields` module docblock says a declared search column whose type can never hold a string is admitted but contributes no clause
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The module docblock of `validate-searchable-fields.ts` (`@objectstack/lint`, shipped in its `.d.ts`) still said a `json` or `lookup` column on an object's own `searchableFields` is "a scan that CAN match" via `$contains`. The `$search` expansion in `@objectstack/objectql` reads it differently, and the docblock now says so:
11+
12+
- a declared `json` column (or any column whose declared type can never hold a string, the text-operator door's `door-refusal` verdict, `textOperatorDoorVerdict`) is admitted to the set but contributes no clause;
13+
- a set made only of such columns answers no rows, never an error;
14+
- a declared single-valued `lookup` column becomes an `$icontains` over the stored foreign key; a `multiple: true` one, a `$contains` membership match.
15+
16+
Comment-only: no rule behaviour, finding or test moves.

‎packages/lint/src/validate-searchable-fields.test.ts‎

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -216,9 +216,12 @@ describe('validateSearchableFields — what it deliberately does not flag', () =
216216
});
217217

218218
it('does not flag a field that exists but is an odd search target', () => {
219-
// An explicit `searchableFields` is authoritative — the engine scans
219+
// An explicit `searchableFields` is authoritative — the runtime admits
220220
// exactly what it names — so declaring a json column is a choice, not
221-
// drift. This rule answers existence only.
221+
// drift. It is admitted but contributes no clause to the `$search`
222+
// expansion (its declared type can never hold a string). This rule
223+
// answers existence only: flagging it would reject metadata the runtime
224+
// accepts (ADR-0072 D1).
222225
const findings = validateSearchableFields({
223226
objects: [
224227
{
@@ -536,9 +539,11 @@ describe('validateSearchableFields — list views that narrow the set', () => {
536539

537540
it('passes a view entry of odd type once the object declares it searchable', () => {
538541
// The runtime's declared branch filters by EXISTENCE, never by type: a
539-
// json/lookup column declared on the OBJECT is honored by the engine and
540-
// admitted by the gate, so the view echoing it must stay green — flagging
541-
// it would reject metadata the runtime accepts.
542+
// json/lookup column declared on the OBJECT is admitted to the resolved
543+
// set and by the gate (a json column contributes no clause to the
544+
// `$search` expansion; a single lookup becomes an `$icontains` over the
545+
// stored foreign key), so the view echoing it must stay green — flagging
546+
// it would reject metadata the runtime accepts (ADR-0072 D1).
542547
const findings = validateSearchableFields({
543548
objects: [
544549
{
@@ -962,11 +967,13 @@ describe('[#6674] validateSearchableFields — a virtual formula entry', () => {
962967

963968
it('CONTROL — a json or lookup entry on the OBJECT\'s own set stays clean', () => {
964969
// The carve-out #4830 wrote down, deliberately preserved: the runtime's
965-
// declared branch executes those (a `$contains` over the stored JSON text /
966-
// the stored foreign key). Narrow and rarely useful, but a scan that CAN
967-
// match — flagging it would reject metadata the runtime accepts (ADR-0072
968-
// D1). If this control ever goes red, #6674 has quietly become "the declared
969-
// branch is type-filtered", which it is not.
970+
// declared branch admits those. A json column contributes no clause to the
971+
// `$search` expansion (its declared type can never hold a string); a single
972+
// lookup becomes an `$icontains` over the stored foreign key, narrow and
973+
// rarely useful but a scan that CAN match. Either way, flagging it would
974+
// reject metadata the runtime accepts (ADR-0072 D1). If this control ever
975+
// goes red, #6674 has quietly become "the declared branch is
976+
// type-filtered", which it is not.
970977
expect(
971978
validateSearchableFields({
972979
objects: [

‎packages/lint/src/validate-searchable-fields.ts‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,19 @@
9494
*
9595
* The OBJECT's own `searchableFields` stays existence-only OTHERWISE, and the
9696
* dividing line is STORAGE, not search quality: the runtime's declared branch
97-
* filters by existence and scannability, never by type taste, so a json or
98-
* lookup column declared THERE is a choice the engine executes (a `$contains`
99-
* over the stored JSON text / the stored foreign key) — narrow, rarely useful,
100-
* but a scan that CAN match, so it is neither a 400 nor a finding. Flagging
101-
* those would reject metadata the runtime accepts — the false finding that
102-
* makes authors stop trusting the linter (ADR-0072 D1). A virtual entry is the
97+
* filters by existence and scannability, never by type taste, so a lookup or
98+
* json column declared THERE is admitted to the resolved set: neither a 400
99+
* nor a finding. What it then contributes is the `$search` expansion's call
100+
* (`expandSearchToFilter`, `@objectstack/objectql`). A single-valued lookup
101+
* becomes an `$icontains` over the stored foreign key (a `multiple: true` one,
102+
* a `$contains` membership match) — narrow, rarely useful, but a scan that
103+
* CAN match. A column whose declared type can never hold a string — the
104+
* text-operator door's `door-refusal` verdict (`textOperatorDoorVerdict`,
105+
* `@objectstack/spec/data`), json among them — is admitted but contributes NO
106+
* clause, and a set made only of such columns answers no rows, never an
107+
* error. Flagging either would reject metadata the runtime accepts — the
108+
* false finding that makes authors stop trusting the linter (ADR-0072 D1).
109+
* A virtual entry is the
103110
* opposite case: no column exists on any driver, so admitting it is the
104111
* fail-open #4254 closed one axis over, surviving on the known-but-virtual
105112
* axis.

0 commit comments

Comments
 (0)