@oml/owl 0.19.2 → 0.20.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.
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
@@ -30,12 +30,15 @@ export type DerivedShaclSelectQuery = {
30
30
  containmentPredicateIri?: string;
31
31
  containmentIsInverse?: boolean;
32
32
  containmentIsReadOnly?: boolean;
33
- supplementalQueries?: Array<{ columnLabel: string; boundVariable: string; sparql: string }>;
34
33
  /**
35
- * SHACL `sh:rule [a sh:SPARQLRule]` entries whose CONSTRUCT predicate is NOT bound to a column.
36
- * These exist to enrich the data graph for downstream queries (including other rules).
37
- * Executed in `sh:order` order before the table SELECT runs; produced quads are inserted into
38
- * a scratch graph for the duration of the query, then removed.
34
+ * Hidden SELECT variable that is "true" when the focus node has at least one writable-shape
35
+ * type assertion in the editable graph. Used to distinguish ref-ed vs unref-ed foreign rows.
36
+ */
37
+ isRefedVariable?: string;
38
+ /**
39
+ * SHACL `sh:rule [a sh:SPARQLRule]` entries, executed in `sh:order` before the SELECT query.
40
+ * Each rule runs as a SPARQL CONSTRUCT with all prior rules' quads already visible in the store,
41
+ * so later rules can depend on earlier ones. All quads stay injected for the SELECT, then removed.
39
42
  */
40
43
  materializationRules?: Array<{ order: number; constructSparql: string }>;
41
44
  };
@@ -50,10 +53,6 @@ type ParsedShaclColumn = {
50
53
  };
51
54
 
52
55
  type ParsedShaclRule = {
53
- predicateIri: string;
54
- boundVariable: string;
55
- whereBody: string;
56
- prefixDeclarations: string;
57
56
  constructSparql: string;
58
57
  order?: number;
59
58
  };
@@ -63,6 +62,7 @@ type ParsedShaclNodeShape = {
63
62
  targetNodes: Term[];
64
63
  columns: ParsedShaclColumn[];
65
64
  rules: ParsedShaclRule[];
65
+ isReadOnly: boolean;
66
66
  };
67
67
 
68
68
  type ConnectionLike = {
@@ -237,10 +237,6 @@ export function deriveSelectQueryFromShacl(shaclSource: string, graphIri: string
237
237
  }
238
238
 
239
239
  const allRules = nodeShapes.flatMap((s) => s.rules);
240
- const ruleByPredicateIri = new Map<string, ParsedShaclRule>();
241
- for (const rule of allRules) {
242
- ruleByPredicateIri.set(rule.predicateIri, rule);
243
- }
244
240
 
245
241
  const allTargetClasses = nodeShapes.flatMap((shape) => shape.targetClasses);
246
242
  const allTargetNodes = nodeShapes.flatMap((shape) => shape.targetNodes);
@@ -286,7 +282,7 @@ export function deriveSelectQueryFromShacl(shaclSource: string, graphIri: string
286
282
  return a.insertionIndex - b.insertionIndex;
287
283
  });
288
284
 
289
- const usedNames = new Set<string>(['focus', '__rowType', '__specificType']);
285
+ const usedNames = new Set<string>(['this', '__rowType', '__specificType']);
290
286
  type ShapeVariable = { path: string; readOnly: boolean; variable: string; sourceVariable: string; name?: string; ownerClasses: Term[]; isComposite?: boolean };
