@oml/owl 0.19.3 → 0.20.1

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.
Files changed (46) hide show
  1. package/out/index.d.ts +3 -0
  2. package/out/index.js +3 -0
  3. package/out/index.js.map +1 -1
  4. package/out/owl/owl-abox.d.ts +16 -5
  5. package/out/owl/owl-abox.js +365 -188
  6. package/out/owl/owl-abox.js.map +1 -1
  7. package/out/owl/owl-imports.js +4 -2
  8. package/out/owl/owl-imports.js.map +1 -1
  9. package/out/owl/owl-interfaces.d.ts +26 -1
  10. package/out/owl/owl-mapper.d.ts +2 -0
  11. package/out/owl/owl-mapper.js +77 -38
  12. package/out/owl/owl-mapper.js.map +1 -1
  13. package/out/owl/owl-service.d.ts +4 -0
  14. package/out/owl/owl-service.js +139 -44
  15. package/out/owl/owl-service.js.map +1 -1
  16. package/out/owl/owl-shacl.d.ts +8 -9
  17. package/out/owl/owl-shacl.js +43 -117
  18. package/out/owl/owl-shacl.js.map +1 -1
  19. package/out/owl/owl-sparql-engine.d.ts +6 -0
  20. package/out/owl/owl-sparql-engine.js +11 -0
  21. package/out/owl/owl-sparql-engine.js.map +1 -0
  22. package/out/owl/owl-sparql-oxigraph.d.ts +34 -0
  23. package/out/owl/owl-sparql-oxigraph.js +436 -0
  24. package/out/owl/owl-sparql-oxigraph.js.map +1 -0
  25. package/out/owl/owl-sparql.d.ts +0 -44
  26. package/out/owl/owl-sparql.js +0 -320
  27. package/out/owl/owl-sparql.js.map +1 -1
  28. package/out/owl/owl-store-lean.d.ts +29 -0
  29. package/out/owl/owl-store-lean.js +445 -0
  30. package/out/owl/owl-store-lean.js.map +1 -0
  31. package/out/owl/owl-store.d.ts +11 -1
  32. package/out/owl/owl-store.js +80 -6
  33. package/out/owl/owl-store.js.map +1 -1
  34. package/package.json +3 -3
  35. package/src/index.ts +3 -0
  36. package/src/owl/owl-abox.ts +401 -200
  37. package/src/owl/owl-imports.ts +4 -2
  38. package/src/owl/owl-interfaces.ts +28 -1
  39. package/src/owl/owl-mapper.ts +79 -41
  40. package/src/owl/owl-service.ts +149 -49
  41. package/src/owl/owl-shacl.ts +55 -132
  42. package/src/owl/owl-sparql-engine.ts +34 -0
  43. package/src/owl/owl-sparql-oxigraph.ts +527 -0
  44. package/src/owl/owl-sparql.ts +3 -366
  45. package/src/owl/owl-store-lean.ts +438 -0
  46. package/src/owl/owl-store.ts +86 -7
