@lde/search 0.37.0 → 0.38.0

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/period.d.ts CHANGED
@@ -53,11 +53,11 @@ export declare const OPEN_END: number;
53
53
  */
54
54
  export declare function periodOf(value: string): StoredPeriod | undefined;
55
55
  /**
56
- * The period several values cover between them: the earliest start and the
57
- * latest end, or `undefined` when none states a period. One stored range per
58
- * document is what an engine can sort on and compare in a single clause, so a
59
- * work dated `1668`, `1676` and `1686` is stored as 1668 through 1686 – and a
60
- * range falling between its dates still matches it.
56
+ * The period several periods cover between them: the earliest start and the
57
+ * latest end. One stored range per document is what an engine can sort on and
58
+ * compare in a single clause, so a work dated `1668`, `1676` and `1686` is
59
+ * stored as 1668 through 1686 – and a range falling between its dates still
60
+ * matches it.
61
61
  */
62
- export declare function periodOfAll(values: readonly string[]): StoredPeriod | undefined;
62
+ export declare function periodCovering(first: StoredPeriod, ...rest: readonly StoredPeriod[]): StoredPeriod;
63
63
  //# sourceMappingURL=period.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"period.d.ts","sourceRoot":"","sources":["../src/period.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,QAAa,CAAC;AAErC;oCACoC;AACpC,eAAO,MAAM,QAAQ,QAAU,CAAC;AAEhC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAehE;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,SAAS,MAAM,EAAE,GACxB,YAAY,GAAG,SAAS,CAgB1B"}
