@spotto/semantic-query 1.0.70-alpha.26 → 1.0.70-alpha.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/evaluate.js CHANGED
@@ -1,510 +1,546 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.evaluateSemanticQuery = void 0;
4
- // Workspace Dependencies
5
- const contract_1 = require("@spotto/contract");
6
- // Project Dependencies
7
- const dates_1 = require("./dates");
8
- const resolve_name_1 = require("./resolve-name");
9
- const validate_1 = require("./validate");
10
- const SLOT = {
11
- STRING: 'valueString',
12
- INTEGER: 'valueInteger',
13
- DECIMAL: 'valueDecimal',
14
- BOOLEAN: 'valueBoolean',
15
- DATE: 'valueDate',
16
- };
17
- function get(doc, path) {
18
- return path.split('.').reduce((a, k) => (a == null ? undefined : a[k]), doc);
19
- }
20
- function cmp(actual, op, target) {
21
- if (actual === undefined || actual === null)
22
- return false;
23
- switch (op) {
24
- case 'eq':
25
- return actual === target;
26
- case 'ne':
27
- return actual !== target;
28
- case 'gt':
29
- return actual > target;
30
- case 'gte':
31
- return actual >= target;
32
- case 'lt':
33
- return actual < target;
34
- case 'lte':
35
- return actual <= target;
36
- default:
37
- return false;
38
- }
39
- }
40
- function evaluateSemanticQuery(query, docs, schema, opts = {}) {
41
- var _a, _b, _c;
42
- const ctx = {
43
- schema,
44
- now: (_a = opts.now) !== null && _a !== void 0 ? _a : new Date(),
45
- tz: (_b = opts.timeZone) !== null && _b !== void 0 ? _b : 'UTC',
46
- fieldByName: new Map(schema.fields.map((f) => [f.name.toLowerCase(), f])),
47
- typeById: new Map(schema.types.map((t) => [t.id, t])),
48
- entityType: (_c = opts.entityType) !== null && _c !== void 0 ? _c : 'asset',
49
- };
50
- return docs.filter((d) => query.conditions.every((c) => match(d, c, ctx)));
51
- }
52
- exports.evaluateSemanticQuery = evaluateSemanticQuery;
53
- /**
54
- * Grouping is shared — it is structure, not subject matter — and everything
55
- * else dispatches to the entity's own matcher, mirroring the Mongo compiler so
56
- * the two engines are organised the same way and can be read side by side.
57
- */
58
- function match(doc, cond, ctx) {
59
- if (cond.type === 'and')
60
- return cond.conditions.every((c) => match(doc, c, ctx));
61
- if (cond.type === 'or')
62
- return cond.conditions.some((c) => match(doc, c, ctx));
63
- if (ctx.entityType === 'location') {
64
- return matchLocation(doc, cond, ctx);
65
- }
66
- if (ctx.entityType === 'event') {
67
- return matchEvent(doc, cond, ctx);
68
- }
69
- if (ctx.entityType === 'snapshot') {
70
- return matchSnapshot(doc, cond, ctx);
71
- }
72
- if (ctx.entityType === 'reader') {
73
- return matchReader(doc, cond);
74
- }
75
- return matchAsset(doc, cond, ctx);
76
- }
77
- /**
78
- * Reads the SERVER event document. Events are never evaluated in the browser —
79
- * the client caches query RESULTS, not events, so there is no corpus to run
80
- * over. This matcher exists as the ORACLE: the acceptance property
81
- * `evaluate(q, docs) === find(compile(q))` is what catches an encoding mistake
82
- * in the Mongo compiler, and an entity with no second implementation has
83
- * nothing checking it but the person who wrote it.
84
- *
85
- * The three name-resolved words compare against the ids VALIDATION already
86
- * resolved, from the same catalogue the compiler uses — so the two engines
87
- * cannot disagree about who "Sarah" is.
88
- */
89
- function matchEvent(doc, cond, ctx) {
90
- var _a, _b, _c, _d, _e;
91
- switch (cond.type) {
92
- case 'occurred':
93
- return cmpTimestamp(get(doc, 'timestamp'), cond.operator, cond.value, ctx);
94
- case 'eventType':
95
- return ((_a = cond.values) !== null && _a !== void 0 ? _a : []).includes(get(doc, 'type'));
96
- case 'assetType': {
97
- // BOTH halves, exactly as `action` below and exactly as the compiler:
98
- // `thing` is a full asset only on the CRUD events, so the type scope is
99
- // part of the condition rather than something a caller must remember.
100
- // Checked FIRST — a movement event has no `thing.typePath` at all, and
101
- // reading one would only ever produce a confusing miss.
102
- if (!contract_1.ASSET_CRUD_EVENT_TYPES.includes(String(get(doc, 'type')))) {
103
- return false;
104
- }
105
- const actual = String((_b = get(doc, 'thing.typePath')) !== null && _b !== void 0 ? _b : '');
106
- return cond.includeSubtypes !== false
107
- ? actual.startsWith(cond.path)
108
- : actual === cond.path;
109
- }
110
- case 'location': {
111
- // The stored pipe path, exactly as a location document keeps it — events
112
- // embed it under `location.name`.
113
- const actual = String((_c = get(doc, 'location.name')) !== null && _c !== void 0 ? _c : '');
114
- return cond.includeSublocations !== false
115
- ? actual.startsWith(cond.path)
116
- : actual === cond.path;
117
- }
118
- case 'reader':
119
- return idIn(get(doc, 'reader._id'), resolvedIds(ctx.schema.readers, cond.name));
120
- case 'user':
121
- // A resolved user id inherently excludes system-generated events — Slate
122
- // movements carry no user at all, and must never answer "what did X do".
123
- return idIn(get(doc, 'user._id'), resolvedIds(ctx.schema.users, cond.name));
124
- case 'action': {
125
- // An action implies a submission, so both halves are checked — matching
126
- // the compiler, which constrains the type for the same reason.
127
- // Both submission types — see the compiler's note.
128
- if (!SUBMISSION_EVENT_TYPES.includes(String(get(doc, 'type'))))
129
- return false;
130
- // Resolve ONCE — resolving inside the predicate re-ran it per action.
131
- const ids = resolvedIds(ctx.schema.actions, cond.name);
132
- const names = ((_d = ctx.schema.actions) !== null && _d !== void 0 ? _d : [])
133
- .filter((a) => ids.includes(a.id))
134
- .map((a) => a.name);
135
- return names.includes(String((_e = get(doc, 'submission.actionName')) !== null && _e !== void 0 ? _e : ''));
136
- }
137
- default:
138
- // LOUD, not false. This function is the ORACLE the Mongo compiler is
139
- // checked against, and an oracle that silently matches nothing AGREES
140
- // with a compiler that returns nothing — so a word added to the
141
- // vocabulary and the compiler but forgotten here would let the
142
- // differential check pass by both engines being wrong together.
143
- throw new Error(`semantic-query evaluator has no arm for event condition ` +
144
- `'${cond.type}'`);
145
- }
146
- }
147
- /**
148
- * Reads the SERVER snapshot document.
149
- *
150
- * Unlike the event matcher, this one is NOT only an oracle — it is the
151
- * production path. Snapshots are cached whole and the list filters them in the
152
- * browser (`useSnapshotList` → `db.snapshots.toArray()`), so a mistake here is
153
- * a wrong answer a user sees, not a test that fails. The Mongo compiler still
154
- * has to agree with it for saved queries and widgets.
155
- *
156
- * Two arms below are not the obvious reading of the field, and both are
157
- * copied from the flat filter (`snapshots/filters.ts`) rather than invented,
158
- * because chip and query must return the same rows.
159
- */
160
- function matchSnapshot(doc, cond, ctx) {
161
- var _a, _b, _c;
162
- switch (cond.type) {
163
- case 'occurred':
164
- // `submitted`, not `created` — the flat time filter's field. It is
165
- // optional, so an unsubmitted snapshot matches no bound; `cmpTimestamp`
166
- // returns false for an absent value, which is the same answer the flat
167
- // filter's range gives.
168
- return cmpTimestamp(get(doc, 'submitted'), cond.operator, cond.value, ctx);
169
- case 'snapshotType': {
170
- const type = get(doc, 'type');
171
- const hasManifest = get(doc, 'manifest') != null;
172
- return ((_a = cond.values) !== null && _a !== void 0 ? _a : []).some((wanted) => {
173
- const rule = contract_1.SNAPSHOT_TYPE_RULES[wanted];
174
- if (!rule)
175
- return false;
176
- if (type === rule.explicit)
177
- return true;
178
- // Legacy: no `type` at all, classified by the manifest's presence.
179
- // Without this arm every pre-`type` snapshot silently disappears from
180
- // a kind filter that the chip beside it still returns.
181
- return (type == null &&
182
- rule.legacyHasManifest !== undefined &&
183
- hasManifest === rule.legacyHasManifest);
184
- });
185
- }
186
- case 'asset':
187
- // The snapshot's SUBJECT asset, not its contents — see the contract.
188
- return idIn(get(doc, 'asset._id'), [cond.assetId]);
189
- case 'assetType': {
190
- const actual = String((_b = get(doc, 'asset.typePath')) !== null && _b !== void 0 ? _b : '');
191
- return cond.includeSubtypes !== false
192
- ? actual.startsWith(cond.path)
193
- : actual === cond.path;
194
- }
195
- case 'location': {
196
- // `location.path` here, NOT `location.name` as events use. Absent on
197
- // legacy snapshots, which therefore match no path condition — the same
198
- // acceptance record scope already makes (usergroups S7).
199
- const actual = String((_c = get(doc, 'location.path')) !== null && _c !== void 0 ? _c : '');
200
- if (!actual)
201
- return false;
202
- return cond.includeSublocations !== false
203
- ? actual.startsWith(cond.path)
204
- : actual === cond.path;
205
- }
206
- case 'manifest':
207
- return idIn(get(doc, 'manifest._id'), [cond.manifestId]);
208
- case 'user':
209
- return idIn(get(doc, 'user._id'), resolvedIds(ctx.schema.users, cond.name));
210
- case 'reader':
211
- return idIn(get(doc, 'reader._id'), resolvedIds(ctx.schema.readers, cond.name));
212
- default:
213
- // LOUD, for the reason the event matcher gives — and more so here, since
214
- // this arm running in the browser is what the user actually sees.
215
- throw new Error(`semantic-query evaluator has no arm for snapshot condition ` +
216
- `'${cond.type}'`);
217
- }
218
- }
219
- /**
220
- * Reads the SERVER reader document — and, like the snapshot matcher, this is
221
- * PRODUCTION code: `useReaderList` holds the whole set and the list filters it
222
- * in memory.
223
- *
224
- * No `ctx`: every reader condition is a plain field comparison. Nothing here
225
- * resolves a name or a date, which is the whole reason this vocabulary is the
226
- * least treacherous of the four.
227
- */
228
- function matchReader(doc, cond) {
229
- var _a, _b, _c;
230
- switch (cond.type) {
231
- case 'deviceType':
232
- return ((_a = cond.values) !== null && _a !== void 0 ? _a : []).includes(get(doc, 'deviceType'));
233
- case 'readerType':
234
- return ((_b = cond.values) !== null && _b !== void 0 ? _b : []).includes(get(doc, 'type'));
235
- case 'location': {
236
- // `locationName` holds the FULL pipe path (`readers/helpers.ts` copies
237
- // `location.name`, which is a location document's path). A reader with
238
- // no location matches nothing rather than everything.
239
- const actual = String((_c = get(doc, 'locationName')) !== null && _c !== void 0 ? _c : '');
240
- if (!actual)
241
- return false;
242
- return cond.includeSublocations !== false
243
- ? actual.startsWith(cond.path)
244
- : actual === cond.path;
245
- }
246
- case 'attachedAsset':
247
- return idIn(get(doc, 'asset'), [cond.assetId]);
248
- case 'online':
249
- // Strict equality against the stored boolean. A reader that has never
250
- // reported has no `state` at all, so it is neither online nor offline —
251
- // `hasReported` is the word for that, and conflating the two is exactly
252
- // what the prototype did.
253
- return get(doc, 'state.online') === cond.value;
254
- case 'hasReported':
255
- // `state.onlineTransTime` is the marker the flat filter uses, not
256
- // `state` itself.
257
- return (get(doc, 'state.onlineTransTime') !== undefined) === cond.value;
258
- case 'hasGeolocation':
259
- return (get(doc, 'state.geolocation') != null) === cond.value;
260
- default:
261
- throw new Error(`semantic-query evaluator has no arm for reader condition ` +
262
- `'${cond.type}'`);
263
- }
264
- }
265
- /**
266
- * An event's ONE timestamp against a boundary. No `includeNeverSet` branch:
267
- * every event has a timestamp by construction, so there is no absent case to
268
- * decide a polarity for.
269
- */
270
- function cmpTimestamp(actual, operator, value, ctx) {
271
- if (actual === undefined || actual === null)
272
- return false;
273
- const ms = (0, dates_1.semanticDateBoundaryMs)(value, operator, ctx.now, ctx.tz);
274
- return cmp(actual, operator, ms);
275
- }
276
- /** The event types that carry `submission.actionName`. */
277
- const SUBMISSION_EVENT_TYPES = ['FormSubmitted', 'FormSubmissionVoided'];
278
- /** Ids a name resolves to; empty when it does not resolve (validation refused it first). */
279
- function resolvedIds(catalogue, name) {
280
- return (0, resolve_name_1.resolveName)(catalogue, name, { path: '', noun: '' }).ids;
281
- }
282
- function idIn(actual, ids) {
283
- // Case-insensitive: `String(ObjectId)` is lowercase hex, but a caller-built
284
- // catalogue may carry uppercase (the wire's own id check accepts either).
285
- // Comparing raw would make the ORACLE miss rows the compiler matches — and
286
- // an oracle that under-matches agrees with a broken compiler.
287
- if (actual == null)
288
- return false;
289
- const a = String(actual).toLowerCase();
290
- return ids.some((id) => id.toLowerCase() === a);
291
- }
292
- /**
293
- * Reads the SERVER document shape, as the asset matcher does — a client
294
- * caching parsed responses maps into it first (`evaluable-*`).
295
- *
296
- * Two words differ from their asset namesakes and both would fail silently:
297
- * the pipe path lives in `name` (a location document has no `path` field), and
298
- * `geolocation` is the CONFIGURED position at the top level, not the observed
299
- * `state.geolocation` an asset's condition reads.
300
- */
301
- function matchLocation(doc, cond, ctx) {
302
- var _a, _b;
303
- switch (cond.type) {
304
- case 'path':
305
- return prefixMatch(String((_a = get(doc, 'name')) !== null && _a !== void 0 ? _a : ''), cond.path, cond.includeSublocations !== false);
306
- case 'locationType':
307
- return ((_b = cond.values) !== null && _b !== void 0 ? _b : []).includes(get(doc, 'type'));
308
- case 'hasReaders': {
309
- const readers = get(doc, 'readers');
310
- return (Array.isArray(readers) && readers.length > 0) === cond.value;
311
- }
312
- case 'geofence': {
313
- // `!= null` is deliberate (missing OR explicit null), matching the
314
- // compiler's `{geofence: null}` partition exactly. `!== undefined` alone
315
- // would put a null geofence on the opposite side from Mongo.
316
- const v = get(doc, 'geofence');
317
- return cond.exists ? v != null : v == null;
318
- }
319
- case 'manifest': {
320
- const ids = get(doc, 'manifestIds');
321
- return Array.isArray(ids) && ids.some((id) => String(id) === cond.manifestId);
322
- }
323
- case 'geolocation': {
324
- const v = get(doc, 'geolocation');
325
- return cond.exists !== false ? v !== undefined : v === undefined;
326
- }
327
- case 'tags':
328
- return matchTags(doc, cond);
329
- // No lastSeen / firstSeen / telemetry — see the vocabulary in the contract.
330
- case 'createdDate':
331
- case 'lastUpdated':
332
- case 'lastChanged':
333
- return matchTimestamp(doc, cond, ctx);
334
- default:
335
- return false;
336
- }
337
- }
338
- function matchTags(doc, cond) {
339
- var _a, _b;
340
- const tags = ((_a = doc.tagIds) !== null && _a !== void 0 ? _a : []).map(String);
341
- const want = ((_b = cond.values) !== null && _b !== void 0 ? _b : []).map(String);
342
- return cond.operator === 'all'
343
- ? want.every((t) => tags.includes(t))
344
- : want.some((t) => tags.includes(t));
345
- }
346
- function matchAsset(doc, cond, ctx) {
347
- var _a, _b, _c, _d;
348
- switch (cond.type) {
349
- case 'and':
350
- return cond.conditions.every((c) => match(doc, c, ctx));
351
- case 'or':
352
- return cond.conditions.some((c) => match(doc, c, ctx));
353
- case 'assetType':
354
- return prefixMatch(String((_a = get(doc, 'typePath')) !== null && _a !== void 0 ? _a : ''), cond.path, cond.includeSubtypes !== false);
355
- case 'location':
356
- return prefixMatch(String((_b = get(doc, 'state.locationName')) !== null && _b !== void 0 ? _b : ''), cond.path, cond.includeSublocations !== false);
357
- case 'homeLocation':
358
- return prefixMatch(String((_c = get(doc, 'homeLocationPath')) !== null && _c !== void 0 ? _c : ''), cond.path, cond.includeSublocations !== false);
359
- case 'withAsset': {
360
- const v = get(doc, 'state.locationWithId');
361
- if (cond.hasAny !== undefined)
362
- return (v !== undefined && v !== null) === cond.hasAny;
363
- return v != null && String(v) === cond.assetId;
364
- }
365
- case 'geolocation': {
366
- const v = get(doc, 'state.geolocation');
367
- return cond.exists !== false ? v !== undefined : v === undefined;
368
- }
369
- case 'tags':
370
- return matchTags(doc, cond);
371
- case 'telemetry':
372
- return cmp(get(doc, `telemetry.${cond.field}`), cond.operator, cond.value);
373
- case 'readiness': {
374
- const levels = (_d = cond.levels) !== null && _d !== void 0 ? _d : (cond.level === undefined ? [] : [cond.level]);
375
- // Missing readiness ≡ green (0).
376
- const actual = get(doc, 'readiness.ready');
377
- const effective = typeof actual === 'number' ? actual : 0;
378
- return levels.includes(effective);
379
- }
380
- case 'kit': {
381
- if (cond.parentAssetId !== undefined) {
382
- const p = get(doc, 'groupParentId');
383
- return p != null && String(p) === cond.parentAssetId;
384
- }
385
- if (cond.isKit !== undefined)
386
- return (doc.typeKit === true) === cond.isKit;
387
- if (cond.isMember !== undefined)
388
- return (doc.groupMember === true) === cond.isMember;
389
- // Three-valued, unlike the two flags above: `groupSatisfiesKit` is only
390
- // set when the asset IS a kit, its type carries a manifest template, and
391
- // that template declares requirements (groups/satisfies.ts). Absent means
392
- // the question does not apply — so it is neither satisfied NOR
393
- // unsatisfied, and an exact match answers both directions.
394
- return doc.groupSatisfiesKit === cond.satisfied;
395
- }
396
- case 'locationStatus': {
397
- const status = get(doc, 'state.locationStatus');
398
- if (typeof status === 'string' && cond.values.includes(status))
399
- return true;
400
- // NEVERSEEN also means "no state at all".
401
- return cond.values.includes('NEVERSEEN') && get(doc, 'state') === undefined;
402
- }
403
- case 'createdDate':
404
- case 'lastUpdated':
405
- case 'lastChanged':
406
- case 'lastSeen':
407
- case 'firstSeen':
408
- return matchTimestamp(doc, cond, ctx);
409
- case 'customField':
410
- return matchCustomField(doc, cond, ctx);
411
- default:
412
- return false;
413
- }
414
- }
415
- function prefixMatch(actual, path, includeDescendants) {
416
- return includeDescendants ? actual.startsWith(path) : actual === path;
417
- }
418
- const TIMESTAMP_FIELD = {
419
- lastUpdated: 'lastUpdated',
420
- lastChanged: 'lastChanged',
421
- lastSeen: 'state.lastSeen',
422
- firstSeen: 'state.firstSeen',
423
- };
424
- function matchTimestamp(doc, cond, ctx) {
425
- const ms = (0, dates_1.semanticDateBoundaryMs)(cond.value, cond.operator, ctx.now, ctx.tz);
426
- if (cond.type === 'createdDate') {
427
- // The ObjectId's first 4 bytes are the creation time in SECONDS. The
428
- // compiler steps `lte`/`gt` to the next second (a zero-tail boundary id
429
- // sorts before every real id in the same second); mirror that exactly,
430
- // or documents created within the boundary second classify differently.
431
- const created = parseInt(String(doc._id).slice(0, 8), 16);
432
- const second = Math.floor(ms / 1000);
433
- switch (cond.operator) {
434
- case 'gte':
435
- return created >= second;
436
- case 'lt':
437
- return created < second;
438
- case 'lte':
439
- return created < second + 1;
440
- default: // gt
441
- return created >= second + 1;
442
- }
443
- }
444
- const actual = get(doc, TIMESTAMP_FIELD[cond.type]);
445
- const neverSet = actual === undefined || actual === null;
446
- if (neverSet) {
447
- return ((cond.operator === 'lt' || cond.operator === 'lte') &&
448
- cond.includeNeverSet !== false);
449
- }
450
- return cmp(actual, cond.operator, ms);
451
- }
452
- // ---------------------------------------------------------------------------
453
- // Custom fields — mirror of the compiler's two-container conjunct
454
- // ---------------------------------------------------------------------------
455
- /**
456
- * Every value the field holds in this container.
457
- *
458
- * Matched by field `_id` ONLY — the compiler's `$elemMatch: {_id: …}` cannot
459
- * match a name — and ALL matching entries are returned, because `$elemMatch`
460
- * is existential: a duplicated field where one entry is null and another
461
- * holds the value must match, so inspecting only the first entry would
462
- * disagree with Mongo.
463
- */
464
- function valuesFor(entries, fdef, slot) {
465
- return (entries !== null && entries !== void 0 ? entries : [])
466
- .filter((e) => (e === null || e === void 0 ? void 0 : e._id) !== undefined && String(e._id) === fdef.id)
467
- .map((e) => e[slot]);
468
- }
469
- function isPresent(v) {
470
- return v !== undefined && v !== null && v !== '';
471
- }
472
- function matchCustomField(doc, cond, ctx) {
473
- var _a;
474
- const fdef = ctx.fieldByName.get(String(cond.fieldName).toLowerCase());
475
- if (!fdef)
476
- return false;
477
- const slot = SLOT[fdef.dataType];
478
- // The in-memory equivalent of the joinedType $lookup: the asset's own
479
- // fieldValues entries, and its type's inheritance-resolved entries.
480
- const type = ctx.typeById.get(String(doc.typeId));
481
- const values = [
482
- ...valuesFor(doc.fieldValues, fdef, slot),
483
- ...valuesFor(type === null || type === void 0 ? void 0 : type.typeFieldValuesAll, fdef, slot),
484
- ];
485
- const anyPresent = values.some(isPresent);
486
- switch (cond.operator) {
487
- case 'exists':
488
- return anyPresent;
489
- case 'notExists':
490
- return !anyPresent;
491
- case 'contains': {
492
- const term = String((_a = cond.value) !== null && _a !== void 0 ? _a : '').toLowerCase();
493
- return values.some((v) => isPresent(v) && String(v).toLowerCase().includes(term));
494
- }
495
- default:
496
- break;
497
- }
498
- // The SAME coercion the Mongo compiler applies — shared, not mirrored.
499
- // A numeric literal for a text field, or a relative date for a DATE field,
500
- // has to become one concrete value; two copies of this rule would classify
501
- // the same document differently.
502
- const target = (0, validate_1.coerceSemanticValue)(cond.value, fdef.dataType, ctx.now, ctx.tz);
503
- if (cond.operator === 'ne') {
504
- // Present-and-different at either level; default also counts fully unset.
505
- const strict = values.some((v) => isPresent(v) && v !== target);
506
- return cond.includeUnset === false ? strict : strict || !anyPresent;
507
- }
508
- return values.some((v) => cmp(v, cond.operator, target));
509
- }
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.evaluateSemanticQuery = void 0;
4
+ // Workspace Dependencies
5
+ const contract_1 = require("@spotto/contract");
6
+ // Project Dependencies
7
+ const dates_1 = require("./dates");
8
+ const resolve_name_1 = require("./resolve-name");
9
+ const validate_1 = require("./validate");
10
+ const SLOT = {
11
+ STRING: 'valueString',
12
+ INTEGER: 'valueInteger',
13
+ DECIMAL: 'valueDecimal',
14
+ BOOLEAN: 'valueBoolean',
15
+ DATE: 'valueDate',
16
+ };
17
+ function get(doc, path) {
18
+ return path.split('.').reduce((a, k) => (a == null ? undefined : a[k]), doc);
19
+ }
20
+ function cmp(actual, op, target) {
21
+ if (actual === undefined || actual === null)
22
+ return false;
23
+ switch (op) {
24
+ case 'eq':
25
+ return actual === target;
26
+ case 'ne':
27
+ return actual !== target;
28
+ case 'gt':
29
+ return actual > target;
30
+ case 'gte':
31
+ return actual >= target;
32
+ case 'lt':
33
+ return actual < target;
34
+ case 'lte':
35
+ return actual <= target;
36
+ default:
37
+ return false;
38
+ }
39
+ }
40
+ function evaluateSemanticQuery(query, docs, schema, opts = {}) {
41
+ var _a, _b, _c;
42
+ const ctx = {
43
+ schema,
44
+ now: (_a = opts.now) !== null && _a !== void 0 ? _a : new Date(),
45
+ tz: (_b = opts.timeZone) !== null && _b !== void 0 ? _b : 'UTC',
46
+ fieldByName: new Map(schema.fields.map((f) => [f.name.toLowerCase(), f])),
47
+ typeById: new Map(schema.types.map((t) => [t.id, t])),
48
+ entityType: (_c = opts.entityType) !== null && _c !== void 0 ? _c : 'asset',
49
+ };
50
+ return docs.filter((d) => query.conditions.every((c) => match(d, c, ctx)));
51
+ }
52
+ exports.evaluateSemanticQuery = evaluateSemanticQuery;
53
+ /**
54
+ * Grouping is shared — it is structure, not subject matter — and everything
55
+ * else dispatches to the entity's own matcher, mirroring the Mongo compiler so
56
+ * the two engines are organised the same way and can be read side by side.
57
+ */
58
+ function match(doc, cond, ctx) {
59
+ if (cond.type === 'and')
60
+ return cond.conditions.every((c) => match(doc, c, ctx));
61
+ if (cond.type === 'or')
62
+ return cond.conditions.some((c) => match(doc, c, ctx));
63
+ if (ctx.entityType === 'location') {
64
+ return matchLocation(doc, cond, ctx);
65
+ }
66
+ if (ctx.entityType === 'event') {
67
+ return matchEvent(doc, cond, ctx);
68
+ }
69
+ if (ctx.entityType === 'snapshot') {
70
+ return matchSnapshot(doc, cond, ctx);
71
+ }
72
+ if (ctx.entityType === 'reader') {
73
+ return matchReader(doc, cond);
74
+ }
75
+ return matchAsset(doc, cond, ctx);
76
+ }
77
+ /**
78
+ * Reads the SERVER event document. Events are never evaluated in the browser —
79
+ * the client caches query RESULTS, not events, so there is no corpus to run
80
+ * over. This matcher exists as the ORACLE: the acceptance property
81
+ * `evaluate(q, docs) === find(compile(q))` is what catches an encoding mistake
82
+ * in the Mongo compiler, and an entity with no second implementation has
83
+ * nothing checking it but the person who wrote it.
84
+ *
85
+ * The three name-resolved words compare against the ids VALIDATION already
86
+ * resolved, from the same catalogue the compiler uses — so the two engines
87
+ * cannot disagree about who "Sarah" is.
88
+ */
89
+ function matchEvent(doc, cond, ctx) {
90
+ var _a, _b, _c, _d, _e;
91
+ switch (cond.type) {
92
+ case 'occurred':
93
+ return cmpTimestamp(get(doc, 'timestamp'), cond.operator, cond.value, ctx);
94
+ case 'eventType':
95
+ return ((_a = cond.values) !== null && _a !== void 0 ? _a : []).includes(get(doc, 'type'));
96
+ case 'assetType': {
97
+ // BOTH halves, exactly as `action` below and exactly as the compiler:
98
+ // `thing` is a full asset only on the CRUD events, so the type scope is
99
+ // part of the condition rather than something a caller must remember.
100
+ // Checked FIRST — a movement event has no `thing.typePath` at all, and
101
+ // reading one would only ever produce a confusing miss.
102
+ if (!contract_1.ASSET_CRUD_EVENT_TYPES.includes(String(get(doc, 'type')))) {
103
+ return false;
104
+ }
105
+ const actual = String((_b = get(doc, 'thing.typePath')) !== null && _b !== void 0 ? _b : '');
106
+ return cond.includeSubtypes !== false
107
+ ? actual.startsWith(cond.path)
108
+ : actual === cond.path;
109
+ }
110
+ case 'location': {
111
+ // The stored pipe path, exactly as a location document keeps it — events
112
+ // embed it under `location.name`.
113
+ const actual = String((_c = get(doc, 'location.name')) !== null && _c !== void 0 ? _c : '');
114
+ return cond.includeSublocations !== false
115
+ ? actual.startsWith(cond.path)
116
+ : actual === cond.path;
117
+ }
118
+ case 'reader':
119
+ return idIn(get(doc, 'reader._id'), resolvedIds(ctx.schema.readers, cond.name));
120
+ case 'user':
121
+ // A resolved user id inherently excludes system-generated events — Slate
122
+ // movements carry no user at all, and must never answer "what did X do".
123
+ return idIn(get(doc, 'user._id'), resolvedIds(ctx.schema.users, cond.name));
124
+ case 'action': {
125
+ // An action implies a submission, so both halves are checked — matching
126
+ // the compiler, which constrains the type for the same reason.
127
+ // Both submission types — see the compiler's note.
128
+ if (!SUBMISSION_EVENT_TYPES.includes(String(get(doc, 'type'))))
129
+ return false;
130
+ // Resolve ONCE — resolving inside the predicate re-ran it per action.
131
+ const ids = resolvedIds(ctx.schema.actions, cond.name);
132
+ const names = ((_d = ctx.schema.actions) !== null && _d !== void 0 ? _d : [])
133
+ .filter((a) => ids.includes(a.id))
134
+ .map((a) => a.name);
135
+ return names.includes(String((_e = get(doc, 'submission.actionName')) !== null && _e !== void 0 ? _e : ''));
136
+ }
137
+ default:
138
+ // LOUD, not false. This function is the ORACLE the Mongo compiler is
139
+ // checked against, and an oracle that silently matches nothing AGREES
140
+ // with a compiler that returns nothing — so a word added to the
141
+ // vocabulary and the compiler but forgotten here would let the
142
+ // differential check pass by both engines being wrong together.
143
+ throw new Error(`semantic-query evaluator has no arm for event condition ` +
144
+ `'${cond.type}'`);
145
+ }
146
+ }
147
+ /**
148
+ * Reads the SERVER snapshot document.
149
+ *
150
+ * Unlike the event matcher, this one is NOT only an oracle — it is the
151
+ * production path. Snapshots are cached whole and the list filters them in the
152
+ * browser (`useSnapshotList` → `db.snapshots.toArray()`), so a mistake here is
153
+ * a wrong answer a user sees, not a test that fails. The Mongo compiler still
154
+ * has to agree with it for saved queries and widgets.
155
+ *
156
+ * Two arms below are not the obvious reading of the field, and both are
157
+ * copied from the flat filter (`snapshots/filters.ts`) rather than invented,
158
+ * because chip and query must return the same rows.
159
+ */
160
+ function matchSnapshot(doc, cond, ctx) {
161
+ var _a, _b, _c;
162
+ switch (cond.type) {
163
+ case 'occurred':
164
+ // `submitted`, not `created` — the flat time filter's field. It is
165
+ // optional, so an unsubmitted snapshot matches no bound; `cmpTimestamp`
166
+ // returns false for an absent value, which is the same answer the flat
167
+ // filter's range gives.
168
+ return cmpTimestamp(get(doc, 'submitted'), cond.operator, cond.value, ctx);
169
+ case 'snapshotType': {
170
+ const type = get(doc, 'type');
171
+ const hasManifest = get(doc, 'manifest') != null;
172
+ return ((_a = cond.values) !== null && _a !== void 0 ? _a : []).some((wanted) => {
173
+ const rule = contract_1.SNAPSHOT_TYPE_RULES[wanted];
174
+ if (!rule)
175
+ return false;
176
+ if (type === rule.explicit)
177
+ return true;
178
+ // Legacy: no `type` at all, classified by the manifest's presence.
179
+ // Without this arm every pre-`type` snapshot silently disappears from
180
+ // a kind filter that the chip beside it still returns.
181
+ return (type == null &&
182
+ rule.legacyHasManifest !== undefined &&
183
+ hasManifest === rule.legacyHasManifest);
184
+ });
185
+ }
186
+ case 'asset':
187
+ // The snapshot's SUBJECT asset, not its contents — see the contract.
188
+ return idIn(get(doc, 'asset._id'), [cond.assetId]);
189
+ case 'assetType': {
190
+ const actual = String((_b = get(doc, 'asset.typePath')) !== null && _b !== void 0 ? _b : '');
191
+ return cond.includeSubtypes !== false
192
+ ? actual.startsWith(cond.path)
193
+ : actual === cond.path;
194
+ }
195
+ case 'location': {
196
+ // `location.path` here, NOT `location.name` as events use. Absent on
197
+ // legacy snapshots, which therefore match no path condition — the same
198
+ // acceptance record scope already makes (usergroups S7).
199
+ const actual = String((_c = get(doc, 'location.path')) !== null && _c !== void 0 ? _c : '');
200
+ if (!actual)
201
+ return false;
202
+ return cond.includeSublocations !== false
203
+ ? actual.startsWith(cond.path)
204
+ : actual === cond.path;
205
+ }
206
+ case 'manifest': {
207
+ // Mirrors the compiler byte-for-byte: `satisfied` is an exact match on
208
+ // the frozen `c_satisfiesManifest` annotation — a snapshot that never
209
+ // stamped a verdict carries no flag and matches NEITHER polarity. Both
210
+ // keys AND together when present; the validator guarantees at least
211
+ // one, so reaching the end means every present key matched.
212
+ if (cond.manifestId !== undefined &&
213
+ !idIn(get(doc, 'manifest._id'), [cond.manifestId])) {
214
+ return false;
215
+ }
216
+ if (cond.satisfied !== undefined &&
217
+ get(doc, 'c_satisfiesManifest') !== cond.satisfied) {
218
+ return false;
219
+ }
220
+ return true;
221
+ }
222
+ case 'user':
223
+ return idIn(get(doc, 'user._id'), resolvedIds(ctx.schema.users, cond.name));
224
+ case 'reader':
225
+ return idIn(get(doc, 'reader._id'), resolvedIds(ctx.schema.readers, cond.name));
226
+ default:
227
+ // LOUD, for the reason the event matcher gives — and more so here, since
228
+ // this arm running in the browser is what the user actually sees.
229
+ throw new Error(`semantic-query evaluator has no arm for snapshot condition ` +
230
+ `'${cond.type}'`);
231
+ }
232
+ }
233
+ /**
234
+ * Reads the SERVER reader document and, like the snapshot matcher, this is
235
+ * PRODUCTION code: `useReaderList` holds the whole set and the list filters it
236
+ * in memory.
237
+ *
238
+ * No `ctx`: every reader condition is a plain field comparison. Nothing here
239
+ * resolves a name or a date, which is the whole reason this vocabulary is the
240
+ * least treacherous of the four.
241
+ */
242
+ function matchReader(doc, cond) {
243
+ var _a, _b, _c;
244
+ switch (cond.type) {
245
+ case 'deviceType':
246
+ return ((_a = cond.values) !== null && _a !== void 0 ? _a : []).includes(get(doc, 'deviceType'));
247
+ case 'readerType':
248
+ return ((_b = cond.values) !== null && _b !== void 0 ? _b : []).includes(get(doc, 'type'));
249
+ case 'location': {
250
+ // `locationName` holds the FULL pipe path (`readers/helpers.ts` copies
251
+ // `location.name`, which is a location document's path). A reader with
252
+ // no location matches nothing rather than everything.
253
+ const actual = String((_c = get(doc, 'locationName')) !== null && _c !== void 0 ? _c : '');
254
+ if (!actual)
255
+ return false;
256
+ return cond.includeSublocations !== false
257
+ ? actual.startsWith(cond.path)
258
+ : actual === cond.path;
259
+ }
260
+ case 'attachedAsset':
261
+ return idIn(get(doc, 'asset'), [cond.assetId]);
262
+ case 'online':
263
+ // Strict equality against the stored boolean. A reader that has never
264
+ // reported has no `state` at all, so it is neither online nor offline —
265
+ // `hasReported` is the word for that, and conflating the two is exactly
266
+ // what the prototype did.
267
+ return get(doc, 'state.online') === cond.value;
268
+ case 'hasReported':
269
+ // `state.onlineTransTime` is the marker the flat filter uses, not
270
+ // `state` itself.
271
+ return (get(doc, 'state.onlineTransTime') !== undefined) === cond.value;
272
+ case 'hasGeolocation':
273
+ return (get(doc, 'state.geolocation') != null) === cond.value;
274
+ default:
275
+ throw new Error(`semantic-query evaluator has no arm for reader condition ` +
276
+ `'${cond.type}'`);
277
+ }
278
+ }
279
+ /**
280
+ * An event's ONE timestamp against a boundary. No `includeNeverSet` branch:
281
+ * every event has a timestamp by construction, so there is no absent case to
282
+ * decide a polarity for.
283
+ */
284
+ function cmpTimestamp(actual, operator, value, ctx) {
285
+ if (actual === undefined || actual === null)
286
+ return false;
287
+ const ms = (0, dates_1.semanticDateBoundaryMs)(value, operator, ctx.now, ctx.tz);
288
+ return cmp(actual, operator, ms);
289
+ }
290
+ /** The event types that carry `submission.actionName`. */
291
+ const SUBMISSION_EVENT_TYPES = ['FormSubmitted', 'FormSubmissionVoided'];
292
+ /** Ids a name resolves to; empty when it does not resolve (validation refused it first). */
293
+ function resolvedIds(catalogue, name) {
294
+ return (0, resolve_name_1.resolveName)(catalogue, name, { path: '', noun: '' }).ids;
295
+ }
296
+ function idIn(actual, ids) {
297
+ // Case-insensitive: `String(ObjectId)` is lowercase hex, but a caller-built
298
+ // catalogue may carry uppercase (the wire's own id check accepts either).
299
+ // Comparing raw would make the ORACLE miss rows the compiler matches — and
300
+ // an oracle that under-matches agrees with a broken compiler.
301
+ if (actual == null)
302
+ return false;
303
+ const a = String(actual).toLowerCase();
304
+ return ids.some((id) => id.toLowerCase() === a);
305
+ }
306
+ /**
307
+ * Reads the SERVER document shape, as the asset matcher does a client
308
+ * caching parsed responses maps into it first (`evaluable-*`).
309
+ *
310
+ * Two words differ from their asset namesakes and both would fail silently:
311
+ * the pipe path lives in `name` (a location document has no `path` field), and
312
+ * `geolocation` is the CONFIGURED position at the top level, not the observed
313
+ * `state.geolocation` an asset's condition reads.
314
+ */
315
+ function matchLocation(doc, cond, ctx) {
316
+ var _a, _b;
317
+ switch (cond.type) {
318
+ case 'path':
319
+ return prefixMatch(String((_a = get(doc, 'name')) !== null && _a !== void 0 ? _a : ''), cond.path, cond.includeSublocations !== false);
320
+ case 'locationType':
321
+ return ((_b = cond.values) !== null && _b !== void 0 ? _b : []).includes(get(doc, 'type'));
322
+ case 'hasReaders': {
323
+ const readers = get(doc, 'readers');
324
+ return (Array.isArray(readers) && readers.length > 0) === cond.value;
325
+ }
326
+ case 'geofence': {
327
+ // `!= null` is deliberate (missing OR explicit null), matching the
328
+ // compiler's `{geofence: null}` partition exactly. `!== undefined` alone
329
+ // would put a null geofence on the opposite side from Mongo.
330
+ const v = get(doc, 'geofence');
331
+ return cond.exists ? v != null : v == null;
332
+ }
333
+ case 'manifest': {
334
+ const ids = get(doc, 'manifestIds');
335
+ return Array.isArray(ids) && ids.some((id) => String(id) === cond.manifestId);
336
+ }
337
+ case 'geolocation': {
338
+ const v = get(doc, 'geolocation');
339
+ return cond.exists !== false ? v !== undefined : v === undefined;
340
+ }
341
+ case 'tags':
342
+ return matchTags(doc, cond);
343
+ // No lastSeen / firstSeen / telemetry — see the vocabulary in the contract.
344
+ case 'createdDate':
345
+ case 'lastUpdated':
346
+ case 'lastChanged':
347
+ return matchTimestamp(doc, cond, ctx);
348
+ default:
349
+ return false;
350
+ }
351
+ }
352
+ function matchTags(doc, cond) {
353
+ var _a, _b;
354
+ const tags = ((_a = doc.tagIds) !== null && _a !== void 0 ? _a : []).map(String);
355
+ const want = ((_b = cond.values) !== null && _b !== void 0 ? _b : []).map(String);
356
+ return cond.operator === 'all'
357
+ ? want.every((t) => tags.includes(t))
358
+ : want.some((t) => tags.includes(t));
359
+ }
360
+ function matchAsset(doc, cond, ctx) {
361
+ var _a, _b, _c, _d;
362
+ switch (cond.type) {
363
+ case 'and':
364
+ return cond.conditions.every((c) => match(doc, c, ctx));
365
+ case 'or':
366
+ return cond.conditions.some((c) => match(doc, c, ctx));
367
+ case 'assetType':
368
+ return prefixMatch(String((_a = get(doc, 'typePath')) !== null && _a !== void 0 ? _a : ''), cond.path, cond.includeSubtypes !== false);
369
+ case 'location':
370
+ return prefixMatch(String((_b = get(doc, 'state.locationName')) !== null && _b !== void 0 ? _b : ''), cond.path, cond.includeSublocations !== false);
371
+ case 'homeLocation':
372
+ return prefixMatch(String((_c = get(doc, 'homeLocationPath')) !== null && _c !== void 0 ? _c : ''), cond.path, cond.includeSublocations !== false);
373
+ case 'withAsset': {
374
+ const v = get(doc, 'state.locationWithId');
375
+ if (cond.hasAny !== undefined)
376
+ return (v !== undefined && v !== null) === cond.hasAny;
377
+ return v != null && String(v) === cond.assetId;
378
+ }
379
+ case 'geolocation': {
380
+ const v = get(doc, 'state.geolocation');
381
+ return cond.exists !== false ? v !== undefined : v === undefined;
382
+ }
383
+ case 'tags':
384
+ return matchTags(doc, cond);
385
+ case 'telemetry':
386
+ return cmp(get(doc, `telemetry.${cond.field}`), cond.operator, cond.value);
387
+ case 'readiness': {
388
+ const levels = (_d = cond.levels) !== null && _d !== void 0 ? _d : (cond.level === undefined ? [] : [cond.level]);
389
+ // Missing readiness green (0).
390
+ const actual = get(doc, 'readiness.ready');
391
+ const effective = typeof actual === 'number' ? actual : 0;
392
+ return levels.includes(effective);
393
+ }
394
+ case 'kit': {
395
+ if (cond.parentAssetId !== undefined) {
396
+ const p = get(doc, 'groupParentId');
397
+ return p != null && String(p) === cond.parentAssetId;
398
+ }
399
+ if (cond.isKit !== undefined)
400
+ return (doc.typeKit === true) === cond.isKit;
401
+ if (cond.isMember !== undefined)
402
+ return (doc.groupMember === true) === cond.isMember;
403
+ // The 4-state manifest-check status, mirroring the Mongo compile
404
+ // exactly — `neverChecked` carries the same typeKit self-scope, and
405
+ // every state requires `hasManifest` (a stored status outlives a
406
+ // manifest detach as inert history; the gate keeps the filter agreeing
407
+ // with the display).
408
+ if (cond.manifest !== undefined) {
409
+ if (doc.hasManifest !== true)
410
+ return false;
411
+ const status = doc.groupSatisfiesManifest;
412
+ switch (cond.manifest) {
413
+ case 'satisfied':
414
+ return status === 0;
415
+ case 'needsRecheck':
416
+ return status === 1;
417
+ case 'notSatisfied':
418
+ return status === 2;
419
+ case 'neverChecked':
420
+ return doc.typeKit === true && status === undefined;
421
+ }
422
+ }
423
+ // Legacy `satisfied` boolean over the 4-state `groupSatisfiesManifest`
424
+ // (0 satisfied · 1 needs-recheck · 2 not satisfied · absent never
425
+ // checked): true 0, false 2, exactly mirroring the Mongo compile —
426
+ // including its `hasManifest` gate. Exact value matches — absent means
427
+ // no manifest check has ever stamped the field, so it is neither
428
+ // satisfied NOR unsatisfied.
429
+ return (doc.hasManifest === true &&
430
+ doc.groupSatisfiesManifest === (cond.satisfied ? 0 : 2));
431
+ }
432
+ case 'locationStatus': {
433
+ const status = get(doc, 'state.locationStatus');
434
+ if (typeof status === 'string' && cond.values.includes(status))
435
+ return true;
436
+ // NEVERSEEN also means "no state at all".
437
+ return cond.values.includes('NEVERSEEN') && get(doc, 'state') === undefined;
438
+ }
439
+ case 'createdDate':
440
+ case 'lastUpdated':
441
+ case 'lastChanged':
442
+ case 'lastSeen':
443
+ case 'firstSeen':
444
+ return matchTimestamp(doc, cond, ctx);
445
+ case 'customField':
446
+ return matchCustomField(doc, cond, ctx);
447
+ default:
448
+ return false;
449
+ }
450
+ }
451
+ function prefixMatch(actual, path, includeDescendants) {
452
+ return includeDescendants ? actual.startsWith(path) : actual === path;
453
+ }
454
+ const TIMESTAMP_FIELD = {
455
+ lastUpdated: 'lastUpdated',
456
+ lastChanged: 'lastChanged',
457
+ lastSeen: 'state.lastSeen',
458
+ firstSeen: 'state.firstSeen',
459
+ };
460
+ function matchTimestamp(doc, cond, ctx) {
461
+ const ms = (0, dates_1.semanticDateBoundaryMs)(cond.value, cond.operator, ctx.now, ctx.tz);
462
+ if (cond.type === 'createdDate') {
463
+ // The ObjectId's first 4 bytes are the creation time in SECONDS. The
464
+ // compiler steps `lte`/`gt` to the next second (a zero-tail boundary id
465
+ // sorts before every real id in the same second); mirror that exactly,
466
+ // or documents created within the boundary second classify differently.
467
+ const created = parseInt(String(doc._id).slice(0, 8), 16);
468
+ const second = Math.floor(ms / 1000);
469
+ switch (cond.operator) {
470
+ case 'gte':
471
+ return created >= second;
472
+ case 'lt':
473
+ return created < second;
474
+ case 'lte':
475
+ return created < second + 1;
476
+ default: // gt
477
+ return created >= second + 1;
478
+ }
479
+ }
480
+ const actual = get(doc, TIMESTAMP_FIELD[cond.type]);
481
+ const neverSet = actual === undefined || actual === null;
482
+ if (neverSet) {
483
+ return ((cond.operator === 'lt' || cond.operator === 'lte') &&
484
+ cond.includeNeverSet !== false);
485
+ }
486
+ return cmp(actual, cond.operator, ms);
487
+ }
488
+ // ---------------------------------------------------------------------------
489
+ // Custom fields — mirror of the compiler's two-container conjunct
490
+ // ---------------------------------------------------------------------------
491
+ /**
492
+ * Every value the field holds in this container.
493
+ *
494
+ * Matched by field `_id` ONLY — the compiler's `$elemMatch: {_id: …}` cannot
495
+ * match a name — and ALL matching entries are returned, because `$elemMatch`
496
+ * is existential: a duplicated field where one entry is null and another
497
+ * holds the value must match, so inspecting only the first entry would
498
+ * disagree with Mongo.
499
+ */
500
+ function valuesFor(entries, fdef, slot) {
501
+ return (entries !== null && entries !== void 0 ? entries : [])
502
+ .filter((e) => (e === null || e === void 0 ? void 0 : e._id) !== undefined && String(e._id) === fdef.id)
503
+ .map((e) => e[slot]);
504
+ }
505
+ function isPresent(v) {
506
+ return v !== undefined && v !== null && v !== '';
507
+ }
508
+ function matchCustomField(doc, cond, ctx) {
509
+ var _a;
510
+ const fdef = ctx.fieldByName.get(String(cond.fieldName).toLowerCase());
511
+ if (!fdef)
512
+ return false;
513
+ const slot = SLOT[fdef.dataType];
514
+ // The in-memory equivalent of the joinedType $lookup: the asset's own
515
+ // fieldValues entries, and its type's inheritance-resolved entries.
516
+ const type = ctx.typeById.get(String(doc.typeId));
517
+ const values = [
518
+ ...valuesFor(doc.fieldValues, fdef, slot),
519
+ ...valuesFor(type === null || type === void 0 ? void 0 : type.typeFieldValuesAll, fdef, slot),
520
+ ];
521
+ const anyPresent = values.some(isPresent);
522
+ switch (cond.operator) {
523
+ case 'exists':
524
+ return anyPresent;
525
+ case 'notExists':
526
+ return !anyPresent;
527
+ case 'contains': {
528
+ const term = String((_a = cond.value) !== null && _a !== void 0 ? _a : '').toLowerCase();
529
+ return values.some((v) => isPresent(v) && String(v).toLowerCase().includes(term));
530
+ }
531
+ default:
532
+ break;
533
+ }
534
+ // The SAME coercion the Mongo compiler applies — shared, not mirrored.
535
+ // A numeric literal for a text field, or a relative date for a DATE field,
536
+ // has to become one concrete value; two copies of this rule would classify
537
+ // the same document differently.
538
+ const target = (0, validate_1.coerceSemanticValue)(cond.value, fdef.dataType, ctx.now, ctx.tz);
539
+ if (cond.operator === 'ne') {
540
+ // Present-and-different at either level; default also counts fully unset.
541
+ const strict = values.some((v) => isPresent(v) && v !== target);
542
+ return cond.includeUnset === false ? strict : strict || !anyPresent;
543
+ }
544
+ return values.some((v) => cmp(v, cond.operator, target));
545
+ }
510
546
  //# sourceMappingURL=evaluate.js.map