@oml/server 0.14.17 → 0.15.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.
@@ -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, '&quot;')
254
464
  .replace(/'/g, '&#39;');
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 File</span>
803
- <input id="modelUri" type="text" spellcheck="false" placeholder="Select an OML file from Browse" readonly>
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 Files</div>
863
- <div class="dialog-subtitle">Select the model file to run the query against.</div>
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 files by path" spellcheck="false">
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 modelUriInput = document.getElementById('modelUri');
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 selectedModelId = '';
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 files found for the current filter.</div>';
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 normalizedPath = String(entry.path || '').trim();
1005
- const modelId = normalizedPath.toLowerCase().endsWith('.oml')
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-model-id="' + escapeClientHtml(modelId) + '" data-path="' + escapeClientHtml(entry.path) + '">'
1011
- + '<div class="browse-entry-path" title="' + escapeClientHtml(entry.path) + '">' + escapeClientHtml(entry.path) + '</div>'
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.path.toLowerCase().includes(filter));
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 files...';
1217
+ browseStatus.textContent = 'Loading workspace OML ontologies...';
1026
1218
  browseStatus.className = 'status';
1027
1219
  try {
1028
- const response = await fetch(routeUrl('v0/models'));
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 files.');
1223
+ throw new Error(payload.error || 'Unable to list workspace OML ontologies.');
1032
1224
  }