1
+ {"version":3,"file":"period.d.ts","sourceRoot":"","sources":["../src/period.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,QAAa,CAAC;AAErC;oCACoC;AACpC,eAAO,MAAM,QAAQ,QAAU,CAAC;AAEhC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAehE;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,YAAY,EACnB,GAAG,IAAI,EAAE,SAAS,YAAY,EAAE,GAC/B,YAAY,CAQd"}
package/dist/period.js CHANGED
@@ -59,28 +59,17 @@ export function periodOf(value) {
59
59
  return start <= end && !openAtBothEnds ? { start, end } : undefined;
60
60
  }
61
61
  /**
62
- * The period several values cover between them: the earliest start and the
63
- * latest end, or `undefined` when none states a period. One stored range per
64
- * document is what an engine can sort on and compare in a single clause, so a
65
- * work dated `1668`, `1676` and `1686` is stored as 1668 through 1686 – and a
66
- * range falling between its dates still matches it.
62
+ * The period several periods cover between them: the earliest start and the
63
+ * latest end. One stored range per document is what an engine can sort on and
64
+ * compare in a single clause, so a work dated `1668`, `1676` and `1686` is
65
+ * stored as 1668 through 1686 – and a range falling between its dates still
66
+ * matches it.
67
67
  */
68
- export function periodOfAll(values) {
69
- let covered;
70
- for (const value of values) {
71
- const period = periodOf(value);
72
- if (period === undefined) {
73
- continue;
74
- }
75
- covered =
76
- covered === undefined
77
- ? period
78
- : {
79
- start: Math.min(covered.start, period.start),
80
- end: Math.max(covered.end, period.end),
81
- };
82
- }
83
- return covered;
68
+ export function periodCovering(first, ...rest) {
69
+ return rest.reduce((covered, period) => ({
70
+ start: Math.min(covered.start, period.start),
71
+ end: Math.max(covered.end, period.end),
72
+ }), first);
84
73
  }
85
74
  /** One side of an interval, or a whole plain value. */
86
75
  function sideOf(text) {
package/dist/project.d.ts CHANGED
@@ -27,6 +27,24 @@ export type SearchDocument = {
27
27
  export interface ProjectionContext extends Partial<Readonly<Record<ProjectionValue, string>>> {
28
28
  /** The IRI of the dataset being indexed. */
29
29
  readonly dataset?: string;
30
+ /**
31
+ * Called once for each value a typed field could not read and so left out:
32
+ * a `date` over `Z.j`, an `integer` over `onbekend`, a `period` over
33
+ * `1987/1897`, a `reference` over a literal, a `boolean` outside
34
+ * `true`/`false`/`1`/`0`. Values from a `derive` count too; one that is not
35
+ * a string is reported as `String(value)`.
36
+ */
37
+ readonly onUnreadable?: (unreadable: UnreadableValue) => void;
38
+ }
39
+ /** A value a typed field could not read ({@link ProjectionContext.onUnreadable}). */
40
+ export interface UnreadableValue {
41
+ /** The field that could not read it; dotted for a field of a nested
42
+ * referent (`creator.birthDate`). */
43
+ readonly field: string;
44
+ /** The value as the source – or the derive – stated it. */
45
+ readonly value: string;
46
+ /** The IRI of the node the document was projected from. */
47
+ readonly id: string;
30
48
  }
31
49
  /**
32
50
  * Project one framed JSON-LD node into a flat search document: apply each field
@@ -1 +1 @@
1
- {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAGzC,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAwBL,KAAK,eAAe,EAIpB,KAAK,QAAQ,EAEb,KAAK,YAAY,EACjB,KAAK,UAAU,EAGhB,MAAM,aAAa,CAAC;AAGrB;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEpD,+DAA+D;AAC/D,MAAM,MAAM,cAAc,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,aAAa,CAAC;AAE5D;;;;;;;;;;GAUG;AACH,MAAM,WAAW,iBAAkB,SAAQ,OAAO,CAChD,QAAQ,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC,CAC1C;IACC,4CAA4C;IAC5C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,UAAU,EACtB,MAAM,CAAC,EAAE,YAAY,EACrB,OAAO,GAAE,iBAAyC,GACjD,cAAc,CAgBhB;AAsLD;;;;;;;;;;;;GAYG;AACH,wBAAuB,YAAY,CACjC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,QAAQ,EACpB,OAAO,GAAE,iBAAyC,GACjD,aAAa,CAAC,cAAc,CAAC,CAc/B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,aAAa,EACvB,UAAU,EAAE,UAAU,EACtB,MAAM,CAAC,EAAE,YAAY,GACpB,SAAS,MAAM,EAAE,CAmCnB"}
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAGzC,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAwBL,KAAK,eAAe,EAIpB,KAAK,QAAQ,EAEb,KAAK,YAAY,EACjB,KAAK,UAAU,EAGhB,MAAM,aAAa,CAAC;AAGrB;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEpD,+DAA+D;AAC/D,MAAM,MAAM,cAAc,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,aAAa,CAAC;AAE5D;;;;;;;;;;GAUG;AACH,MAAM,WAAW,iBAAkB,SAAQ,OAAO,CAChD,QAAQ,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC,CAC1C;IACC,4CAA4C;IAC5C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,eAAe,KAAK,IAAI,CAAC;CAC/D;AAED,qFAAqF;AACrF,MAAM,WAAW,eAAe;IAC9B;0CACsC;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,UAAU,EACtB,MAAM,CAAC,EAAE,YAAY,EACrB,OAAO,GAAE,iBAAyC,GACjD,cAAc,CAkBhB;AAgMD;;;;;;;;;;;;GAYG;AACH,wBAAuB,YAAY,CACjC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,QAAQ,EACpB,OAAO,GAAE,iBAAyC,GACjD,aAAa,CAAC,cAAc,CAAC,CAc/B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,aAAa,EACvB,UAAU,EAAE,UAAU,EACtB,MAAM,CAAC,EAAE,YAAY,GACpB,SAAS,MAAM,EAAE,CAmCnB"}
package/dist/project.js CHANGED
@@ -2,7 +2,7 @@ import { fold } from '@lde/text-normalization';
2
2
  import { matchLanguage } from './language.js';
3
3
  import { buildSubjectIndex, frameSubjects, } from './frame-by-type.js';
4
4
  import { companionNames, assertTypeInSchema, displayFieldName, displayLangOf, documentKeyOf, endpointFieldOf, fieldNamed, identityFieldOf, inheritedFacetPolicies, inlineFramingDepth, irAlias, isAbsoluteIri, isInternalField, isInlineReference, isoToUnixSeconds, localLookupTargetsOf, physicalFields, referencedTargetsOf, referenceTypeNamed, storableUnixSeconds, storedTargetOf, TARGET_FIELD, } from './schema.js';
5
- import { periodOfAll } from './period.js';
5
+ import { periodCovering, periodOf } from './period.js';
6
6
  /** The context a projection runs with when its caller supplied none. */
7
7
  const NO_PROJECTION_CONTEXT = {};
8
8
  /**
@@ -20,12 +20,13 @@ const NO_PROJECTION_CONTEXT = {};
20
20
  * the engine collection definition and the query compiler.
21
21
  */
22
22
  export function projectDocument(node, searchType, schema, context = NO_PROJECTION_CONTEXT) {
23
- if (documentKey(node) === undefined) {
23
+ const key = documentKey(node);
24
+ if (key === undefined) {
24
25
  throw new Error(`Cannot project a “${searchType.name}” node whose @id is not an absolute IRI (got ${JSON.stringify(node['@id']) ?? 'none'}; a blank node label or a relative reference is not one): every search document needs a stable key, and an empty one would collide with other keyless nodes.`);
25
26
  }
26
27
  // The guard above is what makes this a SearchDocument: projectFields sets `id`
27
28
  // for exactly the nodes documentKey resolves.
28
- const document = projectFields(node, searchType, schema, context);
29
+ const document = projectFields(node, searchType, schema, context, unreadableSink(context, key));
29
30
  pruneInternalFields(document, searchType, schema);
30
31
  return document;
31
32
  }
@@ -93,10 +94,10 @@ function pruneInternalFields(document, searchType, schema) {
93
94
  * Pruning is deferred to a single recursive pass ({@link pruneInternalFields})
94
95
  * once the whole nested structure is projected, so a derive at any depth can
95
96
  * still read an internal field before it is removed. */
96
- function projectFields(node, searchType, schema, context, nested = false) {
97
+ function projectFields(node, searchType, schema, context, sink, nested = false) {
97
98
  const document = emptyDocument(documentIdOf(node, searchType), searchType);
98
99
  for (const field of searchType.fields) {
99
- applyField(document, node, field, searchType, schema, context, nested);
100
+ applyField(document, node, field, searchType, schema, context, sink, nested);
100
101
  }
101
102
  return document;
102
103
  }
@@ -255,7 +256,13 @@ function hasValue(document, field, searchType) {
255
256
  (displayLangOf(field, key) !== undefined &&
256
257
  fieldNamed(searchType, key) === undefined)));
257
258
  }
258
- function applyField(document, node, field, searchType, schema, context, nested) {
259
+ function applyField(document, node, field, searchType, schema, context, sink, nested) {
260
+ // A blank value states nothing, so there is nothing to correct at the source.
261
+ const unread = (value) => {
262
+ if (value.trim() !== '') {
263
+ sink(field.name, value);
264
+ }
265
+ };
259
266
  // The three value sources, mutually exclusive by declaration
260
267
  // (`validateSearchType`): a projection value, a computed value, a graph path.
261
268
  if ((field.kind === 'keyword' || field.kind === 'reference') &&
@@ -271,13 +278,13 @@ function applyField(document, node, field, searchType, schema, context, nested)
271
278
  // value that survives nothing leaves the field absent, exactly as a derive
272
279
  // returning `undefined` does.
273
280
  if (field.kind === 'date') {
274
- return applyDerivedDate(document, field, derived);
281
+ return applyDerivedDate(document, field, derived, unread);
275
282
  }
276
283
  if (field.kind === 'period') {
277
- return setPeriod(document, field, derivedStrings(derived));
284
+ return setPeriod(document, field, derivedStrings(derived, unread), unread);
278
285
  }
279
286
  const value = field.kind === 'reference' && derived !== undefined
280
- ? derivedIris(derived)
287
+ ? derivedIris(derived, unread)
281
288
  : derived;
282
289
  // NaN is not a value any kind stores, and a derive is the only route that
283
290
  // can produce one (`setNumber` guards the read route). It serializes as
@@ -286,6 +293,9 @@ function applyField(document, node, field, searchType, schema, context, nested)
286
293
  if (value !== undefined && !Number.isNaN(value)) {
287
294
  document[field.name] = value;
288
295
  }
296
+ else if (value !== undefined) {
297
+ unread(String(value));
298
+ }
289
299
  return;
290
300
  }
291
301
  if (field.path === undefined && field.name !== searchType.literalField) {
@@ -307,7 +317,7 @@ function applyField(document, node, field, searchType, schema, context, nested)
307
317
  // project nothing rather than fall through and emit the referent IRIs under
308
318
  // the field name (the wrong shape).
309
319
  if (schema !== undefined) {
310
- applyInlineReference(document, node, alias, field, schema, context, nested);
320
+ applyInlineReference(document, node, alias, field, schema, context, sink, nested);
311
321
  }
312
322
  return;
313
323
  }
@@ -318,7 +328,7 @@ function applyField(document, node, field, searchType, schema, context, nested)
318
328
  if (field.kind === 'reference' && schema !== undefined) {
319
329
  const localTargets = localLookupTargetsOf(field, schema);
320
330
  if (localTargets.length > 0) {
321
- const endpoints = applyNestedReferents(document, valuesOf(node, alias), field, localTargets, schema, context);
331
+ const endpoints = applyNestedReferents(document, valuesOf(node, alias), field, localTargets, schema, context, sink);
322
332
  // A `local` lookup stores an object, so its own id is a level deeper than
323
333
  // a filter can reach – an engine welds conditions on an entry's LEAF
324
334
  // fields only, and facets flat values. `filterable` and `facetable`
@@ -335,26 +345,41 @@ function applyField(document, node, field, searchType, schema, context, nested)
335
345
  return applyFacet(document, literalsOf(node, alias), field, schema);
336
346
  case 'reference': {
337
347
  const targetsById = new Map();
338
- const values = referenceValues(node, alias, field, schema, targetsById);
348
+ const values = referenceValues(node, alias, field, schema, targetsById, unread);
339
349
  rememberTargets(document, targetsById);
340
350
  return applyFacet(document, values, field, schema, targetsById);
341
351
  }
342
352
  case 'integer':
343
- return setNumber(document, field.name, toInteger(firstLiteralOf(node, alias)));
344
- case 'number':
345
- return setNumber(document, field.name, toNumber(firstLiteralOf(node, alias)));
353
+ case 'number': {
354
+ const literal = firstLiteralOf(node, alias);
355
+ const value = field.kind === 'integer' ? toInteger(literal) : toNumber(literal);
356
+ if (literal !== undefined && Number.isNaN(value)) {
357
+ unread(literal);
358
+ }
359
+ return setNumber(document, field.name, value);
360
+ }
346
361
  case 'date': {
347
362
  const literal = firstLiteralOf(node, alias);
348
- return setDate(document, field, literal === undefined ? undefined : isoToUnixSeconds(literal));
363
+ const stored = literal === undefined ? undefined : isoToUnixSeconds(literal);
364
+ if (literal !== undefined && stored === undefined) {
365
+ unread(literal);
366
+ }
367
+ return setDate(document, field, stored);
349
368
  }
350
369
  case 'period':
351
- return setPeriod(document, field, literalsOf(node, alias));
370
+ return setPeriod(document, field, literalsOf(node, alias), unread);
352
371
  case 'boolean': {
353
- // The xsd:boolean lexical space: true/false/1/0.
354
- const literal = firstLiteralOf(node, alias);
355
- if (literal !== undefined) {
356
- document[field.name] = literal === 'true' || literal === '1';
372
+ // The xsd:boolean lexical space: true/false/1/0. Anything else – `ja`,
373
+ // `yes` – states neither, so it is left out rather than stored as false.
374
+ // Surrounding whitespace is collapsed, as xsd:boolean’s facet says.
375
+ const literal = firstLiteralOf(node, alias)?.trim();
376
+ if (literal === undefined) {
377
+ return;
378
+ }
379
+ if (!BOOLEAN_LEXICAL_SPACE.has(literal)) {
380
+ return unread(literal);
357
381
  }
382
+ document[field.name] = literal === 'true' || literal === '1';
358
383
  return;
359
384
  }
360
385
  }
@@ -385,7 +410,16 @@ function applyField(document, node, field, searchType, schema, context, nested)
385
410
  * referent’s node IRIs therefore sees a key instead, which is why the two are
386
411
  * worth declaring together only deliberately.
387
412
  */
388
- function referenceValues(node, alias, field, schema, targetsById) {
413
+ function referenceValues(node, alias, field, schema, targetsById, unread) {
414
+ // A literal where a referent belongs states no IRI to store. A node without
415
+ // one – a blank node – carries no value a publisher could look up, so only
416
+ // literals are reported.
417
+ for (const value of valuesOf(node, alias)) {
418
+ const literal = iriString(value) === undefined ? literalString(value) : undefined;
419
+ if (literal !== undefined) {
420
+ unread(literal);
421
+ }
422
+ }
389
423
  // Guaranteed declared: `searchSchema` validates that every named target
390
424
  // resolves, and the projection only ever runs against a type of its schema.
391
425
  const targets = referencedTargetsOf(field, schema);
@@ -685,7 +719,7 @@ function applyProjectionValue(document, field, from, context, schema) {
685
719
  * the schema’s declared depth. The referent type is guaranteed declared by
686
720
  * {@link searchSchema}.
687
721
  */
688
- function applyInlineReference(document, node, alias, field, schema, context, nested) {
722
+ function applyInlineReference(document, node, alias, field, schema, context, sink, nested) {
689
723
  // Resolves for a schema that declares the referent (always so for the schema a
690
724
  // type is projected through); a type framed against a foreign schema that
691
725
  // omits it simply contributes no nesting.
@@ -695,7 +729,7 @@ function applyInlineReference(document, node, alias, field, schema, context, nes
695
729
  }
696
730
  const asEdge = edgeReader(referenceType, field.ref.identity);
697
731
  const asLiteralEntry = literalReader(referenceType);
698
- const referents = applyNestedReferents(document, valuesOf(node, alias).map((value) => asLiteralEntry(value) ?? asEdge(value)), field, [referenceType], schema, context);
732
+ const referents = applyNestedReferents(document, valuesOf(node, alias).map((value) => asLiteralEntry(value) ?? asEdge(value)), field, [referenceType], schema, context, sink);
699
733
  applyIdentityCompanion(document, referents, field, schema, nested);
700
734
  }
701
735
  /**
@@ -897,7 +931,7 @@ function identityValue(value) {
897
931
  * top-level document of that type would be – so the stored id is the one the
898
932
  * target’s collection files it under, and the lookup can find it.
899
933
  */
900
- function applyNestedReferents(document, values, field, nestedTypes, schema, context) {
934
+ function applyNestedReferents(document, values, field, nestedTypes, schema, context, sink) {
901
935
  // One node may stand for several entries: a weldable leaf is single-valued,
902
936
  // so an edge the graph gave several roles or several endpoints fans out into
903
937
  // one entry per combination BEFORE it is projected (ADR 26).
@@ -910,8 +944,18 @@ function applyNestedReferents(document, values, field, nestedTypes, schema, cont
910
944
  const nestedType = nestedTypes.length === 1
911
945
  ? nestedTypes[0]
912
946
  : targetOfReferent(value, nestedTypes);
947
+ // Every tuple of a node carries the node’s other values, so each value
948
+ // is reported once per node, not once per tuple it was copied into.
949
+ let reported;
950
+ const nestedSink = (name, value) => {
951
+ const key = JSON.stringify([name, value]);
952
+ if (!(reported ??= new Set()).has(key)) {
953
+ reported.add(key);
954
+ sink(`${field.name}.${name}`, value);
955
+ }
956
+ };
913
957
  return tuplesOf(value, nestedType, field).map((tuple) => {
914
- const projected = projectFields(tuple, nestedType, schema, context, true);
958
+ const projected = projectFields(tuple, nestedType, schema, context, nestedSink, true);
915
959
  if (nestedTypes.length > 1 && Object.keys(projected).length > 0) {
916
960
  projected[TARGET_FIELD] = nestedType.name;
917
961
  }
@@ -1080,11 +1124,18 @@ function iriString(value) {
1080
1124
  * promises (a list stays a list, a single value stays single) so `array` still
1081
1125
  * decides the stored shape. A lone non-IRI becomes `undefined`, i.e. absent –
1082
1126
  * the same outcome the graph path gives it. */
1083
- function derivedIris(value) {
1127
+ function derivedIris(value, unread) {
1128
+ const isIri = (entry) => {
1129
+ const iri = typeof entry === 'string' && isAbsoluteIri(entry);
1130
+ if (!iri && entry !== undefined) {
1131
+ unread(String(entry));
1132
+ }
1133
+ return iri;
1134
+ };
1084
1135
  if (Array.isArray(value)) {
1085
- return value.filter((entry) => typeof entry === 'string' && isAbsoluteIri(entry));
1136
+ return value.filter(isIri);
1086
1137
  }
1087
- return typeof value === 'string' && isAbsoluteIri(value) ? value : undefined;
1138
+ return isIri(value) ? value : undefined;
1088
1139
  }
1089
1140
  function toInteger(literal) {
1090
1141
  return literal === undefined ? undefined : Math.trunc(Number(literal));
@@ -1102,16 +1153,22 @@ function toNumber(literal) {
1102
1153
  * field is stored in would be refused by the engine where an unparseable
1103
1154
  * string merely leaves the field absent – so drop those too.
1104
1155
  */
1105
- function applyDerivedDate(document, field, derived) {
1156
+ function applyDerivedDate(document, field, derived, unread) {
1157
+ let stored;
1106
1158
  if (typeof derived === 'string') {
1107
- return setDate(document, field, isoToUnixSeconds(derived));
1159
+ stored = isoToUnixSeconds(derived);
1108
1160
  }
1109
- if (typeof derived === 'number') {
1161
+ else if (typeof derived === 'number') {
1110
1162
  // Truncated, as the codec truncates a string’s fractional seconds: the
1111
1163
  // field is an int64. `storableUnixSeconds` drops NaN.
1112
1164
  const seconds = storableUnixSeconds(Math.trunc(derived));
1113
- return setDate(document, field, seconds === undefined ? undefined : { seconds, precision: 'dateTime' });
1165
+ stored =
1166
+ seconds === undefined ? undefined : { seconds, precision: 'dateTime' };
1167
+ }
1168
+ if (derived !== undefined && stored === undefined) {
1169
+ unread(String(derived));
1114
1170
  }
1171
+ setDate(document, field, stored);
1115
1172
  }
1116
1173
  /** Store a `date` field’s seconds, and its precision in the companion beside
1117
1174
  * them ({@link physicalFields}), so the surface can serve the value in the
@@ -1124,35 +1181,54 @@ function setDate(document, field, stored) {
1124
1181
  document[physicalFields(field).precision] = stored.precision;
1125
1182
  }
1126
1183
  /**
1127
- * Store a `period` field: its values verbatim under the field’s own name, for
1128
- * output, and the stretch they cover between them in the bound companions
1129
- * ({@link physicalFields}). A value that states no period is still stored – it
1130
- * is what the source said – but bounds nothing.
1184
+ * Store a `period` field: the values that state a period, verbatim under the
1185
+ * field’s own name, for output, and the stretch they cover between them in the
1186
+ * bound companions ({@link physicalFields}). A value that states no period –
1187
+ * `onbekend`, `1987/1897` – is reported and left out, so every value served is
1188
+ * the EDTF its scalar promises; a deployment that wants to show such text
1189
+ * declares a `keyword` field over the same path.
1131
1190
  *
1132
- * Blank values are dropped before a single-valued field keeps its first, so
1133
- * an empty literal cannot cost the field the value behind it; and the bounds
1134
- * are taken from exactly the values stored, so what is served and what is
1135
- * filtered cannot disagree.
1191
+ * Blank and unreadable values are dropped before a single-valued field keeps
1192
+ * its first, so neither can cost the field the period behind it; and the
1193
+ * bounds are taken from exactly the values stored, so what is served and what
1194
+ * is filtered cannot disagree.
1136
1195
  */
1137
- function setPeriod(document, field, values) {
1138
- const nonBlank = dedupe(values.map((value) => value.trim())).filter((value) => value !== '');
1139
- const stated = field.array === true ? nonBlank : nonBlank.slice(0, 1);
1196
+ function setPeriod(document, field, values, unread) {
1197
+ const readable = dedupe(values.map((value) => value.trim())).flatMap((value) => {
1198
+ if (value === '') {
1199
+ return [];
1200
+ }
1201
+ const period = periodOf(value);
1202
+ if (period === undefined) {
1203
+ unread(value);
1204
+ return [];
1205
+ }
1206
+ return [{ value, period }];
1207
+ });
1208
+ const stated = field.array === true ? readable : readable.slice(0, 1);
1140
1209
  if (stated.length === 0) {
1141
1210
  return;
1142
1211
  }
1143
- document[field.name] = field.array === true ? stated : stated[0];
1212
+ document[field.name] =
1213
+ field.array === true ? stated.map(({ value }) => value) : stated[0].value;
1144
1214
  const companions = physicalFields(field).period;
1145
- const covered = periodOfAll(stated);
1146
- if (companions !== undefined && covered !== undefined) {
1215
+ if (companions !== undefined) {
1216
+ const [first, ...rest] = stated.map(({ period }) => period);
1217
+ const covered = periodCovering(first, ...rest);
1147
1218
  document[companions.start] = covered.start;
1148
1219
  document[companions.end] = covered.end;
1149
1220
  }
1150
1221
  }
1151
1222
  /** A derive’s value as the strings a `period` stores: one string, or the
1152
1223
  * strings in a list; anything else states no value. */
1153
- function derivedStrings(derived) {
1224
+ function derivedStrings(derived, unread) {
1154
1225
  const values = Array.isArray(derived) ? derived : [derived];
1155
- return values.filter((value) => typeof value === 'string');
1226
+ return values.filter((value) => {
1227
+ if (typeof value !== 'string' && value !== undefined) {
1228
+ unread(String(value));
1229
+ }
1230
+ return typeof value === 'string';
1231
+ });
1156
1232
  }
1157
1233
  function setNumber(document, field, value) {
1158
1234
  if (value !== undefined && !Number.isNaN(value)) {
@@ -1187,3 +1263,22 @@ function setArray(document, field, values) {
1187
1263
  function isObject(value) {
1188
1264
  return typeof value === 'object' && value !== null && !Array.isArray(value);
1189
1265
  }
1266
+ /** The sink a root document’s fields report to: the context’s
1267
+ * {@link ProjectionContext.onUnreadable}, told which node the value was
1268
+ * dropped from. */
1269
+ function unreadableSink(context, id) {
1270
+ const { onUnreadable } = context;
1271
+ return onUnreadable === undefined
1272
+ ? ignoreUnreadable
1273
+ : (field, value) => onUnreadable({ field, value, id });
1274
+ }
1275
+ function ignoreUnreadable() {
1276
+ // Nobody asked for the report.
1277
+ }
1278
+ /** The xsd:boolean lexical space. */
1279
+ const BOOLEAN_LEXICAL_SPACE = new Set([
1280
+ 'true',
1281
+ 'false',
1282
+ '1',
1283
+ '0',
1284
+ ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/search",
3
- "version": "0.37.0",
3
+ "version": "0.38.0",
4
4
  "description": "Engine- and domain-agnostic core for RDF-backed search: a unified declarative field model (SearchField/SearchType/SearchSchema), a neutral query IR, the SearchEngine port with logical result types, and a streaming CONSTRUCT-to-document projection. Bakes in no engine, protocol, or domain.",
5
5
  "repository": {
6
6
  "url": "git+https://github.com/ldelements/lde.git",