Skip to content

Commit cce0aa9

Browse files
os-warrenclaude
andauthored
fix(spec): drop four dead members from SEARCH_AUTO_EXCLUDED_TYPES and extend the [#13695] pin to every search type vocabulary (#13869)
'object', 'grid', 'geometry' and 'encrypted' were never FieldType members at any commit, so none could match a real field's type. The [#13695] pin now holds all four search type vocabularies to FieldType membership. Claude-Session: https://claude.ai/code/session_01PBjwYLS6BciTQW3c9xQiD2 Co-authored-by: Claude <noreply@anthropic.com>
1 parent db9c460 commit cce0aa9

3 files changed

Lines changed: 55 additions & 18 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
Drop four dead members — 'object', 'grid', 'geometry', 'encrypted' — from `SEARCH_AUTO_EXCLUDED_TYPES` (`search-fields.ts`). None of the four was ever a member of the `FieldType` enum at any commit, so none could ever match a real field's `type`: the exclusion set's live behaviour is unchanged, and the file's fail-closed tiebreak comment no longer asserts a safety property for names that cannot occur. The `[#13695]` pin in `search-fields.test.ts` is extended to hold every search type vocabulary (`SEARCHABLE_TEXTUAL_TYPES`, `SEARCHABLE_ENUM_TYPES`, `SEARCH_AUTO_EXCLUDED_TYPES`, `SEARCH_VIRTUAL_TYPES`) to `FieldType` membership, so a future ghost entry is a red test instead of a silent no-op.

packages/spec/src/data/search-fields.test.ts

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -144,25 +144,50 @@ describe('[#4483] $search auto field set — lead orders, never admits', () => {
144144
// and in opposite directions, so no single relaxation can make this pin vacuous.
145145
// ---------------------------------------------------------------------------
146146
// ---------------------------------------------------------------------------
147-
// [#13695] `SEARCHABLE_ENUM_TYPES` members must be real `FieldType`s.
147+
// [#13695] Every search type vocabulary in this file is a subset of `FieldType`.
148148
//
149-
// The #6934 disjointness pins above check the vocabularies against EACH OTHER
149+
// The #6934 disjointness pins below check the vocabularies against EACH OTHER
150150
// but never against `FieldType` itself, so a member that matches no real field
151151
// type at all — a pure ghost, distinct from an overlap — passed every existing
152-
// pin silently: `'status'` sat in this set matching nothing, for as long as it
153-
// took a docs sweep to notice by hand. This pin closes that probe gap for the
154-
// enum vocabulary specifically (the one the finding hit); it is deliberately
155-
// NOT extended to `SEARCH_AUTO_EXCLUDED_TYPES`, which is a separate, larger
156-
// finding of its own (`object`/`grid`/`geometry`/`encrypted` are ghosts there
157-
// too) filed out of scope for this fix.
152+
// pin silently: `'status'` sat in `SEARCHABLE_ENUM_TYPES` matching nothing for
153+
// as long as it took a docs sweep to notice by hand, and the same probe run
154+
// against the other sets found four more in `SEARCH_AUTO_EXCLUDED_TYPES`
155+
// (#13716: 'object', 'grid', 'geometry', 'encrypted' — never `FieldType`
156+
// members at any commit, so the fail-closed tiebreak comment was asserting a
157+
// safety property for names that cannot occur). This pin now holds ALL of the
158+
// file's type vocabularies to the enum: an exclusion or allowance spelled with
159+
// a name `FieldType` does not contain governs nothing.
160+
// (`SEARCH_AUTO_EXCLUDED_FIELDS` is deliberately absent — its members are
161+
// field NAMES, not types.)
158162
// ---------------------------------------------------------------------------
159-
describe('[#13695] SEARCHABLE_ENUM_TYPES ⊆ FieldType', () => {
163+
describe('[#13695] search type vocabularies ⊆ FieldType', () => {
160164
const validTypes: ReadonlySet<string> = new Set(FieldType.options);
165+
const vocabularies: ReadonlyArray<[string, ReadonlySet<string>]> = [
166+
['SEARCHABLE_TEXTUAL_TYPES', SEARCHABLE_TEXTUAL_TYPES],
167+
['SEARCHABLE_ENUM_TYPES', SEARCHABLE_ENUM_TYPES],
168+
['SEARCH_AUTO_EXCLUDED_TYPES', SEARCH_AUTO_EXCLUDED_TYPES],
169+
['SEARCH_VIRTUAL_TYPES', SEARCH_VIRTUAL_TYPES],
170+
];
161171

162-
it('every member is a real FieldType — no ghost vocabulary entries', () => {
163-
for (const t of SEARCHABLE_ENUM_TYPES) {
164-
expect(validTypes.has(t), `'${t}' is in SEARCHABLE_ENUM_TYPES but not a FieldType member`).toBe(true);
172+
it.each(vocabularies)('%s has no ghost members', (name, set) => {
173+
const ghosts = [...set].filter((t) => !validTypes.has(t)).sort();
174+
expect(ghosts, `${name} names types that are not FieldType members`).toEqual([]);
175+
});
176+
177+
it('CONTROL — the subset assertions are not vacuous', () => {
178+
// A broken `FieldType` import (empty `options`) or an accidentally emptied
179+
// vocabulary would green every subset check above while pinning nothing.
180+
expect(validTypes.size).toBeGreaterThan(0);
181+
for (const [name, set] of vocabularies) {
182+
expect(set.size, `${name} is unexpectedly empty`).toBeGreaterThan(0);
165183
}
184+
// Positive control — one known-real member per vocabulary, so the pin is
185+
// measuring membership, not an accident of empty intersections.
186+
expect(validTypes.has('text')).toBe(true);
187+
expect(SEARCHABLE_TEXTUAL_TYPES.has('text')).toBe(true);
188+
expect(SEARCHABLE_ENUM_TYPES.has('select')).toBe(true);
189+
expect(SEARCH_AUTO_EXCLUDED_TYPES.has('secret')).toBe(true);
190+
expect(SEARCH_VIRTUAL_TYPES.has('formula')).toBe(true);
166191
});
167192
});
168193

packages/spec/src/data/search-fields.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,14 @@ export const SEARCH_AUTO_EXCLUDED_FIELDS: ReadonlySet<string> = new Set([
4545
'id', '_id', 'created', 'modified', 'created_at', 'updated_at',
4646
'created_by', 'updated_by', 'owner_id', 'organization_id', 'space', 'company_id',
4747
]);
48+
// [#13716] 'object', 'grid', 'geometry' and 'encrypted' were dropped from this
49+
// set: none was a `FieldType` member at ANY commit (imported vocabulary, never
50+
// renamed spellings — 'location', 'secret' and 'json' were already in both the
51+
// enum and this set the day the set landed), so each could only ever match
52+
// nothing. The [#13695] pin now holds every member to `FieldType`.
4853
export const SEARCH_AUTO_EXCLUDED_TYPES: ReadonlySet<string> = new Set([
49-
'json', 'object', 'grid', 'image', 'file', 'avatar', 'vector', 'location',
50-
'geometry', 'secret', 'password', 'encrypted', 'boolean', 'lookup', 'master_detail',
54+
'json', 'image', 'file', 'avatar', 'vector', 'location',
55+
'secret', 'password', 'boolean', 'lookup', 'master_detail',
5156
]);
5257
/**
5358
* [#6674] Field types with NO STORED COLUMN — the value is computed on read, so
@@ -117,8 +122,10 @@ function autoDefaultFields(fields: Record<string, SearchFieldMeta>, displayField
117122
// `SEARCH_AUTO_EXCLUDED_TYPES` is disjoint from both positive lists, so
118123
// every type it names already falls through the `return` below as `false`
119124
// — identically to a type in none of the three sets (`number`, `date`, …).
120-
// Measured over the full 56-type domain (`FieldType` ∪ all three
121-
// vocabularies), deleting this line moves not one resolution. So it is NOT
125+
// Measured over the full domain of `FieldType` ∪ all three vocabularies
126+
// (56 types when the guard landed; the union IS `FieldType` since #13716
127+
// dropped the last ghost members), deleting this line moves not one
128+
// resolution. So it is NOT
122129
// load-bearing: adding a type to `SEARCHABLE_TEXTUAL_TYPES` does not also
123130
// require keeping it out of this set for the auto-default to reject it.
124131
//
@@ -129,8 +136,8 @@ function autoDefaultFields(fields: Record<string, SearchFieldMeta>, displayField
129136
// it the positive list wins and the type enters the auto-default AND, one
130137
// layer up, the #4254 ingress allow-list — so `$searchFields=<that field>`
131138
// flips from refused to ACCEPTED, the same widening #4483 closed for `id`.
132-
// This set names `secret`, `password`, `encrypted` and `vector`: failing
133-
// open there means a `$contains` scan over a masked or heavy column.
139+
// This set names `secret`, `password` and `vector`: failing open there
140+
// means a `$contains` scan over a masked or heavy column.
134141
//
135142
// The disjointness is not left to coincidence. `search-fields.test.ts` pins
136143
// all three vocabularies pairwise disjoint AND pins both resolution

0 commit comments

Comments
 (0)