@oml/server 0.14.17 → 0.16.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 +8 -1
- package/out/auth/feature-gate.d.ts +20 -5
- package/out/auth/feature-gate.js +80 -24
- package/out/auth/feature-gate.js.map +1 -1
- package/out/auth/feature-policy.js +1 -0
- package/out/auth/feature-policy.js.map +1 -1
- package/out/lsp/language-server.js +3 -47
- package/out/lsp/language-server.js.map +1 -1
- package/out/lsp/protocol/reasoner-protocol.d.ts +8 -0
- package/out/lsp/protocol/reasoner-protocol.js +3 -0
- package/out/lsp/protocol/reasoner-protocol.js.map +1 -1
- package/out/rest/export.js +1 -3
- package/out/rest/export.js.map +1 -1
- package/out/rest/routes.d.ts +5 -0
- package/out/rest/routes.js +165 -91
- package/out/rest/routes.js.map +1 -1
- package/out/rest/server.d.ts +0 -2
- package/out/rest/server.js +760 -474
- package/out/rest/server.js.map +1 -1
- package/out/rest/shacl-index.d.ts +19 -0
- package/out/rest/shacl-index.js +166 -0
- package/out/rest/shacl-index.js.map +1 -0
- package/out/rest/template.js +54 -38
- package/out/rest/template.js.map +1 -1
- package/out/rest/validation.d.ts +2 -1
- package/out/rest/validation.js +198 -136
- package/out/rest/validation.js.map +1 -1
- package/out/server.js +99 -8
- package/out/server.js.map +1 -1
- package/out/workspace-settings.d.ts +19 -0
- package/out/workspace-settings.js +27 -0
- package/out/workspace-settings.js.map +1 -0
- package/package.json +9 -6
package/out/rest/server.js
CHANGED
|
@@ -14,20 +14,20 @@ import { DataFactory, Writer } from 'n3';
|
|
|
14
14
|
import uFuzzy from '@leeoniya/ufuzzy';
|
|
15
15
|
import { MarkdownHandlerRegistry, MarkdownPreviewRuntime, buildTemplateCatalog as buildNavigationTemplateCatalog, extractLeadingFrontMatter, resolveTemplateForNavigation, renderTemplate, MarkdownExecutor, } from '@oml/markdown';
|
|
16
16
|
import { STATIC_MARKDOWN_RUNTIME_BUNDLE_FILE, STATIC_MARKDOWN_RUNTIME_CSS } from '@oml/markdown/static';
|
|
17
|
-
import { applyOmlUpdate, collectOntologyMembers, getIriForNode, getOntologyModelIndex, iriFragment, isDescription, isOntology, isVocabulary, tokenizeForFuzzy, } from '@oml/language';
|
|
17
|
+
import { applyOmlUpdate, collectOntologyMembers, getIriForNode, getOntologyModelIndex, iriFragment, isDescription, isDescriptionBundle, isOntology, isVocabulary, isVocabularyBundle, tokenizeForFuzzy, } from '@oml/language';
|
|
18
18
|
import { detectSparqlKind } from '@oml/owl';
|
|
19
19
|
import { exportAssertedWorkspace, exportWorkspace } from './export.js';
|
|
20
20
|
import { startOmlLanguageServer } from '../lsp/language-server.js';
|
|
21
21
|
import { createOpenApiSpec, dispatchRestOperation, resolveRestOperationId } from './routes.js';
|
|
22
22
|
import { buildTemplateCatalog, expandTemplateComposeBlocks, findFilesByExtension, frontMatterString, isTemplateMarkdownFile, normalizeContextOntologyIri, } from './template.js';
|
|
23
23
|
import { lintWorkspace, validateWorkspace } from './validation.js';
|
|
24
|
+
import { reindexWorkspaceShacl } from './shacl-index.js';
|
|
24
25
|
import { OmlAccessError, requiredFeatureForRestOperation, } from '../auth/feature-policy.js';
|
|
25
26
|
const JSON_CONTENT_TYPE = 'application/json; charset=utf-8';
|
|
26
27
|
const HTML_CONTENT_TYPE = 'text/html; charset=utf-8';
|
|
27
28
|
const DEFAULT_BODY_LIMIT_BYTES = 2 * 1024 * 1024;
|
|
28
29
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
29
30
|
const require = createModuleRequire();
|
|
30
|
-
const REST_DEBUG_ENABLED = false;
|
|
31
31
|
const SWAGGER_ASSET_FILES = new Set([
|
|
32
32
|
'swagger-ui.css',
|
|
33
33
|
'swagger-ui-bundle.js',
|
|
@@ -43,6 +43,162 @@ const SWAGGER_CONTENT_TYPES = {
|
|
|
43
43
|
let swaggerAssetValidationError;
|
|
44
44
|
let markdownStaticEntryFileCache;
|
|
45
45
|
const swaggerAssetPathCache = new Map();
|
|
46
|
+
function positionToOffset(text, position) {
|
|
47
|
+
let line = 0;
|
|
48
|
+
let index = 0;
|
|
49
|
+
while (line < position.line && index < text.length) {
|
|
50
|
+
const nl = text.indexOf('\n', index);
|
|
51
|
+
if (nl < 0) {
|
|
52
|
+
return text.length;
|
|
53
|
+
}
|
|
54
|
+
index = nl + 1;
|
|
55
|
+
line += 1;
|
|
56
|
+
}
|
|
57
|
+
return Math.min(text.length, index + Math.max(0, position.character));
|
|
58
|
+
}
|
|
59
|
+
function applyTextEditsToContent(content, edits) {
|
|
60
|
+
const normalized = [...edits].sort((left, right) => {
|
|
61
|
+
const leftStart = left.range.start.line === right.range.start.line
|
|
62
|
+
? left.range.start.character - right.range.start.character
|
|
63
|
+
: left.range.start.line - right.range.start.line;
|
|
64
|
+
if (leftStart !== 0) {
|
|
65
|
+
return -leftStart;
|
|
66
|
+
}
|
|
67
|
+
const leftEnd = left.range.end.line === right.range.end.line
|
|
68
|
+
? left.range.end.character - right.range.end.character
|
|
69
|
+
: left.range.end.line - right.range.end.line;
|
|
70
|
+
return -leftEnd;
|
|
71
|
+
});
|
|
72
|
+
let current = content;
|
|
73
|
+
for (const edit of normalized) {
|
|
74
|
+
const start = positionToOffset(current, edit.range.start);
|
|
75
|
+
const end = positionToOffset(current, edit.range.end);
|
|
76
|
+
current = `${current.slice(0, start)}${edit.newText}${current.slice(end)}`;
|
|
77
|
+
}
|
|
78
|
+
return current;
|
|
79
|
+
}
|
|
80
|
+
async function applyWorkspaceEditToFiles(edit) {
|
|
81
|
+
const byUri = new Map();
|
|
82
|
+
const changedFiles = [];
|
|
83
|
+
if (edit.changes) {
|
|
84
|
+
for (const [uri, edits] of Object.entries(edit.changes)) {
|
|
85
|
+
byUri.set(uri, [...(byUri.get(uri) ?? []), ...edits]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(edit.documentChanges)) {
|
|
89
|
+
for (const change of edit.documentChanges) {
|
|
90
|
+
if (change?.kind === 'delete' && typeof change.uri === 'string') {
|
|
91
|
+
if (change.uri.startsWith('file://')) {
|
|
92
|
+
const filePath = fileURLToPath(change.uri);
|
|
93
|
+
await fs.rm(filePath, { force: true });
|
|
94
|
+
changedFiles.push(change.uri);
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const uri = change?.textDocument?.uri;
|
|
99
|
+
const edits = change?.edits;
|
|
100
|
+
if (!uri || !Array.isArray(edits)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
byUri.set(uri, [...(byUri.get(uri) ?? []), ...edits]);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const [uri, edits] of byUri.entries()) {
|
|
107
|
+
if (!uri.startsWith('file://')) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const filePath = fileURLToPath(uri);
|
|
111
|
+
const current = await fs.readFile(filePath, 'utf-8').catch((error) => {
|
|
112
|
+
if (error?.code === 'ENOENT') {
|
|
113
|
+
return '';
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
});
|
|
117
|
+
const next = applyTextEditsToContent(current, edits);
|
|
118
|
+
if (next !== current) {
|
|
119
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
120
|
+
await fs.writeFile(filePath, next, 'utf-8');
|
|
121
|
+
changedFiles.push(uri);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return changedFiles;
|
|
125
|
+
}
|
|
126
|
+
function collectWorkspaceEditChangedFiles(edit) {
|
|
127
|
+
const changed = new Set();
|
|
128
|
+
if (edit.changes) {
|
|
129
|
+
for (const uri of Object.keys(edit.changes)) {
|
|
130
|
+
if (uri.startsWith('file://')) {
|
|
131
|
+
changed.add(uri);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (Array.isArray(edit.documentChanges)) {
|
|
136
|
+
for (const change of edit.documentChanges) {
|
|
137
|
+
if (change?.kind === 'delete' && typeof change?.uri === 'string' && change.uri.startsWith('file://')) {
|
|
138
|
+
changed.add(change.uri);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const uri = change?.textDocument?.uri;
|
|
142
|
+
if (typeof uri === 'string' && uri.startsWith('file://')) {
|
|
143
|
+
changed.add(uri);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return [...changed];
|
|
148
|
+
}
|
|
149
|
+
function collectWorkspaceEditReloadUris(edit) {
|
|
150
|
+
const uris = new Set();
|
|
151
|
+
if (edit.changes) {
|
|
152
|
+
for (const uri of Object.keys(edit.changes)) {
|
|
153
|
+
if (uri.startsWith('file://')) {
|
|
154
|
+
uris.add(uri);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (Array.isArray(edit.documentChanges)) {
|
|
159
|
+
for (const change of edit.documentChanges) {
|
|
160
|
+
if (change?.kind === 'delete') {
|
|
161
|
+
if (typeof change?.uri === 'string' && change.uri.startsWith('file://')) {
|
|
162
|
+
uris.add(change.uri);
|
|
163
|
+
}
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const uri = change?.textDocument?.uri;
|
|
167
|
+
if (typeof uri === 'string' && uri.startsWith('file://')) {
|
|
168
|
+
uris.add(uri);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return [...uris].map((uri) => URI.parse(uri));
|
|
173
|
+
}
|
|
174
|
+
function normalizeUpdateOperation(operation) {
|
|
175
|
+
if (!operation || typeof operation !== 'object' || Array.isArray(operation)) {
|
|
176
|
+
return operation;
|
|
177
|
+
}
|
|
178
|
+
const record = operation;
|
|
179
|
+
const keys = Object.keys(record);
|
|
180
|
+
if (keys.length !== 1) {
|
|
181
|
+
return operation;
|
|
182
|
+
}
|
|
183
|
+
const kind = keys[0];
|
|
184
|
+
const payload = record[kind];
|
|
185
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
186
|
+
return operation;
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
kind,
|
|
190
|
+
...payload,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function normalizeUpdateRequest(params) {
|
|
194
|
+
const operations = Array.isArray(params.operations)
|
|
195
|
+
? params.operations.map((operation) => normalizeUpdateOperation(operation))
|
|
196
|
+
: [];
|
|
197
|
+
return {
|
|
198
|
+
...params,
|
|
199
|
+
operations,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
46
202
|
function createModuleRequire() {
|
|
47
203
|
if (typeof __filename === 'string' && path.isAbsolute(__filename)) {
|
|
48
204
|
return createRequire(__filename);
|
|
@@ -53,16 +209,6 @@ function createModuleRequire() {
|
|
|
53
209
|
}
|
|
54
210
|
throw new Error('Unable to initialize module-anchored require() in @oml/server REST runtime.');
|
|
55
211
|
}
|
|
56
|
-
function debugRest(event, details = {}) {
|
|
57
|
-
if (!REST_DEBUG_ENABLED) {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
const fields = Object.entries(details)
|
|
61
|
-
.map(([key, value]) => `${key}=${String(value)}`)
|
|
62
|
-
.join(' ');
|
|
63
|
-
const suffix = fields.length > 0 ? ` ${fields}` : '';
|
|
64
|
-
process.stderr.write(`[oml-rest][debug] ${new Date().toISOString()} pid=${process.pid} event=${event}${suffix}\n`);
|
|
65
|
-
}
|
|
66
212
|
const SUPPORTED_MD_BLOCK_KINDS = new Set([
|
|
67
213
|
'table',
|
|
68
214
|
'tree',
|
|
@@ -74,6 +220,70 @@ const SUPPORTED_MD_BLOCK_KINDS = new Set([
|
|
|
74
220
|
'matrix',
|
|
75
221
|
'table-editor'
|
|
76
222
|
]);
|
|
223
|
+
function getOntologiesFromRuntime(runtime) {
|
|
224
|
+
const reasoningService = runtime.Oml.reasoning.ReasoningService;
|
|
225
|
+
const importGraph = reasoningService.getImportGraph();
|
|
226
|
+
const ontologyIndex = getOntologyModelIndex(runtime.shared);
|
|
227
|
+
const langiumDocuments = runtime.shared.workspace.LangiumDocuments;
|
|
228
|
+
// Collect all unique model URIs from the import graph
|
|
229
|
+
const modelUris = new Set();
|
|
230
|
+
for (const [importer, importedSet] of importGraph.imports.entries()) {
|
|
231
|
+
modelUris.add(importer);
|
|
232
|
+
for (const imported of importedSet) {
|
|
233
|
+
modelUris.add(imported);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Also include model URIs that are importers but not imported by anyone
|
|
237
|
+
for (const importer of importGraph.importedBy.keys()) {
|
|
238
|
+
modelUris.add(importer);
|
|
239
|
+
}
|
|
240
|
+
const ontologies = [];
|
|
241
|
+
for (const modelUri of modelUris) {
|
|
242
|
+
// Get ontology IRI
|
|
243
|
+
const ontologyIri = ontologyIndex.resolveOntologyIri(modelUri);
|
|
244
|
+
if (!ontologyIri)
|
|
245
|
+
continue;
|
|
246
|
+
// Get oml:type by loading the document
|
|
247
|
+
let omlType = 'http://opencaesar.io/oml#Vocabulary'; // default
|
|
248
|
+
try {
|
|
249
|
+
const uri = URI.parse(modelUri);
|
|
250
|
+
const document = langiumDocuments.getDocument(uri);
|
|
251
|
+
if (document?.parseResult?.value) {
|
|
252
|
+
const ontology = document.parseResult.value;
|
|
253
|
+
if (isOntology(ontology)) {
|
|
254
|
+
if (isVocabulary(ontology)) {
|
|
255
|
+
omlType = 'http://opencaesar.io/oml#Vocabulary';
|
|
256
|
+
}
|
|
257
|
+
else if (isVocabularyBundle(ontology)) {
|
|
258
|
+
omlType = 'http://opencaesar.io/oml#VocabularyBundle';
|
|
259
|
+
}
|
|
260
|
+
else if (isDescription(ontology)) {
|
|
261
|
+
omlType = 'http://opencaesar.io/oml#Description';
|
|
262
|
+
}
|
|
263
|
+
else if (isDescriptionBundle(ontology)) {
|
|
264
|
+
omlType = 'http://opencaesar.io/oml#DescriptionBundle';
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// Keep default oml:type if document can't be loaded
|
|
271
|
+
}
|
|
272
|
+
// Get imports
|
|
273
|
+
const importedUris = importGraph.directImportsOf(modelUri);
|
|
274
|
+
const imports = [];
|
|
275
|
+
for (const importedUri of importedUris) {
|
|
276
|
+
const importedOntologyIri = ontologyIndex.resolveOntologyIri(importedUri);
|
|
277
|
+
if (importedOntologyIri) {
|
|
278
|
+
imports.push(importedOntologyIri);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
ontologies.push({ ontology: ontologyIri, modelUri, oml_type: omlType, imports });
|
|
282
|
+
}
|
|
283
|
+
// Sort by ontology IRI
|
|
284
|
+
ontologies.sort((a, b) => a.ontology.localeCompare(b.ontology));
|
|
285
|
+
return ontologies;
|
|
286
|
+
}
|
|
77
287
|
class RestHttpError extends Error {
|
|
78
288
|
constructor(statusCode, message) {
|
|
79
289
|
super(message);
|
|
@@ -253,34 +463,6 @@ function escapeHtml(value) {
|
|
|
253
463
|
.replace(/"/g, '"')
|
|
254
464
|
.replace(/'/g, ''');
|
|
255
465
|
}
|
|
256
|
-
async function listWorkspaceModelFiles(workspaceRoot) {
|
|
257
|
-
const entries = [];
|
|
258
|
-
const ignoredNames = new Set(['.git', 'node_modules', 'dist', 'build', 'out']);
|
|
259
|
-
const visit = async (currentDir) => {
|
|
260
|
-
const children = await fs.readdir(currentDir, { withFileTypes: true });
|
|
261
|
-
for (const child of children) {
|
|
262
|
-
const childPath = path.join(currentDir, child.name);
|
|
263
|
-
if (child.isDirectory()) {
|
|
264
|
-
if (ignoredNames.has(child.name) || child.name.startsWith('.')) {
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
await visit(childPath);
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
if (!child.isFile() || !child.name.toLowerCase().endsWith('.oml')) {
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
const relativePath = path.relative(workspaceRoot, childPath).split(path.sep).join('/');
|
|
274
|
-
entries.push({
|
|
275
|
-
path: relativePath,
|
|
276
|
-
uri: pathToFileURL(childPath).toString(),
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
await visit(workspaceRoot);
|
|
281
|
-
entries.sort((left, right) => left.path.localeCompare(right.path));
|
|
282
|
-
return entries;
|
|
283
|
-
}
|
|
284
466
|
function createSparqlWorkbenchPage(defaultWorkspaceRoot) {
|
|
285
467
|
const escapedWorkspaceRoot = escapeHtml(defaultWorkspaceRoot);
|
|
286
468
|
return `<!DOCTYPE html>
|
|
@@ -799,8 +981,8 @@ function createSparqlWorkbenchPage(defaultWorkspaceRoot) {
|
|
|
799
981
|
</div>
|
|
800
982
|
<div class="file-row">
|
|
801
983
|
<label class="field">
|
|
802
|
-
<span class="field-label">OML
|
|
803
|
-
<input id="
|
|
984
|
+
<span class="field-label">OML Ontology</span>
|
|
985
|
+
<input id="ontologyIri" type="text" spellcheck="false" placeholder="Select an OML ontology from Browse" readonly>
|
|
804
986
|
</label>
|
|
805
987
|
<button id="browseModelsButton" class="ghost" type="button">Browse</button>
|
|
806
988
|
</div>
|
|
@@ -859,13 +1041,13 @@ LIMIT 25</textarea>
|
|
|
859
1041
|
<form method="dialog">
|
|
860
1042
|
<div class="dialog-header">
|
|
861
1043
|
<div>
|
|
862
|
-
<div class="dialog-title">Workspace OML
|
|
863
|
-
<div class="dialog-subtitle">Select the
|
|
1044
|
+
<div class="dialog-title">Workspace OML Ontologies</div>
|
|
1045
|
+
<div class="dialog-subtitle">Select the ontology to run the query against.</div>
|
|
864
1046
|
</div>
|
|
865
1047
|
<button id="closeBrowseDialog" type="submit" class="ghost">Close</button>
|
|
866
1048
|
</div>
|
|
867
1049
|
<div class="dialog-body">
|
|
868
|
-
<input id="browseFilter" type="text" placeholder="Filter
|
|
1050
|
+
<input id="browseFilter" type="text" placeholder="Filter ontologies by IRI" spellcheck="false">
|
|
869
1051
|
<div id="browseStatus" class="status">Loading...</div>
|
|
870
1052
|
<div id="browseList" class="browse-list"></div>
|
|
871
1053
|
</div>
|
|
@@ -873,7 +1055,7 @@ LIMIT 25</textarea>
|
|
|
873
1055
|
</dialog>
|
|
874
1056
|
|
|
875
1057
|
<script>
|
|
876
|
-
const
|
|
1058
|
+
const ontologyIriInput = document.getElementById('ontologyIri');
|
|
877
1059
|
const sparqlInput = document.getElementById('sparql');
|
|
878
1060
|
const runButton = document.getElementById('runQueryButton');
|
|
879
1061
|
const browseModelsButton = document.getElementById('browseModelsButton');
|
|
@@ -891,7 +1073,7 @@ LIMIT 25</textarea>
|
|
|
891
1073
|
const browseFilter = document.getElementById('browseFilter');
|
|
892
1074
|
|
|
893
1075
|
let browseEntries = [];
|
|
894
|
-
let
|
|
1076
|
+
let selectedOntologyIri = '';
|
|
895
1077
|
let currentTableColumns = [];
|
|
896
1078
|
let currentTableRows = [];
|
|
897
1079
|
let currentPageIndex = 0;
|
|
@@ -995,20 +1177,30 @@ LIMIT 25</textarea>
|
|
|
995
1177
|
renderTablePage();
|
|
996
1178
|
}
|
|
997
1179
|
|
|
1180
|
+
function ontologyIriToModelId(iri) {
|
|
1181
|
+
try {
|
|
1182
|
+
const url = new URL(iri);
|
|
1183
|
+
const rawPath = url.pathname.replace(/[/]+$/, '');
|
|
1184
|
+
const segments = rawPath.split('/').filter(Boolean);
|
|
1185
|
+
const stem = segments[segments.length - 1] || 'index';
|
|
1186
|
+
const dirSegs = url.host ? [url.host, ...segments.slice(0, -1)] : segments.slice(0, -1);
|
|
1187
|
+
return [...dirSegs, stem].join('/');
|
|
1188
|
+
} catch {
|
|
1189
|
+
return iri;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
998
1193
|
function renderBrowseEntries(entries) {
|
|
999
1194
|
if (entries.length === 0) {
|
|
1000
|
-
browseList.innerHTML = '<div class="empty">No OML
|
|
1195
|
+
browseList.innerHTML = '<div class="empty">No OML ontologies found for the current filter.</div>';
|
|
1001
1196
|
return;
|
|
1002
1197
|
}
|
|
1003
1198
|
browseList.innerHTML = entries.map((entry) => {
|
|
1004
|
-
const
|
|
1005
|
-
const
|
|
1006
|
-
? normalizedPath.slice(0, -4)
|
|
1007
|
-
: normalizedPath;
|
|
1008
|
-
const isSelected = selectedModelId && selectedModelId === modelId;
|
|
1199
|
+
const iri = String(entry.ontology || '').trim();
|
|
1200
|
+
const isSelected = selectedOntologyIri && selectedOntologyIri === iri;
|
|
1009
1201
|
const selectedClass = isSelected ? ' selected' : '';
|
|
1010
|
-
return '<button type="button" class="browse-entry' + selectedClass + '" data-
|
|
1011
|
-
+ '<div class="browse-entry-path" title="' + escapeClientHtml(
|
|
1202
|
+
return '<button type="button" class="browse-entry' + selectedClass + '" data-ontology-iri="' + escapeClientHtml(iri) + '">'
|
|
1203
|
+
+ '<div class="browse-entry-path" title="' + escapeClientHtml(iri) + '">' + escapeClientHtml(iri) + '</div>'
|
|
1012
1204
|
+ '</button>';
|
|
1013
1205
|
}).join('');
|
|
1014
1206
|
}
|
|
@@ -1017,52 +1209,50 @@ LIMIT 25</textarea>
|
|
|
1017
1209
|
const filter = (browseFilter.value || '').trim().toLowerCase();
|
|
1018
1210
|
const filtered = filter.length === 0
|
|
1019
1211
|
? browseEntries
|
|
1020
|
-
: browseEntries.filter((entry) => entry.
|
|
1212
|
+
: browseEntries.filter((entry) => String(entry.ontology || '').toLowerCase().includes(filter));
|
|
1021
1213
|
renderBrowseEntries(filtered);
|
|
1022
1214
|
}
|
|
1023
1215
|
|
|
1024
1216
|
async function loadWorkspaceModels() {
|
|
1025
|
-
browseStatus.textContent = 'Loading workspace OML
|
|
1217
|
+
browseStatus.textContent = 'Loading workspace OML ontologies...';
|
|
1026
1218
|
browseStatus.className = 'status';
|
|
1027
1219
|
try {
|
|
1028
|
-
const response = await fetch(routeUrl('v0/
|
|
1220
|
+
const response = await fetch(routeUrl('v0/ontologies'));
|
|
1029
1221
|
const payload = await response.json();
|
|
1030
1222
|
if (!response.ok) {
|
|
1031
|
-
throw new Error(payload.error || 'Unable to list workspace OML
|
|
1223
|
+
throw new Error(payload.error || 'Unable to list workspace OML ontologies.');
|
|
1032
1224
|
}
|
|
1033
|
-
browseEntries = Array.isArray(payload.
|
|
1034
|
-
browseStatus.textContent = 'Loaded ' + browseEntries.length + ' workspace
|
|
1225
|
+
browseEntries = Array.isArray(payload.ontologies) ? payload.ontologies : [];
|
|
1226
|
+
browseStatus.textContent = 'Loaded ' + browseEntries.length + ' workspace ontolog' + (browseEntries.length === 1 ? 'y' : 'ies') + '.';
|
|
1035
1227
|
browseStatus.className = 'status success';
|
|
1036
1228
|
applyBrowseFilter();
|
|
1037
1229
|
} catch (error) {
|
|
1038
1230
|
browseEntries = [];
|
|
1039
|
-
browseList.innerHTML = '<div class="empty">Unable to load workspace OML
|
|
1231
|
+
browseList.innerHTML = '<div class="empty">Unable to load workspace OML ontologies.</div>';
|
|
1040
1232
|
browseStatus.textContent = error instanceof Error ? error.message : String(error);
|
|
1041
1233
|
browseStatus.className = 'status error';
|
|
1042
1234
|
}
|
|
1043
1235
|
}
|
|
1044
1236
|
|
|
1045
|
-
function
|
|
1046
|
-
if (!
|
|
1047
|
-
return 'OML
|
|
1048
|
-
}
|
|
1049
|
-
if (modelId.toLowerCase().endsWith('.md')) {
|
|
1050
|
-
return 'Markdown files are not queryable directly. Choose an .oml file.';
|
|
1237
|
+
function validateOntologyIri(iri) {
|
|
1238
|
+
if (!iri) {
|
|
1239
|
+
return 'OML ontology is required.';
|
|
1051
1240
|
}
|
|
1052
1241
|
return undefined;
|
|
1053
1242
|
}
|
|
1054
1243
|
|
|
1055
1244
|
async function executeQuery() {
|
|
1056
|
-
const
|
|
1245
|
+
const iri = selectedOntologyIri.trim();
|
|
1057
1246
|
const sparql = sparqlInput.value.trim();
|
|
1058
|
-
const
|
|
1059
|
-
if (
|
|
1060
|
-
setStatus(
|
|
1061
|
-
rawOutput.textContent = JSON.stringify({ error:
|
|
1062
|
-
resetTable('Choose a valid OML
|
|
1247
|
+
const iriError = validateOntologyIri(iri);
|
|
1248
|
+
if (iriError) {
|
|
1249
|
+
setStatus(iriError, 'error');
|
|
1250
|
+
rawOutput.textContent = JSON.stringify({ error: iriError }, null, 2);
|
|
1251
|
+
resetTable('Choose a valid OML ontology to run a query.');
|
|
1063
1252
|
browseModelsButton.focus();
|
|
1064
1253
|
return;
|
|
1065
1254
|
}
|
|
1255
|
+
const modelId = ontologyIriToModelId(iri);
|
|
1066
1256
|
if (!sparql) {
|
|
1067
1257
|
setStatus('Query is required.', 'error');
|
|
1068
1258
|
rawOutput.textContent = JSON.stringify({ error: 'Query is required.' }, null, 2);
|
|
@@ -1142,18 +1332,17 @@ LIMIT 25</textarea>
|
|
|
1142
1332
|
});
|
|
1143
1333
|
|
|
1144
1334
|
browseList.addEventListener('click', (event) => {
|
|
1145
|
-
const target = event.target && event.target.closest ? event.target.closest('button[data-
|
|
1335
|
+
const target = event.target && event.target.closest ? event.target.closest('button[data-ontology-iri]') : null;
|
|
1146
1336
|
if (!target) {
|
|
1147
1337
|
return;
|
|
1148
1338
|
}
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1151
|
-
if (!modelId) {
|
|
1339
|
+
const iri = target.getAttribute('data-ontology-iri');
|
|
1340
|
+
if (!iri) {
|
|
1152
1341
|
return;
|
|
1153
1342
|
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
setStatus('Selected
|
|
1343
|
+
selectedOntologyIri = iri;
|
|
1344
|
+
ontologyIriInput.value = iri;
|
|
1345
|
+
setStatus('Selected OML ontology.', 'success');
|
|
1157
1346
|
applyBrowseFilter();
|
|
1158
1347
|
if (browseDialog.open) {
|
|
1159
1348
|
browseDialog.close();
|
|
@@ -1233,47 +1422,36 @@ function resolveSparqlModelPathFromRoute(pathname) {
|
|
|
1233
1422
|
* optionally with `.oml` extension) to a canonical `file://` URI.
|
|
1234
1423
|
* Throws a plain Error if the path escapes the workspace root.
|
|
1235
1424
|
*/
|
|
1236
|
-
function
|
|
1237
|
-
const
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
}
|
|
1244
|
-
return trimmed;
|
|
1245
|
-
}
|
|
1246
|
-
const relative = trimmed.replace(/^\/+/, '');
|
|
1247
|
-
if (!relative) {
|
|
1248
|
-
throw new Error('Empty model path.');
|
|
1249
|
-
}
|
|
1250
|
-
const candidate = path.resolve(workspaceRoot, relative);
|
|
1251
|
-
if (candidate !== normalizedWorkspace && !candidate.startsWith(`${normalizedWorkspace}${path.sep}`)) {
|
|
1252
|
-
throw new Error('Model path escapes the workspace root.');
|
|
1253
|
-
}
|
|
1254
|
-
return pathToFileURL(candidate).toString();
|
|
1425
|
+
function ontologyIriToModelId(ontologyIri) {
|
|
1426
|
+
const iri = new URL(ontologyIri);
|
|
1427
|
+
const rawPath = iri.pathname.replace(/\/+$/, '');
|
|
1428
|
+
const segments = rawPath.split('/').filter(Boolean);
|
|
1429
|
+
const stem = segments.at(-1) || 'index';
|
|
1430
|
+
const dirSegs = iri.host ? [iri.host, ...segments.slice(0, -1)] : segments.slice(0, -1);
|
|
1431
|
+
return [...dirSegs, stem].join('/');
|
|
1255
1432
|
}
|
|
1256
|
-
function
|
|
1257
|
-
const normalized =
|
|
1433
|
+
function resolveModelUriFromIriModelId(docs, ontologyIndex, modelId) {
|
|
1434
|
+
const normalized = modelId.trim().replace(/^\/+|\/+$/g, '');
|
|
1258
1435
|
if (!normalized) {
|
|
1259
|
-
throw new RestHttpError(400, 'Missing model
|
|
1260
|
-
}
|
|
1261
|
-
if (path.isAbsolute(normalized)) {
|
|
1262
|
-
throw new RestHttpError(400, 'SPARQL model path must be workspace-relative.');
|
|
1436
|
+
throw new RestHttpError(400, 'Missing model ID in SPARQL endpoint route.');
|
|
1263
1437
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1438
|
+
for (const doc of docs) {
|
|
1439
|
+
const modelUri = String(doc?.uri ?? '').trim();
|
|
1440
|
+
if (!modelUri)
|
|
1441
|
+
continue;
|
|
1442
|
+
const ontologyIri = ontologyIndex.resolveOntologyIri?.(modelUri);
|
|
1443
|
+
if (!ontologyIri)
|
|
1444
|
+
continue;
|
|
1445
|
+
try {
|
|
1446
|
+
const derived = ontologyIriToModelId(ontologyIri);
|
|
1447
|
+
if (derived === normalized)
|
|
1448
|
+
return modelUri;
|
|
1449
|
+
}
|
|
1450
|
+
catch {
|
|
1451
|
+
// skip unparseable IRIs
|
|
1452
|
+
}
|
|
1271
1453
|
}
|
|
1272
|
-
|
|
1273
|
-
}
|
|
1274
|
-
async function assertModelUriExists(modelUri) {
|
|
1275
|
-
const filePath = fileURLToPath(modelUri);
|
|
1276
|
-
await fs.access(filePath, fsSync.constants.F_OK);
|
|
1454
|
+
throw new RestHttpError(404, `No OML ontology found for model ID '${normalized}'.`);
|
|
1277
1455
|
}
|
|
1278
1456
|
function parseSparqlQueryFromRequest(req, parsedUrl, bodyText) {
|
|
1279
1457
|
const method = (req.method ?? 'GET').toUpperCase();
|
|
@@ -1393,14 +1571,7 @@ function normalizeOntologyNamespace(value) {
|
|
|
1393
1571
|
return normalized.length > 0 ? normalized : undefined;
|
|
1394
1572
|
}
|
|
1395
1573
|
function resolveOutputPathFromOntologyIriString(ontologyIri, format) {
|
|
1396
|
-
|
|
1397
|
-
const rawPath = iri.pathname.replace(/\/+$/, '');
|
|
1398
|
-
const pathSegments = rawPath.split('/').filter(Boolean);
|
|
1399
|
-
const fileStem = pathSegments.at(-1) || 'index';
|
|
1400
|
-
const directorySegments = iri.host
|
|
1401
|
-
? [iri.host, ...pathSegments.slice(0, -1)]
|
|
1402
|
-
: pathSegments.slice(0, -1);
|
|
1403
|
-
return path.join(...directorySegments, `${fileStem}.${format}`);
|
|
1574
|
+
return ontologyIriToModelId(ontologyIri).split('/').join(path.sep) + '.' + format;
|
|
1404
1575
|
}
|
|
1405
1576
|
function normalizeRdfSerializationFormat(value) {
|
|
1406
1577
|
const normalized = String(value ?? 'ttl').trim().toLowerCase();
|
|
@@ -1409,7 +1580,27 @@ function normalizeRdfSerializationFormat(value) {
|
|
|
1409
1580
|
}
|
|
1410
1581
|
return 'ttl';
|
|
1411
1582
|
}
|
|
1412
|
-
|
|
1583
|
+
function filterPrefixesForQuads(quads, workspacePrefixes) {
|
|
1584
|
+
const usedNs = new Set();
|
|
1585
|
+
for (const quad of quads) {
|
|
1586
|
+
for (const term of [quad.subject, quad.predicate, quad.object]) {
|
|
1587
|
+
if (term?.termType === 'NamedNode') {
|
|
1588
|
+
const iri = term.value;
|
|
1589
|
+
const h = iri.lastIndexOf('#');
|
|
1590
|
+
const ns = h >= 0 ? iri.slice(0, h + 1) : iri.slice(0, iri.lastIndexOf('/') + 1);
|
|
1591
|
+
if (ns)
|
|
1592
|
+
usedNs.add(ns);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
const result = {};
|
|
1597
|
+
for (const [px, ns] of Object.entries(workspacePrefixes)) {
|
|
1598
|
+
if (usedNs.has(ns))
|
|
1599
|
+
result[px] = ns;
|
|
1600
|
+
}
|
|
1601
|
+
return result;
|
|
1602
|
+
}
|
|
1603
|
+
async function serializeQuads(quads, format, pretty = false, prefixes) {
|
|
1413
1604
|
const writer = new Writer({
|
|
1414
1605
|
format: format === 'ttl'
|
|
1415
1606
|
? 'text/turtle'
|
|
@@ -1418,6 +1609,7 @@ async function serializeQuads(quads, format, pretty = false) {
|
|
|
1418
1609
|
: (format === 'nt'
|
|
1419
1610
|
? 'N-Triples'
|
|
1420
1611
|
: (format === 'nq' ? 'N-Quads' : 'application/n3'))),
|
|
1612
|
+
...(prefixes && Object.keys(prefixes).length > 0 ? { prefixes } : {}),
|
|
1421
1613
|
});
|
|
1422
1614
|
writer.addQuads(quads);
|
|
1423
1615
|
const serialized = await new Promise((resolve, reject) => {
|
|
@@ -1470,6 +1662,60 @@ function isRecord(value) {
|
|
|
1470
1662
|
function toMdBlockKind(language) {
|
|
1471
1663
|
return SUPPORTED_MD_BLOCK_KINDS.has(language) ? language : undefined;
|
|
1472
1664
|
}
|
|
1665
|
+
const SCRIPT_LANGUAGES = new Set(['javascript', 'js', 'python', 'r']);
|
|
1666
|
+
function extractScriptQueryStrings(code) {
|
|
1667
|
+
const seen = new Set();
|
|
1668
|
+
const patterns = [
|
|
1669
|
+
/\bquery\s*\(\s*`([\s\S]*?)`\s*\)/g,
|
|
1670
|
+
/\bquery\s*\(\s*"""([\s\S]*?)"""\s*\)/g,
|
|
1671
|
+
/\bquery\s*\(\s*'''([\s\S]*?)'''\s*\)/g,
|
|
1672
|
+
/\bquery\s*\(\s*"((?:[^"\\]|\\[\s\S])*)"\s*\)/g,
|
|
1673
|
+
/\bquery\s*\(\s*'((?:[^'\\]|\\[\s\S])*)'\s*\)/g,
|
|
1674
|
+
];
|
|
1675
|
+
for (const re of patterns) {
|
|
1676
|
+
let m;
|
|
1677
|
+
while ((m = re.exec(code)) !== null) {
|
|
1678
|
+
const sparql = (m[1] ?? '').replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\"/g, '"').replace(/\\'/g, "'").replace(/\\\\/g, '\\');
|
|
1679
|
+
if (sparql.trim()) {
|
|
1680
|
+
seen.add(sparql);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
return [...seen];
|
|
1685
|
+
}
|
|
1686
|
+
async function buildScriptSparqlCache(codeBlocks, query) {
|
|
1687
|
+
const cache = {};
|
|
1688
|
+
for (const block of codeBlocks) {
|
|
1689
|
+
if (!SCRIPT_LANGUAGES.has(block.language)) {
|
|
1690
|
+
continue;
|
|
1691
|
+
}
|
|
1692
|
+
const queryStrings = extractScriptQueryStrings(block.content);
|
|
1693
|
+
if (queryStrings.length === 0) {
|
|
1694
|
+
continue;
|
|
1695
|
+
}
|
|
1696
|
+
const blockCache = {};
|
|
1697
|
+
for (const sparql of queryStrings) {
|
|
1698
|
+
try {
|
|
1699
|
+
const result = await query(sparql);
|
|
1700
|
+
if (result.success && result.rows) {
|
|
1701
|
+
const columns = result.rows.length > 0 ? [...result.rows[0].keys()] : [];
|
|
1702
|
+
const rows = result.rows.map((row) => Object.fromEntries(columns.map((c) => [c, row.get(c)?.value ?? ''])));
|
|
1703
|
+
blockCache[sparql] = { success: true, columns, rows };
|
|
1704
|
+
}
|
|
1705
|
+
else {
|
|
1706
|
+
blockCache[sparql] = { success: false, columns: [], rows: [], error: result.error ?? 'Query failed' };
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
catch (e) {
|
|
1710
|
+
blockCache[sparql] = { success: false, columns: [], rows: [], error: e instanceof Error ? e.message : String(e) };
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
if (Object.keys(blockCache).length > 0) {
|
|
1714
|
+
cache[block.id] = blockCache;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
return cache;
|
|
1718
|
+
}
|
|
1473
1719
|
function toExecutableBlocks(codeBlocks) {
|
|
1474
1720
|
const executable = [];
|
|
1475
1721
|
for (const block of codeBlocks) {
|
|
@@ -1682,11 +1928,12 @@ function sanitizeBlockId(value) {
|
|
|
1682
1928
|
return trimmed.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
1683
1929
|
}
|
|
1684
1930
|
async function writeBlockArtifacts(outputFile, results) {
|
|
1931
|
+
const blockDirName = `${path.basename(outputFile, '.html')}.blocks`;
|
|
1932
|
+
const blockDir = path.join(path.dirname(outputFile), blockDirName);
|
|
1933
|
+
await fs.rm(blockDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
1685
1934
|
if (results.length === 0) {
|
|
1686
1935
|
return { count: 0, manifest: [] };
|
|
1687
1936
|
}
|
|
1688
|
-
const blockDirName = `${path.basename(outputFile, '.html')}.blocks`;
|
|
1689
|
-
const blockDir = path.join(path.dirname(outputFile), blockDirName);
|
|
1690
1937
|
await fs.mkdir(blockDir, { recursive: true });
|
|
1691
1938
|
const manifest = [];
|
|
1692
1939
|
for (const result of results) {
|
|
@@ -1852,36 +2099,46 @@ function buildIriAliasMapForPage(aliasesByIri, outputFile) {
|
|
|
1852
2099
|
}
|
|
1853
2100
|
return aliases;
|
|
1854
2101
|
}
|
|
1855
|
-
function wrapHtml(content, runtimeScriptPath, stylesheetPath, blockManifest, blockResults, wikiLinkHrefByKey, iriAliasByIri) {
|
|
2102
|
+
function wrapHtml(content, runtimeScriptPath, stylesheetPath, blockManifest, blockResults, wikiLinkHrefByKey, iriAliasByIri, scriptSparqlCache) {
|
|
1856
2103
|
const escapedManifest = escapeJsonForScript(JSON.stringify(blockManifest));
|
|
1857
2104
|
const inlineResults = Object.fromEntries(blockResults.map((result) => [result.blockId, result]));
|
|
1858
2105
|
const escapedInlineResults = escapeJsonForScript(JSON.stringify(inlineResults));
|
|
1859
2106
|
const escapedWikiIndex = escapeJsonForScript(JSON.stringify(wikiLinkHrefByKey));
|
|
1860
2107
|
const escapedIriAliasIndex = escapeJsonForScript(JSON.stringify(iriAliasByIri));
|
|
1861
2108
|
const escapedLinkingConfig = escapeJsonForScript(JSON.stringify({ linkingEnabled: true }));
|
|
2109
|
+
const escapedScriptCache = escapeJsonForScript(JSON.stringify(scriptSparqlCache));
|
|
1862
2110
|
return `<!doctype html>
|
|
1863
2111
|
<html lang="en">
|
|
1864
2112
|
<head>
|
|
1865
2113
|
<meta charset="UTF-8">
|
|
1866
2114
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1867
2115
|
<title>OML Markdown</title>
|
|
2116
|
+
<script>
|
|
2117
|
+
document.documentElement.classList.add('oml-md-booting');
|
|
2118
|
+
</script>
|
|
1868
2119
|
<link rel="stylesheet" href="${escapeAttribute(stylesheetPath)}">
|
|
2120
|
+
<style>
|
|
2121
|
+
html.oml-md-booting body { visibility: hidden; }
|
|
2122
|
+
body.oml-md-booting #oml-md-content { visibility: hidden; }
|
|
2123
|
+
</style>
|
|
1869
2124
|
</head>
|
|
1870
|
-
<body>
|
|
1871
|
-
|
|
2125
|
+
<body class="oml-md-booting">
|
|
2126
|
+
<div id="oml-md-content"></div>
|
|
2127
|
+
<template id="oml-md-initial-content">${content}</template>
|
|
1872
2128
|
<script id="oml-md-block-manifest" type="application/json">${escapedManifest}</script>
|
|
1873
2129
|
<script id="oml-md-block-inline-results" type="application/json">${escapedInlineResults}</script>
|
|
1874
2130
|
<script id="oml-md-wikilink-index" type="application/json">${escapedWikiIndex}</script>
|
|
1875
2131
|
<script id="oml-md-wikilink-iri-aliases" type="application/json">${escapedIriAliasIndex}</script>
|
|
1876
2132
|
<script id="oml-md-wikilink-config" type="application/json">${escapedLinkingConfig}</script>
|
|
1877
2133
|
<script id="oml-md-member-labels" type="application/json">{}</script>
|
|
2134
|
+
<script id="oml-md-script-sparql-cache" type="application/json">${escapedScriptCache}</script>
|
|
1878
2135
|
<script src="${escapeAttribute(runtimeScriptPath)}"></script>
|
|
1879
2136
|
</body>
|
|
1880
2137
|
</html>
|
|
1881
2138
|
`;
|
|
1882
2139
|
}
|
|
1883
2140
|
class InMemoryJsonRpcLspClient {
|
|
1884
|
-
constructor(workspaceRoot, requestTimeoutMs,
|
|
2141
|
+
constructor(workspaceRoot, requestTimeoutMs, runtime) {
|
|
1885
2142
|
this.watchers = new Set();
|
|
1886
2143
|
this.watcherActive = false;
|
|
1887
2144
|
this.refreshInFlight = false;
|
|
@@ -1890,13 +2147,6 @@ class InMemoryJsonRpcLspClient {
|
|
|
1890
2147
|
this.workspaceRoot = workspaceRoot;
|
|
1891
2148
|
this.requestTimeoutMs = requestTimeoutMs;
|
|
1892
2149
|
this.useExternalRuntime = runtime !== undefined;
|
|
1893
|
-
this.watchWorkspace = runtime ? false : watchWorkspace;
|
|
1894
|
-
debugRest('client.constructor', {
|
|
1895
|
-
workspaceRoot: path.resolve(workspaceRoot),
|
|
1896
|
-
requestTimeoutMs,
|
|
1897
|
-
useExternalRuntime: this.useExternalRuntime,
|
|
1898
|
-
watchWorkspace: this.watchWorkspace,
|
|
1899
|
-
});
|
|
1900
2150
|
if (runtime) {
|
|
1901
2151
|
this.runtime = runtime;
|
|
1902
2152
|
}
|
|
@@ -1913,9 +2163,17 @@ class InMemoryJsonRpcLspClient {
|
|
|
1913
2163
|
this.clientConnection = createConnection(this.serverToClient, this.clientToServer);
|
|
1914
2164
|
this.clientConnection.listen();
|
|
1915
2165
|
}
|
|
1916
|
-
|
|
1917
|
-
|
|
2166
|
+
this.startWorkspaceWatcher();
|
|
2167
|
+
}
|
|
2168
|
+
getWorkspacePrefixes() {
|
|
2169
|
+
const prefixes = {};
|
|
2170
|
+
for (const doc of this.getWorkspaceOmlDocuments()) {
|
|
2171
|
+
const root = doc?.parseResult?.value;
|
|
2172
|
+
if (root?.namespace && root?.prefix) {
|
|
2173
|
+
prefixes[root.prefix] = root.namespace;
|
|
2174
|
+
}
|
|
1918
2175
|
}
|
|
2176
|
+
return prefixes;
|
|
1919
2177
|
}
|
|
1920
2178
|
getWorkspaceOmlDocuments() {
|
|
1921
2179
|
const documents = this.runtime.shared.workspace.LangiumDocuments;
|
|
@@ -1927,206 +2185,38 @@ class InMemoryJsonRpcLspClient {
|
|
|
1927
2185
|
.filter((doc) => String(doc?.uri ?? '').trim().toLowerCase().endsWith('.oml'))
|
|
1928
2186
|
.sort((left, right) => String(left?.uri ?? '').localeCompare(String(right?.uri ?? '')));
|
|
1929
2187
|
}
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
let errors = 0;
|
|
1933
|
-
let warnings = 0;
|
|
1934
|
-
let unresolved = 0;
|
|
1935
|
-
const unresolvedSamples = [];
|
|
1936
|
-
for (const doc of docs) {
|
|
1937
|
-
const uri = String(doc?.uri ?? '').trim();
|
|
1938
|
-
const docDiagnostics = Array.isArray(doc?.diagnostics) ? doc.diagnostics : [];
|
|
1939
|
-
for (const diagnostic of docDiagnostics) {
|
|
1940
|
-
diagnostics += 1;
|
|
1941
|
-
const severity = Number(diagnostic?.severity ?? 0);
|
|
1942
|
-
if (severity === 1) {
|
|
1943
|
-
errors += 1;
|
|
1944
|
-
}
|
|
1945
|
-
else if (severity === 2) {
|
|
1946
|
-
warnings += 1;
|
|
1947
|
-
}
|
|
1948
|
-
const message = String(diagnostic?.message ?? '');
|
|
1949
|
-
if (message.includes('Could not resolve reference')) {
|
|
1950
|
-
unresolved += 1;
|
|
1951
|
-
if (unresolvedSamples.length < 8) {
|
|
1952
|
-
const line = Number(diagnostic?.range?.start?.line ?? 0) + 1;
|
|
1953
|
-
const column = Number(diagnostic?.range?.start?.character ?? 0) + 1;
|
|
1954
|
-
unresolvedSamples.push(`${uri}:${line}:${column}:${message}`);
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
|
-
return {
|
|
1960
|
-
docs: docs.length,
|
|
1961
|
-
diagnostics,
|
|
1962
|
-
errors,
|
|
1963
|
-
warnings,
|
|
1964
|
-
unresolved,
|
|
1965
|
-
unresolvedSamples,
|
|
1966
|
-
};
|
|
2188
|
+
async waitForValidatedWithTrace() {
|
|
2189
|
+
await this.runtime.shared.workspace.DocumentBuilder.waitUntil(DocumentState.Validated);
|
|
1967
2190
|
}
|
|
1968
|
-
|
|
1969
|
-
const summary = {
|
|
1970
|
-
changed: 0,
|
|
1971
|
-
parsed: 0,
|
|
1972
|
-
indexedContent: 0,
|
|
1973
|
-
computedScopes: 0,
|
|
1974
|
-
linked: 0,
|
|
1975
|
-
indexedReferences: 0,
|
|
1976
|
-
validated: 0,
|
|
1977
|
-
};
|
|
1978
|
-
for (const doc of docs) {
|
|
1979
|
-
const state = Number(doc?.state ?? -1);
|
|
1980
|
-
if (state >= DocumentState.Changed)
|
|
1981
|
-
summary.changed += 1;
|
|
1982
|
-
if (state >= DocumentState.Parsed)
|
|
1983
|
-
summary.parsed += 1;
|
|
1984
|
-
if (state >= DocumentState.IndexedContent)
|
|
1985
|
-
summary.indexedContent += 1;
|
|
1986
|
-
if (state >= DocumentState.ComputedScopes)
|
|
1987
|
-
summary.computedScopes += 1;
|
|
1988
|
-
if (state >= DocumentState.Linked)
|
|
1989
|
-
summary.linked += 1;
|
|
1990
|
-
if (state >= DocumentState.IndexedReferences)
|
|
1991
|
-
summary.indexedReferences += 1;
|
|
1992
|
-
if (state >= DocumentState.Validated)
|
|
1993
|
-
summary.validated += 1;
|
|
1994
|
-
}
|
|
1995
|
-
return summary;
|
|
1996
|
-
}
|
|
1997
|
-
async waitForValidatedWithTrace(reason, targetUris) {
|
|
1998
|
-
const builder = this.runtime.shared.workspace.DocumentBuilder;
|
|
1999
|
-
if (!REST_DEBUG_ENABLED) {
|
|
2000
|
-
await builder.waitUntil(DocumentState.Validated);
|
|
2001
|
-
return;
|
|
2002
|
-
}
|
|
2003
|
-
const startedAt = Date.now();
|
|
2004
|
-
const seenValidated = new Set();
|
|
2005
|
-
const beforeDocs = this.getWorkspaceOmlDocuments();
|
|
2006
|
-
const beforeStates = this.summarizeDocumentStates(beforeDocs);
|
|
2007
|
-
const beforeByUri = new Map(beforeDocs.map((doc) => [String(doc?.uri ?? '').trim(), Number(doc?.state ?? -1)]));
|
|
2008
|
-
const targetList = targetUris
|
|
2009
|
-
? [...targetUris].filter((uri) => uri.toLowerCase().endsWith('.oml'))
|
|
2010
|
-
: [...beforeByUri.keys()];
|
|
2011
|
-
const alreadyValidatedBeforeWait = targetList.filter((uri) => (beforeByUri.get(uri) ?? -1) >= DocumentState.Validated).length;
|
|
2012
|
-
const pendingBeforeWait = targetList.filter((uri) => (beforeByUri.get(uri) ?? -1) < DocumentState.Validated);
|
|
2013
|
-
const listener = builder.onDocumentPhase(DocumentState.Validated, (doc) => {
|
|
2014
|
-
const uri = String(doc?.uri ?? '').trim();
|
|
2015
|
-
if (!uri) {
|
|
2016
|
-
return;
|
|
2017
|
-
}
|
|
2018
|
-
if (targetUris && !targetUris.has(uri)) {
|
|
2019
|
-
return;
|
|
2020
|
-
}
|
|
2021
|
-
if (seenValidated.has(uri)) {
|
|
2022
|
-
return;
|
|
2023
|
-
}
|
|
2024
|
-
seenValidated.add(uri);
|
|
2025
|
-
const count = seenValidated.size;
|
|
2026
|
-
if (count <= 5 || count % 25 === 0) {
|
|
2027
|
-
debugRest('workspace.validated.doc', { reason, count, uri });
|
|
2028
|
-
}
|
|
2029
|
-
});
|
|
2030
|
-
try {
|
|
2031
|
-
debugRest('workspace.validated.wait.begin', {
|
|
2032
|
-
reason,
|
|
2033
|
-
targetCount: targetUris ? targetUris.size : -1,
|
|
2034
|
-
docs: beforeDocs.length,
|
|
2035
|
-
stateChanged: beforeStates.changed,
|
|
2036
|
-
stateParsed: beforeStates.parsed,
|
|
2037
|
-
stateIndexedContent: beforeStates.indexedContent,
|
|
2038
|
-
stateComputedScopes: beforeStates.computedScopes,
|
|
2039
|
-
stateLinked: beforeStates.linked,
|
|
2040
|
-
stateIndexedReferences: beforeStates.indexedReferences,
|
|
2041
|
-
stateValidated: beforeStates.validated,
|
|
2042
|
-
alreadyValidatedBeforeWait,
|
|
2043
|
-
pendingBeforeWait: pendingBeforeWait.length,
|
|
2044
|
-
pendingBeforeWaitSample: pendingBeforeWait.slice(0, 8).join(' || '),
|
|
2045
|
-
});
|
|
2046
|
-
await builder.waitUntil(DocumentState.Validated);
|
|
2047
|
-
const afterDocs = this.getWorkspaceOmlDocuments();
|
|
2048
|
-
const afterStates = this.summarizeDocumentStates(afterDocs);
|
|
2049
|
-
const afterByUri = new Map(afterDocs.map((doc) => [String(doc?.uri ?? '').trim(), Number(doc?.state ?? -1)]));
|
|
2050
|
-
const pendingAfterWait = targetList.filter((uri) => (afterByUri.get(uri) ?? -1) < DocumentState.Validated);
|
|
2051
|
-
debugRest('workspace.validated.wait.end', {
|
|
2052
|
-
reason,
|
|
2053
|
-
elapsedMs: Math.max(0, Date.now() - startedAt),
|
|
2054
|
-
observedValidatedDocs: seenValidated.size,
|
|
2055
|
-
docs: afterDocs.length,
|
|
2056
|
-
stateChanged: afterStates.changed,
|
|
2057
|
-
stateParsed: afterStates.parsed,
|
|
2058
|
-
stateIndexedContent: afterStates.indexedContent,
|
|
2059
|
-
stateComputedScopes: afterStates.computedScopes,
|
|
2060
|
-
stateLinked: afterStates.linked,
|
|
2061
|
-
stateIndexedReferences: afterStates.indexedReferences,
|
|
2062
|
-
stateValidated: afterStates.validated,
|
|
2063
|
-
pendingAfterWait: pendingAfterWait.length,
|
|
2064
|
-
pendingAfterWaitSample: pendingAfterWait.slice(0, 8).join(' || '),
|
|
2065
|
-
});
|
|
2066
|
-
}
|
|
2067
|
-
finally {
|
|
2068
|
-
listener.dispose();
|
|
2069
|
-
}
|
|
2070
|
-
}
|
|
2071
|
-
async lintWorkspace(_params = {}) {
|
|
2072
|
-
const preDocs = this.getWorkspaceOmlDocuments();
|
|
2073
|
-
const preSummary = this.summarizeDiagnostics(preDocs);
|
|
2074
|
-
const preStates = this.summarizeDocumentStates(preDocs);
|
|
2075
|
-
debugRest('lint.begin', {
|
|
2076
|
-
workspaceRoot: path.resolve(this.workspaceRoot),
|
|
2077
|
-
docs: preSummary.docs,
|
|
2078
|
-
diagnostics: preSummary.diagnostics,
|
|
2079
|
-
errors: preSummary.errors,
|
|
2080
|
-
warnings: preSummary.warnings,
|
|
2081
|
-
unresolved: preSummary.unresolved,
|
|
2082
|
-
unresolvedSample: preSummary.unresolvedSamples.join(' || '),
|
|
2083
|
-
stateChanged: preStates.changed,
|
|
2084
|
-
stateParsed: preStates.parsed,
|
|
2085
|
-
stateIndexedContent: preStates.indexedContent,
|
|
2086
|
-
stateComputedScopes: preStates.computedScopes,
|
|
2087
|
-
stateLinked: preStates.linked,
|
|
2088
|
-
stateIndexedReferences: preStates.indexedReferences,
|
|
2089
|
-
stateValidated: preStates.validated,
|
|
2090
|
-
});
|
|
2191
|
+
async lintWorkspace(params = {}) {
|
|
2091
2192
|
const context = {
|
|
2092
2193
|
workspaceRoot: this.workspaceRoot,
|
|
2093
2194
|
runtime: this.runtime,
|
|
2094
2195
|
ensureInitialized: () => this.ensureInitialized(),
|
|
2095
|
-
ensureWorkspaceCurrent: () => this.
|
|
2196
|
+
ensureWorkspaceCurrent: () => this.ensureWorkspaceSettled(false),
|
|
2197
|
+
waitForValidated: () => this.waitForValidatedWithTrace(),
|
|
2096
2198
|
getWorkspaceOmlDocuments: () => this.getWorkspaceOmlDocuments(),
|
|
2097
2199
|
};
|
|
2098
|
-
const result = await lintWorkspace(context);
|
|
2099
|
-
const postDocs = this.getWorkspaceOmlDocuments();
|
|
2100
|
-
const postStates = this.summarizeDocumentStates(postDocs);
|
|
2101
|
-
debugRest('lint.end', {
|
|
2102
|
-
filesChecked: result.filesChecked,
|
|
2103
|
-
errors: result.errors,
|
|
2104
|
-
warnings: result.warnings,
|
|
2105
|
-
problems: result.problems.length,
|
|
2106
|
-
elapsedMs: result.elapsedMs,
|
|
2107
|
-
stateValidated: postStates.validated,
|
|
2108
|
-
stateLinked: postStates.linked,
|
|
2109
|
-
stateComputedScopes: postStates.computedScopes,
|
|
2110
|
-
});
|
|
2200
|
+
const result = await lintWorkspace(context, params);
|
|
2111
2201
|
return result;
|
|
2112
2202
|
}
|
|
2113
2203
|
async queryWorkspaceRaw(modelUri, sparql) {
|
|
2114
2204
|
await this.ensureInitialized();
|
|
2115
|
-
await this.
|
|
2205
|
+
await this.ensureWorkspaceSettled(false);
|
|
2116
2206
|
const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
|
|
2117
2207
|
await reasoningService.ensureQueryContext(modelUri);
|
|
2118
2208
|
return await reasoningService.getSparqlService().query(modelUri, sparql);
|
|
2119
2209
|
}
|
|
2120
2210
|
async askWorkspaceRaw(modelUri, sparql) {
|
|
2121
2211
|
await this.ensureInitialized();
|
|
2122
|
-
await this.
|
|
2212
|
+
await this.ensureWorkspaceSettled(false);
|
|
2123
2213
|
const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
|
|
2124
2214
|
await reasoningService.ensureQueryContext(modelUri);
|
|
2125
2215
|
return await reasoningService.getSparqlService().ask(modelUri, sparql);
|
|
2126
2216
|
}
|
|
2127
2217
|
async constructWorkspaceRaw(modelUri, sparql) {
|
|
2128
2218
|
await this.ensureInitialized();
|
|
2129
|
-
await this.
|
|
2219
|
+
await this.ensureWorkspaceSettled(false);
|
|
2130
2220
|
const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
|
|
2131
2221
|
await reasoningService.ensureQueryContext(modelUri);
|
|
2132
2222
|
return await reasoningService.getSparqlService().construct(modelUri, sparql);
|
|
@@ -2197,7 +2287,8 @@ class InMemoryJsonRpcLspClient {
|
|
|
2197
2287
|
}
|
|
2198
2288
|
async updateWorkspace(params) {
|
|
2199
2289
|
await this.ensureInitialized();
|
|
2200
|
-
await this.
|
|
2290
|
+
await this.ensureWorkspaceSettled(false);
|
|
2291
|
+
const preview = params.preview === true;
|
|
2201
2292
|
const lint = await this.lintWorkspace({});
|
|
2202
2293
|
if (lint.errors > 0 || lint.warnings > 0) {
|
|
2203
2294
|
return {
|
|
@@ -2208,11 +2299,125 @@ class InMemoryJsonRpcLspClient {
|
|
|
2208
2299
|
error: `lint failed with ${lint.errors} error(s) and ${lint.warnings} warning(s).`,
|
|
2209
2300
|
};
|
|
2210
2301
|
}
|
|
2211
|
-
|
|
2302
|
+
const result = await applyOmlUpdate(this.runtime.shared, normalizeUpdateRequest(params), (message) => this.logError(message), pathToFileURL(this.workspaceRoot).toString(), preview);
|
|
2303
|
+
const errors = Array.isArray(result?.errors) ? result.errors : [];
|
|
2304
|
+
if (errors.length > 0) {
|
|
2305
|
+
return result;
|
|
2306
|
+
}
|
|
2307
|
+
const edit = result?.edit;
|
|
2308
|
+
const changedFiles = [];
|
|
2309
|
+
if (edit) {
|
|
2310
|
+
if (preview) {
|
|
2311
|
+
changedFiles.push(...collectWorkspaceEditChangedFiles(edit));
|
|
2312
|
+
const changedUris = collectWorkspaceEditReloadUris(edit);
|
|
2313
|
+
if (changedUris.length > 0) {
|
|
2314
|
+
await this.runtime.shared.workspace.DocumentBuilder.update(changedUris, []);
|
|
2315
|
+
await this.waitForValidatedWithTrace();
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
else {
|
|
2319
|
+
try {
|
|
2320
|
+
const deletedUriStrings = new Set((edit.documentChanges ?? [])
|
|
2321
|
+
.filter((change) => change?.kind === 'delete' && typeof change?.uri === 'string')
|
|
2322
|
+
.map((change) => String(change.uri)));
|
|
2323
|
+
changedFiles.push(...await applyWorkspaceEditToFiles(edit));
|
|
2324
|
+
if (changedFiles.length > 0) {
|
|
2325
|
+
const changedUris = changedFiles
|
|
2326
|
+
.filter((uri) => !deletedUriStrings.has(uri))
|
|
2327
|
+
.map((uri) => URI.parse(uri));
|
|
2328
|
+
const deletedUris = [...deletedUriStrings].map((uri) => URI.parse(uri));
|
|
2329
|
+
await this.runtime.shared.workspace.DocumentBuilder.update(changedUris, deletedUris);
|
|
2330
|
+
await this.waitForValidatedWithTrace();
|
|
2331
|
+
if (changedUris.length > 0) {
|
|
2332
|
+
await this.forceRevalidateUris(changedUris);
|
|
2333
|
+
}
|
|
2334
|
+
// Force one canonical workspace refresh cycle so diagnostics are computed
|
|
2335
|
+
// from the latest on-disk snapshot, not a transient intermediate version.
|
|
2336
|
+
await this.refreshWorkspaceOmlDocuments();
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
catch (error) {
|
|
2340
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2341
|
+
return {
|
|
2342
|
+
...result,
|
|
2343
|
+
errors: [{ operationIndex: -1, message }],
|
|
2344
|
+
error: message,
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
if (preview) {
|
|
2350
|
+
const diagnostics = await this.lintWorkspaceStable({});
|
|
2351
|
+
return {
|
|
2352
|
+
...result,
|
|
2353
|
+
changedFiles,
|
|
2354
|
+
diagnostics: {
|
|
2355
|
+
filesChecked: diagnostics.filesChecked,
|
|
2356
|
+
errors: diagnostics.errors,
|
|
2357
|
+
warnings: diagnostics.warnings,
|
|
2358
|
+
problems: diagnostics.problems,
|
|
2359
|
+
},
|
|
2360
|
+
success: true,
|
|
2361
|
+
preview: true,
|
|
2362
|
+
};
|
|
2363
|
+
}
|
|
2364
|
+
const diagnostics = await this.lintWorkspaceStable({});
|
|
2365
|
+
return {
|
|
2366
|
+
...result,
|
|
2367
|
+
changedFiles,
|
|
2368
|
+
diagnostics: {
|
|
2369
|
+
filesChecked: diagnostics.filesChecked,
|
|
2370
|
+
errors: diagnostics.errors,
|
|
2371
|
+
warnings: diagnostics.warnings,
|
|
2372
|
+
problems: diagnostics.problems,
|
|
2373
|
+
},
|
|
2374
|
+
success: true,
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
lintFingerprint(lint) {
|
|
2378
|
+
const problems = lint.problems
|
|
2379
|
+
.map((problem) => [
|
|
2380
|
+
problem.uri,
|
|
2381
|
+
problem.line,
|
|
2382
|
+
problem.column,
|
|
2383
|
+
problem.severity,
|
|
2384
|
+
problem.kind,
|
|
2385
|
+
problem.source ?? '',
|
|
2386
|
+
problem.code ?? '',
|
|
2387
|
+
problem.message,
|
|
2388
|
+
].join('|'))
|
|
2389
|
+
.join('\n');
|
|
2390
|
+
return `${lint.filesChecked}|${lint.errors}|${lint.warnings}\n${problems}`;
|
|
2391
|
+
}
|
|
2392
|
+
async lintWorkspaceStable(params) {
|
|
2393
|
+
let previous;
|
|
2394
|
+
let previousFingerprint = '';
|
|
2395
|
+
for (let i = 0; i < 6; i += 1) {
|
|
2396
|
+
await this.waitForWatcherRefreshIdle();
|
|
2397
|
+
await this.waitForValidatedWithTrace();
|
|
2398
|
+
const current = await this.lintWorkspace(params);
|
|
2399
|
+
const currentFingerprint = this.lintFingerprint(current);
|
|
2400
|
+
if (previous && previousFingerprint === currentFingerprint) {
|
|
2401
|
+
return current;
|
|
2402
|
+
}
|
|
2403
|
+
previous = current;
|
|
2404
|
+
previousFingerprint = currentFingerprint;
|
|
2405
|
+
}
|
|
2406
|
+
return previous ?? await this.lintWorkspace(params);
|
|
2407
|
+
}
|
|
2408
|
+
async forceRevalidateUris(changedUris) {
|
|
2409
|
+
const documents = this.runtime.shared.workspace.LangiumDocuments;
|
|
2410
|
+
const builder = this.runtime.shared.workspace.DocumentBuilder;
|
|
2411
|
+
const docs = await Promise.all(changedUris.map((uri) => documents.getOrCreateDocument(uri)));
|
|
2412
|
+
if (docs.length === 0) {
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
await builder.build(docs, { validation: true });
|
|
2416
|
+
await this.waitForValidatedWithTrace();
|
|
2212
2417
|
}
|
|
2213
2418
|
async fuzzySearchWorkspace(params) {
|
|
2214
2419
|
await this.ensureInitialized();
|
|
2215
|
-
await this.
|
|
2420
|
+
await this.ensureWorkspaceSettled(false);
|
|
2216
2421
|
try {
|
|
2217
2422
|
const text = (typeof params.text === 'string' ? params.text : '').trim();
|
|
2218
2423
|
if (!text) {
|
|
@@ -2229,7 +2434,9 @@ class InMemoryJsonRpcLspClient {
|
|
|
2229
2434
|
const candidateByIri = new Map();
|
|
2230
2435
|
for (const doc of iterable) {
|
|
2231
2436
|
const root = doc?.parseResult?.value;
|
|
2232
|
-
|
|
2437
|
+
const ontologyOk = !!root && isOntology(root);
|
|
2438
|
+
const vocabOrDescOk = ontologyOk && (isVocabulary(root) || isDescription(root));
|
|
2439
|
+
if (!vocabOrDescOk) {
|
|
2233
2440
|
continue;
|
|
2234
2441
|
}
|
|
2235
2442
|
modelUris.push(doc.uri.toString());
|
|
@@ -2284,7 +2491,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
2284
2491
|
}
|
|
2285
2492
|
}
|
|
2286
2493
|
return {
|
|
2287
|
-
|
|
2494
|
+
member: entry.iri,
|
|
2288
2495
|
label: entry.label,
|
|
2289
2496
|
score: ranked.length - index,
|
|
2290
2497
|
diagnostics: {
|
|
@@ -2311,7 +2518,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
2311
2518
|
}
|
|
2312
2519
|
async assertionsWorkspace(params) {
|
|
2313
2520
|
await this.ensureInitialized();
|
|
2314
|
-
await this.
|
|
2521
|
+
await this.ensureWorkspaceSettled(false);
|
|
2315
2522
|
const lint = await this.lintWorkspace({});
|
|
2316
2523
|
if (lint.errors > 0 || lint.warnings > 0) {
|
|
2317
2524
|
return {
|
|
@@ -2324,23 +2531,24 @@ class InMemoryJsonRpcLspClient {
|
|
|
2324
2531
|
error: `lint failed with ${lint.errors} error(s) and ${lint.warnings} warning(s).`,
|
|
2325
2532
|
};
|
|
2326
2533
|
}
|
|
2327
|
-
const
|
|
2534
|
+
const ontologyIriFilterRaw = typeof params.ontologyIri === 'string' ? params.ontologyIri.trim() : '';
|
|
2328
2535
|
let modelUriFilter = '';
|
|
2329
|
-
if (
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
catch (e) {
|
|
2536
|
+
if (ontologyIriFilterRaw.length > 0) {
|
|
2537
|
+
const ontologyIndex = getOntologyModelIndex(this.runtime.shared);
|
|
2538
|
+
const resolved = ontologyIndex.resolveModelUri(ontologyIriFilterRaw);
|
|
2539
|
+
if (!resolved) {
|
|
2334
2540
|
return {
|
|
2335
2541
|
success: false,
|
|
2336
2542
|
files: [],
|
|
2337
|
-
error:
|
|
2543
|
+
error: `No model found for ontology IRI '${ontologyIriFilterRaw}'.`,
|
|
2338
2544
|
};
|
|
2339
2545
|
}
|
|
2546
|
+
modelUriFilter = resolved;
|
|
2340
2547
|
}
|
|
2341
2548
|
const format = normalizeRdfSerializationFormat(params.format);
|
|
2342
2549
|
const pretty = params.pretty === true;
|
|
2343
2550
|
const docs = this.getWorkspaceOmlDocuments();
|
|
2551
|
+
const workspacePrefixes = this.getWorkspacePrefixes();
|
|
2344
2552
|
const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
|
|
2345
2553
|
const store = reasoningService.getStore().getStore();
|
|
2346
2554
|
const files = [];
|
|
@@ -2360,8 +2568,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
2360
2568
|
files.push({
|
|
2361
2569
|
modelUri,
|
|
2362
2570
|
ontologyIri,
|
|
2363
|
-
|
|
2364
|
-
content: await serializeQuads(quads, format, pretty),
|
|
2571
|
+
content: await serializeQuads(quads, format, pretty, filterPrefixesForQuads(quads, workspacePrefixes)),
|
|
2365
2572
|
});
|
|
2366
2573
|
}
|
|
2367
2574
|
files.sort((left, right) => left.ontologyIri.localeCompare(right.ontologyIri));
|
|
@@ -2373,6 +2580,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
2373
2580
|
async writeWorkspaceAssertedOwl(outputDir, format, pretty) {
|
|
2374
2581
|
const entries = [];
|
|
2375
2582
|
const docs = this.getWorkspaceOmlDocuments();
|
|
2583
|
+
const workspacePrefixes = this.getWorkspacePrefixes();
|
|
2376
2584
|
const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
|
|
2377
2585
|
const store = reasoningService.getStore().getStore();
|
|
2378
2586
|
for (const doc of docs) {
|
|
@@ -2387,7 +2595,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
2387
2595
|
.map((quad) => DataFactory.quad(quad.subject, quad.predicate, quad.object));
|
|
2388
2596
|
const owlPath = path.join(outputDir, resolveOutputPathFromOntologyIriString(ontologyIri, format));
|
|
2389
2597
|
await fs.mkdir(path.dirname(owlPath), { recursive: true });
|
|
2390
|
-
await fs.writeFile(owlPath, await serializeQuads(quads, format, pretty), 'utf-8');
|
|
2598
|
+
await fs.writeFile(owlPath, await serializeQuads(quads, format, pretty, filterPrefixesForQuads(quads, workspacePrefixes)), 'utf-8');
|
|
2391
2599
|
entries.push({ modelUri, ontologyIri, owlPath });
|
|
2392
2600
|
}
|
|
2393
2601
|
entries.sort((left, right) => left.ontologyIri.localeCompare(right.ontologyIri));
|
|
@@ -2398,7 +2606,8 @@ class InMemoryJsonRpcLspClient {
|
|
|
2398
2606
|
workspaceRoot: this.workspaceRoot,
|
|
2399
2607
|
runtime: this.runtime,
|
|
2400
2608
|
ensureInitialized: () => this.ensureInitialized(),
|
|
2401
|
-
ensureWorkspaceCurrent: () => this.
|
|
2609
|
+
ensureWorkspaceCurrent: () => this.ensureWorkspaceSettled(true),
|
|
2610
|
+
waitForValidated: () => this.waitForValidatedWithTrace(),
|
|
2402
2611
|
getWorkspaceOmlDocuments: () => this.getWorkspaceOmlDocuments(),
|
|
2403
2612
|
};
|
|
2404
2613
|
return await validateWorkspace(context, params);
|
|
@@ -2418,7 +2627,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
2418
2627
|
const context = {
|
|
2419
2628
|
workspaceRoot: this.workspaceRoot,
|
|
2420
2629
|
ensureInitialized: () => this.ensureInitialized(),
|
|
2421
|
-
ensureWorkspaceCurrent: () => this.
|
|
2630
|
+
ensureWorkspaceCurrent: () => this.ensureWorkspaceSettled(false),
|
|
2422
2631
|
writeWorkspaceAssertedOwl: (outputDir, format, pretty) => this.writeWorkspaceAssertedOwl(outputDir, format, pretty),
|
|
2423
2632
|
exportAssertedWorkspace: (options) => exportAssertedWorkspace(context, options),
|
|
2424
2633
|
};
|
|
@@ -2528,7 +2737,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
2528
2737
|
const context = {
|
|
2529
2738
|
workspaceRoot: this.workspaceRoot,
|
|
2530
2739
|
ensureInitialized: () => this.ensureInitialized(),
|
|
2531
|
-
ensureWorkspaceCurrent: () => this.
|
|
2740
|
+
ensureWorkspaceCurrent: () => this.ensureWorkspaceSettled(false),
|
|
2532
2741
|
writeWorkspaceAssertedOwl: (outputDir, format, pretty) => this.writeWorkspaceAssertedOwl(outputDir, format, pretty),
|
|
2533
2742
|
exportAssertedWorkspace: (options) => exportAssertedWorkspace(context, options),
|
|
2534
2743
|
};
|
|
@@ -2536,7 +2745,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
2536
2745
|
}
|
|
2537
2746
|
async renderWorkspace(params) {
|
|
2538
2747
|
await this.ensureInitialized();
|
|
2539
|
-
await this.
|
|
2748
|
+
await this.ensureWorkspaceSettled(false);
|
|
2540
2749
|
const lint = await this.lintWorkspace({});
|
|
2541
2750
|
if (lint.errors > 0 || lint.warnings > 0) {
|
|
2542
2751
|
return {
|
|
@@ -2554,9 +2763,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
2554
2763
|
const outputFromOptions = typeof params.web === 'string' && params.web.trim().length > 0 ? params.web.trim() : 'build/web';
|
|
2555
2764
|
const inputDir = path.resolve(workspaceRoot, inputFromOptions);
|
|
2556
2765
|
const outputDir = path.resolve(workspaceRoot, outputFromOptions);
|
|
2557
|
-
|
|
2558
|
-
await fs.rm(outputDir, { recursive: true, force: true });
|
|
2559
|
-
}
|
|
2766
|
+
await fs.rm(outputDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
2560
2767
|
const runtime = new MarkdownPreviewRuntime(new MarkdownHandlerRegistry());
|
|
2561
2768
|
const templateCatalog = await buildTemplateCatalog(workspaceRoot);
|
|
2562
2769
|
const navigationTemplateCatalog = buildNavigationTemplateCatalog(Array.from(templateCatalog.values()).flatMap((entries) => entries.map((entry) => entry.definition)));
|
|
@@ -2603,7 +2810,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
2603
2810
|
continue;
|
|
2604
2811
|
}
|
|
2605
2812
|
const frontMatter = extractLeadingFrontMatter(markdown);
|
|
2606
|
-
const contextOntologyIri = normalizeContextOntologyIri(frontMatter?.
|
|
2813
|
+
const contextOntologyIri = normalizeContextOntologyIri(frontMatter?.ontology
|
|
2607
2814
|
?? frontMatterString(frontMatter?.data, 'ontologyIri')
|
|
2608
2815
|
?? frontMatterString(frontMatter?.data, 'ontology'));
|
|
2609
2816
|
const contextMemberIri = frontMatterString(frontMatter?.data, 'memberIri')
|
|
@@ -2659,16 +2866,21 @@ class InMemoryJsonRpcLspClient {
|
|
|
2659
2866
|
const blockArtifacts = await writeBlockArtifacts(htmlPath, rewrittenBlockResults);
|
|
2660
2867
|
blockArtifactFiles += blockArtifacts.count;
|
|
2661
2868
|
const wikiLinkHrefByKey = buildWikiLinkHrefMapForPage(wikiPageIndex, htmlPath);
|
|
2869
|
+
const scriptSparqlCache = contextModelUri
|
|
2870
|
+
? await buildScriptSparqlCache(prepared.codeBlocks, (sparql) => reasoningService.getSparqlService().query(contextModelUri, sparql))
|
|
2871
|
+
: {};
|
|
2662
2872
|
renderJobs.push({
|
|
2663
2873
|
htmlPath,
|
|
2664
2874
|
renderedHtml: rewriteResult.html,
|
|
2665
2875
|
blockManifest: blockArtifacts.manifest,
|
|
2666
2876
|
blockResults: rewrittenBlockResults,
|
|
2667
2877
|
wikiLinkHrefByKey,
|
|
2878
|
+
scriptSparqlCache,
|
|
2668
2879
|
});
|
|
2669
2880
|
filesRendered += 1;
|
|
2670
2881
|
}
|
|
2671
2882
|
const renderedNavigationPages = new Set();
|
|
2883
|
+
const navTasks = [];
|
|
2672
2884
|
for (const modelUri of contextModelUris) {
|
|
2673
2885
|
await reasoningService.ensureQueryContext(modelUri);
|
|
2674
2886
|
const membersToTypes = await queryMemberTypes(reasoningService, modelUri);
|
|
@@ -2717,59 +2929,66 @@ class InMemoryJsonRpcLspClient {
|
|
|
2717
2929
|
const syntheticSourcePath = resolution.template.sourceUri?.startsWith('file:')
|
|
2718
2930
|
? decodeURIComponent(new URL(resolution.template.sourceUri).pathname)
|
|
2719
2931
|
: path.join(workspaceRoot, 'index.md');
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2932
|
+
navTasks.push(async () => {
|
|
2933
|
+
const expanded = await expandTemplateComposeBlocks(rendered.output, templateCatalog, {
|
|
2934
|
+
workspaceRoot,
|
|
2935
|
+
sourceMarkdownPath: syntheticSourcePath,
|
|
2936
|
+
contextOntologyIri,
|
|
2937
|
+
contextModelUri: modelUri,
|
|
2938
|
+
contextMemberIri: memberIri,
|
|
2939
|
+
});
|
|
2940
|
+
const prepared = runtime.prepare(expanded);
|
|
2941
|
+
const rewriteResult = rewriteRenderedLinks(prepared.renderedHtml, {
|
|
2942
|
+
workspaceRoot,
|
|
2943
|
+
inputRoot: inputDir,
|
|
2944
|
+
inputFile: syntheticSourcePath,
|
|
2945
|
+
outputRoot: outputDir,
|
|
2946
|
+
outputFile: targetFile,
|
|
2947
|
+
});
|
|
2948
|
+
for (const asset of rewriteResult.workspaceAssets) {
|
|
2949
|
+
workspaceAssetFiles.add(asset);
|
|
2950
|
+
}
|
|
2951
|
+
const executableBlocks = toExecutableBlocks(prepared.codeBlocks);
|
|
2952
|
+
const optionsByBlockId = new Map(prepared.codeBlocks.map((block) => [block.id, block.options]));
|
|
2953
|
+
const blockResults = executableBlocks.length === 0
|
|
2954
|
+
? []
|
|
2955
|
+
: (await executor.executeBlocks({
|
|
2956
|
+
markdownUri: sourceDocumentUri,
|
|
2957
|
+
modelUri,
|
|
2958
|
+
blocks: executableBlocks,
|
|
2959
|
+
})).results.map((result) => ({
|
|
2960
|
+
...result,
|
|
2961
|
+
options: optionsByBlockId.get(result.blockId),
|
|
2962
|
+
}));
|
|
2963
|
+
const rewrittenBlockResults = blockResults.map((result) => rewriteBlockResultAssetPaths(result, {
|
|
2964
|
+
workspaceRoot,
|
|
2965
|
+
inputRoot: inputDir,
|
|
2966
|
+
sourceFile: syntheticSourcePath,
|
|
2967
|
+
outputRoot: outputDir,
|
|
2968
|
+
outputFile: targetFile,
|
|
2969
|
+
}, workspaceAssetFiles));
|
|
2970
|
+
const blockArtifacts = await writeBlockArtifacts(targetFile, rewrittenBlockResults);
|
|
2971
|
+
blockArtifactFiles += blockArtifacts.count;
|
|
2972
|
+
const scriptSparqlCache = await buildScriptSparqlCache(prepared.codeBlocks, (sparql) => reasoningService.getSparqlService().query(modelUri, sparql));
|
|
2973
|
+
renderJobs.push({
|
|
2974
|
+
htmlPath: targetFile,
|
|
2975
|
+
renderedHtml: rewriteResult.html,
|
|
2976
|
+
blockManifest: blockArtifacts.manifest,
|
|
2977
|
+
blockResults: rewrittenBlockResults,
|
|
2978
|
+
wikiLinkHrefByKey: buildWikiLinkHrefMapForPage(wikiPageIndex, targetFile),
|
|
2979
|
+
scriptSparqlCache,
|
|
2980
|
+
});
|
|
2981
|
+
filesRendered += 1;
|
|
2765
2982
|
});
|
|
2766
|
-
filesRendered += 1;
|
|
2767
2983
|
}
|
|
2768
2984
|
}
|
|
2985
|
+
for (const task of navTasks) {
|
|
2986
|
+
await task();
|
|
2987
|
+
}
|
|
2769
2988
|
for (const job of renderJobs) {
|
|
2770
2989
|
const runtimeScriptRelative = toRelativeWebPath(path.dirname(job.htmlPath), staticAssets.runtimeScriptFile);
|
|
2771
2990
|
const stylesheetRelative = toRelativeWebPath(path.dirname(job.htmlPath), staticAssets.stylesheetFile);
|
|
2772
|
-
const html = wrapHtml(job.renderedHtml, runtimeScriptRelative, stylesheetRelative, job.blockManifest, job.blockResults, job.wikiLinkHrefByKey, buildIriAliasMapForPage(aliasesByIri, job.htmlPath));
|
|
2991
|
+
const html = wrapHtml(job.renderedHtml, runtimeScriptRelative, stylesheetRelative, job.blockManifest, job.blockResults, job.wikiLinkHrefByKey, buildIriAliasMapForPage(aliasesByIri, job.htmlPath), job.scriptSparqlCache);
|
|
2773
2992
|
await fs.mkdir(path.dirname(job.htmlPath), { recursive: true });
|
|
2774
2993
|
await fs.writeFile(job.htmlPath, html, 'utf-8');
|
|
2775
2994
|
}
|
|
@@ -2799,35 +3018,145 @@ class InMemoryJsonRpcLspClient {
|
|
|
2799
3018
|
}
|
|
2800
3019
|
return { success: true, filesRendered, outputDir, blockArtifactFiles };
|
|
2801
3020
|
}
|
|
2802
|
-
async
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
3021
|
+
async ontologiesWorkspace() {
|
|
3022
|
+
await this.ensureInitialized();
|
|
3023
|
+
await this.ensureWorkspaceSettled(false);
|
|
3024
|
+
return getOntologiesFromRuntime(this.runtime);
|
|
3025
|
+
}
|
|
3026
|
+
resolveModelUriForSparqlModelId(modelId) {
|
|
3027
|
+
const ontologyIndex = getOntologyModelIndex(this.runtime.shared);
|
|
3028
|
+
const docs = this.getWorkspaceOmlDocuments();
|
|
3029
|
+
return resolveModelUriFromIriModelId(docs, ontologyIndex, modelId);
|
|
3030
|
+
}
|
|
3031
|
+
async shapesWorkspace(params) {
|
|
3032
|
+
await this.ensureInitialized();
|
|
3033
|
+
await this.ensureWorkspaceSettled(true);
|
|
3034
|
+
const typeIri = typeof params.type === 'string' ? params.type.trim() : '';
|
|
3035
|
+
if (!typeIri) {
|
|
3036
|
+
return {
|
|
3037
|
+
success: false,
|
|
3038
|
+
error: 'Missing required parameter: type',
|
|
3039
|
+
};
|
|
2811
3040
|
}
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
3041
|
+
try {
|
|
3042
|
+
const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
|
|
3043
|
+
const reasoningStoreWrapper = reasoningService.getStore();
|
|
3044
|
+
const store = reasoningStoreWrapper.getStore();
|
|
3045
|
+
const OML_CONTEXT_ONTOLOGY = 'http://opencaesar.io/oml#contextOntology';
|
|
3046
|
+
const SH_NS = 'http://www.w3.org/ns/shacl#';
|
|
3047
|
+
const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
3048
|
+
const shapeEntries = [];
|
|
3049
|
+
// Find all markdown graph URIs
|
|
3050
|
+
const markdownGraphs = new Set();
|
|
3051
|
+
const contextQuads = store.getQuads(null, DataFactory.namedNode(OML_CONTEXT_ONTOLOGY), null, null);
|
|
3052
|
+
for (const quad of contextQuads) {
|
|
3053
|
+
if (quad.subject.termType === 'NamedNode' && quad.object.termType === 'NamedNode') {
|
|
3054
|
+
markdownGraphs.add(quad.subject.value);
|
|
3055
|
+
}
|
|
2817
3056
|
}
|
|
2818
|
-
|
|
2819
|
-
|
|
3057
|
+
// Query each markdown graph for shapes matching the type
|
|
3058
|
+
for (const graphUri of markdownGraphs) {
|
|
3059
|
+
const graphNode = DataFactory.namedNode(graphUri);
|
|
3060
|
+
const ontologyQuad = store.getQuads(DataFactory.namedNode(graphUri), DataFactory.namedNode(OML_CONTEXT_ONTOLOGY), null, null)[0];
|
|
3061
|
+
if (!ontologyQuad || ontologyQuad.object.termType !== 'NamedNode') {
|
|
3062
|
+
continue;
|
|
3063
|
+
}
|
|
3064
|
+
const ontologyIri = ontologyQuad.object.value;
|
|
3065
|
+
// Find NodeShapes in this graph
|
|
3066
|
+
const nodeShapeQuads = store.getQuads(null, DataFactory.namedNode(RDF_TYPE), DataFactory.namedNode(`${SH_NS}NodeShape`), graphNode);
|
|
3067
|
+
for (const shapeQuad of nodeShapeQuads) {
|
|
3068
|
+
if (shapeQuad.subject.termType !== 'NamedNode') {
|
|
3069
|
+
continue;
|
|
3070
|
+
}
|
|
3071
|
+
const shapeIri = shapeQuad.subject.value;
|
|
3072
|
+
// Check if this shape targets our type (sh:targetClass)
|
|
3073
|
+
const targetClassQuads = store.getQuads(shapeQuad.subject, DataFactory.namedNode(`${SH_NS}targetClass`), DataFactory.namedNode(typeIri), graphNode);
|
|
3074
|
+
if (targetClassQuads.length > 0) {
|
|
3075
|
+
shapeEntries.push({ ontology: ontologyIri, shape: shapeIri, graphUri });
|
|
3076
|
+
continue;
|
|
3077
|
+
}
|
|
3078
|
+
// Check if any property shape has sh:class matching our type
|
|
3079
|
+
const propertyQuads = store.getQuads(shapeQuad.subject, DataFactory.namedNode(`${SH_NS}property`), null, graphNode);
|
|
3080
|
+
for (const propQuad of propertyQuads) {
|
|
3081
|
+
if (propQuad.object.termType === 'NamedNode' || propQuad.object.termType === 'BlankNode') {
|
|
3082
|
+
const classQuads = store.getQuads(propQuad.object, DataFactory.namedNode(`${SH_NS}class`), DataFactory.namedNode(typeIri), graphNode);
|
|
3083
|
+
if (classQuads.length > 0) {
|
|
3084
|
+
shapeEntries.push({ ontology: ontologyIri, shape: shapeIri, graphUri });
|
|
3085
|
+
break;
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
2820
3090
|
}
|
|
2821
|
-
|
|
2822
|
-
|
|
3091
|
+
// Helper to collect all axioms for a shape (including blank nodes)
|
|
3092
|
+
const collectShapeAxioms = (shapeIri, graphUri) => {
|
|
3093
|
+
const graphNode = DataFactory.namedNode(graphUri);
|
|
3094
|
+
const visited = new Set();
|
|
3095
|
+
const axioms = [];
|
|
3096
|
+
const writer = new Writer({ format: 'N-Triples' });
|
|
3097
|
+
const collectNode = (node) => {
|
|
3098
|
+
const nodeKey = node.termType === 'BlankNode' ? `_:${node.value}` : node.value;
|
|
3099
|
+
if (visited.has(nodeKey))
|
|
3100
|
+
return;
|
|
3101
|
+
visited.add(nodeKey);
|
|
3102
|
+
const quads = store.getQuads(node, null, null, graphNode);
|
|
3103
|
+
for (const quad of quads) {
|
|
3104
|
+
// Convert quad to triple (without graph)
|
|
3105
|
+
writer.addQuad(DataFactory.quad(quad.subject, quad.predicate, quad.object));
|
|
3106
|
+
// If object is a blank node, recursively collect its triples
|
|
3107
|
+
if (quad.object.termType === 'BlankNode') {
|
|
3108
|
+
collectNode(quad.object);
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
};
|
|
3112
|
+
collectNode(DataFactory.namedNode(shapeIri));
|
|
3113
|
+
// Get serialized triples
|
|
3114
|
+
writer.end((error, result) => {
|
|
3115
|
+
if (!error && result) {
|
|
3116
|
+
const lines = result.trim().split('\n').filter((line) => line.trim().length > 0);
|
|
3117
|
+
axioms.push(...lines);
|
|
3118
|
+
}
|
|
3119
|
+
});
|
|
3120
|
+
return axioms;
|
|
3121
|
+
};
|
|
3122
|
+
// Group by ontology and collect axioms
|
|
3123
|
+
const grouped = new Map();
|
|
3124
|
+
for (const entry of shapeEntries) {
|
|
3125
|
+
const axioms = collectShapeAxioms(entry.shape, entry.graphUri);
|
|
3126
|
+
const shapes = grouped.get(entry.ontology) ?? [];
|
|
3127
|
+
shapes.push({ shape: entry.shape, axioms });
|
|
3128
|
+
grouped.set(entry.ontology, shapes);
|
|
3129
|
+
}
|
|
3130
|
+
const results = Array.from(grouped.entries())
|
|
3131
|
+
.map(([ontology, shapes]) => ({
|
|
3132
|
+
ontology,
|
|
3133
|
+
shapes: shapes.sort((a, b) => a.shape.localeCompare(b.shape)),
|
|
3134
|
+
}))
|
|
3135
|
+
.sort((a, b) => a.ontology.localeCompare(b.ontology));
|
|
3136
|
+
return {
|
|
3137
|
+
success: true,
|
|
3138
|
+
results,
|
|
3139
|
+
};
|
|
3140
|
+
}
|
|
3141
|
+
catch (error) {
|
|
3142
|
+
return {
|
|
3143
|
+
success: false,
|
|
3144
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3145
|
+
};
|
|
2823
3146
|
}
|
|
3147
|
+
}
|
|
3148
|
+
async ensureWorkspaceSettled(includeShacl) {
|
|
2824
3149
|
if (!this.initialWorkspaceSyncCompleted) {
|
|
2825
3150
|
await this.bootstrapWorkspaceOmlDocuments();
|
|
2826
3151
|
this.initialWorkspaceSyncCompleted = true;
|
|
2827
|
-
debugRest('workspace.current.initial.bootstrap.complete');
|
|
2828
3152
|
}
|
|
2829
3153
|
await this.waitForWatcherRefreshIdle();
|
|
2830
|
-
|
|
3154
|
+
if (this.useExternalRuntime) {
|
|
3155
|
+
await this.waitForValidatedWithTrace();
|
|
3156
|
+
}
|
|
3157
|
+
if (includeShacl) {
|
|
3158
|
+
await this.reindexWorkspaceShacl();
|
|
3159
|
+
}
|
|
2831
3160
|
}
|
|
2832
3161
|
async waitForWatcherRefreshIdle() {
|
|
2833
3162
|
while (this.watcherFlushTimer !== undefined || this.refreshInFlight || this.refreshQueued) {
|
|
@@ -2855,18 +3184,10 @@ class InMemoryJsonRpcLspClient {
|
|
|
2855
3184
|
const deletedUris = [...currentOmlUris]
|
|
2856
3185
|
.filter((uri) => !changedUriStrings.has(uri))
|
|
2857
3186
|
.map((uri) => URI.parse(uri));
|
|
2858
|
-
debugRest('workspace.refresh.plan', {
|
|
2859
|
-
workspaceRoot,
|
|
2860
|
-
changed: changedUris.length,
|
|
2861
|
-
deleted: deletedUris.length,
|
|
2862
|
-
current: currentOmlUris.size,
|
|
2863
|
-
});
|
|
2864
3187
|
await Promise.all(changedUris.map((uri) => documents.getOrCreateDocument(uri)));
|
|
2865
3188
|
const builder = this.runtime.shared.workspace.DocumentBuilder;
|
|
2866
3189
|
await builder.update(changedUris, deletedUris);
|
|
2867
|
-
await this.waitForValidatedWithTrace(
|
|
2868
|
-
const postDocs = this.getWorkspaceOmlDocuments();
|
|
2869
|
-
debugRest('workspace.refresh.done', { docsAfter: postDocs.length });
|
|
3190
|
+
await this.waitForValidatedWithTrace();
|
|
2870
3191
|
}
|
|
2871
3192
|
async bootstrapWorkspaceOmlDocuments() {
|
|
2872
3193
|
const workspaceRoot = path.resolve(this.workspaceRoot);
|
|
@@ -2874,38 +3195,32 @@ class InMemoryJsonRpcLspClient {
|
|
|
2874
3195
|
const documents = this.runtime.shared.workspace.LangiumDocuments;
|
|
2875
3196
|
const builder = this.runtime.shared.workspace.DocumentBuilder;
|
|
2876
3197
|
const docs = await Promise.all(omlFiles.map(async (filePath) => documents.getOrCreateDocument(URI.parse(pathToFileURL(filePath).toString()))));
|
|
2877
|
-
debugRest('workspace.bootstrap.plan', {
|
|
2878
|
-
workspaceRoot,
|
|
2879
|
-
files: docs.length,
|
|
2880
|
-
});
|
|
2881
3198
|
if (docs.length > 0) {
|
|
2882
3199
|
await builder.build(docs, { validation: true });
|
|
2883
|
-
await this.waitForValidatedWithTrace(
|
|
3200
|
+
await this.waitForValidatedWithTrace();
|
|
2884
3201
|
}
|
|
2885
|
-
|
|
2886
|
-
|
|
3202
|
+
}
|
|
3203
|
+
async reindexWorkspaceShacl() {
|
|
3204
|
+
await reindexWorkspaceShacl({
|
|
3205
|
+
workspaceRoot: this.workspaceRoot,
|
|
3206
|
+
runtime: this.runtime,
|
|
3207
|
+
});
|
|
2887
3208
|
}
|
|
2888
3209
|
async ensureInitialized() {
|
|
2889
3210
|
if (this.initPromise) {
|
|
2890
|
-
debugRest('initialize.await.existing');
|
|
2891
3211
|
await this.initPromise;
|
|
2892
3212
|
return;
|
|
2893
3213
|
}
|
|
2894
3214
|
if (this.useExternalRuntime) {
|
|
2895
3215
|
this.initPromise = (async () => {
|
|
2896
|
-
const begin = Date.now();
|
|
2897
|
-
debugRest('initialize.external.begin');
|
|
2898
3216
|
const workspace = this.runtime.shared.workspace.WorkspaceManager;
|
|
2899
3217
|
await workspace.ready;
|
|
2900
|
-
debugRest('initialize.external.ready', { elapsedMs: Math.max(0, Date.now() - begin) });
|
|
2901
3218
|
})();
|
|
2902
3219
|
}
|
|
2903
3220
|
else {
|
|
2904
3221
|
const rootPath = path.resolve(this.workspaceRoot);
|
|
2905
3222
|
const rootUri = pathToFileURL(rootPath).toString();
|
|
2906
3223
|
this.initPromise = (async () => {
|
|
2907
|
-
const begin = Date.now();
|
|
2908
|
-
debugRest('initialize.internal.begin', { rootPath, rootUri });
|
|
2909
3224
|
await withTimeout(this.clientConnection.sendRequest('initialize', {
|
|
2910
3225
|
processId: process.pid,
|
|
2911
3226
|
rootUri,
|
|
@@ -2919,14 +3234,13 @@ class InMemoryJsonRpcLspClient {
|
|
|
2919
3234
|
// before lint/query calls trigger document refresh and diagnostics.
|
|
2920
3235
|
const workspace = this.runtime.shared.workspace.WorkspaceManager;
|
|
2921
3236
|
await workspace.ready;
|
|
2922
|
-
debugRest('initialize.internal.ready', { elapsedMs: Math.max(0, Date.now() - begin) });
|
|
2923
3237
|
})();
|
|
2924
3238
|
}
|
|
2925
3239
|
await this.initPromise;
|
|
2926
3240
|
}
|
|
2927
3241
|
}
|
|
2928
3242
|
export async function runOmlWorkspaceLocalOperation(options) {
|
|
2929
|
-
const client = new InMemoryJsonRpcLspClient(options.workspaceRoot ? path.resolve(options.workspaceRoot) : process.cwd(), options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS, options.
|
|
3243
|
+
const client = new InMemoryJsonRpcLspClient(options.workspaceRoot ? path.resolve(options.workspaceRoot) : process.cwd(), options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS, options.runtime);
|
|
2930
3244
|
try {
|
|
2931
3245
|
const params = options.params ?? {};
|
|
2932
3246
|
switch (options.operation) {
|
|
@@ -2950,16 +3264,7 @@ export async function runOmlWorkspaceLocalOperation(options) {
|
|
|
2950
3264
|
}
|
|
2951
3265
|
export async function startOmlRestServer(options) {
|
|
2952
3266
|
const workspaceRoot = options.workspaceRoot ? path.resolve(options.workspaceRoot) : process.cwd();
|
|
2953
|
-
|
|
2954
|
-
host: options.host,
|
|
2955
|
-
requestedPort: options.port,
|
|
2956
|
-
workspaceRoot,
|
|
2957
|
-
watchWorkspace: options.watchWorkspace === true,
|
|
2958
|
-
hasRuntime: options.runtime !== undefined,
|
|
2959
|
-
hasAuthToken: Boolean(options.authToken),
|
|
2960
|
-
hasFeatureGate: Boolean(options.featureGate),
|
|
2961
|
-
});
|
|
2962
|
-
const client = new InMemoryJsonRpcLspClient(workspaceRoot, options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS, options.watchWorkspace === true, options.runtime);
|
|
3267
|
+
const client = new InMemoryJsonRpcLspClient(workspaceRoot, options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS, options.runtime);
|
|
2963
3268
|
let openApiSpec = createOpenApiSpec(options.host, options.port);
|
|
2964
3269
|
let currentAccessToken = options.authToken;
|
|
2965
3270
|
const featureGate = options.featureGate;
|
|
@@ -2972,17 +3277,6 @@ export async function startOmlRestServer(options) {
|
|
|
2972
3277
|
const requestId = randomUUID();
|
|
2973
3278
|
const method = req.method ?? 'GET';
|
|
2974
3279
|
const rawUrl = req.url ?? '/';
|
|
2975
|
-
const requestStartedAt = Date.now();
|
|
2976
|
-
debugRest('request.begin', { requestId, method, rawUrl });
|
|
2977
|
-
res.once('finish', () => {
|
|
2978
|
-
debugRest('request.end', {
|
|
2979
|
-
requestId,
|
|
2980
|
-
method,
|
|
2981
|
-
rawUrl,
|
|
2982
|
-
statusCode: res.statusCode,
|
|
2983
|
-
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
|
2984
|
-
});
|
|
2985
|
-
});
|
|
2986
3280
|
try {
|
|
2987
3281
|
const parsed = new URL(rawUrl, `http://${req.headers.host ?? 'localhost'}`);
|
|
2988
3282
|
const pathname = parsed.pathname;
|
|
@@ -3029,20 +3323,22 @@ export async function startOmlRestServer(options) {
|
|
|
3029
3323
|
return;
|
|
3030
3324
|
}
|
|
3031
3325
|
if (method === 'GET' && pathname === '/v0/workspace') {
|
|
3326
|
+
const ontologies = await client.ontologiesWorkspace();
|
|
3327
|
+
const allImported = new Set(ontologies.flatMap((o) => o.imports));
|
|
3328
|
+
const roots = ontologies
|
|
3329
|
+
.filter((o) => !allImported.has(o.ontology))
|
|
3330
|
+
.map((o) => o.ontology);
|
|
3032
3331
|
jsonResponse(res, 200, {
|
|
3033
|
-
workspaceRoot,
|
|
3034
3332
|
workspaceUri: pathToFileURL(workspaceRoot).toString(),
|
|
3333
|
+
roots,
|
|
3035
3334
|
requestId,
|
|
3036
3335
|
});
|
|
3037
3336
|
return;
|
|
3038
3337
|
}
|
|
3039
|
-
if (method === 'GET' && pathname === '/v0/
|
|
3040
|
-
const
|
|
3041
|
-
const workspaceModelFiles = (featureGate && requiredFeature)
|
|
3042
|
-
? await featureGate.runWithFeature(requiredFeature, () => listWorkspaceModelFiles(workspaceRoot), { transport: 'rest', operationId: 'models' })
|
|
3043
|
-
: await listWorkspaceModelFiles(workspaceRoot);
|
|
3338
|
+
if (method === 'GET' && pathname === '/v0/ontologies') {
|
|
3339
|
+
const ontologies = await client.ontologiesWorkspace();
|
|
3044
3340
|
jsonResponse(res, 200, {
|
|
3045
|
-
|
|
3341
|
+
ontologies,
|
|
3046
3342
|
requestId,
|
|
3047
3343
|
});
|
|
3048
3344
|
return;
|
|
@@ -3053,13 +3349,7 @@ export async function startOmlRestServer(options) {
|
|
|
3053
3349
|
if (!routeModelPath) {
|
|
3054
3350
|
throw new RestHttpError(404, `No route for ${method} ${pathname}.`);
|
|
3055
3351
|
}
|
|
3056
|
-
const modelUri =
|
|
3057
|
-
try {
|
|
3058
|
-
await assertModelUriExists(modelUri);
|
|
3059
|
-
}
|
|
3060
|
-
catch {
|
|
3061
|
-
throw new RestHttpError(404, `No OML model found for '${routeModelPath}'.`);
|
|
3062
|
-
}
|
|
3352
|
+
const modelUri = client.resolveModelUriForSparqlModelId(routeModelPath);
|
|
3063
3353
|
const bodyText = method === 'POST' ? await readTextBody(req) : '';
|
|
3064
3354
|
const sparql = parseSparqlQueryFromRequest(req, parsed, bodyText);
|
|
3065
3355
|
if (!sparql) {
|
|
@@ -3087,7 +3377,7 @@ export async function startOmlRestServer(options) {
|
|
|
3087
3377
|
if (!graphFormat) {
|
|
3088
3378
|
throw new RestHttpError(406, 'Not Acceptable for graph results.');
|
|
3089
3379
|
}
|
|
3090
|
-
const serialized = await serializeQuads(queryResult.result.quads, graphFormat.format, false);
|
|
3380
|
+
const serialized = await serializeQuads(queryResult.result.quads, graphFormat.format, false, filterPrefixesForQuads(queryResult.result.quads, client.getWorkspacePrefixes()));
|
|
3091
3381
|
textResponse(res, 200, graphFormat.contentType, serialized);
|
|
3092
3382
|
};
|
|
3093
3383
|
const requiredFeature = requiredFeatureForRestOperation('query');
|
|
@@ -3120,7 +3410,6 @@ export async function startOmlRestServer(options) {
|
|
|
3120
3410
|
}
|
|
3121
3411
|
catch (error) {
|
|
3122
3412
|
const message = error instanceof Error ? error.message : String(error);
|
|
3123
|
-
debugRest('request.error', { requestId, method, rawUrl, message });
|
|
3124
3413
|
if (error instanceof OmlAccessError) {
|
|
3125
3414
|
jsonResponse(res, error.statusCode, accessErrorPayload(error, requestId));
|
|
3126
3415
|
return;
|
|
@@ -3143,18 +3432,15 @@ export async function startOmlRestServer(options) {
|
|
|
3143
3432
|
server.listen(options.port, options.host, () => resolve());
|
|
3144
3433
|
});
|
|
3145
3434
|
const listeningPort = resolveListeningPort(server);
|
|
3146
|
-
debugRest('rest.start.listening', { host: options.host, requestedPort: options.port, listeningPort });
|
|
3147
3435
|
openApiSpec = createOpenApiSpec(options.host, listeningPort);
|
|
3148
3436
|
return {
|
|
3149
3437
|
server,
|
|
3150
3438
|
updateToken: async (token) => {
|
|
3151
|
-
debugRest('token.update.begin', { tokenProvided: Boolean(token) });
|
|
3152
3439
|
currentAccessToken = token;
|
|
3153
3440
|
featureGate?.setAccessToken(token);
|
|
3154
3441
|
if (featureGate) {
|
|
3155
3442
|
await featureGate.primeEntitlements();
|
|
3156
3443
|
}
|
|
3157
|
-
debugRest('token.update.end');
|
|
3158
3444
|
},
|
|
3159
3445
|
};
|
|
3160
3446
|
}
|