rdf-reasoner-konclude 0.2.0 → 0.3.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
@@ -114,26 +114,107 @@ The browser build requires COOP/COEP HTTP headers for `SharedArrayBuffer` (used
114
114
  const reasoner = new RdfReasoner();
115
115
  await reasoner.ready; // resolves when WASM module is loaded
116
116
 
117
- await reasoner.reason(store); // classify + write inferred triples into store
118
- await reasoner.classify(store); // alias for reason(store)
119
- const ok = await reasoner.checkConsistency(store); // returns boolean
117
+ // TBox: class hierarchy (rdfs:subClassOf, owl:equivalentClass)
118
+ await reasoner.classify(store);
119
+
120
+ // ABox: individual type entailments (rdf:type)
121
+ await reasoner.materialize(store);
122
+ await reasoner.materialize(store, { includeClassHierarchy: true }); // TBox + ABox
123
+
124
+ // Property hierarchy (rdfs:subPropertyOf)
125
+ await reasoner.classifyProperties(store);
126
+
127
+ // Consistency
128
+ const ok = await reasoner.checkConsistency(store); // true = consistent
129
+
130
+ // Entailment queries (post-reasoning)
131
+ const entailed = await reasoner.isEntailed(store, quad);
132
+ const results = await reasoner.isEntailed(store, [quad1, quad2]);
133
+
134
+ // Hypothetical reasoning
135
+ const { added, removed } = await reasoner.whatIf(store, [newAxiom]);
136
+
137
+ // Explanation / justification
138
+ const justs = await reasoner.explain(store, quad, { maxJustifications: 3 });
139
+ const inconsJusts = await reasoner.explainInconsistency(store);
140
+
141
+ // Satisfiability
142
+ const classes = await reasoner.getUnsatisfiableClasses(store);
143
+ const ok2 = await reasoner.isSatisfiable(store, "http://example.org/MyClass");
144
+
145
+ // Combined diagnostic
146
+ const report = await reasoner.validate(store);
147
+ // report.consistent, report.errors (Quad[][]), report.warnings (ClassWarning[])
120
148
 
121
149
  reasoner.terminate(); // shut down the Worker
122
150
  ```
123
151
 
124
- `reason(store)` and `classify(store)` write inferred triples into the
125
- `INFERRED_GRAPH_IRI` named graph inside the store. The graph is cleared before
126
- each call — do not store ontology triples there.
152
+ `classify(store)`, `materialize(store)`, and `classifyProperties(store)` write
153
+ inferred triples into the `INFERRED_GRAPH_IRI` named graph inside the store.
154
+ The graph is cleared before each call — do not store ontology triples there.
127
155
 
128
156
  Named graphs in the input are dropped at the WASM boundary (NTriples wire
129
157
  format is triple-only). Reasoning runs over the union of all graphs.
130
158
 
131
- Options for `reason(store, opts)` and `classify(store, opts)`:
159
+ Options for `classify(store, opts)`, `materialize(store, opts)`, and `classifyProperties(store, opts)`:
132
160
 
133
161
  ```typescript
162
+ // classify, classifyProperties
134
163
  interface StoreReasoningOptions {
135
- inferredGraph?: string; // IRI of the named graph for inferred triples
136
- // default: INFERRED_GRAPH_IRI
164
+ inferredGraph?: string; // default: INFERRED_GRAPH_IRI
165
+ }
166
+
167
+ // materialize
168
+ interface MaterializeStoreOptions {
169
+ inferredGraph?: string; // default: INFERRED_GRAPH_IRI
170
+ includeClassHierarchy?: boolean; // also emit subClassOf/equivalentClass; default false
171
+ }
172
+ ```
173
+
174
+ Options for the Phase 2 axiom-work methods:
175
+
176
+ ```typescript
177
+ // isEntailed: no options
178
+
179
+ // whatIf
180
+ interface WhatIfOptions {
181
+ removals?: Quad[]; // quads to remove from the base ontology
182
+ outputGraph?: string; // named graph for hypothetical inferences
183
+ }
184
+
185
+ // explain / explainInconsistency
186
+ interface ExplainOptions {
187
+ maxJustifications?: number; // default: 1
188
+ axiomFilter?: (q: Quad) => boolean; // restrict candidate axiom set
189
+ }
190
+
191
+ // validate
192
+ interface ValidateOptions {
193
+ maxJustificationsPerError?: number; // default: 1
194
+ maxJustificationsPerWarning?: number; // default: 1 (pass 0 for IRI-only scan)
195
+ axiomFilter?: (q: Quad) => boolean;
196
+ }
197
+ ```
198
+
199
+ Return types for the Phase 2 axiom-work methods:
200
+
201
+ ```typescript
202
+ // Return types
203
+ isEntailed(store, axiom: Quad): Promise<boolean | null>
204
+ isEntailed(store, axioms: Quad[]): Promise<(boolean | null)[]>
205
+ whatIf(store, additions): Promise<{ added: Quad[], removed: Quad[] }>
206
+ explain(store, axiom): Promise<Quad[][]> // [] if not entailed; throws for unsupported predicate
207
+ explainInconsistency(store): Promise<Quad[][]> // [] if consistent
208
+ validate(store): Promise<ValidationResult>
209
+ // where:
210
+ interface ValidationResult {
211
+ consistent: boolean;
212
+ errors: Quad[][]; // MIPS; non-empty only when consistent === false
213
+ warnings: ClassWarning[]; // one per unsatisfiable class
214
+ }
215
+ interface ClassWarning {
216
+ classIRI: string;
217
+ justifications: Quad[][];
137
218
  }
138
219
  ```
139
220
 
@@ -144,11 +225,315 @@ import { INFERRED_GRAPH_IRI } from "rdf-reasoner-konclude";
144
225
  // "urn:konclude:inferred"
145
226
  ```
146
227
 
147
- The default named graph where inferred triples are written by `reason(store)`.
228
+ The default named graph where inferred triples are written.
148
229
 
149
230
  ### Deprecated overloads
150
231
 
151
- `reason(quads: Iterable<Quad>)` and `classify(quads)` / `checkConsistency(quads)` accept a raw `Iterable<Quad>` and return `Promise<Quad[]>` / `Promise<boolean>`. These overloads are deprecated — use the `Store`-based API instead.
232
+ `reason(quads)`, `classify(quads)`, `checkConsistency(quads)`, and `materialize(quads)` accept a raw `Iterable<Quad>` and return `Promise<Quad[]>` / `Promise<boolean>`. These overloads are deprecated — use the `Store`-based API instead.
233
+
234
+ ## Common workflows
235
+
236
+ All examples assume a single `RdfReasoner` instance:
237
+
238
+ ```typescript
239
+ import { RdfReasoner, INFERRED_GRAPH_IRI } from "rdf-reasoner-konclude";
240
+ import { Store, Parser, DataFactory } from "n3";
241
+
242
+ const reasoner = new RdfReasoner();
243
+ await reasoner.ready;
244
+ ```
245
+
246
+ ### TBox only: classify a class hierarchy
247
+
248
+ Use `classify(store)` when the ontology has no named individuals. Returns
249
+ `rdfs:subClassOf` and `owl:equivalentClass` entailments.
250
+
251
+ ```typescript
252
+ await reasoner.classify(store);
253
+ const subClasses = store.getQuads(
254
+ null,
255
+ "http://www.w3.org/2000/01/rdf-schema#subClassOf",
256
+ null,
257
+ INFERRED_GRAPH_IRI,
258
+ );
259
+ ```
260
+
261
+ ### ABox: derive individual types
262
+
263
+ Use `materialize(store)` when the ontology includes named individuals. Returns
264
+ `rdf:type` entailments for every individual that can be classified under the
265
+ TBox. Classification runs internally as part of realization — no separate call
266
+ needed.
267
+
268
+ ```typescript
269
+ await reasoner.materialize(store);
270
+ const types = store.getQuads(
271
+ null,
272
+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
273
+ null,
274
+ INFERRED_GRAPH_IRI,
275
+ );
276
+ ```
277
+
278
+ To also receive the class hierarchy in the same call:
279
+
280
+ ```typescript
281
+ await reasoner.materialize(store, { includeClassHierarchy: true });
282
+ ```
283
+
284
+ ### Property hierarchy
285
+
286
+ Use `classifyProperties(store)` to infer `rdfs:subPropertyOf` entailments.
287
+ Requires `owl:ObjectProperty` / `owl:DatatypeProperty` declarations in the
288
+ ontology.
289
+
290
+ ```typescript
291
+ await reasoner.classifyProperties(store);
292
+ const subProps = store.getQuads(
293
+ null,
294
+ "http://www.w3.org/2000/01/rdf-schema#subPropertyOf",
295
+ null,
296
+ INFERRED_GRAPH_IRI,
297
+ );
298
+ ```
299
+
300
+ ### Combine class hierarchy + property hierarchy
301
+
302
+ `INFERRED_GRAPH_IRI` is cleared by each call, so write results to separate
303
+ named graphs if you need both at once:
304
+
305
+ ```typescript
306
+ await reasoner.classify(store, { inferredGraph: "urn:myapp:class-inferred" });
307
+ await reasoner.classifyProperties(store, {
308
+ inferredGraph: "urn:myapp:prop-inferred",
309
+ });
310
+ ```
311
+
312
+ Or if storing them together is fine, call in sequence and read after both:
313
+
314
+ ```typescript
315
+ await reasoner.classify(store);
316
+ const classTriples = store.getQuads(null, null, null, INFERRED_GRAPH_IRI);
317
+
318
+ await reasoner.classifyProperties(store); // clears + repopulates INFERRED_GRAPH_IRI
319
+ const propTriples = store.getQuads(null, null, null, INFERRED_GRAPH_IRI);
320
+ ```
321
+
322
+ ### Consistency checking
323
+
324
+ ```typescript
325
+ const consistent = await reasoner.checkConsistency(store);
326
+ if (!consistent) {
327
+ console.error("Ontology is inconsistent");
328
+ }
329
+ ```
330
+
331
+ ### Incremental ontology evolution
332
+
333
+ The reasoner resets and re-loads the full store on every call. To reason over
334
+ an evolving ontology, mutate the store and call again:
335
+
336
+ ```typescript
337
+ await reasoner.classify(store);
338
+ // → first inference written to INFERRED_GRAPH_IRI
339
+
340
+ // Add new axioms
341
+ store.addQuad(
342
+ DataFactory.quad(
343
+ DataFactory.namedNode("http://example.org/Cat"),
344
+ DataFactory.namedNode("http://www.w3.org/2000/01/rdf-schema#subClassOf"),
345
+ DataFactory.namedNode("http://example.org/Animal"),
346
+ ),
347
+ );
348
+
349
+ // Re-reason: INFERRED_GRAPH_IRI is cleared and fully repopulated
350
+ await reasoner.classify(store);
351
+ ```
352
+
353
+ Concurrent calls on the same `RdfReasoner` instance are serialized
354
+ automatically — queued calls run in order, never interleaved.
355
+
356
+ ### Checking if a specific entailment holds (`isEntailed`)
357
+
358
+ `isEntailed` supports `rdfs:subClassOf`, `owl:equivalentClass`, `rdf:type`, and
359
+ `rdfs:subPropertyOf`. Returns `null` for unsupported predicates.
360
+
361
+ ```typescript
362
+ await reasoner.materialize(store);
363
+ const isAnimal = await reasoner.isEntailed(
364
+ store,
365
+ DataFactory.quad(
366
+ DataFactory.namedNode("http://example.org/Fido"),
367
+ DataFactory.namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
368
+ DataFactory.namedNode("http://example.org/Animal"),
369
+ ),
370
+ );
371
+ // true if Fido rdf:type Animal is entailed
372
+ ```
373
+
374
+ ### Hypothetical reasoning (`whatIf`)
375
+
376
+ `whatIf` reasons over a temporary store mutation without modifying the input store.
377
+
378
+ ```typescript
379
+ const { added, removed } = await reasoner.whatIf(store, [newAxiom]);
380
+ // added/removed are deltas relative to the current INFERRED_GRAPH_IRI
381
+ ```
382
+
383
+ ### Explaining an entailment (`explain`)
384
+
385
+ `explain` returns minimal justifications — each is the smallest set of axioms
386
+ that alone entails the target. Returns `[]` if the axiom is not entailed.
387
+ Each BlackBox call issues multiple Worker round-trips; use `maxJustifications: 1`
388
+ for cheap first-justification lookups.
389
+
390
+ ```typescript
391
+ const justs = await reasoner.explain(store, quad, { maxJustifications: 3 });
392
+ // justs: Quad[][] — each inner array is one minimal justification
393
+ ```
394
+
395
+ ### Diagnosing an inconsistency (`explainInconsistency`)
396
+
397
+ Returns minimal inconsistent sub-ontologies (MIPS). Returns `[]` on consistent ontologies.
398
+
399
+ ```typescript
400
+ const mips = await reasoner.explainInconsistency(store);
401
+ if (mips.length > 0) {
402
+ console.log("Minimal inconsistent core:", mips[0]);
403
+ }
404
+ ```
405
+
406
+ ### Finding unsatisfiable classes (`getUnsatisfiableClasses`, `isSatisfiable`)
407
+
408
+ `owl:Nothing` is excluded from the output. Classes absent from the taxonomy
409
+ return `true` from `isSatisfiable` (open-world assumption).
410
+
411
+ ```typescript
412
+ const unsatClasses = await reasoner.getUnsatisfiableClasses(store);
413
+ // ["http://example.org/EmptyClass", ...]
414
+
415
+ const ok = await reasoner.isSatisfiable(store, "http://example.org/MyClass");
416
+ // false if MyClass is equivalent to owl:Nothing in the ontology
417
+ ```
418
+
419
+ ### Full ontology validation (`validate`)
420
+
421
+ `validate` combines consistency checking, unsatisfiable-class detection, and
422
+ optional justification computation in a single call.
423
+
424
+ ```typescript
425
+ const report = await reasoner.validate(store);
426
+ // report.consistent — true/false
427
+ // report.errors — Quad[][], minimal inconsistent sub-ontologies (non-empty when inconsistent)
428
+ // report.warnings — ClassWarning[], one per unsatisfiable class
429
+
430
+ // Cheap IRI-only scan (skip BlackBox justifications for warnings):
431
+ const quickReport = await reasoner.validate(store, {
432
+ maxJustificationsPerWarning: 0,
433
+ });
434
+ ```
435
+
436
+ `errors` are non-empty only when `consistent` is `false`. When `consistent` is
437
+ `true`, only `warnings` may be present. Callers should check `consistent` before
438
+ acting on `warnings`.
439
+
440
+ ### Separate inferred graph per operation
441
+
442
+ The `INFERRED_GRAPH_IRI` graph is cleared before each call. If you need to
443
+ preserve results from a previous call while re-reasoning with a different
444
+ operation, use separate named graphs:
445
+
446
+ ```typescript
447
+ await reasoner.classify(store, { inferredGraph: "urn:myapp:class-hierarchy" });
448
+ await reasoner.materialize(store, { inferredGraph: "urn:myapp:abox-types" });
449
+
450
+ // Both named graphs now coexist in the store
451
+ const classTriples = store.getQuads(
452
+ null,
453
+ null,
454
+ null,
455
+ "urn:myapp:class-hierarchy",
456
+ );
457
+ const typeTriples = store.getQuads(null, null, null, "urn:myapp:abox-types");
458
+ ```
459
+
460
+ ### Node.js and browser: same API
461
+
462
+ `RdfReasoner` is identical in both environments. Under Node.js the `"node"`
463
+ export condition in `package.json` loads `index.node.mjs`, which installs a
464
+ `NodeWorkerShim` over `node:worker_threads` that matches the browser `Worker`
465
+ interface. The worker dispatch code (`dist/worker.js`) and all protocol messages
466
+ are shared. No code changes are needed when porting between environments.
467
+
468
+ ## OWL 2 DL coverage
469
+
470
+ Konclude implements the full OWL 2 DL tableau (expressivity SROIQ(D)). The sections below
471
+ document what has been verified to work, what is known not to work, and what the WASM port
472
+ does not yet surface as output.
473
+
474
+ ### Consistency checking (`checkConsistency`)
475
+
476
+ Violation detection verified against native Konclude v0.7.0 ground truth:
477
+
478
+ | # | Violation pattern | Native | WASM | Status |
479
+ | --- | --------------------------------------------------------- | ------------------ | -------------- | ---------------------------------- |
480
+ | 1 | `owl:disjointWith` (direct) | inconsistent ✓ | inconsistent ✓ | **PARITY** |
481
+ | 2 | `owl:disjointWith` (via domain/range inference) | inconsistent ✓ | inconsistent ✓ | **PARITY** |
482
+ | 3 | `owl:AsymmetricProperty` bidirectional assertion | consistent ✗ (bug) | inconsistent ✓ | **PARITY (WASM surpasses native)** |
483
+ | 4 | `owl:IrreflexiveProperty` self-reference | consistent ✗ (bug) | inconsistent ✓ | **PARITY (WASM surpasses native)** |
484
+ | 5 | `owl:maxQualifiedCardinality` + `owl:differentFrom` | inconsistent ✓ | inconsistent ✓ | **PARITY** |
485
+ | 6 | `owl:allValuesFrom` + `owl:disjointWith` | inconsistent ✓ | inconsistent ✓ | **PARITY** |
486
+ | 7 | `owl:ReflexiveProperty` + `ObjectComplementOf(HasSelf)` | inconsistent ✓ | inconsistent ✓ | **PARITY** |
487
+ | 8 | `owl:InverseFunctionalProperty` + `DifferentIndividuals` | inconsistent ✓ | inconsistent ✓ | **PARITY** |
488
+ | 9 | `owl:AllDisjointClasses` (3-way) + double membership | inconsistent ✓ | inconsistent ✓ | **PARITY** |
489
+ | 10 | `DisjointObjectProperties` + `EquivalentObjectProperties` | consistent ✗ (bug) | inconsistent ✓ | **PARITY (WASM surpasses native)** |
490
+ | 11 | `owl:disjointUnionOf` + double membership | inconsistent ✓ | inconsistent ✓ | **PARITY** |
491
+ | 12 | `owl:NegativeObjectPropertyAssertion` contradiction | inconsistent ✓ | inconsistent ✓ | **PARITY** |
492
+ | 13 | `DataAllValuesFrom xsd:minInclusive` (consistent case) | consistent ✓ | consistent ✓ | **PARITY** |
493
+ | 14 | `DataAllValuesFrom xsd:minInclusive` (inconsistent case) | inconsistent ✓ | inconsistent ✓ | **PARITY** |
494
+
495
+ **PARITY (WASM surpasses native v0.7.0)** means native Konclude v0.7.0 has a kernel bug for this
496
+ construct; this package fixes it via patches 011–012 (role axiom correctness, NPA builder fix).
497
+
498
+ `owl:AllDisjointClasses`, `owl:disjointUnionOf`, and `owl:NegativePropertyAssertion` all work in
499
+ `materialize()` — the JS layer expands list axioms to pairwise form before handing off to WASM.
500
+ The only known remaining gap is `owl:FunctionalProperty` + `owl:InverseFunctionalProperty` on the
501
+ same role with a single filler (ALIF+ precompute hang). This case is tracked in
502
+ `tests/integration/known-limitations.test.ts`.
503
+
504
+ ### Classification (`classify`)
505
+
506
+ Verified working:
507
+
508
+ - `rdfs:subClassOf` — transitive closure, with `owl:equivalentClass` folding
509
+ - `owl:intersectionOf`, `owl:unionOf`, `owl:complementOf`
510
+ - `owl:someValuesFrom`, `owl:allValuesFrom`
511
+ - `owl:minCardinality`, `owl:maxCardinality`, `owl:exactCardinality`
512
+ - `owl:minQualifiedCardinality`, `owl:maxQualifiedCardinality`, `owl:qualifiedCardinality`
513
+ - `owl:ObjectProperty` with `rdfs:domain`, `rdfs:range`, `owl:inverseOf`
514
+ - `owl:TransitiveProperty`, `owl:FunctionalProperty`, `owl:InverseFunctionalProperty`
515
+ - `owl:ReflexiveProperty`, `owl:SymmetricProperty`
516
+ - `owl:propertyChainAxiom`
517
+
518
+ ### ABox realization (`materialize`)
519
+
520
+ `materialize(store)` infers `rdf:type` entailments for named individuals under the
521
+ class hierarchy. Verified working on ontologies up to ~25 000 individuals (LUBM + data).
522
+
523
+ Input ABox axioms accepted:
524
+
525
+ - `rdf:type` assertions
526
+ - Object property assertions (`owl:ObjectProperty`)
527
+ - `owl:differentFrom` / `owl:AllDifferent`
528
+
529
+ ### Known output gaps
530
+
531
+ | Feature | Status |
532
+ | ----------------------------------------- | ----------------------------------------- |
533
+ | `rdfs:subPropertyOf` (property hierarchy) | Available via `classifyProperties(store)` |
534
+ | `owl:sameAs` entailments | Emitted by `materialize()` ✓ |
535
+ | `owl:differentFrom` entailments | Not emitted (accepted as input ✓) |
536
+ | Data property assertions | Emitted by `materialize()` ✓ |
152
537
 
153
538
  ## Browser deployment
154
539
 
@@ -206,7 +591,7 @@ Benchmarked on an 8-core Linux host. Native = Konclude v0.7.0 Docker image; WASM
206
591
  | LUBM schema | SHI | 307 | 34 ms | 207 ms | 202 ms | ~6× |
207
592
  | GALEN | SHIF | 30 817 | 286 ms | 968 ms | 852 ms | ~3.4× |
208
593
  | Roberts family | SROIQ | 3 866 | 2 118 ms | 38 769 ms | 37 903 ms | ~18× |
209
- | LUBM schema + data | SHI | 100 850 | 1 017 ms | 1 672 ms | — | ~1.6× |
594
+ | LUBM schema + data | SHI | 100 850 | 1 017 ms | 1 672 ms | 1 965 ms | ~1.6× |
210
595
 
211
596
  ¹ Native pipeline (OWL 2 XML parse + classify/realize). Native uses `classification` for
212
597
  TBox-only ontologies and `realization` for ontologies with individuals (Roberts family,
@@ -214,14 +599,42 @@ LUBM + data) — matching WASM's operation selection. LUBM schema ratio is domin
214
599
  fixed WASM startup cost (pthreads pool init) on a tiny 307-triple ontology.
215
600
 
216
601
  ² WASM timing covers binary buffer encode (Quads → buffer) + `loadTripleBuffer` + realization
217
- + decode. Input RDF is pre-parsed into quads before the timing window — NTriples/Turtle parsing
218
- is excluded, matching what your application pays after data is already loaded into a Store.
219
- Node.js 20 and Chromium 135 are within ~12% of each other on most ontologies; Roberts family
220
- (SROIQ with ABox realization) is effectively equal. LUBM schema + data not measured in browser
221
- (17 MB fixture).
602
+
603
+ - decode. Input RDF is pre-parsed into quads before the timing window — NTriples/Turtle parsing
604
+ is excluded, matching what your application pays after data is already loaded into a Store.
605
+ Node.js 20 and Chromium 135 are within ~12% of each other on most ontologies; Roberts family
606
+ (SROIQ with ABox realization) is effectively equal.
222
607
 
223
608
  Run `npm run bench` to reproduce (requires a built WASM binary — see [Build from source](#build-from-source)).
224
609
 
610
+ ### What each fixture tests
611
+
612
+ **LUBM schema** (SHI, TBox-only) — a shallow university-domain ontology: 49 classes, 25 object
613
+ properties, 36 subclass edges, one transitive property. No individuals. Konclude runs pure
614
+ classification; actual tableau work is trivial. The 207 ms is almost entirely pthread pool
615
+ startup on a 307-triple ontology.
616
+
617
+ **GALEN** (SHIF, TBox-only) — a medical terminology ontology: 4 740 classes, 413 object
618
+ properties, 150 functional properties, 26 transitive properties, and 3 446 existential
619
+ restrictions (`someValuesFrom`) cross-connected to 3 237 subclass edges. No individuals.
620
+ The dense restriction graph drives TBox saturation — constraints propagate across thousands
621
+ of interleaved concept/role pairs. SHIF adds functional property reasoning on top. This is
622
+ pure classification load.
623
+
624
+ **Roberts family** (SROIQ, TBox + ABox) — a genealogy ontology: 171 classes, 80 object
625
+ properties, 405 named individuals, 11 symmetric properties, 8 transitive properties, and
626
+ 24 property chain axioms (`owl:propertyChainAxiom`). SROIQ is the full OWL 2 DL
627
+ expressiveness. The 405 individuals trigger ABox realization — Konclude computes the type
628
+ of every individual under every applicable concept while propagating role chains across the
629
+ family tree. Role chains require joining property paths, which multiplies the search space.
630
+ This is why 3 866 triples takes 38 s.
631
+
632
+ **LUBM schema + data** (SHI, TBox + ABox) — the same shallow TBox combined with ~25 000
633
+ individuals (students, professors, courses across multiple universities). SHI has no property
634
+ chains or nominals, so ABox realization is type propagation only: each individual is
635
+ classified under the existing concept hierarchy. Cost scales roughly linearly with individual
636
+ count rather than combinatorially, hence the 1.6× native ratio despite 25 000 instances.
637
+
225
638
  ## How it works
226
639
 
227
640
  ```text