1033
- browseEntries = Array.isArray(payload.files) ? payload.files : [];
1034
- browseStatus.textContent = 'Loaded ' + browseEntries.length + ' workspace OML file(s).';
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 files.</div>';
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 validateModelId(modelId) {
1046
- if (!modelId) {
1047
- return 'OML file is required.';
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 modelId = selectedModelId.trim();
1245
+ const iri = selectedOntologyIri.trim();
1057
1246
  const sparql = sparqlInput.value.trim();
1058
- const modelIdError = validateModelId(modelId);
1059
- if (modelIdError) {
1060
- setStatus(modelIdError, 'error');
1061
- rawOutput.textContent = JSON.stringify({ error: modelIdError }, null, 2);
1062
- resetTable('Choose a valid OML model file to run a query.');
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-model-id]') : null;
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 modelId = target.getAttribute('data-model-id');
1150
- const relativePath = target.getAttribute('data-path');
1151
- if (!modelId) {
1339
+ const iri = target.getAttribute('data-ontology-iri');
1340
+ if (!iri) {
1152
1341
  return;
1153
1342
  }
1154
- selectedModelId = modelId;
1155
- modelUriInput.value = relativePath || '';
1156
- setStatus('Selected workspace OML file.', 'success');
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 normalizeModelUri(workspaceRoot, input) {
1237
- const trimmed = input.trim();
1238
- const normalizedWorkspace = path.resolve(workspaceRoot);
1239
- if (trimmed.startsWith('file://')) {
1240
- const filePath = fileURLToPath(trimmed);
1241
- if (filePath !== normalizedWorkspace && !filePath.startsWith(`${normalizedWorkspace}${path.sep}`)) {
1242
- throw new Error('Model URI escapes the workspace root.');
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 resolveModelUriFromRouteModelPath(workspaceRoot, routeModelPath) {
1257
- const normalized = routeModelPath.trim().replace(/^\/+|\/+$/g, '');
1433
+ function resolveModelUriFromIriModelId(docs, ontologyIndex, modelId) {
1434
+ const normalized = modelId.trim().replace(/^\/+|\/+$/g, '');
1258
1435
  if (!normalized) {
1259
- throw new RestHttpError(400, 'Missing model path in SPARQL endpoint route.');
1260
- }
1261
- if (path.isAbsolute(normalized)) {
1262
- throw new RestHttpError(400, 'SPARQL model path must be workspace-relative.');
1263
- }
1264
- if (normalized.toLowerCase().endsWith('.oml')) {
1265
- throw new RestHttpError(400, "SPARQL model path must not include '.oml'. Use workspace-relative path without extension.");
1436
+ throw new RestHttpError(400, 'Missing model ID in SPARQL endpoint route.');
1266
1437
  }
1267
- const candidate = path.resolve(workspaceRoot, `${normalized}.oml`);
1268
- const normalizedWorkspace = path.resolve(workspaceRoot);
1269
- if (candidate !== normalizedWorkspace && !candidate.startsWith(`${normalizedWorkspace}${path.sep}`)) {
1270
- throw new RestHttpError(400, 'SPARQL model path escapes the workspace root.');
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
- return pathToFileURL(candidate).toString();
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
- const iri = new URL(ontologyIri);
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
- async function serializeQuads(quads, format, pretty = false) {
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) => {
@@ -1682,11 +1874,12 @@ function sanitizeBlockId(value) {
1682
1874
  return trimmed.replace(/[^a-zA-Z0-9._-]/g, '_');
1683
1875
  }
1684
1876
  async function writeBlockArtifacts(outputFile, results) {
1877
+ const blockDirName = `${path.basename(outputFile, '.html')}.blocks`;
1878
+ const blockDir = path.join(path.dirname(outputFile), blockDirName);
1879
+ await fs.rm(blockDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
1685
1880
  if (results.length === 0) {
1686
1881
  return { count: 0, manifest: [] };
1687
1882
  }
1688
- const blockDirName = `${path.basename(outputFile, '.html')}.blocks`;
1689
- const blockDir = path.join(path.dirname(outputFile), blockDirName);
1690
1883
  await fs.mkdir(blockDir, { recursive: true });
1691
1884
  const manifest = [];
1692
1885
  for (const result of results) {
@@ -1881,7 +2074,7 @@ ${content}
1881
2074
  `;
1882
2075
  }
1883
2076
  class InMemoryJsonRpcLspClient {
1884
- constructor(workspaceRoot, requestTimeoutMs, watchWorkspace, runtime) {
2077
+ constructor(workspaceRoot, requestTimeoutMs, runtime) {
1885
2078
  this.watchers = new Set();
1886
2079
  this.watcherActive = false;
1887
2080
  this.refreshInFlight = false;
@@ -1890,13 +2083,6 @@ class InMemoryJsonRpcLspClient {
1890
2083
  this.workspaceRoot = workspaceRoot;
1891
2084
  this.requestTimeoutMs = requestTimeoutMs;
1892
2085
  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
2086
  if (runtime) {
1901
2087
  this.runtime = runtime;
1902
2088
  }
@@ -1913,9 +2099,17 @@ class InMemoryJsonRpcLspClient {
1913
2099
  this.clientConnection = createConnection(this.serverToClient, this.clientToServer);
1914
2100
  this.clientConnection.listen();
1915
2101
  }
1916
- if (this.watchWorkspace) {
1917
- this.startWorkspaceWatcher();
2102
+ this.startWorkspaceWatcher();
2103
+ }
2104
+ getWorkspacePrefixes() {
2105
+ const prefixes = {};
2106
+ for (const doc of this.getWorkspaceOmlDocuments()) {
2107
+ const root = doc?.parseResult?.value;
2108
+ if (root?.namespace && root?.prefix) {
2109
+ prefixes[root.prefix] = root.namespace;
2110
+ }
1918
2111
  }
2112
+ return prefixes;
1919
2113
  }
1920
2114
  getWorkspaceOmlDocuments() {
1921
2115
  const documents = this.runtime.shared.workspace.LangiumDocuments;
@@ -1927,206 +2121,38 @@ class InMemoryJsonRpcLspClient {
1927
2121
  .filter((doc) => String(doc?.uri ?? '').trim().toLowerCase().endsWith('.oml'))
1928
2122
  .sort((left, right) => String(left?.uri ?? '').localeCompare(String(right?.uri ?? '')));
1929
2123
  }
1930
- summarizeDiagnostics(docs) {
1931
- let diagnostics = 0;
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
- };
2124
+ async waitForValidatedWithTrace() {
2125
+ await this.runtime.shared.workspace.DocumentBuilder.waitUntil(DocumentState.Validated);
1967
2126
  }
1968
- summarizeDocumentStates(docs) {
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
- });
2127
+ async lintWorkspace(params = {}) {
2091
2128
  const context = {
2092
2129
  workspaceRoot: this.workspaceRoot,
2093
2130
  runtime: this.runtime,
2094
2131
  ensureInitialized: () => this.ensureInitialized(),
2095
- ensureWorkspaceCurrent: () => this.ensureWorkspaceCurrent(),
2132
+ ensureWorkspaceCurrent: () => this.ensureWorkspaceSettled(false),
2133
+ waitForValidated: () => this.waitForValidatedWithTrace(),
2096
2134
  getWorkspaceOmlDocuments: () => this.getWorkspaceOmlDocuments(),
2097
2135
  };
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
- });
2136
+ const result = await lintWorkspace(context, params);
2111
2137
  return result;
2112
2138
  }
2113
2139
  async queryWorkspaceRaw(modelUri, sparql) {
2114
2140
  await this.ensureInitialized();
2115
- await this.ensureWorkspaceCurrent();
2141
+ await this.ensureWorkspaceSettled(false);
2116
2142
  const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
2117
2143
  await reasoningService.ensureQueryContext(modelUri);
2118
2144
  return await reasoningService.getSparqlService().query(modelUri, sparql);
2119
2145
  }
2120
2146
  async askWorkspaceRaw(modelUri, sparql) {
2121
2147
  await this.ensureInitialized();
2122
- await this.ensureWorkspaceCurrent();
2148
+ await this.ensureWorkspaceSettled(false);
2123
2149
  const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
2124
2150
  await reasoningService.ensureQueryContext(modelUri);
2125
2151
  return await reasoningService.getSparqlService().ask(modelUri, sparql);
2126
2152
  }
2127
2153
  async constructWorkspaceRaw(modelUri, sparql) {
2128
2154
  await this.ensureInitialized();
2129
- await this.ensureWorkspaceCurrent();
2155
+ await this.ensureWorkspaceSettled(false);
2130
2156
  const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
2131
2157
  await reasoningService.ensureQueryContext(modelUri);
2132
2158
  return await reasoningService.getSparqlService().construct(modelUri, sparql);
@@ -2197,7 +2223,8 @@ class InMemoryJsonRpcLspClient {
2197
2223
  }
2198
2224
  async updateWorkspace(params) {
2199
2225
  await this.ensureInitialized();
2200
- await this.ensureWorkspaceCurrent();
2226
+ await this.ensureWorkspaceSettled(false);
2227
+ const preview = params.preview === true;
2201
2228
  const lint = await this.lintWorkspace({});
2202
2229
  if (lint.errors > 0 || lint.warnings > 0) {
2203
2230
  return {
@@ -2208,11 +2235,125 @@ class InMemoryJsonRpcLspClient {
2208
2235
  error: `lint failed with ${lint.errors} error(s) and ${lint.warnings} warning(s).`,
2209
2236
  };
2210
2237
  }
2211
- return await applyOmlUpdate(this.runtime.shared, params, (message) => this.logError(message));
2238
+ const result = await applyOmlUpdate(this.runtime.shared, normalizeUpdateRequest(params), (message) => this.logError(message), pathToFileURL(this.workspaceRoot).toString(), preview);
2239
+ const errors = Array.isArray(result?.errors) ? result.errors : [];
2240
+ if (errors.length > 0) {
2241
+ return result;
2242
+ }
2243
+ const edit = result?.edit;
2244
+ const changedFiles = [];
2245
+ if (edit) {
2246
+ if (preview) {
2247
+ changedFiles.push(...collectWorkspaceEditChangedFiles(edit));
2248
+ const changedUris = collectWorkspaceEditReloadUris(edit);
2249
+ if (changedUris.length > 0) {
2250
+ await this.runtime.shared.workspace.DocumentBuilder.update(changedUris, []);
2251
+ await this.waitForValidatedWithTrace();
2252
+ }
2253
+ }
2254
+ else {
2255
+ try {
2256
+ const deletedUriStrings = new Set((edit.documentChanges ?? [])
2257
+ .filter((change) => change?.kind === 'delete' && typeof change?.uri === 'string')
2258
+ .map((change) => String(change.uri)));
2259
+ changedFiles.push(...await applyWorkspaceEditToFiles(edit));
2260
+ if (changedFiles.length > 0) {
2261
+ const changedUris = changedFiles
2262
+ .filter((uri) => !deletedUriStrings.has(uri))
2263
+ .map((uri) => URI.parse(uri));
2264
+ const deletedUris = [...deletedUriStrings].map((uri) => URI.parse(uri));
2265
+ await this.runtime.shared.workspace.DocumentBuilder.update(changedUris, deletedUris);
2266
+ await this.waitForValidatedWithTrace();
2267
+ if (changedUris.length > 0) {
2268
+ await this.forceRevalidateUris(changedUris);
2269
+ }
2270
+ // Force one canonical workspace refresh cycle so diagnostics are computed
2271
+ // from the latest on-disk snapshot, not a transient intermediate version.
2272
+ await this.refreshWorkspaceOmlDocuments();
2273
+ }
2274
+ }
2275
+ catch (error) {
2276
+ const message = error instanceof Error ? error.message : String(error);
2277
+ return {
2278
+ ...result,
2279
+ errors: [{ operationIndex: -1, message }],
2280
+ error: message,
2281
+ };
2282
+ }
2283
+ }
2284
+ }
2285
+ if (preview) {
2286
+ const diagnostics = await this.lintWorkspaceStable({});
2287
+ return {
2288
+ ...result,
2289
+ changedFiles,
2290
+ diagnostics: {
2291
+ filesChecked: diagnostics.filesChecked,
2292
+ errors: diagnostics.errors,
2293
+ warnings: diagnostics.warnings,
2294
+ problems: diagnostics.problems,
2295
+ },
2296
+ success: true,
2297
+ preview: true,
2298
+ };
2299
+ }
2300
+ const diagnostics = await this.lintWorkspaceStable({});
2301
+ return {
2302
+ ...result,
2303
+ changedFiles,
2304
+ diagnostics: {
2305
+ filesChecked: diagnostics.filesChecked,
2306
+ errors: diagnostics.errors,
2307
+ warnings: diagnostics.warnings,
2308
+ problems: diagnostics.problems,
2309
+ },
2310
+ success: true,
2311
+ };
2312
+ }
2313
+ lintFingerprint(lint) {
2314
+ const problems = lint.problems
2315
+ .map((problem) => [
2316
+ problem.uri,
2317
+ problem.line,
2318
+ problem.column,
2319
+ problem.severity,
2320
+ problem.kind,
2321
+ problem.source ?? '',
2322
+ problem.code ?? '',
2323
+ problem.message,
2324
+ ].join('|'))
2325
+ .join('\n');
2326
+ return `${lint.filesChecked}|${lint.errors}|${lint.warnings}\n${problems}`;
2327
+ }
2328
+ async lintWorkspaceStable(params) {
2329
+ let previous;
2330
+ let previousFingerprint = '';
2331
+ for (let i = 0; i < 6; i += 1) {
2332
+ await this.waitForWatcherRefreshIdle();
2333
+ await this.waitForValidatedWithTrace();
2334
+ const current = await this.lintWorkspace(params);
2335
+ const currentFingerprint = this.lintFingerprint(current);
2336
+ if (previous && previousFingerprint === currentFingerprint) {
2337
+ return current;
2338
+ }
2339
+ previous = current;
2340
+ previousFingerprint = currentFingerprint;
2341
+ }
2342
+ return previous ?? await this.lintWorkspace(params);
2343
+ }
2344
+ async forceRevalidateUris(changedUris) {
2345
+ const documents = this.runtime.shared.workspace.LangiumDocuments;
2346
+ const builder = this.runtime.shared.workspace.DocumentBuilder;
2347
+ const docs = await Promise.all(changedUris.map((uri) => documents.getOrCreateDocument(uri)));
2348
+ if (docs.length === 0) {
2349
+ return;
2350
+ }
2351
+ await builder.build(docs, { validation: true });
2352
+ await this.waitForValidatedWithTrace();
2212
2353
  }
2213
2354
  async fuzzySearchWorkspace(params) {
2214
2355
  await this.ensureInitialized();
2215
- await this.ensureWorkspaceCurrent();
2356
+ await this.ensureWorkspaceSettled(false);
2216
2357
  try {
2217
2358
  const text = (typeof params.text === 'string' ? params.text : '').trim();
2218
2359
  if (!text) {
@@ -2229,7 +2370,9 @@ class InMemoryJsonRpcLspClient {
2229
2370
  const candidateByIri = new Map();
2230
2371
  for (const doc of iterable) {
2231
2372
  const root = doc?.parseResult?.value;
2232
- if (!root || !isOntology(root) || (!isVocabulary(root) && !isDescription(root))) {
2373
+ const ontologyOk = !!root && isOntology(root);
2374
+ const vocabOrDescOk = ontologyOk && (isVocabulary(root) || isDescription(root));
2375
+ if (!vocabOrDescOk) {
2233
2376
  continue;
2234
2377
  }
2235
2378
  modelUris.push(doc.uri.toString());
@@ -2284,7 +2427,7 @@ class InMemoryJsonRpcLspClient {
2284
2427
  }
2285
2428
  }
2286
2429
  return {
2287
- iri: entry.iri,
2430
+ member: entry.iri,
2288
2431
  label: entry.label,
2289
2432
  score: ranked.length - index,
2290
2433
  diagnostics: {
@@ -2311,7 +2454,7 @@ class InMemoryJsonRpcLspClient {
2311
2454
  }
2312
2455
  async assertionsWorkspace(params) {
2313
2456
  await this.ensureInitialized();
2314
- await this.ensureWorkspaceCurrent();
2457
+ await this.ensureWorkspaceSettled(false);
2315
2458
  const lint = await this.lintWorkspace({});
2316
2459
  if (lint.errors > 0 || lint.warnings > 0) {
2317
2460
  return {
@@ -2324,23 +2467,24 @@ class InMemoryJsonRpcLspClient {
2324
2467
  error: `lint failed with ${lint.errors} error(s) and ${lint.warnings} warning(s).`,
2325
2468
  };
2326
2469
  }
2327
- const modelUriFilterRaw = typeof params.modelUri === 'string' ? params.modelUri.trim() : '';
2470
+ const ontologyIriFilterRaw = typeof params.ontologyIri === 'string' ? params.ontologyIri.trim() : '';
2328
2471
  let modelUriFilter = '';
2329
- if (modelUriFilterRaw.length > 0) {
2330
- try {
2331
- modelUriFilter = normalizeModelUri(this.workspaceRoot, modelUriFilterRaw);
2332
- }
2333
- catch (e) {
2472
+ if (ontologyIriFilterRaw.length > 0) {
2473
+ const ontologyIndex = getOntologyModelIndex(this.runtime.shared);
2474
+ const resolved = ontologyIndex.resolveModelUri(ontologyIriFilterRaw);
2475
+ if (!resolved) {
2334
2476
  return {
2335
2477
  success: false,
2336
2478
  files: [],
2337
- error: e instanceof Error ? e.message : String(e),
2479
+ error: `No model found for ontology IRI '${ontologyIriFilterRaw}'.`,
2338
2480
  };
2339
2481
  }
2482
+ modelUriFilter = resolved;
2340
2483
  }
2341
2484
  const format = normalizeRdfSerializationFormat(params.format);
2342
2485
  const pretty = params.pretty === true;
2343
2486
  const docs = this.getWorkspaceOmlDocuments();
2487
+ const workspacePrefixes = this.getWorkspacePrefixes();
2344
2488
  const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
2345
2489
  const store = reasoningService.getStore().getStore();
2346
2490
  const files = [];
@@ -2360,8 +2504,7 @@ class InMemoryJsonRpcLspClient {
2360
2504
  files.push({
2361
2505
  modelUri,
2362
2506
  ontologyIri,
2363
- path: resolveOutputPathFromOntologyIriString(ontologyIri, format).split(path.sep).join('/'),
2364
- content: await serializeQuads(quads, format, pretty),
2507
+ content: await serializeQuads(quads, format, pretty, filterPrefixesForQuads(quads, workspacePrefixes)),
2365
2508
  });
2366
2509
  }
2367
2510
  files.sort((left, right) => left.ontologyIri.localeCompare(right.ontologyIri));
@@ -2373,6 +2516,7 @@ class InMemoryJsonRpcLspClient {
2373
2516
  async writeWorkspaceAssertedOwl(outputDir, format, pretty) {
2374
2517
  const entries = [];
2375
2518
  const docs = this.getWorkspaceOmlDocuments();
2519
+ const workspacePrefixes = this.getWorkspacePrefixes();
2376
2520
  const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
2377
2521
  const store = reasoningService.getStore().getStore();
2378
2522
  for (const doc of docs) {
@@ -2387,7 +2531,7 @@ class InMemoryJsonRpcLspClient {
2387
2531
  .map((quad) => DataFactory.quad(quad.subject, quad.predicate, quad.object));
2388
2532
  const owlPath = path.join(outputDir, resolveOutputPathFromOntologyIriString(ontologyIri, format));
2389
2533
  await fs.mkdir(path.dirname(owlPath), { recursive: true });
2390
- await fs.writeFile(owlPath, await serializeQuads(quads, format, pretty), 'utf-8');
2534
+ await fs.writeFile(owlPath, await serializeQuads(quads, format, pretty, filterPrefixesForQuads(quads, workspacePrefixes)), 'utf-8');
2391
2535
  entries.push({ modelUri, ontologyIri, owlPath });
2392
2536
  }
2393
2537
  entries.sort((left, right) => left.ontologyIri.localeCompare(right.ontologyIri));
@@ -2398,7 +2542,8 @@ class InMemoryJsonRpcLspClient {
2398
2542
  workspaceRoot: this.workspaceRoot,
2399
2543
  runtime: this.runtime,
2400
2544
  ensureInitialized: () => this.ensureInitialized(),
2401
- ensureWorkspaceCurrent: () => this.ensureWorkspaceCurrent(),
2545
+ ensureWorkspaceCurrent: () => this.ensureWorkspaceSettled(true),
2546
+ waitForValidated: () => this.waitForValidatedWithTrace(),
2402
2547
  getWorkspaceOmlDocuments: () => this.getWorkspaceOmlDocuments(),
2403
2548
  };
2404
2549
  return await validateWorkspace(context, params);
@@ -2418,7 +2563,7 @@ class InMemoryJsonRpcLspClient {
2418
2563
  const context = {
2419
2564
  workspaceRoot: this.workspaceRoot,
2420
2565
  ensureInitialized: () => this.ensureInitialized(),
2421
- ensureWorkspaceCurrent: () => this.ensureWorkspaceCurrent(),
2566
+ ensureWorkspaceCurrent: () => this.ensureWorkspaceSettled(false),
2422
2567
  writeWorkspaceAssertedOwl: (outputDir, format, pretty) => this.writeWorkspaceAssertedOwl(outputDir, format, pretty),
2423
2568
  exportAssertedWorkspace: (options) => exportAssertedWorkspace(context, options),
2424
2569
  };
@@ -2528,7 +2673,7 @@ class InMemoryJsonRpcLspClient {
2528
2673
  const context = {
2529
2674
  workspaceRoot: this.workspaceRoot,
2530
2675
  ensureInitialized: () => this.ensureInitialized(),
2531
- ensureWorkspaceCurrent: () => this.ensureWorkspaceCurrent(),
2676
+ ensureWorkspaceCurrent: () => this.ensureWorkspaceSettled(false),
2532
2677
  writeWorkspaceAssertedOwl: (outputDir, format, pretty) => this.writeWorkspaceAssertedOwl(outputDir, format, pretty),
2533
2678
  exportAssertedWorkspace: (options) => exportAssertedWorkspace(context, options),
2534
2679
  };
@@ -2536,7 +2681,7 @@ class InMemoryJsonRpcLspClient {
2536
2681
  }
2537
2682
  async renderWorkspace(params) {
2538
2683
  await this.ensureInitialized();
2539
- await this.ensureWorkspaceCurrent();
2684
+ await this.ensureWorkspaceSettled(false);
2540
2685
  const lint = await this.lintWorkspace({});
2541
2686
  if (lint.errors > 0 || lint.warnings > 0) {
2542
2687
  return {
@@ -2554,9 +2699,7 @@ class InMemoryJsonRpcLspClient {
2554
2699
  const outputFromOptions = typeof params.web === 'string' && params.web.trim().length > 0 ? params.web.trim() : 'build/web';
2555
2700
  const inputDir = path.resolve(workspaceRoot, inputFromOptions);
2556
2701
  const outputDir = path.resolve(workspaceRoot, outputFromOptions);
2557
- if (params.clean === true) {
2558
- await fs.rm(outputDir, { recursive: true, force: true });
2559
- }
2702
+ await fs.rm(outputDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
2560
2703
  const runtime = new MarkdownPreviewRuntime(new MarkdownHandlerRegistry());
2561
2704
  const templateCatalog = await buildTemplateCatalog(workspaceRoot);
2562
2705
  const navigationTemplateCatalog = buildNavigationTemplateCatalog(Array.from(templateCatalog.values()).flatMap((entries) => entries.map((entry) => entry.definition)));
@@ -2603,7 +2746,7 @@ class InMemoryJsonRpcLspClient {
2603
2746
  continue;
2604
2747
  }
2605
2748
  const frontMatter = extractLeadingFrontMatter(markdown);
2606
- const contextOntologyIri = normalizeContextOntologyIri(frontMatter?.contextOntologyIri
2749
+ const contextOntologyIri = normalizeContextOntologyIri(frontMatter?.ontology
2607
2750
  ?? frontMatterString(frontMatter?.data, 'ontologyIri')
2608
2751
  ?? frontMatterString(frontMatter?.data, 'ontology'));
2609
2752
  const contextMemberIri = frontMatterString(frontMatter?.data, 'memberIri')
@@ -2669,6 +2812,7 @@ class InMemoryJsonRpcLspClient {
2669
2812
  filesRendered += 1;
2670
2813
  }
2671
2814
  const renderedNavigationPages = new Set();
2815
+ const navTasks = [];
2672
2816
  for (const modelUri of contextModelUris) {
2673
2817
  await reasoningService.ensureQueryContext(modelUri);
2674
2818
  const membersToTypes = await queryMemberTypes(reasoningService, modelUri);
@@ -2717,55 +2861,60 @@ class InMemoryJsonRpcLspClient {
2717
2861
  const syntheticSourcePath = resolution.template.sourceUri?.startsWith('file:')
2718
2862
  ? decodeURIComponent(new URL(resolution.template.sourceUri).pathname)
2719
2863
  : path.join(workspaceRoot, 'index.md');
2720
- const expanded = await expandTemplateComposeBlocks(rendered.output, templateCatalog, {
2721
- workspaceRoot,
2722
- sourceMarkdownPath: syntheticSourcePath,
2723
- contextOntologyIri,
2724
- contextModelUri: modelUri,
2725
- contextMemberIri: memberIri,
2726
- });
2727
- const prepared = runtime.prepare(expanded);
2728
- const rewriteResult = rewriteRenderedLinks(prepared.renderedHtml, {
2729
- workspaceRoot,
2730
- inputRoot: inputDir,
2731
- inputFile: syntheticSourcePath,
2732
- outputRoot: outputDir,
2733
- outputFile: targetFile,
2734
- });
2735
- for (const asset of rewriteResult.workspaceAssets) {
2736
- workspaceAssetFiles.add(asset);
2737
- }
2738
- const executableBlocks = toExecutableBlocks(prepared.codeBlocks);
2739
- const optionsByBlockId = new Map(prepared.codeBlocks.map((block) => [block.id, block.options]));
2740
- const blockResults = executableBlocks.length === 0
2741
- ? []
2742
- : (await executor.executeBlocks({
2743
- markdownUri: sourceDocumentUri,
2744
- modelUri,
2745
- blocks: executableBlocks,
2746
- })).results.map((result) => ({
2747
- ...result,
2748
- options: optionsByBlockId.get(result.blockId),
2749
- }));
2750
- const rewrittenBlockResults = blockResults.map((result) => rewriteBlockResultAssetPaths(result, {
2751
- workspaceRoot,
2752
- inputRoot: inputDir,
2753
- sourceFile: syntheticSourcePath,
2754
- outputRoot: outputDir,
2755
- outputFile: targetFile,
2756
- }, workspaceAssetFiles));
2757
- const blockArtifacts = await writeBlockArtifacts(targetFile, rewrittenBlockResults);
2758
- blockArtifactFiles += blockArtifacts.count;
2759
- renderJobs.push({
2760
- htmlPath: targetFile,
2761
- renderedHtml: rewriteResult.html,
2762
- blockManifest: blockArtifacts.manifest,
2763
- blockResults: rewrittenBlockResults,
2764
- wikiLinkHrefByKey: buildWikiLinkHrefMapForPage(wikiPageIndex, targetFile),
2864
+ navTasks.push(async () => {
2865
+ const expanded = await expandTemplateComposeBlocks(rendered.output, templateCatalog, {
2866
+ workspaceRoot,
2867
+ sourceMarkdownPath: syntheticSourcePath,
2868
+ contextOntologyIri,
2869
+ contextModelUri: modelUri,
2870
+ contextMemberIri: memberIri,
2871
+ });
2872
+ const prepared = runtime.prepare(expanded);
2873
+ const rewriteResult = rewriteRenderedLinks(prepared.renderedHtml, {
2874
+ workspaceRoot,
2875
+ inputRoot: inputDir,
2876
+ inputFile: syntheticSourcePath,
2877
+ outputRoot: outputDir,
2878
+ outputFile: targetFile,
2879
+ });
2880
+ for (const asset of rewriteResult.workspaceAssets) {
2881
+ workspaceAssetFiles.add(asset);
2882
+ }
2883
+ const executableBlocks = toExecutableBlocks(prepared.codeBlocks);
2884
+ const optionsByBlockId = new Map(prepared.codeBlocks.map((block) => [block.id, block.options]));
2885
+ const blockResults = executableBlocks.length === 0
2886
+ ? []
2887
+ : (await executor.executeBlocks({
2888
+ markdownUri: sourceDocumentUri,
2889
+ modelUri,
2890
+ blocks: executableBlocks,
2891
+ })).results.map((result) => ({
2892
+ ...result,
2893
+ options: optionsByBlockId.get(result.blockId),
2894
+ }));
2895
+ const rewrittenBlockResults = blockResults.map((result) => rewriteBlockResultAssetPaths(result, {
2896
+ workspaceRoot,
2897
+ inputRoot: inputDir,
2898
+ sourceFile: syntheticSourcePath,
2899
+ outputRoot: outputDir,
2900
+ outputFile: targetFile,
2901
+ }, workspaceAssetFiles));
2902
+ const blockArtifacts = await writeBlockArtifacts(targetFile, rewrittenBlockResults);
2903
+ blockArtifactFiles += blockArtifacts.count;
2904
+ renderJobs.push({
2905
+ htmlPath: targetFile,
2906
+ renderedHtml: rewriteResult.html,
2907
+ blockManifest: blockArtifacts.manifest,
2908
+ blockResults: rewrittenBlockResults,
2909
+ wikiLinkHrefByKey: buildWikiLinkHrefMapForPage(wikiPageIndex, targetFile),
2910
+ });
2911
+ filesRendered += 1;
2765
2912
  });
2766
- filesRendered += 1;
2767
2913
  }
2768
2914
  }
2915
+ for (const task of navTasks) {
2916
+ await task();
2917
+ }
2769
2918
  for (const job of renderJobs) {
2770
2919
  const runtimeScriptRelative = toRelativeWebPath(path.dirname(job.htmlPath), staticAssets.runtimeScriptFile);
2771
2920
  const stylesheetRelative = toRelativeWebPath(path.dirname(job.htmlPath), staticAssets.stylesheetFile);
@@ -2799,35 +2948,145 @@ class InMemoryJsonRpcLspClient {
2799
2948
  }
2800
2949
  return { success: true, filesRendered, outputDir, blockArtifactFiles };
2801
2950
  }
2802
- async ensureWorkspaceCurrent() {
2803
- debugRest('workspace.current.begin', {
2804
- useExternalRuntime: this.useExternalRuntime,
2805
- watchWorkspace: this.watchWorkspace,
2806
- initialWorkspaceSyncCompleted: this.initialWorkspaceSyncCompleted,
2807
- });
2808
- if (this.useExternalRuntime) {
2809
- debugRest('workspace.current.skip.external');
2810
- return;
2951
+ async ontologiesWorkspace() {
2952
+ await this.ensureInitialized();
2953
+ await this.ensureWorkspaceSettled(false);
2954
+ return getOntologiesFromRuntime(this.runtime);
2955
+ }
2956
+ resolveModelUriForSparqlModelId(modelId) {
2957
+ const ontologyIndex = getOntologyModelIndex(this.runtime.shared);
2958
+ const docs = this.getWorkspaceOmlDocuments();
2959
+ return resolveModelUriFromIriModelId(docs, ontologyIndex, modelId);
2960
+ }
2961
+ async shapesWorkspace(params) {
2962
+ await this.ensureInitialized();
2963
+ await this.ensureWorkspaceSettled(true);
2964
+ const typeIri = typeof params.type === 'string' ? params.type.trim() : '';
2965
+ if (!typeIri) {
2966
+ return {
2967
+ success: false,
2968
+ error: 'Missing required parameter: type',
2969
+ };
2811
2970
  }
2812
- if (!this.watchWorkspace) {
2813
- if (!this.initialWorkspaceSyncCompleted) {
2814
- await this.bootstrapWorkspaceOmlDocuments();
2815
- this.initialWorkspaceSyncCompleted = true;
2816
- debugRest('workspace.current.initial.bootstrap.complete');
2971
+ try {
2972
+ const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
2973
+ const reasoningStoreWrapper = reasoningService.getStore();
2974
+ const store = reasoningStoreWrapper.getStore();
2975
+ const OML_CONTEXT_ONTOLOGY = 'http://opencaesar.io/oml#contextOntology';
2976
+ const SH_NS = 'http://www.w3.org/ns/shacl#';
2977
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
2978
+ const shapeEntries = [];
2979
+ // Find all markdown graph URIs
2980
+ const markdownGraphs = new Set();
2981
+ const contextQuads = store.getQuads(null, DataFactory.namedNode(OML_CONTEXT_ONTOLOGY), null, null);
2982
+ for (const quad of contextQuads) {
2983
+ if (quad.subject.termType === 'NamedNode' && quad.object.termType === 'NamedNode') {
2984
+ markdownGraphs.add(quad.subject.value);
2985
+ }
2817
2986
  }
2818
- else {
2819
- await this.refreshWorkspaceOmlDocuments();
2987
+ // Query each markdown graph for shapes matching the type
2988
+ for (const graphUri of markdownGraphs) {
2989
+ const graphNode = DataFactory.namedNode(graphUri);
2990
+ const ontologyQuad = store.getQuads(DataFactory.namedNode(graphUri), DataFactory.namedNode(OML_CONTEXT_ONTOLOGY), null, null)[0];
2991
+ if (!ontologyQuad || ontologyQuad.object.termType !== 'NamedNode') {
2992
+ continue;
2993
+ }
2994
+ const ontologyIri = ontologyQuad.object.value;
2995
+ // Find NodeShapes in this graph
2996
+ const nodeShapeQuads = store.getQuads(null, DataFactory.namedNode(RDF_TYPE), DataFactory.namedNode(`${SH_NS}NodeShape`), graphNode);
2997
+ for (const shapeQuad of nodeShapeQuads) {
2998
+ if (shapeQuad.subject.termType !== 'NamedNode') {
2999
+ continue;
3000
+ }
3001
+ const shapeIri = shapeQuad.subject.value;
3002
+ // Check if this shape targets our type (sh:targetClass)
3003
+ const targetClassQuads = store.getQuads(shapeQuad.subject, DataFactory.namedNode(`${SH_NS}targetClass`), DataFactory.namedNode(typeIri), graphNode);
3004
+ if (targetClassQuads.length > 0) {
3005
+ shapeEntries.push({ ontology: ontologyIri, shape: shapeIri, graphUri });
3006
+ continue;
3007
+ }
3008
+ // Check if any property shape has sh:class matching our type
3009
+ const propertyQuads = store.getQuads(shapeQuad.subject, DataFactory.namedNode(`${SH_NS}property`), null, graphNode);
3010
+ for (const propQuad of propertyQuads) {
3011
+ if (propQuad.object.termType === 'NamedNode' || propQuad.object.termType === 'BlankNode') {
3012
+ const classQuads = store.getQuads(propQuad.object, DataFactory.namedNode(`${SH_NS}class`), DataFactory.namedNode(typeIri), graphNode);
3013
+ if (classQuads.length > 0) {
3014
+ shapeEntries.push({ ontology: ontologyIri, shape: shapeIri, graphUri });
3015
+ break;
3016
+ }
3017
+ }
3018
+ }
3019
+ }
2820
3020
  }
2821
- debugRest('workspace.current.refreshed.nowatch');
2822
- return;
3021
+ // Helper to collect all axioms for a shape (including blank nodes)
3022
+ const collectShapeAxioms = (shapeIri, graphUri) => {
3023
+ const graphNode = DataFactory.namedNode(graphUri);
3024
+ const visited = new Set();
3025
+ const axioms = [];
3026
+ const writer = new Writer({ format: 'N-Triples' });
3027
+ const collectNode = (node) => {
3028
+ const nodeKey = node.termType === 'BlankNode' ? `_:${node.value}` : node.value;
3029
+ if (visited.has(nodeKey))
3030
+ return;
3031
+ visited.add(nodeKey);
3032
+ const quads = store.getQuads(node, null, null, graphNode);
3033
+ for (const quad of quads) {
3034
+ // Convert quad to triple (without graph)
3035
+ writer.addQuad(DataFactory.quad(quad.subject, quad.predicate, quad.object));
3036
+ // If object is a blank node, recursively collect its triples
3037
+ if (quad.object.termType === 'BlankNode') {
3038
+ collectNode(quad.object);
3039
+ }
3040
+ }
3041
+ };
3042
+ collectNode(DataFactory.namedNode(shapeIri));
3043
+ // Get serialized triples
3044
+ writer.end((error, result) => {
3045
+ if (!error && result) {
3046
+ const lines = result.trim().split('\n').filter((line) => line.trim().length > 0);
3047
+ axioms.push(...lines);
3048
+ }
3049
+ });
3050
+ return axioms;
3051
+ };
3052
+ // Group by ontology and collect axioms
3053
+ const grouped = new Map();
3054
+ for (const entry of shapeEntries) {
3055
+ const axioms = collectShapeAxioms(entry.shape, entry.graphUri);
3056
+ const shapes = grouped.get(entry.ontology) ?? [];
3057
+ shapes.push({ shape: entry.shape, axioms });
3058
+ grouped.set(entry.ontology, shapes);
3059
+ }
3060
+ const results = Array.from(grouped.entries())
3061
+ .map(([ontology, shapes]) => ({
3062
+ ontology,
3063
+ shapes: shapes.sort((a, b) => a.shape.localeCompare(b.shape)),
3064
+ }))
3065
+ .sort((a, b) => a.ontology.localeCompare(b.ontology));
3066
+ return {
3067
+ success: true,
3068
+ results,
3069
+ };
2823
3070
  }
3071
+ catch (error) {
3072
+ return {
3073
+ success: false,
3074
+ error: error instanceof Error ? error.message : String(error),
3075
+ };
3076
+ }
3077
+ }
3078
+ async ensureWorkspaceSettled(includeShacl) {
2824
3079
  if (!this.initialWorkspaceSyncCompleted) {
2825
3080
  await this.bootstrapWorkspaceOmlDocuments();
2826
3081
  this.initialWorkspaceSyncCompleted = true;
2827
- debugRest('workspace.current.initial.bootstrap.complete');
2828
3082
  }
2829
3083
  await this.waitForWatcherRefreshIdle();
2830
- debugRest('workspace.current.end');
3084
+ if (this.useExternalRuntime) {
3085
+ await this.waitForValidatedWithTrace();
3086
+ }
3087
+ if (includeShacl) {
3088
+ await this.reindexWorkspaceShacl();
3089
+ }
2831
3090
  }
2832
3091
  async waitForWatcherRefreshIdle() {
2833
3092
  while (this.watcherFlushTimer !== undefined || this.refreshInFlight || this.refreshQueued) {
@@ -2855,18 +3114,10 @@ class InMemoryJsonRpcLspClient {
2855
3114
  const deletedUris = [...currentOmlUris]
2856
3115
  .filter((uri) => !changedUriStrings.has(uri))
2857
3116
  .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
3117
  await Promise.all(changedUris.map((uri) => documents.getOrCreateDocument(uri)));
2865
3118
  const builder = this.runtime.shared.workspace.DocumentBuilder;
2866
3119
  await builder.update(changedUris, deletedUris);
2867
- await this.waitForValidatedWithTrace('refresh', changedUriStrings);
2868
- const postDocs = this.getWorkspaceOmlDocuments();
2869
- debugRest('workspace.refresh.done', { docsAfter: postDocs.length });
3120
+ await this.waitForValidatedWithTrace();
2870
3121
  }
2871
3122
  async bootstrapWorkspaceOmlDocuments() {
2872
3123
  const workspaceRoot = path.resolve(this.workspaceRoot);
@@ -2874,38 +3125,32 @@ class InMemoryJsonRpcLspClient {
2874
3125
  const documents = this.runtime.shared.workspace.LangiumDocuments;
2875
3126
  const builder = this.runtime.shared.workspace.DocumentBuilder;
2876
3127
  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
3128
  if (docs.length > 0) {
2882
3129
  await builder.build(docs, { validation: true });
2883
- await this.waitForValidatedWithTrace('bootstrap', new Set(docs.map((doc) => String(doc?.uri ?? '').trim())));
3130
+ await this.waitForValidatedWithTrace();
2884
3131
  }
2885
- const postDocs = this.getWorkspaceOmlDocuments();
2886
- debugRest('workspace.bootstrap.done', { docsAfter: postDocs.length });
3132
+ }
3133
+ async reindexWorkspaceShacl() {
3134
+ await reindexWorkspaceShacl({
3135
+ workspaceRoot: this.workspaceRoot,
3136
+ runtime: this.runtime,
3137
+ });
2887
3138
  }
2888
3139
  async ensureInitialized() {
2889
3140
  if (this.initPromise) {
2890
- debugRest('initialize.await.existing');
2891
3141
  await this.initPromise;
2892
3142
  return;
2893
3143
  }
2894
3144
  if (this.useExternalRuntime) {
2895
3145
  this.initPromise = (async () => {
2896
- const begin = Date.now();
2897
- debugRest('initialize.external.begin');
2898
3146
  const workspace = this.runtime.shared.workspace.WorkspaceManager;
2899
3147
  await workspace.ready;
2900
- debugRest('initialize.external.ready', { elapsedMs: Math.max(0, Date.now() - begin) });
2901
3148
  })();
2902
3149
  }
2903
3150
  else {
2904
3151
  const rootPath = path.resolve(this.workspaceRoot);
2905
3152
  const rootUri = pathToFileURL(rootPath).toString();
2906
3153
  this.initPromise = (async () => {
2907
- const begin = Date.now();
2908
- debugRest('initialize.internal.begin', { rootPath, rootUri });
2909
3154
  await withTimeout(this.clientConnection.sendRequest('initialize', {
2910
3155
  processId: process.pid,
2911
3156
  rootUri,
@@ -2919,14 +3164,13 @@ class InMemoryJsonRpcLspClient {
2919
3164
  // before lint/query calls trigger document refresh and diagnostics.
2920
3165
  const workspace = this.runtime.shared.workspace.WorkspaceManager;
2921
3166
  await workspace.ready;
2922
- debugRest('initialize.internal.ready', { elapsedMs: Math.max(0, Date.now() - begin) });
2923
3167
  })();
2924
3168
  }
2925
3169
  await this.initPromise;
2926
3170
  }
2927
3171
  }
2928
3172
  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.watchWorkspace === true, options.runtime);
3173
+ const client = new InMemoryJsonRpcLspClient(options.workspaceRoot ? path.resolve(options.workspaceRoot) : process.cwd(), options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS, options.runtime);
2930
3174
  try {
2931
3175
  const params = options.params ?? {};
2932
3176
  switch (options.operation) {
@@ -2950,16 +3194,7 @@ export async function runOmlWorkspaceLocalOperation(options) {
2950
3194
  }
2951
3195
  export async function startOmlRestServer(options) {
2952
3196
  const workspaceRoot = options.workspaceRoot ? path.resolve(options.workspaceRoot) : process.cwd();
2953
- debugRest('rest.start.options', {
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);
3197
+ const client = new InMemoryJsonRpcLspClient(workspaceRoot, options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS, options.runtime);
2963
3198
  let openApiSpec = createOpenApiSpec(options.host, options.port);
2964
3199
  let currentAccessToken = options.authToken;
2965
3200
  const featureGate = options.featureGate;
@@ -2972,17 +3207,6 @@ export async function startOmlRestServer(options) {
2972
3207
  const requestId = randomUUID();
2973
3208
  const method = req.method ?? 'GET';
2974
3209
  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
3210
  try {
2987
3211
  const parsed = new URL(rawUrl, `http://${req.headers.host ?? 'localhost'}`);
2988
3212
  const pathname = parsed.pathname;
@@ -3029,20 +3253,22 @@ export async function startOmlRestServer(options) {
3029
3253
  return;
3030
3254
  }
3031
3255
  if (method === 'GET' && pathname === '/v0/workspace') {
3256
+ const ontologies = await client.ontologiesWorkspace();
3257
+ const allImported = new Set(ontologies.flatMap((o) => o.imports));
3258
+ const roots = ontologies
3259
+ .filter((o) => !allImported.has(o.ontology))
3260
+ .map((o) => o.ontology);
3032
3261
  jsonResponse(res, 200, {
3033
- workspaceRoot,
3034
3262
  workspaceUri: pathToFileURL(workspaceRoot).toString(),
3263
+ roots,
3035
3264
  requestId,
3036
3265
  });
3037
3266
  return;
3038
3267
  }
3039
- if (method === 'GET' && pathname === '/v0/models') {
3040
- const requiredFeature = requiredFeatureForRestOperation('models');
3041
- const workspaceModelFiles = (featureGate && requiredFeature)
3042
- ? await featureGate.runWithFeature(requiredFeature, () => listWorkspaceModelFiles(workspaceRoot), { transport: 'rest', operationId: 'models' })
3043
- : await listWorkspaceModelFiles(workspaceRoot);
3268
+ if (method === 'GET' && pathname === '/v0/ontologies') {
3269
+ const ontologies = await client.ontologiesWorkspace();
3044
3270
  jsonResponse(res, 200, {
3045
- files: workspaceModelFiles,
3271
+ ontologies,
3046
3272
  requestId,
3047
3273
  });
3048
3274
  return;
@@ -3053,13 +3279,7 @@ export async function startOmlRestServer(options) {
3053
3279
  if (!routeModelPath) {
3054
3280
  throw new RestHttpError(404, `No route for ${method} ${pathname}.`);
3055
3281
  }
3056
- const modelUri = resolveModelUriFromRouteModelPath(workspaceRoot, routeModelPath);
3057
- try {
3058
- await assertModelUriExists(modelUri);
3059
- }
3060
- catch {
3061
- throw new RestHttpError(404, `No OML model found for '${routeModelPath}'.`);
3062
- }
3282
+ const modelUri = client.resolveModelUriForSparqlModelId(routeModelPath);
3063
3283
  const bodyText = method === 'POST' ? await readTextBody(req) : '';
3064
3284
  const sparql = parseSparqlQueryFromRequest(req, parsed, bodyText);
3065
3285
  if (!sparql) {
@@ -3087,7 +3307,7 @@ export async function startOmlRestServer(options) {
3087
3307
  if (!graphFormat) {
3088
3308
  throw new RestHttpError(406, 'Not Acceptable for graph results.');
3089
3309
  }
3090
- const serialized = await serializeQuads(queryResult.result.quads, graphFormat.format, false);
3310
+ const serialized = await serializeQuads(queryResult.result.quads, graphFormat.format, false, filterPrefixesForQuads(queryResult.result.quads, client.getWorkspacePrefixes()));
3091
3311
  textResponse(res, 200, graphFormat.contentType, serialized);
3092
3312
  };
3093
3313
  const requiredFeature = requiredFeatureForRestOperation('query');
@@ -3120,7 +3340,6 @@ export async function startOmlRestServer(options) {
3120
3340
  }
3121
3341
  catch (error) {
3122
3342
  const message = error instanceof Error ? error.message : String(error);
3123
- debugRest('request.error', { requestId, method, rawUrl, message });
3124
3343
  if (error instanceof OmlAccessError) {
3125
3344
  jsonResponse(res, error.statusCode, accessErrorPayload(error, requestId));
3126
3345
  return;
@@ -3143,18 +3362,15 @@ export async function startOmlRestServer(options) {
3143
3362
  server.listen(options.port, options.host, () => resolve());
3144
3363
  });
3145
3364
  const listeningPort = resolveListeningPort(server);
3146
- debugRest('rest.start.listening', { host: options.host, requestedPort: options.port, listeningPort });
3147
3365
  openApiSpec = createOpenApiSpec(options.host, listeningPort);
3148
3366
  return {
3149
3367
  server,
3150
3368
  updateToken: async (token) => {
3151
- debugRest('token.update.begin', { tokenProvided: Boolean(token) });
3152
3369
  currentAccessToken = token;
3153
3370
  featureGate?.setAccessToken(token);
3154
3371
  if (featureGate) {
3155
3372
  await featureGate.primeEntitlements();
3156
3373
  }
3157
- debugRest('token.update.end');
3158
3374
  },
3159
3375
  };
3160
3376
  }