291
287
  const allVariables: ShapeVariable[] = aggregatedColumns.map((col) => {
292
288
  const base = normalizeSparqlVariableName(col.name || col.pathVariableHint || 'value');
@@ -300,9 +296,12 @@ export function deriveSelectQueryFromShacl(shaclSource: string, graphIri: string
300
296
  const graphPattern = (pattern: string): string =>
301
297
  `GRAPH <${escapeIriForSparql(graphIri)}> { ${pattern} }`;
302
298
 
299
+ const writableClassesForRefed = nodeShapes.filter((s) => !s.isReadOnly).flatMap((s) => s.targetClasses);
300
+ const hasRefedColumn = writableClassesForRefed.length > 0 && nodeShapes.some((s) => s.isReadOnly);
303
301
  const selectVars = [
304
- '?focus',
302
+ '?this',
305
303
  '(GROUP_CONCAT(DISTINCT STR(?__specificType); SEPARATOR=", ") AS ?__rowType)',
304
+ ...(hasRefedColumn ? ['(MAX(IF(BOUND(?__refedType), "true", "")) AS ?__isRefed)'] : []),
306
305
  ...allVariables.map((entry) =>
307
306
  `(GROUP_CONCAT(DISTINCT STR(?${entry.sourceVariable}); SEPARATOR=", ") AS ?${entry.variable})`
308
307
  ),
@@ -312,67 +311,60 @@ export function deriveSelectQueryFromShacl(shaclSource: string, graphIri: string
312
311
  const graphLines: string[] = [];
313
312
 
314
313
  if (allTargetClasses.length > 0) {
315
- const classValuesList = allTargetClasses.map((term: Term) => `<${term.value}>`);
316
- const classValuesSpaced = classValuesList.join(' ');
317
- const classValuesComma = classValuesList.join(', ');
318
- whereLines.push(` VALUES ?__targetClass { ${classValuesSpaced} }`);
319
- graphLines.push(` ${graphPattern('?focus a ?__targetClass .')}`);
320
- graphLines.push(` OPTIONAL { ${graphPattern(`?focus a ?__specificType . FILTER(?__specificType IN (${classValuesComma}))`)} }`);
314
+ const readOnlyClasses = nodeShapes.filter((s) => s.isReadOnly).flatMap((s) => s.targetClasses);
315
+ const writableClasses = nodeShapes.filter((s) => !s.isReadOnly).flatMap((s) => s.targetClasses);
316
+ const allClassesComma = allTargetClasses.map((t: Term) => `<${t.value}>`).join(', ');
317
+
318
+ if (readOnlyClasses.length > 0 && writableClasses.length > 0) {
319
+ // Mixed: readOnly shapes query across all graphs, writable shapes restricted to editable graph
320
+ const roValues = readOnlyClasses.map((t) => `<${t.value}>`).join(' ');
321
+ const wValues = writableClasses.map((t) => `<${t.value}>`).join(' ');
322
+ whereLines.push(
323
+ ` { VALUES ?__c { ${roValues} } . ?this a ?__c . }`,
324
+ ` UNION`,
325
+ ` { VALUES ?__c { ${wValues} } . ${graphPattern('?this a ?__c .')} }`
326
+ );
327
+ } else if (readOnlyClasses.length > 0) {
328
+ whereLines.push(` VALUES ?__targetClass { ${readOnlyClasses.map((t) => `<${t.value}>`).join(' ')} }`);
329
+ whereLines.push(` ?this a ?__targetClass .`);
330
+ } else {
331
+ whereLines.push(` VALUES ?__targetClass { ${writableClasses.map((t) => `<${t.value}>`).join(' ')} }`);
332
+ graphLines.push(` ${graphPattern('?this a ?__targetClass .')}`);
333
+ }
334
+ // specificType is always cross-graph (display only, not write-scoped)
335
+ graphLines.push(` OPTIONAL { ?this a ?__specificType . FILTER(?__specificType IN (${allClassesComma})) }`);
336
+ if (hasRefedColumn) {
337
+ const writableComma = writableClassesForRefed.map((t) => `<${t.value}>`).join(', ');
338
+ graphLines.push(` OPTIONAL { ${graphPattern(`?this a ?__refedType . FILTER(?__refedType IN (${writableComma}))`)} }`);
339
+ }
321
340
  }
322
341
 
323
342
  if (allTargetNodes.length > 0) {
324
343
  const nodeValues = allTargetNodes.map((term: Term) => termToSparql(term)).join(' ');
325
- whereLines.push(` VALUES ?focus { ${nodeValues} }`);
344
+ whereLines.push(` VALUES ?this { ${nodeValues} }`);
326
345
  }
327
346
 
328
- const supplementalQueries: Array<{ columnLabel: string; boundVariable: string; sparql: string }> = [];
329
- const consumedRulePredicates = new Set<string>();
330
347
  for (const entry of allVariables) {
331
348
  if (entry.readOnly) {
332
- const simpleIri = /^<([^>]+)>$/.exec(entry.path.trim())?.[1];
333
- const rule = simpleIri ? ruleByPredicateIri.get(simpleIri) : undefined;
334
- if (rule) {
335
- consumedRulePredicates.add(rule.predicateIri);
336
- graphLines.push([
337
- ` OPTIONAL {`,
338
- ` SELECT ?focus (?${rule.boundVariable} AS ?${entry.sourceVariable})`,
339
- ` WHERE {`,
340
- ` ${rule.whereBody}`,
341
- ` }`,
342
- ` }`,
343
- ].join('\n'));
344
- supplementalQueries.push({
345
- columnLabel: entry.name ?? entry.variable,
346
- boundVariable: rule.boundVariable,
347
- sparql: [
348
- rule.prefixDeclarations,
349
- `SELECT ?focus ?${rule.boundVariable}`,
350
- 'WHERE {',
351
- ` ${rule.whereBody}`,
352
- '}',
353
- ].filter(Boolean).join('\n'),
354
- });
355
- } else {
356
- graphLines.push(` OPTIONAL { ?focus ${entry.path} ?${entry.sourceVariable} . }`);
357
- }
349
+ graphLines.push(` OPTIONAL { ?this ${entry.path} ?${entry.sourceVariable} . }`);
358
350
  } else {
359
351
  const isUniversal = allTargetClasses.length === 0 ||
360
352
  allTargetClasses.every((tc) => entry.ownerClasses.some((oc) => oc.value === tc.value));
361
353
  if (isUniversal) {
362
- graphLines.push(` OPTIONAL { ${graphPattern(`?focus ${entry.path} ?${entry.sourceVariable} .`)} }`);
354
+ graphLines.push(` OPTIONAL { ${graphPattern(`?this ${entry.path} ?${entry.sourceVariable} .`)} }`);
363
355
  } else if (entry.ownerClasses.length === 1) {
364
- graphLines.push(` OPTIONAL { ${graphPattern(`?focus a <${entry.ownerClasses[0].value}> . ?focus ${entry.path} ?${entry.sourceVariable} .`)} }`);
356
+ graphLines.push(` OPTIONAL { ${graphPattern(`?this a <${entry.ownerClasses[0].value}> . ?this ${entry.path} ?${entry.sourceVariable} .`)} }`);
365
357
  } else {
366
358
  const guardVar = `__g_${entry.variable}`;
367
359
  const classesComma = entry.ownerClasses.map((t: Term) => `<${t.value}>`).join(', ');
368
- graphLines.push(` OPTIONAL { ${graphPattern(`?focus a ?${guardVar} . FILTER(?${guardVar} IN (${classesComma})) . ?focus ${entry.path} ?${entry.sourceVariable} .`)} }`);
360
+ graphLines.push(` OPTIONAL { ${graphPattern(`?this a ?${guardVar} . FILTER(?${guardVar} IN (${classesComma})) . ?this ${entry.path} ?${entry.sourceVariable} .`)} }`);
369
361
  }
370
362
  }
371
363
  }
372
364
  whereLines.push(...graphLines);
373
365
 
374
- const columnVariables = ['focus', ...allVariables.map((entry) => entry.variable)];
375
- const columnLabels = new Map<string, string>([['focus', 'focus'], ['__rowType', '__rowType']]);
366
+ const columnVariables = ['this', ...allVariables.map((entry) => entry.variable)];
367
+ const columnLabels = new Map<string, string>([['this', 'this'], ['__rowType', '__rowType']]);
376
368
  for (const entry of allVariables) {
377
369
  columnLabels.set(entry.variable, entry.name || entry.variable);
378
370
  }
@@ -388,30 +380,24 @@ export function deriveSelectQueryFromShacl(shaclSource: string, graphIri: string
388
380
  const containmentIsInverse: true | undefined = inverseMatch ? true : undefined;
389
381
  const containmentIsReadOnly: true | undefined = compositeVar?.readOnly ? true : undefined;
390
382
 
391
- const rulePrefixLines = [...new Set(
392
- allRules.flatMap((r) => r.prefixDeclarations ? r.prefixDeclarations.split('\n') : [])
393
- )].filter(Boolean);
394
-
395
383
  return {
396
384
  sparql: [
397
- ...rulePrefixLines,
398
385
  `SELECT ${selectVars}`,
399
386
  'WHERE {',
400
387
  ...whereLines,
401
388
  '}',
402
- 'GROUP BY ?focus',
389
+ 'GROUP BY ?this',
403
390
  ].join('\n'),
404
391
  columnVariables,
405
392
  columnLabels,
406
393
  rowTypeVariable: '__rowType',
394
+ isRefedVariable: hasRefedColumn ? '__isRefed' : undefined,
407
395
  containmentLabels,
408
396
  containmentPredicateIri,
409
397
  containmentIsInverse,
410
398
  containmentIsReadOnly,
411
- supplementalQueries: supplementalQueries.length > 0 ? supplementalQueries : undefined,
412
399
  materializationRules: (() => {
413
400
  const mats = allRules
414
- .filter((r) => !consumedRulePredicates.has(r.predicateIri))
415
401
  .map((r, idx) => ({ order: r.order ?? idx, constructSparql: r.constructSparql }));
416
402
  return mats.length > 0
417
403
  ? mats.sort((a, b) => a.order - b.order)
@@ -455,6 +441,7 @@ function parseAllNodeShapes(shapeQuads: readonly Quad[]): ParsedShaclNodeShape[]
455
441
  continue;
456
442
  }
457
443
 
444
+ const shapeIsReadOnly = isReadOnlyShape(shapesStore, shapeTerm);
458
445
  const columns = getObjects(shapesStore, shapeTerm, SH_PROPERTY)
459
446
  .filter((term): term is Term => term.termType === 'NamedNode' || term.termType === 'BlankNode')
460
447
  .filter((term) => !isDeactivatedShape(shapesStore, term))
@@ -479,7 +466,7 @@ function parseAllNodeShapes(shapeQuads: readonly Quad[]): ParsedShaclNodeShape[]
479
466
  return {
480
467
  path: parsedPath.sparql,
481
468
  pathVariableHint: parsedPath.variableHint,
482
- readOnly: isReadOnlyShape(shapesStore, term),
469
+ readOnly: shapeIsReadOnly || isReadOnlyShape(shapesStore, term),
483
470
  name: nameTerm && nameTerm.termType === 'Literal' ? nameTerm.value : undefined,
484
471
  isComposite: isCompositeShape(shapesStore, term),
485
472
  order,
@@ -488,82 +475,18 @@ function parseAllNodeShapes(shapeQuads: readonly Quad[]): ParsedShaclNodeShape[]
488
475
  .filter((column): column is ParsedShaclColumn => Boolean(column));
489
476
 
490
477
  const rules = parseShaclRules(shapesStore, shapeTerm);
491
- shapes.push({ targetClasses, targetNodes, columns, rules });
478
+ shapes.push({ targetClasses, targetNodes, columns, rules, isReadOnly: shapeIsReadOnly });
492
479
  }
493
480
 
494
481
  return shapes;
495
482
  }
496
483
 
497
484
  function parseSparqlConstructRule(constructQuery: string): ParsedShaclRule | undefined {
498
- // Build prefix map from PREFIX declarations
499
- const prefixMap = new Map<string, string>();
500
- const prefixRegex = /PREFIX\s+(\w*):\s*<([^>]+)>/gi;
501
- let prefixMatch: RegExpExecArray | null;
502
- while ((prefixMatch = prefixRegex.exec(constructQuery)) !== null) {
503
- prefixMap.set(prefixMatch[1], prefixMatch[2]);
504
- }
505
-
506
- // Match CONSTRUCT { $this <predicate> ?variable }
507
- const constructHeadRegex = /CONSTRUCT\s*\{[^}]*\$this\s+(\S+)\s+\?(\w+)/i;
508
- const headMatch = constructHeadRegex.exec(constructQuery);
509
- if (!headMatch) {
510
- return undefined;
511
- }
512
- const rawPredicate = headMatch[1];
513
- const boundVariable = headMatch[2];
514
-
515
- // Resolve predicate IRI
516
- let predicateIri: string;
517
- const angleMatch = /^<([^>]+)>$/.exec(rawPredicate);
518
- if (angleMatch) {
519
- predicateIri = angleMatch[1];
520
- } else {
521
- const prefixedMatch = /^(\w*):(.+)$/.exec(rawPredicate);
522
- if (!prefixedMatch) {
523
- return undefined;
524
- }
525
- const ns = prefixMap.get(prefixedMatch[1]);
526
- if (ns === undefined) {
527
- return undefined;
528
- }
529
- predicateIri = ns + prefixedMatch[2];
530
- }
531
-
532
- // Find WHERE { ... } using brace-depth counter
533
- const whereKeywordMatch = /WHERE\s*\{/i.exec(constructQuery);
534
- if (!whereKeywordMatch) {
485
+ // Only accept rules whose CONSTRUCT head follows the $this <predicate> ?variable pattern.
486
+ if (!/CONSTRUCT\s*\{[^}]*\$this\s+\S+\s+\?\w+/i.test(constructQuery)) {
535
487
  return undefined;
536
488
  }
537
- const whereOpenBrace = whereKeywordMatch.index + whereKeywordMatch[0].length - 1;
538
- let depth = 0;
539
- let start = -1;
540
- let end = -1;
541
- for (let i = whereOpenBrace; i < constructQuery.length; i++) {
542
- if (constructQuery[i] === '{') {
543
- if (depth === 0) {
544
- start = i;
545
- }
546
- depth++;
547
- } else if (constructQuery[i] === '}') {
548
- depth--;
549
- if (depth === 0) {
550
- end = i;
551
- break;
552
- }
553
- }
554
- }
555
- if (start === -1 || end === -1) {
556
- return undefined;
557
- }
558
-
559
- const whereBody = constructQuery.slice(start + 1, end).replace(/\$this/g, '?focus').trim();
560
-
561
- // Reconstruct PREFIX declarations as SPARQL lines to inject into the outer query
562
- const prefixDeclarations = [...prefixMap.entries()]
563
- .map(([prefix, ns]) => `PREFIX ${prefix}: <${ns}>`)
564
- .join('\n');
565
-
566
- return { predicateIri, boundVariable, whereBody, prefixDeclarations, constructSparql: constructQuery };
489
+ return { constructSparql: constructQuery };
567
490
  }
568
491
 
569
492
  function parseShaclRules(store: N3Store, shapeTerm: Term): ParsedShaclRule[] {
@@ -0,0 +1,34 @@
1
+ // Copyright (c) 2026 Modelware. All rights reserved.
2
+
3
+ // Single construction point for the SPARQL engine. Oxigraph (Rust/WASM) is the only engine: it is
4
+ // far faster on cross-graph queries (`GRAPH ?g` over a large import closure) and is used in Node and
5
+ // in the browser/worker (which bundles oxigraph's web build and inits its wasm — see
6
+ // browser-server.ts). The N3 reasoning store is mirrored into oxigraph for querying.
7
+
8
+ import type {
9
+ OwlABoxEntailmentState,
10
+ OwlImportGraph,
11
+ OwlInferenceRunner,
12
+ OwlSparqlEngine,
13
+ OwlStore,
14
+ } from './owl-interfaces.js';
15
+ import { OxigraphSparqlService } from './owl-sparql-oxigraph.js';
16
+
17
+ export type SparqlEngineKind = 'oxigraph';
18
+
19
+ export const DEFAULT_SPARQL_ENGINE: SparqlEngineKind = 'oxigraph';
20
+
21
+ export function resolveSparqlEngineKind(): SparqlEngineKind {
22
+ return 'oxigraph';
23
+ }
24
+
25
+ /** Construct the SPARQL engine. The only place that knows about the concrete engine. */
26
+ export function createSparqlEngine(
27
+ _kind: SparqlEngineKind,
28
+ reasoningStore: OwlStore,
29
+ importGraph: OwlImportGraph,
30
+ aboxEntailmentCache: OwlABoxEntailmentState,
31
+ reasoningService: OwlInferenceRunner,
32
+ ): OwlSparqlEngine {
33
+ return new OxigraphSparqlService(reasoningStore, importGraph, aboxEntailmentCache, reasoningService);
34
+ }