@@ -68,9 +68,11 @@ export class ImportGraph {
68
68
  dependentsOf(modelUri: string): string[] {
69
69
  const visited = new Set<string>();
70
70
  const worklist = [modelUri];
71
+ let cursor = 0;
71
72
 
72
- while (worklist.length > 0) {
73
- const current = worklist.shift();
73
+ while (cursor < worklist.length) {
74
+ const current = worklist[cursor];
75
+ cursor += 1;
74
76
  if (!current || visited.has(current)) continue;
75
77
  visited.add(current);
76
78
  const importers = this.importedBy.get(current) ?? new Set<string>();
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) 2026 Modelware. All rights reserved.
2
2
 
3
- import type { NamedNode, Quad, Store } from 'n3';
3
+ import type { NamedNode, Quad, Store, Term } from 'n3';
4
4
  import type * as RDF from '@rdfjs/types';
5
5
  import type { Ontology } from '@oml/language';
6
6
  import type { ChainingResult } from './owl-abox.js';
@@ -11,14 +11,26 @@ export interface OwlMapper {
11
11
  toQuads(ontology: Ontology): Quad[];
12
12
  }
13
13
 
14
+ /**
15
+ * Visits a model's stored facts as (subject, predicate, object, key) — `key` is the dedup key
16
+ * `${subject.id}|${predicate.id}|${object.id}`. Lets the ABox chainer read scope facts without the
17
+ * store materializing per-model `Quad` arrays (the lean store yields them straight from its columns).
18
+ */
19
+ export type ScopeFactVisitor = (subject: Term, predicate: NamedNode, object: Term, key: string) => void;
20
+
14
21
  export interface OwlStore {
15
22
  loadModel(modelUri: string, quads: Quad[]): void;
16
23
  loadEntailments(modelUri: string, quads: Quad[]): void;
24
+ addEntailment(modelUri: string, quad: Quad): void;
25
+ removeEntailment(modelUri: string, quad: Quad): void;
17
26
  applyModelDelta(modelUri: string, quads: Quad[], retracted: Quad[], asserted: Quad[]): void;
18
27
  retractModel(modelUri: string): void;
19
28
  diffModel(modelUri: string, newQuads: Quad[]): ModelDiff;
20
29
  clearEntailments(modelUri: string): void;
21
30
  graphs(modelUri: string): ModelGraphs;
31
+ forEachAssertedFact(modelUri: string, visit: ScopeFactVisitor): void;
32
+ forEachEntailedFact(modelUri: string, visit: ScopeFactVisitor): void;
33
+ getModelDataGen(modelUri: string): number;
22
34
  getStore(): Store;
23
35
  }
24
36
 
@@ -115,6 +127,21 @@ export interface OwlSparqlService {
115
127
  query(modelUri: string, sparql: string): Promise<OwlQueryResult>;
116
128
  construct(modelUri: string, sparql: string): Promise<OwlConstructResult>;
117
129
  ask(modelUri: string, sparql: string): Promise<OwlAskResult>;
130
+ /**
131
+ * Make `quads` visible to queries run inside `action`, then drop them. Used to chain SHACL
132
+ * `sh:rule` CONSTRUCT outputs (each rule's quads feed the next). Each engine reflects the
133
+ * quads wherever its queries read: Comunica into the live N3 store, Oxigraph into its mirror.
134
+ */
135
+ withMaterializedQuads<T>(modelUri: string, quads: RDF.Quad[], action: () => Promise<T>): Promise<T>;
136
+ }
137
+
138
+ /**
139
+ * A swappable SPARQL engine: an OwlSparqlService plus the construction-time wiring the
140
+ * ReasoningService needs. Concrete engines (Comunica, Oxigraph) implement this so they are
141
+ * fully interchangeable behind the factory in owl-sparql-engine.ts.
142
+ */
143
+ export interface OwlSparqlEngine extends OwlSparqlService {
144
+ setOntologyIriResolver(resolver: (modelUri: string) => string): void;
118
145
  }
119
146
 
120
147
  export interface OwlShaclValidationIssue {
@@ -48,7 +48,6 @@ import {
48
48
  isAnonymousConceptInstance,
49
49
  isAnonymousRelationInstance,
50
50
  isAspect,
51
- isBooleanLiteral,
52
51
  isBuiltInPredicate,
53
52
  isBuiltIn,
54
53
  isConcept,
@@ -67,7 +66,6 @@ import {
67
66
  isPropertyValueRestrictionAxiom,
68
67
  isQuantity,
69
68
  isQuantityProperty,
70
- isQuotedLiteral,
71
69
  isRelation,
72
70
  isRelationEntityPredicate,
73
71
  isRelationEntity,
@@ -284,9 +282,20 @@ const BUILT_IN_ONTOLOGIES = new Set([
284
282
  'http://www.w3.org/2003/11/swrlb',
285
283
  ]);
286
284
 
287
- const { blankNode, namedNode, literal, quad } = DataFactory;
285
+ const { blankNode, literal, quad } = DataFactory;
288
286
  type TermNode = ReturnType<typeof namedNode> | ReturnType<typeof blankNode>;
289
287
 
288
+ const namedNodeCache = new Map<string, ReturnType<typeof DataFactory.namedNode>>();
289
+
290
+ function namedNode(iri: string): ReturnType<typeof DataFactory.namedNode> {
291
+ let node = namedNodeCache.get(iri);
292
+ if (!node) {
293
+ node = DataFactory.namedNode(iri);
294
+ namedNodeCache.set(iri, node);
295
+ }
296
+ return node;
297
+ }
298
+
290
299
  function fnv1a(input: string): string {
291
300
  let hash = 2166136261;
292
301
  for (let i = 0; i < input.length; i += 1) {
@@ -354,6 +363,9 @@ export class Oml2OwlMapper {
354
363
  });
355
364
  }
356
365
 
366
+ if (isDescription(ontology)) {
367
+ return triples;
368
+ }
357
369
  return this.deduplicateQuads(triples);
358
370
  }
359
371
 
@@ -593,12 +605,14 @@ export class Oml2OwlMapper {
593
605
  }
594
606
 
595
607
  private mapConceptInstance(instance: ConceptInstance, ctx: MappingContext, triples: Quad[]): void {
596
- const iri = this.elementIri(instance, ctx);
608
+ const iri = (instance as any).ref ? this.elementIri(instance, ctx) : this.ownedElementIri(instance, ctx);
597
609
  if (!iri) return;
598
610
  const node = namedNode(iri);
599
611
  const isDefinition = !(instance as any).ref;
600
612
 
601
- instance.ownedTypes?.forEach((typeAssertion) => this.mapTypeAssertion(node, typeAssertion, ctx, triples));
613
+ instance.ownedTypes?.forEach((typeAssertion) => {
614
+ this.mapTypeAssertion(node, typeAssertion, ctx, triples);
615
+ });
602
616
  if (isDefinition) {
603
617
  triples.push(this.quadWithGraph(node, namedNode(OML.type), namedNode(OML.ConceptInstance)));
604
618
  }
@@ -607,7 +621,7 @@ export class Oml2OwlMapper {
607
621
  }
608
622
 
609
623
  private mapRelationInstance(instance: RelationInstance, ctx: MappingContext, triples: Quad[]): void {
610
- const iri = this.elementIri(instance, ctx);
624
+ const iri = (instance as any).ref ? this.elementIri(instance, ctx) : this.ownedElementIri(instance, ctx);
611
625
  if (!iri) return;
612
626
  const node = namedNode(iri);
613
627
  const isDefinition = !(instance as any).ref;
@@ -873,14 +887,16 @@ export class Oml2OwlMapper {
873
887
  const propertyIri = this.resolveIri(assertion.property, ctx);
874
888
  if (!propertyIri) return;
875
889
  const propertyNode = namedNode(propertyIri);
876
-
877
- const propertyRef = assertion.property?.ref as any;
878
- const isQuantityProp = propertyRef && isQuantityProperty(propertyRef);
890
+ let isQuantityProp: boolean | undefined;
879
891
 
880
892
  assertion.literalValues?.forEach((lit, index) => {
881
893
  const isNumeric = isIntegerLiteral(lit) || isDecimalLiteral(lit) || isDoubleLiteral(lit);
882
894
  const unitRef = (lit as any)?.unit;
883
895
  const explicitUnitIri = unitRef ? this.resolveIri(unitRef, ctx) : undefined;
896
+ if (isNumeric && explicitUnitIri === undefined && isQuantityProp === undefined) {
897
+ const propertyRef = assertion.property?.ref as any;
898
+ isQuantityProp = propertyRef && isQuantityProperty(propertyRef);
899
+ }
884
900
  // Always wrap numeric literals on quantity properties in a QuantityLiteral
885
901
  // (the property's range is oml:QuantityLiteral). Plain literals only when
886
902
  // the property isn't a quantity property.
@@ -915,7 +931,6 @@ export class Oml2OwlMapper {
915
931
  triples.push(this.quadWithGraph(subject, propertyNode, valueNode));
916
932
  this.mapAnonymousInstance(contained, valueNode, ctx, triples, subject);
917
933
  });
918
-
919
934
  }
920
935
 
921
936
  private mapAnnotations(
@@ -1450,9 +1465,14 @@ export class Oml2OwlMapper {
1450
1465
  map[prefix] = ns;
1451
1466
  }
1452
1467
  ontology.ownedImports?.forEach((imp: Import) => {
1453
- const imported = imp.imported?.ref as Ontology | undefined;
1454
- const importedNs = imported ? this.normalizeNamespace((imported as any).namespace ?? '') : undefined;
1455
- const importedPrefix = imp.prefix ?? (imported as any)?.prefix;
1468
+ const refText = (imp.imported as any)?.$refText ?? (imp.imported as any)?.refText;
1469
+ let importedNs = refText ? this.resolveFromRefText(refText, { ...this.emptyContext(), prefixes: map }) : undefined;
1470
+ let importedPrefix = imp.prefix;
1471
+ if (!importedNs || !importedPrefix) {
1472
+ const imported = imp.imported?.ref as Ontology | undefined;
1473
+ importedNs = importedNs ?? (imported ? this.normalizeNamespace((imported as any).namespace ?? '') : undefined);
1474
+ importedPrefix = importedPrefix ?? (imported as any)?.prefix;
1475
+ }
1456
1476
  if (importedPrefix && importedNs) {
1457
1477
  map[importedPrefix] = importedNs;
1458
1478
  }
@@ -1469,13 +1489,13 @@ export class Oml2OwlMapper {
1469
1489
  }
1470
1490
 
1471
1491
  private importedOntologyIri(imp: Import, ctx: MappingContext): string | undefined {
1472
- if (imp.imported?.ref && isOntology(imp.imported.ref)) {
1473
- return this.normalizeNamespace((imp.imported.ref as any).namespace ?? '').replace(/[\/#]+$/, '');
1474
- }
1475
1492
  const refText = (imp.imported as any)?.$refText ?? (imp.imported as any)?.refText;
1476
1493
  if (refText) {
1477
1494
  return this.resolveFromRefText(refText, ctx)?.replace(/[\/#]+$/, '');
1478
1495
  }
1496
+ if (imp.imported?.ref && isOntology(imp.imported.ref)) {
1497
+ return this.normalizeNamespace((imp.imported.ref as any).namespace ?? '').replace(/[\/#]+$/, '');
1498
+ }
1479
1499
  return undefined;
1480
1500
  }
1481
1501
 
@@ -1505,6 +1525,12 @@ export class Oml2OwlMapper {
1505
1525
  return `${namespace}${name.replace(/^\^/, '')}`;
1506
1526
  }
1507
1527
 
1528
+ private ownedElementIri(element: { name?: string }, ctx: MappingContext): string | undefined {
1529
+ const name = (element as any)?.name;
1530
+ if (!name) return undefined;
1531
+ return `${ctx.namespace}${name.replace(/^\^/, '')}`;
1532
+ }
1533
+
1508
1534
  private getOntology(element: any): Ontology | undefined {
1509
1535
  let current: any = element;
1510
1536
  while (current && !isOntology(current)) {
@@ -1518,19 +1544,28 @@ export class Oml2OwlMapper {
1518
1544
  if (typeof ref === 'string') {
1519
1545
  return this.resolveFromRefText(ref, ctx);
1520
1546
  }
1547
+ const refText = ref.$refText ?? ref.refText;
1548
+ if (refText) {
1549
+ return this.resolveFromRefText(refText, ctx);
1550
+ }
1521
1551
  if (ref.ref && (ref.ref as any).name) {
1522
1552
  return this.elementIri(ref.ref as any, ctx);
1523
1553
  }
1524
1554
  if (ref.name) {
1525
1555
  return this.elementIri(ref, ctx);
1526
1556
  }
1527
- const refText = ref.$refText ?? ref.refText;
1528
- if (refText) {
1529
- return this.resolveFromRefText(refText, ctx);
1530
- }
1531
1557
  return undefined;
1532
1558
  }
1533
1559
 
1560
+ private emptyContext(): MappingContext {
1561
+ return {
1562
+ ontologyIri: '',
1563
+ namespace: '',
1564
+ prefix: '',
1565
+ prefixes: {},
1566
+ };
1567
+ }
1568
+
1534
1569
  private resolveFromRefText(refText: string, ctx: MappingContext): string | undefined {
1535
1570
  if (refText.startsWith('<') && refText.endsWith('>')) {
1536
1571
  return refText.slice(1, -1);
@@ -1543,6 +1578,9 @@ export class Oml2OwlMapper {
1543
1578
  return `${ns}${local}`;
1544
1579
  }
1545
1580
  }
1581
+ if (!refText.includes(':') && ctx.namespace) {
1582
+ return `${ctx.namespace}${refText.replace(/^\^/, '')}`;
1583
+ }
1546
1584
  return undefined;
1547
1585
  }
1548
1586
 
@@ -1561,29 +1599,29 @@ export class Oml2OwlMapper {
1561
1599
  }
1562
1600
 
1563
1601
  private toLiteral(value: Literal, ctx: MappingContext): Quad_Object | undefined {
1564
- if (isBooleanLiteral(value)) {
1565
- return literal(String(value.value), namedNode(XSD.boolean));
1566
- }
1567
- if (isIntegerLiteral(value)) {
1568
- return literal(String(value.value), namedNode(XSD.integer));
1569
- }
1570
- if (isDecimalLiteral(value)) {
1571
- return literal(String(value.value), namedNode(XSD.decimal));
1572
- }
1573
- if (isDoubleLiteral(value)) {
1574
- return literal(formatDoubleLiteralValue(value.value), namedNode(XSD.double));
1575
- }
1576
- if (isQuotedLiteral(value)) {
1577
- if (value.type) {
1578
- const dt = this.resolveIri(value.type, ctx);
1579
- return literal(value.value, dt ? namedNode(dt) : undefined);
1580
- }
1581
- if (value.langTag) {
1582
- return literal(value.value, value.langTag);
1602
+ switch ((value as any).$type) {
1603
+ case 'BooleanLiteral':
1604
+ return literal(String(value.value), namedNode(XSD.boolean));
1605
+ case 'IntegerLiteral':
1606
+ return literal(String(value.value), namedNode(XSD.integer));
1607
+ case 'DecimalLiteral':
1608
+ return literal(String(value.value), namedNode(XSD.decimal));
1609
+ case 'DoubleLiteral':
1610
+ return literal(formatDoubleLiteralValue(value.value), namedNode(XSD.double));
1611
+ case 'QuotedLiteral': {
1612
+ const quoted = value as any;
1613
+ if (quoted.type) {
1614
+ const dt = this.resolveIri(quoted.type, ctx);
1615
+ return literal(quoted.value, dt ? namedNode(dt) : undefined);
1616
+ }
1617
+ if (quoted.langTag) {
1618
+ return literal(quoted.value, quoted.langTag);
1619
+ }
1620
+ return literal(quoted.value);
1583
1621
  }
1584
- return literal(value.value);
1622
+ default:
1623
+ return undefined;
1585
1624
  }
1586
- return undefined;
1587
1625
  }
1588
1626
 
1589
1627
  private toFirstUpper(name: string): string {
@@ -2,13 +2,14 @@
2
2
 
3
3
  import { DocumentState, URI, type LangiumDocument } from 'langium';
4
4
  import { DataFactory, type NamedNode, type Quad } from 'n3';
5
+ import type * as RDF from '@rdfjs/types';
5
6
  import { OmlIndex, getOntologyModelIndex, isDescription, isDescriptionBundle, isOntology, isVocabulary, isVocabularyBundle, type Import, type OmlServices, type Ontology } from '@oml/language';
6
7
  import { Oml2OwlMapper } from './owl-mapper.js';
7
8
  import { VocabularyBundleClosureBuilder } from './owl-closure.js';
8
- import { ReasoningStore } from './owl-store.js';
9
+ import { LeanReasoningStore } from './owl-store-lean.js';
9
10
  import { ABoxChainer, ABoxEntailmentCache, type ChainingResult } from './owl-abox.js';
10
11
  import { ImportGraph } from './owl-imports.js';
11
- import { SparqlService } from './owl-sparql.js';
12
+ import { createSparqlEngine, resolveSparqlEngineKind } from './owl-sparql-engine.js';
12
13
  import { TBoxChainer, TBoxIndexBuilder, TBoxIndexCache } from './owl-tbox.js';
13
14
  import type {
14
15
  OwlABoxChainer,
@@ -38,6 +39,7 @@ export class ReasoningService {
38
39
  private readonly preparedModelAliases = new Map<string, string>();
39
40
  private readonly activeDocuments = new Set<string>();
40
41
  private readonly modelLoadInFlight = new Map<string, Promise<void>>();
42
+ private readonly importModelUriResolutionCache = new Map<string, string | undefined>();
41
43
  private readonly semanticChangeListeners = new Set<(modelUris: string[]) => void>();
42
44
  private readonly pendingSemanticChangedUris = new Set<string>();
43
45
  private semanticChangeSuppressionDepth = 0;
@@ -54,8 +56,9 @@ export class ReasoningService {
54
56
  this.aboxChainer = resolved.aboxChainer;
55
57
  this.aboxEntailmentCache = resolved.aboxEntailmentCache;
56
58
  this.sparqlService = resolved.createSparqlService(this);
57
- if (this.sparqlService instanceof SparqlService) {
58
- this.sparqlService.setOntologyIriResolver((modelUri) => this.resolveOntologyIriForModelUri(modelUri));
59
+ const sparqlWithResolver = this.sparqlService as { setOntologyIriResolver?: (resolver: (modelUri: string) => string) => void };
60
+ if (typeof sparqlWithResolver.setOntologyIriResolver === 'function') {
61
+ sparqlWithResolver.setOntologyIriResolver((modelUri) => this.resolveOntologyIriForModelUri(modelUri));
59
62
  }
60
63
  this.ontologyModelIndex = getOntologyModelIndex(services.shared);
61
64
 
@@ -69,6 +72,9 @@ export class ReasoningService {
69
72
  this.notifySemanticChanged(uris);
70
73
  }
71
74
  });
75
+ services.shared.workspace.DocumentBuilder.onUpdate(() => {
76
+ this.importModelUriResolutionCache.clear();
77
+ });
72
78
  }
73
79
 
74
80
  onDocumentValidated(document: LangiumDocument): void {
@@ -83,20 +89,24 @@ export class ReasoningService {
83
89
 
84
90
  const quads = this.mapOntologyToQuads(ontology as Ontology);
85
91
  const wasModelLoaded = this.isModelLoaded(modelUri);
86
- const diff = this.reasoningStore.diffModel(modelUri, quads);
87
- if (diff.isEmpty) {
88
- return;
92
+ if (wasModelLoaded) {
93
+ const diff = this.reasoningStore.diffModel(modelUri, quads);
94
+ if (diff.isEmpty) {
95
+ return;
96
+ }
97
+ this.reasoningStore.retractModel(modelUri);
89
98
  }
90
99
  this.tboxIndexCache.invalidateMerged(modelUri);
91
- this.reasoningStore.retractModel(modelUri);
92
100
  this.reasoningStore.loadModel(modelUri, quads);
93
- const impactedByLoad = [modelUri, ...this.importGraph.dependentsOf(modelUri)];
94
- for (const uri of impactedByLoad) {
95
- this.sparqlService.invalidateFilteredStore(uri);
96
- if (!wasModelLoaded || this.semanticChangeSuppressionDepth > 0) {
97
- continue;
101
+ if (wasModelLoaded) {
102
+ const impactedByLoad = [modelUri, ...this.importGraph.dependentsOf(modelUri)];
103
+ for (const uri of impactedByLoad) {
104
+ this.sparqlService.invalidateFilteredStore(uri);
105
+ if (this.semanticChangeSuppressionDepth > 0) {
106
+ continue;
107
+ }
108
+ this.pendingSemanticChangedUris.add(uri);
98
109
  }
99
- this.pendingSemanticChangedUris.add(uri);
100
110
  }
101
111
  if (isVocabulary(ontology)) {
102
112
  this.tboxChainer.chain(modelUri);
@@ -158,7 +168,16 @@ export class ReasoningService {
158
168
  let merged = this.tboxIndexCache.getMerged(chainedModelUri);
159
169
  if (!merged || this.tboxIndexCache.isMergedDirty(chainedModelUri)) {
160
170
  merged = this.tboxIndexBuilder.buildMerged(chainedModelUri);
161
- this.tboxIndexCache.setMerged(chainedModelUri, merged);
171
+ // Retain the merged TBox index only for active (open/edited) documents — matching
172
+ // the policy in onDocumentValidated. For the rest it is just a transient input to
173
+ // this chain pass; caching it for all models keeps thousands of largely-duplicate
174
+ // merged vocabulary TBoxes resident. getMerged's only consumer is right here and it
175
+ // rebuilds on demand, so skipping the cache is correctness-neutral and costs one
176
+ // buildMerged on the next chain of that model (cheap — the per-vocabulary own
177
+ // indexes it merges stay cached).
178
+ if (this.isActiveDocument(chainedModelUri)) {
179
+ this.tboxIndexCache.setMerged(chainedModelUri, merged);
180
+ }
162
181
  }
163
182
 
164
183
  const readScope = this.resolveLoadedDependencyOrder(chainedModelUri);
@@ -236,6 +255,9 @@ export class ReasoningService {
236
255
  ask: (modelUri: string, sparql: string) => {
237
256
  return this.sparqlService.ask(this.resolveQueryModelUri(modelUri), sparql);
238
257
  },
258
+ withMaterializedQuads: <T>(modelUri: string, quads: RDF.Quad[], action: () => Promise<T>) => {
259
+ return this.sparqlService.withMaterializedQuads(this.resolveQueryModelUri(modelUri), quads, action);
260
+ },
239
261
  };
240
262
  }
241
263
 
@@ -312,28 +334,54 @@ export class ReasoningService {
312
334
  }
313
335
 
314
336
  private resolveImportedModelUri(ownedImport: Import, prefixes: Map<string, string>, referencingModelUri?: string): string | undefined {
315
- if (ownedImport.imported?.ref && isOntology(ownedImport.imported.ref)) {
316
- const importedNamespace = this.normalizeNamespace((ownedImport.imported.ref as any).namespace ?? '');
317
- const viaIndex = this.ontologyModelIndex.resolveModelUri(importedNamespace, referencingModelUri);
318
- if (viaIndex) {
319
- return viaIndex;
320
- }
321
- const importedDocumentUri = (ownedImport.imported.ref as any)?.$document?.uri?.toString();
322
- if (importedDocumentUri) {
323
- return importedDocumentUri;
337
+ const refText = this.getImportRefText(ownedImport);
338
+ if (refText) {
339
+ const ontologyIdentifier = this.resolveFromRefText(refText, prefixes);
340
+ if (!ontologyIdentifier || BUILT_IN_ONTOLOGIES.has(ontologyIdentifier)) {
341
+ return undefined;
324
342
  }
325
- return this.resolveModelUriFromOntologyIdentifier(importedNamespace);
343
+ return this.resolveImportedOntologyIdentifier(ontologyIdentifier, referencingModelUri);
326
344
  }
327
- const refText = (ownedImport.imported as any)?.$refText ?? (ownedImport.imported as any)?.refText;
328
- if (!refText) {
345
+
346
+ const imported = ownedImport.imported?.ref;
347
+ if (!imported || !isOntology(imported)) {
329
348
  return undefined;
330
349
  }
331
- const ontologyIdentifier = this.resolveFromRefText(refText, prefixes);
332
- if (!ontologyIdentifier || BUILT_IN_ONTOLOGIES.has(ontologyIdentifier)) {
333
- return undefined;
350
+ const importedNamespace = this.normalizeNamespace((imported as any).namespace ?? '');
351
+ const viaIndex = this.ontologyModelIndex.resolveModelUri(importedNamespace, referencingModelUri);
352
+ if (viaIndex) {
353
+ return viaIndex;
354
+ }
355
+ const importedDocumentUri = (imported as any)?.$document?.uri?.toString();
356
+ if (importedDocumentUri) {
357
+ return importedDocumentUri;
358
+ }
359
+ return this.resolveModelUriFromOntologyIdentifier(importedNamespace);
360
+ }
361
+
362
+ private resolveImportedOntologyIdentifier(identifier: string, referencingModelUri?: string): string | undefined {
363
+ const globalCacheKey = `|${identifier}`;
364
+ if (this.importModelUriResolutionCache.has(globalCacheKey)) {
365
+ const resolved = this.importModelUriResolutionCache.get(globalCacheKey);
366
+ if (resolved) {
367
+ return resolved;
368
+ }
369
+ } else {
370
+ const globallyResolved = this.resolveModelUriFromOntologyIdentifier(identifier);
371
+ this.importModelUriResolutionCache.set(globalCacheKey, globallyResolved);
372
+ if (globallyResolved) {
373
+ return globallyResolved;
374
+ }
375
+ }
376
+
377
+ const cacheKey = `${referencingModelUri ?? ''}|${identifier}`;
378
+ if (this.importModelUriResolutionCache.has(cacheKey)) {
379
+ return this.importModelUriResolutionCache.get(cacheKey);
334
380
  }
335
- return this.ontologyModelIndex.resolveModelUri(ontologyIdentifier, referencingModelUri)
336
- ?? this.resolveModelUriFromOntologyIdentifier(ontologyIdentifier);
381
+ const resolved = this.ontologyModelIndex.resolveModelUri(identifier, referencingModelUri)
382
+ ?? this.resolveModelUriFromOntologyIdentifier(identifier);
383
+ this.importModelUriResolutionCache.set(cacheKey, resolved);
384
+ return resolved;
337
385
  }
338
386
 
339
387
  private buildPrefixMap(ontology: Ontology): Map<string, string> {
@@ -344,9 +392,12 @@ export class ReasoningService {
344
392
  map.set(ontologyPrefix, ontologyNamespace);
345
393
  }
346
394
  for (const ownedImport of ontology.ownedImports ?? []) {
347
- const imported = ownedImport.imported?.ref;
348
- const importedPrefix = ownedImport.prefix ?? (imported as any)?.prefix;
349
- const importedNamespace = imported ? this.normalizeNamespace((imported as any).namespace ?? '') : undefined;
395
+ const importedPrefix = ownedImport.prefix;
396
+ if (!importedPrefix) {
397
+ continue;
398
+ }
399
+ const refText = this.getImportRefText(ownedImport);
400
+ const importedNamespace = refText ? this.resolveFromRefText(refText, map) : undefined;
350
401
  if (importedPrefix && importedNamespace) {
351
402
  map.set(importedPrefix, importedNamespace);
352
403
  }
@@ -354,6 +405,11 @@ export class ReasoningService {
354
405
  return map;
355
406
  }
356
407
 
408
+ private getImportRefText(ownedImport: Import): string | undefined {
409
+ const imported = ownedImport.imported as any;
410
+ return imported?.$refText ?? imported?.refText;
411
+ }
412
+
357
413
  private resolveFromRefText(refText: string, prefixes: Map<string, string>): string | undefined {
358
414
  if (refText.startsWith('<') && refText.endsWith('>')) {
359
415
  return refText.slice(1, -1);
@@ -418,8 +474,12 @@ export class ReasoningService {
418
474
  }
419
475
  }
420
476
  await this.withSemanticChangeSuppressed(async () => {
421
- await this.services.shared.workspace.DocumentBuilder.build([document], { validation: true });
477
+ await this.services.shared.workspace.DocumentBuilder.build([document], { validation: false, eagerLinking: false });
422
478
  });
479
+ const root = document.parseResult.value;
480
+ if (isOntology(root)) {
481
+ this.onDocumentValidated(document);
482
+ }
423
483
  })().finally(() => {
424
484
  this.modelLoadInFlight.delete(modelUri);
425
485
  });
@@ -462,8 +522,10 @@ export class ReasoningService {
462
522
 
463
523
  private async ensureContextDatasetReady(modelUri: string): Promise<void> {
464
524
  const attemptedLoads = new Set<string>();
525
+ let pass = 0;
465
526
 
466
527
  while (true) {
528
+ pass += 1;
467
529
  const allUris = [modelUri, ...this.importGraph.dependenciesOf(modelUri)];
468
530
  const unresolvedIdentifiers: string[] = [];
469
531
  const resolvedModelUris: string[] = [];
@@ -487,17 +549,17 @@ export class ReasoningService {
487
549
 
488
550
  const loadFailures: string[] = [];
489
551
  let loadedAny = false;
490
- for (const uri of unloadedModelUris) {
491
- if (attemptedLoads.has(uri)) {
492
- continue;
493
- }
552
+ const loadTargets = unloadedModelUris.filter((uri) => !attemptedLoads.has(uri));
553
+ for (const uri of loadTargets) {
494
554
  attemptedLoads.add(uri);
555
+ }
556
+ if (loadTargets.length > 0) {
495
557
  try {
496
- await this.ensureModelLoaded(uri);
558
+ await this.ensureModelsLoaded(loadTargets);
497
559
  loadedAny = true;
498
560
  } catch (error) {
499
561
  const message = error instanceof Error ? error.message : String(error);
500
- loadFailures.push(`${uri} (${message})`);
562
+ loadFailures.push(`${loadTargets.length} model(s) (${message})`);
501
563
  }
502
564
  }
503
565
  if (loadFailures.length > 0) {
@@ -526,6 +588,46 @@ export class ReasoningService {
526
588
  }
527
589
  }
528
590
 
591
+ private async ensureModelsLoaded(modelUris: ReadonlyArray<string>): Promise<void> {
592
+ const uniqueUris = [...new Set(modelUris)].filter((uri) => !this.isModelLoaded(uri));
593
+ if (uniqueUris.length === 0) {
594
+ return;
595
+ }
596
+
597
+ const documents: LangiumDocument[] = [];
598
+ for (const modelUri of uniqueUris) {
599
+ const inFlight = this.modelLoadInFlight.get(modelUri);
600
+ if (inFlight) {
601
+ await inFlight;
602
+ continue;
603
+ }
604
+ if (this.isModelLoaded(modelUri)) {
605
+ continue;
606
+ }
607
+ const uri = URI.parse(modelUri);
608
+ let document = this.services.shared.workspace.LangiumDocuments.getDocument(uri);
609
+ if (!document) {
610
+ document = await this.services.shared.workspace.LangiumDocuments.getOrCreateDocument(uri);
611
+ }
612
+ documents.push(document);
613
+ }
614
+
615
+ if (documents.length === 0) {
616
+ return;
617
+ }
618
+
619
+ await this.withSemanticChangeSuppressed(async () => {
620
+ await this.services.shared.workspace.DocumentBuilder.build(documents, { validation: false, eagerLinking: false });
621
+ });
622
+
623
+ for (const document of documents) {
624
+ const root = document.parseResult.value;
625
+ if (isOntology(root)) {
626
+ this.onDocumentValidated(document);
627
+ }
628
+ }
629
+ }
630
+
529
631
  async countContextDatasetQuads(modelUri: string): Promise<number> {
530
632
  const resolvedModelUri = this.resolveQueryModelUri(modelUri);
531
633
  await this.services.shared.workspace.WorkspaceManager.ready;
@@ -744,19 +846,17 @@ const BUILT_IN_ONTOLOGIES = new Set([
744
846
 
745
847
  function createDefaultDependencies(overrides: Partial<OwlReasoningDependencies>): OwlReasoningDependencies {
746
848
  const mapper = overrides.mapper ?? new Oml2OwlMapper();
747
- const reasoningStore = overrides.reasoningStore ?? new ReasoningStore();
849
+ // LeanReasoningStore is term-interned with packed columns — ~9x less memory than the
850
+ // n3-backed ReasoningStore, and faster scans.
851
+ const reasoningStore = overrides.reasoningStore ?? new LeanReasoningStore();
748
852
  const importGraph = overrides.importGraph ?? new ImportGraph();
749
853
  const tboxIndexCache = overrides.tboxIndexCache ?? new TBoxIndexCache();
750
854
  const tboxChainer = overrides.tboxChainer ?? new TBoxChainer(reasoningStore);
751
855
  const tboxIndexBuilder = overrides.tboxIndexBuilder ?? new TBoxIndexBuilder(reasoningStore, tboxIndexCache, importGraph);
752
856
  const aboxChainer = overrides.aboxChainer ?? new ABoxChainer(reasoningStore);
753
857
  const aboxEntailmentCache = overrides.aboxEntailmentCache ?? new ABoxEntailmentCache();
754
- const createSparqlService = overrides.createSparqlService ?? ((runner) => new SparqlService(
755
- reasoningStore,
756
- importGraph,
757
- aboxEntailmentCache,
758
- runner,
759
- ));
858
+ const createSparqlService = overrides.createSparqlService ?? ((runner) =>
859
+ createSparqlEngine(resolveSparqlEngineKind(), reasoningStore, importGraph, aboxEntailmentCache, runner));
760
860
  return {
761
861
  mapper,
762
862
  reasoningStore,