@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
@@ -0,0 +1,438 @@
1
+ // Copyright (c) 2026 Modelware. All rights reserved.
2
+
3
+ // Memory-lean reasoning store (the default OwlStore implementation).
4
+ //
5
+ // Rationale: the n3 Store builds full S/P/O/G index Maps + Quad/Term object wrappers over every
6
+ // quad (~1.3 KB/quad → ~1 GB on mass-props). The reasoners don't need general SPARQL indexing of
7
+ // the canonical store (oxigraph owns querying via the mirror); they need per-graph scans, bound
8
+ // pattern lookups over small graphs (TBox/SHACL), membership, and add/remove. LeanQuadStore stores
9
+ // quads as interned uint32 term-id columns with per-graph + per-predicate id lists (~130 B/quad),
10
+ // and exposes the n3-Store API subset the code actually calls — so it's a drop-in for the store
11
+ // behind ReasoningStore, ~8–9x smaller and faster on those ops (benchmarked).
12
+ //
13
+ // There are no per-model Quad arrays: the ABox chainer reads scope facts via forEachAssertedFact/
14
+ // forEachEntailedFact, which yield shared interned Term instances + a per-row cached key straight
15
+ // from the columns — so a model's facts are re-read across dependents (~21x) without materializing
16
+ // or retaining Quad objects, and without a separate factOf memo.
17
+
18
+ import { DataFactory } from 'n3';
19
+ import type { NamedNode, Quad, Store, Term } from 'n3';
20
+ import type { ModelDiff, ModelGraphs } from './owl-store.js';
21
+ import type { OwlStore, ScopeFactVisitor } from './owl-interfaces.js';
22
+
23
+ const { namedNode, blankNode, literal, quad, defaultGraph } = DataFactory;
24
+ const XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string';
25
+
26
+ // --- LeanQuadStore: interned-column quad store exposing the n3-Store API subset we use ----------
27
+
28
+ const NULL_ID = -1; // wildcard (term not bound in a query)
29
+ const ABSENT_ID = -2; // bound term that was never interned -> matches nothing
30
+
31
+ class LeanQuadStore {
32
+ private readonly termToId = new Map<string, number>();
33
+ private readonly idToKey: string[] = [];
34
+ // Packed term-id columns; row index === quad id.
35
+ private readonly cs: number[] = [];
36
+ private readonly cp: number[] = [];
37
+ private readonly co: number[] = [];
38
+ private readonly cg: number[] = [];
39
+ private readonly graphRows = new Map<number, number[]>();
40
+ private readonly predRows = new Map<number, number[]>();
41
+ private readonly dead = new Set<number>();
42
+ // Caches for the chainer's scope-fact path: one shared Term instance per term id, and the dedup
43
+ // key (`${s.id}|${p.id}|${o.id}`) per row — computed once, reused across the ~21x re-reads.
44
+ private readonly idToTerm: (Term | undefined)[] = [];
45
+ private readonly rowKey: (string | undefined)[] = [];
46
+
47
+ private keyOfTerm(t: Term): string {
48
+ switch (t.termType) {
49
+ case 'NamedNode': return 'N\x00' + t.value;
50
+ case 'BlankNode': return 'B\x00' + t.value;
51
+ case 'DefaultGraph': return 'D';
52
+ default: {
53
+ const lit = t as { value: string; datatype?: { value: string }; language?: string };
54
+ const dt = lit.datatype ? lit.datatype.value : XSD_STRING;
55
+ return 'L\x00' + lit.value + '\x00' + dt + '\x00' + (lit.language || '');
56
+ }
57
+ }
58
+ }
59
+
60
+ private intern(key: string): number {
61
+ let id = this.termToId.get(key);
62
+ if (id === undefined) {
63
+ id = this.idToKey.length;
64
+ this.termToId.set(key, id);
65
+ this.idToKey.push(key);
66
+ }
67
+ return id;
68
+ }
69
+
70
+ // -1 for null/undefined (wildcard); -2 for a bound term that isn't interned (matches nothing).
71
+ private idOf(t: Term | null | undefined): number {
72
+ if (t === null || t === undefined) {
73
+ return NULL_ID;
74
+ }
75
+ const id = this.termToId.get(this.keyOfTerm(t));
76
+ return id === undefined ? ABSENT_ID : id;
77
+ }
78
+
79
+ private termAt(id: number): Term {
80
+ const k = this.idToKey[id];
81
+ const kind = k.charCodeAt(0);
82
+ if (kind === 78 /* N */) return namedNode(k.slice(2));
83
+ if (kind === 66 /* B */) return blankNode(k.slice(2));
84
+ if (kind === 68 /* D */) return defaultGraph();
85
+ const parts = k.slice(2).split('\x00');
86
+ const value = parts[0], dt = parts[1], lang = parts[2];
87
+ if (lang) return literal(value, lang);
88
+ if (dt && dt !== XSD_STRING) return literal(value, namedNode(dt));
89
+ return literal(value);
90
+ }
91
+
92
+ private materialize(row: number): Quad {
93
+ // Casts are safe: subjects are NamedNode/BlankNode, predicates NamedNode, objects any term,
94
+ // graphs NamedNode/DefaultGraph — guaranteed by how quads are inserted.
95
+ return quad(
96
+ this.termAt(this.cs[row]) as Quad['subject'],
97
+ this.termAt(this.cp[row]) as Quad['predicate'],
98
+ this.termAt(this.co[row]) as Quad['object'],
99
+ this.termAt(this.cg[row]) as Quad['graph'],
100
+ );
101
+ }
102
+
103
+ private addRow(si: number, pi: number, oi: number, gi: number): number {
104
+ const row = this.cs.length;
105
+ this.cs.push(si); this.cp.push(pi); this.co.push(oi); this.cg.push(gi);
106
+ let gl = this.graphRows.get(gi); if (!gl) { gl = []; this.graphRows.set(gi, gl); } gl.push(row);
107
+ let pl = this.predRows.get(pi); if (!pl) { pl = []; this.predRows.set(pi, pl); } pl.push(row);
108
+ return row;
109
+ }
110
+
111
+ private sharedTermAt(id: number): Term {
112
+ let t = this.idToTerm[id];
113
+ if (!t) { t = this.termAt(id); this.idToTerm[id] = t; }
114
+ return t;
115
+ }
116
+
117
+ private keyAt(row: number): string {
118
+ let k = this.rowKey[row];
119
+ if (k === undefined) {
120
+ k = `${this.sharedTermAt(this.cs[row]).id}|${this.sharedTermAt(this.cp[row]).id}|${this.sharedTermAt(this.co[row]).id}`;
121
+ this.rowKey[row] = k;
122
+ }
123
+ return k;
124
+ }
125
+
126
+ isDead(row: number): boolean {
127
+ return this.dead.has(row);
128
+ }
129
+
130
+ private visitFact(row: number, visit: (s: Term, p: NamedNode, o: Term, key: string) => void): void {
131
+ visit(this.sharedTermAt(this.cs[row]), this.sharedTermAt(this.cp[row]) as NamedNode, this.sharedTermAt(this.co[row]), this.keyAt(row));
132
+ }
133
+
134
+ // Scope-fact iteration for the ABox chainer: shared terms + cached keys, no Quad materialization.
135
+ forEachFactInGraph(graph: Term, visit: (s: Term, p: NamedNode, o: Term, key: string) => void): void {
136
+ const gid = this.idOf(graph);
137
+ if (gid < 0) return;
138
+ const rows = this.graphRows.get(gid);
139
+ if (!rows) return;
140
+ for (const r of rows) {
141
+ if (!this.dead.has(r)) this.visitFact(r, visit);
142
+ }
143
+ }
144
+
145
+ forEachFactInRows(rows: Iterable<number>, visit: (s: Term, p: NamedNode, o: Term, key: string) => void): void {
146
+ for (const r of rows) {
147
+ if (!this.dead.has(r)) this.visitFact(r, visit);
148
+ }
149
+ }
150
+
151
+ // Iterate rows matching the (possibly wildcard) pattern, skipping tombstoned rows.
152
+ private forEachMatch(si: number, pi: number, oi: number, gi: number, visit: (row: number) => void): void {
153
+ if (si === ABSENT_ID || pi === ABSENT_ID || oi === ABSENT_ID || gi === ABSENT_ID) {
154
+ return;
155
+ }
156
+ // Pick the most selective available index.
157
+ let candidates: number[] | undefined;
158
+ if (gi !== NULL_ID) candidates = this.graphRows.get(gi);
159
+ else if (pi !== NULL_ID) candidates = this.predRows.get(pi);
160
+ if (candidates) {
161
+ for (let i = 0; i < candidates.length; i++) {
162
+ const r = candidates[i];
163
+ if (this.dead.has(r)) continue;
164
+ if (si !== NULL_ID && this.cs[r] !== si) continue;
165
+ if (pi !== NULL_ID && this.cp[r] !== pi) continue;
166
+ if (oi !== NULL_ID && this.co[r] !== oi) continue;
167
+ if (gi !== NULL_ID && this.cg[r] !== gi) continue;
168
+ visit(r);
169
+ }
170
+ return;
171
+ }
172
+ // Full scan (no graph or predicate bound).
173
+ const n = this.cs.length;
174
+ for (let r = 0; r < n; r++) {
175
+ if (this.dead.has(r)) continue;
176
+ if (si !== NULL_ID && this.cs[r] !== si) continue;
177
+ if (pi !== NULL_ID && this.cp[r] !== pi) continue;
178
+ if (oi !== NULL_ID && this.co[r] !== oi) continue;
179
+ visit(r);
180
+ }
181
+ }
182
+
183
+ // --- n3-Store-compatible API (the subset used across the codebase) -----------------------
184
+
185
+ // Returns the new row id (used by LeanReasoningStore to track per-model entailment rows).
186
+ addQuad(subjectOrQuad: Quad | Term, predicate?: Term, object?: Term, graph?: Term): number {
187
+ let s: Term, p: Term, o: Term, g: Term;
188
+ if (predicate === undefined) {
189
+ const q = subjectOrQuad as Quad;
190
+ s = q.subject; p = q.predicate; o = q.object; g = q.graph;
191
+ } else {
192
+ s = subjectOrQuad as Term; p = predicate; o = object as Term; g = (graph ?? defaultGraph()) as Term;
193
+ }
194
+ return this.addRow(this.intern(this.keyOfTerm(s)), this.intern(this.keyOfTerm(p)), this.intern(this.keyOfTerm(o)), this.intern(this.keyOfTerm(g)));
195
+ }
196
+
197
+ addQuads(quads: Quad[]): void {
198
+ for (const q of quads) this.addQuad(q.subject, q.predicate, q.object, q.graph);
199
+ }
200
+
201
+ removeQuad(subjectOrQuad: Quad | Term, predicate?: Term, object?: Term, graph?: Term): void {
202
+ let s: Term, p: Term, o: Term, g: Term;
203
+ if (predicate === undefined) {
204
+ const q = subjectOrQuad as Quad;
205
+ s = q.subject; p = q.predicate; o = q.object; g = q.graph;
206
+ } else {
207
+ s = subjectOrQuad as Term; p = predicate; o = object as Term; g = (graph ?? defaultGraph()) as Term;
208
+ }
209
+ const si = this.idOf(s), pi = this.idOf(p), oi = this.idOf(o), gi = this.idOf(g);
210
+ this.forEachMatch(si, pi, oi, gi, (row) => { this.tombstone(row, gi); });
211
+ }
212
+
213
+ removeQuads(quads: Quad[]): void {
214
+ for (const q of quads) this.removeQuad(q.subject, q.predicate, q.object, q.graph);
215
+ }
216
+
217
+ private tombstone(row: number, gi: number): void {
218
+ if (this.dead.has(row)) return;
219
+ this.dead.add(row);
220
+ // Keep the graph list compact (graph removal is the common bulk case); predRows is filtered lazily via `dead`.
221
+ const gl = this.graphRows.get(gi >= 0 ? gi : this.cg[row]);
222
+ if (gl) {
223
+ const idx = gl.indexOf(row);
224
+ if (idx >= 0) gl.splice(idx, 1);
225
+ }
226
+ }
227
+
228
+ getQuads(subject: Term | null, predicate: Term | null, object: Term | null, graph: Term | null): Quad[] {
229
+ const out: Quad[] = [];
230
+ this.forEachMatch(this.idOf(subject), this.idOf(predicate), this.idOf(object), this.idOf(graph), (row) => {
231
+ out.push(this.materialize(row));
232
+ });
233
+ return out;
234
+ }
235
+
236
+ countQuads(subject: Term | null, predicate: Term | null, object: Term | null, graph: Term | null): number {
237
+ let count = 0;
238
+ this.forEachMatch(this.idOf(subject), this.idOf(predicate), this.idOf(object), this.idOf(graph), () => { count += 1; });
239
+ return count;
240
+ }
241
+
242
+ has(subjectOrQuad: Quad | Term, predicate?: Term, object?: Term, graph?: Term): boolean {
243
+ let s: Term, p: Term, o: Term, g: Term;
244
+ if (predicate === undefined) {
245
+ const q = subjectOrQuad as Quad;
246
+ s = q.subject; p = q.predicate; o = q.object; g = q.graph;
247
+ } else {
248
+ s = subjectOrQuad as Term; p = predicate; o = object as Term; g = (graph ?? defaultGraph()) as Term;
249
+ }
250
+ let found = false;
251
+ this.forEachMatch(this.idOf(s), this.idOf(p), this.idOf(o), this.idOf(g), () => { found = true; });
252
+ return found;
253
+ }
254
+ }
255
+
256
+ // --- LeanReasoningStore: OwlStore over LeanQuadStore (mirrors ReasoningStore exactly) -----------
257
+
258
+ export class LeanReasoningStore implements OwlStore {
259
+ private readonly store = new LeanQuadStore();
260
+ private readonly modelGraphs = new Map<string, ModelGraphs>();
261
+ // Asserted facts live in the model's own graph (read straight from the store's columns, no
262
+ // per-model array). Entailed facts written via addEntailment/loadEntailments are tracked here
263
+ // by row id so forEachEntailedFact returns ONLY them (not TBox quads that the
264
+ // TBox chainer writes directly into the entailments graph via getStore().addQuads).
265
+ private readonly entailedRows = new Map<string, number[]>();
266
+ private readonly modelDataGen = new Map<string, number>();
267
+
268
+ private bumpDataGen(modelUri: string): void {
269
+ this.modelDataGen.set(modelUri, (this.modelDataGen.get(modelUri) ?? 0) + 1);
270
+ }
271
+
272
+ getModelDataGen(modelUri: string): number {
273
+ return this.modelDataGen.get(modelUri) ?? 0;
274
+ }
275
+
276
+ loadModel(modelUri: string, quads: Quad[]): void {
277
+ const existingGraphs = this.modelGraphs.get(modelUri);
278
+ const graphs = existingGraphs ?? this.graphsFromModelUri(modelUri);
279
+ if (existingGraphs) {
280
+ this.retractModel(modelUri);
281
+ }
282
+ this.modelGraphs.set(modelUri, graphs);
283
+ this.entailedRows.delete(modelUri);
284
+ for (const q of quads) {
285
+ this.store.addQuad(q.subject, q.predicate, q.object, graphs.own);
286
+ }
287
+ this.bumpDataGen(modelUri);
288
+ }
289
+
290
+ loadEntailments(modelUri: string, quads: Quad[]): void {
291
+ const graphs = this.requireGraphs(modelUri);
292
+ this.removeGraph(graphs.entailments);
293
+ this.bumpDataGen(modelUri);
294
+ if (quads.length === 0) {
295
+ this.entailedRows.set(modelUri, []);
296
+ return;
297
+ }
298
+ const rows: number[] = [];
299
+ for (const q of quads) {
300
+ rows.push(this.store.addQuad(q.subject, q.predicate, q.object, graphs.entailments));
301
+ }
302
+ this.entailedRows.set(modelUri, rows);
303
+ }
304
+
305
+ addEntailment(modelUri: string, q: Quad): void {
306
+ const graphs = this.requireGraphs(modelUri);
307
+ const row = this.store.addQuad(q.subject, q.predicate, q.object, graphs.entailments);
308
+ const rows = this.entailedRows.get(modelUri) ?? [];
309
+ rows.push(row);
310
+ this.entailedRows.set(modelUri, rows);
311
+ this.bumpDataGen(modelUri);
312
+ }
313
+
314
+ removeEntailment(modelUri: string, q: Quad): void {
315
+ const graphs = this.requireGraphs(modelUri);
316
+ this.store.removeQuad(quad(q.subject, q.predicate, q.object, graphs.entailments));
317
+ const rows = this.entailedRows.get(modelUri);
318
+ if (rows) {
319
+ this.entailedRows.set(modelUri, rows.filter((r) => !this.store.isDead(r)));
320
+ }
321
+ this.bumpDataGen(modelUri);
322
+ }
323
+
324
+ applyModelDelta(modelUri: string, quads: Quad[], retracted: Quad[], asserted: Quad[]): void {
325
+ const graphs = this.resolveGraphs(modelUri);
326
+ this.modelGraphs.set(modelUri, graphs);
327
+ if (retracted.length > 0) {
328
+ this.store.removeQuads(retracted.map((q) => this.withGraph(q, graphs.own)));
329
+ }
330
+ if (asserted.length > 0) {
331
+ for (const q of asserted) {
332
+ this.store.addQuad(q.subject, q.predicate, q.object, graphs.own);
333
+ }
334
+ }
335
+ this.bumpDataGen(modelUri);
336
+ }
337
+
338
+ retractModel(modelUri: string): void {
339
+ const graphs = this.modelGraphs.get(modelUri);
340
+ if (!graphs) return;
341
+ this.removeGraph(graphs.own);
342
+ this.removeGraph(graphs.entailments);
343
+ this.removeGraph(graphs.shaclRules);
344
+ this.modelGraphs.delete(modelUri);
345
+ this.entailedRows.delete(modelUri);
346
+ this.bumpDataGen(modelUri);
347
+ }
348
+
349
+ diffModel(modelUri: string, newQuads: Quad[]): ModelDiff {
350
+ const graphs = this.resolveGraphs(modelUri);
351
+ const existing = this.store.getQuads(null, null, null, graphs.own);
352
+ const next = newQuads.map((q) => this.withGraph(q, graphs.own));
353
+
354
+ const existingByKey = new Map(existing.map((q) => [this.quadKey(q), q]));
355
+ const nextByKey = new Map(next.map((q) => [this.quadKey(q), q]));
356
+
357
+ const retracted = [...existingByKey.entries()]
358
+ .filter(([key]) => !nextByKey.has(key))
359
+ .map(([, value]) => value)
360
+ .sort((a, b) => this.quadKey(a).localeCompare(this.quadKey(b)));
361
+
362
+ const asserted = [...nextByKey.entries()]
363
+ .filter(([key]) => !existingByKey.has(key))
364
+ .map(([, value]) => value)
365
+ .sort((a, b) => this.quadKey(a).localeCompare(this.quadKey(b)));
366
+
367
+ return {
368
+ isEmpty: retracted.length === 0 && asserted.length === 0,
369
+ retracted,
370
+ asserted,
371
+ };
372
+ }
373
+
374
+ clearEntailments(modelUri: string): void {
375
+ const graphs = this.requireGraphs(modelUri);
376
+ this.removeGraph(graphs.entailments);
377
+ this.entailedRows.set(modelUri, []);
378
+ this.bumpDataGen(modelUri);
379
+ }
380
+
381
+ graphs(modelUri: string): ModelGraphs {
382
+ return this.requireGraphs(modelUri);
383
+ }
384
+
385
+ getStore(): Store {
386
+ return this.store as unknown as Store;
387
+ }
388
+
389
+ forEachAssertedFact(modelUri: string, visit: ScopeFactVisitor): void {
390
+ const graphs = this.modelGraphs.get(modelUri);
391
+ if (!graphs) return;
392
+ this.store.forEachFactInGraph(graphs.own, visit);
393
+ }
394
+
395
+ forEachEntailedFact(modelUri: string, visit: ScopeFactVisitor): void {
396
+ const rows = this.entailedRows.get(modelUri);
397
+ if (!rows) return;
398
+ this.store.forEachFactInRows(rows, visit);
399
+ }
400
+
401
+ private resolveGraphs(modelUri: string): ModelGraphs {
402
+ const existing = this.modelGraphs.get(modelUri);
403
+ if (existing) {
404
+ return existing;
405
+ }
406
+ const graphs = this.graphsFromModelUri(modelUri);
407
+ this.modelGraphs.set(modelUri, graphs);
408
+ return graphs;
409
+ }
410
+
411
+ private graphsFromModelUri(modelUri: string): ModelGraphs {
412
+ return {
413
+ own: namedNode(modelUri),
414
+ entailments: namedNode(`${modelUri}__entailments`),
415
+ shaclRules: namedNode(`${modelUri}__shacl_rules`),
416
+ };
417
+ }
418
+
419
+ private requireGraphs(modelUri: string): ModelGraphs {
420
+ const graphs = this.modelGraphs.get(modelUri);
421
+ if (!graphs) {
422
+ throw new Error(`Unknown model '${modelUri}'. Load the model first.`);
423
+ }
424
+ return graphs;
425
+ }
426
+
427
+ private withGraph(q: Quad, graph: NamedNode): Quad {
428
+ return quad(q.subject, q.predicate, q.object, graph);
429
+ }
430
+
431
+ private removeGraph(graph: NamedNode): void {
432
+ this.store.removeQuads(this.store.getQuads(null, null, null, graph));
433
+ }
434
+
435
+ private quadKey(q: Quad): string {
436
+ return `${q.subject.id}|${q.predicate.id}|${q.object.id}|${q.graph.id}`;
437
+ }
438
+ }
@@ -1,7 +1,7 @@
1
1
  // Copyright (c) 2026 Modelware. All rights reserved.
