@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.
Files changed (92) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/LICENSE +48 -0
  3. package/README.md +287 -0
  4. package/docs/API.md +654 -0
  5. package/docs/design/OWL2-/347/274/272/345/217/243/345/210/206/346/236/220.md +140 -0
  6. package/docs/owl2-rl/OWL2-RL-/350/247/204/345/210/231/347/273/206/345/210/231.md +168 -0
  7. package/docs/owl2-rl/owl2-profiles-spec-zh.html +1914 -0
  8. package/docs/owl2-rl/owl2-profiles-spec.html +1916 -0
  9. package/docs/owl2-rl/rl_rules.json +392 -0
  10. package/main.js +46 -0
  11. package/package.json +76 -0
  12. package/sample/README.md +74 -0
  13. package/sample/case1-ecommerce-risk.js +170 -0
  14. package/sample/case10-ecommerce-customer-service.js +454 -0
  15. package/sample/case2-family-swrl.js +147 -0
  16. package/sample/case3-pharma-safety.js +132 -0
  17. package/sample/case4-knowledge-publishing.js +111 -0
  18. package/sample/case5-obda-profiles.js +153 -0
  19. package/sample/case6-medical-ontology.js +152 -0
  20. package/sample/case7-manufacturing-ppr.js +155 -0
  21. package/sample/case8-real-medical-bfo-ogms.js +353 -0
  22. package/sample/case9-real-manufacturing-iao.js +360 -0
  23. package/sample/ontologies/bfo.owl +1715 -0
  24. package/sample/ontologies/iao.owl +8532 -0
  25. package/sample/ontologies/ogms.owl +4320 -0
  26. package/sample/ontologies/ro-core.owl +1010 -0
  27. package/scripts/verify-api.js +53 -0
  28. package/src/index.js +176 -0
  29. package/src/inference/OWL2ProfileReasoners.js +67 -0
  30. package/src/inference/OWL2RLReasoner.js +65 -0
  31. package/src/inference/ReasonerQueries.js +111 -0
  32. package/src/inference/SWRLReasoner.js +409 -0
  33. package/src/inference/TripleStore.js +82 -0
  34. package/src/inference/rdf.js +83 -0
  35. package/src/inference/rules/owl2el.js +33 -0
  36. package/src/inference/rules/owl2ql.js +29 -0
  37. package/src/inference/rules/owl2rl.js +823 -0
  38. package/src/io/FunctionalSyntaxParser.js +399 -0
  39. package/src/io/FunctionalSyntaxWriter.js +180 -0
  40. package/src/io/ManchesterSyntaxParser.js +398 -0
  41. package/src/io/OWLXMLParser.js +356 -0
  42. package/src/io/OntologyLoader.js +125 -0
  43. package/src/io/RDFGraphToOntology.js +702 -0
  44. package/src/io/RDFXMLParser.js +319 -0
  45. package/src/io/RDFXMLWriter.js +196 -0
  46. package/src/io/SWRLParser.js +150 -0
  47. package/src/io/TurtleParser.js +363 -0
  48. package/src/io/TurtleWriter.js +393 -0
  49. package/src/model/IRI.js +62 -0
  50. package/src/model/OWLAxiom.js +507 -0
  51. package/src/model/OWLClassExpression.js +242 -0
  52. package/src/model/OWLEntity.js +142 -0
  53. package/src/model/OWLLiteral.js +30 -0
  54. package/src/model/OWLModelManager.js +106 -0
  55. package/src/model/OWLOntology.js +119 -0
  56. package/src/model/SWRL.js +152 -0
  57. package/src/model/event/OWLModelManagerEvent.js +33 -0
  58. package/src/model/hierarchy/HierarchyProvider.js +214 -0
  59. package/src/profiles/OWL2Profiles.js +185 -0
  60. package/src/server/public/app.js +97 -0
  61. package/src/server/public/index.html +49 -0
  62. package/src/server/webServer.js +138 -0
  63. package/src/validation/GlobalRestrictionsValidator.js +172 -0
  64. package/test/core.test.js +111 -0
  65. package/test/exports-map.test.js +165 -0
  66. package/test/owl2/axioms.test.js +160 -0
  67. package/test/owl2/classExpressions.test.js +99 -0
  68. package/test/owl2/functional-syntax-writer.test.js +65 -0
  69. package/test/owl2/functional.test.js +113 -0
  70. package/test/owl2/global-restrictions.test.js +76 -0
  71. package/test/owl2/longtail.test.js +74 -0
  72. package/test/owl2/manchester-syntax.test.js +100 -0
  73. package/test/owl2/ontology-loader-imports.test.js +62 -0
  74. package/test/owl2/owlxml-parser.test.js +112 -0
  75. package/test/owl2/profile-reasoners.test.js +108 -0
  76. package/test/owl2/profiles.test.js +66 -0
  77. package/test/owl2/rdf-graph-to-ontology.test.js +177 -0
  78. package/test/owl2/rdfxml-punning.test.js +25 -0
  79. package/test/owl2/rdfxml-writer.test.js +55 -0
  80. package/test/owl2/rdfxml.test.js +91 -0
  81. package/test/owl2/reasoner-queries.test.js +58 -0
  82. package/test/owl2/sample-e2e.test.js +195 -0
  83. package/test/owl2/swrl-parser.test.js +60 -0
  84. package/test/owl2/swrl.test.js +131 -0
  85. package/test/owl2/turtle-writer.test.js +84 -0
  86. package/test/owl2/turtle.test.js +124 -0
  87. package/test/owl2rl/cax.test.js +57 -0
  88. package/test/owl2rl/cls.test.js +210 -0
  89. package/test/owl2rl/dt.test.js +72 -0
  90. package/test/owl2rl/eq.test.js +95 -0
  91. package/test/owl2rl/prp.test.js +198 -0
  92. 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
+ });