@oml/cli 0.7.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 +75 -0
- package/bin/cli.js +6 -0
- package/out/auth.d.ts +25 -0
- package/out/auth.js +253 -0
- package/out/auth.js.map +1 -0
- package/out/backend/backend-types.d.ts +19 -0
- package/out/backend/backend-types.js +3 -0
- package/out/backend/backend-types.js.map +1 -0
- package/out/backend/create-backend.d.ts +2 -0
- package/out/backend/create-backend.js +6 -0
- package/out/backend/create-backend.js.map +1 -0
- package/out/backend/direct-backend.d.ts +17 -0
- package/out/backend/direct-backend.js +97 -0
- package/out/backend/direct-backend.js.map +1 -0
- package/out/backend/reasoned-output.d.ts +38 -0
- package/out/backend/reasoned-output.js +568 -0
- package/out/backend/reasoned-output.js.map +1 -0
- package/out/cli.d.ts +1 -0
- package/out/cli.js +132 -0
- package/out/cli.js.map +1 -0
- package/out/commands/closure.d.ts +33 -0
- package/out/commands/closure.js +537 -0
- package/out/commands/closure.js.map +1 -0
- package/out/commands/compile.d.ts +11 -0
- package/out/commands/compile.js +63 -0
- package/out/commands/compile.js.map +1 -0
- package/out/commands/lint.d.ts +5 -0
- package/out/commands/lint.js +31 -0
- package/out/commands/lint.js.map +1 -0
- package/out/commands/reason.d.ts +13 -0
- package/out/commands/reason.js +62 -0
- package/out/commands/reason.js.map +1 -0
- package/out/commands/render.d.ts +15 -0
- package/out/commands/render.js +753 -0
- package/out/commands/render.js.map +1 -0
- package/out/commands/validate.d.ts +5 -0
- package/out/commands/validate.js +186 -0
- package/out/commands/validate.js.map +1 -0
- package/out/main.d.ts +1 -0
- package/out/main.js +4 -0
- package/out/main.js.map +1 -0
- package/out/update.d.ts +1 -0
- package/out/update.js +79 -0
- package/out/update.js.map +1 -0
- package/out/util.d.ts +10 -0
- package/out/util.js +63 -0
- package/out/util.js.map +1 -0
- package/package.json +36 -0
- package/src/auth.ts +315 -0
- package/src/backend/backend-types.ts +25 -0
- package/src/backend/create-backend.ts +8 -0
- package/src/backend/direct-backend.ts +114 -0
- package/src/backend/reasoned-output.ts +697 -0
- package/src/cli.ts +147 -0
- package/src/commands/closure.ts +624 -0
- package/src/commands/compile.ts +88 -0
- package/src/commands/lint.ts +35 -0
- package/src/commands/reason.ts +79 -0
- package/src/commands/render.ts +1021 -0
- package/src/commands/validate.ts +226 -0
- package/src/main.ts +5 -0
- package/src/update.ts +103 -0
- package/src/util.ts +83 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import type { Quad } from '@rdfjs/types';
|
|
4
|
+
import { DataFactory } from 'n3';
|
|
5
|
+
import type { Concept, Ontology, RelationEntity, Vocabulary, VocabularyBundle } from '@oml/language';
|
|
6
|
+
import { isConcept, isRelationEntity, isVocabulary, isVocabularyBundle } from '@oml/language';
|
|
7
|
+
|
|
8
|
+
const RDF_TYPE_IRI = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
9
|
+
const RDF_FIRST_IRI = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first';
|
|
10
|
+
const RDF_REST_IRI = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest';
|
|
11
|
+
const RDF_NIL_IRI = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil';
|
|
12
|
+
const RDFS_SUBCLASS_OF_IRI = 'http://www.w3.org/2000/01/rdf-schema#subClassOf';
|
|
13
|
+
const OWL_CLASS_IRI = 'http://www.w3.org/2002/07/owl#Class';
|
|
14
|
+
const OWL_THING_IRI = 'http://www.w3.org/2002/07/owl#Thing';
|
|
15
|
+
const OWL_NOTHING_IRI = 'http://www.w3.org/2002/07/owl#Nothing';
|
|
16
|
+
const OWL_COMPLEMENT_OF_IRI = 'http://www.w3.org/2002/07/owl#complementOf';
|
|
17
|
+
const OWL_INTERSECTION_OF_IRI = 'http://www.w3.org/2002/07/owl#intersectionOf';
|
|
18
|
+
const OWL_UNION_OF_IRI = 'http://www.w3.org/2002/07/owl#unionOf';
|
|
19
|
+
const OWL_DISJOINT_WITH_IRI = 'http://www.w3.org/2002/07/owl#disjointWith';
|
|
20
|
+
const OWL_ALL_DISJOINT_CLASSES_IRI = 'http://www.w3.org/2002/07/owl#AllDisjointClasses';
|
|
21
|
+
const OWL_MEMBERS_IRI = 'http://www.w3.org/2002/07/owl#members';
|
|
22
|
+
|
|
23
|
+
const { blankNode, namedNode, quad } = DataFactory;
|
|
24
|
+
|
|
25
|
+
type ClosureMember = Concept | RelationEntity;
|
|
26
|
+
|
|
27
|
+
type ClassExpression =
|
|
28
|
+
| { kind: 'named'; iri: string; key: string }
|
|
29
|
+
| { kind: 'universal'; key: string }
|
|
30
|
+
| { kind: 'empty'; key: string }
|
|
31
|
+
| { kind: 'complement'; operand: ClassExpression; key: string }
|
|
32
|
+
| { kind: 'intersection'; operands: ClassExpression[]; key: string }
|
|
33
|
+
| { kind: 'union'; operands: ClassExpression[]; key: string };
|
|
34
|
+
|
|
35
|
+
interface Edge {
|
|
36
|
+
parent: string;
|
|
37
|
+
child: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface TaxonomyNode {
|
|
41
|
+
expr: ClassExpression;
|
|
42
|
+
parents: Set<string>;
|
|
43
|
+
children: Set<string>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type ClosureAxiom =
|
|
47
|
+
| { kind: 'subclass'; sub: ClassExpression; sup: ClassExpression }
|
|
48
|
+
| { kind: 'disjoint'; expressions: ClassExpression[] };
|
|
49
|
+
|
|
50
|
+
const UNIVERSAL: ClassExpression = { kind: 'universal', key: 'U' };
|
|
51
|
+
const EMPTY: ClassExpression = { kind: 'empty', key: 'E' };
|
|
52
|
+
|
|
53
|
+
export class VocabularyBundleClosureBuilder {
|
|
54
|
+
constructor(private readonly ontologyByNamespace: ReadonlyMap<string, Ontology>) {}
|
|
55
|
+
|
|
56
|
+
buildClosureQuads(bundle: VocabularyBundle): Quad[] {
|
|
57
|
+
const vocabularies = this.collectBundledVocabularies(bundle);
|
|
58
|
+
const members = this.collectClosureMembers(vocabularies);
|
|
59
|
+
if (members.length < 2) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const memberIris = new Set(members.map((member) => this.memberIri(member)));
|
|
64
|
+
const vertices = members
|
|
65
|
+
.map((member) => this.named(this.memberIri(member)))
|
|
66
|
+
.sort((left, right) => left.key.localeCompare(right.key));
|
|
67
|
+
const edges = this.transitiveReduce(this.collectNamedEdges(members, memberIris));
|
|
68
|
+
const rooted = this.rootAtUniversal(this.createTaxonomy(vertices, edges));
|
|
69
|
+
const tree = this.treeify(rooted);
|
|
70
|
+
const axioms = this.generateClosureAxioms(tree);
|
|
71
|
+
return this.serializeAxioms(axioms);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private collectBundledVocabularies(bundle: VocabularyBundle): Vocabulary[] {
|
|
75
|
+
const vocabularies = new Map<string, Vocabulary>();
|
|
76
|
+
const visitedBundles = new Set<string>();
|
|
77
|
+
const visitedVocabularies = new Set<string>();
|
|
78
|
+
|
|
79
|
+
const visitVocabulary = (vocabulary: Vocabulary): void => {
|
|
80
|
+
const iri = normalizeNamespace(vocabulary.namespace);
|
|
81
|
+
if (visitedVocabularies.has(iri)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
visitedVocabularies.add(iri);
|
|
85
|
+
vocabularies.set(iri, vocabulary);
|
|
86
|
+
for (const ownedImport of vocabulary.ownedImports ?? []) {
|
|
87
|
+
if (ownedImport.kind !== 'extends') {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const imported = ownedImport.imported?.ref;
|
|
91
|
+
if (imported && isVocabulary(imported)) {
|
|
92
|
+
visitVocabulary(imported);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const visitBundle = (vocabularyBundle: VocabularyBundle): void => {
|
|
98
|
+
const iri = normalizeNamespace(vocabularyBundle.namespace);
|
|
99
|
+
if (visitedBundles.has(iri)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
visitedBundles.add(iri);
|
|
103
|
+
for (const ownedImport of vocabularyBundle.ownedImports ?? []) {
|
|
104
|
+
const imported = ownedImport.imported?.ref;
|
|
105
|
+
if (!imported) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (ownedImport.kind === 'includes' && isVocabulary(imported)) {
|
|
109
|
+
visitVocabulary(imported);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (ownedImport.kind === 'extends' && isVocabularyBundle(imported)) {
|
|
113
|
+
visitBundle(imported);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const importedNamespace = this.importedOntologyNamespace(imported);
|
|
117
|
+
if (!importedNamespace) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const resolved = this.ontologyByNamespace.get(importedNamespace);
|
|
121
|
+
if (!resolved) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (ownedImport.kind === 'includes' && isVocabulary(resolved)) {
|
|
125
|
+
visitVocabulary(resolved);
|
|
126
|
+
} else if (ownedImport.kind === 'extends' && isVocabularyBundle(resolved)) {
|
|
127
|
+
visitBundle(resolved);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
visitBundle(bundle);
|
|
133
|
+
return [...vocabularies.values()];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private collectClosureMembers(vocabularies: ReadonlyArray<Vocabulary>): ClosureMember[] {
|
|
137
|
+
const members = new Map<string, ClosureMember>();
|
|
138
|
+
for (const vocabulary of vocabularies) {
|
|
139
|
+
for (const statement of vocabulary.ownedStatements ?? []) {
|
|
140
|
+
if (!isConcept(statement) && !isRelationEntity(statement)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
members.set(this.memberIri(statement), statement);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return [...members.values()];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private collectNamedEdges(
|
|
150
|
+
members: ReadonlyArray<ClosureMember>,
|
|
151
|
+
memberIris: ReadonlySet<string>,
|
|
152
|
+
): Edge[] {
|
|
153
|
+
const edges = new Map<string, Edge>();
|
|
154
|
+
for (const member of members) {
|
|
155
|
+
const childIri = this.memberIri(member);
|
|
156
|
+
for (const specialization of member.ownedSpecializations ?? []) {
|
|
157
|
+
const superTerm = specialization.superTerm?.ref;
|
|
158
|
+
if (!superTerm || (!isConcept(superTerm) && !isRelationEntity(superTerm))) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const parentIri = this.memberIri(superTerm);
|
|
162
|
+
if (!memberIris.has(parentIri)) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
edges.set(`${parentIri}|${childIri}`, { parent: parentIri, child: childIri });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return [...edges.values()].sort((left, right) =>
|
|
169
|
+
`${left.parent}|${left.child}`.localeCompare(`${right.parent}|${right.child}`),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private transitiveReduce(edges: ReadonlyArray<Edge>): Edge[] {
|
|
174
|
+
const vertices = new Set<string>();
|
|
175
|
+
const childrenByParent = new Map<string, Set<string>>();
|
|
176
|
+
for (const edge of edges) {
|
|
177
|
+
vertices.add(edge.parent);
|
|
178
|
+
vertices.add(edge.child);
|
|
179
|
+
const children = childrenByParent.get(edge.parent) ?? new Set<string>();
|
|
180
|
+
children.add(edge.child);
|
|
181
|
+
childrenByParent.set(edge.parent, children);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const hasAlternatePath = (parent: string, child: string): boolean => {
|
|
185
|
+
const visited = new Set<string>([parent]);
|
|
186
|
+
const worklist = [...(childrenByParent.get(parent) ?? [])].filter((candidate) => candidate !== child);
|
|
187
|
+
while (worklist.length > 0) {
|
|
188
|
+
const next = worklist.pop()!;
|
|
189
|
+
if (next === child) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
if (visited.has(next)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
visited.add(next);
|
|
196
|
+
worklist.push(...(childrenByParent.get(next) ?? []));
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return edges.filter((edge) => !hasAlternatePath(edge.parent, edge.child));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private rootAtUniversal(taxonomy: Map<string, TaxonomyNode>): Map<string, TaxonomyNode> {
|
|
205
|
+
const rooted = this.cloneTaxonomy(taxonomy);
|
|
206
|
+
this.ensureNode(rooted, UNIVERSAL);
|
|
207
|
+
const roots = [...rooted.values()]
|
|
208
|
+
.filter((node) => node.expr.kind !== 'universal' && node.parents.size === 0)
|
|
209
|
+
.map((node) => node.expr);
|
|
210
|
+
for (const root of roots) {
|
|
211
|
+
this.addEdge(rooted, UNIVERSAL, root);
|
|
212
|
+
}
|
|
213
|
+
return rooted;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private treeify(taxonomy: Map<string, TaxonomyNode>): Map<string, TaxonomyNode> {
|
|
217
|
+
const child = this.lowestMultiParentChild(taxonomy);
|
|
218
|
+
if (!child) {
|
|
219
|
+
return taxonomy;
|
|
220
|
+
}
|
|
221
|
+
return this.treeify(this.reduceChild(this.bypassIsolate(taxonomy, child), child));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private bypassIsolate(taxonomy: Map<string, TaxonomyNode>, child: ClassExpression): Map<string, TaxonomyNode> {
|
|
225
|
+
const next = new Map<string, TaxonomyNode>();
|
|
226
|
+
const parents = this.parentsOf(taxonomy, child);
|
|
227
|
+
const parentKeys = new Set(parents.map((parent) => parent.key));
|
|
228
|
+
const grandparents = new Map<string, ClassExpression>();
|
|
229
|
+
for (const parent of parents) {
|
|
230
|
+
for (const grandparent of this.parentsOf(taxonomy, parent)) {
|
|
231
|
+
grandparents.set(grandparent.key, grandparent);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const replacements = new Map<string, ClassExpression>();
|
|
235
|
+
for (const parent of parents) {
|
|
236
|
+
replacements.set(parent.key, this.difference(parent, child));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (const node of taxonomy.values()) {
|
|
240
|
+
if (!parentKeys.has(node.expr.key)) {
|
|
241
|
+
this.ensureNode(next, node.expr);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
for (const replacement of replacements.values()) {
|
|
245
|
+
this.ensureNode(next, replacement);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (const node of taxonomy.values()) {
|
|
249
|
+
for (const childKey of node.children) {
|
|
250
|
+
const source = node.expr;
|
|
251
|
+
const target = taxonomy.get(childKey)?.expr;
|
|
252
|
+
if (!target) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (parentKeys.has(source.key)) {
|
|
256
|
+
if (target.key !== child.key) {
|
|
257
|
+
this.addEdge(next, replacements.get(source.key)!, target);
|
|
258
|
+
}
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (parentKeys.has(target.key)) {
|
|
262
|
+
this.addEdge(next, source, replacements.get(target.key)!);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
this.addEdge(next, source, target);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (const grandparent of grandparents.values()) {
|
|
270
|
+
this.addEdge(next, grandparent, child);
|
|
271
|
+
}
|
|
272
|
+
return next;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private reduceChild(taxonomy: Map<string, TaxonomyNode>, child: ClassExpression): Map<string, TaxonomyNode> {
|
|
276
|
+
const next = new Map<string, TaxonomyNode>();
|
|
277
|
+
for (const node of taxonomy.values()) {
|
|
278
|
+
this.ensureNode(next, node.expr);
|
|
279
|
+
}
|
|
280
|
+
for (const node of taxonomy.values()) {
|
|
281
|
+
for (const childKey of node.children) {
|
|
282
|
+
const target = taxonomy.get(childKey)?.expr;
|
|
283
|
+
if (!target || target.key === child.key) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
this.addEdge(next, node.expr, target);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
for (const parent of this.directParentsOf(taxonomy, child)) {
|
|
290
|
+
this.addEdge(next, parent, child);
|
|
291
|
+
}
|
|
292
|
+
return next;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private generateClosureAxioms(taxonomy: Map<string, TaxonomyNode>): ClosureAxiom[] {
|
|
296
|
+
const axioms = new Map<string, ClosureAxiom>();
|
|
297
|
+
const orderedParents = [...taxonomy.values()]
|
|
298
|
+
.filter((node) => node.children.size > 0)
|
|
299
|
+
.map((node) => node.expr)
|
|
300
|
+
.sort((left, right) => left.key.localeCompare(right.key));
|
|
301
|
+
|
|
302
|
+
for (const parent of orderedParents) {
|
|
303
|
+
const children = this.childrenOf(taxonomy, parent)
|
|
304
|
+
.sort((left, right) => left.key.localeCompare(right.key));
|
|
305
|
+
for (const child of children) {
|
|
306
|
+
axioms.set(
|
|
307
|
+
`subclass|${child.key}|${parent.key}`,
|
|
308
|
+
{ kind: 'subclass', sub: child, sup: parent },
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
if (children.length > 1) {
|
|
312
|
+
axioms.set(
|
|
313
|
+
`disjoint|${children.map((child) => child.key).join('|')}`,
|
|
314
|
+
{ kind: 'disjoint', expressions: children },
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return [...axioms.values()];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private serializeAxioms(axioms: ReadonlyArray<ClosureAxiom>): Quad[] {
|
|
322
|
+
const quads: Quad[] = [];
|
|
323
|
+
const expressionNodes = new Map<string, ReturnType<typeof namedNode> | ReturnType<typeof blankNode>>();
|
|
324
|
+
const listNodes = new Map<string, ReturnType<typeof blankNode> | ReturnType<typeof namedNode>>();
|
|
325
|
+
|
|
326
|
+
const termForExpression = (expr: ClassExpression): ReturnType<typeof namedNode> | ReturnType<typeof blankNode> => {
|
|
327
|
+
const cached = expressionNodes.get(expr.key);
|
|
328
|
+
if (cached) {
|
|
329
|
+
return cached;
|
|
330
|
+
}
|
|
331
|
+
if (expr.kind === 'named') {
|
|
332
|
+
const node = namedNode(expr.iri);
|
|
333
|
+
expressionNodes.set(expr.key, node);
|
|
334
|
+
return node;
|
|
335
|
+
}
|
|
336
|
+
if (expr.kind === 'universal') {
|
|
337
|
+
const node = namedNode(OWL_THING_IRI);
|
|
338
|
+
expressionNodes.set(expr.key, node);
|
|
339
|
+
return node;
|
|
340
|
+
}
|
|
341
|
+
if (expr.kind === 'empty') {
|
|
342
|
+
const node = namedNode(OWL_NOTHING_IRI);
|
|
343
|
+
expressionNodes.set(expr.key, node);
|
|
344
|
+
return node;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const node = blankNode();
|
|
348
|
+
expressionNodes.set(expr.key, node);
|
|
349
|
+
quads.push(quad(node, namedNode(RDF_TYPE_IRI), namedNode(OWL_CLASS_IRI)));
|
|
350
|
+
if (expr.kind === 'complement') {
|
|
351
|
+
quads.push(quad(node, namedNode(OWL_COMPLEMENT_OF_IRI), termForExpression(expr.operand)));
|
|
352
|
+
return node;
|
|
353
|
+
}
|
|
354
|
+
const predicate = expr.kind === 'intersection' ? OWL_INTERSECTION_OF_IRI : OWL_UNION_OF_IRI;
|
|
355
|
+
quads.push(quad(node, namedNode(predicate), listForExpressions(expr.operands)));
|
|
356
|
+
return node;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const listForExpressions = (expressions: ReadonlyArray<ClassExpression>) => {
|
|
360
|
+
const key = expressions.map((expr) => expr.key).join('|');
|
|
361
|
+
const cached = listNodes.get(key);
|
|
362
|
+
if (cached) {
|
|
363
|
+
return cached;
|
|
364
|
+
}
|
|
365
|
+
if (expressions.length === 0) {
|
|
366
|
+
return namedNode(RDF_NIL_IRI);
|
|
367
|
+
}
|
|
368
|
+
const head = blankNode();
|
|
369
|
+
listNodes.set(key, head);
|
|
370
|
+
let current = head;
|
|
371
|
+
expressions.forEach((expr, index) => {
|
|
372
|
+
quads.push(quad(current, namedNode(RDF_FIRST_IRI), termForExpression(expr)));
|
|
373
|
+
const isLast = index === expressions.length - 1;
|
|
374
|
+
const restNode = isLast ? namedNode(RDF_NIL_IRI) : blankNode();
|
|
375
|
+
quads.push(quad(current, namedNode(RDF_REST_IRI), restNode));
|
|
376
|
+
if (!isLast && restNode.termType === 'BlankNode') {
|
|
377
|
+
current = restNode;
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
return head;
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
for (const axiom of axioms) {
|
|
384
|
+
if (axiom.kind === 'subclass') {
|
|
385
|
+
quads.push(quad(termForExpression(axiom.sub), namedNode(RDFS_SUBCLASS_OF_IRI), termForExpression(axiom.sup)));
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (axiom.expressions.length === 2) {
|
|
389
|
+
quads.push(quad(
|
|
390
|
+
termForExpression(axiom.expressions[0]),
|
|
391
|
+
namedNode(OWL_DISJOINT_WITH_IRI),
|
|
392
|
+
termForExpression(axiom.expressions[1]),
|
|
393
|
+
));
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const node = blankNode();
|
|
397
|
+
quads.push(quad(node, namedNode(RDF_TYPE_IRI), namedNode(OWL_ALL_DISJOINT_CLASSES_IRI)));
|
|
398
|
+
quads.push(quad(node, namedNode(OWL_MEMBERS_IRI), listForExpressions(axiom.expressions)));
|
|
399
|
+
}
|
|
400
|
+
return quads;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private createTaxonomy(vertices: ReadonlyArray<ClassExpression>, edges: ReadonlyArray<Edge>): Map<string, TaxonomyNode> {
|
|
404
|
+
const taxonomy = new Map<string, TaxonomyNode>();
|
|
405
|
+
for (const vertex of vertices) {
|
|
406
|
+
this.ensureNode(taxonomy, vertex);
|
|
407
|
+
}
|
|
408
|
+
for (const edge of edges) {
|
|
409
|
+
this.addEdge(taxonomy, this.named(edge.parent), this.named(edge.child));
|
|
410
|
+
}
|
|
411
|
+
return taxonomy;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private cloneTaxonomy(taxonomy: Map<string, TaxonomyNode>): Map<string, TaxonomyNode> {
|
|
415
|
+
const clone = new Map<string, TaxonomyNode>();
|
|
416
|
+
for (const node of taxonomy.values()) {
|
|
417
|
+
clone.set(node.expr.key, {
|
|
418
|
+
expr: node.expr,
|
|
419
|
+
parents: new Set(node.parents),
|
|
420
|
+
children: new Set(node.children),
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
return clone;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private ensureNode(taxonomy: Map<string, TaxonomyNode>, expr: ClassExpression): TaxonomyNode {
|
|
427
|
+
const existing = taxonomy.get(expr.key);
|
|
428
|
+
if (existing) {
|
|
429
|
+
return existing;
|
|
430
|
+
}
|
|
431
|
+
const created: TaxonomyNode = {
|
|
432
|
+
expr,
|
|
433
|
+
parents: new Set<string>(),
|
|
434
|
+
children: new Set<string>(),
|
|
435
|
+
};
|
|
436
|
+
taxonomy.set(expr.key, created);
|
|
437
|
+
return created;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private addEdge(taxonomy: Map<string, TaxonomyNode>, parent: ClassExpression, child: ClassExpression): void {
|
|
441
|
+
if (parent.key === child.key) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const parentNode = this.ensureNode(taxonomy, parent);
|
|
445
|
+
const childNode = this.ensureNode(taxonomy, child);
|
|
446
|
+
parentNode.children.add(child.key);
|
|
447
|
+
childNode.parents.add(parent.key);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private childrenOf(taxonomy: Map<string, TaxonomyNode>, expr: ClassExpression): ClassExpression[] {
|
|
451
|
+
const node = taxonomy.get(expr.key);
|
|
452
|
+
if (!node) {
|
|
453
|
+
return [];
|
|
454
|
+
}
|
|
455
|
+
return [...node.children]
|
|
456
|
+
.map((key) => taxonomy.get(key)?.expr)
|
|
457
|
+
.filter((entry): entry is ClassExpression => !!entry);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private parentsOf(taxonomy: Map<string, TaxonomyNode>, expr: ClassExpression): ClassExpression[] {
|
|
461
|
+
const node = taxonomy.get(expr.key);
|
|
462
|
+
if (!node) {
|
|
463
|
+
return [];
|
|
464
|
+
}
|
|
465
|
+
return [...node.parents]
|
|
466
|
+
.map((key) => taxonomy.get(key)?.expr)
|
|
467
|
+
.filter((entry): entry is ClassExpression => !!entry);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private ancestorsOf(taxonomy: Map<string, TaxonomyNode>, expr: ClassExpression): Set<string> {
|
|
471
|
+
const ancestors = new Set<string>();
|
|
472
|
+
const worklist = this.parentsOf(taxonomy, expr);
|
|
473
|
+
while (worklist.length > 0) {
|
|
474
|
+
const next = worklist.pop()!;
|
|
475
|
+
if (ancestors.has(next.key)) {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
ancestors.add(next.key);
|
|
479
|
+
worklist.push(...this.parentsOf(taxonomy, next));
|
|
480
|
+
}
|
|
481
|
+
return ancestors;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private directParentsOf(taxonomy: Map<string, TaxonomyNode>, expr: ClassExpression): ClassExpression[] {
|
|
485
|
+
const parents = this.parentsOf(taxonomy, expr);
|
|
486
|
+
const ancestorParents = new Set<string>();
|
|
487
|
+
for (const parent of parents) {
|
|
488
|
+
for (const ancestor of this.ancestorsOf(taxonomy, parent)) {
|
|
489
|
+
ancestorParents.add(ancestor);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return parents.filter((parent) => !ancestorParents.has(parent.key));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private lowestMultiParentChild(taxonomy: Map<string, TaxonomyNode>): ClassExpression | undefined {
|
|
496
|
+
const roots = [...taxonomy.values()]
|
|
497
|
+
.filter((node) => node.parents.size === 0)
|
|
498
|
+
.map((node) => node.expr)
|
|
499
|
+
.sort((left, right) => left.key.localeCompare(right.key));
|
|
500
|
+
const visited = new Set<string>();
|
|
501
|
+
const postorder: ClassExpression[] = [];
|
|
502
|
+
const visit = (expr: ClassExpression): void => {
|
|
503
|
+
if (visited.has(expr.key)) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
visited.add(expr.key);
|
|
507
|
+
const children = this.childrenOf(taxonomy, expr).sort((left, right) => left.key.localeCompare(right.key));
|
|
508
|
+
for (const child of children) {
|
|
509
|
+
visit(child);
|
|
510
|
+
}
|
|
511
|
+
postorder.push(expr);
|
|
512
|
+
};
|
|
513
|
+
for (const root of roots) {
|
|
514
|
+
visit(root);
|
|
515
|
+
}
|
|
516
|
+
return postorder.find((expr) => this.directParentsOf(taxonomy, expr).length > 1);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private named(iri: string): ClassExpression {
|
|
520
|
+
return { kind: 'named', iri, key: `N:${iri}` };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private complement(expr: ClassExpression): ClassExpression {
|
|
524
|
+
if (expr.kind === 'universal') {
|
|
525
|
+
return EMPTY;
|
|
526
|
+
}
|
|
527
|
+
if (expr.kind === 'empty') {
|
|
528
|
+
return UNIVERSAL;
|
|
529
|
+
}
|
|
530
|
+
if (expr.kind === 'complement') {
|
|
531
|
+
return expr.operand;
|
|
532
|
+
}
|
|
533
|
+
return { kind: 'complement', operand: expr, key: `C:${expr.key}` };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private intersection(left: ClassExpression, right: ClassExpression): ClassExpression {
|
|
537
|
+
return this.combine('intersection', left, right);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private difference(left: ClassExpression, right: ClassExpression): ClassExpression {
|
|
541
|
+
if (left.kind === 'empty' || right.kind === 'universal' || left.key === right.key) {
|
|
542
|
+
return EMPTY;
|
|
543
|
+
}
|
|
544
|
+
if (right.kind === 'empty') {
|
|
545
|
+
return left;
|
|
546
|
+
}
|
|
547
|
+
return this.intersection(left, this.complement(right));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private combine(kind: 'intersection' | 'union', left: ClassExpression, right: ClassExpression): ClassExpression {
|
|
551
|
+
const isIntersection = kind === 'intersection';
|
|
552
|
+
if (left.key === right.key) {
|
|
553
|
+
return left;
|
|
554
|
+
}
|
|
555
|
+
if (isIntersection && (left.kind === 'empty' || right.kind === 'empty')) {
|
|
556
|
+
return EMPTY;
|
|
557
|
+
}
|
|
558
|
+
if (!isIntersection && (left.kind === 'universal' || right.kind === 'universal')) {
|
|
559
|
+
return UNIVERSAL;
|
|
560
|
+
}
|
|
561
|
+
if (isIntersection && left.kind === 'universal') {
|
|
562
|
+
return right;
|
|
563
|
+
}
|
|
564
|
+
if (isIntersection && right.kind === 'universal') {
|
|
565
|
+
return left;
|
|
566
|
+
}
|
|
567
|
+
if (!isIntersection && left.kind === 'empty') {
|
|
568
|
+
return right;
|
|
569
|
+
}
|
|
570
|
+
if (!isIntersection && right.kind === 'empty') {
|
|
571
|
+
return left;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const operands = new Map<string, ClassExpression>();
|
|
575
|
+
const addOperand = (expr: ClassExpression): void => {
|
|
576
|
+
if (expr.kind === kind) {
|
|
577
|
+
for (const operand of expr.operands) {
|
|
578
|
+
operands.set(operand.key, operand);
|
|
579
|
+
}
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
operands.set(expr.key, expr);
|
|
583
|
+
};
|
|
584
|
+
addOperand(left);
|
|
585
|
+
addOperand(right);
|
|
586
|
+
|
|
587
|
+
const sorted = [...operands.values()].sort((a, b) => a.key.localeCompare(b.key));
|
|
588
|
+
if (sorted.length === 1) {
|
|
589
|
+
return sorted[0];
|
|
590
|
+
}
|
|
591
|
+
if (isIntersection) {
|
|
592
|
+
const operandKeys = new Set(sorted.map((expr) => expr.key));
|
|
593
|
+
if (sorted.some((expr) => expr.kind === 'complement' && operandKeys.has(expr.operand.key))) {
|
|
594
|
+
return EMPTY;
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
kind: 'intersection',
|
|
598
|
+
operands: sorted,
|
|
599
|
+
key: `I:${sorted.map((expr) => expr.key).join('&')}`,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
kind: 'union',
|
|
604
|
+
operands: sorted,
|
|
605
|
+
key: `U:${sorted.map((expr) => expr.key).join('|')}`,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private memberIri(member: ClosureMember): string {
|
|
610
|
+
const namespace = normalizeNamespace((member.$container as Vocabulary).namespace);
|
|
611
|
+
return `${namespace}${member.name}`;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private importedOntologyNamespace(ontology: Ontology): string | undefined {
|
|
615
|
+
const namespace = (ontology as { namespace?: string }).namespace;
|
|
616
|
+
return typeof namespace === 'string' && namespace.length > 0
|
|
617
|
+
? normalizeNamespace(namespace)
|
|
618
|
+
: undefined;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function normalizeNamespace(value: string): string {
|
|
623
|
+
return value.replace(/^<|>$/g, '');
|
|
624
|
+
}
|