@skaterqiang/protege-js 0.1.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/CHANGELOG.md +65 -0
- package/LICENSE +48 -0
- package/README.md +287 -0
- package/docs/API.md +654 -0
- package/docs/design/OWL2-/347/274/272/345/217/243/345/210/206/346/236/220.md +140 -0
- package/docs/owl2-rl/OWL2-RL-/350/247/204/345/210/231/347/273/206/345/210/231.md +168 -0
- package/docs/owl2-rl/owl2-profiles-spec-zh.html +1914 -0
- package/docs/owl2-rl/owl2-profiles-spec.html +1916 -0
- package/docs/owl2-rl/rl_rules.json +392 -0
- package/main.js +46 -0
- package/package.json +76 -0
- package/sample/README.md +74 -0
- package/sample/case1-ecommerce-risk.js +170 -0
- package/sample/case10-ecommerce-customer-service.js +454 -0
- package/sample/case2-family-swrl.js +147 -0
- package/sample/case3-pharma-safety.js +132 -0
- package/sample/case4-knowledge-publishing.js +111 -0
- package/sample/case5-obda-profiles.js +153 -0
- package/sample/case6-medical-ontology.js +152 -0
- package/sample/case7-manufacturing-ppr.js +155 -0
- package/sample/case8-real-medical-bfo-ogms.js +353 -0
- package/sample/case9-real-manufacturing-iao.js +360 -0
- package/sample/ontologies/bfo.owl +1715 -0
- package/sample/ontologies/iao.owl +8532 -0
- package/sample/ontologies/ogms.owl +4320 -0
- package/sample/ontologies/ro-core.owl +1010 -0
- package/scripts/verify-api.js +53 -0
- package/src/index.js +176 -0
- package/src/inference/OWL2ProfileReasoners.js +67 -0
- package/src/inference/OWL2RLReasoner.js +65 -0
- package/src/inference/ReasonerQueries.js +111 -0
- package/src/inference/SWRLReasoner.js +409 -0
- package/src/inference/TripleStore.js +82 -0
- package/src/inference/rdf.js +83 -0
- package/src/inference/rules/owl2el.js +33 -0
- package/src/inference/rules/owl2ql.js +29 -0
- package/src/inference/rules/owl2rl.js +823 -0
- package/src/io/FunctionalSyntaxParser.js +399 -0
- package/src/io/FunctionalSyntaxWriter.js +180 -0
- package/src/io/ManchesterSyntaxParser.js +398 -0
- package/src/io/OWLXMLParser.js +356 -0
- package/src/io/OntologyLoader.js +125 -0
- package/src/io/RDFGraphToOntology.js +702 -0
- package/src/io/RDFXMLParser.js +319 -0
- package/src/io/RDFXMLWriter.js +196 -0
- package/src/io/SWRLParser.js +150 -0
- package/src/io/TurtleParser.js +363 -0
- package/src/io/TurtleWriter.js +393 -0
- package/src/model/IRI.js +62 -0
- package/src/model/OWLAxiom.js +507 -0
- package/src/model/OWLClassExpression.js +242 -0
- package/src/model/OWLEntity.js +142 -0
- package/src/model/OWLLiteral.js +30 -0
- package/src/model/OWLModelManager.js +106 -0
- package/src/model/OWLOntology.js +119 -0
- package/src/model/SWRL.js +152 -0
- package/src/model/event/OWLModelManagerEvent.js +33 -0
- package/src/model/hierarchy/HierarchyProvider.js +214 -0
- package/src/profiles/OWL2Profiles.js +185 -0
- package/src/server/public/app.js +97 -0
- package/src/server/public/index.html +49 -0
- package/src/server/webServer.js +138 -0
- package/src/validation/GlobalRestrictionsValidator.js +172 -0
- package/test/core.test.js +111 -0
- package/test/exports-map.test.js +165 -0
- package/test/owl2/axioms.test.js +160 -0
- package/test/owl2/classExpressions.test.js +99 -0
- package/test/owl2/functional-syntax-writer.test.js +65 -0
- package/test/owl2/functional.test.js +113 -0
- package/test/owl2/global-restrictions.test.js +76 -0
- package/test/owl2/longtail.test.js +74 -0
- package/test/owl2/manchester-syntax.test.js +100 -0
- package/test/owl2/ontology-loader-imports.test.js +62 -0
- package/test/owl2/owlxml-parser.test.js +112 -0
- package/test/owl2/profile-reasoners.test.js +108 -0
- package/test/owl2/profiles.test.js +66 -0
- package/test/owl2/rdf-graph-to-ontology.test.js +177 -0
- package/test/owl2/rdfxml-punning.test.js +25 -0
- package/test/owl2/rdfxml-writer.test.js +55 -0
- package/test/owl2/rdfxml.test.js +91 -0
- package/test/owl2/reasoner-queries.test.js +58 -0
- package/test/owl2/sample-e2e.test.js +195 -0
- package/test/owl2/swrl-parser.test.js +60 -0
- package/test/owl2/swrl.test.js +131 -0
- package/test/owl2/turtle-writer.test.js +84 -0
- package/test/owl2/turtle.test.js +124 -0
- package/test/owl2rl/cax.test.js +57 -0
- package/test/owl2rl/cls.test.js +210 -0
- package/test/owl2rl/dt.test.js +72 -0
- package/test/owl2rl/eq.test.js +95 -0
- package/test/owl2rl/prp.test.js +198 -0
- package/test/owl2rl/scm.test.js +203 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Protégé JS</title>
|
|
6
|
+
<style>
|
|
7
|
+
* { box-sizing: border-box; }
|
|
8
|
+
body { margin: 0; font-family: -apple-system, 'SF Pro', 'PingFang SC', sans-serif; background: #1e1e1e; color: #ddd; }
|
|
9
|
+
header { padding: 10px 16px; background: #252526; border-bottom: 1px solid #333; display: flex; gap: 10px; align-items: center; }
|
|
10
|
+
header h1 { font-size: 15px; margin: 0; font-weight: 600; }
|
|
11
|
+
input[type=text] { flex: 1; max-width: 480px; padding: 6px 10px; border: 1px solid #3a3a3a; background: #1e1e1e; color: #ddd; border-radius: 6px; font-size: 13px; }
|
|
12
|
+
button { padding: 6px 14px; border: 1px solid #3a3a3a; background: #0e639c; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; }
|
|
13
|
+
button.ghost { background: #3a3a3a; }
|
|
14
|
+
#stats { margin-left: auto; font-size: 12px; color: #999; }
|
|
15
|
+
main { display: flex; height: calc(100vh - 49px); }
|
|
16
|
+
nav { width: 200px; background: #252526; border-right: 1px solid #333; padding: 10px; }
|
|
17
|
+
nav .tab { padding: 8px 10px; border-radius: 6px; cursor: pointer; font-size: 13px; margin-bottom: 4px; }
|
|
18
|
+
nav .tab.active { background: #0e639c; color: #fff; }
|
|
19
|
+
#tree-wrap { flex: 1; overflow: auto; padding: 14px 18px; }
|
|
20
|
+
.node { margin: 2px 0; }
|
|
21
|
+
.node .row { display: flex; align-items: center; gap: 6px; padding: 2px 4px; border-radius: 4px; cursor: default; }
|
|
22
|
+
.node .row:hover { background: #2d2d30; }
|
|
23
|
+
.twisty { width: 14px; display: inline-block; text-align: center; color: #888; cursor: pointer; user-select: none; }
|
|
24
|
+
.children { margin-left: 20px; }
|
|
25
|
+
.collapsed > .children { display: none; }
|
|
26
|
+
.badge { font-size: 10px; color: #888; margin-left: 6px; }
|
|
27
|
+
#empty { color: #777; padding: 40px; text-align: center; }
|
|
28
|
+
#msg { font-size: 12px; color: #7cc7ff; }
|
|
29
|
+
</style>
|
|
30
|
+
</head>
|
|
31
|
+
<body>
|
|
32
|
+
<header>
|
|
33
|
+
<h1>Protégé <span style="color:#888">JS</span></h1>
|
|
34
|
+
<input type="text" id="filePath" placeholder="输入 .owl / .rdf 文件路径后加载…">
|
|
35
|
+
<button id="btn-load">加载本体</button>
|
|
36
|
+
<span id="msg"></span>
|
|
37
|
+
<span id="stats"></span>
|
|
38
|
+
</header>
|
|
39
|
+
<main>
|
|
40
|
+
<nav>
|
|
41
|
+
<div class="tab active" data-kind="class">Classes</div>
|
|
42
|
+
<div class="tab" data-kind="objectProperty">Object Properties</div>
|
|
43
|
+
<div class="tab" data-kind="dataProperty">Data Properties</div>
|
|
44
|
+
</nav>
|
|
45
|
+
<div id="tree-wrap"><div id="empty">尚未加载本体。请输入 OWL (RDF/XML) 文件路径并点击「加载本体」。</div></div>
|
|
46
|
+
</main>
|
|
47
|
+
<script src="/app.js"></script>
|
|
48
|
+
</body>
|
|
49
|
+
</html>
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const url = require('url');
|
|
7
|
+
const { OWLModelManager } = require('../model/OWLModelManager');
|
|
8
|
+
const {
|
|
9
|
+
AssertedClassHierarchyProvider,
|
|
10
|
+
OWLObjectPropertyHierarchyProvider,
|
|
11
|
+
OWLDataPropertyHierarchyProvider
|
|
12
|
+
} = require('../model/hierarchy/HierarchyProvider');
|
|
13
|
+
const { OntologyLoader } = require('../io/OntologyLoader');
|
|
14
|
+
|
|
15
|
+
const PORT = process.env.PROTEGE_JS_PORT ? Number(process.env.PROTEGE_JS_PORT) : 8899;
|
|
16
|
+
|
|
17
|
+
const manager = new OWLModelManager();
|
|
18
|
+
const loader = new OntologyLoader();
|
|
19
|
+
|
|
20
|
+
function sendJson(res, data, code = 200) {
|
|
21
|
+
const body = JSON.stringify(data);
|
|
22
|
+
res.writeHead(code, { 'content-type': 'application/json; charset=utf-8' });
|
|
23
|
+
res.end(body);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sendFile(res, filePath) {
|
|
27
|
+
const ext = path.extname(filePath);
|
|
28
|
+
const mime = {
|
|
29
|
+
'.html': 'text/html; charset=utf-8',
|
|
30
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
31
|
+
'.css': 'text/css; charset=utf-8'
|
|
32
|
+
}[ext] || 'text/plain; charset=utf-8';
|
|
33
|
+
fs.readFile(filePath, (err, data) => {
|
|
34
|
+
if (err) { res.writeHead(404); res.end('not found'); return; }
|
|
35
|
+
res.writeHead(200, { 'content-type': mime });
|
|
36
|
+
res.end(data);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readBody(req) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
let s = '';
|
|
43
|
+
req.on('data', d => (s += d));
|
|
44
|
+
req.on('end', () => {
|
|
45
|
+
try { resolve(JSON.parse(s || '{}')); } catch { resolve({}); }
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function activeProviders() {
|
|
51
|
+
const ont = manager.getActiveOntology();
|
|
52
|
+
if (!ont) return null;
|
|
53
|
+
const classHp = new AssertedClassHierarchyProvider(ont);
|
|
54
|
+
classHp.rebuild();
|
|
55
|
+
const objHp = new OWLObjectPropertyHierarchyProvider(ont);
|
|
56
|
+
objHp.rebuild();
|
|
57
|
+
const dataHp = new OWLDataPropertyHierarchyProvider(ont);
|
|
58
|
+
dataHp.rebuild();
|
|
59
|
+
return { ont, classHp, objHp, dataHp };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function handleApi(req, res, pathname) {
|
|
63
|
+
if (pathname === '/api/load' && req.method === 'POST') {
|
|
64
|
+
const body = await readBody(req);
|
|
65
|
+
if (!body.filePath) return sendJson(res, { error: 'filePath required' }, 400);
|
|
66
|
+
try {
|
|
67
|
+
const ont = loader.loadFromFile(body.filePath);
|
|
68
|
+
manager.addOntology(ont);
|
|
69
|
+
return sendJson(res, {
|
|
70
|
+
ok: true,
|
|
71
|
+
ontology: {
|
|
72
|
+
id: ont.getOntologyID().toString(),
|
|
73
|
+
axiomCount: ont.getAxiomCount(),
|
|
74
|
+
classes: ont.getClassesInSignature().length,
|
|
75
|
+
objectProperties: ont.getObjectPropertiesInSignature().length,
|
|
76
|
+
dataProperties: ont.getDataPropertiesInSignature().length,
|
|
77
|
+
individuals: ont.getIndividualsInSignature().length
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
} catch (e) {
|
|
81
|
+
return sendJson(res, { error: String(e.message || e) }, 500);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (pathname === '/api/tree' && req.method === 'GET') {
|
|
86
|
+
const p = activeProviders();
|
|
87
|
+
if (!p) return sendJson(res, { error: 'no ontology loaded' }, 404);
|
|
88
|
+
const q = url.parse(req.url, true).query;
|
|
89
|
+
const kind = q.kind || 'class';
|
|
90
|
+
let tree;
|
|
91
|
+
if (kind === 'objectProperty') tree = p.objHp.toTree();
|
|
92
|
+
else if (kind === 'dataProperty') tree = p.dataHp.toTree();
|
|
93
|
+
else tree = p.classHp.toTree();
|
|
94
|
+
return sendJson(res, { ok: true, kind, tree });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (pathname === '/api/stats' && req.method === 'GET') {
|
|
98
|
+
const ont = manager.getActiveOntology();
|
|
99
|
+
if (!ont) return sendJson(res, { loaded: false });
|
|
100
|
+
return sendJson(res, {
|
|
101
|
+
loaded: true,
|
|
102
|
+
id: ont.getOntologyID().toString(),
|
|
103
|
+
axiomCount: ont.getAxiomCount(),
|
|
104
|
+
classes: ont.getClassesInSignature().length,
|
|
105
|
+
objectProperties: ont.getObjectPropertiesInSignature().length,
|
|
106
|
+
dataProperties: ont.getDataPropertiesInSignature().length,
|
|
107
|
+
individuals: ont.getIndividualsInSignature().length
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return sendJson(res, { error: 'unknown api' }, 404);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function createServer() {
|
|
115
|
+
const webRoot = path.join(__dirname, 'public');
|
|
116
|
+
return http.createServer(async (req, res) => {
|
|
117
|
+
const pathname = url.parse(req.url, true).pathname;
|
|
118
|
+
if (pathname.startsWith('/api/')) {
|
|
119
|
+
return handleApi(req, res, pathname);
|
|
120
|
+
}
|
|
121
|
+
let fp = pathname === '/' ? '/index.html' : pathname;
|
|
122
|
+
return sendFile(res, path.join(webRoot, fp));
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function start(port = PORT) {
|
|
127
|
+
const server = createServer();
|
|
128
|
+
server.listen(port, () => {
|
|
129
|
+
console.log(`protege-js web UI listening on http://localhost:${port}/`);
|
|
130
|
+
});
|
|
131
|
+
return server;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (require.main === module) {
|
|
135
|
+
start();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { start, createServer, manager, PORT };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// validation/GlobalRestrictionsValidator — checks the OWL 2 global
|
|
5
|
+
// restrictions on axioms (W3C §11.1 & §11.2):
|
|
6
|
+
//
|
|
7
|
+
// 1. Regularity of the property hierarchy / property chains:
|
|
8
|
+
// the property hierarchy induced by SubObjectPropertyOf and
|
|
9
|
+
// SubObjectPropertyOf(ObjectPropertyChain(...)) must be a strict partial
|
|
10
|
+
// order; a property may not be (in)directly a sub-property of a chain
|
|
11
|
+
// that contains itself in a "non-simple" way.
|
|
12
|
+
// 2. Simple-property restrictions:
|
|
13
|
+
// Non-simple properties (super-properties of chains, or chain members
|
|
14
|
+
// that are non-simple) may not be used in:
|
|
15
|
+
// - FunctionalObjectProperty / InverseFunctionalObjectProperty
|
|
16
|
+
// - IrreflexiveObjectProperty / AsymmetricObjectProperty
|
|
17
|
+
// - ObjectHasSelf
|
|
18
|
+
// - ObjectMin/Max/ExactCardinality
|
|
19
|
+
// - DisjointObjectProperties
|
|
20
|
+
// 3. owl:Nothing / owl:Thing misuse (nothing may be instance of owl:Nothing).
|
|
21
|
+
//
|
|
22
|
+
// Returns a list of violations:
|
|
23
|
+
// { rule, message, axiom? , iri? }
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
const { AxiomType: A } = require('../model/OWLAxiom');
|
|
27
|
+
const { ClassExpressionType: T } = require('../model/OWLClassExpression');
|
|
28
|
+
|
|
29
|
+
class GlobalRestrictionsValidator {
|
|
30
|
+
/**
|
|
31
|
+
* @param {OWLOntology} ont
|
|
32
|
+
*/
|
|
33
|
+
validate(ont) {
|
|
34
|
+
this.violations = [];
|
|
35
|
+
this.ont = ont;
|
|
36
|
+
this._buildPropertyHierarchy();
|
|
37
|
+
this._checkSimplePropertyRestrictions();
|
|
38
|
+
this._checkNothingInstance();
|
|
39
|
+
return this.violations;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_v(rule, message, axiom = null, iri = null) {
|
|
43
|
+
this.violations.push({ rule, message, axiom, iri });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- property hierarchy ---------------------------------------------------
|
|
47
|
+
|
|
48
|
+
_buildPropertyHierarchy() {
|
|
49
|
+
// nonSimple: set of property IRIs that are "non-simple"
|
|
50
|
+
// A property is non-simple if:
|
|
51
|
+
// - it is the super-property of a property chain axiom
|
|
52
|
+
// - it is a super-property (transitively) of a non-simple property
|
|
53
|
+
this.nonSimple = new Set();
|
|
54
|
+
this.chainSupers = []; // [{superProperty, chain:[iri...]}]
|
|
55
|
+
|
|
56
|
+
for (const ax of this.ont.getAxiomsOfType(A.SUB_PROPERTY_CHAIN_OF)) {
|
|
57
|
+
const sup = ax.superProperty.getIRI().toString();
|
|
58
|
+
this.nonSimple.add(sup);
|
|
59
|
+
this.chainSupers.push({
|
|
60
|
+
superProperty: sup,
|
|
61
|
+
chain: ax.propertyChain.map(p => p.getIRI().toString())
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// Propagate non-simplicity along subPropertyOf
|
|
65
|
+
let changed = true;
|
|
66
|
+
while (changed) {
|
|
67
|
+
changed = false;
|
|
68
|
+
for (const ax of this.ont.getAxiomsOfType(A.SUB_OBJECT_PROPERTY_OF)) {
|
|
69
|
+
const sub = ax.subProperty.getIRI().toString();
|
|
70
|
+
const sup = ax.superProperty.getIRI().toString();
|
|
71
|
+
if (this.nonSimple.has(sub) && !this.nonSimple.has(sup)) {
|
|
72
|
+
this.nonSimple.add(sup);
|
|
73
|
+
changed = true;
|
|
74
|
+
}
|
|
75
|
+
// A chain member that is non-simple makes the chain's super non-simple
|
|
76
|
+
// (OWL 2 §11.1: a property chain may not contain a non-simple property
|
|
77
|
+
// in the chain unless the super is also non-simple; we flag the more
|
|
78
|
+
// common violation: chain contains a non-simple property that is
|
|
79
|
+
// equal to or a super-property of the chain's own super → circularity)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Regularity: a chain member must be a strict sub-property of the chain's
|
|
83
|
+
// super-property, and may not be the super-property itself.
|
|
84
|
+
for (const cs of this.chainSupers) {
|
|
85
|
+
for (const member of cs.chain) {
|
|
86
|
+
if (member === cs.superProperty) {
|
|
87
|
+
this._v('regularity',
|
|
88
|
+
`Property chain for ${cs.superProperty} contains itself as a member (non-regular)`, null, cs.superProperty);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_isNonSimple(iri) { return this.nonSimple.has(iri); }
|
|
95
|
+
|
|
96
|
+
// --- simple property restrictions -----------------------------------------
|
|
97
|
+
|
|
98
|
+
_checkSimplePropertyRestrictions() {
|
|
99
|
+
const checkProp = (ax, propIRI, axiomName) => {
|
|
100
|
+
if (this._isNonSimple(propIRI)) {
|
|
101
|
+
this._v('simple-property',
|
|
102
|
+
`${axiomName} uses non-simple property ${propIRI} (super of a property chain)`, ax, propIRI);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
for (const ax of this.ont.getAxiomsOfType(A.FUNCTIONAL_OBJECT_PROPERTY)) {
|
|
106
|
+
checkProp(ax, ax.property.getIRI().toString(), 'FunctionalObjectProperty');
|
|
107
|
+
}
|
|
108
|
+
for (const ax of this.ont.getAxiomsOfType(A.INVERSE_FUNCTIONAL_OBJECT_PROPERTY)) {
|
|
109
|
+
checkProp(ax, ax.property.getIRI().toString(), 'InverseFunctionalObjectProperty');
|
|
110
|
+
}
|
|
111
|
+
for (const ax of this.ont.getAxiomsOfType(A.IRREFLEXIVE_OBJECT_PROPERTY)) {
|
|
112
|
+
checkProp(ax, ax.property.getIRI().toString(), 'IrreflexiveObjectProperty');
|
|
113
|
+
}
|
|
114
|
+
for (const ax of this.ont.getAxiomsOfType(A.ASYMMETRIC_OBJECT_PROPERTY)) {
|
|
115
|
+
checkProp(ax, ax.property.getIRI().toString(), 'AsymmetricObjectProperty');
|
|
116
|
+
}
|
|
117
|
+
for (const ax of this.ont.getAxiomsOfType(A.DISJOINT_OBJECT_PROPERTIES)) {
|
|
118
|
+
for (const p of ax.properties) {
|
|
119
|
+
checkProp(ax, p.getIRI().toString(), 'DisjointObjectProperties');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Cardinality restrictions and hasSelf inside class expressions
|
|
123
|
+
for (const ax of this.ont.getAxioms()) {
|
|
124
|
+
this._walkExprAxiom(ax, ax.subClass);
|
|
125
|
+
this._walkExprAxiom(ax, ax.superClass);
|
|
126
|
+
if (Array.isArray(ax.classExpressions)) {
|
|
127
|
+
for (const ce of ax.classExpressions) this._walkExprAxiom(ax, ce);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
_walkExprAxiom(ax, expr) {
|
|
133
|
+
if (!expr || !expr.type) return;
|
|
134
|
+
const self = this;
|
|
135
|
+
(function walk(e) {
|
|
136
|
+
if (!e || typeof e !== 'object') return;
|
|
137
|
+
if (e.type === T.OBJECT_HAS_SELF) {
|
|
138
|
+
const p = e.property.getIRI().toString();
|
|
139
|
+
if (self._isNonSimple(p)) {
|
|
140
|
+
self._v('simple-property', `ObjectHasSelf uses non-simple property ${p}`, ax, p);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (e.type === T.OBJECT_MIN_CARDINALITY || e.type === T.OBJECT_MAX_CARDINALITY
|
|
144
|
+
|| e.type === T.OBJECT_EXACT_CARDINALITY
|
|
145
|
+
|| e.type === T.OBJECT_MIN_QUALIFIED_CARDINALITY
|
|
146
|
+
|| e.type === T.OBJECT_MAX_QUALIFIED_CARDINALITY
|
|
147
|
+
|| e.type === T.OBJECT_EXACT_QUALIFIED_CARDINALITY) {
|
|
148
|
+
const p = e.property.getIRI().toString();
|
|
149
|
+
if (self._isNonSimple(p)) {
|
|
150
|
+
self._v('simple-property', `Cardinality restriction uses non-simple property ${p}`, ax, p);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (e.operand) walk(e.operand);
|
|
154
|
+
if (e.filler) walk(e.filler);
|
|
155
|
+
if (Array.isArray(e.operands)) for (const o of e.operands) walk(o);
|
|
156
|
+
})(expr);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// --- owl:Nothing -----------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
_checkNothingInstance() {
|
|
162
|
+
for (const ax of this.ont.getAxiomsOfType(A.CLASS_ASSERTION)) {
|
|
163
|
+
const ce = ax.classExpression;
|
|
164
|
+
if (ce && ce.getIRI && ce.getIRI().toString() === 'http://www.w3.org/2002/07/owl#Nothing') {
|
|
165
|
+
this._v('owl:Nothing-instance',
|
|
166
|
+
'An individual is asserted to be an instance of owl:Nothing', ax);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = { GlobalRestrictionsValidator };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test');
|
|
4
|
+
const assert = require('node:assert');
|
|
5
|
+
const { IRI } = require('../src/model/IRI');
|
|
6
|
+
const { OWLClass, OWLObjectProperty, OWLNamedIndividual, OWL } = require('../src/model/OWLEntity');
|
|
7
|
+
const { OWLSubClassOfAxiom, OWLDeclarationAxiom } = require('../src/model/OWLAxiom');
|
|
8
|
+
const { OWLOntology, OWLOntologyID } = require('../src/model/OWLOntology');
|
|
9
|
+
const { OWLModelManager } = require('../src/model/OWLModelManager');
|
|
10
|
+
const { AssertedClassHierarchyProvider } = require('../src/model/hierarchy/HierarchyProvider');
|
|
11
|
+
const { OntologyLoader } = require('../src/io/OntologyLoader');
|
|
12
|
+
|
|
13
|
+
test('IRI fragment and namespace', () => {
|
|
14
|
+
const iri = IRI.create('http://example.org/ont#Person');
|
|
15
|
+
assert.strictEqual(iri.getFragment(), 'Person');
|
|
16
|
+
assert.strictEqual(iri.getNamespace(), 'http://example.org/ont#');
|
|
17
|
+
assert.strictEqual(iri.getShortForm(), 'Person');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('OWLEntity equality by IRI and type', () => {
|
|
21
|
+
const a = new OWLClass('http://example.org#A');
|
|
22
|
+
const b = new OWLClass('http://example.org#A');
|
|
23
|
+
const c = new OWLObjectProperty('http://example.org#A');
|
|
24
|
+
assert.ok(a.equals(b));
|
|
25
|
+
assert.ok(!a.equals(c));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('OWLOntology stores axioms and indexes signature', () => {
|
|
29
|
+
const ont = new OWLOntology(new OWLOntologyID('http://example.org/ont'));
|
|
30
|
+
const person = new OWLClass('http://example.org/ont#Person');
|
|
31
|
+
const agent = new OWLClass('http://example.org/ont#Agent');
|
|
32
|
+
ont.addAxiom(new OWLDeclarationAxiom(person));
|
|
33
|
+
ont.addAxiom(new OWLDeclarationAxiom(agent));
|
|
34
|
+
ont.addAxiom(new OWLSubClassOfAxiom(person, agent));
|
|
35
|
+
assert.strictEqual(ont.getAxiomCount(), 3);
|
|
36
|
+
assert.strictEqual(ont.getClassesInSignature().length, 2);
|
|
37
|
+
assert.strictEqual(ont.getSubClassAxiomsForSubClass(person).length, 1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('OWLModelManager fires active ontology change events', () => {
|
|
41
|
+
const mm = new OWLModelManager();
|
|
42
|
+
const events = [];
|
|
43
|
+
mm.addListener(e => events.push(e.getType()));
|
|
44
|
+
const ont = mm.createOntology('http://example.org/ont');
|
|
45
|
+
assert.strictEqual(mm.getActiveOntology(), ont);
|
|
46
|
+
assert.ok(events.includes('ACTIVE_ONTOLOGY_CHANGED'));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('AssertedClassHierarchyProvider builds tree rooted at owl:Thing', () => {
|
|
50
|
+
const ont = new OWLOntology(new OWLOntologyID('http://example.org/ont'));
|
|
51
|
+
const thing = OWL.THING;
|
|
52
|
+
const agent = new OWLClass('http://example.org/ont#Agent');
|
|
53
|
+
const person = new OWLClass('http://example.org/ont#Person');
|
|
54
|
+
ont.addAxiom(new OWLDeclarationAxiom(agent));
|
|
55
|
+
ont.addAxiom(new OWLDeclarationAxiom(person));
|
|
56
|
+
ont.addAxiom(new OWLSubClassOfAxiom(person, agent));
|
|
57
|
+
|
|
58
|
+
const hp = new AssertedClassHierarchyProvider(ont);
|
|
59
|
+
hp.rebuild();
|
|
60
|
+
const roots = hp.getRoots();
|
|
61
|
+
assert.strictEqual(roots.length, 1);
|
|
62
|
+
assert.ok(roots[0].equals(thing));
|
|
63
|
+
const thingChildren = hp.getChildren(thing);
|
|
64
|
+
assert.strictEqual(thingChildren.length, 1);
|
|
65
|
+
assert.ok(thingChildren[0].equals(agent));
|
|
66
|
+
const agentChildren = hp.getChildren(agent);
|
|
67
|
+
assert.ok(agentChildren.some(c => c.equals(person)));
|
|
68
|
+
|
|
69
|
+
const tree = hp.toTree();
|
|
70
|
+
assert.strictEqual(tree[0].name, 'Thing');
|
|
71
|
+
assert.strictEqual(tree[0].children[0].name, 'Agent');
|
|
72
|
+
assert.strictEqual(tree[0].children[0].children[0].name, 'Person');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('OntologyLoader parses RDF/XML into ontology with classes and subclasses', () => {
|
|
76
|
+
const xml = `<?xml version="1.0"?>
|
|
77
|
+
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
78
|
+
xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
|
|
79
|
+
xmlns:owl="http://www.w3.org/2002/07/owl#"
|
|
80
|
+
xml:base="http://example.org/ont">
|
|
81
|
+
<owl:Ontology rdf:about="http://example.org/ont"/>
|
|
82
|
+
<owl:Class rdf:about="http://example.org/ont#Agent">
|
|
83
|
+
<rdfs:label>Agent</rdfs:label>
|
|
84
|
+
</owl:Class>
|
|
85
|
+
<owl:Class rdf:about="http://example.org/ont#Person">
|
|
86
|
+
<rdfs:subClassOf rdf:resource="http://example.org/ont#Agent"/>
|
|
87
|
+
<rdfs:label>Person</rdfs:label>
|
|
88
|
+
</owl:Class>
|
|
89
|
+
<owl:ObjectProperty rdf:about="http://example.org/ont#knows">
|
|
90
|
+
<rdfs:domain rdf:resource="http://example.org/ont#Person"/>
|
|
91
|
+
<rdfs:range rdf:resource="http://example.org/ont#Person"/>
|
|
92
|
+
</owl:ObjectProperty>
|
|
93
|
+
<owl:NamedIndividual rdf:about="http://example.org/ont#alice">
|
|
94
|
+
<rdf:type rdf:resource="http://example.org/ont#Person"/>
|
|
95
|
+
</owl:NamedIndividual>
|
|
96
|
+
</rdf:RDF>`;
|
|
97
|
+
const loader = new OntologyLoader();
|
|
98
|
+
const ont = loader.loadFromString(xml);
|
|
99
|
+
assert.strictEqual(ont.getOntologyID().ontologyIRI, 'http://example.org/ont');
|
|
100
|
+
const classes = ont.getClassesInSignature().map(c => c.getShortForm()).sort();
|
|
101
|
+
assert.deepStrictEqual(classes, ['Agent', 'Person']);
|
|
102
|
+
assert.strictEqual(ont.getObjectPropertiesInSignature().length, 1);
|
|
103
|
+
const inds = ont.getIndividualsInSignature().map(i => i.getShortForm());
|
|
104
|
+
assert.deepStrictEqual(inds, ['alice']);
|
|
105
|
+
|
|
106
|
+
const hp = new AssertedClassHierarchyProvider(ont);
|
|
107
|
+
hp.rebuild();
|
|
108
|
+
const tree = hp.toTree();
|
|
109
|
+
assert.strictEqual(tree[0].children[0].name, 'Agent');
|
|
110
|
+
assert.strictEqual(tree[0].children[0].children[0].name, 'Person');
|
|
111
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// test/exports-map.test.js — guards the package.json "exports" map.
|
|
5
|
+
//
|
|
6
|
+
// WHY THIS TEST EXISTS
|
|
7
|
+
// --------------------
|
|
8
|
+
// Every other test in this suite `require`s sibling modules by *relative path*
|
|
9
|
+
// (e.g. require('../src/model/IRI')). Relative requires bypass the "exports"
|
|
10
|
+
// map entirely and fall back to legacy CommonJS resolution, which DOES append
|
|
11
|
+
// `.js` for you. So the whole suite can pass while the published package is
|
|
12
|
+
// broken for real consumers.
|
|
13
|
+
//
|
|
14
|
+
// That is exactly what happened: "exports" declared `"./src/*": "./src/*"`,
|
|
15
|
+
// and Node's subpath *pattern* substitution performs no extension resolution.
|
|
16
|
+
// `require('@skaterqiang/protege-js/src/model/IRI')` — the form documented in
|
|
17
|
+
// 42 places across README.md and docs/API.md — threw MODULE_NOT_FOUND, while
|
|
18
|
+
// only the explicit `.js` spelling worked.
|
|
19
|
+
//
|
|
20
|
+
// The fix is two sibling pattern keys:
|
|
21
|
+
// "./src/*.js": "./src/*.js", // explicit-extension spelling
|
|
22
|
+
// "./src/*": "./src/*.js", // extensionless spelling
|
|
23
|
+
// Node's best-match rule picks the most specific key by suffix length, so both
|
|
24
|
+
// spellings resolve. (An *array* value does NOT work as a fallback chain: once
|
|
25
|
+
// a pattern key matches, a failed resolution throws instead of trying the next
|
|
26
|
+
// entry.)
|
|
27
|
+
//
|
|
28
|
+
// HOW THIS TEST STAYS HONEST
|
|
29
|
+
// --------------------------
|
|
30
|
+
// It resolves the package *by its own name* (Node self-reference, enabled by
|
|
31
|
+
// having both "name" and "exports"), so it exercises the real exports map with
|
|
32
|
+
// no packing or installing. And rather than hard-coding a list of deep-import
|
|
33
|
+
// paths — which would drift from the docs — it *scrapes* README.md and
|
|
34
|
+
// docs/API.md for every `require('<pkg>/...')` / `from '<pkg>/...'` specifier
|
|
35
|
+
// and asserts each one resolves. If someone documents a new deep import, it is
|
|
36
|
+
// covered automatically; if someone breaks "exports", the docs fail the build.
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
const { test } = require('node:test');
|
|
40
|
+
const assert = require('node:assert');
|
|
41
|
+
const fs = require('node:fs');
|
|
42
|
+
const path = require('node:path');
|
|
43
|
+
const { createRequire } = require('node:module');
|
|
44
|
+
|
|
45
|
+
const require_ = createRequire(path.join(__dirname, 'noop.js'));
|
|
46
|
+
const ROOT = path.join(__dirname, '..');
|
|
47
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
48
|
+
const NAME = pkg.name;
|
|
49
|
+
|
|
50
|
+
// Documents that advertise import specifiers to end users.
|
|
51
|
+
const DOC_FILES = ['README.md', path.join('docs', 'API.md')];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Pull every bare-package import specifier out of a markdown/JS-ish document.
|
|
55
|
+
* Matches require('pkg/...'), require("pkg/..."), from 'pkg/...' and the
|
|
56
|
+
* bare `require('pkg')` form. Deliberately ignores GitHub URLs, which contain
|
|
57
|
+
* the unscoped repo name and must stay unscoped.
|
|
58
|
+
*/
|
|
59
|
+
function scrapeSpecifiers(text) {
|
|
60
|
+
const out = new Set();
|
|
61
|
+
const re = new RegExp(
|
|
62
|
+
String.raw`(?:require\(|from\s+)\s*['"\`](${NAME.replace(/[/@]/g, (c) => '\\' + c)}(?:/[^'"\`]*)?)['"\`]`,
|
|
63
|
+
'g'
|
|
64
|
+
);
|
|
65
|
+
let m;
|
|
66
|
+
while ((m = re.exec(text)) !== null) out.add(m[1]);
|
|
67
|
+
return [...out];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
test('package.json declares a scoped name and an exports map', () => {
|
|
71
|
+
assert.ok(NAME.startsWith('@'), `expected a scoped name, got ${NAME}`);
|
|
72
|
+
assert.ok(pkg.exports && typeof pkg.exports === 'object', 'exports map missing');
|
|
73
|
+
assert.strictEqual(pkg.exports['.'], './src/index.js', 'root export must point at src/index.js');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('exports map covers both .js and extensionless deep-import spellings', () => {
|
|
77
|
+
// The extensionless key must map to a *.js target, otherwise Node performs no
|
|
78
|
+
// extension resolution and every documented deep import breaks.
|
|
79
|
+
assert.ok(pkg.exports['./src/*'], 'missing "./src/*" pattern key');
|
|
80
|
+
const target = pkg.exports['./src/*'];
|
|
81
|
+
const targets = Array.isArray(target) ? target : [target];
|
|
82
|
+
assert.ok(
|
|
83
|
+
targets.some((t) => t === './src/*.js'),
|
|
84
|
+
`"./src/*" must resolve to "./src/*.js", got ${JSON.stringify(target)}`
|
|
85
|
+
);
|
|
86
|
+
// And the explicit-extension spelling must still work.
|
|
87
|
+
assert.strictEqual(pkg.exports['./src/*.js'], './src/*.js', 'missing "./src/*.js" identity key');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('flat import by package name resolves (self-reference)', () => {
|
|
91
|
+
const api = require_(NAME);
|
|
92
|
+
assert.ok(api && typeof api === 'object', 'root import returned nothing');
|
|
93
|
+
// A few load-bearing symbols, so a hollow index.js cannot pass.
|
|
94
|
+
for (const sym of ['OWL2RLReasoner', 'TripleStore', 'TurtleParser', 'IRI', 'checkRL']) {
|
|
95
|
+
assert.ok(sym in api, `root export missing ${sym}`);
|
|
96
|
+
}
|
|
97
|
+
assert.ok(api.model && api.io && api.inference && api.profiles, 'namespaced exports missing');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('package.json subpath is exported', () => {
|
|
101
|
+
const meta = require_(`${NAME}/package.json`);
|
|
102
|
+
assert.strictEqual(meta.name, NAME);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('every deep-import specifier documented in the docs actually resolves', () => {
|
|
106
|
+
const specifiers = new Set();
|
|
107
|
+
for (const rel of DOC_FILES) {
|
|
108
|
+
const abs = path.join(ROOT, rel);
|
|
109
|
+
assert.ok(fs.existsSync(abs), `doc not found: ${rel}`);
|
|
110
|
+
for (const s of scrapeSpecifiers(fs.readFileSync(abs, 'utf8'))) specifiers.add(s);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Sanity: if the scraper silently matched nothing the test would be vacuous.
|
|
114
|
+
const deep = [...specifiers].filter((s) => s.includes('/'));
|
|
115
|
+
assert.ok(deep.length >= 20, `expected >=20 documented deep imports, scraped ${deep.length}`);
|
|
116
|
+
|
|
117
|
+
const failures = [];
|
|
118
|
+
for (const spec of [...specifiers].sort()) {
|
|
119
|
+
try {
|
|
120
|
+
const mod = require_(spec);
|
|
121
|
+
assert.ok(mod, `${spec} resolved to a falsy value`);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
failures.push(`${spec} -> ${err.code || err.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
assert.deepStrictEqual(failures, [], `unresolvable documented imports:\n ${failures.join('\n ')}`);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('extensionless and .js spellings resolve to the same module', () => {
|
|
130
|
+
// The two pattern keys must not accidentally expose different files.
|
|
131
|
+
const probes = [
|
|
132
|
+
'src/model/IRI',
|
|
133
|
+
'src/inference/OWL2RLReasoner',
|
|
134
|
+
'src/inference/TripleStore',
|
|
135
|
+
'src/io/TurtleParser',
|
|
136
|
+
'src/profiles/OWL2Profiles'
|
|
137
|
+
];
|
|
138
|
+
for (const p of probes) {
|
|
139
|
+
const a = require_(`${NAME}/${p}`);
|
|
140
|
+
const b = require_(`${NAME}/${p}.js`);
|
|
141
|
+
assert.strictEqual(a, b, `${p}: extensionless and .js spellings diverged`);
|
|
142
|
+
assert.ok(Object.keys(a).length > 0, `${p}: module exports nothing`);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('a resolved deep import is functional, not just loadable', () => {
|
|
147
|
+
// Guard against an exports map that resolves to the wrong/empty file.
|
|
148
|
+
const { OWL2RLReasoner } = require_(`${NAME}/src/inference/OWL2RLReasoner`);
|
|
149
|
+
const { TripleStore } = require_(`${NAME}/src/inference/TripleStore`);
|
|
150
|
+
// Take the IRIs from the library itself. Hard-coding them here once produced a
|
|
151
|
+
// false failure: the RDF namespace is http://www.w3.org/1999/02/22-rdf-syntax-ns#
|
|
152
|
+
// and a single mistyped separator makes cax-sco silently not fire, because the
|
|
153
|
+
// rule matches on exact IRI equality. Deriving them removes that whole class of
|
|
154
|
+
// error — and it exercises one more deep import in the process.
|
|
155
|
+
const { NS } = require_(`${NAME}/src/inference/rdf`);
|
|
156
|
+
const TYPE = NS.RDF + 'type';
|
|
157
|
+
const SUBCLASSOF = NS.RDFS + 'subClassOf';
|
|
158
|
+
|
|
159
|
+
const r = new OWL2RLReasoner(new TripleStore());
|
|
160
|
+
r.store.add('ex:A', SUBCLASSOF, 'ex:B');
|
|
161
|
+
r.store.add('ex:x', TYPE, 'ex:A');
|
|
162
|
+
r.materialize();
|
|
163
|
+
assert.ok(r.entails('ex:x', TYPE, 'ex:B'), 'deep-imported reasoner failed to entail cax-sco');
|
|
164
|
+
assert.deepStrictEqual(r.inconsistencies, [], 'unexpected inconsistencies');
|
|
165
|
+
});
|