2
2
 
3
3
  import { DataFactory, Store } from 'n3';
4
- import type { NamedNode, Quad } from 'n3';
4
+ import type { NamedNode, Quad, Term } from 'n3';
5
5
 
6
6
  const { namedNode, quad } = DataFactory;
7
7
 
@@ -14,40 +14,96 @@ export interface ModelDiff {
14
14
  export interface ModelGraphs {
15
15
  own: NamedNode;
16
16
  entailments: NamedNode;
17
+ shaclRules: NamedNode;
17
18
  }
18
19
 
19
20
  export class ReasoningStore {
20
21
  private readonly store = new Store();
21
22
  private readonly modelGraphs = new Map<string, ModelGraphs>();
23
+ private readonly assertedQuadsByModel = new Map<string, Quad[]>();
24
+ private readonly entailedQuadsByModel = new Map<string, Quad[]>();
25
+ // Monotonic per-model data version, bumped on every mutation of a model's asserted or entailed
26
+ // quads. Consumers (e.g. the Oxigraph mirror) compare it to detect exactly which models changed,
27
+ // without re-reading the data — so an edit re-syncs only the models that actually changed.
28
+ private readonly modelDataGen = new Map<string, number>();
29
+
30
+ private bumpDataGen(modelUri: string): void {
31
+ this.modelDataGen.set(modelUri, (this.modelDataGen.get(modelUri) ?? 0) + 1);
32
+ }
33
+
34
+ getModelDataGen(modelUri: string): number {
35
+ return this.modelDataGen.get(modelUri) ?? 0;
36
+ }
22
37
 
23
38
  // Load a model's quads into its named graph.
24
39
  // Retracts any existing quads for that model first.
25
40
  // Graph IRI is derived from the model URI to keep model snapshots isolated.
26
41
  loadModel(modelUri: string, quads: Quad[]): void {
27
- const graphs = this.resolveGraphs(modelUri);
28
- this.retractModel(modelUri);
42
+ const existingGraphs = this.modelGraphs.get(modelUri);
43
+ const graphs = existingGraphs ?? this.graphsFromModelUri(modelUri);
44
+ if (existingGraphs) {
45
+ this.retractModel(modelUri);
46
+ }
29
47
  this.modelGraphs.set(modelUri, graphs);
30
- this.store.addQuads(quads.map((q) => this.withGraph(q, graphs.own)));
48
+ this.assertedQuadsByModel.set(modelUri, [...quads]);
49
+ this.entailedQuadsByModel.delete(modelUri);
50
+ for (const q of quads) {
51
+ this.store.addQuad(q.subject, q.predicate, q.object, graphs.own);
52
+ }
53
+ this.bumpDataGen(modelUri);
31
54
  }
32
55
 
33
56
  loadEntailments(modelUri: string, quads: Quad[]): void {
34
57
  const graphs = this.requireGraphs(modelUri);
35
58
  this.removeGraph(graphs.entailments);
59
+ this.bumpDataGen(modelUri);
36
60
  if (quads.length === 0) {
61
+ this.entailedQuadsByModel.set(modelUri, []);
37
62
  return;
38
63
  }
39
- this.store.addQuads(quads.map((q) => this.withGraph(q, graphs.entailments)));
64
+ const entailed: Quad[] = [];
65
+ for (const q of quads) {
66
+ const entailedQuad = quad(q.subject, q.predicate, q.object, graphs.entailments);
67
+ entailed.push(entailedQuad);
68
+ this.store.addQuad(entailedQuad);
69
+ }
70
+ this.entailedQuadsByModel.set(modelUri, entailed);
71
+ }
72
+
73
+ addEntailment(modelUri: string, q: Quad): void {
74
+ const graphs = this.requireGraphs(modelUri);
75
+ const entailedQuad = quad(q.subject, q.predicate, q.object, graphs.entailments);
76
+ const entailed = this.entailedQuadsByModel.get(modelUri) ?? [];
77
+ entailed.push(entailedQuad);
78
+ this.entailedQuadsByModel.set(modelUri, entailed);
79
+ this.store.addQuad(entailedQuad);
80
+ this.bumpDataGen(modelUri);
81
+ }
82
+
83
+ removeEntailment(modelUri: string, q: Quad): void {
84
+ const graphs = this.requireGraphs(modelUri);
85
+ const entailedQuad = quad(q.subject, q.predicate, q.object, graphs.entailments);
86
+ const key = this.quadKey(entailedQuad);
87
+ const entailed = this.entailedQuadsByModel.get(modelUri) ?? [];
88
+ const next = entailed.filter((entry) => this.quadKey(entry) !== key);
89
+ this.entailedQuadsByModel.set(modelUri, next);
90
+ this.store.removeQuad(entailedQuad);
91
+ this.bumpDataGen(modelUri);
40
92
  }
41
93
 
42
94
  applyModelDelta(modelUri: string, quads: Quad[], retracted: Quad[], asserted: Quad[]): void {
43
95
  const graphs = this.resolveGraphs(modelUri);
44
96
  this.modelGraphs.set(modelUri, graphs);
97
+ this.assertedQuadsByModel.set(modelUri, [...quads]);
45
98
  if (retracted.length > 0) {
46
99
  this.store.removeQuads(retracted.map((q) => this.withGraph(q, graphs.own)));
47
100
  }
48
101
  if (asserted.length > 0) {
49
- this.store.addQuads(asserted.map((q) => this.withGraph(q, graphs.own)));
102
+ for (const q of asserted) {
103
+ this.store.addQuad(q.subject, q.predicate, q.object, graphs.own);
104
+ }
50
105
  }
106
+ this.bumpDataGen(modelUri);
51
107
  }
52
108
 
53
109
  // Retract all quads in all named graphs for a model
@@ -57,7 +113,11 @@ export class ReasoningStore {
57
113
  if (!graphs) return;
58
114
  this.removeGraph(graphs.own);
59
115
  this.removeGraph(graphs.entailments);
116
+ this.removeGraph(graphs.shaclRules);
60
117
  this.modelGraphs.delete(modelUri);
118
+ this.assertedQuadsByModel.delete(modelUri);
119
+ this.entailedQuadsByModel.delete(modelUri);
120
+ this.bumpDataGen(modelUri);
61
121
  }
62
122
 
63
123
  // Compare newly mapped quads against what is currently in the store.
@@ -93,6 +153,8 @@ export class ReasoningStore {
93
153
  clearEntailments(modelUri: string): void {
94
154
  const graphs = this.requireGraphs(modelUri);
95
155
  this.removeGraph(graphs.entailments);
156
+ this.entailedQuadsByModel.set(modelUri, []);
157
+ this.bumpDataGen(modelUri);
96
158
  }
97
159
 
98
160
  // Returns the named graph IRIs for a model.
@@ -108,6 +170,22 @@ export class ReasoningStore {
108
170
  return this.store;
109
171
  }
110
172
 
173
+ forEachAssertedFact(modelUri: string, visit: (subject: Term, predicate: NamedNode, object: Term, key: string) => void): void {
174
+ const quads = this.assertedQuadsByModel.get(modelUri);
175
+ if (!quads) return;
176
+ for (const q of quads) {
177
+ visit(q.subject, q.predicate as NamedNode, q.object, `${q.subject.id}|${q.predicate.id}|${q.object.id}`);
178
+ }
179
+ }
180
+
181
+ forEachEntailedFact(modelUri: string, visit: (subject: Term, predicate: NamedNode, object: Term, key: string) => void): void {
182
+ const quads = this.entailedQuadsByModel.get(modelUri);
183
+ if (!quads) return;
184
+ for (const q of quads) {
185
+ visit(q.subject, q.predicate as NamedNode, q.object, `${q.subject.id}|${q.predicate.id}|${q.object.id}`);
186
+ }
187
+ }
188
+
111
189
  private resolveGraphs(modelUri: string): ModelGraphs {
112
190
  const existing = this.modelGraphs.get(modelUri);
113
191
  if (existing) {
@@ -121,7 +199,8 @@ export class ReasoningStore {
121
199
  private graphsFromModelUri(modelUri: string): ModelGraphs {
122
200
  return {
123
201
  own: namedNode(modelUri),
124
- entailments: namedNode(`${modelUri}__entailments`)
202
+ entailments: namedNode(`${modelUri}__entailments`),
203
+ shaclRules: namedNode(`${modelUri}__shacl_rules`),
125
204
  };
126
205
  }
127
206