@lde/search 0.10.0 → 0.11.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/README.md CHANGED
@@ -71,11 +71,22 @@ Exports are stratified by audience:
71
71
  The model has three levels, with analogues in SHACL ([one possible source](#why-a-declarative-model))
72
72
  and GraphQL (one of the surfaces):
73
73
 
74
- | Term | What it is | SHACL | GraphQL |
75
- | -------------- | ---------------------------------------------------------------------------------------------------------------- | -------------- | ----------- |
76
- | `SearchField` | One queryable field: a `kind`, the IR `path` it projects from, and the capability flags it opts into | property shape | field |
77
- | `SearchType` | One root type’s complete declaration: its logical API `name`, its `class` IRI and its fields (incl. derived) | NodeShape | object type |
78
- | `SearchSchema` | The whole search declaration: every `SearchType`, keyed by `class` IRI – build one with `searchSchema(...types)` | shapes graph | schema |
74
+ | Term | What it is | SHACL | GraphQL |
75
+ | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ----------- |
76
+ | `SearchField` | One queryable field: a `kind`, the IR `path` it projects from, and the capability flags it opts into | property shape | field |
77
+ | `SearchType` | One type’s complete declaration: its logical API `name` and fields (incl. derived). A **Root Type** declares a `class` (indexed, keys the schema); a **Reference Type** declares none (reached only through an inline reference) | NodeShape | object type |
78
+ | `SearchSchema` | The whole search declaration: every Root Type, keyed by `class` IRI, plus the Reference Types – build one with `searchSchema(...types)` | shapes graph | schema |
79
+
80
+ A `SearchType` is a **Root Type** or a **Reference Type**, told apart by one
81
+ absence: a Root Type declares a `class`, so roots are selected for it and a
82
+ writer opens a collection for it; a Reference Type declares none, so it is
83
+ never selected, framed by type or indexed – its identity is its name, and its
84
+ type comes from the edge that points at it. `searchSchema` partitions its
85
+ arguments accordingly: Root Types key the class map (`schema.values()` yields
86
+ only them, so no writer ever opens a collection for a Reference Type), Reference
87
+ Types go into a name index an inline `ref.typeName` resolves against. The
88
+ absence is load-bearing, and enforced at the type level – an indexed Reference
89
+ Type fails to compile ([ADR 11](../../docs/decisions/0011-decouple-rdf-depth-from-the-api-surface.md)).
79
90
 
80
91
  `projectRoots` and the engine port each execute one `SearchType` at a time –
81
92
  projection over the roots the pipeline selector supplied for that type; the
@@ -208,10 +219,29 @@ directly.
208
219
  | `date` | `range` (inclusive) | yes | yes | ISO 8601 string (surface) |
209
220
  | `boolean` | `is` | yes | – | boolean (absent = false) |
210
221
 
211
- A `reference` carries `labelOnly` today (id + display label); the `idOnly` and
212
- `inline` strategies are forward declarations. References are object-shaped from
213
- day one so that `inline` can later **add** fields to a reference type without
214
- breaking clients.
222
+ A `reference` carries one of two strategies today: `labelOnly` (id + display
223
+ label, resolved at query time from a label source) and `inline` (the referent’s
224
+ own projected fields, carried inline). `idOnly` stays a forward declaration.
225
+
226
+ An **inline reference** resolves `ref.typeName` to a declared **Reference Type**
227
+ and projects the referent through it – a nested `SearchDocument`, or an array
228
+ for an `array` reference. Roles decide whether the nesting surfaces, so the same
229
+ construct serves two jobs (see
230
+ [ADR 11](../../docs/decisions/0011-decouple-rdf-depth-from-the-api-surface.md)):
231
+
232
+ - a **reading device** declares no role, so it is an internal field: projected
233
+ so a later `derive` can select and flatten a value a `path` cannot address (a
234
+ qualified hop), then pruned before the writer – nothing nested reaches the
235
+ engine or the API;
236
+ - an **API device** declares `output`, deliberately surfacing the nested
237
+ Reference Type.
238
+
239
+ So RDF depth and API shape stay independent: inline as deep as the source
240
+ demands, expose exactly the flat fields you want. Framing follows the inline
241
+ reference graph to the depth the schema declares (`Dataset → Subset →
242
+ Measurement` is two hops), and `searchSchema` rejects inline cycles – the one
243
+ way that depth could be unbounded – so it stays a bounded property of the
244
+ declaration.
215
245
 
216
246
  A reference resolves its label from a **label source**: `labelSource` names
217
247
  the `SearchType` whose collection holds the referenced entities. The named
package/dist/adapter.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * deployment author declaring a schema and running searches needs none of
7
7
  * this; the main entry point carries that (small) authoring surface.
8
8
  */
9
- export { assertTypeInSchema, physicalFields, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, isInternalField, referenceFields, fieldNamed, labelFieldOf, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
9
+ export { assertTypeInSchema, physicalFields, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, isInternalField, isInlineReference, referenceFields, referenceTypeNamed, inlineFramingDepth, fieldNamed, labelFieldOf, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
10
10
  export type { PhysicalFields } from './schema.js';
11
11
  export { physicalNameTokens } from './physical-name.js';
12
12
  export { filterOperatorFor, filterOperator, validateQuery, assertValidQuery, pageForOffset, } from './query.js';
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,eAAe,EACf,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
package/dist/adapter.js CHANGED
@@ -6,6 +6,6 @@
6
6
  * deployment author declaring a schema and running searches needs none of
7
7
  * this; the main entry point carries that (small) authoring surface.
8
8
  */
9
- export { assertTypeInSchema, physicalFields, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, isInternalField, referenceFields, fieldNamed, labelFieldOf, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
9
+ export { assertTypeInSchema, physicalFields, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, isInternalField, isInlineReference, referenceFields, referenceTypeNamed, inlineFramingDepth, fieldNamed, labelFieldOf, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
10
10
  export { physicalNameTokens } from './physical-name.js';
11
11
  export { filterOperatorFor, filterOperator, validateQuery, assertValidQuery, pageForOffset, } from './query.js';
package/dist/engine.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { SearchQuery } from './query.js';
2
- import type { SearchSchema, SearchType } from './schema.js';
2
+ import type { RootType, RootTypeOf, SearchSchema, SearchType } from './schema.js';
3
3
  /**
4
4
  * The engine port: the boundary a concrete engine adapter (e.g. the engine
5
5
  * `@lde/search-typesense`’s `createTypesenseSearchEngine` returns) implements.
@@ -34,7 +34,7 @@ export interface SearchEngine<Types extends readonly SearchType[] = readonly Sea
34
34
  /** The declaration this engine serves – exposed so a surface can route and
35
35
  * a caller can enumerate the searchable types. */
36
36
  readonly schema: SearchSchema<Types>;
37
- search<T extends Types[number]>(searchType: T, query: SearchQuery): Promise<SearchResult<FacetFieldsOf<T>, OutputFieldsOf<T>>>;
37
+ search<T extends RootTypeOf<Types>>(searchType: T, query: SearchQuery): Promise<SearchResult<FacetFieldsOf<T>, OutputFieldsOf<T>>>;
38
38
  /**
39
39
  * The batch entry point: answer several facet-only queries against one type
40
40
  * in a single engine round-trip (where the engine supports one, e.g.
@@ -52,7 +52,7 @@ export interface SearchEngine<Types extends readonly SearchType[] = readonly Sea
52
52
  * transport itself). The port contract holds for every query in the batch;
53
53
  * an empty `queries` resolves to `[]` without touching the engine.
54
54
  */
55
- searchFacets<T extends Types[number]>(searchType: T, queries: readonly SearchQuery[]): Promise<readonly FacetsOutcome<FacetFieldsOf<T>>[]>;
55
+ searchFacets<T extends RootTypeOf<Types>>(searchType: T, queries: readonly SearchQuery[]): Promise<readonly FacetsOutcome<FacetFieldsOf<T>>[]>;
56
56
  }
57
57
  /**
58
58
  * One query’s outcome in a {@link SearchEngine.searchFacets} batch – its
@@ -84,7 +84,7 @@ export type FacetMap<FacetField extends string = string> = Readonly<Partial<Reco
84
84
  * `: SearchType` annotation degrades to `string` (its field names are not
85
85
  * statically known).
86
86
  */
87
- export type FacetFieldsOf<Type extends SearchType> = SearchType extends Type ? string : Extract<Type['fields'][number], {
87
+ export type FacetFieldsOf<Type extends SearchType> = RootType extends Type ? string : Extract<Type['fields'][number], {
88
88
  readonly facetable: true;
89
89
  }>['name'];
90
90
  /**
@@ -92,7 +92,7 @@ export type FacetFieldsOf<Type extends SearchType> = SearchType extends Type ? s
92
92
  * can hold. Like {@link FacetFieldsOf}, degrades to `string` for a widened
93
93
  * declaration.
94
94
  */
95
- export type OutputFieldsOf<Type extends SearchType> = SearchType extends Type ? string : Extract<Type['fields'][number], {
95
+ export type OutputFieldsOf<Type extends SearchType> = RootType extends Type ? string : Extract<Type['fields'][number], {
96
96
  readonly output: true;
97
97
  }>['name'];
98
98
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,WAAW,YAAY,CAC3B,KAAK,SAAS,SAAS,UAAU,EAAE,GAAG,SAAS,UAAU,EAAE;IAE3D;uDACmD;IACnD,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC;IACrC,MAAM,CAAC,CAAC,SAAS,KAAK,CAAC,MAAM,CAAC,EAC5B,UAAU,EAAE,CAAC,EACb,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9D;;;;;;;;;;;;;;;;OAgBG;IACH,YAAY,CAAC,CAAC,SAAS,KAAK,CAAC,MAAM,CAAC,EAClC,UAAU,EAAE,CAAC,EACb,OAAO,EAAE,SAAS,WAAW,EAAE,GAC9B,OAAO,CAAC,SAAS,aAAa,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;CACxD;AAED;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,CAAC,UAAU,SAAS,MAAM,GAAG,MAAM,IACxD;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAA;CAAE,GACzC;IAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAEhC,+EAA+E;AAC/E,MAAM,WAAW,YAAY,CAC3B,UAAU,SAAS,MAAM,GAAG,MAAM,EAClC,WAAW,SAAS,MAAM,GAAG,MAAM;IAEnC,QAAQ,CAAC,IAAI,EAAE,SAAS,SAAS,CAAC,WAAW,CAAC,EAAE,CAAC;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;CACvC;AAED;;;GAGG;AACH,MAAM,MAAM,QAAQ,CAAC,UAAU,SAAS,MAAM,GAAG,MAAM,IAAI,QAAQ,CACjE,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,SAAS,WAAW,EAAE,CAAC,CAAC,CACpD,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,CAAC,IAAI,SAAS,UAAU,IAAI,UAAU,SAAS,IAAI,GACxE,MAAM,GACN,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,EAAE;IAAE,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAA;CAAE,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1E;;;;GAIG;AACH,MAAM,MAAM,cAAc,CAAC,IAAI,SAAS,UAAU,IAAI,UAAU,SAAS,IAAI,GACzE,MAAM,GACN,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,EAAE;IAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAA;CAAE,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvE;;;;;;GAMG;AACH,MAAM,WAAW,SAAS,CAAC,WAAW,SAAS,MAAM,GAAG,MAAM;IAC5D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC,WAAW,CAAC,CAAC;CAChD;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,CAAC,WAAW,SAAS,MAAM,GAAG,MAAM,IAAI,QAAQ,CACxE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAC1C,CAAC;AAEF,6BAA6B;AAC7B,MAAM,MAAM,WAAW,GACnB,MAAM,GACN,MAAM,GACN,OAAO,GACP,SAAS,MAAM,EAAE,GACjB,cAAc,GACd,SAAS,GACT,SAAS,SAAS,EAAE,CAAC;AAEzB;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;AAEzE;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,CAAC,EAAE,cAAc,CAAC;CACjC;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,cAAc,CAAC;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
@@ -27,15 +27,21 @@ export interface SubjectIndex {
27
27
  export declare function buildSubjectIndex(quads: Iterable<Quad>): SubjectIndex;
28
28
  /**
29
29
  * Frame each of the given `roots` from a prebuilt {@link SubjectIndex} into one
30
- * JSON-LD node. Each root subject’s own triples plus the one-hop nodes it
31
- * references (e.g. nested publisher/distribution resources) are framed one
32
- * subject at a time, so beyond the shared subject index only a single subgraph
33
- * is held (whole-graph `jsonld.frame()` is ~O(N²)). The frame carries no
34
- * `@context`, so framed keys are full predicate IRIs.
30
+ * JSON-LD node. Each root subject’s own triples plus the nodes it references –
31
+ * to `depth` hops (e.g. nested publisher/distribution resources at one hop, an
32
+ * inline reference’s referents deeper) – are framed one subject at a time, so
33
+ * beyond the shared subject index only a single subgraph is held (whole-graph
34
+ * `jsonld.frame()` is ~O(N²)). The frame carries no `@context`, so framed keys
35
+ * are full predicate IRIs.
36
+ *
37
+ * `depth` is the number of reference hops to embed; it defaults to one (the
38
+ * long-standing single-hop embed) and comes from the schema for a type with
39
+ * inline references (`inlineFramingDepth`). Bounded per batch, so the O(N²)
40
+ * framing cost applies to a batch of roots, not the graph (ADR 12).
35
41
  *
36
42
  * The roots are supplied explicitly rather than discovered from `rdf:type`: the
37
43
  * caller ({@link projectRoots}, from the pipeline selector) already holds them
38
44
  * and passes them directly. A root absent from the index simply frames nothing.
39
45
  */
40
- export declare function frameSubjects(index: SubjectIndex, roots: readonly string[]): AsyncIterable<FramedNode>;
46
+ export declare function frameSubjects(index: SubjectIndex, roots: readonly string[], depth?: number): AsyncIterable<FramedNode>;
41
47
  //# sourceMappingURL=frame-by-type.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"frame-by-type.d.ts","sourceRoot":"","sources":["../src/frame-by-type.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAGzC,4EAA4E;AAC5E,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAIjD;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,IAAI,EAAE,CAAC,CAAC;CAC1D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,YAAY,CAkBrE;AAED;;;;;;;;;;;GAWG;AACH,wBAAuB,aAAa,CAClC,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,aAAa,CAAC,UAAU,CAAC,CA6B3B"}
1
+ {"version":3,"file":"frame-by-type.d.ts","sourceRoot":"","sources":["../src/frame-by-type.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAGzC,4EAA4E;AAC5E,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAIjD;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,IAAI,EAAE,CAAC,CAAC;CAC1D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,YAAY,CAkBrE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAuB,aAAa,CAClC,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,KAAK,SAAI,GACR,aAAa,CAAC,UAAU,CAAC,CAqB3B"}
@@ -34,25 +34,26 @@ export function buildSubjectIndex(quads) {
34
34
  }
35
35
  /**
36
36
  * Frame each of the given `roots` from a prebuilt {@link SubjectIndex} into one
37
- * JSON-LD node. Each root subject’s own triples plus the one-hop nodes it
38
- * references (e.g. nested publisher/distribution resources) are framed one
39
- * subject at a time, so beyond the shared subject index only a single subgraph
40
- * is held (whole-graph `jsonld.frame()` is ~O(N²)). The frame carries no
41
- * `@context`, so framed keys are full predicate IRIs.
37
+ * JSON-LD node. Each root subject’s own triples plus the nodes it references –
38
+ * to `depth` hops (e.g. nested publisher/distribution resources at one hop, an
39
+ * inline reference’s referents deeper) – are framed one subject at a time, so
40
+ * beyond the shared subject index only a single subgraph is held (whole-graph
41
+ * `jsonld.frame()` is ~O(N²)). The frame carries no `@context`, so framed keys
42
+ * are full predicate IRIs.
43
+ *
44
+ * `depth` is the number of reference hops to embed; it defaults to one (the
45
+ * long-standing single-hop embed) and comes from the schema for a type with
46
+ * inline references (`inlineFramingDepth`). Bounded per batch, so the O(N²)
47
+ * framing cost applies to a batch of roots, not the graph (ADR 12).
42
48
  *
43
49
  * The roots are supplied explicitly rather than discovered from `rdf:type`: the
44
50
  * caller ({@link projectRoots}, from the pipeline selector) already holds them
45
51
  * and passes them directly. A root absent from the index simply frames nothing.
46
52
  */
47
- export async function* frameSubjects(index, roots) {
53
+ export async function* frameSubjects(index, roots, depth = 1) {
48
54
  const { bySubject } = index;
49
55
  for (const rootIri of roots) {
50
- const owned = bySubject.get(rootIri) ?? [];
51
- const referenced = owned
52
- .filter((quad) => quad.object.termType === 'NamedNode' ||
53
- quad.object.termType === 'BlankNode')
54
- .flatMap((quad) => bySubject.get(quad.object.value) ?? []);
55
- const subgraph = [...owned, ...referenced];
56
+ const subgraph = collectSubgraph(bySubject, rootIri, depth);
56
57
  const expanded = await jsonld.fromRDF(subgraph);
57
58
  // Frame for THIS specific root subject by its `@id`. The subgraph embeds the
58
59
  // root’s one-hop references, and such a referent can itself be one of the
@@ -67,3 +68,36 @@ export async function* frameSubjects(index, roots) {
67
68
  }
68
69
  }
69
70
  }
71
+ /**
72
+ * Collect a root subject’s triples plus those of every subject it references
73
+ * within `depth` hops, breadth-first. Each subject is visited once (so a
74
+ * reference cycle in the data terminates and no subject’s triples are
75
+ * duplicated); `jsonld.frame` embeds whatever the subgraph reaches. Bounded by
76
+ * `depth`, so only the reachable subgraph of one root is materialized at a time.
77
+ */
78
+ function collectSubgraph(bySubject, rootIri, depth) {
79
+ const collected = [];
80
+ const visited = new Set();
81
+ let frontier = [rootIri];
82
+ for (let hop = 0; hop <= depth; hop++) {
83
+ const next = [];
84
+ for (const subject of frontier) {
85
+ if (visited.has(subject)) {
86
+ continue;
87
+ }
88
+ visited.add(subject);
89
+ const owned = bySubject.get(subject) ?? [];
90
+ collected.push(...owned);
91
+ if (hop < depth) {
92
+ for (const quad of owned) {
93
+ if (quad.object.termType === 'NamedNode' ||
94
+ quad.object.termType === 'BlankNode') {
95
+ next.push(quad.object.value);
96
+ }
97
+ }
98
+ }
99
+ }
100
+ frontier = next;
101
+ }
102
+ return collected;
103
+ }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { projectRoots } from './project.js';
2
2
  export type { SearchDocument } from './project.js';
3
3
  export { defineSearchType, searchSchema, validateSearchType, assertValidSearchType, } from './schema.js';
4
- export type { FieldKind, SearchField, SearchFieldBase, TextField, KeywordField, ReferenceField, NumericField, BooleanField, SearchType, SearchTypeIssue, SearchSchema, FacetRange, } from './schema.js';
4
+ export type { FieldKind, SearchField, SearchFieldBase, TextField, KeywordField, ReferenceField, NumericField, BooleanField, SearchType, SearchTypeBase, RootType, ReferenceType, RootTypeOf, SearchTypeIssue, SearchSchema, FacetRange, } from './schema.js';
5
5
  export type { SearchQuery, Filter, Sort } from './query.js';
6
6
  export type { SearchEngine, SearchResult, SearchHit, ResultDocument, SearchValue, LocalizedValue, Reference, FacetBucket, FacetMap, FacetsOutcome, FacetFieldsOf, OutputFieldsOf, } from './engine.js';
7
7
  export type { FramedNode } from './frame-by-type.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAKnD,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,SAAS,EACT,WAAW,EACX,eAAe,EACf,SAAS,EACT,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,eAAe,EACf,YAAY,EACZ,UAAU,GACX,MAAM,aAAa,CAAC;AAIrB,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAI5D,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,cAAc,EACd,WAAW,EACX,cAAc,EACd,SAAS,EACT,WAAW,EACX,QAAQ,EACR,aAAa,EACb,aAAa,EACb,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAKnD,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,SAAS,EACT,WAAW,EACX,eAAe,EACf,SAAS,EACT,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,cAAc,EACd,QAAQ,EACR,aAAa,EACb,UAAU,EACV,eAAe,EACf,YAAY,EACZ,UAAU,GACX,MAAM,aAAa,CAAC;AAIrB,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAI5D,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,cAAc,EACd,WAAW,EACX,cAAc,EACd,SAAS,EACT,WAAW,EACX,QAAQ,EACR,aAAa,EACb,aAAa,EACb,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/project.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Quad } from '@rdfjs/types';
2
2
  import { type FramedNode } from './frame-by-type.js';
3
- import { type SearchSchema, type SearchType } from './schema.js';
3
+ import { type RootType, type SearchSchema, type SearchType } from './schema.js';
4
4
  /** A flat search document. `id` is the engine document key. */
5
5
  export type SearchDocument = {
6
6
  id: string;
@@ -13,11 +13,13 @@ export type SearchDocument = {
13
13
  * statement of what the projection reads. {@link isInternalField Internal
14
14
  * fields} (those declaring no role) are populated so a later derive can read
15
15
  * them, then pruned before the document is returned: they must reach neither a
16
- * writer nor the collection definition. The physical field names a field fans
17
- * out to come from {@link physicalFields}, the single source shared with the
18
- * engine collection definition and the query compiler.
16
+ * writer nor the collection definition. Pruning ({@link pruneInternalFields})
17
+ * recurses into the referents of surfaced inline references, so that invariant
18
+ * holds at every depth, not just the root. The physical field names a field
19
+ * fans out to come from {@link physicalFields}, the single source shared with
20
+ * the engine collection definition and the query compiler.
19
21
  */
20
- export declare function projectDocument(node: FramedNode, searchType: SearchType): SearchDocument;
22
+ export declare function projectDocument(node: FramedNode, searchType: SearchType, schema?: SearchSchema): SearchDocument;
21
23
  /**
22
24
  * Project a single type over a known set of `roots` – the per-type, roots-given
23
25
  * projection. The roots are supplied by the caller (the pipeline selector)
@@ -31,5 +33,5 @@ export declare function projectDocument(node: FramedNode, searchType: SearchType
31
33
  * Consumes `quads` once, so it accepts any `Iterable` – a batch’s materialized
32
34
  * array or a chained generator merging several readers.
33
35
  */
34
- export declare function projectRoots(quads: Iterable<Quad>, roots: readonly string[], schema: SearchSchema, searchType: SearchType): AsyncIterable<SearchDocument>;
36
+ export declare function projectRoots(quads: Iterable<Quad>, roots: readonly string[], schema: SearchSchema, searchType: RootType): AsyncIterable<SearchDocument>;
35
37
  //# sourceMappingURL=project.d.ts.map
@@ -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;AAEzC,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EASL,KAAK,YAAY,EACjB,KAAK,UAAU,EAEhB,MAAM,aAAa,CAAC;AAErB,+DAA+D;AAC/D,MAAM,MAAM,cAAc,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEtE;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,UAAU,GACrB,cAAc,CAoBhB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAuB,YAAY,CACjC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,UAAU,GACrB,aAAa,CAAC,cAAc,CAAC,CAU/B"}
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAWL,KAAK,QAAQ,EAEb,KAAK,YAAY,EACjB,KAAK,UAAU,EAEhB,MAAM,aAAa,CAAC;AAErB,+DAA+D;AAC/D,MAAM,MAAM,cAAc,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEtE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,UAAU,EACtB,MAAM,CAAC,EAAE,YAAY,GACpB,cAAc,CAIhB;AAiED;;;;;;;;;;;;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,GACnB,aAAa,CAAC,cAAc,CAAC,CAW/B"}
package/dist/project.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { fold } from '@lde/text-normalization';
2
2
  import { buildSubjectIndex, frameSubjects, } from './frame-by-type.js';
3
- import { assertTypeInSchema, displayFieldName, isInternalField, isoToUnixSeconds, physicalFields, } from './schema.js';
3
+ import { assertTypeInSchema, displayFieldName, inlineFramingDepth, isInternalField, isInlineReference, isoToUnixSeconds, physicalFields, referenceTypeNamed, } from './schema.js';
4
4
  /**
5
5
  * Project one framed JSON-LD node into a flat search document: apply each field
6
6
  * of the type in declaration order. A field with a `derive` function computes
@@ -9,26 +9,65 @@ import { assertTypeInSchema, displayFieldName, isInternalField, isoToUnixSeconds
9
9
  * statement of what the projection reads. {@link isInternalField Internal
10
10
  * fields} (those declaring no role) are populated so a later derive can read
11
11
  * them, then pruned before the document is returned: they must reach neither a
12
- * writer nor the collection definition. The physical field names a field fans
13
- * out to come from {@link physicalFields}, the single source shared with the
14
- * engine collection definition and the query compiler.
12
+ * writer nor the collection definition. Pruning ({@link pruneInternalFields})
13
+ * recurses into the referents of surfaced inline references, so that invariant
14
+ * holds at every depth, not just the root. The physical field names a field
15
+ * fans out to come from {@link physicalFields}, the single source shared with
16
+ * the engine collection definition and the query compiler.
15
17
  */
16
- export function projectDocument(node, searchType) {
18
+ export function projectDocument(node, searchType, schema) {
19
+ const document = projectFields(node, searchType, schema);
20
+ pruneInternalFields(document, searchType, schema);
21
+ return document;
22
+ }
23
+ /**
24
+ * Prune every internal field from a fully projected document, in place. Runs
25
+ * only after all projection – so every derive that might read an internal
26
+ * field, at any depth, has already run – which makes this single post-order
27
+ * pass safe. A no-role inline reference is itself internal, so it is deleted
28
+ * whole here; a surfaced (`output`) inline reference survives, but its own
29
+ * internal helper fields are pruned from the nested document. That keeps the
30
+ * *a field without a role reaches neither the engine nor the API* invariant
31
+ * true at every depth of the reference graph, not just at the root.
32
+ */
33
+ function pruneInternalFields(document, searchType, schema) {
34
+ for (const field of searchType.fields) {
35
+ if (isInternalField(field)) {
36
+ delete document[field.name];
37
+ continue;
38
+ }
39
+ // A surfaced inline reference nests its referent(s) as SearchDocument(s);
40
+ // prune those too, by their reference type.
41
+ if (schema === undefined || field.kind !== 'reference') {
42
+ continue;
43
+ }
44
+ const ref = field.ref;
45
+ if (ref?.strategy !== 'inline') {
46
+ continue;
47
+ }
48
+ const referenceType = referenceTypeNamed(schema, ref.typeName);
49
+ const nested = document[field.name];
50
+ if (referenceType === undefined || nested === undefined) {
51
+ continue;
52
+ }
53
+ for (const referent of Array.isArray(nested) ? nested : [nested]) {
54
+ pruneInternalFields(referent, referenceType, schema);
55
+ }
56
+ }
57
+ }
58
+ /** Apply every field of `searchType` to a fresh document, without pruning – the
59
+ * shared core of {@link projectDocument} and inline-referent projection.
60
+ * Pruning is deferred to a single recursive pass ({@link pruneInternalFields})
61
+ * once the whole nested structure is projected, so a derive at any depth can
62
+ * still read an internal field before it is removed. */
63
+ function projectFields(node, searchType, schema) {
17
64
  const id = node['@id'];
18
65
  if (typeof id !== 'string') {
19
- throw new Error(`Cannot project a ${searchType.class} node without an @id: every search document needs a stable key, and an empty one would collide with other keyless nodes.`);
66
+ throw new Error(`Cannot project a “${searchType.name}” node without an @id: every search document needs a stable key, and an empty one would collide with other keyless nodes.`);
20
67
  }
21
68
  const document = { id };
22
69
  for (const field of searchType.fields) {
23
- applyField(document, node, field);
24
- }
25
- // Prune internal fields: they were projected only so the derives above could
26
- // read them (a structural memo shared across every derive that needs the
27
- // value), and must not reach the writer or the collection definition.
28
- for (const field of searchType.fields) {
29
- if (isInternalField(field)) {
30
- delete document[field.name];
31
- }
70
+ applyField(document, node, field, schema);
32
71
  }
33
72
  return document;
34
73
  }
@@ -52,11 +91,12 @@ export async function* projectRoots(quads, roots, schema, searchType) {
52
91
  // non-`DISTINCT` `SELECT` over a one-to-many join yields the same subject per
53
92
  // matched row – and a repeated root would otherwise frame and emit a
54
93
  // duplicate document under the same `id`.
55
- for await (const node of frameSubjects(index, [...new Set(roots)])) {
56
- yield projectDocument(node, searchType);
94
+ const depth = inlineFramingDepth(schema, searchType);
95
+ for await (const node of frameSubjects(index, [...new Set(roots)], depth)) {
96
+ yield projectDocument(node, searchType, schema);
57
97
  }
58
98
  }
59
- function applyField(document, node, field) {
99
+ function applyField(document, node, field, schema) {
60
100
  if (field.derive !== undefined) {
61
101
  const value = field.derive(document);
62
102
  if (value !== undefined) {
@@ -69,6 +109,16 @@ function applyField(document, node, field) {
69
109
  // Neither path nor derive: populated outside the projection, if at all.
70
110
  return;
71
111
  }
112
+ if (isInlineReference(field)) {
113
+ // An inline reference is a nested structure, not a bare IRI: it can only be
114
+ // projected with the schema that declares its reference type. Without one,
115
+ // project nothing rather than fall through and emit the referent IRIs under
116
+ // the field name (the wrong shape).
117
+ if (schema !== undefined) {
118
+ applyInlineReference(document, node, path, field, schema);
119
+ }
120
+ return;
121
+ }
72
122
  switch (field.kind) {
73
123
  case 'text':
74
124
  return applyText(document, langValuesOf(node, path), field);
@@ -153,6 +203,36 @@ function applyFacet(document, raw, field) {
153
203
  setArray(document, physicalFields(field).search[0], dedupe(values.map((value) => fold(value))));
154
204
  }
155
205
  }
206
+ /**
207
+ * Project an inline reference: the referent node(s) embedded under `path` are
208
+ * each projected through the reference’s {@link ReferenceType} (its fields’
209
+ * paths relative to the referent node) and attached under the field’s name – a
210
+ * nested {@link SearchDocument} for a single reference, an array for an
211
+ * `array` one. The referent is projected in full – internal fields included –
212
+ * so the declaring type’s (or the reference type’s own) derives can read them;
213
+ * {@link pruneInternalFields} then removes the internal fields from a *surfaced*
214
+ * referent and deletes an internal inline reference whole. Recurses through
215
+ * `schema`, so an inline reference may itself carry further inline references to
216
+ * the schema’s declared depth. The referent type is guaranteed declared by
217
+ * {@link searchSchema}.
218
+ */
219
+ function applyInlineReference(document, node, path, field, schema) {
220
+ // Resolves for a schema that declares the referent (always so for the schema a
221
+ // type is projected through); a type framed against a foreign schema that
222
+ // omits it simply contributes no nesting.
223
+ const referenceType = referenceTypeNamed(schema, field.ref.typeName);
224
+ if (referenceType === undefined) {
225
+ return;
226
+ }
227
+ const referents = valuesOf(node, path)
228
+ .filter(isObject)
229
+ .filter((referent) => typeof referent['@id'] === 'string')
230
+ .map((referent) => projectFields(referent, referenceType, schema));
231
+ if (referents.length === 0) {
232
+ return;
233
+ }
234
+ document[field.name] = field.array === true ? referents : referents[0];
235
+ }
156
236
  function langValuesOf(node, path) {
157
237
  return valuesOf(node, path)
158
238
  .map(toLangValue)
package/dist/schema.d.ts CHANGED
@@ -194,26 +194,60 @@ export interface FacetRange {
194
194
  readonly min?: number;
195
195
  readonly max?: number;
196
196
  }
197
- /**
198
- * One root type’s complete search declaration: its logical API `name`, the
199
- * `class` IRI its documents are instances of (the RDF class), and the queryable
200
- * `fields` (including {@link SearchField.derive derived} ones). A SHACL
201
- * generator can emit one per NodeShape (`name`←`sh:name`/local name,
202
- * `class`←`sh:targetClass`, `fields`←its property shapes), but that is a source,
203
- * not a requirement.
204
- */
205
- export interface SearchType {
197
+ /** The declaration members every {@link SearchType} shares. */
198
+ export interface SearchTypeBase {
206
199
  /** Logical API name (PascalCase, e.g. `Dataset`) – names the type in the API
207
200
  * surfaces (GraphQL type names, a REST path), the way each field’s
208
201
  * {@link SearchField.name} names that field. Deliberately declared rather
209
202
  * than derived from the `class` IRI, so re-modelling the vocabulary cannot
210
203
  * silently rename the public contract. */
211
204
  readonly name: string;
205
+ readonly fields: readonly SearchField[];
206
+ }
207
+ /**
208
+ * A **Root Type**: a {@link SearchType} that is indexed. It declares a `class`,
209
+ * roots are selected for it, a Writer owns a collection for it, and the
210
+ * {@link SearchSchema} is keyed by it. A SHACL generator can emit one per
211
+ * NodeShape (`name`←`sh:name`/local name, `class`←`sh:targetClass`,
212
+ * `fields`←its property shapes), but that is a source, not a requirement.
213
+ */
214
+ export interface RootType extends SearchTypeBase {
212
215
  /** The RDF class IRI its documents are instances of (`sh:targetClass`); the
213
- * key a {@link SearchSchema} maps this type under. */
216
+ * key a {@link SearchSchema} maps this type under. Its presence is what makes
217
+ * a type a Root Type – and so what gives it a collection. */
214
218
  readonly class: string;
215
- readonly fields: readonly SearchField[];
216
219
  }
220
+ /**
221
+ * A **Reference Type**: a {@link SearchType} reached only through an
222
+ * {@link ReferenceField.ref inline reference}. It declares **no `class`** –
223
+ * never selected, never framed by type, never indexed; its identity is its
224
+ * `name`, and its type comes from the edge that points at it, not from the node.
225
+ * The absence is load-bearing, not stylistic: a `class` would put it in the
226
+ * {@link SearchSchema} map and silently earn it a collection nobody asked for.
227
+ * The shape an inline reference carries – see
228
+ * [ADR 11](../../docs/decisions/0011-decouple-rdf-depth-from-the-api-surface.md).
229
+ */
230
+ export interface ReferenceType extends SearchTypeBase {
231
+ /** A Reference Type declares no class; declaring one makes it a
232
+ * {@link RootType}. Typed as `never` so the two shapes discriminate the way
233
+ * {@link SearchField} discriminates by `kind`: an indexed Reference Type
234
+ * fails to compile, not at run time. */
235
+ readonly class?: never;
236
+ }
237
+ /**
238
+ * One type’s complete search declaration: its logical API `name`, the queryable
239
+ * `fields` (including {@link SearchField.derive derived} ones), and – for a
240
+ * {@link RootType} – the RDF `class` its documents are instances of. Either a
241
+ * Root Type or a {@link ReferenceType}; the absence of a `class` tells them
242
+ * apart.
243
+ */
244
+ export type SearchType = RootType | ReferenceType;
245
+ /** The Root Types among a declared tuple – the ones a {@link SearchSchema}
246
+ * keys and a Writer opens a collection for. Reference Types are excluded, so
247
+ * no consumer that iterates `schema.values()` ever meets one. */
248
+ export type RootTypeOf<Types extends readonly SearchType[]> = Extract<Types[number], {
249
+ readonly class: string;
250
+ }>;
217
251
  /**
218
252
  * Declare a {@link SearchType}, capturing it as a literal: the `const` type
219
253
  * parameter preserves the field names and capability flags that the type-level
@@ -224,31 +258,67 @@ export interface SearchType {
224
258
  */
225
259
  export declare function defineSearchType<const Type extends SearchType>(searchType: Type): Type;
226
260
  /**
227
- * The complete search declaration of a deployment: every root {@link SearchType},
228
- * keyed by its `class` IRI. Build one with {@link searchSchema}, which captures
229
- * the declared types as a literal tuple (`Types`), so schema-bound consumers
230
- * (the engine port) can type their per-type behaviour off it. A plain
261
+ * The complete search declaration of a deployment: every {@link RootType},
262
+ * keyed by its `class` IRI, plus the {@link ReferenceType}s an inline reference
263
+ * resolves against. Build one with {@link searchSchema}, which captures the
264
+ * declared types as a literal tuple (`Types`), so schema-bound consumers (the
265
+ * engine port) can type their per-type behaviour off it. A plain
231
266
  * `: SearchSchema` annotation widens gracefully to `SearchType`.
267
+ *
268
+ * `values()` yields only Root Types – Reference Types are held apart in a name
269
+ * index ({@link referenceTypeNamed}), so a Writer that opens one collection per
270
+ * `values()` entry can never open one for a Reference Type.
232
271
  */
233
272
  /** Brand for {@link SearchSchema}: type-only, no runtime existence. Makes the
234
273
  * schema NOMINAL – a hand-built `Map` is not assignable, so `searchSchema()`
235
274
  * (which validates) is the only way to obtain one and downstream consumers
236
275
  * need no defensive re-validation. */
237
276
  export declare const validSearchSchema: unique symbol;
238
- export interface SearchSchema<Types extends readonly SearchType[] = readonly SearchType[]> extends ReadonlyMap<string, Types[number]> {
277
+ export interface SearchSchema<Types extends readonly SearchType[] = readonly SearchType[]> extends ReadonlyMap<string, RootTypeOf<Types>> {
239
278
  readonly [validSearchSchema]: true;
240
279
  }
241
280
  /**
242
- * Build a {@link SearchSchema} from root-type declarations, keyed by `class`.
281
+ * Build a {@link SearchSchema} from type declarations. Its arguments are
282
+ * **partitioned**: {@link RootType}s (those declaring a `class`) key the map –
283
+ * so a Writer opens exactly one collection per Root Type – and
284
+ * {@link ReferenceType}s (no `class`) go into a name index that an inline
285
+ * `ref.typeName` resolves against ({@link referenceTypeNamed}).
243
286
  *
244
287
  * Every declaration is validated ({@link assertValidSearchType}) – the
245
288
  * declaration-time counterpart of the port’s `assertValidQuery` – and the
246
- * schema-wide invariants are enforced: no two types may share a `class` IRI
247
- * (they would silently overwrite each other in the map) or a `name` (names
248
- * key the API surfaces). Throws on the first invalid declaration, so a bad
249
- * schema fails at startup, not per document at index time or per query.
289
+ * schema-wide invariants are enforced: no two Root Types may share a `class` IRI
290
+ * (they would silently overwrite each other in the map) and no two types may
291
+ * share a `name` (names key the API surfaces, across Root and Reference Types
292
+ * alike). Every inline reference must resolve to a declared Reference Type, and
293
+ * the inline reference graph must be acyclic – the only way its depth could be
294
+ * unbounded. Throws on the first invalid declaration, so a bad schema fails at
295
+ * startup, not per document at index time or per query.
250
296
  */
251
297
  export declare function searchSchema<const Types extends readonly SearchType[]>(...types: Types): SearchSchema<Types>;
298
+ /**
299
+ * The {@link ReferenceType} an inline `ref.typeName` names, or `undefined` when
300
+ * the schema declares none by that name. The read side of the name index
301
+ * {@link searchSchema} partitions the Reference Types into – the projection
302
+ * resolves an inline reference’s referent shape through it.
303
+ */
304
+ export declare function referenceTypeNamed(schema: SearchSchema, name: string): ReferenceType | undefined;
305
+ /** Whether a field is an inline reference – a {@link ReferenceField} whose
306
+ * `ref` carries its referent’s projected fields ({@link ReferenceType}). */
307
+ export declare function isInlineReference(field: SearchField): field is ReferenceField & {
308
+ readonly ref: {
309
+ readonly typeName: string;
310
+ readonly strategy: 'inline';
311
+ };
312
+ };
313
+ /**
314
+ * The framing depth a Root Type needs: how many hops the inline reference graph
315
+ * reaches from it (`Dataset → Subset → Measurement` is two), floored at one so
316
+ * the existing single-hop embed for non-inline references is preserved. Depth is
317
+ * a property of the declaration, bounded because {@link searchSchema} rejects
318
+ * inline cycles – never a knob or a constant. Framing bounded per batch keeps
319
+ * memory bounded by the unit of work (ADR 12), not the graph.
320
+ */
321
+ export declare function inlineFramingDepth(schema: SearchSchema, searchType: SearchType): number;
252
322
  /**
253
323
  * The text field a label source serves labels from – the ‘label’ convention
254
324
  * in one place: an `output` (something to reconstruct a label from),
@@ -304,7 +374,7 @@ export declare function validateSearchType(searchType: SearchType): readonly Sea
304
374
  * meet an index the deployment did not declare. Identity-based: the exact
305
375
  * declaration object must be in the schema, not a lookalike.
306
376
  */
307
- export declare function assertTypeInSchema(schema: SearchSchema, searchType: SearchType): void;
377
+ export declare function assertTypeInSchema(schema: SearchSchema, searchType: RootType): void;
308
378
  /** Throw on a structurally invalid {@link SearchType} ({@link validateSearchType}),
309
379
  * naming every issue. Called by {@link searchSchema} for each declaration. */
310
380
  export declare function assertValidSearchType(searchType: SearchType): void;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
package/dist/schema.js CHANGED
@@ -28,32 +28,143 @@ export function defineSearchType(searchType) {
28
28
  return searchType;
29
29
  }
30
30
  /**
31
- * Build a {@link SearchSchema} from root-type declarations, keyed by `class`.
31
+ * The Reference Type name index each {@link SearchSchema} carries alongside its
32
+ * class-keyed root map, kept out of the map itself so no consumer iterating
33
+ * `values()` ever meets a Reference Type. Held in a `WeakMap` rather than a
34
+ * property, so the schema stays a plain branded `Map` and the index is read only
35
+ * through {@link referenceTypeNamed}.
36
+ */
37
+ const referenceTypesBySchema = new WeakMap();
38
+ /** Whether a declared type is a {@link RootType} (declares a `class`) rather
39
+ * than a {@link ReferenceType}. */
40
+ function isRootType(searchType) {
41
+ return searchType.class !== undefined;
42
+ }
43
+ /**
44
+ * Build a {@link SearchSchema} from type declarations. Its arguments are
45
+ * **partitioned**: {@link RootType}s (those declaring a `class`) key the map –
46
+ * so a Writer opens exactly one collection per Root Type – and
47
+ * {@link ReferenceType}s (no `class`) go into a name index that an inline
48
+ * `ref.typeName` resolves against ({@link referenceTypeNamed}).
32
49
  *
33
50
  * Every declaration is validated ({@link assertValidSearchType}) – the
34
51
  * declaration-time counterpart of the port’s `assertValidQuery` – and the
35
- * schema-wide invariants are enforced: no two types may share a `class` IRI
36
- * (they would silently overwrite each other in the map) or a `name` (names
37
- * key the API surfaces). Throws on the first invalid declaration, so a bad
38
- * schema fails at startup, not per document at index time or per query.
52
+ * schema-wide invariants are enforced: no two Root Types may share a `class` IRI
53
+ * (they would silently overwrite each other in the map) and no two types may
54
+ * share a `name` (names key the API surfaces, across Root and Reference Types
55
+ * alike). Every inline reference must resolve to a declared Reference Type, and
56
+ * the inline reference graph must be acyclic – the only way its depth could be
57
+ * unbounded. Throws on the first invalid declaration, so a bad schema fails at
58
+ * startup, not per document at index time or per query.
39
59
  */
40
60
  export function searchSchema(...types) {
41
61
  const typeIris = new Set();
42
62
  const names = new Set();
43
63
  for (const searchType of types) {
44
64
  assertValidSearchType(searchType);
45
- if (typeIris.has(searchType.class)) {
46
- throw new Error(`Duplicate search type IRI “${searchType.class}”; each SearchType must declare a distinct class.`);
65
+ if (isRootType(searchType)) {
66
+ if (typeIris.has(searchType.class)) {
67
+ throw new Error(`Duplicate search type IRI “${searchType.class}”; each Root Type must declare a distinct class.`);
68
+ }
69
+ typeIris.add(searchType.class);
47
70
  }
48
71
  if (names.has(searchType.name)) {
49
72
  throw new Error(`Duplicate search type name “${searchType.name}”; each SearchType must declare a distinct name.`);
50
73
  }
51
- typeIris.add(searchType.class);
52
74
  names.add(searchType.name);
53
75
  }
76
+ const referenceTypes = new Map(types
77
+ .filter((searchType) => !isRootType(searchType))
78
+ .map((searchType) => [searchType.name, searchType]));
79
+ assertResolvableInlineReferences(types, referenceTypes);
54
80
  assertResolvableLabelSources(types);
55
81
  // The one blessed cast: only this validated constructor mints the brand.
56
- return new Map(types.map((searchType) => [searchType.class, searchType]));
82
+ const schema = new Map(types
83
+ .filter(isRootType)
84
+ .map((searchType) => [searchType.class, searchType]));
85
+ referenceTypesBySchema.set(schema, referenceTypes);
86
+ return schema;
87
+ }
88
+ /**
89
+ * The {@link ReferenceType} an inline `ref.typeName` names, or `undefined` when
90
+ * the schema declares none by that name. The read side of the name index
91
+ * {@link searchSchema} partitions the Reference Types into – the projection
92
+ * resolves an inline reference’s referent shape through it.
93
+ */
94
+ export function referenceTypeNamed(schema, name) {
95
+ return referenceTypesBySchema.get(schema)?.get(name);
96
+ }
97
+ /** Whether a field is an inline reference – a {@link ReferenceField} whose
98
+ * `ref` carries its referent’s projected fields ({@link ReferenceType}). */
99
+ export function isInlineReference(field) {
100
+ return field.kind === 'reference' && field.ref?.strategy === 'inline';
101
+ }
102
+ /**
103
+ * The framing depth a Root Type needs: how many hops the inline reference graph
104
+ * reaches from it (`Dataset → Subset → Measurement` is two), floored at one so
105
+ * the existing single-hop embed for non-inline references is preserved. Depth is
106
+ * a property of the declaration, bounded because {@link searchSchema} rejects
107
+ * inline cycles – never a knob or a constant. Framing bounded per batch keeps
108
+ * memory bounded by the unit of work (ADR 12), not the graph.
109
+ */
110
+ export function inlineFramingDepth(schema, searchType) {
111
+ return Math.max(1, inlineChainLength(schema, searchType));
112
+ }
113
+ /** The longest inline reference chain reachable from `searchType`. The recursion
114
+ * terminates because {@link searchSchema} rejects inline cycles; a reference the
115
+ * given schema does not declare (e.g. a type framed against another schema)
116
+ * contributes no depth. */
117
+ function inlineChainLength(schema, searchType) {
118
+ let longest = 0;
119
+ for (const field of searchType.fields) {
120
+ if (!isInlineReference(field)) {
121
+ continue;
122
+ }
123
+ const referent = referenceTypeNamed(schema, field.ref.typeName);
124
+ if (referent === undefined) {
125
+ continue;
126
+ }
127
+ longest = Math.max(longest, 1 + inlineChainLength(schema, referent));
128
+ }
129
+ return longest;
130
+ }
131
+ /**
132
+ * Every inline `ref.typeName` must resolve to a **declared Reference Type**, and
133
+ * the inline reference graph must be acyclic. Unlike a `labelOnly` reference –
134
+ * whose `typeName` is just an API name – an inline reference carries its
135
+ * referent’s fields, so it must know their shape, and an inline cycle is the one
136
+ * way framing depth could be unbounded. Checked schema-wide, because a single
137
+ * declaration cannot see its siblings.
138
+ */
139
+ function assertResolvableInlineReferences(types, referenceTypes) {
140
+ for (const searchType of types) {
141
+ for (const field of searchType.fields) {
142
+ if (!isInlineReference(field)) {
143
+ continue;
144
+ }
145
+ if (!referenceTypes.has(field.ref.typeName)) {
146
+ throw new Error(`Inline reference “${searchType.name}.${field.name}” names “${field.ref.typeName}”, which is not a declared reference type; declare a reference type (a SearchType with no class) with that name.`);
147
+ }
148
+ }
149
+ }
150
+ for (const referenceType of referenceTypes.values()) {
151
+ assertNoInlineCycle(referenceType, referenceTypes, new Set());
152
+ }
153
+ }
154
+ function assertNoInlineCycle(referenceType, referenceTypes, onPath) {
155
+ if (onPath.has(referenceType.name)) {
156
+ throw new Error(`Inline reference cycle through reference type “${referenceType.name}”; an inline reference graph must be acyclic, so its framing depth stays bounded.`);
157
+ }
158
+ const extended = new Set([...onPath, referenceType.name]);
159
+ for (const field of referenceType.fields) {
160
+ if (!isInlineReference(field)) {
161
+ continue;
162
+ }
163
+ // Resolvability is validated before any cycle check, so every inline
164
+ // `typeName` here names a declared reference type.
165
+ const referent = referenceTypes.get(field.ref.typeName);
166
+ assertNoInlineCycle(referent, referenceTypes, extended);
167
+ }
57
168
  }
58
169
  /**
59
170
  * The text field a label source serves labels from – the ‘label’ convention
@@ -1 +1 @@
1
- {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAahD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,YAAY,GACzB,IAAI,CAsJN"}
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAShD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,YAAY,GACzB,IAAI,CAsJN"}
package/dist/testing.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { filterOperatorFor, } from './query.js';
3
- import { facetableFields, filterableFields, } from './schema.js';
3
+ import { facetableFields, filterableFields } from './schema.js';
4
4
  /**
5
5
  * The executable {@link SearchEngine} port contract (import from
6
6
  * `@lde/search/testing`): a vitest suite every engine adapter runs against a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/search",
3
- "version": "0.10.0",
3
+ "version": "0.11.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",